diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 699f215d0e0..60a6ee4fa61 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -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" diff --git a/code/modules/unit_tests/mecha_damage.dm b/code/modules/unit_tests/mecha_damage.dm new file mode 100644 index 00000000000..6bc5e37069f --- /dev/null +++ b/code/modules/unit_tests/mecha_damage.dm @@ -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])") diff --git a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm index 40f12a6ff17..8d3a52d071e 100644 --- a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm +++ b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm @@ -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 diff --git a/code/modules/vehicles/mecha/mecha_defense.dm b/code/modules/vehicles/mecha/mecha_defense.dm index c8a4b6322af..9b25bbb6c7b 100644 --- a/code/modules/vehicles/mecha/mecha_defense.dm +++ b/code/modules/vehicles/mecha/mecha_defense.dm @@ -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) ..() diff --git a/code/modules/vehicles/mecha/mecha_mob_interaction.dm b/code/modules/vehicles/mecha/mecha_mob_interaction.dm index aa3f6f3f56f..de2e6b68c75 100644 --- a/code/modules/vehicles/mecha/mecha_mob_interaction.dm +++ b/code/modules/vehicles/mecha/mecha_mob_interaction.dm @@ -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)