From bc1db6310e33db6ee51ad33f07fb14329827e129 Mon Sep 17 00:00:00 2001 From: Neerti Date: Fri, 7 Aug 2020 00:24:10 -0400 Subject: [PATCH] Adds Modifier Armor (#7416) --- code/modules/mob/_modifiers/modifiers.dm | 16 ++- code/modules/mob/_modifiers/modifiers_misc.dm | 31 ++++- .../mob/living/carbon/human/human_defense.dm | 24 +++- code/modules/mob/living/carbon/human/life.dm | 31 ++++- code/modules/mob/living/simple_mob/defense.dm | 67 ++++++++-- code/unit_tests/mob_tests.dm | 117 ++++++++++++++++++ 6 files changed, 267 insertions(+), 19 deletions(-) diff --git a/code/modules/mob/_modifiers/modifiers.dm b/code/modules/mob/_modifiers/modifiers.dm index 66afd732a2..56422efdf6 100644 --- a/code/modules/mob/_modifiers/modifiers.dm +++ b/code/modules/mob/_modifiers/modifiers.dm @@ -55,6 +55,14 @@ var/emp_modifier // Added to the EMP strength, which is an inverse scale from 1 to 4, with 1 being the strongest EMP. 5 is a nullification. var/explosion_modifier // Added to the bomb strength, which is an inverse scale from 1 to 3, with 1 being gibstrength. 4 is a nullification. + // Note that these are combined with the mob's real armor values additatively. You can also omit specific armor types. + var/list/armor_percent = null // List of armor values to add to the holder when doing armor calculations. This is for percentage based armor. E.g. 50 = half damage. + var/list/armor_flat = null // Same as above but only for flat armor calculations. E.g. 5 = 5 less damage (this comes after percentage). + // Unlike armor, this is multiplicative. Two 50% protection modifiers will be combined into 75% protection (assuming no base protection on the mob). + var/heat_protection = null // Modifies how 'heat' protection is calculated, like wearing a firesuit. 1 = full protection. + var/cold_protection = null // Ditto, but for cold, like wearing a winter coat. + var/siemens_coefficient = null // Similar to above two vars but 0 = full protection, to be consistant with siemens numbers everywhere else. + var/vision_flags // Vision flags to add to the mob. SEE_MOB, SEE_OBJ, etc. /datum/modifier/New(var/new_holder, var/new_origin) @@ -186,10 +194,14 @@ // Checks if the mob has a modifier type. /mob/living/proc/has_modifier_of_type(var/modifier_type) + return get_modifier_of_type(modifier_type) ? TRUE : FALSE + +// Gets the first instance of a specific modifier type or subtype. +/mob/living/proc/get_modifier_of_type(var/modifier_type) for(var/datum/modifier/M in modifiers) if(istype(M, modifier_type)) - return TRUE - return FALSE + return M + return null // This displays the actual 'numbers' that a modifier is doing. Should only be shown in OOC contexts. // When adding new effects, be sure to update this as well. diff --git a/code/modules/mob/_modifiers/modifiers_misc.dm b/code/modules/mob/_modifiers/modifiers_misc.dm index 2efa5c0ef9..3b9663d855 100644 --- a/code/modules/mob/_modifiers/modifiers_misc.dm +++ b/code/modules/mob/_modifiers/modifiers_misc.dm @@ -397,4 +397,33 @@ the artifact triggers the rage. /datum/modifier/outline_test/tick() animate(filter_instance, size = 3, time = 0.25 SECONDS) - animate(size = 1, 0.25 SECONDS) \ No newline at end of file + animate(size = 1, 0.25 SECONDS) + + +// Acts as a psuedo-godmode, yet probably is more reliable than the actual var for it nowdays. +// Can't protect from instantly killing things like singulos. +/datum/modifier/invulnerable + name = "invulnerable" + desc = "You are almost immune to harm, for a little while at least." + stacks = MODIFIER_STACK_EXTEND + + disable_duration_percent = 0 + incoming_damage_percent = 0 +// bleeding_rate_percent = 0 + pain_immunity = TRUE + armor_percent = list("melee" = 2000, "bullet" = 2000, "laser" = 2000, "bomb" = 2000, "energy" = 2000, "bio" = 2000, "rad" = 2000) + heat_protection = 1.0 + cold_protection = 1.0 + siemens_coefficient = 0.0 + +// Reduces resistance to "elements". +// Note that most things that do give resistance gives 100% protection, +// and due to multiplicitive stacking, this modifier won't do anything to change that. +/datum/modifier/elemental_vulnerability + name = "elemental vulnerability" + desc = "You're more vulnerable to extreme temperatures and electricity." + stacks = MODIFIER_STACK_EXTEND + + heat_protection = -0.5 + cold_protection = -0.5 + siemens_coefficient = 1.5 \ No newline at end of file diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 8e351372c9..0199d387ae 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -133,6 +133,12 @@ emp_act if(istype(C) && (C.body_parts_covered & def_zone.body_part)) // Is that body part being targeted covered? siemens_coefficient *= C.siemens_coefficient + // Modifiers. + for(var/thing in modifiers) + var/datum/modifier/M = thing + if(!isnull(M.siemens_coefficient)) + siemens_coefficient *= M.siemens_coefficient + return siemens_coefficient // Similar to above but is for the mob's overall protection, being the average of all slots. @@ -150,11 +156,11 @@ emp_act if(fire_stacks < 0) // Water makes you more conductive. siemens_value *= 1.5 - return (siemens_value/max(total, 1)) + return (siemens_value / max(total, 1)) // Returns a number between 0 to 1, with 1 being total protection. /mob/living/carbon/human/get_shock_protection() - return between(0, 1-get_siemens_coefficient_average(), 1) + return min(1 - get_siemens_coefficient_average(), 1) // Don't go above 1, but negatives are fine. // Returns a list of clothing that is currently covering def_zone. /mob/living/carbon/human/proc/get_clothing_list_organ(var/obj/item/organ/external/def_zone, var/type) @@ -173,6 +179,13 @@ emp_act var/list/protective_gear = def_zone.get_covering_clothing() for(var/obj/item/clothing/gear in protective_gear) protection += gear.armor[type] + + for(var/thing in modifiers) + var/datum/modifier/M = thing + var/modifier_armor = LAZYACCESS(M.armor_percent, type) + if(modifier_armor) + protection += modifier_armor + return protection /mob/living/carbon/human/proc/getsoak_organ(var/obj/item/organ/external/def_zone, var/type) @@ -182,6 +195,13 @@ emp_act var/list/protective_gear = def_zone.get_covering_clothing() for(var/obj/item/clothing/gear in protective_gear) soaked += gear.armorsoak[type] + + for(var/thing in modifiers) + var/datum/modifier/M = thing + var/modifier_armor = LAZYACCESS(M.armor_flat, type) + if(modifier_armor) + soaked += modifier_armor + return soaked // Checked in borer code diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index b29afe4da5..479874b185 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -717,7 +717,7 @@ cold_dam = COLD_DAMAGE_LEVEL_1 take_overall_damage(burn=cold_dam, used_weapon = "Low Body Temperature") - + else clear_alert("temp") // Account for massive pressure differences. Done by Polymorph @@ -836,7 +836,19 @@ /mob/living/carbon/human/get_heat_protection(temperature) //Temperature is the temperature you're being exposed to. var/thermal_protection_flags = get_heat_protection_flags(temperature) - return get_thermal_protection(thermal_protection_flags) + + . = get_thermal_protection(thermal_protection_flags) + . = 1 - . // Invert from 1 = immunity to 0 = immunity. + + // Doing it this way makes multiplicative stacking not get out of hand, so two modifiers that give 0.5 protection will be combined to 0.75 in the end. + for(var/thing in modifiers) + var/datum/modifier/M = thing + if(!isnull(M.heat_protection)) + . *= 1 - M.heat_protection + + // Code that calls this expects 1 = immunity so we need to invert again. + . = 1 - . + . = min(., 1.0) /mob/living/carbon/human/get_cold_protection(temperature) if(COLD_RESISTANCE in mutations) @@ -844,7 +856,20 @@ temperature = max(temperature, 2.7) //There is an occasional bug where the temperature is miscalculated in ares with a small amount of gas on them, so this is necessary to ensure that that bug does not affect this calculation. Space's temperature is 2.7K and most suits that are intended to protect against any cold, protect down to 2.0K. var/thermal_protection_flags = get_cold_protection_flags(temperature) - return get_thermal_protection(thermal_protection_flags) + + . = get_thermal_protection(thermal_protection_flags) + . = 1 - . // Invert from 1 = immunity to 0 = immunity. + + // Doing it this way makes multiplicative stacking not get out of hand, so two modifiers that give 0.5 protection will be combined to 0.75 in the end. + for(var/thing in modifiers) + var/datum/modifier/M = thing + if(!isnull(M.cold_protection)) + // Invert the modifier values so they align with the current working value. + . *= 1 - M.cold_protection + + // Code that calls this expects 1 = immunity so we need to invert again. + . = 1 - . + . = min(., 1.0) /mob/living/carbon/human/proc/get_thermal_protection(var/flags) .=0 diff --git a/code/modules/mob/living/simple_mob/defense.dm b/code/modules/mob/living/simple_mob/defense.dm index d94d83c0a9..1a301b8bfd 100644 --- a/code/modules/mob/living/simple_mob/defense.dm +++ b/code/modules/mob/living/simple_mob/defense.dm @@ -140,7 +140,18 @@ // Cold stuff. /mob/living/simple_mob/get_cold_protection() - return cold_resist + . = cold_resist + . = 1 - . // Invert from 1 = immunity to 0 = immunity. + + // Doing it this way makes multiplicative stacking not get out of hand, so two modifiers that give 0.5 protection will be combined to 0.75 in the end. + for(var/thing in modifiers) + var/datum/modifier/M = thing + if(!isnull(M.cold_protection)) + . *= 1 - M.cold_protection + + // Code that calls this expects 1 = immunity so we need to invert again. + . = 1 - . + . = min(., 1.0) // Fire stuff. Not really exciting at the moment. @@ -154,7 +165,18 @@ return /mob/living/simple_mob/get_heat_protection() - return heat_resist + . = heat_resist + . = 1 - . // Invert from 1 = immunity to 0 = immunity. + + // Doing it this way makes multiplicative stacking not get out of hand, so two modifiers that give 0.5 protection will be combined to 0.75 in the end. + for(var/thing in modifiers) + var/datum/modifier/M = thing + if(!isnull(M.heat_protection)) + . *= 1 - M.heat_protection + + // Code that calls this expects 1 = immunity so we need to invert again. + . = 1 - . + . = min(., 1.0) // Electricity /mob/living/simple_mob/electrocute_act(var/shock_damage, var/obj/source, var/siemens_coeff = 1.0, var/def_zone = null) @@ -170,7 +192,18 @@ s.start() /mob/living/simple_mob/get_shock_protection() - return shock_resist + . = shock_resist + . = 1 - . // Invert from 1 = immunity to 0 = immunity. + + // Doing it this way makes multiplicative stacking not get out of hand, so two modifiers that give 0.5 protection will be combined to 0.75 in the end. + for(var/thing in modifiers) + var/datum/modifier/M = thing + if(!isnull(M.siemens_coefficient)) + . *= M.siemens_coefficient + + // Code that calls this expects 1 = immunity so we need to invert again. + . = 1 - . + . = min(., 1.0) // Shot with taser/stunvolver /mob/living/simple_mob/stun_effect_act(var/stun_amount, var/agony_amount, var/def_zone, var/used_weapon=null) @@ -218,17 +251,29 @@ // Armor /mob/living/simple_mob/getarmor(def_zone, attack_flag) var/armorval = armor[attack_flag] - if(!armorval) - return 0 - else - return armorval + if(isnull(armorval)) + armorval = 0 + + for(var/thing in modifiers) + var/datum/modifier/M = thing + var/modifier_armor = LAZYACCESS(M.armor_percent, attack_flag) + if(modifier_armor) + armorval += modifier_armor + + return armorval /mob/living/simple_mob/getsoak(def_zone, attack_flag) var/armorval = armor_soak[attack_flag] - if(!armorval) - return 0 - else - return armorval + if(isnull(armorval)) + armorval = 0 + + for(var/thing in modifiers) + var/datum/modifier/M = thing + var/modifier_armor = LAZYACCESS(M.armor_flat, attack_flag) + if(modifier_armor) + armorval += modifier_armor + + return armorval // Lightning /mob/living/simple_mob/lightning_act() diff --git a/code/unit_tests/mob_tests.dm b/code/unit_tests/mob_tests.dm index c6b21e5d36..0c74449d2d 100644 --- a/code/unit_tests/mob_tests.dm +++ b/code/unit_tests/mob_tests.dm @@ -27,3 +27,120 @@ qdel(H) return 1 + + +/datum/modifier/unit_test + +/datum/unit_test/modifier + name = "modifier test template" + var/mob/living/subject = null + var/subject_type = /mob/living/carbon/human + var/list/inputs = list(1.00, 0.75, 0.50, 0.25, 0.00, -0.50, -1.0, -2.0) + var/list/expected_outputs = list(1.00, 0.75, 0.50, 0.25, 0.00, -0.50, -1.0, -2.0) + var/datum/modifier/test_modifier = null + var/issues = 0 + +/datum/unit_test/modifier/start_test() + // Arrange. + subject = new subject_type(get_standard_turf()) + subject.add_modifier(/datum/modifier/unit_test) + test_modifier = subject.get_modifier_of_type(/datum/modifier/unit_test) + + // Act, + for(var/i = 1 to inputs.len) + set_tested_variable(test_modifier, inputs[i]) + var/actual = round(get_test_value(subject), 0.01) // Rounding because floating point schannigans. + if(actual != expected_outputs[i]) + issues++ + log_bad("Input '[inputs[i]]' did not match expected output '[expected_outputs[i]]', but was instead '[actual]'.") + + // Assert. + if(issues) + fail("[issues] issues were found.") + else + pass("No issues found.") + qdel(subject) + return TRUE + +// Override for subtypes. +/datum/unit_test/modifier/proc/set_tested_variable(datum/modifier/M, new_value) + return + +/datum/unit_test/modifier/proc/get_test_value(mob/living/L) + return + + +/datum/unit_test/modifier/heat_protection + name = "MOB: human mob heat protection is calculated correctly" + +/datum/unit_test/modifier/heat_protection/set_tested_variable(datum/modifier/M, new_value) + M.heat_protection = new_value + +/datum/unit_test/modifier/heat_protection/get_test_value(mob/living/L) + return L.get_heat_protection(1000) + +/datum/unit_test/modifier/heat_protection/simple_mob + name = "MOB: simple mob heat protection is calculated correctly" + subject_type = /mob/living/simple_mob + + +/datum/unit_test/modifier/cold_protection + name = "MOB: human mob cold protection is calculated correctly" + +/datum/unit_test/modifier/cold_protection/set_tested_variable(datum/modifier/M, new_value) + M.cold_protection = new_value + +/datum/unit_test/modifier/cold_protection/get_test_value(mob/living/L) + return L.get_cold_protection(50) + +/datum/unit_test/modifier/cold_protection/simple_mob + name = "MOB: simple mob cold protection is calculated correctly" + subject_type = /mob/living/simple_mob + + +/datum/unit_test/modifier/shock_protection + name = "MOB: human mob shock protection is calculated correctly" + inputs = list(3.00, 2.00, 1.50, 1.00, 0.75, 0.50, 0.25, 0.00) + expected_outputs = list(-2.00, -1.00, -0.50, 0.00, 0.25, 0.50, 0.75, 1.00) + +/datum/unit_test/modifier/shock_protection/set_tested_variable(datum/modifier/M, new_value) + M.siemens_coefficient = new_value + +/datum/unit_test/modifier/shock_protection/get_test_value(mob/living/L) + return L.get_shock_protection() + +/datum/unit_test/modifier/shock_protection/simple_mob + name = "MOB: simple mob shock protection is calculated correctly" + subject_type = /mob/living/simple_mob + + +/datum/unit_test/modifier/percentage_armor + name = "MOB: human mob percentage armor is calculated correctly" + inputs = list(100, 75, 50, 25, 0) + expected_outputs = list(100, 75, 50, 25, 0) + +/datum/unit_test/modifier/percentage_armor/set_tested_variable(datum/modifier/M, new_value) + M.armor_percent = list("melee" = new_value) + +/datum/unit_test/modifier/percentage_armor/get_test_value(mob/living/L) + return L.getarmor(null, "melee") + +/datum/unit_test/modifier/percentage_armor/simple_mob + name = "MOB: simple mob percentage armor is calculated correctly" + subject_type = /mob/living/simple_mob + + +/datum/unit_test/modifier/percentage_flat + name = "MOB: human mob flat armor is calculated correctly" + inputs = list(100, 75, 50, 25, 0) + expected_outputs = list(100, 75, 50, 25, 0) + +/datum/unit_test/modifier/percentage_flat/set_tested_variable(datum/modifier/M, new_value) + M.armor_flat = list("melee" = new_value) + +/datum/unit_test/modifier/percentage_flat/get_test_value(mob/living/L) + return L.getsoak(null, "melee") + +/datum/unit_test/modifier/percentage_flat/simple_mob + name = "MOB: simple mob flat armor is calculated correctly" + subject_type = /mob/living/simple_mob