Fixes attacks on mech equipment ignoring armor / melee damage, also fixes mech equipment not being disabled at 0% health, also also unit tests mech armor (#67411)

This commit is contained in:
MrMelbert
2022-06-11 04:01:23 -05:00
committed by GitHub
parent 4eb6a1f196
commit a6b4a4354f
5 changed files with 138 additions and 20 deletions
+1
View File
@@ -104,6 +104,7 @@
#include "load_map_security.dm"
#include "machine_disassembly.dm"
#include "mapping.dm"
#include "mecha_damage.dm"
#include "medical_wounds.dm"
#include "merge_type.dm"
#include "metabolizing.dm"
+86
View File
@@ -0,0 +1,86 @@
/**
* Unit test to ensure that mechs take the correct amount of damage
* based on armor, and that their equipment is properly damaged as well.
*/
/datum/unit_test/mecha_damage
/datum/unit_test/mecha_damage/Run()
// "Loaded Mauler" was chosen deliberately here.
// We need a mech that starts with arm equipment and has fair enough armor.
var/obj/vehicle/sealed/mecha/demo_mech = allocate(/obj/vehicle/sealed/mecha/combat/marauder/mauler/loaded)
// We need to face our guy explicitly, because mechs have directional armor
demo_mech.setDir(EAST)
var/expected_melee_armor = demo_mech.armor.getRating(MELEE)
var/expected_laser_armor = demo_mech.armor.getRating(LASER)
var/expected_bullet_armor = demo_mech.armor.getRating(BULLET)
var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human)
dummy.forceMove(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
// The dummy needs to be targeting an arm. Left is chosen here arbitrarily.
dummy.zone_selected = BODY_ZONE_L_ARM
// Not strictly necessary, but you never know
dummy.face_atom(demo_mech)
// Get a sample "melee" weapon.
// The energy axe is chosen here due to having a high base force, to make sure we get over the equipment DT.
var/obj/item/dummy_melee = allocate(/obj/item/melee/energy/axe)
var/expected_melee_damage = round(dummy_melee.force * (1 - expected_melee_armor / 100), DAMAGE_PRECISION)
// Get a sample laser weapon.
// The captain's laser gun here is chosen primarily because it deals more damage than normal lasers.
var/obj/item/gun/energy/laser/dummy_laser = allocate(/obj/item/gun/energy/laser/captain)
var/obj/item/ammo_casing/laser_ammo = dummy_laser.ammo_type[1]
var/obj/projectile/beam/laser_fired = initial(laser_ammo.projectile_type)
var/expected_laser_damage = round(dummy_laser.projectile_damage_multiplier * initial(laser_fired.damage) * (1 - expected_laser_armor / 100), DAMAGE_PRECISION)
// Get a sample ballistic weapon.
// The syndicate .357 here is chosen because it does a lot of damage.
var/obj/item/gun/ballistic/dummy_gun = allocate(/obj/item/gun/ballistic/revolver)
var/obj/item/ammo_casing/ballistic_ammo = dummy_gun.magazine.ammo_type
var/obj/projectile/bullet_fired = initial(ballistic_ammo.projectile_type)
var/expected_bullet_damage = round(dummy_gun.projectile_damage_multiplier * initial(bullet_fired.damage) * (1 - expected_bullet_armor / 100), DAMAGE_PRECISION)
var/obj/item/mecha_parts/mecha_equipment/left_arm_equipment = demo_mech.equip_by_category[MECHA_L_ARM]
TEST_ASSERT_NOTNULL(left_arm_equipment, "[demo_mech] spawned without any equipment in their left arm slot.")
// Now it's time to actually beat the heck out of the mech to see if it takes damage correctly.
TEST_ASSERT_EQUAL(demo_mech.get_integrity(), demo_mech.max_integrity, "[demo_mech] was spawned at not its maximum integrity.")
TEST_ASSERT_EQUAL(left_arm_equipment.get_integrity(), left_arm_equipment.max_integrity, "[left_arm_equipment] ([demo_mech]'s left arm) spawned at not its maximum integrity.")
// SMACK IT
var/pre_melee_integrity = demo_mech.get_integrity()
var/pre_melee_arm_integrity = left_arm_equipment.get_integrity()
demo_mech.attacked_by(dummy_melee, dummy)
check_integrity(demo_mech, pre_melee_integrity, expected_melee_damage, "hit with a melee item")
check_integrity(left_arm_equipment, pre_melee_arm_integrity, expected_melee_damage, "hit with a melee item")
// BLAST IT
var/pre_laser_integrity = demo_mech.get_integrity()
var/pre_laser_arm_integrity = left_arm_equipment.get_integrity()
dummy_laser.fire_gun(demo_mech, dummy, FALSE)
check_integrity(demo_mech, pre_laser_integrity, expected_laser_damage, "shot with a laser")
check_integrity(left_arm_equipment, pre_laser_arm_integrity, expected_laser_damage, "shot with a laser")
// SHOOT IT
var/pre_bullet_integrity = demo_mech.get_integrity()
var/pre_bullet_arm_integrity = left_arm_equipment.get_integrity()
dummy_gun.fire_gun(demo_mech, dummy, FALSE)
check_integrity(demo_mech, pre_bullet_integrity, expected_bullet_damage, "shot with a bullet")
check_integrity(left_arm_equipment, pre_bullet_arm_integrity, expected_bullet_damage, "shot with a bullet")
// Additional check: The right arm of the mech should have taken no damage by this point.
var/obj/item/mecha_parts/mecha_equipment/right_arm_equipment = demo_mech.equip_by_category[MECHA_R_ARM]
TEST_ASSERT_NOTNULL(right_arm_equipment, "[demo_mech] spawned without any equipment in their right arm slot.")
TEST_ASSERT_EQUAL(right_arm_equipment.get_integrity(), right_arm_equipment.max_integrity, "[demo_mech] somehow took damage to its right arm, despite not being targeted.")
/// Simple helper to check if the integrity of an atom involved has taken damage, and if they took the amount of damage it should have.
/datum/unit_test/mecha_damage/proc/check_integrity(atom/checking, pre_integrity, expected_damage, hit_by_phrase)
var/post_hit_health = checking.get_integrity()
TEST_ASSERT(post_hit_health < pre_integrity, "[checking] was [hit_by_phrase], but didn't take any damage.")
var/damage_taken = round(pre_integrity - post_hit_health, DAMAGE_PRECISION)
TEST_ASSERT_EQUAL(damage_taken, expected_damage, "[checking] didn't take the expected amount of damage when [hit_by_phrase]. (Expected damage: [expected_damage], recieved damage: [damage_taken])")
@@ -89,6 +89,9 @@
if(chassis.equipment_disabled)
to_chat(chassis.occupants, span_warning("Error -- Equipment control unit is unresponsive."))
return FALSE
if(get_integrity() <= 1)
to_chat(chassis.occupants, span_warning("Error -- Equipment critically damaged."))
return FALSE
if(TIMER_COOLDOWN_CHECK(chassis, COOLDOWN_MECHA_EQUIPMENT(type)))
return FALSE
return TRUE
+47 -19
View File
@@ -25,24 +25,31 @@
gear = equip_by_category[MECHA_R_ARM]
if(!gear)
return
var/brokenstatus = gear.get_integrity()
var/component_health = gear.get_integrity()
// always leave at least 1 health
brokenstatus--
var/damage_to_deal = min(brokenstatus, damage)
if(!damage_to_deal)
var/damage_to_deal = min(component_health - 1, damage)
if(damage_to_deal <= 0)
return
gear.take_damage(damage_to_deal)
if(gear.get_integrity() <= 1)
to_chat(occupants, "[icon2html(src, occupants)][span_danger("[gear] is critically damaged!")]")
playsound(src, gear.destroy_sound, 50)
/obj/vehicle/sealed/mecha/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
. = ..()
if(. && atom_integrity > 0)
spark_system.start()
try_deal_internal_damage(.)
if(. >= 5 || prob(33))
to_chat(occupants, "[icon2html(src, occupants)][span_userdanger("Taking damage!")]")
log_message("Took [.] points of damage. Damage type: [damage_type]", LOG_MECHA)
/obj/vehicle/sealed/mecha/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
var/damage_taken = ..()
if(damage_taken <= 0 || atom_integrity < 0)
return damage_taken
/obj/vehicle/sealed/mecha/run_atom_armor(damage_amount, damage_type, damage_flag = 0, attack_dir, armour_penentration)
spark_system.start()
try_deal_internal_damage(damage_taken)
if(damage_taken >= 5 || prob(33))
to_chat(occupants, "[icon2html(src, occupants)][span_userdanger("Taking damage!")]")
log_message("Took [damage_taken] points of damage. Damage type: [damage_type]", LOG_MECHA)
return damage_taken
/obj/vehicle/sealed/mecha/run_atom_armor(damage_amount, damage_type, damage_flag = 0, attack_dir, armour_penetration)
. = ..()
if(attack_dir)
var/facing_modifier = get_armour_facing(abs(dir2angle(dir) - dir2angle(attack_dir)))
@@ -113,7 +120,13 @@
return BULLET_ACT_HIT
log_message("Hit by projectile. Type: [hitting_projectile]([hitting_projectile.damage_type]).", LOG_MECHA, color="red")
// yes we *have* to run the armor calc proc here I love tg projectile code too
try_damage_component(run_atom_armor(hitting_projectile.damage, hitting_projectile.damage_type, hitting_projectile.damage_type, 0, REVERSE_DIR(hitting_projectile.dir), hitting_projectile.armour_penetration), hitting_projectile.def_zone)
try_damage_component(run_atom_armor(
damage_amount = hitting_projectile.damage,
damage_type = hitting_projectile.damage_type,
damage_flag = hitting_projectile.armor_flag,
attack_dir = REVERSE_DIR(hitting_projectile.dir),
armour_penetration = hitting_projectile.armour_penetration,
), hitting_projectile.def_zone)
return ..()
/obj/vehicle/sealed/mecha/ex_act(severity, target)
@@ -258,11 +271,26 @@
var/obj/item/mecha_parts/P = W
P.try_attach_part(user, src, FALSE)
return
. = ..()
log_message("Attacked by [W]. Attacker - [user], Damage - [.]", LOG_MECHA)
if(isliving(user))
var/mob/living/living_user = user
try_damage_component(., living_user.zone_selected)
return ..()
/obj/vehicle/sealed/mecha/attacked_by(obj/item/attacking_item, mob/living/user)
if(!attacking_item.force)
return
var/damage_taken = take_damage(attacking_item.force * attacking_item.demolition_mod, attacking_item.damtype, MELEE, 1)
try_damage_component(damage_taken, user.zone_selected)
var/hit_verb = length(attacking_item.attack_verb_simple) ? "[pick(attacking_item.attack_verb_simple)]" : "hit"
user.visible_message(
span_danger("[user] [hit_verb][plural_s(hit_verb)] [src] with [attacking_item][damage_taken ? "." : ", without leaving a mark!"]"),
span_danger("You [hit_verb] [src] with [attacking_item][damage_taken ? "." : ", without leaving a mark!"]"),
span_hear("You hear a [hit_verb]."),
COMBAT_MESSAGE_RANGE,
)
log_combat(user, src, "attacked", attacking_item)
log_message("Attacked by [user]. Item - [attacking_item], Damage - [damage_taken]", LOG_MECHA)
/obj/vehicle/sealed/mecha/wrench_act(mob/living/user, obj/item/I)
..()
@@ -4,7 +4,7 @@
if(HAS_TRAIT(M, TRAIT_PRIMITIVE)) //no lavalizards either.
to_chat(M, span_warning("The knowledge to use this device eludes you!"))
return
log_message("[M] tries to move into [src].", LOG_MECHA)
log_message("[M] tried to move into [src].", LOG_MECHA)
if(dna_lock && M.has_dna())
var/mob/living/carbon/entering_carbon = M
if(entering_carbon.dna.unique_enzymes != dna_lock)