Files
GS13NG/code/modules/mob/living/living_blocking_parrying.dm
T
2020-05-07 09:30:34 -07:00

326 lines
15 KiB
Plaintext

// yell at me later for file naming
// This file contains stuff relating to the new directional blocking and parry system.
/mob/living
/// Whether ot not the user is in the middle of an active parry. Set to [UNARMED_PARRY], [ITEM_PARRY], [MARTIAL_PARRY] if parrying.
var/parrying = FALSE
/// The itme the user is currently parrying with, if any.
var/obj/item/active_parry_item
/// world.time of parry action start
var/parry_start_time = 0
/// Whether or not we can unarmed parry
var/parry_while_unarmed = FALSE
/// Should we prioritize martial art parrying when unarmed?
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)
/proc/return_block_parry_datum(datum/block_parry_data/type_id_datum)
if(istype(type_id_datum))
return type_id_datum
if(ispath(type_id_datum))
. = GLOB.block_parry_data["[type_id_datum]"]
if(!.)
. = GLOB.block_parry_data["[type_id_datum]"] = new type_id_datum
else //text id
return GLOB.block_parry_data["[type_id_datum]"]
/proc/set_block_parry_datum(id, datum/block_parry_data/data)
if(ispath(id))
CRASH("Path-fetching of block parry data is only to grab static data, do not attempt to modify global caches of paths. Use string IDs.")
GLOB.block_parry_data["[id]"] = data
/// Carries data like list data that would be a waste of memory if we initialized the list on every /item as we can cache datums easier.
/datum/block_parry_data
/////////// BLOCKING ////////////
/// NOTE: FOR ATTACK_TYPE_DEFINE, you MUST wrap it in "[DEFINE_HERE]"! The defines are bitflags, and therefore, NUMBERS!
/// See defines. Point of reference is someone facing north.
var/can_block_directions = BLOCK_DIR_NORTH | BLOCK_DIR_NORTHEAST | BLOCK_DIR_NORTHWEST
/// Our slowdown added while blocking
var/block_slowdown = 2
/// Clickdelay added to user after block ends
var/block_end_click_cd_add = 4
/// Disallow attacking during block
var/block_lock_attacking = TRUE
/// The priority we get in [mob/do_run_block()] while we're being used to parry.
var/block_active_priority = BLOCK_PRIORITY_ACTIVE_BLOCK
/// Windup before we have our blocking active.
var/block_start_delay = 5
/// Amount of "free" damage blocking absorbs
var/block_damage_absorption = 10
/// Override absorption, list("[ATTACK_TYPE_DEFINE]" = absorption), see [block_damage_absorption]
var/list/block_damage_absorption_override
/// Ratio of damage block above absorption amount, coefficient, lower is better, this is multiplied by damage to determine how much is blocked.
var/block_damage_multiplier = 0.5
/// Override damage overrun efficiency, list("[ATTACK_TYPE_DEFINE]" = absorption), see [block_damage_efficiency]
var/list/block_damage_multiplier_override
/// Upper bound of damage block, anything above this will go right through.
var/block_damage_limit = 80
/// Override upper bound of damage block, list("[ATTACK_TYPE_DEFINE]" = absorption), see [block_damage_limit]
var/list/block_damage_limit_override
/*
* NOTE: Overrides for attack types for most the block_stamina variables were removed,
* because at the time of writing nothing needed to use it. Add them if you need it,
* it should be pretty easy, just copy [active_block_damage_mitigation]
* for how to override with list.
*/
/// Default damage-to-stamina coefficient, higher is better. This is based on amount of damage BLOCKED, not initial damage, to prevent damage from "double dipping".
var/block_stamina_efficiency = 2
/// Override damage-to-stamina coefficient, see [block_efficiency], this should be list("[ATTACK_TYPE_DEFINE]" = coefficient_number)
var/list/block_stamina_efficiency_override
/// Ratio of stamina incurred by blocking that goes to the arm holding the object instead of the chest. Has no effect if this is not held in hand.
var/block_stamina_limb_ratio = 0.5
/// Ratio of stamina incurred by chest (so after [block_stamina_limb_ratio] runs) that is buffered.
var/block_stamina_buffer_ratio = 1
/// Stamina dealt directly via adjustStaminaLossBuffered() per SECOND of block.
var/block_stamina_cost_per_second = 1.5
/// Bitfield for attack types that we can block while down. This will work in any direction.
var/block_resting_attack_types_anydir = ATTACK_TYPE_MELEE | ATTACK_TYPE_UNARMED | ATTACK_TYPE_TACKLE
/// Bitfield for attack types that we can block while down but only in our normal directions.
var/block_resting_attack_types_directional = ATTACK_TYPE_PROJECTILE | ATTACK_TYPE_THROWN
/// Multiplier to stamina damage taken for attacks blocked while downed.
var/block_resting_stamina_penalty_multiplier = 1.5
/// Override list for multiplier to stamina damage taken for attacks blocked while down. list("[ATTACK_TYPE_DEFINE]" = multiplier_number)
var/list/block_resting_stamina_penalty_multiplier_override
/// Sounds for blocking
var/list/block_sounds = list('sound/block_parry/block_metal1.ogg' = 1, 'sound/block_parry/block_metal1.ogg' = 1)
/////////// PARRYING ////////////
/// Prioriry for [mob/do_run_block()] while we're being used to parry.
// None - Parry is always highest priority!
/// Parry doesn't work if you aren't able to otherwise attack due to clickdelay
var/parry_respect_clickdelay
#warn implement
/// Parry stamina cost
var/parry_stamina_cost = 5
#warn implement
/// Parry windup duration in deciseconds. 0 to this is windup, afterwards is main stage.
var/parry_time_windup = 2
/// Parry spindown duration in deciseconds. main stage end to this is the spindown stage, afterwards the parry fully ends.
var/parry_time_spindown = 3
/// Main parry window in deciseconds. This is between [parry_time_windup] and [parry_time_spindown]
var/parry_time_active = 5
/// Perfect parry window in deciseconds from the start of the main window. 3 with main 5 = perfect on third decisecond of main window.
var/parry_time_perfect = 2.5
/// Time on both sides of perfect parry that still counts as part of the perfect window.
var/parry_time_perfect_leeway = 1
/// [parry_time_perfect_leeway] override for attack types, list("[ATTACK_TYPE_DEFINE]" = deciseconds)
var/list/parry_time_perfect_leeway_override
/// Parry "efficiency" falloff in percent per decisecond once perfect window is over.
var/parry_imperfect_falloff_percent = 20
/// [parry_imperfect_falloff_percent] override for attack types, list("[ATTACK_TYPE_DEFINE]" = deciseconds)
var/list/parry_imperfect_falloff_percent_override
/// Efficiency in percent on perfect parry.
var/parry_efficiency_perfect = 120
/// 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)
/// Stagger duration post-parry if you fail to parry an attack
var/parry_failed_stagger_duration = 3.5 SECONDS
#warn implement
/// Clickdelay duration post-parry if you fail to parry an attack
var/parry_failed_clickcd_duration = 2 SECONDS
#warn implement
/**
* 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
/**
* Gets this item's datum/block_parry_data
*/
/obj/item/proc/get_block_parry_data()
return return_block_parry_datum(block_parry_data)
/mob/living/proc/handle_block_parry(seconds = 1)
if(active_blocking)
var/datum/block_parry_data/data = return_block_parry_datum(active_block_item.block_parry_data)
adjustStaminaLossBuffered(data.block_stamina_cost_per_second * seconds)
//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)
/mob/living/proc/active_parry_reflex_counter(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list)
/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
var/datum/block_parry_data/data = get_parry_data()
var/windup_end = data.parry_time_windup
var/active_end = windup_end + data.parry_time_active
var/spindown_end = active_end + data.parry_time_spindown
switch(get_parry_time())
if(0 to windup_end)
return PARRY_WINDUP
if(windup_end to active_end)
return PARRY_ACTIVE
if(active_end to spindown_end)
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.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.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/datum/block_parry_data/data = get_parry_data()
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(data.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("<span class='danger'>[src] parries \the [attack_text][length(effect_text)? ", [english_list(effect_text)] [attacker]" : ""]!</span>")
/// 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(), parry_efficiency)
if(!isliving(attacker))
return
var/mob/living/L = attacker
var/datum/block_parry_data/data = get_parry_data()
var/list/effect_text = list()
if(data.parry_data[PARRY_REFLEX_COUNTERATTACK])
switch(data.parry_data[PARRY_REFLEX_COUNTERATTACK])
if(PARRY_COUNTERATTACK_PROC)
switch(parrying)
if(ITEM_PARRY)
active_parry_item.active_parry_reflex_counter(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)
if(UNARMED_PARRY)
active_parry_reflex_counter(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)
if(MARTIAL_PARRY)
mind.martial_art.active_parry_reflex_counter(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)
if(PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN)
switch(parrying)
if(ITEM_PARRY)
active_parry_item.melee_attack_chain(src, attacker, null)
if(UNARMED_PARRY)
UnarmedAttack(attacker)
if(MARTIAL_PARRY)
UnarmedAttack(attacker)
if(data.parry_data[PARRY_DISARM_ATTACKER])
L.drop_all_held_items()
effect_text += "disarming"
if(data.parry_data[PARRY_KNOCKDOWN_ATTACKER])
L.DefaultCombatKnockdown(data.parry_data[PARRY_KNOCKDOWN_ATTACKER])
effect_text += "knocking them to the ground"
if(data.parry_data[PARRY_STAGGER_ATTACKER])
L.Stagger(data.parry_data[PARRY_STAGGER_ATTACKER])
effect_text += "staggering"
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()
if(parrying == ITEM_PARRY)
return active_parry_item.get_block_parry_data()
else if(parrying == UNARMED_PARRY)
return return_block_parry_datum(block_parry_data)
else if(parrying == MARTIAL_PARRY)
return return_block_parry_datum(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_animation(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")