diff --git a/code/__DEFINES/combat/block.dm b/code/__DEFINES/combat/block.dm index 2877d983f3..b489662078 100644 --- a/code/__DEFINES/combat/block.dm +++ b/code/__DEFINES/combat/block.dm @@ -21,7 +21,9 @@ /// Meta-flag for run_block/do_run_block : By default, BLOCK_SUCCESS tells do_run_block() to assume the attack is completely blocked and not continue the block chain. If this is present, it will continue to check other items in the chain rather than stopping. #define BLOCK_CONTINUE_CHAIN (1<<8) /// Attack should change the amount of damage incurred. This means something calling run_block() has to handle it! -#define BLOCK_CHANGE_DAMAGE (1<<9) +#define BLOCK_SHOULD_CHANGE_DAMAGE (1<<9) +/// Attack should scale by this percent, 0 for no block and 100 for full blocked +#define BLOCK_SHOULD_PARTIAL_MITIGATE (1<<10) /// For keys in associative list/block_return as we don't want to saturate our (somewhat) limited flags. #define BLOCK_RETURN_REDIRECT_METHOD "REDIRECT_METHOD" @@ -47,6 +49,8 @@ #define BLOCK_RETURN_ACTIVE_BLOCK_DAMAGE_MITIGATED "damage_mitigated" /// For [BLOCK_CHANGE_DAMAGE]. Set damage to this. #define BLOCK_RETURN_SET_DAMAGE_TO "set_damage_to" +/// For [BLOCK_SHOULD_PARTIAL_MITIGATE]. Percentage mitigation. +#define BLOCK_RETURN_MITIGATION_PERCENT "partial_mitigation" /// Default if the above isn't set in the list. #define DEFAULT_REDIRECT_METHOD_PROJECTILE REDIRECT_METHOD_DEFLECT diff --git a/code/modules/mob/living/living_active_block.dm b/code/modules/mob/living/living_active_block.dm index 84e2f06329..f3034f00f2 100644 --- a/code/modules/mob/living/living_active_block.dm +++ b/code/modules/mob/living/living_active_block.dm @@ -134,9 +134,9 @@ /// The amount of damage that is blocked. /obj/item/proc/active_block_damage_mitigation(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) var/datum/block_parry_data/data = get_block_parry_data() - var/absorption = data.block_damage_absorption_override["[attack_type]"] - var/efficiency = data.block_damage_multiplier_override["[attack_type]"] - var/limit = data.block_damage_limit_override["[attack_type]"] + var/absorption = data.attack_type_list_scan(data.block_damage_absorption_override, attack_type) + var/efficiency = data.attack_type_list_scan(data.block_damage_multiplier_override, attack_type) + var/limit = data.attack_type_list_scan(data.block_damage_limit_override, attack_type) // must use isnulls to handle 0's. if(isnull(absorption)) absorption = data.block_damage_absorption @@ -159,12 +159,12 @@ /// Amount of stamina from damage blocked. Note that the damage argument is damage_blocked. /obj/item/proc/active_block_stamina_cost(mob/living/owner, atom/object, damage_blocked, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) var/datum/block_parry_data/data = get_block_parry_data() - var/efficiency = data.block_stamina_efficiency_override["[attack_type]"] + var/efficiency = data.attack_type_list_scan(data.block_stamina_efficiency_override, attack_type) if(isnull(efficiency)) efficiency = data.block_stamina_efficiency var/multiplier = 1 if(!CHECK_MOBILITY(owner, MOBILITY_STAND)) - multiplier = data.block_resting_stamina_penalty_multiplier_override["["[attack_type]"]"] + multiplier = data.attack_type_list_scan(data.block_resting_stamina_penalty_multiplier_override, attack_type) if(isnull(multiplier)) multiplier = data.block_resting_stamina_penalty_multiplier return (damage_blocked / efficiency) diff --git a/code/modules/mob/living/living_blocking_parrying.dm b/code/modules/mob/living/living_blocking_parrying.dm index 2261bfd922..5c3b8155c0 100644 --- a/code/modules/mob/living/living_blocking_parrying.dm +++ b/code/modules/mob/living/living_blocking_parrying.dm @@ -13,6 +13,8 @@ var/parry_prioritize_martial = TRUE /// Our block_parry_data for unarmed blocks/parries. Currently only used for parrying, as unarmed block isn't implemented yet. YOU MUST RUN [get_block_parry_data(this)] INSTEAD OF DIRECTLY ACCESSING! var/datum/block_parry_data/block_parry_data + /// Successful parries within the current parry cycle. It's a list of efficiency percentages. + var/list/successful_parries GLOBAL_LIST_EMPTY(block_parry_data) @@ -118,20 +120,42 @@ GLOBAL_LIST_EMPTY(block_parry_data) var/list/parry_imperfect_falloff_percent_override /// Efficiency in percent on perfect parry. var/parry_efficiency_perfect = 120 - /// Parry data. + /// Parry effect data. var/list/parry_data = list( PARRY_REFLEX_COUNTERATTACK = PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN ) + /// Enable default handling of audio/visual feedback + var/parry_default_handle_feedback = TRUE + /// Sounds for parrying + var/list/parry_sounds = list('sound/block_parry/block_metal1.ogg' = 1, 'sound/block_parry/block_metal1.ogg' = 1) + +/** + * Quirky proc to get average of flags in list that are in attack_type because why is attack_type a flag. + */ +/datum/block_parry_data/proc/attack_type_list_scan(list/L, attack_type) + var/total = 0 + var/div = 0 + for(var/flagtext in L) + if(attack_type & num2text(flagtext)) + total += L[flagtext] + div++ + // if none, return null. + if(!div) + return + return total/div //groan /mob/living/proc/handle_block_parry(seconds = 1) if(active_blocking) var/datum/block_parry_data/data = get_block_parry_data(active_block_item.block_parry_data) adjustStaminaLossBuffered(data.block_stamina_cost_per_second * seconds) -/obj/item/proc/active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) - if(!CHECK_BITFIELD(item_flags, ITEM_CAN_PARRY)) - return - /// Yadda yadda WIP access block/parry data... +//Stubs. + +/obj/item/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, parry_efficiency) + +/mob/living/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, parry_efficiency) + +/datum/martial_art/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, parry_efficiency) /obj/item/proc/active_parry_reflex_counter(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list) @@ -139,6 +163,9 @@ GLOBAL_LIST_EMPTY(block_parry_data) /datum/martial_art/proc/active_parry_reflex_counter(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list) +/** + * Gets the stage of our parry sequence we're currently in. + */ /mob/living/proc/get_parry_stage() if(!parrying) return NOT_PARRYING @@ -155,31 +182,63 @@ GLOBAL_LIST_EMPTY(block_parry_data) return PARRY_SPINDOWN return NOT_PARRYING +/** + * Gets the percentage efficiency of our parry. + */ /mob/living/proc/get_parry_efficiency(attack_type) var/datum/block_parry_data/data = get_parry_data() if(get_parry_stage() != PARRY_ACTIVE) return 0 var/difference = abs(get_parry_time() - (data.parry_time_perfect + data.parry_time_windup)) - var/leeway = data.parry_time_perfect_leeway_override["[attack_type]"] + var/leeway = data.attack_type_list_scan(data.parry_time_perfect_leeway_override, attack_type) if(isnull(leeway)) leeway = data.parry_time_perfect_leeway difference -= leeway . = data.parry_efficiency_perfect if(difference <= 0) return - var/falloff = data.parry_imperfect_falloff_percent_override["[attack_type]"] + var/falloff = data.attack_type_list_scan(data.parry_imperfect_falloff_percent_override, attack_type) if(isnull(falloff)) falloff = data.parry_imperfect_falloff_percent . -= falloff * difference +/** + * Gets the current decisecond "frame" of an active parry. + */ /mob/living/proc/get_parry_time() return world.time - parry_start_time /// same return values as normal blocking, called with absolute highest priority in the block "chain". /mob/living/proc/run_parry(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list()) + var/stage = get_parry_stage() + if(stage == NOT_PARRYING) + return BLOCK_NONE + var/efficiency = get_parry_efficiency(attack_type) + switch(parrying) + if(ITEM_PARRY) + . = active_parry_item.on_active_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency) + if(UNARMED_PARRY) + . = on_active_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency) + if(MARTIAL_PARRY) + . = mind.martial_art.on_active_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency) + if(efficiency <= 0) + return + . |= BLOCK_SHOULD_PARTIAL_MITIGATE + var/current = return_list[BLOCK_RETURN_MITIGATION_PERCENT] || 0 + return_list[BLOCK_RETURN_MITIGATION_PERCENT] = 100 - (clamp(100 - current, 0, 100) * clamp(1 - (efficiency / 100), 0, 1)) + var/list/effect_text = run_parry_countereffects(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency) + if(parry_default_handle-feedback) + handle_parry_feedback(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, effect_text) + successful_parries |= efficiency + +/mob/living/proc/handle_parry_feedback(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), parry_efficiency, list/effect_text) + var/datum/block_parry_data/data = get_parry_data() + if(data.parry_sounds) + playsound(src, pick(data.parry_sounds), 75) + visible_message("[src] parries \the [attack_text][length(effect_text)? ", [english_list(effect_text)] [attacker]" : ""]!") /// Run counterattack if any -/mob/living/proc/run_parry_countereffects(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list()) +/mob/living/proc/run_parry_countereffects(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), parry_efficiency) if(!isliving(attacker)) return var/mob/living/L = attacker @@ -215,6 +274,7 @@ GLOBAL_LIST_EMPTY(block_parry_data) if(data.parry_data[PARRY_DAZE_ATTACKER]) L.Daze(data.parry_data[PARRY_DAZE_ATTACKER]) effect_text += "dazing" + return effect_text /// Gets the datum/block_parry_data we're going to use to parry. /mob/living/proc/get_parry_data() @@ -225,3 +285,21 @@ GLOBAL_LIST_EMPTY(block_parry_data) else if(parrying == MARTIAL_PARRY) return get_block_parry_data(mind.martial_art.block_parry_data) +/// Effects +/obj/effect/abstract/parry + icon = 'icons/effects/block_parry.dmi' + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + layer = FLOAT_LAYER + plane = FLOAT_PLANE + vis_flags = VIS_INHERIT_LAYER|VIS_INHERIT_PLANE + +/obj/effect/abstract/parry/main + icon_state = "parry_bm_hold" + +/obj/effect/abstract/parry/main/proc/run(windup_time = 2, active_time = 5, spindown_time = 3, qdel_end = TRUE) + QDEL_IN(src, windup_time + active_time + spindown_time) + var/matrix/current = transform + transform = matrix(0.1, 0, 0, 0, 0.1, 0) + animate(src, transform = current, time = windup_time) + sleep(active_time) + flick(icon, "parry_bm_end") diff --git a/icons/effects/block_parry.dmi b/icons/effects/block_parry.dmi new file mode 100644 index 0000000000..4a0ade5e61 Binary files /dev/null and b/icons/effects/block_parry.dmi differ