// Active parry system goes in here. /** * Determines if we can actively parry. */ /obj/item/proc/can_active_parry(mob/user) return block_parry_data && (item_flags & ITEM_CAN_PARRY) /** * Called from keybindings. */ /mob/living/proc/keybind_parry() initiate_parry_sequence() /** * Initiates a parrying sequence. */ /mob/living/proc/initiate_parry_sequence(silent = FALSE, list/override_method_data) if(parrying) return FALSE // already parrying if(!(mobility_flags & MOBILITY_USE)) if(!silent) to_chat(src, "You can't move your arms!") return FALSE if(!(combat_flags & COMBAT_FLAG_PARRY_CAPABLE)) if(!silent) to_chat(src, "You are not something that can parry attacks.") return FALSE if(!(mobility_flags & MOBILITY_STAND)) if(!silent) to_chat(src, "You aren't able to parry without solid footing!") return FALSE var/datum/block_parry_data/data var/list/determined = override_method_data || determine_parry_method(FALSE, FALSE) if(!islist(determined)) return FALSE var/method = determined[1] data = return_block_parry_datum(determined[2]) var/datum/tool = determined[3] var/full_parry_duration = data.parry_time_windup + data.parry_time_active + data.parry_time_spindown // no system in place to "fallback" if out of the 3 the top priority one can't parry due to constraints but something else can. // can always implement it later, whatever. if((data.parry_respect_clickdelay && !CheckActionCooldown()) || ((parry_end_time_last + data.parry_cooldown) > world.time)) if(!silent) to_chat(src, "You are not ready to parry (again)!") return FALSE // Point of no return, make sure everything is set. parrying = method if(method == ITEM_PARRY) active_parry_item = tool if(!UseStaminaBuffer(data.parry_stamina_cost, TRUE)) return FALSE parry_start_time = world.time successful_parries = list() successful_parry_counterattacks = list() addtimer(CALLBACK(src, PROC_REF(end_parry_sequence)), full_parry_duration) if(data.parry_flags & PARRY_LOCK_ATTACKING) ADD_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_PARRY_TRAIT) if(data.parry_flags & PARRY_LOCK_SPRINTING) ADD_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_PARRY_TRAIT) handle_parry_starting_effects(data) return TRUE /** * Massive snowflake proc for getting something to parry with. * * @return list of [method, data, tool], where method is the parry method define, data is the block_parry_data var that must be run through return_block_parry_data, and tool is the concept/object/martial art/etc used. */ /mob/living/proc/determine_parry_method(silent = TRUE, autoparry = FALSE) // Prioritize item, then martial art, then unarmed. // yanderedev else if time var/obj/item/using_item = get_active_held_item() var/datum/block_parry_data/data var/datum/tool var/method if(using_item?.can_active_parry(src)) data = using_item.block_parry_data method = ITEM_PARRY tool = using_item else if(mind?.martial_art?.can_martial_parry) data = mind.martial_art.block_parry_data method = MARTIAL_PARRY tool = mind.martial_art else if((combat_flags & COMBAT_FLAG_UNARMED_PARRY) && check_unarmed_parry_activation_special()) data = block_parry_data method = UNARMED_PARRY tool = src else // QOL: If none of the above work, try to find another item. var/obj/item/backup = find_backup_parry_item() if(backup) tool = backup data = backup.block_parry_data using_item = backup method = ITEM_PARRY var/list/other_items = list() var/list/override = list() if(SEND_SIGNAL(src, COMSIG_LIVING_ACTIVE_PARRY_START, method, tool, other_items, override) & COMPONENT_PREVENT_PARRY_START) if(!silent) to_chat(src, "Something is preventing you from parrying!") return if(length(override)) var/datum/thing = override[1] var/_method = override[thing] if(_method == ITEM_PARRY) using_item = thing tool = using_item method = ITEM_PARRY data = using_item.block_parry_data else if(_method == UNARMED_PARRY) method = UNARMED_PARRY tool = src data = thing if(!using_item && !method && length(other_items)) using_item = other_items[1] tool = using_item method = ITEM_PARRY data = using_item.block_parry_data if(!method) if(!silent) to_chat(src, "You have nothing to parry with!") return FALSE return list(method, data, tool) /** * Tries to find a backup parry item. * Does not look at active held item. */ /mob/living/proc/find_backup_parry_item() for(var/obj/item/I in held_items - get_active_held_item()) if(I.can_active_parry(src)) return I /** * Check if we can unarmed parry */ /mob/living/proc/check_unarmed_parry_activation_special() return TRUE /** * Called via timer when the parry sequence ends. */ /mob/living/proc/end_parry_sequence() if(!parrying) return REMOVE_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_PARRY_TRAIT) REMOVE_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_PARRY_TRAIT) if(parry_visual_effect) QDEL_NULL(parry_visual_effect) var/datum/block_parry_data/data = get_parry_data() var/list/effect_text = list() var/successful = FALSE for(var/efficiency in successful_parries) if(efficiency >= data.parry_efficiency_considered_successful) successful = TRUE break if(!successful) // didn't parry anything successfully if(data.parry_failed_stagger_duration) Stagger(data.parry_failed_stagger_duration) effect_text += "staggering themselves" if(data.parry_failed_clickcd_duration) DelayNextAction(data.parry_failed_clickcd_duration, flush = TRUE) effect_text += "throwing themselves off balance" handle_parry_ending_effects(data, effect_text) parrying = NOT_PARRYING parry_start_time = 0 parry_end_time_last = world.time + (successful? 0 : data.parry_failed_cooldown_duration) successful_parries = null successful_parry_counterattacks = null /** * Handles starting effects for parrying. */ /mob/living/proc/handle_parry_starting_effects(datum/block_parry_data/data) playsound(src, data.parry_start_sound, 75, 1) parry_visual_effect = new /obj/effect/abstract/parry/main(null, TRUE, src, data.parry_effect_icon_state, data.parry_time_windup_visual_override || data.parry_time_windup, data.parry_time_active_visual_override || data.parry_time_active, data.parry_time_spindown_visual_override || data.parry_time_spindown) switch(parrying) if(ITEM_PARRY) visible_message("[src] swings [active_parry_item]!") else visible_message("[src] rushes forwards!") /** * Handles ending effects for parrying. */ /mob/living/proc/handle_parry_ending_effects(datum/block_parry_data/data, list/failed_effect_text) if(length(successful_parries)) return visible_message("[src] fails to connect their parry[failed_effect_text? ", [english_list(failed_effect_text)]" : ""]!") /** * Gets this item's datum/block_parry_data */ /obj/item/proc/get_block_parry_data() return return_block_parry_datum(block_parry_data) //Stubs. /** * Called when an attack is parried using this, whether or not the parry was successful. */ /obj/item/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/block_return, parry_efficiency, parry_time, autoparry = FALSE) /** * Called when an attack is parried innately, whether or not the parry was successful. */ /mob/living/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/block_return, parry_efficiency, parry_time, autoparry = FALSE) /** * Called when an attack is parried using this, whether or not the parry was successful. */ /datum/martial_art/proc/on_active_parry(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/block_return, parry_efficiency, parry_time, autoparry = FALSE) /** * Called when an attack is parried and block_parra_data indicates to use a proc to handle counterattack. */ /obj/item/proc/active_parry_reflex_counter(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list, parry_efficiency, list/effect_text) /** * Called when an attack is parried and block_parra_data indicates to use a proc to handle counterattack. */ /mob/living/proc/active_parry_reflex_counter(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list, parry_efficiency, list/effect_text) /** * Called when an attack is parried and block_parra_data indicates to use a proc to handle counterattack. */ /datum/martial_art/proc/active_parry_reflex_counter(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list, parry_efficiency, list/effect_text) /** * Attempts to automatically parry an attacker. */ /mob/living/proc/attempt_auto_parry(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list()) // determine how we'll parry var/list/determined = determine_parry_method(TRUE, TRUE) if(!islist(determined)) return BLOCK_NONE var/datum/block_parry_data/data = return_block_parry_datum(determined[2]) if(!data.parry_automatic_enabled || (last_autoparry > (world.time - data.autoparry_cooldown_absolute))) return BLOCK_NONE if(attack_type && !(attack_type & data.parry_attack_types)) return BLOCK_NONE // before doing anything, check if the user moused over them properly if(!client) return BLOCK_NONE var/found = attacker == client.mouse_object_ref?.resolve() if(!found) for(var/i in client.moused_over_objects) if(i == object) if((client.moused_over_objects[i] + (data.autoparry_mouse_delay_maximum)) >= world.time) found = TRUE break if(!found) return BLOCK_NONE // if that works, try to start parry // first, check cooldowns // now, depending on if we're doing a single simulation or a full sequence last_autoparry = world.time if(data.autoparry_sequence_simulation) // for full sequence simulation initiate_parry_sequence(TRUE, determined) if(data.autoparry_sequence_start_time == -1) parry_start_time = world.time - data.parry_time_windup else parry_start_time = world.time - data.autoparry_sequence_start_time // recurse back to original return run_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, FALSE) else // yes, this is mostly a copypaste of run_parry. var/efficiency = data.attack_type_list_scan(data.autoparry_single_efficiency_override, attack_type) if(isnull(efficiency)) efficiency = data.autoparry_single_efficiency var/method = determined[1] switch(method) if(ITEM_PARRY) var/obj/item/I = determined[3] . = I.on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, null, TRUE) if(UNARMED_PARRY) . = on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, null, TRUE) if(MARTIAL_PARRY) . = mind.martial_art.on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, null, TRUE) if(!isnull(return_list[BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY])) // one of our procs overrode efficiency = return_list[BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY] if(efficiency <= 0) // Do not allow automatically handled/standardized parries that increase damage for now. return . |= BLOCK_SHOULD_PARTIAL_MITIGATE if(efficiency >= data.parry_efficiency_perfect) . |= data.perfect_parry_block_return_flags if(data.perfect_parry_block_return_list) return_list |= data.perfect_parry_block_return_list else if(efficiency >= data.parry_efficiency_considered_successful) . |= data.imperfect_parry_block_return_flags if(data.imperfect_parry_block_return_list) return_list |= data.imperfect_parry_block_return_list else . |= data.failed_parry_block_return_flags if(data.failed_parry_block_return_list) return_list |= data.failed_parry_block_return_list if(isnull(return_list[BLOCK_RETURN_MITIGATION_PERCENT])) // if one of the on_active_parry procs overrode. We don't have to worry about interference since parries are the first thing checked in the [do_run_block()] sequence. return_list[BLOCK_RETURN_MITIGATION_PERCENT] = clamp(efficiency, 0, 100) // do not allow > 100% or < 0% for now. if((return_list[BLOCK_RETURN_MITIGATION_PERCENT] >= 100) || (damage <= 0)) . |= BLOCK_SUCCESS var/list/effect_text var/pacifist_counter_check = TRUE if(HAS_TRAIT(src, TRAIT_PACIFISM)) switch(parrying) if(ITEM_PARRY) pacifist_counter_check = (!active_parry_item.force || active_parry_item.damtype == STAMINA) else pacifist_counter_check = FALSE //Both martial and unarmed counter attacks generally are harmful, so no need to have the same line twice. if(efficiency >= data.parry_efficiency_to_counterattack && pacifist_counter_check && !return_list[BLOCK_RETURN_FORCE_NO_PARRY_COUNTERATTACK] && (!(attacker in successful_parry_counterattacks) && !data.parry_allow_repeated_counterattacks)) effect_text = run_parry_countereffects(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, data) if(data.parry_flags & PARRY_DEFAULT_HANDLE_FEEDBACK) handle_parry_feedback(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, effect_text, data) /** * 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 var/current_time = get_parry_time() // Not a switch statement because byond switch statements don't support floats at time of writing with "to" keyword. if(current_time < 0) return NOT_PARRYING else if(current_time < windup_end) return PARRY_WINDUP else if(current_time <= active_end) // this uses <= on purpose, give a slight bit of advantage because time is rounded to world.tick_lag return PARRY_ACTIVE else if(current_time <= spindown_end) return PARRY_SPINDOWN else return NOT_PARRYING /** * 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(), allow_auto = TRUE) var/stage = get_parry_stage() if(attack_type & ATTACK_TYPE_PARRY_COUNTERATTACK) return BLOCK_NONE // don't infinite loop if(stage != PARRY_ACTIVE) // If they're not currently parrying, attempt auto parry if(stage == NOT_PARRYING) if(!allow_auto || SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE)) return BLOCK_NONE return attempt_auto_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list) else return BLOCK_NONE var/datum/block_parry_data/data = get_parry_data() if(attack_type && !(attack_type & data.parry_attack_types)) return BLOCK_NONE var/efficiency = data.get_parry_efficiency(attack_type, get_parry_time()) switch(parrying) if(ITEM_PARRY) if(!active_parry_item.can_active_parry(src)) return BLOCK_NONE . = active_parry_item.on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, get_parry_time()) if(UNARMED_PARRY) . = on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, get_parry_time()) if(MARTIAL_PARRY) . = mind.martial_art.on_active_parry(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency, get_parry_time()) if(!isnull(return_list[BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY])) // one of our procs overrode efficiency = return_list[BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY] if(efficiency <= 0) // Do not allow automatically handled/standardized parries that increase damage for now. return . |= BLOCK_SHOULD_PARTIAL_MITIGATE if(efficiency >= data.parry_efficiency_perfect) . |= data.perfect_parry_block_return_flags if(data.perfect_parry_block_return_list) return_list |= data.perfect_parry_block_return_list else if(efficiency >= data.parry_efficiency_considered_successful) . |= data.imperfect_parry_block_return_flags if(data.imperfect_parry_block_return_list) return_list |= data.imperfect_parry_block_return_list else . |= data.failed_parry_block_return_flags if(data.failed_parry_block_return_list) return_list |= data.failed_parry_block_return_list if(isnull(return_list[BLOCK_RETURN_MITIGATION_PERCENT])) // if one of the on_active_parry procs overrode. We don't have to worry about interference since parries are the first thing checked in the [do_run_block()] sequence. return_list[BLOCK_RETURN_MITIGATION_PERCENT] = clamp(efficiency, 0, 100) // do not allow > 100% or < 0% for now. if((return_list[BLOCK_RETURN_MITIGATION_PERCENT] >= 100) || (damage <= 0)) . |= BLOCK_SUCCESS var/list/effect_text var/pacifist_counter_check = TRUE if(HAS_TRAIT(src, TRAIT_PACIFISM)) switch(parrying) if(ITEM_PARRY) pacifist_counter_check = (!active_parry_item.force || active_parry_item.damtype == STAMINA) else pacifist_counter_check = FALSE //Both martial and unarmed counter attacks generally are harmful, so no need to have the same line twice. if(efficiency >= data.parry_efficiency_to_counterattack && pacifist_counter_check && !return_list[BLOCK_RETURN_FORCE_NO_PARRY_COUNTERATTACK] && (!(attacker in successful_parry_counterattacks) && !data.parry_allow_repeated_counterattacks)) effect_text = run_parry_countereffects(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, efficiency) if(data.parry_flags & 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 if(length(successful_parries) >= data.parry_max_attacks) end_parry_sequence() /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, datum/block_parry_data/data) if(!data) data = get_parry_data() var/knockdown_check = FALSE if(data.parry_data[PARRY_KNOCKDOWN_ATTACKER] && parry_efficiency >= data.parry_efficiency_to_counterattack) knockdown_check = TRUE if(data.parry_sounds) playsound(src, pick(data.parry_sounds), 75) visible_message("[src] parries [attack_text][length(effect_text)? ", [english_list(effect_text)] [attacker]" : ""][length(effect_text) && knockdown_check? " and" : ""][knockdown_check? " knocking them to the ground" : ""]!") /// 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, datum/block_parry_data/data) if(!isliving(attacker)) return var/mob/living/L = attacker if(!data) data = get_parry_data() var/list/effect_text = list() successful_parry_counterattacks |= attacker // Always proc so items can override behavior easily switch(parrying) if(ITEM_PARRY) active_parry_item.active_parry_reflex_counter(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, parry_efficiency, effect_text) if(UNARMED_PARRY) active_parry_reflex_counter(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, parry_efficiency, effect_text) if(MARTIAL_PARRY) mind.martial_art.active_parry_reflex_counter(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, parry_efficiency, effect_text) if(Adjacent(attacker) || data.parry_data[PARRY_COUNTERATTACK_IGNORE_ADJACENCY]) if(data.parry_data[PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN]) switch(parrying) if(ITEM_PARRY) active_parry_item.melee_attack_chain(src, attacker, null, ATTACK_IS_PARRY_COUNTERATTACK | ATTACK_IGNORE_CLICKDELAY | ATTACK_IGNORE_ACTION | NO_AUTO_CLICKDELAY_HANDLING, data.parry_data[PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN]) effect_text += "reflexively counterattacking with [active_parry_item]" if(UNARMED_PARRY) // WARNING: If you are using these two, the attackchain parry counterattack flags and damage multipliers are unimplemented. Be careful with how you handle this. UnarmedAttack(attacker) effect_text += "reflexively counterattacking in the process" if(MARTIAL_PARRY) // Not well implemeneted, recommend custom implementation using the martial art datums. UnarmedAttack(attacker) effect_text += "reflexively maneuvering to retaliate" if(data.parry_data[PARRY_DISARM_ATTACKER]) L.drop_all_held_items() effect_text += "disarming" 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" if(data.parry_data[PARRY_KNOCKDOWN_ATTACKER]) L.DefaultCombatKnockdown(data.parry_data[PARRY_KNOCKDOWN_ATTACKER]) // effect_text += "knocking them to the ground" - snowflaked above 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 /// The person we're on var/mob/living/owner /obj/effect/abstract/parry/main name = null /obj/effect/abstract/parry/main/Initialize(mapload, autorun, mob/living/owner, set_icon_state, windup, active, spindown) . = ..() icon_state = set_icon_state if(owner) attach_to(owner) if(autorun) INVOKE_ASYNC(src, PROC_REF(run_animation), windup, active, spindown) /obj/effect/abstract/parry/main/Destroy() detach_from(owner) return ..() /obj/effect/abstract/parry/main/proc/attach_to(mob/living/attaching) if(owner) detach_from(owner) owner = attaching owner.vis_contents += src /obj/effect/abstract/parry/main/proc/detach_from(mob/living/detaching) if(detaching == owner) owner = null detaching.vis_contents -= src /obj/effect/abstract/parry/main/proc/run_animation(windup_time = 2, active_time = 5, spindown_time = 3) var/matrix/current = transform transform = matrix(0.1, 0, 0, 0, 0.1, 0) animate(src, transform = current, time = windup_time) sleep(active_time) animate(src, alpha = 0, spindown_time)