Merge pull request #14482 from silicons/combat_v7

Combat v7 - Sprint removal, automatic block/parry, turns combat mode into a pure UI/interaction toggle with no side effects, and a truckload of other stuff.
This commit is contained in:
Lin
2021-06-27 13:04:29 -07:00
committed by GitHub
76 changed files with 854 additions and 392 deletions

View File

@@ -466,7 +466,7 @@
block_return[BLOCK_RETURN_BLOCK_CAPACITY] = (block_return[BLOCK_RETURN_BLOCK_CAPACITY] || 0) + remaining_uses
return ..()
/obj/item/shield/changeling/active_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
/obj/item/shield/changeling/directional_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
. = ..()
if(--remaining_uses < 1)
if(ishuman(loc))

View File

@@ -90,7 +90,6 @@
parry_time_perfect = 2
parry_efficiency_perfect = 110 //Very low leeway for counterattacks...
parry_efficiency_considered_successful = 0.8
parry_efficiency_to_counterattack = 1
parry_efficiency_to_counterattack = 110
parry_cooldown = 15 //But also very low cooldown..
parry_failed_stagger_duration = 2 SECONDS //And relatively small penalties for failing.
parry_failed_clickcd_duration = 1 SECONDS

View File

@@ -183,3 +183,7 @@
//world.time of when the crew manifest can be accessed
var/crew_manifest_delay
/// Should go in persistent round player data sometime. This tracks what items have already warned the user on pickup that they can block/parry.
var/list/block_parry_hinted = list()
/// moused over objects, currently capped at 7. this is awful, and should be replaced with a component to track it using signals for parrying at some point.
var/list/moused_over_objects = list()

View File

@@ -236,10 +236,9 @@
parry_efficiency_considered_successful = 0.01
parry_efficiency_to_counterattack = INFINITY // no auto counter
parry_max_attacks = INFINITY
parry_failed_cooldown_duration = 2.25 SECONDS
parry_failed_stagger_duration = 2.25 SECONDS
parry_failed_cooldown_duration = 1.5 SECONDS
parry_failed_stagger_duration = 1.5 SECONDS
parry_cooldown = 0
parry_failed_clickcd_duration = 0
/obj/item/clothing/gloves/fingerless/pugilist/mauler
name = "mauler gauntlets"

View File

@@ -76,3 +76,15 @@
else
user.mob.dropItemToGround(I)
return TRUE
/datum/keybinding/mob/examine_immediate
hotkey_keys = list()
classic_keys = list()
name = "examine_immediate"
full_name = "Examine (Immediate)"
description = "Immediately examine anything you're hovering your mouse over."
/datum/keybinding/mob/examine_immediate/down(client/user)
var/atom/A = user.mouseObject
if(A)
A.attempt_examinate(user.mob)

View File

@@ -196,7 +196,6 @@
parry_imperfect_falloff_percent = 20
parry_efficiency_to_counterattack = 100 // perfect parry or you're cringe
parry_failed_stagger_duration = 1.5 SECONDS // a good time to reconsider your actions...
parry_failed_clickcd_duration = 1.5 SECONDS // or your failures
/obj/item/kinetic_crusher/glaive/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) // if you're dumb enough to go for a parry...
var/turf/proj_turf = owner.loc // destabilizer bolt, ignoring cooldown

View File

@@ -307,7 +307,7 @@
/obj/item/energy_katana,
/obj/item/hierophant_club,
/obj/item/his_grace,
/obj/item/gun/ballistic/minigun,
/obj/item/gun/energy/minigun,
/obj/item/gun/ballistic/automatic/l6_saw,
/obj/item/gun/magic/staff/chaos,
/obj/item/gun/magic/staff/spellblade,

View File

@@ -2,7 +2,7 @@
//The effects include: stun, knockdown, unconscious, sleeping, resting, jitteriness, dizziness, ear damage,
// eye damage, eye_blind, eye_blurry, druggy, TRAIT_BLIND trait, and TRAIT_NEARSIGHT trait.
/mob/living/carbon/alien/DefaultCombatKnockdown(amount, updating = TRUE, ignore_canknockdown = FALSE, override_hardstun, override_stamdmg)
/mob/living/carbon/alien/DefaultCombatKnockdown(amount, updating = TRUE, ignore_canknockdown = FALSE, override_hardstun, override_stamdmg, knocktofloor)
return //no
/////////////////////////////////// STUN ////////////////////////////////////

View File

@@ -358,10 +358,11 @@
return
I.item_flags |= BEING_REMOVED
breakouttime = I.breakouttime
var/datum/cuffbreak_checker/cuffbreak_checker = new(get_turf(src))
if(!cuff_break)
visible_message("<span class='warning'>[src] attempts to remove [I]!</span>")
to_chat(src, "<span class='notice'>You attempt to remove [I]... (This will take around [DisplayTimeText(breakouttime)] and you need to stand still.)</span>")
if(do_after(src, breakouttime, 0, target = src, required_mobility_flags = MOBILITY_RESIST))
if(do_after_advanced(src, breakouttime, src, NONE, CALLBACK(cuffbreak_checker, /datum/cuffbreak_checker.proc/check_movement), required_mobility_flags = MOBILITY_RESIST))
clear_cuffs(I, cuff_break)
else
to_chat(src, "<span class='warning'>You fail to remove [I]!</span>")
@@ -370,15 +371,30 @@
breakouttime = 50
visible_message("<span class='warning'>[src] is trying to break [I]!</span>")
to_chat(src, "<span class='notice'>You attempt to break [I]... (This will take around 5 seconds and you need to stand still.)</span>")
if(do_after(src, breakouttime, 0, target = src, required_mobility_flags = MOBILITY_RESIST))
if(do_after_advanced(src, breakouttime, src, NONE, CALLBACK(cuffbreak_checker, /datum/cuffbreak_checker.proc/check_movement), required_mobility_flags = MOBILITY_RESIST))
clear_cuffs(I, cuff_break)
else
to_chat(src, "<span class='warning'>You fail to break [I]!</span>")
else if(cuff_break == INSTANT_CUFFBREAK)
clear_cuffs(I, cuff_break)
QDEL_NULL(cuffbreak_checker)
I.item_flags &= ~BEING_REMOVED
/datum/cuffbreak_checker
var/turf/last
/datum/cuffbreak_checker/New(turf/initial_turf)
last = initial_turf
/datum/cuffbreak_checker/proc/check_movement(atom/user, delay, atom/target, time_left, do_after_flags, required_mobility_flags, required_combat_flags, mob_redirect, stage, initially_held_item, tool, list/passed_in)
if(get_turf(user) != last)
last = get_turf(user)
passed_in[1] = 0.5
else
passed_in[1] = 1
/mob/living/carbon/proc/uncuff()
if (handcuffed)
var/obj/item/W = handcuffed
@@ -459,10 +475,6 @@
if(HAS_TRAIT(src, TRAIT_CLUMSY))
modifier -= 40 //Clumsy people are more likely to hit themselves -Honk!
//CIT CHANGES START HERE
else if(SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
modifier -= 50
if(modifier < 100)
dropItemToGround(I)
//END OF CIT CHANGES

View File

@@ -1055,10 +1055,10 @@
return
if(!HAS_TRAIT(src, TRAIT_IGNOREDAMAGESLOWDOWN)) //if we want to ignore slowdown from damage, but not from equipment
var/scaling = maxHealth / 100
var/health_deficiency = ((maxHealth / scaling) - (health / scaling) + (getStaminaLoss()*0.75))//CIT CHANGE - reduces the impact of staminaloss and makes stamina buffer influence it
var/health_deficiency = max(((maxHealth / scaling) - (health / scaling)), (getStaminaLoss()*0.75))
if(health_deficiency >= 40)
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown, TRUE, (health_deficiency-39) / 75)
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying, TRUE, (health_deficiency-39) / 25)
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown, TRUE, (health_deficiency - 15) / 75)
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying, TRUE, (health_deficiency - 15) / 25)
else
remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown)
remove_movespeed_modifier(/datum/movespeed_modifier/damage_slowdown_flying)

View File

@@ -409,7 +409,7 @@
return
var/informed = FALSE
if(isrobotic(src))
apply_status_effect(/datum/status_effect/no_combat_mode/robotic_emp, severity / 20)
apply_status_effect(/datum/status_effect/robotic_emp, severity / 20)
severity *= 0.5
var/do_not_stun = FALSE
if(HAS_TRAIT(src, TRAIT_ROBOTIC_ORGANISM))

View File

@@ -143,7 +143,7 @@
)
parry_efficiency_considered_successful = 0.01
parry_efficiency_to_counterattack = 0.01
parry_efficiency_to_counterattack = INFINITY // no counterattacks
parry_max_attacks = INFINITY
parry_failed_cooldown_duration = 1.5 SECONDS
parry_failed_stagger_duration = 1 SECONDS

View File

@@ -56,7 +56,6 @@
ADD_TRAIT(H, TRAIT_MOBILITY_NOPICKUP, SLIMEPUDDLE_TRAIT)
ADD_TRAIT(H, TRAIT_MOBILITY_NOUSE, SLIMEPUDDLE_TRAIT)
ADD_TRAIT(H, TRAIT_SPRINT_LOCKED, SLIMEPUDDLE_TRAIT)
ADD_TRAIT(H, TRAIT_COMBAT_MODE_LOCKED, SLIMEPUDDLE_TRAIT)
ADD_TRAIT(H, TRAIT_MOBILITY_NOREST, SLIMEPUDDLE_TRAIT)
ADD_TRAIT(H, TRAIT_ARMOR_BROKEN, SLIMEPUDDLE_TRAIT)
H.update_disabled_bodyparts(silent = TRUE) //silently update arms to be paralysed
@@ -99,7 +98,6 @@
REMOVE_TRAIT(H, TRAIT_MOBILITY_NOPICKUP, SLIMEPUDDLE_TRAIT)
REMOVE_TRAIT(H, TRAIT_MOBILITY_NOUSE, SLIMEPUDDLE_TRAIT)
REMOVE_TRAIT(H, TRAIT_SPRINT_LOCKED, SLIMEPUDDLE_TRAIT)
REMOVE_TRAIT(H, TRAIT_COMBAT_MODE_LOCKED, SLIMEPUDDLE_TRAIT)
REMOVE_TRAIT(H, TRAIT_MOBILITY_NOREST, SLIMEPUDDLE_TRAIT)
REMOVE_TRAIT(H, TRAIT_ARMOR_BROKEN, SLIMEPUDDLE_TRAIT)
REMOVE_TRAIT(H, TRAIT_HUMAN_NO_RENDER, SLIMEPUDDLE_TRAIT)

View File

@@ -1589,12 +1589,8 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
var/punchedbrute = target.getBruteLoss()
//CITADEL CHANGES - makes resting and disabled combat mode reduce punch damage, makes being out of combat mode result in you taking more damage
if(!SEND_SIGNAL(target, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
damage *= 1.2
if(!CHECK_MOBILITY(user, MOBILITY_STAND))
damage *= 0.65
if(SEND_SIGNAL(user, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
damage *= 0.8
//END OF CITADEL CHANGES
var/obj/item/bodypart/affecting = target.get_bodypart(ran_zone(user.zone_selected))
@@ -1718,12 +1714,8 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
else
user.do_attack_animation(target, ATTACK_EFFECT_DISARM)
if(HAS_TRAIT(user, TRAIT_PUGILIST))//CITADEL CHANGE - makes disarmspam cause staminaloss, pugilists can do it almost effortlessly
if(!user.UseStaminaBuffer(1, warn = TRUE))
return
else
if(!user.UseStaminaBuffer(1, warn = TRUE))
return
if(!user.UseStaminaBuffer(1, warn = TRUE))
return
if(attacker_style && attacker_style.disarm_act(user,target))
return TRUE
@@ -1741,12 +1733,8 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
log_combat(user, target, "disarmed out of grab from")
return
var/randn = rand(1, 100)
if(SEND_SIGNAL(target, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE)) // CITADEL CHANGE
randn += -10 //CITADEL CHANGE - being out of combat mode makes it easier for you to get disarmed
if(!CHECK_MOBILITY(user, MOBILITY_STAND)) //CITADEL CHANGE
randn += 100 //CITADEL CHANGE - No kosher disarming if you're resting
if(SEND_SIGNAL(user, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE)) //CITADEL CHANGE
randn += 25 //CITADEL CHANGE - Makes it harder to disarm outside of combat mode
if(user.pulling == target)
randn -= 20 //If you have the time to get someone in a grab, you should have a greater chance at snatching the thing in their hand. Will be made completely obsolete by the grab rework but i've got a poor track record for releasing big projects on time so w/e i guess
if(HAS_TRAIT(user, TRAIT_PUGILIST))
@@ -1951,9 +1939,6 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
if(IS_STAMCRIT(user))
to_chat(user, "<span class='warning'>You're too exhausted for that.</span>")
return
if(SEND_SIGNAL(user, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
to_chat(user, "<span class='warning'>You need combat mode to be active to that!</span>")
return
if(user.IsKnockdown() || user.IsParalyzed() || user.IsStun())
to_chat(user, "<span class='warning'>You can't seem to force yourself up right now!</span>")
return
@@ -1993,6 +1978,8 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
if(CHECK_MOBILITY(target, MOBILITY_STAND))
target.adjustStaminaLoss(5)
else
target.adjustStaminaLoss(target.getStaminaLoss() > 75? 5 : 75)
if(target.is_shove_knockdown_blocked())
return
@@ -2035,7 +2022,6 @@ GLOBAL_LIST_EMPTY(roundstart_race_names)
target.visible_message("<span class='danger'>[user.name] shoves [target.name]!</span>",
"<span class='danger'>[user.name] shoves you!</span>", null, COMBAT_MESSAGE_RANGE, null,
user, "<span class='danger'>You shove [target.name]!</span>")
target.Stagger(SHOVE_STAGGER_DURATION)
var/obj/item/target_held_item = target.get_active_held_item()
if(!target.has_status_effect(STATUS_EFFECT_OFF_BALANCE))
if(target_held_item)

View File

@@ -3,7 +3,7 @@
amount = dna.species.spec_stun(src,amount)
return ..()
/mob/living/carbon/human/DefaultCombatKnockdown(amount, updating = TRUE, ignore_canknockdown = FALSE, override_hardstun, override_stamdmg)
/mob/living/carbon/human/DefaultCombatKnockdown(amount, updating = TRUE, ignore_canknockdown = FALSE, override_hardstun, override_stamdmg, knocktofloor)
amount = dna.species.spec_stun(src,amount)
return ..()

View File

@@ -512,9 +512,8 @@ GLOBAL_LIST_INIT(ballmer_windows_me_msg, list("Yo man, what if, we like, uh, put
//this updates all special effects: stun, sleeping, knockdown, druggy, stuttering, etc..
/mob/living/carbon/handle_status_effects()
..()
var/combat_mode = SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_ACTIVE)
if(getStaminaLoss() && !HAS_TRAIT(src, TRAIT_NO_STAMINA_REGENERATION))
adjustStaminaLoss((!CHECK_MOBILITY(src, MOBILITY_STAND) ? ((combat_flags & COMBAT_FLAG_HARD_STAMCRIT) ? STAM_RECOVERY_STAM_CRIT : STAM_RECOVERY_RESTING) : STAM_RECOVERY_NORMAL) * (combat_mode? 0.25 : 1))
adjustStaminaLoss((!CHECK_MOBILITY(src, MOBILITY_STAND) ? ((combat_flags & COMBAT_FLAG_HARD_STAMCRIT) ? STAM_RECOVERY_STAM_CRIT : STAM_RECOVERY_RESTING) : STAM_RECOVERY_NORMAL))
if(!(combat_flags & COMBAT_FLAG_HARD_STAMCRIT) && incomingstammult != 1)
incomingstammult = max(0.01, incomingstammult)

View File

@@ -48,25 +48,23 @@
animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, FALSE, SINE_EASING | EASE_IN)
/mob/living/proc/continue_starting_active_block()
if(SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
return DO_AFTER_STOP
return (combat_flags & COMBAT_FLAG_ACTIVE_BLOCK_STARTING)? DO_AFTER_CONTINUE : DO_AFTER_STOP
/mob/living/get_standard_pixel_x_offset()
. = ..()
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
if(dir & EAST)
. += 8
. += 4
if(dir & WEST)
. -= 8
. -= 4
/mob/living/get_standard_pixel_y_offset()
. = ..()
if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))
if(dir & NORTH)
. += 8
. += 4
if(dir & SOUTH)
. -= 8
. -= 4
/**
* Proc called by keybindings to toggle active blocking.
@@ -100,11 +98,6 @@
if(!I.can_active_block())
to_chat(src, "<span class='warning'>[I] is either not capable of being used to actively block, or is not currently in a state that can! (Try wielding it if it's twohanded, for example.)</span>")
return
// QOL: Attempt to toggle on combat mode if it isn't already
SEND_SIGNAL(src, COMSIG_ENABLE_COMBAT_MODE)
if(SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
to_chat(src, "<span class='warning'>You must be in combat mode to actively block!</span>")
return FALSE
var/datum/block_parry_data/data = I.get_block_parry_data()
var/delay = data.block_start_delay
combat_flags |= COMBAT_FLAG_ACTIVE_BLOCK_STARTING
@@ -147,7 +140,7 @@
/**
* Calculates FINAL ATTACK DAMAGE after mitigation
*/
/obj/item/proc/active_block_calculate_final_damage(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
/obj/item/proc/active_block_calculate_final_damage(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, passive = FALSE)
var/datum/block_parry_data/data = get_block_parry_data()
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)
@@ -156,7 +149,7 @@
if(isnull(absorption))
absorption = data.block_damage_absorption
if(isnull(efficiency))
efficiency = data.block_damage_multiplier
efficiency = data.block_damage_multiplier * (passive? (1 / data.block_automatic_mitigation_multiplier) : 1)
if(isnull(limit))
limit = data.block_damage_limit
// now we calculate damage to reduce.
@@ -172,7 +165,7 @@
return final_damage
/// 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)
/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, passive = FALSE)
var/datum/block_parry_data/data = get_block_parry_data()
var/efficiency = data.attack_type_list_scan(data.block_stamina_efficiency_override, attack_type)
if(isnull(efficiency))
@@ -182,7 +175,7 @@
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) * multiplier
return (damage_blocked / efficiency) * multiplier * (passive? data.block_automatic_stamina_multiplier : 1)
/// Apply the stamina damage to our user, notice how damage argument is stamina_amount.
/obj/item/proc/active_block_do_stamina_damage(mob/living/owner, atom/object, stamina_amount, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return)
@@ -214,6 +207,18 @@
return
/obj/item/proc/active_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction)
return directional_block(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return, override_direction)
/obj/item/proc/can_passive_block()
if(!block_parry_data || !(item_flags & ITEM_CAN_BLOCK))
return FALSE
var/datum/block_parry_data/data = return_block_parry_datum(block_parry_data)
return data.block_automatic_enabled
/obj/item/proc/passive_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction)
return directional_block(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return, override_direction, TRUE)
/obj/item/proc/directional_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction, passive = FALSE)
if(!can_active_block())
return BLOCK_NONE
var/datum/block_parry_data/data = get_block_parry_data()
@@ -228,12 +233,12 @@
incoming_direction = get_dir(get_turf(attacker) || get_turf(object), src)
if(!CHECK_MOBILITY(owner, MOBILITY_STAND) && !(data.block_resting_attack_types_anydir & attack_type) && (!(data.block_resting_attack_types_directional & attack_type) || !can_block_direction(owner.dir, incoming_direction)))
return BLOCK_NONE
else if(!can_block_direction(owner.dir, incoming_direction))
else if(!can_block_direction(owner.dir, incoming_direction, passive))
return BLOCK_NONE
block_return[BLOCK_RETURN_ACTIVE_BLOCK] = TRUE
var/final_damage = active_block_calculate_final_damage(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
var/final_damage = active_block_calculate_final_damage(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return, passive)
var/damage_blocked = damage - final_damage
var/stamina_cost = active_block_stamina_cost(owner, object, damage_blocked, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
var/stamina_cost = active_block_stamina_cost(owner, object, damage_blocked, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return, passive)
active_block_do_stamina_damage(owner, object, stamina_cost, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return)
block_return[BLOCK_RETURN_ACTIVE_BLOCK_DAMAGE_MITIGATED] = damage - final_damage
block_return[BLOCK_RETURN_SET_DAMAGE_TO] = final_damage
@@ -261,9 +266,9 @@
/**
* Gets the block direction bitflags of what we can block.
*/
/obj/item/proc/blockable_directions()
/obj/item/proc/blockable_directions(passive = FALSE)
var/datum/block_parry_data/data = get_block_parry_data()
return data.can_block_directions
return (!isnull(data.block_automatic_directions) && passive)? data.block_automatic_directions : data.can_block_directions
/**
* Checks if we can block from a specific direction from our direction.
@@ -272,14 +277,14 @@
* * our_dir - our direction.
* * their_dir - their direction. Must be a single direction, or NONE for an attack from the same tile. This is incoming direction.
*/
/obj/item/proc/can_block_direction(our_dir, their_dir)
/obj/item/proc/can_block_direction(our_dir, their_dir, passive = FALSE)
their_dir = turn(their_dir, 180)
if(our_dir != NORTH)
var/turn_angle = dir2angle(our_dir)
// dir2angle(), ss13 proc is clockwise so dir2angle(EAST) == 90
// turn(), byond proc is counterclockwise so turn(NORTH, 90) == WEST
their_dir = turn(their_dir, turn_angle)
return (DIR2BLOCKDIR(their_dir) & blockable_directions())
return (DIR2BLOCKDIR(their_dir) & blockable_directions(passive))
/**
* can_block_direction but for "compound" directions to check all of them and return the number of directions that were blocked.

View File

@@ -14,18 +14,58 @@
/**
* Initiates a parrying sequence.
*/
/mob/living/proc/initiate_parry_sequence()
/mob/living/proc/initiate_parry_sequence(silent = FALSE, list/override_method_data)
if(parrying)
return // already parrying
return FALSE // already parrying
if(!(mobility_flags & MOBILITY_USE))
to_chat(src, "<span class='warning'>You can't move your arms!</span>")
return
if(!silent)
to_chat(src, "<span class='warning'>You can't move your arms!</span>")
return FALSE
if(!(combat_flags & COMBAT_FLAG_PARRY_CAPABLE))
to_chat(src, "<span class='warning'>You are not something that can parry attacks.</span>")
return
if(!silent)
to_chat(src, "<span class='warning'>You are not something that can parry attacks.</span>")
return FALSE
if(!(mobility_flags & MOBILITY_STAND))
to_chat(src, "<span class='warning'>You aren't able to parry without solid footing!</span>")
return
if(!silent)
to_chat(src, "<span class='warning'>You aren't able to parry without solid footing!</span>")
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, "<span class='warning'>You are not ready to parry (again)!</span>")
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/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()
@@ -55,7 +95,8 @@
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)
to_chat(src, "<span class='warning'>Something is preventing you from parrying!</span>")
if(!silent)
to_chat(src, "<span class='warning'>Something is preventing you from parrying!</span>")
return
if(length(override))
var/datum/thing = override[1]
@@ -72,35 +113,10 @@
method = ITEM_PARRY
data = using_item.block_parry_data
if(!method)
to_chat(src, "<span class='warning'>You have nothing to parry with!</span>")
if(!silent)
to_chat(src, "<span class='warning'>You have nothing to parry with!</span>")
return FALSE
//QOL: Try to enable combat mode if it isn't already
SEND_SIGNAL(src, COMSIG_ENABLE_COMBAT_MODE)
if(SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
to_chat(src, "<span class='warning'>You must be in combat mode to parry!</span>")
return FALSE
data = return_block_parry_datum(data)
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))
to_chat(src, "<span class='warning'>You are not ready to parry (again)!</span>")
return FALSE
// Point of no return, make sure everything is set.
parrying = method
if(method == ITEM_PARRY)
active_parry_item = using_item
if(!UseStaminaBuffer(data.parry_stamina_cost, TRUE))
return FALSE
parry_start_time = world.time
successful_parries = list()
addtimer(CALLBACK(src, .proc/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
return list(method, data, tool)
/**
* Tries to find a backup parry item.
@@ -146,6 +162,7 @@
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.
@@ -178,17 +195,17 @@
/**
* 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)
/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)
/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)
/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.
@@ -205,6 +222,94 @@
*/
/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.mouseObject
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.
*/
@@ -235,12 +340,20 @@
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())
/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)
return BLOCK_NONE
// 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) || (attack_type & ATTACK_TYPE_PARRY_COUNTERATTACK))) // if this attack is from a parry do not parry it lest we infinite loop.
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)
@@ -281,7 +394,7 @@
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)
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)
@@ -289,8 +402,9 @@
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)
var/datum/block_parry_data/data = get_parry_data()
/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
@@ -299,12 +413,14 @@
visible_message("<span class='danger'>[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" : ""]!</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)
/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
var/datum/block_parry_data/data = get_parry_data()
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)

View File

@@ -37,6 +37,8 @@
var/results
if(I == active_block_item)
results = I.active_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list, attack_direction)
else if(I.can_passive_block() && !SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))
results = I.passive_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list, attack_direction)
else
results = I.run_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list)
. |= results

View File

@@ -75,7 +75,7 @@ GLOBAL_LIST_EMPTY(block_parry_data)
var/block_stamina_buffer_ratio = 1
/// Stamina dealt directly via UseStaminaBuffer() per SECOND of block.
var/block_stamina_cost_per_second = 1.5
var/block_stamina_cost_per_second = 1
/// Prevent stamina buffer regeneration while block?
var/block_no_stambuffer_regeneration = TRUE
/// Prevent stamina regeneration while block?
@@ -93,20 +93,31 @@ GLOBAL_LIST_EMPTY(block_parry_data)
/// Sounds for blocking
var/list/block_sounds = list('sound/block_parry/block_metal1.ogg' = 1, 'sound/block_parry/block_metal1.ogg' = 1)
// Autoblock
// Other than for overrides, this mostly just reads from the above vars
/// Can this item automatically block?
var/block_automatic_enabled = TRUE
/// Directions that you can autoblock in. Null to default to normal directions.
var/block_automatic_directions = null
/// Effectiveness multiplier for automated block. Only applies to efficiency, absorption and limits stay the same!
var/block_automatic_mitigation_multiplier = 0.33
/// Stamina cost multiplier for automated block
var/block_automatic_stamina_multiplier = 1
/////////// PARRYING ////////////
/// Prioriry for [mob/do_run_block()] while we're being used to parry.
/// Priority 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 = TRUE
var/parry_respect_clickdelay = FALSE
/// Parry stamina cost
var/parry_stamina_cost = 5
/// Attack types we can block
var/parry_attack_types = ALL
/// Parry flags
var/parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK | PARRY_LOCK_ATTACKING
var/parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK
/// Parry windup duration in deciseconds. 0 to this is windup, afterwards is main stage.
var/parry_time_windup = 2
var/parry_time_windup = 0
/// 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]
@@ -139,7 +150,7 @@ GLOBAL_LIST_EMPTY(block_parry_data)
/// Efficiency must be at least this to be considered successful
var/parry_efficiency_considered_successful = 0.1
/// Efficiency must be at least this to run automatic counterattack
var/parry_efficiency_to_counterattack = 0.1
var/parry_efficiency_to_counterattack = INFINITY
/// Maximum attacks to parry successfully or unsuccessfully (but not efficiency < 0) during active period, hitting this immediately ends the sequence.
var/parry_max_attacks = INFINITY
/// Visual icon state override for parrying
@@ -153,7 +164,7 @@ GLOBAL_LIST_EMPTY(block_parry_data)
/// Stagger duration post-parry if you fail to parry an attack
var/parry_failed_stagger_duration = 3.5 SECONDS
/// Clickdelay duration post-parry if you fail to parry an attack
var/parry_failed_clickcd_duration = 2 SECONDS
var/parry_failed_clickcd_duration = 0 SECONDS
/// Parry cooldown post-parry if failed. This is ADDED to parry_cooldown!!!
var/parry_failed_cooldown_duration = 0 SECONDS
@@ -166,6 +177,29 @@ GLOBAL_LIST_EMPTY(block_parry_data)
var/perfect_parry_block_return_list
var/imperfect_parry_block_return_list
var/failed_parry_block_return_list
/// Allow multiple counterattacks per parry sequence. Bad idea.
var/parry_allow_repeated_counterattacks = FALSE
// Auto parry
// Anything not specified like cooldowns/clickdelay respecting is pulled from above.
/// Can this data automatically parry? This is off by default because this is something that requires thought to balance.
var/parry_automatic_enabled = FALSE
/// Hard autoparry cooldown
var/autoparry_cooldown_absolute = 7.5 SECONDS
/// Autoparry : Simulate a parry sequence starting at a certain tick, or simply simulate a single attack parry?
var/autoparry_sequence_simulation = FALSE
// Single attack simulation:
/// Single attack autoparry - efficiency
var/autoparry_single_efficiency = 75
/// Single attack autoparry - efficiency overrides by attack type, see above
var/list/autoparry_single_efficiency_override
// Parry sequence simulation:
/// Decisecond of sequence to start on. -1 to start to 0th tick of active parry window.
var/autoparry_sequence_start_time = -1
// Clickdelay/cooldown settings not included, as well as whether or not to lock attack/sprinting/etc. They will be pulled from the above.
/// ADVANCED - Autoparry requirement for time since last moused over for a specific object
var/autoparry_mouse_delay_maximum = 0.35 SECONDS
/**
* Quirky proc to get average of flags in list that are in attack_type because why is attack_type a flag.
@@ -308,6 +342,7 @@ GLOBAL_LIST_EMPTY(block_parry_data)
RENDER_VARIABLE_SIMPLE(parry_cooldown, "Deciseconds it has to be since the last time a parry sequence <b>ended</b> for you before you can parry again.")
RENDER_VARIABLE_SIMPLE(parry_failed_stagger_duration, "Deciseconds you are staggered for at the of the parry sequence if you do not successfully parry anything.")
RENDER_VARIABLE_SIMPLE(parry_failed_clickcd_duration, "Deciseconds you are put on attack cooldown at the end of the parry sequence if you do not successfully parry anything.")
dat += ""
dat += "</div></table>"
return dat.Join("")
#undef RENDER_VARIABLE_SIMPLE

View File

@@ -50,8 +50,12 @@
var/obj/effect/abstract/parry/parry_visual_effect
/// world.time of last parry end
var/parry_end_time_last = 0
/// Last autoparry
var/last_autoparry = 0
/// Successful parries within the current parry cycle. It's a list of efficiency percentages.
var/list/successful_parries
/// Current parry counterattacks. Makes sure we can only counterattack someone once per parry.
var/list/successful_parry_counterattacks
var/confused = 0 //Makes the mob move in random directions.

View File

@@ -267,16 +267,10 @@ GLOBAL_LIST_INIT(department_radio_keys, list(
eavesdrop_range = EAVESDROP_EXTRA_RANGE
var/list/listening = get_hearers_in_view(message_range+eavesdrop_range, source)
var/list/the_dead = list()
var/list/yellareas //CIT CHANGE - adds the ability for yelling to penetrate walls and echo throughout areas
if(!eavesdrop_range && say_test(message) == "2") //CIT CHANGE - ditto
yellareas = get_areas_in_range(message_range*0.5, source) //CIT CHANGE - ditto
for(var/_M in GLOB.player_list)
var/mob/M = _M
if(M.stat != DEAD) //not dead, not important
if(yellareas) //CIT CHANGE - see above. makes yelling penetrate walls
var/area/A = get_area(M) //CIT CHANGE - ditto
if(istype(A) && A.ambientsounds != SPACE && (A in yellareas)) //CIT CHANGE - ditto
listening |= M //CIT CHANGE - ditto
continue
if(!M.client || !client) //client is so that ghosts don't have to listen to mice
continue
@@ -303,6 +297,9 @@ GLOBAL_LIST_INIT(department_radio_keys, list(
AM.Hear(rendered, src, message_language, message, null, spans, message_mode, source)
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_LIVING_SAY_SPECIAL, src, message)
if(!eavesdrop_range && say_test(message) == "2") // Yell hook
process_yelling(listening, rendered, src, message_language, message, spans, message_mode, source)
//speech bubble
var/list/speech_bubble_recipients = list()
for(var/mob/M in listening)
@@ -312,6 +309,30 @@ GLOBAL_LIST_INIT(department_radio_keys, list(
I.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
INVOKE_ASYNC(GLOBAL_PROC, /.proc/flick_overlay, I, speech_bubble_recipients, 30)
/atom/movable/proc/process_yelling(list/already_heard, rendered, atom/movable/speaker, datum/language/message_language, message, list/spans, message_mode, obj/source)
if(last_yell > (world.time - 10))
to_chat(src, "<span class='warning'>Your voice doesn't project as far as you try to yell in such quick succession.") // yeah no, no spamming an expensive floodfill.
return
last_yell = world.time
var/list/overhearing = list()
var/list/overhearing_text = list()
overhearing = yelling_wavefill(src, yell_power)
if(!overhearing.len)
overhearing_text = "none"
else
for(var/mob/M as anything in overhearing)
overhearing_text += key_name(M)
overhearing_text = english_list(overhearing_text)
log_say("YELL: [ismob(src)? key_name(src) : src] yelled [message] with overhearing mobs [overhearing_text]")
// overhearing = get_hearers_in_view(35, src) | get_hearers_in_range(5, src)
overhearing -= already_heard
if(!overhearing.len)
return
// to_chat(world, "DEBUG: overhearing [english_list(overhearing)]")
for(var/_AM in overhearing)
var/atom/movable/AM = _AM
AM.Hear(rendered, speaker, message_language, message, null, spans, message_mode, source)
/mob/proc/binarycheck()
return FALSE

View File

@@ -35,11 +35,10 @@
return
CONFIG_CACHE_ENTRY_AND_FETCH_VALUE(number/stamina_combat/out_of_combat_timer, out_of_combat_timer)
CONFIG_CACHE_ENTRY_AND_FETCH_VALUE(number/stamina_combat/base_regeneration, base_regeneration)
CONFIG_CACHE_ENTRY_AND_FETCH_VALUE(number/stamina_combat/combat_regeneration, combat_regeneration)
CONFIG_CACHE_ENTRY_AND_FETCH_VALUE(number/stamina_combat/percent_regeneration_out_of_combat, percent_regeneration_out_of_combat)
CONFIG_CACHE_ENTRY_AND_FETCH_VALUE(number/stamina_combat/post_action_penalty_delay, post_action_penalty_delay)
CONFIG_CACHE_ENTRY_AND_FETCH_VALUE(number/stamina_combat/post_action_penalty_factor, post_action_penalty_factor)
var/base_regen = (SEND_SIGNAL(src, COMSIG_COMBAT_MODE_CHECK, COMBAT_MODE_INACTIVE))? base_regeneration : combat_regeneration
var/base_regen = base_regeneration
var/time_since_last_action = world.time - stamina_buffer_last_use
var/action_penalty = ((time_since_last_action) < (post_action_penalty_delay * 10))? post_action_penalty_factor : 1
var/out_of_combat_bonus = (time_since_last_action < (out_of_combat_timer * 10))? 0 : ((buffer_max * percent_regeneration_out_of_combat * 0.01))

View File

@@ -4,7 +4,8 @@
// ignore_castun = same logic as Paralyze() in general
// override_duration = If this is set, does Paralyze() for this duration.
// override_stam = If this is set, does this amount of stamina damage.
/mob/living/proc/DefaultCombatKnockdown(amount, updating = TRUE, ignore_canknockdown = FALSE, override_hardstun, override_stamdmg)
// knocktofloor - whether to knock them to the ground
/mob/living/proc/DefaultCombatKnockdown(amount, updating = TRUE, ignore_canknockdown = FALSE, override_hardstun, override_stamdmg, knocktofloor = TRUE)
if(!iscarbon(src))
return Paralyze(amount, updating, ignore_canknockdown)
if(!ignore_canknockdown && !(status_flags & CANKNOCKDOWN))
@@ -13,7 +14,8 @@
buckled.unbuckle_mob(src)
var/drop_items = amount > 80 //80 is cutoff for old item dropping behavior
var/stamdmg = isnull(override_stamdmg)? (amount * 0.25) : override_stamdmg
KnockToFloor(drop_items, TRUE, updating)
if(knocktofloor)
KnockToFloor(drop_items, TRUE, updating)
adjustStaminaLoss(stamdmg)
if(!isnull(override_hardstun))
Paralyze(override_hardstun)

View File

@@ -45,3 +45,9 @@
/datum/movespeed_modifier/status_effect/mkultra
multiplicative_slowdown = -2
blacklisted_movetypes= FLYING|FLOATING
/datum/movespeed_modifier/status_effect/stagger
variable = TRUE
/datum/movespeed_modifier/status_effect/off_balance
variable = TRUE

View File

@@ -118,7 +118,7 @@ Contents:
ADD_TRAIT(n_hood, TRAIT_NODROP, NINJA_SUIT_TRAIT)
n_shoes = H.shoes
ADD_TRAIT(n_shoes, TRAIT_NODROP, NINJA_SUIT_TRAIT)
n_shoes.slowdown--
n_shoes.slowdown -= 0.5
n_gloves = H.gloves
ADD_TRAIT(n_gloves, TRAIT_NODROP, NINJA_SUIT_TRAIT)
return TRUE
@@ -139,7 +139,7 @@ Contents:
REMOVE_TRAIT(n_hood, TRAIT_NODROP, NINJA_SUIT_TRAIT)
if(n_shoes)
REMOVE_TRAIT(n_shoes, TRAIT_NODROP, NINJA_SUIT_TRAIT)
n_shoes.slowdown++
n_shoes.slowdown += 0.5
if(n_gloves)
n_gloves.icon_state = "s-ninja"
n_gloves.item_state = "s-ninja"

View File

@@ -2,6 +2,9 @@
projectile_type = /obj/item/projectile/beam/laser
select_name = "kill"
/obj/item/ammo_casing/energy/laser/minigun
click_cooldown_override = 2
/obj/item/ammo_casing/energy/lasergun
projectile_type = /obj/item/projectile/beam/laser
e_cost = 83

View File

@@ -9,6 +9,10 @@
projectile_type = /obj/item/projectile/energy/electrode/security
e_cost = 100
/obj/item/ammo_casing/energy/electrode/hos
projectile_type = /obj/item/projectile/energy/electrode/security/hos
e_cost = 100
/obj/item/ammo_casing/energy/electrode/spec
e_cost = 100
@@ -16,7 +20,6 @@
fire_sound = 'sound/weapons/gunshot.ogg'
e_cost = 100
/obj/item/ammo_casing/energy/electrode/old
e_cost = 1000

View File

@@ -3,9 +3,3 @@
ammo_type = /obj/item/ammo_casing/caseless/magspear
caliber = "speargun"
max_ammo = 1
/obj/item/ammo_box/magazine/internal/minigun
name = "gatling gun fusion core"
ammo_type = /obj/item/ammo_casing/caseless/laser/gatling
caliber = "gatling"
max_ammo = 5000

View File

@@ -48,12 +48,24 @@
/obj/item/gun/energy/e_gun/hos
name = "\improper X-01 MultiPhase Energy Gun"
desc = "This is an expensive, modern recreation of an antique laser gun. This gun has several unique firemodes, but lacks the ability to recharge over time in exchange for inbuilt advanced firearm EMP shielding."
desc = "This is an expensive, modern recreation of an antique laser gun. This gun has several unique firemodes, but lacks the ability to recharge over time in exchange for inbuilt advanced firearm EMP shielding. <span class='boldnotice'>Right click in combat mode to fire a taser shot with a cooldown.</span>"
icon_state = "hoslaser"
force = 10
ammo_type = list(/obj/item/ammo_casing/energy/disabler, /obj/item/ammo_casing/energy/laser/hos, /obj/item/ammo_casing/energy/ion/hos)
ammo_type = list(/obj/item/ammo_casing/energy/disabler, /obj/item/ammo_casing/energy/laser/hos, /obj/item/ammo_casing/energy/ion/hos, /obj/item/ammo_casing/energy/electrode/hos)
ammo_x_offset = 4
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF
var/last_altfire = 0
var/altfire_delay = 0
/obj/item/gun/energy/e_gun/hos/altafterattack(atom/target, mob/user, proximity_flag, params)
. = TRUE
if(last_altfire + altfire_delay > world.time)
return
var/current_index = current_firemode_index
set_firemode_to_type(/obj/item/ammo_casing/energy/electrode)
process_afterattack(target, user, proximity_flag, params)
set_firemode_index(current_index)
last_altfire = world.time
/obj/item/gun/energy/e_gun/hos/emp_act(severity)
return

View File

@@ -11,19 +11,19 @@
righthand_file = 'icons/mob/inhands/equipment/backpack_righthand.dmi'
slot_flags = ITEM_SLOT_BACK
w_class = WEIGHT_CLASS_HUGE
var/obj/item/gun/ballistic/minigun/gun
var/obj/item/gun/energy/minigun/gun
var/armed = 0 //whether the gun is attached, 0 is attached, 1 is the gun is wielded.
var/overheat = 0
var/overheat_max = 40
var/overheat_max = 50
var/heat_diffusion = 1
/obj/item/minigunpack/Initialize()
. = ..()
gun = new(src)
START_PROCESSING(SSobj, src)
START_PROCESSING(SSfastprocess, src)
/obj/item/minigunpack/Destroy()
STOP_PROCESSING(SSobj, src)
STOP_PROCESSING(SSfastprocess, src)
return ..()
/obj/item/minigunpack/process()
@@ -72,7 +72,6 @@
var/obj/screen/inventory/hand/H = over_object
M.putItemFromInventoryInHandIfPossible(src, H.held_index)
/obj/item/minigunpack/update_icon_state()
if(armed)
icon_state = "notholstered"
@@ -91,8 +90,7 @@
update_icon()
user.update_inv_back()
/obj/item/gun/ballistic/minigun
/obj/item/gun/energy/minigun
name = "laser gatling gun"
desc = "An advanced laser cannon with an incredible rate of fire. Requires a bulky backpack power source to use."
icon = 'icons/obj/guns/minigun.dmi'
@@ -103,17 +101,20 @@
slot_flags = null
w_class = WEIGHT_CLASS_HUGE
custom_materials = null
burst_size = 3
automatic = 0
fire_delay = 1
automatic = 0.5
fire_delay = 2
ammo_type = list(
/obj/item/ammo_casing/energy/laser
)
weapon_weight = WEAPON_HEAVY
fire_sound = 'sound/weapons/laser.ogg'
mag_type = /obj/item/ammo_box/magazine/internal/minigun
casing_ejector = FALSE
charge_sections = 0
shaded_charge = 0
item_flags = NEEDS_PERMIT | SLOWS_WHILE_IN_HAND
var/obj/item/minigunpack/ammo_pack
/obj/item/gun/ballistic/minigun/Initialize()
/obj/item/gun/energy/minigun/Initialize()
if(istype(loc, /obj/item/minigunpack)) //We should spawn inside an ammo pack so let's use that one.
ammo_pack = loc
else
@@ -121,29 +122,29 @@
return ..()
/obj/item/gun/ballistic/minigun/attack_self(mob/living/user)
/obj/item/gun/energy/minigun/attack_self(mob/living/user)
return
/obj/item/gun/ballistic/minigun/dropped(mob/user)
/obj/item/gun/energy/minigun/dropped(mob/user)
. = ..()
if(ammo_pack)
ammo_pack.attach_gun(user)
else
qdel(src)
/obj/item/gun/ballistic/minigun/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0, stam_cost = 0)
/obj/item/gun/energy/minigun/process_fire(atom/target, mob/living/user, message = TRUE, params = null, zone_override = "", bonus_spread = 0, stam_cost = 0)
if(ammo_pack)
if(ammo_pack.overheat < ammo_pack.overheat_max)
ammo_pack.overheat += burst_size
ammo_pack.overheat++
..()
else
to_chat(user, "The gun's heat sensor locked the trigger to prevent lens damage.")
/obj/item/gun/ballistic/minigun/afterattack(atom/target, mob/living/user, flag, params)
/obj/item/gun/energy/minigun/afterattack(atom/target, mob/living/user, flag, params)
if(!ammo_pack || ammo_pack.loc != user)
to_chat(user, "You need the backpack power source to fire the gun!")
. = ..()
/obj/item/gun/ballistic/minigun/dropped(mob/living/user)
/obj/item/gun/energy/minigun/dropped(mob/living/user)
. = ..()
ammo_pack.attach_gun(user)

View File

@@ -37,15 +37,17 @@
..()
/obj/item/projectile/energy/electrode/security
tase_duration = 30
tase_duration = 40
knockdown = 0
stamina = 10
stamina = 0
knockdown_stamoverride = 0
knockdown_stam_max = 0
strong_tase = FALSE
/obj/item/projectile/energy/electrode/security/hos
knockdown = 100
knockdown_stamoverride = 30
knockdown_stam_max = null
tase_duration = 10
tase_duration = 40
knockdown = 0
stamina = 0
knockdown_stamoverride = 0
knockdown_stam_max = 0
strong_tase = FALSE

View File

@@ -18,7 +18,8 @@
var/spray_range = 3 //the range of tiles the sprayer will reach when in spray mode.
var/stream_range = 1 //the range of tiles the sprayer will reach when in stream mode.
var/stream_amount = 10 //the amount of reagents transfered when in stream mode.
var/spray_delay = 3 //The amount of sleep() delay between each chempuff step.
/// Amount of time it takes for a spray to completely travel.
var/spray_delay = 8
/// Last world.time of spray
var/last_spray = 0
/// Spray cooldown
@@ -72,58 +73,20 @@
if((last_spray + spray_cooldown) > world.time)
return
var/range = clamp(get_dist(src, A), 1, current_range)
var/obj/effect/decal/chempuff/D = new /obj/effect/decal/chempuff(get_turf(src))
D.create_reagents(amount_per_transfer_from_this, NONE, NO_REAGENTS_VALUE)
var/puff_reagent_left = range //how many turf, mob or dense objet we can react with before we consider the chem puff consumed
if(stream_mode)
reagents.trans_to(D, amount_per_transfer_from_this)
puff_reagent_left = 1
else
reagents.trans_to(D, amount_per_transfer_from_this, 1/range)
D.color = mix_color_from_reagents(D.reagents.reagent_list)
var/wait_step = CEILING(spray_delay * INVERSE(range), world.tick_lag)
var/obj/effect/decal/chempuff/D = new /obj/effect/decal/chempuff(get_turf(src), stream_mode, wait_step, range, stream_mode? 1 : range)
var/turf/T = get_turf(src)
if(!T)
return
log_reagent("SPRAY: [key_name(usr)] fired [src] ([REF(src)]) [COORD(T)] at [A] ([REF(A)]) [COORD(A)] (chempuff: [D.reagents.log_list()])")
var/wait_step = max(round(2+ spray_delay * INVERSE(range)), 2)
D.create_reagents(amount_per_transfer_from_this, NONE, NO_REAGENTS_VALUE)
if(stream_mode)
reagents.trans_to(D, amount_per_transfer_from_this)
else
reagents.trans_to(D, amount_per_transfer_from_this, 1/range)
D.color = mix_color_from_reagents(D.reagents.reagent_list)
last_spray = world.time
INVOKE_ASYNC(src, .proc/do_spray, A, wait_step, D, range, puff_reagent_left)
return TRUE
/obj/item/reagent_containers/spray/proc/do_spray(atom/A, wait_step, obj/effect/decal/chempuff/D, range, puff_reagent_left)
var/range_left = range
for(var/i=0, i<range, i++)
range_left--
step_towards(D,A)
sleep(wait_step)
for(var/atom/T in get_turf(D))
if(T == D || T.invisibility) //we ignore the puff itself and stuff below the floor
continue
if(puff_reagent_left <= 0)
break
if(stream_mode)
if(ismob(T))
var/mob/M = T
if(!M.lying || !range_left)
D.reagents.reaction(M, VAPOR)
puff_reagent_left -= 1
else if(!range_left)
D.reagents.reaction(T, VAPOR)
else
D.reagents.reaction(T, VAPOR)
if(ismob(T))
puff_reagent_left -= 1
if(puff_reagent_left > 0 && (!stream_mode || !range_left))
D.reagents.reaction(get_turf(D), VAPOR)
puff_reagent_left -= 1
if(puff_reagent_left <= 0) // we used all the puff so we delete it.
qdel(D)
return
qdel(D)
D.run_puff(A)
/obj/item/reagent_containers/spray/attack_self(mob/user)
stream_mode = !stream_mode
@@ -207,7 +170,7 @@
righthand_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
volume = 40
stream_range = 4
spray_delay = 1
spray_delay = 2
amount_per_transfer_from_this = 5
list_reagents = list(/datum/reagent/consumable/condensedcapsaicin = 40)

View File

@@ -209,7 +209,7 @@
parry_time_perfect = 2
parry_time_perfect_leeway = 0.75
parry_imperfect_falloff_percent = 7.5
parry_efficiency_to_counterattack = 100
parry_efficiency_to_counterattack = INFINITY
parry_efficiency_considered_successful = 80
parry_efficiency_perfect = 120
parry_failed_stagger_duration = 3 SECONDS
@@ -266,14 +266,15 @@
parry_time_perfect = 2
parry_time_perfect_leeway = 2
parry_failed_stagger_duration = 3 SECONDS
parry_failed_clickcd_duration = 3 SECONDS
parry_time_windup = 0
parry_time_spindown = 0
parry_imperfect_falloff_percent = 0
parry_efficiency_to_counterattack = 100
parry_efficiency_to_counterattack = INFINITY
parry_efficiency_considered_successful = 120
parry_efficiency_perfect = 120
parry_data = list(PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN = 4)
parry_automatic_enabled = TRUE
autoparry_single_efficiency = 75
//unique hammers
/obj/item/melee/smith/hammer/toolbox

View File

@@ -32,6 +32,8 @@
var/max_stamina_damage = 0
var/incoming_stam_mult = 1 //Multiplier for incoming staminaloss, decreases when taking staminaloss when the limb is disabled, resets back to 1 when limb is no longer disabled.
var/max_damage = 0
/// Threshold at which we are disabled. Defaults to max_damage if unset.
var/disable_threshold
var/stam_heal_tick = 0 //per Life(). Defaults to 0 due to citadel changes
var/brute_reduction = 0 //Subtracted to brute damage taken
@@ -505,6 +507,12 @@
return
set_disabled(is_disabled(silent), silent)
/**
* Gets the damage at which point we're disabled.
*/
/obj/item/bodypart/proc/get_disable_threshold()
return isnull(disable_threshold)? max_damage : disable_threshold
/obj/item/bodypart/proc/is_disabled(silent = FALSE)
if(!owner)
return
@@ -514,15 +522,16 @@
var/datum/wound/W = i
if(W.disabling)
return BODYPART_DISABLED_WOUND
var/disable_threshold = get_disable_threshold()
if(can_dismember() && !HAS_TRAIT(owner, TRAIT_NODISMEMBER))
. = disabled //inertia, to avoid limbs healing 0.1 damage and being re-enabled
if(get_damage(TRUE) >= max_damage * (HAS_TRAIT(owner, TRAIT_EASYLIMBDISABLE) ? 0.6 : 1)) //Easy limb disable disables the limb at 40% health instead of 0%
if(get_damage(TRUE) >= disable_threshold * (HAS_TRAIT(owner, TRAIT_EASYLIMBDISABLE) ? 0.6 : 1)) //Easy limb disable disables the limb at 40% health instead of 0%
if(!last_maxed && !silent)
owner.emote("scream")
last_maxed = TRUE
if(!is_organic_limb(FALSE) || stamina_dam >= max_damage)
if(!is_organic_limb(FALSE) || stamina_dam >= disable_threshold)
return BODYPART_DISABLED_DAMAGE
else if(disabled && (get_damage(TRUE) <= (max_damage * 0.8))) // reenabled at 80% now instead of 50% as of wounds update
else if(disabled && (get_damage(TRUE) <= (disable_threshold * 0.8))) // reenabled at 80% now instead of 50% as of wounds update
last_maxed = FALSE
return BODYPART_NOT_DISABLED
else

View File

@@ -59,12 +59,13 @@
one though."
icon_state = "default_human_l_arm"
attack_verb = list("slapped", "punched")
max_damage = 50
max_damage = 150
disable_threshold = 75
max_stamina_damage = 50
body_zone = BODY_ZONE_L_ARM
body_part = ARM_LEFT
aux_icons = list(BODY_ZONE_PRECISE_L_HAND = HANDS_PART_LAYER, "l_hand_behind" = BODY_BEHIND_LAYER)
body_damage_coeff = 0.75
body_damage_coeff = 0.25
held_index = 1
px_x = -6
px_y = 0
@@ -120,11 +121,12 @@
among humans missing their right arm."
icon_state = "default_human_r_arm"
attack_verb = list("slapped", "punched")
max_damage = 50
max_damage = 150
disable_threshold = 75
body_zone = BODY_ZONE_R_ARM
body_part = ARM_RIGHT
aux_icons = list(BODY_ZONE_PRECISE_R_HAND = HANDS_PART_LAYER, "r_hand_behind" = BODY_BEHIND_LAYER)
body_damage_coeff = 0.75
body_damage_coeff = 0.25
held_index = 2
px_x = 6
px_y = 0
@@ -182,10 +184,11 @@
luck. In this instance, it probably would not have helped."
icon_state = "default_human_l_leg"
attack_verb = list("kicked", "stomped")
max_damage = 50
max_damage = 150
disable_threshold = 75
body_zone = BODY_ZONE_L_LEG
body_part = LEG_LEFT
body_damage_coeff = 0.75
body_damage_coeff = 0.25
px_x = -2
px_y = 12
stam_heal_tick = STAM_RECOVERY_LIMB
@@ -240,10 +243,11 @@
// alternative spellings of 'pokey' are availible
icon_state = "default_human_r_leg"
attack_verb = list("kicked", "stomped")
max_damage = 50
max_damage = 150
disable_threshold = 75
body_zone = BODY_ZONE_R_LEG
body_part = LEG_RIGHT
body_damage_coeff = 0.75
body_damage_coeff = 0.25
px_x = 2
px_y = 12
max_stamina_damage = 50