diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 3c21e1641e7..99f0f5fa179 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -90,6 +90,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_PULL_BLOCKED "pullblocked" /// Abstract condition that prevents movement if being pulled and might be resisted against. Handcuffs and straight jackets, basically. #define TRAIT_RESTRAINED "restrained" +/// Doesn't miss attacks +#define TRAIT_PERFECT_ATTACKER "perfect_attacker" #define TRAIT_INCAPACITATED "incapacitated" #define TRAIT_CRITICAL_CONDITION "critical-condition" //In some kind of critical condition. Is able to succumb. #define TRAIT_BLIND "blind" diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index a7bfe4598c4..44c0b21788f 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -1323,7 +1323,7 @@ GLOBAL_LIST_EMPTY(roundstart_races) var/miss_chance = 100//calculate the odds that a punch misses entirely. considers stamina and brute damage of the puncher. punches miss by default to prevent weird cases if(user.dna.species.punchdamagelow) - if(atk_verb == ATTACK_EFFECT_KICK) //kicks never miss (provided your species deals more than 0 damage) + if(atk_verb == ATTACK_EFFECT_KICK || HAS_TRAIT(src, TRAIT_PERFECT_ATTACKER)) //kicks never miss (provided your species deals more than 0 damage) miss_chance = 0 else miss_chance = min((user.dna.species.punchdamagehigh/user.dna.species.punchdamagelow) + user.getStaminaLoss() + (user.getBruteLoss()*0.5), 100) //old base chance for a miss + various damage. capped at 100 to prevent weirdness in prob() diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 88226a647ed..a204fe67ccd 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -25,6 +25,7 @@ #include "binary_insert.dm" #include "card_mismatch.dm" #include "chain_pull_through_space.dm" +#include "combat.dm" #include "component_tests.dm" #include "confusion.dm" #include "emoting.dm" diff --git a/code/modules/unit_tests/combat.dm b/code/modules/unit_tests/combat.dm new file mode 100644 index 00000000000..30bad721751 --- /dev/null +++ b/code/modules/unit_tests/combat.dm @@ -0,0 +1,98 @@ +/datum/unit_test/harm_punch/Run() + var/mob/living/carbon/human/puncher = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human) + + // Avoid all randomness in tests + ADD_TRAIT(puncher, TRAIT_PERFECT_ATTACKER, INNATE_TRAIT) + + puncher.a_intent_change(INTENT_HARM) + victim.attack_hand(puncher) + + TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being punched") + +/datum/unit_test/harm_melee/Run() + var/mob/living/carbon/human/tider = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human) + var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox) + + tider.put_in_active_hand(toolbox, forced = TRUE) + tider.a_intent_change(INTENT_HARM) + victim.attackby(toolbox, tider) + + TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being hit by a toolbox") + +/datum/unit_test/harm_different_damage/Run() + var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human) + var/obj/item/weldingtool/welding_tool = allocate(/obj/item/weldingtool) + + attacker.put_in_active_hand(welding_tool, forced = TRUE) + attacker.a_intent_change(INTENT_HARM) + welding_tool.attack_self(attacker) // Turn it on + victim.attackby(welding_tool, attacker) + + TEST_ASSERT_EQUAL(victim.getBruteLoss(), 0, "Victim took brute damage from a lit welding tool") + TEST_ASSERT(victim.getFireLoss() > 0, "Victim took no burn damage after being hit by a lit welding tool") + +/datum/unit_test/attack_chain + var/attack_hit + var/post_attack_hit + var/pre_attack_hit + +/datum/unit_test/attack_chain/proc/attack_hit() + attack_hit = TRUE + +/datum/unit_test/attack_chain/proc/post_attack_hit() + post_attack_hit = TRUE + +/datum/unit_test/attack_chain/proc/pre_attack_hit() + pre_attack_hit = TRUE + +/datum/unit_test/attack_chain/Run() + var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human) + var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox) + + RegisterSignal(toolbox, COMSIG_ITEM_PRE_ATTACK, .proc/pre_attack_hit) + RegisterSignal(toolbox, COMSIG_ITEM_ATTACK, .proc/attack_hit) + RegisterSignal(toolbox, COMSIG_ITEM_AFTERATTACK, .proc/post_attack_hit) + + attacker.put_in_active_hand(toolbox, forced = TRUE) + attacker.a_intent_change(INTENT_HARM) + toolbox.melee_attack_chain(attacker, victim) + + TEST_ASSERT(pre_attack_hit, "Pre-attack signal was not fired") + TEST_ASSERT(attack_hit, "Attack signal was not fired") + TEST_ASSERT(post_attack_hit, "Post-attack signal was not fired") + +/datum/unit_test/disarm/Run() + var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human) + var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human) + var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox) + + victim.put_in_active_hand(toolbox, forced = TRUE) + attacker.a_intent_change(INTENT_DISARM) + + var/obj/structure/barricade/dense_object = allocate(/obj/structure/barricade) + + // Attacker --> Victim --> Empty space --> Wall + attacker.forceMove(run_loc_bottom_left) + victim.forceMove(locate(run_loc_bottom_left.x + 1, run_loc_bottom_left.y, run_loc_bottom_left.z)) + dense_object.forceMove(locate(run_loc_bottom_left.x + 3, run_loc_bottom_left.y, run_loc_bottom_left.z)) + + // First disarm, world should now look like: + // Attacker --> Empty space --> Victim --> Wall + victim.attack_hand(attacker) + + TEST_ASSERT_EQUAL(victim.loc.x, run_loc_bottom_left.x + 2, "Victim wasn't moved back after being pushed") + TEST_ASSERT(!victim.has_status_effect(STATUS_EFFECT_KNOCKDOWN), "Victim was knocked down despite not being against a wall") + TEST_ASSERT_EQUAL(victim.get_active_held_item(), toolbox, "Victim dropped toolbox despite not being against a wall") + + attacker.forceMove(get_step(attacker, EAST)) + + // Second disarm, victim was against wall and should be down + victim.attack_hand(attacker) + + TEST_ASSERT_EQUAL(victim.loc.x, run_loc_bottom_left.x + 2, "Victim was moved after being pushed against a wall") + TEST_ASSERT(victim.has_status_effect(STATUS_EFFECT_KNOCKDOWN), "Victim was not knocked down after being pushed against a wall") + TEST_ASSERT_EQUAL(victim.get_active_held_item(), null, "Victim didn't drop toolbox after being pushed against a wall")