diff --git a/code/__DEFINES/flags.dm b/code/__DEFINES/_flags/_flags.dm similarity index 97% rename from code/__DEFINES/flags.dm rename to code/__DEFINES/_flags/_flags.dm index abc0507bf4..8c8e03d865 100644 --- a/code/__DEFINES/flags.dm +++ b/code/__DEFINES/_flags/_flags.dm @@ -137,3 +137,7 @@ GLOBAL_LIST_INIT(bitflags, list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 204 #define MOBILITY_FLAGS_DEFAULT (MOBILITY_MOVE | MOBILITY_STAND | MOBILITY_PICKUP | MOBILITY_USE | MOBILITY_UI | MOBILITY_STORAGE | MOBILITY_PULL | MOBILITY_RESIST) #define MOBILITY_FLAGS_ANY_INTERACTION (MOBILITY_USE | MOBILITY_PICKUP | MOBILITY_UI | MOBILITY_STORAGE) + +// melee_attack_chain() attackchain_flags +/// The attack is from a parry counterattack. +#define ATTACKCHAIN_PARRY_COUNTERATTACK (1<<0) diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/_flags/item_flags.dm similarity index 74% rename from code/__DEFINES/obj_flags.dm rename to code/__DEFINES/_flags/item_flags.dm index a904d7a7e6..c6a3886ed3 100644 --- a/code/__DEFINES/obj_flags.dm +++ b/code/__DEFINES/_flags/item_flags.dm @@ -1,20 +1,3 @@ -// Flags for the obj_flags var on /obj - - -#define EMAGGED (1<<0) -#define IN_USE (1<<1) //If we have a user using us, this will be set on. We will check if the user has stopped using us, and thus stop updating and LAGGING EVERYTHING! -#define CAN_BE_HIT (1<<2) //can this be bludgeoned by items? -#define BEING_SHOCKED (1<<3) //Whether this thing is currently (already) being shocked by a tesla -#define DANGEROUS_POSSESSION (1<<4) //Admin possession yes/no -#define ON_BLUEPRINTS (1<<5) //Are we visible on the station blueprints at roundstart? -#define UNIQUE_RENAME (1<<6) //can you customize the description/name of the thing? -#define USES_TGUI (1<<7) //put on things that use tgui on ui_interact instead of custom/old UI. -#define FROZEN (1<<8) -#define SHOVABLE_ONTO (1<<9) //called on turf.shove_act() to consider whether an object should have a niche effect (defined in their own shove_act()) when someone is pushed onto it, or do a sanity CanPass() check. -#define BLOCK_Z_FALL (1<<10) - -// If you add new ones, be sure to add them to /obj/Initialize as well for complete mapping support - // Flags for the item_flags var on /obj/item #define BEING_REMOVED (1<<0) @@ -41,6 +24,10 @@ #define NO_UNIFORM_REQUIRED (1<<11) ///Damage when attacking people is not affected by combat mode. #define NO_COMBAT_MODE_FORCE_MODIFIER (1<<12) +/// This item can be used to parry. Only a basic check used to determine if we should proceed with parry chain at all. +#define ITEM_CAN_PARRY (1<<13) +/// This item can be used in the directional blocking system. Only a basic check used to determine if we should proceed with directional block handling at all. +#define ITEM_CAN_BLOCK (1<<14) // Flags for the clothing_flags var on /obj/item/clothing diff --git a/code/__DEFINES/_flags/obj_flags.dm b/code/__DEFINES/_flags/obj_flags.dm new file mode 100644 index 0000000000..ebb9b4bda0 --- /dev/null +++ b/code/__DEFINES/_flags/obj_flags.dm @@ -0,0 +1,15 @@ +// Flags for the obj_flags var on /obj + +#define EMAGGED (1<<0) +#define IN_USE (1<<1) //If we have a user using us, this will be set on. We will check if the user has stopped using us, and thus stop updating and LAGGING EVERYTHING! +#define CAN_BE_HIT (1<<2) //can this be bludgeoned by items? +#define BEING_SHOCKED (1<<3) //Whether this thing is currently (already) being shocked by a tesla +#define DANGEROUS_POSSESSION (1<<4) //Admin possession yes/no +#define ON_BLUEPRINTS (1<<5) //Are we visible on the station blueprints at roundstart? +#define UNIQUE_RENAME (1<<6) //can you customize the description/name of the thing? +#define USES_TGUI (1<<7) //put on things that use tgui on ui_interact instead of custom/old UI. +#define FROZEN (1<<8) +#define SHOVABLE_ONTO (1<<9) //called on turf.shove_act() to consider whether an object should have a niche effect (defined in their own shove_act()) when someone is pushed onto it, or do a sanity CanPass() check. +#define BLOCK_Z_FALL (1<<10) + +// If you add new ones, be sure to add them to /obj/Initialize as well for complete mapping support diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm index eb3c1e32ef..e348781bcc 100644 --- a/code/__DEFINES/combat.dm +++ b/code/__DEFINES/combat.dm @@ -29,10 +29,11 @@ #define EFFECT_DROWSY "drowsy" #define EFFECT_JITTER "jitter" +// mob/living/var/combat_flags variable. /// Default combat flags for those affected by sprinting (combat mode has been made into its own component) -#define COMBAT_FLAGS_DEFAULT NONE +#define COMBAT_FLAGS_DEFAULT (COMBAT_FLAG_PARRY_CAPABLE | COMBAT_FLAG_BLOCK_CAPABLE) /// Default combat flags for everyone else (so literally everyone but humans). -#define COMBAT_FLAGS_SPRINT_EXEMPT (COMBAT_FLAG_SPRINT_ACTIVE | COMBAT_FLAG_SPRINT_TOGGLED | COMBAT_FLAG_SPRINT_FORCED) +#define COMBAT_FLAGS_SPRINT_EXEMPT (COMBAT_FLAG_SPRINT_ACTIVE | COMBAT_FLAG_SPRINT_TOGGLED | COMBAT_FLAG_SPRINT_FORCED | COMBAT_FLAG_PARRY_CAPABLE | COMBAT_FLAG_BLOCK_CAPABLE) /// The user wants sprint mode on #define COMBAT_FLAG_SPRINT_TOGGLED (1<<0) @@ -50,6 +51,16 @@ #define COMBAT_FLAG_SOFT_STAMCRIT (1<<6) /// Force sprint mode on at all times, overrides everything including sprint disable traits. #define COMBAT_FLAG_SPRINT_FORCED (1<<7) +/// This mob is capable of using the active parrying system. +#define COMBAT_FLAG_PARRY_CAPABLE (1<<8) +/// This mob is capable of using the active blocking system. +#define COMBAT_FLAG_BLOCK_CAPABLE (1<<9) +/// This mob is capable of unarmed parrying +#define COMBAT_FLAG_UNARMED_PARRY (1<<10) +/// This mob is currently actively blocking +#define COMBAT_FLAG_ACTIVE_BLOCKING (1<<11) +/// This mob is currently starting an active block +#define COMBAT_FLAG_ACTIVE_BLOCK_STARTING (1<<12) // Helpers for getting someone's stamcrit state. Cast to living. #define NOT_STAMCRIT 0 @@ -112,18 +123,6 @@ #define GRAB_NECK 2 #define GRAB_KILL 3 -/// Attack types for check_block()/run_block(). Flags, combinable. -/// Attack was melee, whether or not armed. -#define ATTACK_TYPE_MELEE (1<<0) -/// Attack was with a gun or something that should count as a gun (but not if a gun shouldn't count for a gun, crazy right?) -#define ATTACK_TYPE_PROJECTILE (1<<1) -/// Attack was unarmed.. this usually means hand to hand combat. -#define ATTACK_TYPE_UNARMED (1<<2) -/// Attack was a thrown atom hitting the victim. -#define ATTACK_TYPE_THROWN (1<<3) -/// Attack was a bodyslam/leap/tackle. See: Xenomorph leap tackles. -#define ATTACK_TYPE_TACKLE (1<<4) - //attack visual effects #define ATTACK_EFFECT_PUNCH "punch" #define ATTACK_EFFECT_KICK "kick" @@ -281,64 +280,3 @@ GLOBAL_LIST_INIT(shove_disarming_types, typecacheof(list( #define BULLET_ACT_BLOCK "BLOCK" //It's a blocked hit, whatever that means in the context of the thing it's hitting. #define BULLET_ACT_FORCE_PIERCE "PIERCE" //It pierces through the object regardless of the bullet being piercing by default. #define BULLET_ACT_TURF "TURF" //It hit us but it should hit something on the same turf too. Usually used for turfs. - -/// Check whether or not we can block, without "triggering" a block. Basically run checks without effects like depleting shields. -/// Wrapper for do_run_block(). The arguments on that means the same as for this. -#define mob_check_block(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)\ - do_run_block(FALSE, object, damage, attack_text, attack_type, armour_penetration, attacker, check_zone(def_zone), return_list) - -/// Runs a block "sequence", effectively checking and then doing effects if necessary. -/// Wrapper for do_run_block(). The arguments on that means the same as for this. -#define mob_run_block(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)\ - do_run_block(TRUE, object, damage, attack_text, attack_type, armour_penetration, attacker, check_zone(def_zone), return_list) - -/// Bitflags for check_block() and run_block(). Meant to be combined. You can be hit and still reflect, for example, if you do not use BLOCK_SUCCESS. -/// Attack was not blocked -#define BLOCK_NONE NONE -/// Attack was blocked, do not do damage. THIS FLAG MUST BE THERE FOR DAMAGE/EFFECT PREVENTION! -#define BLOCK_SUCCESS (1<<1) - -/// The below are for "metadata" on "how" the attack was blocked. - -/// Attack was and should be redirected according to list argument REDIRECT_METHOD (NOTE: the SHOULD here is important, as it says "the thing blocking isn't handling the reflecting for you so do it yourself"!) -#define BLOCK_SHOULD_REDIRECT (1<<2) -/// Attack was redirected (whether by us or by SHOULD_REDIRECT flagging for automatic handling) -#define BLOCK_REDIRECTED (1<<3) -/// Attack was blocked by something like a shield. -#define BLOCK_PHYSICAL_EXTERNAL (1<<4) -/// Attack was blocked by something worn on you. -#define BLOCK_PHYSICAL_INTERNAL (1<<5) -/// Attack outright missed because the target dodged. Should usually be combined with redirection passthrough or something (see martial arts) -#define BLOCK_TARGET_DODGED (1<<7) -/// Meta-flag for run_block/do_run_block : By default, BLOCK_SUCCESS tells do_run_block() to assume the attack is completely blocked and not continue the block chain. If this is present, it will continue to check other items in the chain rather than stopping. -#define BLOCK_CONTINUE_CHAIN (1<<8) - -/// For keys in associative list/block_return as we don't want to saturate our (somewhat) limited flags. -#define BLOCK_RETURN_REDIRECT_METHOD "REDIRECT_METHOD" - /// Pass through victim - #define REDIRECT_METHOD_PASSTHROUGH "passthrough" - /// Deflect at randomish angle - #define REDIRECT_METHOD_DEFLECT "deflect" - /// reverse 180 angle, basically (as opposed to "realistic" wall reflections) - #define REDIRECT_METHOD_REFLECT "reflect" - /// "do not taser the bad man with the desword" - actually aims at the firer/attacker rather than just reversing - #define REDIRECT_METHOD_RETURN_TO_SENDER "no_you" - -/// These keys are generally only applied to the list if real_attack is FALSE. Used incase we want to make "smarter" mob AI in the future or something. -/// Tells the caller how likely from 0 (none) to 100 (always) we are to reflect energy projectiles -#define BLOCK_RETURN_REFLECT_PROJECTILE_CHANCE "reflect_projectile_chance" -/// Tells the caller how likely we are to block attacks from 0 to 100 in general -#define BLOCK_RETURN_NORMAL_BLOCK_CHANCE "normal_block_chance" -/// Tells the caller about how many hits we can soak on average before our blocking fails. -#define BLOCK_RETURN_BLOCK_CAPACITY "block_capacity" - -/// Default if the above isn't set in the list. -#define DEFAULT_REDIRECT_METHOD_PROJECTILE REDIRECT_METHOD_DEFLECT - -/// Block priorities -#define BLOCK_PRIORITY_HELD_ITEM 100 -#define BLOCK_PRIORITY_WEAR_SUIT 75 -#define BLOCK_PRIORITY_CLOTHING 50 -#define BLOCK_PRIORITY_UNIFORM 25 - -#define BLOCK_PRIORITY_DEFAULT BLOCK_PRIORITY_HELD_ITEM diff --git a/code/__DEFINES/combat/attack_types.dm b/code/__DEFINES/combat/attack_types.dm new file mode 100644 index 0000000000..a401c0f2ee --- /dev/null +++ b/code/__DEFINES/combat/attack_types.dm @@ -0,0 +1,32 @@ +// Attack types for check_block()/run_block(). Flags, combinable. +/// Attack was melee, whether or not armed. +#define ATTACK_TYPE_MELEE (1<<0) +/// Attack was with a gun or something that should count as a gun (but not if a gun shouldn't count for a gun, crazy right?) +#define ATTACK_TYPE_PROJECTILE (1<<1) +/// Attack was unarmed.. this usually means hand to hand combat. +#define ATTACK_TYPE_UNARMED (1<<2) +/// Attack was a thrown atom hitting the victim. +#define ATTACK_TYPE_THROWN (1<<3) +/// Attack was a bodyslam/leap/tackle. See: Xenomorph leap tackles. +#define ATTACK_TYPE_TACKLE (1<<4) +/// Attack was from a parry counterattack. Do not attempt to parry-this! +#define ATTACK_TYPE_PARRY_COUNTERATTACK (1<<5) + +// Requires for datum definitions to not error with must be a constant statement when used in lists as text associative keys. +// KEEP IN SYNC WITH ABOVE! + +#define TEXT_ATTACK_TYPE_MELEE "1" +#define TEXT_ATTACK_TYPE_PROJECTILE "2" +#define TEXT_ATTACK_TYPE_UNARMED "4" +#define TEXT_ATTACK_TYPE_THROWN "8" +#define TEXT_ATTACK_TYPE_TACKLE "16" +#define TEXT_ATTACK_TYPE_PARRY_COUNTERATTACK "32" + +GLOBAL_LIST_INIT(attack_type_names, list( + TEXT_ATTACK_TYPE_MELEE = "Melee", + TEXT_ATTACK_TYPE_PROJECTILE = "Projectile", + TEXT_ATTACK_TYPE_UNARMED = "Unarmed", + TEXT_ATTACK_TYPE_THROWN = "Thrown", + TEXT_ATTACK_TYPE_TACKLE = "Tackle", + TEXT_ATTACK_TYPE_PARRY_COUNTERATTACK = "Parry Counterattack" +)) diff --git a/code/__DEFINES/combat/block.dm b/code/__DEFINES/combat/block.dm new file mode 100644 index 0000000000..bc85947d71 --- /dev/null +++ b/code/__DEFINES/combat/block.dm @@ -0,0 +1,80 @@ +/// Check whether or not we can block, without "triggering" a block. Basically run checks without effects like depleting shields. +/// Wrapper for do_run_block(). The arguments on that means the same as for this. +#define mob_check_block(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)\ + do_run_block(FALSE, object, damage, attack_text, attack_type, armour_penetration, attacker, check_zone(def_zone), return_list) + +/// Runs a block "sequence", effectively checking and then doing effects if necessary. +/// Wrapper for do_run_block(). The arguments on that means the same as for this. +#define mob_run_block(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list)\ + do_run_block(TRUE, object, damage, attack_text, attack_type, armour_penetration, attacker, check_zone(def_zone), return_list) + +// Don't ask why there's block_parry.dm and this. This is for the run_block() system, which is the "parent" system of the directional block and parry systems. + +/// Bitflags for check_block() and handle_block(). Meant to be combined. You can be hit and still reflect, for example, if you do not use BLOCK_SUCCESS. +/// Attack was not blocked +#define BLOCK_NONE NONE +/// Attack was blocked, do not do damage. THIS FLAG MUST BE THERE FOR DAMAGE/EFFECT PREVENTION! +#define BLOCK_SUCCESS (1<<1) + +/// The below are for "metadata" on "how" the attack was blocked. + +/// Attack was and should be redirected according to list argument REDIRECT_METHOD (NOTE: the SHOULD here is important, as it says "the thing blocking isn't handling the reflecting for you so do it yourself"!) +#define BLOCK_SHOULD_REDIRECT (1<<2) +/// Attack was redirected (whether by us or by SHOULD_REDIRECT flagging for automatic handling) +#define BLOCK_REDIRECTED (1<<3) +/// Attack was blocked by something like a shield. +#define BLOCK_PHYSICAL_EXTERNAL (1<<4) +/// Attack was blocked by something worn on you. +#define BLOCK_PHYSICAL_INTERNAL (1<<5) +/// Attack outright missed because the target dodged. Should usually be combined with redirection passthrough or something (see martial arts) +#define BLOCK_TARGET_DODGED (1<<7) +/// Meta-flag for run_block/do_run_block : By default, BLOCK_SUCCESS tells do_run_block() to assume the attack is completely blocked and not continue the block chain. If this is present, it will continue to check other items in the chain rather than stopping. +#define BLOCK_CONTINUE_CHAIN (1<<8) +/// Attack should change the amount of damage incurred. This means something calling run_block() has to handle it! +#define BLOCK_SHOULD_CHANGE_DAMAGE (1<<9) +/// Attack should scale by this percent, 0 for no block and 100 for full blocked +#define BLOCK_SHOULD_PARTIAL_MITIGATE (1<<10) + +/// For keys in associative list/block_return as we don't want to saturate our (somewhat) limited flags. +#define BLOCK_RETURN_REDIRECT_METHOD "REDIRECT_METHOD" + /// Pass through victim + #define REDIRECT_METHOD_PASSTHROUGH "passthrough" + /// Deflect at randomish angle + #define REDIRECT_METHOD_DEFLECT "deflect" + /// reverse 180 angle, basically (as opposed to "realistic" wall reflections) + #define REDIRECT_METHOD_REFLECT "reflect" + /// "do not taser the bad man with the desword" - actually aims at the firer/attacker rather than just reversing + #define REDIRECT_METHOD_RETURN_TO_SENDER "no_you" + +/// These keys are generally only applied to the list if real_attack is FALSE. Used incase we want to make "smarter" mob AI in the future or something. +/// Tells the caller how likely from 0 (none) to 100 (always) we are to reflect energy projectiles +#define BLOCK_RETURN_REFLECT_PROJECTILE_CHANCE "reflect_projectile_chance" +/// Tells the caller how likely we are to block attacks from 0 to 100 in general +#define BLOCK_RETURN_NORMAL_BLOCK_CHANCE "normal_block_chance" +/// Tells the caller about how many hits we can soak on average before our blocking fails. +#define BLOCK_RETURN_BLOCK_CAPACITY "block_capacity" +/// Tells the caller we got blocked by active directional block. +#define BLOCK_RETURN_ACTIVE_BLOCK "active_block" +/// Tells the caller our damage mitigation for their attack. +#define BLOCK_RETURN_ACTIVE_BLOCK_DAMAGE_MITIGATED "damage_mitigated" +/// For [BLOCK_CHANGE_DAMAGE]. Set damage to this. +#define BLOCK_RETURN_SET_DAMAGE_TO "set_damage_to" +/// For [BLOCK_SHOULD_PARTIAL_MITIGATE]. Percentage mitigation. +#define BLOCK_RETURN_MITIGATION_PERCENT "partial_mitigation" +/// Used internally by run_parry proc, use on an on_active_parry() proc to override parrying efficiency. +#define BLOCK_RETURN_OVERRIDE_PARRY_EFFICIENCY "override_parry_efficiency" +/// Always set to 100 by run_block() if BLOCK_SUCCESS is in return value. Otherwise, defaults to mitigation percent if not set. Used by projectile/proc/on_hit(). +#define BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE "projectile_block_percentage" + +/// Default if the above isn't set in the list. +#define DEFAULT_REDIRECT_METHOD_PROJECTILE REDIRECT_METHOD_DEFLECT + +/// Block priorities. Higher means it's checked sooner. +// THESE MUST NEVER BE 0! Block code uses ! instead of isnull for the speed boost. +#define BLOCK_PRIORITY_ACTIVE_BLOCK 200 +#define BLOCK_PRIORITY_HELD_ITEM 100 +#define BLOCK_PRIORITY_CLOTHING 50 +#define BLOCK_PRIORITY_WEAR_SUIT 75 +#define BLOCK_PRIORITY_UNIFORM 25 + +#define BLOCK_PRIORITY_DEFAULT BLOCK_PRIORITY_HELD_ITEM diff --git a/code/__DEFINES/combat/block_parry.dm b/code/__DEFINES/combat/block_parry.dm new file mode 100644 index 0000000000..d48d7c8713 --- /dev/null +++ b/code/__DEFINES/combat/block_parry.dm @@ -0,0 +1,72 @@ +// We can't determine things like NORTHEAST vs NORTH *and* EAST without making our own flags :( +#define BLOCK_DIR_NORTH (1<<0) +#define BLOCK_DIR_NORTHEAST (1<<1) +#define BLOCK_DIR_NORTHWEST (1<<2) +#define BLOCK_DIR_WEST (1<<3) +#define BLOCK_DIR_EAST (1<<4) +#define BLOCK_DIR_SOUTH (1<<5) +#define BLOCK_DIR_SOUTHEAST (1<<6) +#define BLOCK_DIR_SOUTHWEST (1<<7) +#define BLOCK_DIR_ONTOP (1<<8) + +GLOBAL_LIST_INIT(dir2blockdir, list( + "[NORTH]" = BLOCK_DIR_NORTH, + "[NORTHEAST]" = BLOCK_DIR_NORTHEAST, + "[NORTHWEST]" = BLOCK_DIR_NORTHWEST, + "[WEST]" = BLOCK_DIR_WEST, + "[EAST]" = BLOCK_DIR_EAST, + "[SOUTH]" = BLOCK_DIR_SOUTH, + "[SOUTHEAST]" = BLOCK_DIR_SOUTHEAST, + "[SOUTHWEST]" = BLOCK_DIR_SOUTHWEST, + "[NONE]" = BLOCK_DIR_ONTOP + )) + +#define DIR2BLOCKDIR(d) (GLOB.dir2blockdir["[d]"]) + +GLOBAL_LIST_INIT(block_direction_names, list( + "[BLOCK_DIR_NORTH]" = "Front", + "[BLOCK_DIR_NORTHEAST]" = "Front Right", + "[BLOCK_DIR_NORTHWEST]" = "Front Left", + "[BLOCK_DIR_WEST]" = "Left", + "[BLOCK_DIR_EAST]" = "Right", + "[BLOCK_DIR_SOUTH]" = "Behind", + "[BLOCK_DIR_SOUTHEAST]" = "Behind Right", + "[BLOCK_DIR_SOUTHWEST]" = "Behind Left", + "[BLOCK_DIR_ONTOP]" = "Ontop" +)) + +/// If this is the value of active_block_starting it signals we want to interrupt the start +#define ACTIVE_BLOCK_STARTING_INTERRUPT "INTERRUPT" + +/// ""types"" of parry "items" +#define UNARMED_PARRY "unarmed" +#define MARTIAL_PARRY "martial" +#define ITEM_PARRY "item" + +/// Parry phase we're in +#define NOT_PARRYING 0 +#define PARRY_WINDUP 1 +#define PARRY_ACTIVE 2 +#define PARRY_SPINDOWN 3 + +// /datum/block_parry_data/var/parry_flags +/// Default handling for audio/visual feedback +#define PARRY_DEFAULT_HANDLE_FEEDBACK (1<<0) +/// Lock sprinting while parrying +#define PARRY_LOCK_SPRINTING (1<<1) +/// Lock attacking while parrying +#define PARRY_LOCK_ATTACKING (1<<2) + +/// Parry effects. +/// Automatically melee attacks back normally, LMB equivalent action of an harm intent attack. List association should be defaulting to 1, being the attack damage multiplier for said counterattack +#define PARRY_COUNTERATTACK_MELEE_ATTACK_CHAIN "melee_counterattack_chain" +/// List association should be TRUE. +#define PARRY_DISARM_ATTACKER "disarm_attacker" +/// List association should be duration or null for just plain knockdown. +#define PARRY_KNOCKDOWN_ATTACKER "knockdown_attacker" +/// List association should be duration. +#define PARRY_STAGGER_ATTACKER "stagger_attacker" +/// List association should be amount of time to daze attacker. +#define PARRY_DAZE_ATTACKER "daze_attacker" +/// Set to TRUE in list association to ignore adjacency checks +#define PARRY_COUNTERATTACK_IGNORE_ADJACENCY "ignore_adjacency" diff --git a/code/__DEFINES/flags/do_after.dm b/code/__DEFINES/flags/do_after.dm new file mode 100644 index 0000000000..26802736cf --- /dev/null +++ b/code/__DEFINES/flags/do_after.dm @@ -0,0 +1,32 @@ +/// Requires absolute stillness from the user +#define DO_AFTER_DISALLOW_MOVING_ABSOLUTE_USER (1<<0) +/// Requires absolute stillness from the target +#define DO_AFTER_DISALLOW_MOVING_ABSOLUTE_TARGET (1<<1) +/// Requires that the user is on a turf. +#define DO_AFTER_REQUIRES_USER_ON_TURF (1<<2) +/// Requires relative stillness to our target via dx and dy coordinate difference but only if both are spacedrifting. Specify DO_AFTER_ALLOW_NONSPACEDRIFT_RELATIVITY to say otherwise. +#define DO_AFTER_DISALLOW_MOVING_RELATIVE (1<<3) +/// Breaks if active hand item changes. Requires a tool be specified, otherwise defaults to active item +#define DO_AFTER_DISALLOW_ACTIVE_ITEM_CHANGE (1<<4) +/// Breaks if the user has no free hands. If a tool is specified, allows that as well. +#define DO_AFTER_REQUIRE_FREE_HAND_OR_TOOL (1<<5) +/// Do not display progressbar. +#define DO_AFTER_NO_PROGRESSBAR (1<<6) +/// Do not check do_after_coefficient() +#define DO_AFTER_NO_COEFFICIENT (1<<7) +/// For relative stillness, allow non spacedrift relative movement +#define DO_AFTER_ALLOW_NONSPACEDRIFT_RELATIVITY (1<<8) + +/// Ignores checks. +#define DO_AFTER_PROCEED "PROCEED" +/// Uses all other checks +#define DO_AFTER_CONTINUE "CONTINUE" +/// Breaks +#define DO_AFTER_STOP "STOP" + +/// Stage - initiating a do_after +#define DO_AFTER_STARTING 1 +/// Stage - main loop of a do_after +#define DO_AFTER_PROGRESSING 2 +/// Stage - Last check of a do_after +#define DO_AFTER_FINISHING 3 diff --git a/code/__DEFINES/movespeed_modification.dm b/code/__DEFINES/movespeed_modification.dm index 1f3ae63940..699f39e79f 100644 --- a/code/__DEFINES/movespeed_modification.dm +++ b/code/__DEFINES/movespeed_modification.dm @@ -8,5 +8,68 @@ //ids #define MOVESPEED_ID_SANITY "mood_sanity" -#define MOVESPEED_ID_MOB_GRAB_STATE "mob_grab_state" +#define MOVESPEED_ID_MOB_WALK_RUN_CONFIG_SPEED "MOB_WALK_RUN" +#define MOVESPEED_ID_MOB_GRAB_STATE "MOB_GRAB_STATE" +#define MOVESPEED_ID_MOB_EQUIPMENT "MOB_EQUIPMENT" +#define MOVESPEED_ID_MOB_GRAVITY "MOB_GRAVITY" +#define MOVESPEED_ID_CONFIG_SPEEDMOD "MOB_CONFIG_MODIFIER" + +#define MOVESPEED_ID_SLIME_REAGENTMOD "SLIME_REAGENT_MODIFIER" +#define MOVESPEED_ID_SLIME_HEALTHMOD "SLIME_HEALTH_MODIFIER" +#define MOVESPEED_ID_SLIME_TEMPMOD "SLIME_TEMPERATURE_MODIFIER" + +#define MOVESPEED_ID_SLIME_STATUS "SLIME_STATUS" + +#define MOVESPEED_ID_TARANTULA_WEB "TARANTULA_WEB" + +#define MOVESPEED_ID_LIVING_TURF_SPEEDMOD "LIVING_TURF_SPEEDMOD" +#define MOVESPEED_ID_LIVING_LIMBLESS "LIVING_LIMBLESS" + +#define MOVESPEED_ID_CARBON_SOFTCRIT "CARBON_SOFTCRIT" +#define MOVESPEED_ID_CARBON_OLDSPEED "CARBON_DEPRECATED_SPEED" + +#define MOVESPEED_ID_DNA_VAULT "DNA_VAULT" + +#define MOVESPEED_ID_YELLOW_ORB "YELLOW_ORB" + +#define MOVESPEED_ID_TARFOOT "TARFOOT" + +#define MOVESPEED_ID_SEPIA "SEPIA" + +#define MOVESPEED_ID_MONKEY_REAGENT_SPEEDMOD "MONKEY_REAGENT_SPEEDMOD" +#define MOVESPEED_ID_MONKEY_TEMPERATURE_SPEEDMOD "MONKEY_TEMPERATURE_SPEEDMOD" +#define MOVESPEED_ID_MONKEY_HEALTH_SPEEDMOD "MONKEY_HEALTH_SPEEDMOD" + +#define MOVESPEED_ID_CHANGELING_MUSCLES "CHANGELING_MUSCLES" + +#define MOVESPEED_ID_SIMPLEMOB_VARSPEED "SIMPLEMOB_VARSPEED_MODIFIER" +#define MOVESPEED_ID_ADMIN_VAREDIT "ADMIN_VAREDIT_MODIFIER" + +#define MOVESPEED_ID_PAI_SPACEWALK_SPEEDMOD "PAI_SPACEWALK_MODIFIER" + +#define MOVESPEED_ID_SPECIES "SPECIES_SPEED_MOD" + +#define MOVESPEED_ID_PRONE_DRAGGING "PRONE_DRAG" +#define MOVESPEED_ID_HUMAN_CARRYING "HUMAN_CARRY" +#define MOVESPEED_ID_SHRINK_RAY "SHRUNKEN_SPEED_MODIFIER" + +#define MOVESPEED_ID_SLAUGHTER "SLAUGHTER" + +#define MOVESPEED_ID_CYBER_THRUSTER "CYBER_IMPLANT_THRUSTER" +#define MOVESPEED_ID_JETPACK "JETPACK" + +#define MOVESPEED_ID_MKULTRA "MKULTRA" + +#define MOVESPEED_ID_TASED_STATUS "TASED" +#define MOVESPEED_ID_ELECTROSTAFF "ELECTROSTAFF" + +#define MOVESPEED_ID_SHOVE "SHOVE" +#define MOVESPEED_ID_FAT "FAT" +#define MOVESPEED_ID_COLD "COLD" +#define MOVESPEED_ID_HUNGRY "HUNGRY" +#define MOVESPEED_ID_DAMAGE_SLOWDOWN "DAMAGE" +#define MOVESPEED_ID_DAMAGE_SLOWDOWN_FLYING "FLYING" + +#define MOVESPEED_ID_ACTIVE_BLOCK "ACTIVE_BLOCK" + #define MOVESPEED_ID_MOB_WALK_RUN "mob_walk_run" diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 74470cf72e..ef3b2c54c3 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -295,3 +295,7 @@ #define CLOWNOP_TRAIT "clown-op" #define MEGAFAUNA_TRAIT "megafauna" #define DEATHSQUAD_TRAIT "deathsquad" +/// This trait is added by the active directional block system. +#define ACTIVE_BLOCK_TRAIT "active_block" +/// This trait is added by the parry system. +#define ACTIVE_PARRY_TRAIT "active_parry" diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index 9b877e8fb0..5fb4d05d42 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -104,7 +104,7 @@ GLOBAL_VAR_INIT(cmp_field, "name") var/a_sign = num2sign(initial(A.value) * -1) var/b_sign = num2sign(initial(B.value) * -1) - // Neutral traits go last. + // Neutral traits go last if(a_sign == 0) a_sign = 2 if(b_sign == 0) diff --git a/code/__HELPERS/do_after.dm b/code/__HELPERS/do_after.dm new file mode 100644 index 0000000000..f1f483c345 --- /dev/null +++ b/code/__HELPERS/do_after.dm @@ -0,0 +1,323 @@ +/** + * Higher overhead "advanced" version of do_after. + * @params + * - atom/user is the atom doing the action or the "physical" user + * - delay is time in deciseconds + * - atom/target is the atom the action is being done to, defaults to user + * - do_after_flags see __DEFINES/flags/do_after.dm for details. + * - datum/callback/extra_checks - Every time this ticks, extra_checks() is invoked with (user, delay, target, time_left, do_after_flags, required_mobility_flags, required_combat_flags, mob_redirect, stage, initially_held_item, tool). + * Stage can be DO_AFTER_STARTING, DO_AFTER_PROGRESSING, DO_AFTER_FINISHING + * If it returns DO_AFTER_STOP, this breaks. + * If it returns nothing, all other checks are done. + * If it returns DO_AFTER_PROCEED, all other checks are ignored. + * - required_mobility_flags is checked with CHECK_ALL_MOBILITY. Will immediately fail if the user isn't a mob. + * - requried_combat_flags is checked with CHECK_MULTIPLE_BITFIELDS. Will immediately fail if the user isn't a mob. + * - mob/living/mob_redirect - advanced option: If this is specified, movement and mobility/combat flag checks will use this instead of user. Progressbars will also go to this. + * - obj/item/tool - The tool we're using. See do_after flags for details. + */ +#define INVOKE_CALLBACK cb_return = extra_checks?.Invoke(user, delay, target, world.time - starttime, do_after_flags, required_mobility_flags, required_combat_flags, mob_redirect, stage, initially_held_item, tool) +#define CHECK_FLAG_FAILURE ((required_mobility_flags || required_combat_flags) && (!living_user || (required_mobility_flags && !CHECK_ALL_MOBILITY(living_user, required_mobility_flags)) || (required_combat_flags && !CHECK_MULTIPLE_BITFIELDS(living_user.combat_flags, required_combat_flags)))) +#define TIMELEFT (world.time - starttime) +/proc/do_after_advanced(atom/user, delay, atom/target, do_after_flags, datum/callback/extra_checks, required_mobility_flags, required_combat_flags, mob/living/mob_redirect, obj/item/tool) + // CHECK AND SET VARIABLES + if(!user) + return FALSE + if(!target) + target = user + if((user.loc == null) || (target.loc == null)) + return FALSE + var/mob/living/living_user = mob_redirect + if(!living_user && isliving(user)) + living_user = user + var/stage = DO_AFTER_STARTING + var/startlocuser = user.loc + var/startloctarget = target.loc + var/turf/userturf = get_turf(user) + var/turf/targetturf = get_turf(target) + if(!userturf || !targetturf) + return FALSE + if((do_after_flags & DO_AFTER_REQUIRES_USER_ON_TURF) && !isturf(user.loc)) + return FALSE + var/starttime = world.time + var/endtime = world.time + delay + var/obj/item/initially_held_item = mob_redirect?.get_active_held_item() + if(!(do_after_flags & DO_AFTER_NO_COEFFICIENT) && living_user) + delay *= living_user.do_after_coefficent() + var/atom/movable/AM_user = ismovable(user) && user + var/drifting = AM_user?.Process_Spacemove(NONE) && AM_user.inertia_dir + var/initial_dx = targetturf.x - userturf.x + var/initial_dy = targetturf.y - userturf.y + var/dx = initial_dx + var/dy = initial_dy + // DO OUR STARTING CHECKS + var/cb_return + INVOKE_CALLBACK + if(cb_return == DO_AFTER_STOP) + return FALSE + else if(cb_return != DO_AFTER_PROCEED) + if(CHECK_FLAG_FAILURE) + return FALSE + // SETUP LOOP + var/datum/progressbar/progbar + if(living_user) + if(!(do_after_flags & DO_AFTER_NO_PROGRESSBAR)) + progbar = new(living_user, delay, target) + // MAIN LOOP + . = TRUE + if(!delay) + return + var/obj/item/held + var/locchanged + var/ctu + var/ctt + while(world.time < endtime) + stoplag(1) + progbar?.update(TIMELEFT) + if(QDELETED(user) || QDELETED(target) || (user.loc == null) || (target.loc == null)) + . = FALSE + break + INVOKE_CALLBACK + if(cb_return == DO_AFTER_STOP) + . = FALSE + break + else if(cb_return == DO_AFTER_PROCEED) + continue + // otherwise, go through our normal checks. + if(((do_after_flags & DO_AFTER_DISALLOW_MOVING_ABSOLUTE_USER) && (user.loc != startlocuser)) || ((do_after_flags & DO_AFTER_DISALLOW_MOVING_ABSOLUTE_TARGET) && (target.loc != startloctarget))) + . = FALSE + break + else if(do_after_flags & DO_AFTER_DISALLOW_MOVING_RELATIVE) + ctu = get_turf(user) + ctt = get_turf(target) + locchanged = (userturf != ctu) || (targetturf != ctt) + userturf = ctu + targetturf = ctt + dx = targetturf.x - userturf.x + dy = targetturf.y - userturf.y + if((dx != initial_dx) || (dy != initial_dy)) + . = FALSE + break + if(locchanged && !drifting && !(do_after_flags & DO_AFTER_ALLOW_NONSPACEDRIFT_RELATIVITY)) + . = FALSE + break + if(!AM_user.inertia_dir) + drifting = FALSE + if((do_after_flags & DO_AFTER_REQUIRES_USER_ON_TURF) && !isturf(user.loc)) + return FALSE + if(CHECK_FLAG_FAILURE) + . = FALSE + break + held = living_user?.get_active_held_item() + if((do_after_flags & DO_AFTER_DISALLOW_ACTIVE_ITEM_CHANGE) && (held != (tool || initially_held_item))) + . = FALSE + break + if((do_after_flags & DO_AFTER_REQUIRE_FREE_HAND_OR_TOOL) && (!living_user?.is_holding(tool) && !length(living_user?.get_empty_held_indexes()))) + . = FALSE + break + + // CLEANUP + qdel(progbar) + // If we failed, just return. + if(!.) + return FALSE + // DO FINISHING CHECKS + if(QDELETED(user) || QDELETED(target)) + return FALSE + INVOKE_CALLBACK + if(cb_return == DO_AFTER_STOP) + return FALSE + else if(cb_return == DO_AFTER_PROCEED) + return TRUE + if(CHECK_FLAG_FAILURE) + return FALSE + if(((do_after_flags & DO_AFTER_DISALLOW_MOVING_ABSOLUTE_USER) && (user.loc != startlocuser)) || ((do_after_flags & DO_AFTER_DISALLOW_MOVING_ABSOLUTE_TARGET) && (target.loc != startloctarget))) + return FALSE + else if(do_after_flags & DO_AFTER_DISALLOW_MOVING_RELATIVE) + ctu = get_turf(user) + ctt = get_turf(target) + locchanged = (userturf != ctu) || (targetturf != ctt) + userturf = ctu + targetturf = ctt + dx = targetturf.x - userturf.x + dy = targetturf.y - userturf.y + if((dx != initial_dx) || (dy != initial_dy)) + return FALSE + if(locchanged && !drifting && !(do_after_flags & DO_AFTER_ALLOW_NONSPACEDRIFT_RELATIVITY)) + return FALSE + if((do_after_flags & DO_AFTER_REQUIRES_USER_ON_TURF) && !isturf(user.loc)) + return FALSE + held = living_user?.get_active_held_item() + if((do_after_flags & DO_AFTER_DISALLOW_ACTIVE_ITEM_CHANGE) && (held != (tool || initially_held_item))) + return FALSE + if((do_after_flags & DO_AFTER_REQUIRE_FREE_HAND_OR_TOOL) && (!living_user?.is_holding(tool) && !length(living_user?.get_empty_held_indexes()))) + return FALSE + +#undef INVOKE_CALLBACK +#undef CHECK_FLAG_FAILURE + +/proc/do_mob(mob/user , mob/target, time = 30, uninterruptible = 0, progress = 1, datum/callback/extra_checks = null, ignorehelditem = FALSE, resume_time = 0 SECONDS) + if(!user || !target) + return 0 + var/user_loc = user.loc + + var/drifting = 0 + if(!user.Process_Spacemove(0) && user.inertia_dir) + drifting = 1 + + var/target_loc = target.loc + + var/holding = user.get_active_held_item() + var/datum/progressbar/progbar + if (progress) + progbar = new(user, time, target) + + var/endtime = world.time+time + var/starttime = world.time + . = 1 + while (world.time + resume_time < endtime) + stoplag(1) + if (progress) + progbar.update(world.time - starttime + resume_time) + if(QDELETED(user) || QDELETED(target)) + . = 0 + break + if(uninterruptible) + continue + + if(drifting && !user.inertia_dir) + drifting = 0 + user_loc = user.loc + + if((!drifting && user.loc != user_loc) || target.loc != target_loc || (!ignorehelditem && user.get_active_held_item() != holding) || user.incapacitated() || user.lying || (extra_checks && !extra_checks.Invoke())) + . = 0 + break + if (progress) + qdel(progbar) + + +//some additional checks as a callback for for do_afters that want to break on losing health or on the mob taking action +/mob/proc/break_do_after_checks(list/checked_health, check_clicks) + if(check_clicks && next_move > world.time) + return FALSE + return TRUE + +//pass a list in the format list("health" = mob's health var) to check health during this +/mob/living/break_do_after_checks(list/checked_health, check_clicks) + if(islist(checked_health)) + if(health < checked_health["health"]) + return FALSE + checked_health["health"] = health + return ..() + +/proc/do_after(mob/user, var/delay, needhand = 1, atom/target = null, progress = 1, datum/callback/extra_checks = null, required_mobility_flags = (MOBILITY_USE|MOBILITY_MOVE), resume_time = 0 SECONDS) + if(!user) + return 0 + var/atom/Tloc = null + if(target && !isturf(target)) + Tloc = target.loc + + var/atom/Uloc = user.loc + + var/drifting = 0 + if(!user.Process_Spacemove(0) && user.inertia_dir) + drifting = 1 + + var/holding = user.get_active_held_item() + + var/holdingnull = 1 //User's hand started out empty, check for an empty hand + if(holding) + holdingnull = 0 //Users hand started holding something, check to see if it's still holding that + + delay *= user.do_after_coefficent() + + var/datum/progressbar/progbar + if (progress) + progbar = new(user, delay, target) + + var/endtime = world.time + delay + var/starttime = world.time + . = 1 + var/mob/living/L = isliving(user) && user //evals to last thing eval'd + while (world.time + resume_time < endtime) + stoplag(1) + if (progress) + progbar.update(world.time - starttime + resume_time) + + if(drifting && !user.inertia_dir) + drifting = 0 + Uloc = user.loc + + if(L && !CHECK_ALL_MOBILITY(L, required_mobility_flags)) + . = 0 + break + + if(QDELETED(user) || user.stat || (!drifting && user.loc != Uloc) || (extra_checks && !extra_checks.Invoke())) + . = 0 + break + + if(!QDELETED(Tloc) && (QDELETED(target) || Tloc != target.loc)) + if((Uloc != Tloc || Tloc != user) && !drifting) + . = 0 + break + + if(needhand) + //This might seem like an odd check, but you can still need a hand even when it's empty + //i.e the hand is used to pull some item/tool out of the construction + if(!holdingnull) + if(!holding) + . = 0 + break + if(user.get_active_held_item() != holding) + . = 0 + break + if (progress) + qdel(progbar) + +/mob/proc/do_after_coefficent() // This gets added to the delay on a do_after, default 1 + . = 1 + return + +/proc/do_after_mob(mob/user, var/list/targets, time = 30, uninterruptible = 0, progress = 1, datum/callback/extra_checks) + if(!user || !targets) + return 0 + if(!islist(targets)) + targets = list(targets) + var/user_loc = user.loc + + var/drifting = 0 + if(!user.Process_Spacemove(0) && user.inertia_dir) + drifting = 1 + + var/list/originalloc = list() + for(var/atom/target in targets) + originalloc[target] = target.loc + + var/holding = user.get_active_held_item() + var/datum/progressbar/progbar + if(progress) + progbar = new(user, time, targets[1]) + + var/endtime = world.time + time + var/starttime = world.time + . = 1 + mainloop: + while(world.time < endtime) + stoplag(1) + if(progress) + progbar.update(world.time - starttime) + if(QDELETED(user) || !targets) + . = 0 + break + if(uninterruptible) + continue + + if(drifting && !user.inertia_dir) + drifting = 0 + user_loc = user.loc + + for(var/atom/target in targets) + if((!drifting && user_loc != user.loc) || QDELETED(target) || originalloc[target] != target.loc || user.get_active_held_item() != holding || user.incapacitated() || user.lying || (extra_checks && !extra_checks.Invoke())) + . = 0 + break mainloop + if(progbar) + qdel(progbar) diff --git a/code/__HELPERS/mobs.dm b/code/__HELPERS/mobs.dm index e62bcc6738..91826ee314 100644 --- a/code/__HELPERS/mobs.dm +++ b/code/__HELPERS/mobs.dm @@ -320,173 +320,6 @@ GLOBAL_LIST_EMPTY(species_list) else return "unknown" -/proc/do_mob(mob/user , mob/target, time = 30, uninterruptible = 0, progress = 1, datum/callback/extra_checks = null, ignorehelditem = FALSE, resume_time = 0 SECONDS) - if(!user || !target) - return 0 - var/user_loc = user.loc - - var/drifting = 0 - if(!user.Process_Spacemove(0) && user.inertia_dir) - drifting = 1 - - var/target_loc = target.loc - - var/holding = user.get_active_held_item() - var/datum/progressbar/progbar - if (progress) - progbar = new(user, time, target) - - var/endtime = world.time+time - var/starttime = world.time - . = 1 - while (world.time + resume_time < endtime) - stoplag(1) - if (progress) - progbar.update(world.time - starttime + resume_time) - if(QDELETED(user) || QDELETED(target)) - . = 0 - break - if(uninterruptible) - continue - - if(drifting && !user.inertia_dir) - drifting = 0 - user_loc = user.loc - - if((!drifting && user.loc != user_loc) || target.loc != target_loc || (!ignorehelditem && user.get_active_held_item() != holding) || user.incapacitated() || user.lying || (extra_checks && !extra_checks.Invoke())) - . = 0 - break - if (progress) - qdel(progbar) - - -//some additional checks as a callback for for do_afters that want to break on losing health or on the mob taking action -/mob/proc/break_do_after_checks(list/checked_health, check_clicks) - if(check_clicks && next_move > world.time) - return FALSE - return TRUE - -//pass a list in the format list("health" = mob's health var) to check health during this -/mob/living/break_do_after_checks(list/checked_health, check_clicks) - if(islist(checked_health)) - if(health < checked_health["health"]) - return FALSE - checked_health["health"] = health - return ..() - -/proc/do_after(mob/user, var/delay, needhand = 1, atom/target = null, progress = 1, datum/callback/extra_checks = null, required_mobility_flags = (MOBILITY_USE|MOBILITY_MOVE), resume_time = 0 SECONDS) - if(!user) - return 0 - var/atom/Tloc = null - if(target && !isturf(target)) - Tloc = target.loc - - var/atom/Uloc = user.loc - - var/drifting = 0 - if(!user.Process_Spacemove(0) && user.inertia_dir) - drifting = 1 - - var/holding = user.get_active_held_item() - - var/holdingnull = 1 //User's hand started out empty, check for an empty hand - if(holding) - holdingnull = 0 //Users hand started holding something, check to see if it's still holding that - - delay *= user.do_after_coefficent() - - var/datum/progressbar/progbar - if (progress) - progbar = new(user, delay, target) - - var/endtime = world.time + delay - var/starttime = world.time - . = 1 - var/mob/living/L = isliving(user) && user //evals to last thing eval'd - while (world.time + resume_time < endtime) - stoplag(1) - if (progress) - progbar.update(world.time - starttime + resume_time) - - if(drifting && !user.inertia_dir) - drifting = 0 - Uloc = user.loc - - if(L && !CHECK_ALL_MOBILITY(L, required_mobility_flags)) - . = 0 - break - - if(QDELETED(user) || user.stat || (!drifting && user.loc != Uloc) || (extra_checks && !extra_checks.Invoke())) - . = 0 - break - - if(!QDELETED(Tloc) && (QDELETED(target) || Tloc != target.loc)) - if((Uloc != Tloc || Tloc != user) && !drifting) - . = 0 - break - - if(needhand) - //This might seem like an odd check, but you can still need a hand even when it's empty - //i.e the hand is used to pull some item/tool out of the construction - if(!holdingnull) - if(!holding) - . = 0 - break - if(user.get_active_held_item() != holding) - . = 0 - break - if (progress) - qdel(progbar) - -/mob/proc/do_after_coefficent() // This gets added to the delay on a do_after, default 1 - . = 1 - return - -/proc/do_after_mob(mob/user, var/list/targets, time = 30, uninterruptible = 0, progress = 1, datum/callback/extra_checks) - if(!user || !targets) - return 0 - if(!islist(targets)) - targets = list(targets) - var/user_loc = user.loc - - var/drifting = 0 - if(!user.Process_Spacemove(0) && user.inertia_dir) - drifting = 1 - - var/list/originalloc = list() - for(var/atom/target in targets) - originalloc[target] = target.loc - - var/holding = user.get_active_held_item() - var/datum/progressbar/progbar - if(progress) - progbar = new(user, time, targets[1]) - - var/endtime = world.time + time - var/starttime = world.time - . = 1 - mainloop: - while(world.time < endtime) - stoplag(1) - if(progress) - progbar.update(world.time - starttime) - if(QDELETED(user) || !targets) - . = 0 - break - if(uninterruptible) - continue - - if(drifting && !user.inertia_dir) - drifting = 0 - user_loc = user.loc - - for(var/atom/target in targets) - if((!drifting && user_loc != user.loc) || QDELETED(target) || originalloc[target] != target.loc || user.get_active_held_item() != holding || user.incapacitated() || user.lying || (extra_checks && !extra_checks.Invoke())) - . = 0 - break mainloop - if(progbar) - qdel(progbar) - /proc/is_species(A, species_datum) . = FALSE if(ishuman(A)) diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm index e038596151..9419000e02 100644 --- a/code/_globalvars/bitfields.dm +++ b/code/_globalvars/bitfields.dm @@ -49,6 +49,8 @@ GLOBAL_LIST_INIT(bitfields, list( "DROPDEL" = DROPDEL, "NOBLUDGEON" = NOBLUDGEON, "ABSTRACT" = ABSTRACT, + "ITEM_CAN_BLOCK" = ITEM_CAN_BLOCK, + "ITEM_CAN_PARRY" = ITEM_CAN_PARRY ), "admin_flags" = list( "BUILDMODE" = R_BUILDMODE, diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 357f317450..ecbcb6ec01 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -17,6 +17,9 @@ // eg: 10*0.5 = 5 deciseconds of delay // DOES NOT EFFECT THE BASE 1 DECISECOND DELAY OF NEXT_CLICK +/mob/proc/timeToNextMove() + return max(0, next_move - world.time) + /mob/proc/changeNext_move(num) next_move = world.time + ((num+next_move_adjust)*next_move_modifier) diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index b2d74844cf..9386564adb 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -7,17 +7,17 @@ *and lastly *afterattack. The return value does not matter. */ -/obj/item/proc/melee_attack_chain(mob/user, atom/target, params) +/obj/item/proc/melee_attack_chain(mob/user, atom/target, params, flags, damage_multiplier = 1) if(isliving(user)) var/mob/living/L = user - if(!CHECK_MOBILITY(L, MOBILITY_USE)) + if(!CHECK_MOBILITY(L, MOBILITY_USE) && !(flags & ATTACKCHAIN_PARRY_COUNTERATTACK)) to_chat(L, "You are unable to swing [src] right now!") return if(tool_behaviour && target.tool_act(user, src, tool_behaviour)) return if(pre_attack(target, user, params)) return - if(target.attackby(src,user, params)) + if(target.attackby(src, user, params, flags, damage_multiplier)) return if(QDELETED(src) || QDELETED(target)) return @@ -52,15 +52,15 @@ /obj/attackby(obj/item/I, mob/living/user, params) return ..() || ((obj_flags & CAN_BE_HIT) && I.attack_obj(src, user)) -/mob/living/attackby(obj/item/I, mob/living/user, params) +/mob/living/attackby(obj/item/I, mob/living/user, params, attackchain_flags, damage_multiplier) if(..()) return TRUE I.attack_delay_done = FALSE //Should be set TRUE in pre_attacked_by() - . = I.attack(src, user) + . = I.attack(src, user, attackchain_flags, damage_multiplier) if(!I.attack_delay_done) //Otherwise, pre_attacked_by() should handle it. user.changeNext_move(I.click_delay) -/obj/item/proc/attack(mob/living/M, mob/living/user) +/obj/item/proc/attack(mob/living/M, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) if(SEND_SIGNAL(src, COMSIG_ITEM_ATTACK, M, user) & COMPONENT_ITEM_NO_ATTACK) return SEND_SIGNAL(user, COMSIG_MOB_ITEM_ATTACK, M, user) @@ -79,7 +79,7 @@ M.lastattackerckey = user.ckey user.do_attack_animation(M) - M.attacked_by(src, user) + M.attacked_by(src, user, attackchain_flags, damage_multiplier) log_combat(user, M, "attacked", src.name, "(INTENT: [uppertext(user.a_intent)]) (DAMTYPE: [uppertext(damtype)])") add_fingerprint(user) @@ -104,8 +104,8 @@ /atom/movable/proc/attacked_by() return -/obj/attacked_by(obj/item/I, mob/living/user) - var/totitemdamage = I.force +/obj/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) + var/totitemdamage = I.force * damage_multiplier var/bad_trait var/stamloss = user.getStaminaLoss() @@ -134,10 +134,12 @@ take_damage(totitemdamage, I.damtype, "melee", 1) return TRUE -/mob/living/attacked_by(obj/item/I, mob/living/user) - var/totitemdamage = pre_attacked_by(I, user) - if((user != src) && mob_run_block(I, totitemdamage, "the [I.name]", ATTACK_TYPE_MELEE, I.armour_penetration, user, null, null) & BLOCK_SUCCESS) +/mob/living/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) + var/list/block_return = list() + var/totitemdamage = pre_attacked_by(I, user) * damage_multiplier + if((user != src) && mob_run_block(I, totitemdamage, "the [I.name]", ((attackchain_flags & ATTACKCHAIN_PARRY_COUNTERATTACK)? ATTACK_TYPE_PARRY_COUNTERATTACK : NONE) | ATTACK_TYPE_MELEE, I.armour_penetration, user, null, block_return) & BLOCK_SUCCESS) return FALSE + totitemdamage = block_calculate_resultant_damage(totitemdamage, block_return) send_item_attack_message(I, user, null, totitemdamage) I.do_stagger_action(src, user, totitemdamage) if(I.force) @@ -151,7 +153,7 @@ user.add_mob_blood(src) return TRUE //successful attack -/mob/living/simple_animal/attacked_by(obj/item/I, mob/living/user) +/mob/living/simple_animal/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) if(I.force < force_threshold || I.damtype == STAMINA) playsound(loc, 'sound/weapons/tap.ogg', I.get_clamped_volume(), 1, -1) user.changeNext_move(I.click_delay) //pre_attacked_by not called diff --git a/code/datums/components/combat_mode.dm b/code/datums/components/combat_mode.dm index b53d407162..b9952e9133 100644 --- a/code/datums/components/combat_mode.dm +++ b/code/datums/components/combat_mode.dm @@ -119,6 +119,8 @@ if(hud_icon) hud_icon.combat_on = FALSE hud_icon.update_icon() + source.stop_active_blocking() + source.end_parry_sequence() ///Changes the user direction to (try) keep match the pointer. /datum/component/combat_mode/proc/on_move(atom/movable/source, dir, atom/oldloc, forced) diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm index 0877e4021a..8241f685d7 100644 --- a/code/datums/martial/_martial.dm +++ b/code/datums/martial/_martial.dm @@ -10,6 +10,10 @@ var/help_verb var/pacifism_check = TRUE //are the martial arts combos/attacks unable to be used by pacifist. var/allow_temp_override = TRUE //if this martial art can be overridden by temporary martial arts + /// Can we be used to unarmed parry? + var/can_martial_parry = FALSE + /// Set this variable to something not null, this'll be the preferred unarmed parry in most cases if [can_martial_parry] is TRUE. YOU MUST RUN [get_block_parry_data(this)] INSTEAD OF DIRECTLY ACCESSING! + var/datum/block_parry_data/block_parry_data var/pugilist = FALSE /datum/martial_art/proc/disarm_act(mob/living/carbon/human/A, mob/living/carbon/human/D) @@ -91,4 +95,4 @@ ///Gets called when a projectile hits the owner. Returning anything other than BULLET_ACT_HIT will stop the projectile from hitting the mob. /datum/martial_art/proc/on_projectile_hit(mob/living/carbon/human/A, obj/item/projectile/P, def_zone) - return BULLET_ACT_HIT \ No newline at end of file + return BULLET_ACT_HIT diff --git a/code/datums/martial/boxing.dm b/code/datums/martial/boxing.dm index 848fdc6a41..e3c7726d61 100644 --- a/code/datums/martial/boxing.dm +++ b/code/datums/martial/boxing.dm @@ -66,9 +66,9 @@ return /obj/item/clothing/gloves/boxing/dropped(mob/user) + . = ..() if(!ishuman(user)) return var/mob/living/carbon/human/H = user if(H.get_item_by_slot(SLOT_GLOVES) == src) style.remove(H) - return diff --git a/code/datums/martial/krav_maga.dm b/code/datums/martial/krav_maga.dm index f054867de4..50438d9d8d 100644 --- a/code/datums/martial/krav_maga.dm +++ b/code/datums/martial/krav_maga.dm @@ -203,6 +203,7 @@ style.teach(H,1) /obj/item/clothing/gloves/krav_maga/dropped(mob/user) + . = ..() if(!ishuman(user)) return var/mob/living/carbon/human/H = user diff --git a/code/datums/martial/wrestling.dm b/code/datums/martial/wrestling.dm index f7923d029f..87fcf78964 100644 --- a/code/datums/martial/wrestling.dm +++ b/code/datums/martial/wrestling.dm @@ -480,12 +480,12 @@ return /obj/item/storage/belt/champion/wrestling/dropped(mob/user) + . = ..() if(!ishuman(user)) return var/mob/living/carbon/human/H = user if(H.get_item_by_slot(SLOT_BELT) == src) style.remove(H) - return //Subtype of wrestling, reserved for the wrestling belts found in the holodeck /datum/martial_art/wrestling/holodeck diff --git a/code/game/gamemodes/gangs/dominator.dm b/code/game/gamemodes/gangs/dominator.dm index a375c4f8b5..db145ffacc 100644 --- a/code/game/gamemodes/gangs/dominator.dm +++ b/code/game/gamemodes/gangs/dominator.dm @@ -145,7 +145,7 @@ new /obj/item/stack/sheet/plasteel(src.loc) qdel(src) -/obj/machinery/dominator/attacked_by(obj/item/I, mob/living/user) +/obj/machinery/dominator/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) add_fingerprint(user) return ..() @@ -240,4 +240,4 @@ #undef DOM_BLOCKED_SPAM_CAP #undef DOM_REQUIRED_TURFS -#undef DOM_HULK_HITS_REQUIRED \ No newline at end of file +#undef DOM_HULK_HITS_REQUIRED diff --git a/code/game/machinery/porta_turret/portable_turret_cover.dm b/code/game/machinery/porta_turret/portable_turret_cover.dm index 2403f903a9..7fdb9b38be 100644 --- a/code/game/machinery/porta_turret/portable_turret_cover.dm +++ b/code/game/machinery/porta_turret/portable_turret_cover.dm @@ -70,7 +70,7 @@ else return ..() -/obj/machinery/porta_turret_cover/attacked_by(obj/item/I, mob/user) +/obj/machinery/porta_turret_cover/attacked_by(obj/item/I, mob/user, attackchain_flags = NONE, damage_multiplier = 1) return parent_turret.attacked_by(I, user) /obj/machinery/porta_turret_cover/attack_alien(mob/living/carbon/alien/humanoid/user) diff --git a/code/game/mecha/mecha_defense.dm b/code/game/mecha/mecha_defense.dm index 56bb8cceda..395ac810f4 100644 --- a/code/game/mecha/mecha_defense.dm +++ b/code/game/mecha/mecha_defense.dm @@ -282,7 +282,7 @@ else return ..() -/obj/mecha/attacked_by(obj/item/I, mob/living/user) +/obj/mecha/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) mecha_log_message("Attacked by [I]. Attacker - [user]") return ..() diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 776980badb..b7e671b9e2 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -141,6 +141,13 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) var/list/grind_results //A reagent list containing the reagents this item produces when ground up in a grinder - this can be an empty list to allow for reagent transferring only var/list/juice_results //A reagent list containing blah blah... but when JUICED in a grinder! + /* Our block parry data. Should be set in init, or something if you are using it. + * This won't be accessed without ITEM_CAN_BLOCK or ITEM_CAN_PARRY so do not set it unless you have to to save memory. + * If you decide it's a good idea to leave this unset while turning the flags on, you will runtime. Enjoy. + * If this is set to a path, it'll run get_block_parry_data(path). YOU MUST RUN [get_block_parry_data(this)] INSTEAD OF DIRECTLY ACCESSING! + */ + var/datum/block_parry_data/block_parry_data + ///Skills vars //list of skill PATHS exercised when using this item. An associated bitfield can be set to indicate additional ways the skill is used by this specific item. var/list/datum/skill/used_skills @@ -240,9 +247,10 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) . += "[src] is made of cold-resistant materials." if(resistance_flags & FIRE_PROOF) . += "[src] is made of fire-retardant materials." - - - + + if(item_flags & (ITEM_CAN_BLOCK | ITEM_CAN_PARRY)) + var/datum/block_parry_data/data = return_block_parry_datum(block_parry_data) + . += "[src] has the capacity to be used to block and/or parry. \[Show Stats\]" if(!user.research_scanner) return @@ -404,6 +412,7 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) return ITALICS | REDUCE_RANGE /obj/item/proc/dropped(mob/user) + SHOULD_CALL_PARENT(TRUE) for(var/X in actions) var/datum/action/A = X A.Remove(user) @@ -416,6 +425,7 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE) // called just as an item is picked up (loc is not yet changed) /obj/item/proc/pickup(mob/user) + SHOULD_CALL_PARENT(TRUE) SEND_SIGNAL(src, COMSIG_ITEM_PICKUP, user) item_flags |= IN_INVENTORY diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm index ae237919ce..bfbacb1a34 100644 --- a/code/game/objects/items/cigs_lighters.dm +++ b/code/game/objects/items/cigs_lighters.dm @@ -803,6 +803,7 @@ CIGARETTE PACKETS ARE IN FANCY.DM to_chat(user, "You need to close the cap first!") /obj/item/clothing/mask/vape/dropped(mob/user) + . = ..() var/mob/living/carbon/C = user if(C.get_item_by_slot(SLOT_WEAR_MASK) == src) ENABLE_BITFIELD(reagents.reagents_holder_flags, NO_REACT) diff --git a/code/game/objects/items/melee/energy.dm b/code/game/objects/items/melee/energy.dm index cbb72c5432..63279d2ddc 100644 --- a/code/game/objects/items/melee/energy.dm +++ b/code/game/objects/items/melee/energy.dm @@ -105,9 +105,28 @@ sharpness = IS_SHARP embedding = list("embed_chance" = 75, "embedded_impact_pain_multiplier" = 10) armour_penetration = 35 - block_chance = 50 + item_flags = NEEDS_PERMIT | ITEM_CAN_PARRY + block_parry_data = /datum/block_parry_data/energy_sword var/list/possible_colors = list("red" = LIGHT_COLOR_RED, "blue" = LIGHT_COLOR_LIGHT_CYAN, "green" = LIGHT_COLOR_GREEN, "purple" = LIGHT_COLOR_LAVENDER) +/datum/block_parry_data/energy_sword + parry_time_windup = 0 + parry_time_active = 25 + parry_time_spindown = 0 + // we want to signal to players the most dangerous phase, the time when automatic counterattack is a thing. + parry_time_windup_visual_override = 1 + parry_time_active_visual_override = 3 + parry_time_spindown_visual_override = 12 + parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK // esword users can attack while + parry_time_perfect = 2.5 // first ds isn't perfect + parry_time_perfect_leeway = 1.5 + parry_imperfect_falloff_percent = 5 + parry_efficiency_to_counterattack = 100 + parry_efficiency_considered_successful = 65 // VERY generous + parry_efficiency_perfect = 100 + parry_failed_stagger_duration = 4 SECONDS + parry_cooldown = 0.5 SECONDS + /obj/item/melee/transforming/energy/sword/Initialize(mapload) . = ..() set_sword_color() diff --git a/code/game/objects/items/melee/misc.dm b/code/game/objects/items/melee/misc.dm index 71437d4706..741607edc3 100644 --- a/code/game/objects/items/melee/misc.dm +++ b/code/game/objects/items/melee/misc.dm @@ -61,13 +61,25 @@ force = 18 throwforce = 15 w_class = WEIGHT_CLASS_BULKY - block_chance = 50 armour_penetration = 75 sharpness = IS_SHARP attack_verb = list("slashed", "cut") hitsound = 'sound/weapons/rapierhit.ogg' custom_materials = list(/datum/material/iron = 1000) total_mass = 3.4 + item_flags = NEEDS_PERMIT | ITEM_CAN_PARRY + block_parry_data = /datum/block_parry_data/captain_saber + +/datum/block_parry_data/captain_saber + parry_time_windup = 0.5 + parry_time_active = 4 + parry_time_spindown = 1 + parry_time_perfect = 0.75 + parry_time_perfect_leeway = 0.75 + parry_imperfect_falloff_percent = 30 + parry_efficiency_perfect = 100 + parry_failed_stagger_duration = 3 SECONDS + parry_failed_clickcd_duration = 2 SECONDS /obj/item/melee/sabre/Initialize() . = ..() @@ -150,7 +162,6 @@ righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' force = 15 throwforce = 25 - block_chance = 50 armour_penetration = 200 //Apparently this gives it the ability to pierce block flags_1 = CONDUCT_1 obj_flags = UNIQUE_RENAME @@ -159,16 +170,47 @@ attack_verb = list("stabs", "punctures", "pierces", "pokes") hitsound = 'sound/weapons/rapierhit.ogg' total_mass = 0.4 + item_flags = ITEM_CAN_PARRY | NEEDS_PERMIT + block_parry_data = /datum/block_parry_data/traitor_rapier + +// Fast, efficient parry. +/datum/block_parry_data/traitor_rapier + parry_time_windup = 0.5 + parry_time_active = 5 + parry_time_spindown = 0 + parry_time_active_visual_override = 3 + parry_time_spindown_visual_override = 2 + parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK | PARRY_LOCK_ATTACKING + parry_time_perfect = 0 + parry_time_perfect_leeway = 3 + parry_time_perfect_leeway_override = list( + TEXT_ATTACK_TYPE_PROJECTILE = 1 + ) + parry_imperfect_falloff_percent_override = list( + TEXT_ATTACK_TYPE_PROJECTILE = 50 // useless after 3rd decisecond + ) + parry_imperfect_falloff_percent = 30 + parry_efficiency_to_counterattack = 100 + parry_efficiency_considered_successful = 1 + parry_efficiency_perfect = 100 + parry_data = list( + PARRY_DISARM_ATTACKER = TRUE, + PARRY_KNOCKDOWN_ATTACKER = 10 + ) + parry_failed_stagger_duration = 2 SECONDS + parry_failed_clickcd_duration = CLICK_CD_RANGE + parry_cooldown = 0 + +/obj/item/melee/rapier/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) + . = ..() + if((attack_type & ATTACK_TYPE_PROJECTILE) && (parry_efficiency >= 100)) + . |= BLOCK_SHOULD_REDIRECT + return_list[BLOCK_RETURN_REDIRECT_METHOD] = REDIRECT_METHOD_DEFLECT /obj/item/melee/rapier/Initialize() . = ..() AddComponent(/datum/component/butchering, 20, 65, 0) -/obj/item/melee/rapier/run_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) - if(attack_type == ATTACK_TYPE_PROJECTILE) - final_block_chance = 0 - return ..() - /obj/item/melee/rapier/on_exit_storage(datum/component/storage/S) var/obj/item/storage/belt/sabre/rapier/B = S.parent if(istype(B)) @@ -191,10 +233,9 @@ . = ..() if(iscarbon(target)) var/mob/living/carbon/H = target - var/loss = H.getStaminaLoss() H.Dizzy(10) H.adjustStaminaLoss(30) - if((loss > 40) && prob(loss)) // if above 40, roll for sleep using 1% every 1 stamina damage + if(CHECK_STAMCRIT(H) != NOT_STAMCRIT) H.Sleeping(180) /obj/item/melee/classic_baton @@ -664,4 +705,4 @@ . = ..() overlay = mutable_appearance(icon, overlay_state) overlay.appearance_flags = RESET_COLOR - add_overlay(overlay) \ No newline at end of file + add_overlay(overlay) diff --git a/code/game/objects/items/robot/robot_items.dm b/code/game/objects/items/robot/robot_items.dm index 2f697553f5..4b8728e426 100644 --- a/code/game/objects/items/robot/robot_items.dm +++ b/code/game/objects/items/robot/robot_items.dm @@ -358,6 +358,7 @@ check_amount() /obj/item/borg/lollipop/dropped(mob/user) + . = ..() check_amount() /obj/item/borg/lollipop/proc/check_amount() //Doesn't even use processing ticks. diff --git a/code/game/objects/items/shields.dm b/code/game/objects/items/shields.dm index b9cde1664f..aefa5d3cc8 100644 --- a/code/game/objects/items/shields.dm +++ b/code/game/objects/items/shields.dm @@ -1,7 +1,8 @@ /obj/item/shield name = "shield" icon = 'icons/obj/shields.dmi' - block_chance = 50 + item_flags = ITEM_CAN_BLOCK + block_parry_data = /datum/block_parry_data/shield armor = list("melee" = 50, "bullet" = 50, "laser" = 50, "energy" = 0, "bomb" = 30, "bio" = 0, "rad" = 0, "fire" = 80, "acid" = 70) /// Shield flags var/shield_flags = SHIELD_FLAGS_DEFAULT @@ -22,6 +23,18 @@ /// Shield bashing push distance var/shieldbash_push_distance = 1 +/datum/block_parry_data/shield + block_damage_multiplier = 0.25 + block_stamina_efficiency = 2.5 + block_stamina_cost_per_second = 3.5 + block_slowdown = 0 + block_lock_attacking = FALSE + block_lock_sprinting = TRUE + block_start_delay = 1.5 + block_damage_absorption = 5 + block_resting_stamina_penalty_multiplier = 2 + block_projectile_mitigation = 75 + /obj/item/shield/examine(mob/user) . = ..() if(shield_flags & SHIELD_CAN_BASH) @@ -154,6 +167,22 @@ icon_state = "shield_bash" duration = 3 +/obj/item/shield/run_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) + if(ismovable(object)) + var/atom/movable/AM = object + if(CHECK_BITFIELD(shield_flags, SHIELD_TRANSPARENT) && (AM.pass_flags & PASSGLASS)) + return BLOCK_NONE + if(attack_type & ATTACK_TYPE_THROWN) + final_block_chance += 30 + if(attack_type & ATTACK_TYPE_TACKLE) + final_block_chance = 100 + . = ..() + if(. & BLOCK_SUCCESS) + on_shield_block(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return) + +/obj/item/shield/on_active_block(mob/living/owner, atom/object, damage, damage_blocked, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction) + on_shield_block(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance) + /obj/item/shield/riot name = "riot shield" desc = "A shield adept at blocking blunt objects from connecting with the torso of the shield wielder." @@ -172,20 +201,7 @@ var/repair_material = /obj/item/stack/sheet/mineral/titanium var/can_shatter = TRUE shield_flags = SHIELD_FLAGS_DEFAULT | SHIELD_TRANSPARENT - max_integrity = 75 - -/obj/item/shield/run_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) - if(ismovable(object)) - var/atom/movable/AM = object - if(CHECK_BITFIELD(shield_flags, SHIELD_TRANSPARENT) && (AM.pass_flags & PASSGLASS)) - return BLOCK_NONE - if(attack_type & ATTACK_TYPE_THROWN) - final_block_chance += 30 - if(attack_type & ATTACK_TYPE_TACKLE) - final_block_chance = 100 - . = ..() - if(. & BLOCK_SUCCESS) - on_shield_block(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return) + max_integrity = 450 /obj/item/shield/riot/attackby(obj/item/W, mob/user, params) if(istype(W, /obj/item/melee/baton)) @@ -238,13 +254,13 @@ lefthand_file = 'icons/mob/inhands/equipment/shields_lefthand.dmi' righthand_file = 'icons/mob/inhands/equipment/shields_righthand.dmi' shield_flags = SHIELD_FLAGS_DEFAULT - max_integrity = 55 //Weak + max_integrity = 300 obj/item/shield/riot/bullet_proof name = "bullet resistant shield" desc = "A far more frail shield made of resistant plastics and kevlar meant to block ballistics." armor = list("melee" = 30, "bullet" = 80, "laser" = 0, "energy" = 0, "bomb" = -40, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 50) - max_integrity = 55 //Weaker + max_integrity = 300 /obj/item/shield/riot/roman name = "\improper Roman shield" @@ -255,13 +271,13 @@ obj/item/shield/riot/bullet_proof righthand_file = 'icons/mob/inhands/equipment/shields_righthand.dmi' repair_material = /obj/item/stack/sheet/mineral/wood shield_flags = SHIELD_FLAGS_DEFAULT - max_integrity = 65 + max_integrity = 250 /obj/item/shield/riot/roman/fake desc = "Bears an inscription on the inside: \"Romanes venio domus\". It appears to be a bit flimsy." block_chance = 0 armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 0, "acid" = 0) - max_integrity = 30 + max_integrity = 40 /obj/item/shield/riot/roman/shatter(mob/living/carbon/human/owner) playsound(owner, 'sound/effects/grillehit.ogg', 100) @@ -279,7 +295,7 @@ obj/item/shield/riot/bullet_proof repair_material = /obj/item/stack/sheet/mineral/wood block_chance = 30 shield_flags = SHIELD_FLAGS_DEFAULT - max_integrity = 55 + max_integrity = 150 /obj/item/shield/riot/buckler/shatter(mob/living/carbon/human/owner) playsound(owner, 'sound/effects/bang.ogg', 50) @@ -297,13 +313,16 @@ obj/item/shield/riot/bullet_proof throw_speed = 3 throw_range = 4 w_class = WEIGHT_CLASS_NORMAL - var/active = 0 + var/active = FALSE /obj/item/shield/riot/tele/run_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) if(!active) return BLOCK_NONE return ..() +/obj/item/shield/riot/tele/can_active_block() + return ..() && active + /obj/item/shield/riot/tele/attack_self(mob/living/user) active = !active icon_state = "teleriot[active]" @@ -335,8 +354,7 @@ obj/item/shield/riot/bullet_proof icon_state = "makeshift_shield" custom_materials = list(/datum/material/iron = 18000) slot_flags = null - block_chance = 35 - max_integrity = 100 //Made of metal welded together its strong but not unkillable + max_integrity = 300 //Made of metal welded together its strong but not unkillable force = 10 throwforce = 7 @@ -346,7 +364,6 @@ obj/item/shield/riot/bullet_proof armor = list("melee" = 95, "bullet" = 95, "laser" = 75, "energy" = 60, "bomb" = 90, "bio" = 90, "rad" = 0, "fire" = 90, "acid" = 10) //Armor for the item, dosnt transfer to user item_state = "metal" icon_state = "metal" - block_chance = 75 //1/4 shots will hit* force = 16 slowdown = 2 throwforce = 15 //Massive pice of metal @@ -357,19 +374,17 @@ obj/item/shield/riot/bullet_proof /obj/item/shield/riot/tower/swat name = "swat shield" desc = "A massive, heavy shield that can block a lot of attacks, can take a lot of abuse before breaking." - max_integrity = 175 - block_chance = 50 + max_integrity = 250 /obj/item/shield/riot/implant name = "telescoping shield implant" desc = "A compact, arm-mounted telescopic shield. While nigh-indestructible when powered by a host user, it will eventually overload from damage. Recharges while inside its implant." item_state = "metal" icon_state = "metal" - block_chance = 50 slowdown = 1 shield_flags = SHIELD_FLAGS_DEFAULT - max_integrity = 60 - obj_integrity = 60 + max_integrity = 100 + obj_integrity = 100 can_shatter = FALSE item_flags = SLOWS_WHILE_IN_HAND var/recharge_timerid diff --git a/code/game/objects/items/stunbaton.dm b/code/game/objects/items/stunbaton.dm index 60fc761bfe..fa0c9ba693 100644 --- a/code/game/objects/items/stunbaton.dm +++ b/code/game/objects/items/stunbaton.dm @@ -170,10 +170,12 @@ return disarming || (user.a_intent != INTENT_HARM) /obj/item/melee/baton/proc/baton_stun(mob/living/L, mob/user, disarming = FALSE) - if(L.mob_run_block(src, 0, "[user]'s [name]", ATTACK_TYPE_MELEE, 0, user, null, null) & BLOCK_SUCCESS) //No message; check_shields() handles that + var/list/return_list = list() + if(L.mob_run_block(src, 0, "[user]'s [name]", ATTACK_TYPE_MELEE, 0, user, null, return_list) & BLOCK_SUCCESS) //No message; check_shields() handles that playsound(L, 'sound/weapons/genhit.ogg', 50, 1) return FALSE var/stunpwr = stamforce + stunpwr = block_calculate_resultant_damage(stunpwr, return_list) var/obj/item/stock_parts/cell/our_cell = get_cell() if(!our_cell) switch_status(FALSE) diff --git a/code/game/objects/items/toys.dm b/code/game/objects/items/toys.dm index f3c35b0495..cd630a2c82 100644 --- a/code/game/objects/items/toys.dm +++ b/code/game/objects/items/toys.dm @@ -446,6 +446,7 @@ throw_range = 5 force_unwielded = 0 force_wielded = 0 + block_parry_data = null attack_verb = list("attacked", "struck", "hit") total_mass_on = TOTAL_MASS_TOY_SWORD sharpness = IS_BLUNT diff --git a/code/game/objects/items/twohanded.dm b/code/game/objects/items/twohanded.dm index 7c59f2fc63..4437eefbf8 100644 --- a/code/game/objects/items/twohanded.dm +++ b/code/game/objects/items/twohanded.dm @@ -30,6 +30,8 @@ var/wieldsound = null var/unwieldsound = null var/slowdown_wielded = 0 + /// Do we need to be wielded to actively block/parry? + var/requires_wield_to_block_parry = TRUE item_flags = SLOWS_WHILE_IN_HAND /obj/item/twohanded/proc/unwield(mob/living/carbon/user, show_message = TRUE) @@ -90,6 +92,12 @@ user.put_in_inactive_hand(O) set_slowdown(slowdown + slowdown_wielded) +/obj/item/twohanded/can_active_block() + return ..() && (!requires_wield_to_block_parry || wielded) + +/obj/item/twohanded/can_active_parry() + return ..() && (!requires_wield_to_block_parry || wielded) + /obj/item/twohanded/dropped(mob/user) . = ..() //handles unwielding a twohanded weapon when dropped as well as clearing up the offhand @@ -129,6 +137,7 @@ return ..() /obj/item/twohanded/offhand/dropped(mob/living/user, show_message = TRUE) //Only utilized by dismemberment since you can't normally switch to the offhand to drop it. + . = ..() var/obj/I = user.get_active_held_item() if(I && istype(I, /obj/item/twohanded)) var/obj/item/twohanded/thw = I @@ -274,6 +283,8 @@ throw_range = 5 w_class = WEIGHT_CLASS_SMALL var/w_class_on = WEIGHT_CLASS_BULKY + item_flags = ITEM_CAN_PARRY | SLOWS_WHILE_IN_HAND | ITEM_CAN_BLOCK + block_parry_data = /datum/block_parry_data/dual_esword force_unwielded = 3 force_wielded = 34 wieldsound = 'sound/weapons/saberon.ogg' @@ -284,7 +295,6 @@ var/saber_color = "green" light_color = "#00ff00"//green attack_verb = list("attacked", "slashed", "stabbed", "sliced", "torn", "ripped", "diced", "cut") - block_chance = 75 max_integrity = 200 armor = list("melee" = 0, "bullet" = 0, "laser" = 0, "energy" = 0, "bomb" = 0, "bio" = 0, "rad" = 0, "fire" = 100, "acid" = 70) resistance_flags = FIRE_PROOF @@ -298,6 +308,42 @@ total_mass = 0.4 //Survival flashlights typically weigh around 5 ounces. var/total_mass_on = 3.4 +/datum/block_parry_data/dual_esword + block_damage_absorption = 2 + block_damage_multiplier = 0.15 + block_damage_multiplier_override = list( + ATTACK_TYPE_MELEE = 0.25 + ) + block_start_delay = 0 // instantaneous block + block_stamina_cost_per_second = 2.5 + block_stamina_efficiency = 3 + block_lock_sprinting = TRUE + // no attacking while blocking + block_lock_attacking = TRUE + block_projectile_mitigation = 75 + + parry_time_windup = 0 + parry_time_active = 8 + parry_time_spindown = 0 + // we want to signal to players the most dangerous phase, the time when automatic counterattack is a thing. + parry_time_windup_visual_override = 1 + parry_time_active_visual_override = 3 + parry_time_spindown_visual_override = 4 + parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK // esword users can attack while parrying. + parry_time_perfect = 2 // first ds isn't perfect + parry_time_perfect_leeway = 1 + parry_imperfect_falloff_percent = 10 + parry_efficiency_to_counterattack = 100 + parry_efficiency_considered_successful = 25 // VERY generous + parry_efficiency_perfect = 90 + parry_failed_stagger_duration = 3 SECONDS + parry_failed_clickcd_duration = CLICK_CD_MELEE + + // more efficient vs projectiles + block_stamina_efficiency_override = list( + TEXT_ATTACK_TYPE_PROJECTILE = 4 + ) + /obj/item/twohanded/dualsaber/suicide_act(mob/living/carbon/user) if(wielded) user.visible_message("[user] begins spinning way too fast! It looks like [user.p_theyre()] trying to commit suicide!") @@ -849,6 +895,7 @@ return (BRUTELOSS) /obj/item/twohanded/pitchfork/demonic/pickup(mob/living/user) + . = ..() if(isliving(user) && user.mind && user.owns_soul() && !is_devil(user)) var/mob/living/U = user U.visible_message("As [U] picks [src] up, [U]'s arms briefly catch fire.", \ @@ -1030,13 +1077,12 @@ force_wielded = 10 throwforce = 15 //if you are a madman and finish someone off with this, power to you. throw_speed = 1 - item_flags = NO_MAT_REDEMPTION | SLOWS_WHILE_IN_HAND - block_chance = 30 + item_flags = NO_MAT_REDEMPTION | SLOWS_WHILE_IN_HAND | ITEM_CAN_BLOCK | ITEM_CAN_PARRY + block_parry_data = /datum/block_parry_data/electrostaff attack_verb = list("struck", "beaten", "thwacked", "pulped") total_mass = 5 //yeah this is a heavy thing, beating people with it while it's off is not going to do you any favors. (to curb stun-kill rampaging without it being on) var/obj/item/stock_parts/cell/cell = /obj/item/stock_parts/cell/high var/on = FALSE - var/can_block_projectiles = FALSE //can't block guns var/lethal_cost = 400 //10000/400*20 = 500. decent enough? var/lethal_damage = 20 var/lethal_stam_cost = 4 @@ -1046,6 +1092,43 @@ var/stun_status_duration = 25 var/stun_stam_cost = 3.5 +// haha security desword time /s +/datum/block_parry_data/electrostaff + block_damage_absorption = 0 + block_damage_multiplier = 1 + can_block_attack_types = ~ATTACK_TYPE_PROJECTILE // only able to parry non projectiles + block_damage_multiplier_override = list( + TEXT_ATTACK_TYPE_MELEE = 0.5, // only useful on melee and unarmed + TEXT_ATTACK_TYPE_UNARMED = 0.3 + ) + block_start_delay = 0.5 // near instantaneous block + block_stamina_cost_per_second = 3 + block_stamina_efficiency = 2 // haha this is a horrible idea + // more slowdown that deswords because security + block_slowdown = 2 + // no attacking while blocking + block_lock_attacking = TRUE + + parry_time_windup = 1 + parry_time_active = 5 + parry_time_spindown = 0 + parry_time_spindown_visual_override = 1 + parry_flags = PARRY_DEFAULT_HANDLE_FEEDBACK | PARRY_LOCK_ATTACKING // no attacking while parrying + parry_time_perfect = 0 + parry_time_perfect_leeway = 0.5 + parry_efficiency_perfect = 100 + parry_imperfect_falloff_percent = 1 + parry_imperfect_falloff_percent_override = list( + TEXT_ATTACK_TYPE_PROJECTILE = 45 // really crappy vs projectiles + ) + parry_time_perfect_leeway_override = list( + TEXT_ATTACK_TYPE_PROJECTILE = 1 // extremely harsh window for projectiles + ) + // not extremely punishing to fail, but no spamming the parry. + parry_cooldown = 2.5 SECONDS + parry_failed_stagger_duration = 1.5 SECONDS + parry_failed_clickcd_duration = 1 SECONDS + /obj/item/twohanded/electrostaff/Initialize(mapload) . = ..() if(ispath(cell)) @@ -1061,11 +1144,6 @@ var/mob/living/silicon/robot/R = loc . = R.get_cell() -/obj/item/twohanded/electrostaff/run_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) - if(!on || (!can_block_projectiles && (attack_type & ATTACK_TYPE_PROJECTILE))) - return BLOCK_NONE - return ..() - /obj/item/twohanded/electrostaff/proc/min_hitcost() return min(stun_cost, lethal_cost) @@ -1183,22 +1261,23 @@ return if(iscyborg(target)) return ..() - if(target.mob_run_block(src, 0, "[user]'s [name]", ATTACK_TYPE_MELEE, 0, user, null, null) & BLOCK_SUCCESS) //No message; run_block() handles that + var/list/return_list = list() + if(target.mob_run_block(src, 0, "[user]'s [name]", ATTACK_TYPE_MELEE, 0, user, null, return_list) & BLOCK_SUCCESS) //No message; run_block() handles that playsound(target, 'sound/weapons/genhit.ogg', 50, 1) return FALSE if(user.a_intent != INTENT_HARM) - if(stun_act(target, user)) + if(stun_act(target, user, null, return_list)) user.do_attack_animation(target) user.adjustStaminaLossBuffered(stun_stam_cost) return - else if(!harm_act(target, user)) + else if(!harm_act(target, user, null, return_list)) return ..() //if you can't fry them just beat them with it else //we did harm act them user.do_attack_animation(target) user.adjustStaminaLossBuffered(lethal_stam_cost) -/obj/item/twohanded/electrostaff/proc/stun_act(mob/living/target, mob/living/user, no_charge_and_force = FALSE) - var/stunforce = stun_stamdmg +/obj/item/twohanded/electrostaff/proc/stun_act(mob/living/target, mob/living/user, no_charge_and_force = FALSE, list/block_return = list()) + var/stunforce = block_calculate_resultant_damage(stun_stamdmg, block_return) if(!no_charge_and_force) if(!on) target.visible_message("[user] has bapped [target] with [src]. Luckily it was off.", \ @@ -1228,8 +1307,8 @@ H.forcesay(GLOB.hit_appends) return TRUE -/obj/item/twohanded/electrostaff/proc/harm_act(mob/living/target, mob/living/user, no_charge_and_force = FALSE) - var/lethal_force = lethal_damage +/obj/item/twohanded/electrostaff/proc/harm_act(mob/living/target, mob/living/user, no_charge_and_force = FALSE, list/block_return = list()) + var/lethal_force = block_calculate_resultant_damage(lethal_damage, block_return) if(!no_charge_and_force) if(!on) return FALSE //standard item attack diff --git a/code/game/objects/items/weaponry.dm b/code/game/objects/items/weaponry.dm index f2b3d1ec9b..b48d2a81e4 100644 --- a/code/game/objects/items/weaponry.dm +++ b/code/game/objects/items/weaponry.dm @@ -122,11 +122,13 @@ for further reading, please see: https://github.com/tgstation/tgstation/pull/301 /obj/item/claymore/highlander/pickup(mob/living/user) + . = ..() to_chat(user, "The power of Scotland protects you! You are shielded from all stuns and knockdowns.") user.add_stun_absorption("highlander", INFINITY, 1, " is protected by the power of Scotland!", "The power of Scotland absorbs the stun!", " is protected by the power of Scotland!") user.ignore_slowdown(HIGHLANDER) /obj/item/claymore/highlander/dropped(mob/living/user) + . = ..() user.unignore_slowdown(HIGHLANDER) if(!QDELETED(src)) qdel(src) //If this ever happens, it's because you lost an arm diff --git a/code/modules/antagonists/changeling/powers/mutations.dm b/code/modules/antagonists/changeling/powers/mutations.dm index 5378ea2276..e4417a6d64 100644 --- a/code/modules/antagonists/changeling/powers/mutations.dm +++ b/code/modules/antagonists/changeling/powers/mutations.dm @@ -432,15 +432,18 @@ /obj/item/shield/changeling name = "shield-like mass" desc = "A mass of tough, boney tissue. You can still see the fingers as a twisted pattern in the shield." - item_flags = ABSTRACT | DROPDEL + item_flags = ABSTRACT | DROPDEL | ITEM_CAN_BLOCK icon = 'icons/obj/items_and_weapons.dmi' icon_state = "ling_shield" lefthand_file = 'icons/mob/inhands/antag/changeling_lefthand.dmi' righthand_file = 'icons/mob/inhands/antag/changeling_righthand.dmi' - block_chance = 50 + block_parry_data = /datum/block_parry_data/shield/changeling var/remaining_uses //Set by the changeling ability. +/datum/block_parry_data/shield/changeling + block_slowdown = 0 + /obj/item/shield/changeling/Initialize(mapload) . = ..() ADD_TRAIT(src, TRAIT_NODROP, CHANGELING_TRAIT) @@ -451,7 +454,7 @@ block_return[BLOCK_RETURN_BLOCK_CAPACITY] = (block_return[BLOCK_RETURN_BLOCK_CAPACITY] || 0) + remaining_uses return ..() -/obj/item/shield/changeling/run_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/active_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)) diff --git a/code/modules/antagonists/clockcult/clock_structure.dm b/code/modules/antagonists/clockcult/clock_structure.dm index 13da9c5a42..2464015b6b 100644 --- a/code/modules/antagonists/clockcult/clock_structure.dm +++ b/code/modules/antagonists/clockcult/clock_structure.dm @@ -101,7 +101,7 @@ return 1 return ..() -/obj/structure/destructible/clockwork/attacked_by(obj/item/I, mob/living/user) +/obj/structure/destructible/clockwork/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) if(is_servant_of_ratvar(user) && immune_to_servant_attacks) return FALSE return ..() diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm index ffab8174e5..8f0f9a658c 100644 --- a/code/modules/antagonists/cult/cult_items.dm +++ b/code/modules/antagonists/cult/cult_items.dm @@ -273,6 +273,7 @@ knockdown = 20 /obj/item/restraints/legcuffs/bola/cult/pickup(mob/living/user) + . = ..() if(!iscultist(user)) to_chat(user, "The bola seems to take on a life of its own!") ensnare(user) diff --git a/code/modules/antagonists/devil/true_devil/_true_devil.dm b/code/modules/antagonists/devil/true_devil/_true_devil.dm index f09592161d..7d7031dad4 100644 --- a/code/modules/antagonists/devil/true_devil/_true_devil.dm +++ b/code/modules/antagonists/devil/true_devil/_true_devil.dm @@ -116,8 +116,7 @@ /mob/living/carbon/true_devil/get_ear_protection() return 2 - -/mob/living/carbon/true_devil/attacked_by(obj/item/I, mob/living/user, def_zone) +/mob/living/carbon/true_devil/attacked_by(obj/item/I, mob/living/user, def_zone, attackchain_flags = NONE, damage_multiplier = 1) var/totitemdamage = pre_attacked_by(I, user) totitemdamage *= check_weakness(I, user) apply_damage(totitemdamage, I.damtype, def_zone) diff --git a/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm b/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm index f3b65c5724..952db8315a 100644 --- a/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm +++ b/code/modules/atmospherics/machinery/portable/portable_atmospherics.dm @@ -147,7 +147,7 @@ /obj/machinery/portable_atmospherics/analyzer_act(mob/living/user, obj/item/I) atmosanalyzer_scan(air_contents, user, src) -/obj/machinery/portable_atmospherics/attacked_by(obj/item/I, mob/user) +/obj/machinery/portable_atmospherics/attacked_by(obj/item/I, mob/user, attackchain_flags = NONE, damage_multiplier = 1) if(I.force < 10 && !(stat & BROKEN)) take_damage(0) else diff --git a/code/modules/events/spacevine.dm b/code/modules/events/spacevine.dm index 89b70f8951..85bcf4959d 100644 --- a/code/modules/events/spacevine.dm +++ b/code/modules/events/spacevine.dm @@ -330,8 +330,8 @@ if(!override) qdel(src) -/obj/structure/spacevine/attacked_by(obj/item/I, mob/living/user) - var/damage_dealt = I.force +/obj/structure/spacevine/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) + var/damage_dealt = I.force * damage_multiplier if(I.get_sharpness()) damage_dealt *= 4 if(I.damtype == BURN) diff --git a/code/modules/keybindings/keybind/__defines.dm b/code/modules/keybindings/keybind/__defines.dm index baa095987c..93d033eea5 100644 --- a/code/modules/keybindings/keybind/__defines.dm +++ b/code/modules/keybindings/keybind/__defines.dm @@ -8,6 +8,7 @@ #define CATEGORY_MISC "MISC" #define CATEGORY_MOVEMENT "MOVEMENT" #define CATEGORY_TARGETING "TARGETING" +#define CATEGORY_COMBAT "COMBAT" #define WEIGHT_HIGHEST 0 #define WEIGHT_ADMIN 10 diff --git a/code/modules/keybindings/keybind/combat.dm b/code/modules/keybindings/keybind/combat.dm new file mode 100644 index 0000000000..457fbb0cb2 --- /dev/null +++ b/code/modules/keybindings/keybind/combat.dm @@ -0,0 +1,38 @@ +/datum/keybinding/living/toggle_combat_mode + hotkey_keys = list("C") + name = "toggle_combat_mode" + full_name = "Toggle combat mode" + category = CATEGORY_COMBAT + description = "Toggles whether or not you're in combat mode." + +/datum/keybinding/living/toggle_combat_mode/down(client/user) + SEND_SIGNAL(user.mob, COMSIG_TOGGLE_COMBAT_MODE) + return TRUE + +/datum/keybinding/living/active_block + hotkey_keys = list("Northwest", "F") // HOME + name = "active_block" + full_name = "Block (Hold)" + category = CATEGORY_COMBAT + description = "Hold down to actively block with your currently in-hand object." + +/datum/keybinding/living/active_block/down(client/user) + var/mob/living/L = user.mob + L.keybind_start_active_blocking() + return TRUE + +/datum/keybinding/living/active_block/up(client/user) + var/mob/living/L = user.mob + L.keybind_stop_active_blocking() + +/datum/keybinding/living/active_parry + hotkey_keys = list("Insert", "G") + name = "active_parry" + full_name = "Parry" + category = CATEGORY_COMBAT + description = "Press to initiate a parry sequence with your currently in-hand object." + +/datum/keybinding/living/active_parry/down(client/user) + var/mob/living/L = user.mob + L.keybind_parry() + return TRUE diff --git a/code/modules/keybindings/keybind/living.dm b/code/modules/keybindings/keybind/living.dm index 38c666e186..b5921b378a 100644 --- a/code/modules/keybindings/keybind/living.dm +++ b/code/modules/keybindings/keybind/living.dm @@ -16,16 +16,6 @@ L.resist() return TRUE -/datum/keybinding/living/toggle_combat_mode - hotkey_keys = list("C") - name = "toggle_combat_mode" - full_name = "Toggle combat mode" - description = "Toggles whether or not you're in combat mode." - -/datum/keybinding/living/toggle_combat_mode/down(client/user) - SEND_SIGNAL(user.mob, COMSIG_TOGGLE_COMBAT_MODE) - return TRUE - /datum/keybinding/living/toggle_resting hotkey_keys = list("V") name = "toggle_resting" diff --git a/code/modules/keybindings/keybind/mob.dm b/code/modules/keybindings/keybind/mob.dm index 083d4a19fd..2ce4dc35a0 100644 --- a/code/modules/keybindings/keybind/mob.dm +++ b/code/modules/keybindings/keybind/mob.dm @@ -17,7 +17,7 @@ return TRUE /datum/keybinding/mob/cycle_intent_right - hotkey_keys = list("Northwest", "F") // HOME + hotkey_keys = list("Unbound") name = "cycle_intent_right" full_name = "Cycle Action Intent Right" description = "" @@ -28,7 +28,7 @@ return TRUE /datum/keybinding/mob/cycle_intent_left - hotkey_keys = list("Insert", "G") + hotkey_keys = list("Unbound") name = "cycle_intent_left" full_name = "Cycle Action Intent Left" description = "" diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index fad3a7e534..74775203b1 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -333,6 +333,7 @@ I.moveToNullspace() else I.forceMove(newloc) + on_item_dropped(I) if(I.dropped(src) == ITEM_RELOCATED_BY_DROPPED) return FALSE return TRUE diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm index 11849641ef..727a22f844 100644 --- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm +++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm @@ -77,20 +77,18 @@ return TRUE /mob/living/carbon/alien/humanoid/get_standard_pixel_y_offset(lying = 0) + . = ..() if(leaping) - return -32 - else if(custom_pixel_y_offset) - return custom_pixel_y_offset - else - return initial(pixel_y) + . -= 32 + if(custom_pixel_y_offset) + . += custom_pixel_y_offset /mob/living/carbon/alien/humanoid/get_standard_pixel_x_offset(lying = 0) + . = ..() if(leaping) - return -32 - else if(custom_pixel_x_offset) - return custom_pixel_x_offset - else - return initial(pixel_x) + . -= 32 + if(custom_pixel_x_offset) + . += custom_pixel_x_offset /mob/living/carbon/alien/humanoid/get_permeability_protection(list/target_zones) return 0.8 diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 37b364ea6f..e4ec902654 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -432,10 +432,9 @@ return /mob/living/carbon/get_standard_pixel_y_offset(lying = 0) + . = ..() if(lying) - return -6 - else - return initial(pixel_y) + . -= 6 /mob/living/carbon/proc/accident(obj/item/I) if(!I || (I.item_flags & ABSTRACT) || HAS_TRAIT(I, TRAIT_NODROP)) diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 6303cf4c5c..f9d38828e3 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -76,11 +76,13 @@ visible_message("[I] embeds itself in [src]'s [L.name]!","[I] embeds itself in your [L.name]!") SEND_SIGNAL(src, COMSIG_ADD_MOOD_EVENT, "embedded", /datum/mood_event/embedded) -/mob/living/carbon/attacked_by(obj/item/I, mob/living/user) - var/totitemdamage = pre_attacked_by(I, user) +/mob/living/carbon/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) + var/totitemdamage = pre_attacked_by(I, user) * damage_multiplier var/impacting_zone = (user == src)? check_zone(user.zone_selected) : ran_zone(user.zone_selected) - if((user != src) && (mob_run_block(I, totitemdamage, "the [I]", ATTACK_TYPE_MELEE, I.armour_penetration, user, impacting_zone, null) & BLOCK_SUCCESS)) + var/list/block_return = list() + if((user != src) && (mob_run_block(I, totitemdamage, "the [I]", ((attackchain_flags & ATTACKCHAIN_PARRY_COUNTERATTACK)? ATTACK_TYPE_PARRY_COUNTERATTACK : NONE) | ATTACK_TYPE_MELEE, I.armour_penetration, user, impacting_zone, block_return) & BLOCK_SUCCESS)) return FALSE + totitemdamage = block_calculate_resultant_damage(totitemdamage, block_return) var/obj/item/bodypart/affecting = get_bodypart(impacting_zone) if(!affecting) //missing limb? we select the first bodypart (you can never have zero, because of chest) affecting = bodyparts[1] diff --git a/code/modules/mob/living/carbon/carbon_movement.dm b/code/modules/mob/living/carbon/carbon_movement.dm index cc390b9329..65e59d0e29 100644 --- a/code/modules/mob/living/carbon/carbon_movement.dm +++ b/code/modules/mob/living/carbon/carbon_movement.dm @@ -20,7 +20,7 @@ if(istype(J) && (movement_dir || J.stabilizers) && J.allow_thrust(0.01, src)) return 1 -/mob/living/carbon/Move(NewLoc, direct) +/mob/living/carbon/Moved() . = ..() if(. && (movement_type & FLOATING)) //floating is easy if(HAS_TRAIT(src, TRAIT_NOHUNGER)) diff --git a/code/modules/mob/living/carbon/human/human_block.dm b/code/modules/mob/living/carbon/human/human_block.dm index 8fe0376a08..4ba7e95564 100644 --- a/code/modules/mob/living/carbon/human/human_block.dm +++ b/code/modules/mob/living/carbon/human/human_block.dm @@ -1,8 +1,11 @@ /mob/living/carbon/human/get_blocking_items() . = ..() if(wear_suit) - . |= wear_suit + if(!.[wear_suit]) + .[wear_suit] = wear_suit.block_priority if(w_uniform) - . |= w_uniform + if(!.[w_uniform]) + .[w_uniform] = w_uniform.block_priority if(wear_neck) - . |= wear_neck + if(!.[wear_neck]) + .[wear_neck] = wear_neck.block_priority diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 3a2d0535e6..6f013f560a 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -78,7 +78,7 @@ ..() -/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user) +/mob/living/carbon/human/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) if(!I || !user) return 0 @@ -95,7 +95,7 @@ SSblackbox.record_feedback("tally", "zone_targeted", 1, target_area) // the attacked_by code varies among species - return dna.species.spec_attacked_by(I, user, affecting, a_intent, src) + return dna.species.spec_attacked_by(I, user, affecting, a_intent, src, attackchain_flags, damage_multiplier) /mob/living/carbon/human/attack_hulk(mob/living/carbon/human/user, does_attack_animation = FALSE) if(user.a_intent == INTENT_HARM) @@ -214,7 +214,7 @@ /mob/living/carbon/human/attack_animal(mob/living/simple_animal/M) . = ..() if(.) - var/damage = rand(M.melee_damage_lower, M.melee_damage_upper) + var/damage = . var/dam_zone = dismembering_strike(M, pick(BODY_ZONE_CHEST, BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)) if(!dam_zone) //Dismemberment successful return TRUE diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index 2ed5e349c3..e1a7a3b221 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -1700,12 +1700,14 @@ GLOBAL_LIST_EMPTY(roundstart_race_names) if("disarm") disarm(M, H, attacker_style) -/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, intent, mob/living/carbon/human/H) - var/totitemdamage = H.pre_attacked_by(I, user) +/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, intent, mob/living/carbon/human/H, attackchain_flags = NONE, damage_multiplier = 1) + var/totitemdamage = H.pre_attacked_by(I, user) * damage_multiplier // Allows you to put in item-specific reactions based on species if(user != H) - if(H.mob_run_block(I, totitemdamage, "the [I.name]", ATTACK_TYPE_MELEE, I.armour_penetration, user, affecting.body_zone, null) & BLOCK_SUCCESS) + var/list/block_return = list() + if(H.mob_run_block(I, totitemdamage, "the [I.name]", ((attackchain_flags & ATTACKCHAIN_PARRY_COUNTERATTACK)? ATTACK_TYPE_PARRY_COUNTERATTACK : NONE) | ATTACK_TYPE_MELEE, I.armour_penetration, user, affecting.body_zone, block_return) & BLOCK_SUCCESS) return 0 + totitemdamage = block_calculate_resultant_damage(totitemdamage, block_return) if(H.check_martial_melee_block()) H.visible_message("[H] blocks [I]!") return 0 @@ -1720,6 +1722,7 @@ GLOBAL_LIST_EMPTY(roundstart_race_names) var/armor_block = H.run_armor_check(affecting, "melee", "Your armor has protected your [hit_area].", "Your armor has softened a hit to your [hit_area].",I.armour_penetration) armor_block = min(90,armor_block) //cap damage reduction at 90% var/Iforce = I.force //to avoid runtimes on the forcesay checks at the bottom. Some items might delete themselves if you drop them. (stunning yourself, ninja swords) + var/weakness = H.check_weakness(I, user) apply_damage(totitemdamage * weakness, I.damtype, def_zone, armor_block, H) //CIT CHANGE - replaces I.force with totitemdamage diff --git a/code/modules/mob/living/carbon/monkey/monkey_defense.dm b/code/modules/mob/living/carbon/monkey/monkey_defense.dm index 50793eb821..8f862af8fa 100644 --- a/code/modules/mob/living/carbon/monkey/monkey_defense.dm +++ b/code/modules/mob/living/carbon/monkey/monkey_defense.dm @@ -141,7 +141,7 @@ /mob/living/carbon/monkey/attack_animal(mob/living/simple_animal/M) . = ..() if(.) - var/damage = rand(M.melee_damage_lower, M.melee_damage_upper) + var/damage = . var/dam_zone = dismembering_strike(M, pick(BODY_ZONE_CHEST, BODY_ZONE_PRECISE_L_HAND, BODY_ZONE_PRECISE_R_HAND, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)) if(!dam_zone) //Dismemberment successful return TRUE diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm index 3257b0e3bf..87d5abb89e 100644 --- a/code/modules/mob/living/damage_procs.dm +++ b/code/modules/mob/living/damage_procs.dm @@ -78,8 +78,6 @@ apply_damage(brain, BRAIN, def_zone, blocked) return 1 - - /mob/living/proc/apply_effect(effect = 0,effecttype = EFFECT_STUN, blocked = FALSE, knockdown_stamoverride, knockdown_stammax) var/hit_percent = (100-blocked)/100 if(!effect || (hit_percent <= 0)) @@ -108,7 +106,7 @@ return 1 -/mob/living/proc/apply_effects(stun = 0, knockdown = 0, unconscious = 0, irradiate = 0, slur = 0, stutter = 0, eyeblur = 0, drowsy = 0, blocked = FALSE, stamina = 0, jitter = 0, kd_stamoverride, kd_stammax) +/mob/living/proc/apply_effects(stun = 0, knockdown = 0, unconscious = 0, irradiate = 0, slur = 0, stutter = 0, eyeblur = 0, drowsy = 0, blocked = 0, stamina = 0, jitter = 0, kd_stamoverride, kd_stammax) if(blocked >= 100) return BULLET_ACT_BLOCK if(stun) diff --git a/code/modules/mob/living/life.dm b/code/modules/mob/living/life.dm index d3199d6f8b..5e04e3ec16 100644 --- a/code/modules/mob/living/life.dm +++ b/code/modules/mob/living/life.dm @@ -64,6 +64,8 @@ handle_gravity() + handle_block_parry(seconds) + if(machine) machine.check_eye(src) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index c1038fbb41..5b21abfb84 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -19,6 +19,8 @@ med_hud_set_status() /mob/living/Destroy() + end_parry_sequence() + stop_active_blocking() if(LAZYLEN(status_effects)) for(var/s in status_effects) var/datum/status_effect/S = s diff --git a/code/modules/mob/living/living_active_block.dm b/code/modules/mob/living/living_active_block.dm new file mode 100644 index 0000000000..e1b90716b6 --- /dev/null +++ b/code/modules/mob/living/living_active_block.dm @@ -0,0 +1,275 @@ +// Active directional block system. Shared code is in [living_blocking_parrying.dm] +/mob/living/proc/stop_active_blocking(was_forced = FALSE) + if(!(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING))) + return FALSE + var/obj/item/I = active_block_item + combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCKING | COMBAT_FLAG_ACTIVE_BLOCK_STARTING) + active_block_effect_end() + active_block_item = null + REMOVE_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_BLOCK_TRAIT) + REMOVE_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_BLOCK_TRAIT) + remove_movespeed_modifier(/datum/movespeed_modifier/active_block) + var/datum/block_parry_data/data = I.get_block_parry_data() + if(timeToNextMove() < data.block_end_click_cd_add) + changeNext_move(data.block_end_click_cd_add) + return TRUE + +/mob/living/proc/start_active_blocking(obj/item/I) + if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING)) + return FALSE + if(!(I in held_items)) + return FALSE + var/datum/block_parry_data/data = I.get_block_parry_data() + if(!istype(data)) //Typecheck because if an admin/coder screws up varediting or something we do not want someone being broken forever, the CRASH logs feedback so we know what happened. + CRASH("start_active_blocking called with an item with no valid data: [I] --> [I.block_parry_data]!") + combat_flags |= COMBAT_FLAG_ACTIVE_BLOCKING + active_block_item = I + if(data.block_lock_attacking) + ADD_TRAIT(src, TRAIT_MOBILITY_NOUSE, ACTIVE_BLOCK_TRAIT) //probably should be something else at some point + if(data.block_lock_sprinting) + ADD_TRAIT(src, TRAIT_SPRINT_LOCKED, ACTIVE_BLOCK_TRAIT) + add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/active_block, multiplicative_slowdown = data.block_slowdown) + active_block_effect_start() + return TRUE + +/// Visual effect setup for starting a directional block +/mob/living/proc/active_block_effect_start() + visible_message("[src] raises their [active_block_item], dropping into a defensive stance!") + animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, FALSE, SINE_EASING | EASE_OUT) + +/// Visual effect cleanup for starting a directional block +/mob/living/proc/active_block_effect_end() + visible_message("[src] lowers their [active_block_item].") + 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 + if(dir & WEST) + . -= 8 + +/mob/living/get_standard_pixel_y_offset() + . = ..() + if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING)) + if(dir & NORTH) + . += 8 + if(dir & SOUTH) + . -= 8 + +/** + * Proc called by keybindings to toggle active blocking. + */ +/mob/living/proc/keybind_toggle_active_blocking() + if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING)) + return keybind_stop_active_blocking() + else + return keybind_start_active_blocking() + +/** + * Proc called by keybindings to start active blocking. + */ +/mob/living/proc/keybind_start_active_blocking() + if(combat_flags & (COMBAT_FLAG_ACTIVE_BLOCK_STARTING | COMBAT_FLAG_ACTIVE_BLOCKING)) + return FALSE + if(!(combat_flags & COMBAT_FLAG_BLOCK_CAPABLE)) + to_chat(src, "You're not something that can actively block.") + return FALSE + // QOL: Instead of trying to just block with held item, grab first available item. + var/obj/item/I = find_active_block_item() + if(!I) + to_chat(src, "You can't block with your bare hands!") + return + if(!I.can_active_block()) + to_chat(src, "[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.)") + 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_ACTIVE)) + to_chat(src, "You must be in combat mode to actively block!") + 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 + animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = delay, FALSE, SINE_EASING | EASE_IN) + if(!do_after_advanced(src, delay, src, DO_AFTER_REQUIRES_USER_ON_TURF|DO_AFTER_NO_COEFFICIENT, CALLBACK(src, .proc/continue_starting_active_block), MOBILITY_USE, null, null, I)) + to_chat(src, "You fail to raise [I].") + combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCK_STARTING) + animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, FALSE, SINE_EASING | EASE_IN, ANIMATION_END_NOW) + return + combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCK_STARTING) + start_active_blocking(I) + +/** + * Gets the first item we can that can block, but if that fails, default to active held item.COMSIG_ENABLE_COMBAT_MODE + */ +/mob/living/proc/find_active_block_item() + var/obj/item/held = get_active_held_item() + if(!held?.can_active_block()) + for(var/obj/item/I in held_items - held) + if(I.can_active_block()) + return I + return held + +/** + * Proc called by keybindings to stop active blocking. + */ +/mob/living/proc/keybind_stop_active_blocking() + combat_flags &= ~(COMBAT_FLAG_ACTIVE_BLOCK_STARTING) + if(combat_flags & COMBAT_FLAG_ACTIVE_BLOCKING) + stop_active_blocking(FALSE) + return TRUE + +/** + * Returns if we can actively block. + */ +/obj/item/proc/can_active_block() + return block_parry_data && (item_flags & ITEM_CAN_BLOCK) + +/** + * 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) + 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) + var/limit = data.attack_type_list_scan(data.block_damage_limit_override, attack_type) + // must use isnulls to handle 0's. + if(isnull(absorption)) + absorption = data.block_damage_absorption + if(isnull(efficiency)) + efficiency = data.block_damage_multiplier + if(isnull(limit)) + limit = data.block_damage_limit + // now we calculate damage to reduce. + var/final_damage = 0 + // apply limit + if(damage > limit) //clamp and apply overrun + final_damage += (damage - limit) + damage = limit + // apply absorption + damage -= min(absorption, damage) //this way if damage is less than absorption it 0's properly. + // apply multiplier to remaining + final_damage += (damage * efficiency) + 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) + 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)) + efficiency = data.block_stamina_efficiency + var/multiplier = 1 + if(!CHECK_MOBILITY(owner, MOBILITY_STAND)) + 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 + +/// 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) + var/datum/block_parry_data/data = get_block_parry_data() + if(iscarbon(owner)) + var/mob/living/carbon/C = owner + var/held_index = C.get_held_index_of_item(src) + var/obj/item/bodypart/BP = C.hand_bodyparts[held_index] + if(!BP?.body_zone) + return C.adjustStaminaLossBuffered(stamina_amount) //nah + var/zone = BP.body_zone + var/stamina_to_zone = data.block_stamina_limb_ratio * stamina_amount + var/stamina_to_chest = stamina_amount - stamina_to_zone + var/stamina_buffered = stamina_to_chest * data.block_stamina_buffer_ratio + stamina_to_chest -= stamina_buffered + C.apply_damage(stamina_to_zone, STAMINA, zone) + C.apply_damage(stamina_to_chest, STAMINA, BODY_ZONE_CHEST) + C.adjustStaminaLossBuffered(stamina_buffered) + else + owner.adjustStaminaLossBuffered(stamina_amount) + +/obj/item/proc/on_active_block(mob/living/owner, atom/object, damage, damage_blocked, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return, override_direction) + 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) + if(!can_active_block()) + return BLOCK_NONE + var/datum/block_parry_data/data = get_block_parry_data() + if(attack_type && !(attack_type & data.can_block_attack_types)) + return BLOCK_NONE + var/incoming_direction + if(isnull(override_direction)) + if(istype(object, /obj/item/projectile)) + var/obj/item/projectile/P = object + incoming_direction = angle2dir(P.Angle) + else + 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)) + 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/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) + 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 + . = BLOCK_SHOULD_CHANGE_DAMAGE + if((final_damage <= 0) || (damage <= 0)) + . |= BLOCK_SUCCESS //full block + owner.visible_message("[owner] blocks \the [attack_text] with [src]!") + else + owner.visible_message("[owner] dampens \the [attack_text] with [src]!") + block_return[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = data.block_projectile_mitigation + if(length(data.block_sounds)) + playsound(loc, pickweight(data.block_sounds), 75, TRUE) + on_active_block(owner, object, damage, damage_blocked, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return, override_direction) + +/obj/item/proc/check_active_block(mob/living/owner, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, final_block_chance, list/block_return) + if(!can_active_block()) + return + var/incoming_direction = get_dir(get_turf(attacker) || get_turf(object), src) + if(!can_block_direction(owner.dir, incoming_direction)) + return + block_return[BLOCK_RETURN_ACTIVE_BLOCK] = TRUE + block_return[BLOCK_RETURN_ACTIVE_BLOCK_DAMAGE_MITIGATED] = damage - active_block_calculate_final_damage(owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return) + +/** + * Gets the block direction bitflags of what we can block. + */ +/obj/item/proc/blockable_directions() + var/datum/block_parry_data/data = get_block_parry_data() + return data.can_block_directions + +/** + * Checks if we can block from a specific direction from our direction. + * + * @params + * * 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) + 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()) + +/** + * can_block_direction but for "compound" directions to check all of them and return the number of directions that were blocked. + * + * @params + * * our_dir - our direction. + * * their_dirs - list of their directions as we cannot use bitfields here. + */ +/obj/item/proc/can_block_directions_multiple(our_dir, list/their_dirs) + . = FALSE + for(var/i in their_dirs) + . |= can_block_direction(our_dir, i) diff --git a/code/modules/mob/living/living_active_parry.dm b/code/modules/mob/living/living_active_parry.dm new file mode 100644 index 0000000000..50b51d4d95 --- /dev/null +++ b/code/modules/mob/living/living_active_parry.dm @@ -0,0 +1,332 @@ +// Active parry system goes in here. +/** + * Determines if we can actively parry. + */ +/obj/item/proc/can_active_parry() + 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() + if(parrying) + return // already parrying + if(!(combat_flags & COMBAT_FLAG_PARRY_CAPABLE)) + to_chat(src, "You are not something that can parry attacks.") + return + // 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/method + if(using_item?.can_active_parry()) + data = using_item.block_parry_data + method = ITEM_PARRY + else if(mind?.martial_art?.can_martial_parry) + data = mind.martial_art.block_parry_data + method = MARTIAL_PARRY + else if(combat_flags & COMBAT_FLAG_UNARMED_PARRY) + data = block_parry_data + method = UNARMED_PARRY + else + // QOL: If none of the above work, try to find another item. + var/obj/item/backup = find_backup_parry_item() + if(!backup) + to_chat(src, "You have nothing to parry with!") + return FALSE + data = backup.block_parry_data + using_item = backup + method = ITEM_PARRY + //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_ACTIVE)) + to_chat(src, "You must be in combat mode to parry!") + 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 && (next_move > world.time)) || ((parry_end_time_last + data.parry_cooldown) > world.time)) + to_chat(src, "You are not ready to parry (again)!") + return + // Point of no return, make sure everything is set. + parrying = method + if(method == ITEM_PARRY) + active_parry_item = using_item + adjustStaminaLossBuffered(data.parry_stamina_cost) + 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 + +/** + * 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()) + return I + +/** + * 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) + changeNext_move(data.parry_failed_clickcd_duration) + 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_parries = 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, final_block_chance, list/block_return, parry_efficiency, parry_time) + +/** + * 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, final_block_chance, list/block_return, parry_efficiency, parry_time) + +/** + * 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, final_block_chance, list/block_return, parry_efficiency, parry_time) + +/** + * 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) + +/** + * 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()) + var/stage = get_parry_stage() + if(stage != PARRY_ACTIVE) + 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. + 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()) + 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(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 + if(efficiency >= data.parry_efficiency_to_counterattack) + 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) + var/datum/block_parry_data/data = get_parry_data() + if(data.parry_sounds) + playsound(src, pick(data.parry_sounds), 75) + visible_message("[src] parries \the [attack_text][length(effect_text)? ", [english_list(effect_text)] [attacker]" : ""]!") + +/// Run counterattack if any +/mob/living/proc/run_parry_countereffects(atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), 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() + // 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, ATTACKCHAIN_PARRY_COUNTERATTACK, 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_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 + /// 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/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) diff --git a/code/modules/mob/living/living_block.dm b/code/modules/mob/living/living_block.dm index 817863f257..d32265e478 100644 --- a/code/modules/mob/living/living_block.dm +++ b/code/modules/mob/living/living_block.dm @@ -1,31 +1,6 @@ // This file has a weird name, but it's for anything related to the checks for shields, blocking, dodging, // and similar "stop this attack before it actually impacts the target" as opposed to "defend once it has hit". -/* -/// You can find the mob_check_block() and mob_run_block() macros in __DEFINES/combat.dm - -/// Bitflags for check_block() and run_block(). Meant to be combined. You can be hit and still reflect, for example, if you do not use BLOCK_SUCCESS. -/// Attack was not blocked -#define BLOCK_NONE NONE -/// Attack was blocked, do not do damage. THIS FLAG MUST BE THERE FOR DAMAGE/EFFECT PREVENTION! -#define BLOCK_SUCCESS (1<<1) - -/// The below are for "metadata" on "how" the attack was blocked. - -/// Attack was and should be reflected (NOTE: the SHOULD here is important, as it says "the thing blocking isn't handling the reflecting for you so do it yourself"!) -#define BLOCK_SHOULD_REFLECT (1<<2) -/// Attack was manually redirected (including reflected) by any means by the defender. For when YOU are handling the reflection, rather than the thing hitting you. (see sleeping carp) -#define BLOCK_REDIRECTED (1<<3) -/// Attack was blocked by something like a shield. -#define BLOCK_PHYSICAL_EXTERNAL (1<<4) -/// Attack was blocked by something worn on you. -#define BLOCK_PHYSICAL_INTERNAL (1<<5) -/// Attack should pass through. Like SHOULD_REFLECT but for.. well, passing through harmlessly. -#define BLOCK_SHOULD_PASSTHROUGH (1<<6) -/// Attack outright missed because the target dodged. Should usually be combined with SHOULD_PASSTHROUGH or something (see martial arts) -#define BLOCK_TARGET_DODGED (1<<7) -*/ - /** The actual proc for block checks. DO NOT USE THIS DIRECTLY UNLESS YOU HAVE VERY GOOD REASON TO. To reduce copypaste for differences between handling for real attacks and virtual checks. * Automatically checks all held items for /obj/item/proc/run_block() with the same parameters. * @params @@ -39,21 +14,31 @@ * attacker - Set to the mob attacking IF KNOWN. Do not expect this to always be set! * def_zone - The zone this'll impact. * return_list - If something wants to grab things from what items/whatever put into list/block_return on obj/item/run_block and the comsig, pass in a list so you can grab anything put in it after block runs. + * attack_direction - Direction of the attack. It is highly recommended to put this in, as the automatic guesswork that's done otherwise is quite inaccurate at times. */ -/mob/living/proc/do_run_block(real_attack = TRUE, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list()) +/mob/living/proc/do_run_block(real_attack = TRUE, atom/object, damage, attack_text, attack_type, armour_penetration, mob/attacker, def_zone, list/return_list = list(), attack_direction) + if(real_attack) + . = run_parry(object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list) //Parry - Highest priority! + if((. & BLOCK_SUCCESS) && !(. & BLOCK_CONTINUE_CHAIN)) + return // Component signal block runs have highest priority.. for now. - . = SEND_SIGNAL(src, COMSIG_LIVING_RUN_BLOCK, real_attack, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list) + . = SEND_SIGNAL(src, COMSIG_LIVING_RUN_BLOCK, real_attack, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, return_list, attack_direction) if((. & BLOCK_SUCCESS) && !(. & BLOCK_CONTINUE_CHAIN)) + return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = 100 return var/list/obj/item/tocheck = get_blocking_items() - sortTim(tocheck, /proc/cmp_item_block_priority_asc) + sortTim(tocheck, /proc/cmp_numeric_dsc, TRUE) // i don't like this var/block_chance_modifier = round(damage / -3) if(real_attack) for(var/obj/item/I in tocheck) // i don't like this too var/final_block_chance = I.block_chance - (clamp((armour_penetration-I.armour_penetration)/2,0,100)) + block_chance_modifier //So armour piercing blades can still be parried by other blades, for example - var/results = I.run_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list) + 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 + results = I.run_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list) . |= results if((results & BLOCK_SUCCESS) && !(results & BLOCK_CONTINUE_CHAIN)) break @@ -61,17 +46,29 @@ for(var/obj/item/I in tocheck) // i don't like this too var/final_block_chance = I.block_chance - (clamp((armour_penetration-I.armour_penetration)/2,0,100)) + block_chance_modifier //So armour piercing blades can still be parried by other blades, for example - I.check_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list) + if(I == active_block_item) //block is long termed enough we give a damn. parry, not so much. + I.check_active_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list, attack_direction) + else + I.check_block(src, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, return_list) + if(. & BLOCK_SUCCESS) + return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = 100 + else if(isnull(return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE])) + return_list[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] = return_list[BLOCK_RETURN_MITIGATION_PERCENT] -/// Gets an unsortedlist of objects to run block checks on. +/// Gets an unsortedlist of objects to run block checks on. List must have associative values for priorities! /mob/living/proc/get_blocking_items() . = list() + if(active_block_item) + var/datum/block_parry_data/data = active_block_item.get_block_parry_data() + .[active_block_item] = data.block_active_priority SEND_SIGNAL(src, COMSIG_LIVING_GET_BLOCKING_ITEMS, .) for(var/obj/item/I in held_items) // this is a bad check but i am not removing it until a better catchall is made if(istype(I, /obj/item/clothing)) continue - . |= I + if(.[I]) //don't override block/parry. + continue + .[I] = I.block_priority /obj/item /// The 0% to 100% chance for the default implementation of random block rolls. @@ -95,3 +92,15 @@ SEND_SIGNAL(src, COMSIG_ITEM_CHECK_BLOCK, owner, object, damage, attack_text, attack_type, armour_penetration, attacker, def_zone, final_block_chance, block_return) var/existing = block_return[BLOCK_RETURN_NORMAL_BLOCK_CHANCE] block_return[BLOCK_RETURN_NORMAL_BLOCK_CHANCE] = max(existing || 0, final_block_chance) + +// HELPER PROCS + +/** + * Considers a block return_list and calculates damage to use from that. + */ +/proc/block_calculate_resultant_damage(damage, list/block_return) + if(!isnull(block_return[BLOCK_RETURN_SET_DAMAGE_TO])) // higher priority + return block_return[BLOCK_RETURN_SET_DAMAGE_TO] + else if(!isnull(block_return[BLOCK_RETURN_MITIGATION_PERCENT])) + return damage * ((100 - block_return[BLOCK_RETURN_MITIGATION_PERCENT]) * 0.01) + return damage diff --git a/code/modules/mob/living/living_blocking_parrying.dm b/code/modules/mob/living/living_blocking_parrying.dm new file mode 100644 index 0000000000..9f1ad1c27a --- /dev/null +++ b/code/modules/mob/living/living_blocking_parrying.dm @@ -0,0 +1,310 @@ +// yell at me later for file naming +// This file contains stuff relating to the new directional blocking and parry system. +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 + /// Attacks we can block + var/can_block_attack_types = ALL + /// Our slowdown added while blocking + var/block_slowdown = 1 + /// Clickdelay added to user after block ends + var/block_end_click_cd_add = 0 + /// Disallow attacking during block + var/block_lock_attacking = TRUE + /// Disallow sprinting during block + var/block_lock_sprinting = FALSE + /// 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 to allow through above absorption and below limit. Multiplied by damage to determine how much to let through. Lower is better. + 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 + + /// The blocked variable of on_hit() on projectiles is impacted by this. Higher is better, 0 to 100, percentage. + var/block_projectile_mitigation = 50 + + /* + * 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 = TRUE + /// 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 + + /// 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 + // Visual overrides + /// If set, overrides visual duration of windup + var/parry_time_windup_visual_override + /// If set, overrides visual duration of active period + var/parry_time_active_visual_override + /// If set, overrides visual duration of spindown + var/parry_time_spindown_visual_override + /// 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_COUNTERATTACK_MELEE_ATTACK_CHAIN = 1 + ) + /// 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 + /// 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 + var/parry_effect_icon_state = "parry_bm_hold" + /// Parrying cooldown, separate of clickdelay. It must be this much deciseconds since their last parry for them to parry with this object. + var/parry_cooldown = 0 + /// Parry start sound + var/parry_start_sound = 'sound/block_parry/sfx-parry.ogg' + /// 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 + /// Clickdelay duration post-parry if you fail to parry an attack + var/parry_failed_clickcd_duration = 2 SECONDS + +/** + * 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 & text2num(flagtext)) + total += L[flagtext] + div++ + // if none, return null. + if(!div) + return + return total/div //groan + + +/** + * Gets the percentage efficiency of our parry. + * + * Returns a percentage in normal 0 to 100 scale, but not clamped to just 0 to 100. + * This is a proc to allow for overriding. + * @params + * * attack_type - int, bitfield of the attack type(s) + * * parry_time - deciseconds since start of the parry. + */ +/datum/block_parry_data/proc/get_parry_efficiency(attack_type, parry_time) + var/difference = abs(parry_time - (parry_time_perfect + parry_time_windup)) + var/leeway = attack_type_list_scan(parry_time_perfect_leeway_override, attack_type) + if(isnull(leeway)) + leeway = parry_time_perfect_leeway + difference -= leeway + . = parry_efficiency_perfect + if(difference <= 0) + return + var/falloff = attack_type_list_scan(parry_imperfect_falloff_percent_override, attack_type) + if(isnull(falloff)) + falloff = parry_imperfect_falloff_percent + . -= falloff * difference + +#define RENDER_VARIABLE_SIMPLE(varname, desc) dat += "[#varname]
[desc][varname]" +#define RENDER_OVERRIDE_LIST(varname, desc) \ + dat += "[#varname]
[desc]"; \ + var/list/assembled__##varname = list(); \ + for(var/textbit in varname){ \ + assembled__##varname += "[GLOB.attack_type_names[textbit]] = [varname[textbit]]"; \ + } \ + dat += "[english_list(assembled__##varname)]"; +#define RENDER_ATTACK_TYPES(varname, desc) dat += "[#varname]
[desc]"; \ + var/list/assembled__##varname = list(); \ + for(var/bit in bitfield2list(varname)){ \ + var/name = GLOB.attack_type_names[num2text(bit)]; \ + if(name){ \ + assembled__##varname += "[name]"; \ + } \ + } \ + dat += "[english_list(assembled__##varname)]"; +#define RENDER_BLOCK_DIRECTIONS(varname, desc) \ + dat += "[#varname]
[desc]"; \ + var/list/assembled__##varname = list(); \ + for(var/bit in bitfield2list(varname)){ \ + var/name = GLOB.block_direction_names[num2text(bit)]; \ + if(name){ \ + assembled__##varname += "[name]"; \ + } \ + } \ + dat += "[english_list(assembled__##varname)]"; + +/datum/block_parry_data/Topic(href, href_list) + . = ..() + if(.) + return + if(href_list["render"]) + var/datum/browser/B = new(usr, REF(src), href_list["name"], 800, 1000) + B.set_content(render_html_readout(href_list["block"], href_list["parry"])) + B.open() + +/** + * Generates a HTML render of this datum for self-documentation + * Maybe make this tgui-next someday haha god this is ugly as sin. + * Does NOT include the popout or title or anything. Just the variables and explanations.. + */ +/datum/block_parry_data/proc/render_html_readout(block_data = FALSE, parry_data = FALSE) + var/list/dat = list() + if(block_data) + dat += "

Block Stats

" + RENDER_BLOCK_DIRECTIONS(can_block_directions, "Which directions this can block in.") + RENDER_ATTACK_TYPES(can_block_attack_types, "The kinds of attacks this can block.") + RENDER_VARIABLE_SIMPLE(block_slowdown, "How much slowdown is applied to the user while blocking. Lower is better.") + RENDER_VARIABLE_SIMPLE(block_end_click_cd_add, "How much click delay in deciseconds is applied to the user when blocking ends. Lower is better.") + RENDER_VARIABLE_SIMPLE(block_lock_attacking, "Whether or not (1 or 0) the user is locked from atacking and/or item usage while blocking.") + RENDER_VARIABLE_SIMPLE(block_active_priority, "The priority of this item in the block sequence. This will probably mean nothing to you unless you are a coder.") + RENDER_VARIABLE_SIMPLE(block_start_delay, "The amount of time in deciseconds it takes to start a block with this item. Lower is better.") + RENDER_VARIABLE_SIMPLE(block_damage_absorption, "The amount of damage that is absorbed by default. Higher is better.") + RENDER_OVERRIDE_LIST(block_damage_absorption_override, "Overrides for the above for each attack type") + RENDER_VARIABLE_SIMPLE(block_damage_multiplier, "Damage between absorption and limit is multiplied by this. Lower is better.") + RENDER_OVERRIDE_LIST(block_damage_multiplier_override, "Overrides for the above for each attack type") + RENDER_VARIABLE_SIMPLE(block_damage_limit, "Damage above this passes right through and is not impacted. Higher is better.") + RENDER_OVERRIDE_LIST(block_damage_limit_override, "Overrides for the above for each attack type.") + RENDER_VARIABLE_SIMPLE(block_stamina_efficiency, "Coefficient for stamina damage dealt to user by damage blocked. Higher is better.") + RENDER_OVERRIDE_LIST(block_stamina_efficiency_override, "Overrides for the above for each attack type.") + RENDER_VARIABLE_SIMPLE(block_stamina_limb_ratio, "The ratio of stamina that is applied to the limb holding this object (if applicable) rather than whole body/chest.") + RENDER_VARIABLE_SIMPLE(block_stamina_buffer_ratio, "The ratio of stamina incurred by chest/whole body that is buffered rather than direct (buffer = your stamina buffer, direct = direct stamina damage like from a disabler.)") + RENDER_VARIABLE_SIMPLE(block_stamina_cost_per_second, "The buffered stamina damage the user incurs per second of block. Lower is better.") + RENDER_ATTACK_TYPES(block_resting_attack_types_anydir, "The kinds of attacks you can block while resting/otherwise knocked to the floor from any direction. can_block_attack_types takes precedence.") + RENDER_ATTACK_TYPES(block_resting_attack_types_directional, "The kinds of attacks you can block wihle resting/otherwise knocked to the floor that are directional only. can_block_attack_types takes precedence.") + RENDER_VARIABLE_SIMPLE(block_resting_stamina_penalty_multiplier, "Multiplier to stamina damage incurred from blocking while downed. Lower is better.") + RENDER_OVERRIDE_LIST(block_resting_stamina_penalty_multiplier, "Overrides for the above for each attack type.") + dat += "
Name/DescriptionValue
" + if(parry_data) + dat += "

Parry Stats

" + RENDER_VARIABLE_SIMPLE(parry_respect_clickdelay, "Whether or not (1 or 0) you can only parry if your attack cooldown isn't in effect.") + RENDER_VARIABLE_SIMPLE(parry_stamina_cost, "Buffered stamina damage incurred by you for parrying with this.") + RENDER_ATTACK_TYPES(parry_attack_types, "Attack types you can parry.") + // parry_flags + dat += "" + RENDER_VARIABLE_SIMPLE(parry_time_windup, "Deciseconds of parry windup.") + RENDER_VARIABLE_SIMPLE(parry_time_spindown, "Deciseconds of parry spindown.") + RENDER_VARIABLE_SIMPLE(parry_time_active, "Deciseconds of active parry window - This is the ONLY time your parry is active.") + RENDER_VARIABLE_SIMPLE(parry_time_windup_visual_override, "Visual effect length override") + RENDER_VARIABLE_SIMPLE(parry_time_spindown_visual_override, "Visual effect length override") + RENDER_VARIABLE_SIMPLE(parry_time_active_visual_override, "Visual effect length override") + RENDER_VARIABLE_SIMPLE(parry_time_perfect, "Deciseconds into the active window considered the 'center' of the perfect period.") + RENDER_VARIABLE_SIMPLE(parry_time_perfect_leeway, "Leeway on both sides of the perfect period's center still considered perfect.") + RENDER_OVERRIDE_LIST(parry_time_perfect_leeway_override, "Override for the above for each attack type") + RENDER_VARIABLE_SIMPLE(parry_imperfect_falloff_percent, "Linear falloff in percent per decisecond for attacks parried outside of perfect window.") + RENDER_OVERRIDE_LIST(parry_imperfect_falloff_percent_override, "Override for the above for each attack type") + RENDER_VARIABLE_SIMPLE(parry_efficiency_perfect, "Efficiency in percentage a parry in the perfect window is considered.") + // parry_data + dat += "" + RENDER_VARIABLE_SIMPLE(parry_efficiency_considered_successful, "Minimum parry efficiency to be considered a successful parry.") + RENDER_VARIABLE_SIMPLE(parry_efficiency_to_counterattack, "Minimum parry efficiency to trigger counterattack effects.") + RENDER_VARIABLE_SIMPLE(parry_max_attacks, "Max attacks parried per parry cycle.") + RENDER_VARIABLE_SIMPLE(parry_effect_icon_state, "Parry effect image name") + RENDER_VARIABLE_SIMPLE(parry_cooldown, "Deciseconds it has to be since the last time a parry sequence ended 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 += "
Name/DescriptionValue
" + return dat.Join("") +#undef RENDER_VARIABLE_SIMPLE +#undef RENDER_OVERRIDE_LIST +#undef RENDER_ATTACK_TYPES +#undef RENDER_BLOCK_DIRECTIONS + +// MOB PROCS + +/** + * Called every life tick to handle blocking/parrying effects. + */ +/mob/living/proc/handle_block_parry(seconds = 1) + if(combat_flags & COMBAT_FLAG_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) + +/mob/living/on_item_dropped(obj/item/I) + if(I == active_block_item) + stop_active_blocking() + if(I == active_parry_item) + end_parry_sequence() + return ..() diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 17eb51b154..2ca1b63b14 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -66,22 +66,30 @@ CRASH("Invalid rediretion mode [redirection_mode]") /mob/living/bullet_act(obj/item/projectile/P, def_zone) + var/totaldamage = P.damage + var/final_percent = 0 if(P.original != src || P.firer != src) //try to block or reflect the bullet, can't do so when shooting oneself var/list/returnlist = list() var/returned = mob_run_block(P, P.damage, "the [P.name]", ATTACK_TYPE_PROJECTILE, P.armour_penetration, P.firer, def_zone, returnlist) + final_percent = returnlist[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE] if(returned & BLOCK_SHOULD_REDIRECT) handle_projectile_attack_redirection(P, returnlist[BLOCK_RETURN_REDIRECT_METHOD]) if(returned & BLOCK_REDIRECTED) return BULLET_ACT_FORCE_PIERCE if(returned & BLOCK_SUCCESS) - P.on_hit(src, 100, def_zone) + P.on_hit(src, final_percent, def_zone) return BULLET_ACT_BLOCK + totaldamage = block_calculate_resultant_damage(totaldamage, returnlist) var/armor = run_armor_check(def_zone, P.flag, null, null, P.armour_penetration, null) if(!P.nodamage) - apply_damage(P.damage, P.damage_type, def_zone, armor) + apply_damage(totaldamage, P.damage_type, def_zone, armor) if(P.dismemberment) check_projectile_dismemberment(P, def_zone) - return P.on_hit(src, armor) ? BULLET_ACT_HIT : BULLET_ACT_BLOCK + var/missing = 100 - final_percent + var/armor_ratio = armor * 0.01 + if(missing > 0) + final_percent += missing * armor_ratio + return P.on_hit(src, final_percent, def_zone) ? BULLET_ACT_HIT : BULLET_ACT_BLOCK /mob/living/proc/check_projectile_dismemberment(obj/item/projectile/P, def_zone) return 0 @@ -111,10 +119,13 @@ I = AM throwpower = I.throwforce var/impacting_zone = ran_zone(BODY_ZONE_CHEST, 65)//Hits a random part of the body, geared towards the chest - if(mob_run_block(AM, throwpower, "\the [AM.name]", ATTACK_TYPE_THROWN, 0, throwingdatum?.thrower, impacting_zone, null) & BLOCK_SUCCESS) + var/list/block_return = list() + var/total_damage = I.throwforce + if(mob_run_block(AM, throwpower, "\the [AM.name]", ATTACK_TYPE_THROWN, 0, throwingdatum?.thrower, impacting_zone, block_return) & BLOCK_SUCCESS) hitpush = FALSE skipcatch = TRUE blocked = TRUE + total_damage = block_calculate_resultant_damage(total_damage, block_return) else if(I && I.throw_speed >= EMBED_THROWSPEED_THRESHOLD && can_embed(I, src) && prob(I.embedding.embed_chance) && !HAS_TRAIT(src, TRAIT_PIERCEIMMUNE) && (!HAS_TRAIT(src, TRAIT_AUTO_CATCH_ITEM) || incapacitated() || get_active_held_item())) embed_item(I) hitpush = FALSE @@ -143,7 +154,7 @@ visible_message("[src] has been hit by [I].", \ "You have been hit by [I].") var/armor = run_armor_check(impacting_zone, "melee", "Your armor has protected your [parse_zone(impacting_zone)].", "Your armor has softened hit to your [parse_zone(impacting_zone)].",I.armour_penetration) - apply_damage(I.throwforce, dtype, impacting_zone, armor) + apply_damage(total_damage, dtype, impacting_zone, armor) if(I.thrownby) log_combat(I.thrownby, src, "threw and hit", I) else @@ -313,8 +324,10 @@ var/damage = rand(5, 35) if(M.is_adult) damage = rand(20, 40) - if(mob_run_block(M, damage, "the [M.name]", ATTACK_TYPE_MELEE, null, M, check_zone(M.zone_selected), null) & BLOCK_SUCCESS) + var/list/block_return = list() + if(mob_run_block(M, damage, "the [M.name]", ATTACK_TYPE_MELEE, null, M, check_zone(M.zone_selected), block_return) & BLOCK_SUCCESS) return FALSE + damage = block_calculate_resultant_damage(damage, block_return) if (stat != DEAD) log_combat(M, src, "attacked") @@ -330,13 +343,16 @@ M.visible_message("\The [M] [M.friendly_verb_continuous] [src]!", "You [M.friendly_verb_simple] [src]!", target = src, target_message = "\The [M] [M.friendly_verb_continuous] you!") - return FALSE + return 0 else if(HAS_TRAIT(M, TRAIT_PACIFISM)) to_chat(M, "You don't want to hurt anyone!") return FALSE - if(mob_run_block(M, rand(M.melee_damage_lower, M.melee_damage_upper), "the [M.name]", ATTACK_TYPE_MELEE, M.armour_penetration, M, check_zone(M.zone_selected), null) & BLOCK_SUCCESS) - return FALSE + var/damage = rand(M.melee_damage_lower, M.melee_damage_upper) + var/list/return_list = list() + if(mob_run_block(M, damage, "the [M.name]", ATTACK_TYPE_MELEE, M.armour_penetration, M, check_zone(M.zone_selected), return_list) & BLOCK_SUCCESS) + return 0 + damage = block_calculate_resultant_damage(damage, return_list) if(M.attack_sound) playsound(loc, M.attack_sound, 50, 1, 1) M.do_attack_animation(src) @@ -344,7 +360,7 @@ "\The [M] [M.attack_verb_continuous] you!", null, COMBAT_MESSAGE_RANGE, null, M, "You [M.attack_verb_simple] [src]!") log_combat(M, src, "attacked") - return TRUE + return damage /mob/living/attack_paw(mob/living/carbon/monkey/M) if (M.a_intent == INTENT_HARM) diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index da24f190e8..b037221e2c 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -29,6 +29,26 @@ var/mobility_flags = MOBILITY_FLAGS_DEFAULT + // Combat - Blocking/Parrying system + /// 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 = /datum/block_parry_data // defaults to *something* because [combat_flags] dictates whether or not we can unarmed block/parry. + // Blocking + /// The item the user is actively blocking with if any. + var/obj/item/active_block_item + // Parrying + /// Whether or 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 + /// Current parry effect. + var/obj/effect/abstract/parry/parry_visual_effect + /// world.time of last parry end + var/parry_end_time_last = 0 + /// Successful parries within the current parry cycle. It's a list of efficiency percentages. + var/list/successful_parries + var/confused = 0 //Makes the mob move in random directions. var/hallucination = 0 //Directly affects how long a mob will hallucinate for diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm index 4b90191dcc..71bcef9aca 100644 --- a/code/modules/mob/living/living_movement.dm +++ b/code/modules/mob/living/living_movement.dm @@ -3,10 +3,21 @@ update_turf_movespeed(loc) //Hide typing indicator if we move. clear_typing_indicator() - if(is_shifted) - is_shifted = FALSE - pixel_x = get_standard_pixel_x_offset(lying) - pixel_y = get_standard_pixel_y_offset(lying) + update_pixel_shifting(TRUE) + +/mob/living/setDir(newdir, ismousemovement) + . = ..() + if(ismousemovement) + update_pixel_shifting() + +/mob/living/proc/update_pixel_shifting(moved = FALSE) + if(combat_flags & COMBAT_FLAG_ACTIVE_BLOCKING) + animate(src, pixel_x = get_standard_pixel_x_offset(), pixel_y = get_standard_pixel_y_offset(), time = 2.5, flags = ANIMATION_END_NOW) + else if(moved) + if(is_shifted) + is_shifted = FALSE + pixel_x = get_standard_pixel_x_offset(lying) + pixel_y = get_standard_pixel_y_offset(lying) /mob/living/CanPass(atom/movable/mover, turf/target) if((mover.pass_flags & PASSMOB)) diff --git a/code/modules/mob/living/silicon/ai/ai_defense.dm b/code/modules/mob/living/silicon/ai/ai_defense.dm index 2bcb3c9b5a..9cefa7a12f 100644 --- a/code/modules/mob/living/silicon/ai/ai_defense.dm +++ b/code/modules/mob/living/silicon/ai/ai_defense.dm @@ -1,4 +1,4 @@ -/mob/living/silicon/ai/attacked_by(obj/item/I, mob/living/user, def_zone) +/mob/living/silicon/ai/attacked_by(obj/item/I, mob/living/user, def_zone, attackchain_flags = NONE, damage_multiplier = 1) . = ..() if(!.) return FALSE diff --git a/code/modules/mob/living/silicon/silicon_defense.dm b/code/modules/mob/living/silicon/silicon_defense.dm index cd50ffbae6..0850f0f886 100644 --- a/code/modules/mob/living/silicon/silicon_defense.dm +++ b/code/modules/mob/living/silicon/silicon_defense.dm @@ -33,7 +33,7 @@ /mob/living/silicon/attack_animal(mob/living/simple_animal/M) . = ..() if(.) - var/damage = rand(M.melee_damage_lower, M.melee_damage_upper) + var/damage = . if(prob(damage)) for(var/mob/living/N in buckled_mobs) N.DefaultCombatKnockdown(20) @@ -120,6 +120,7 @@ flash_act(affect_silicon = 1) /mob/living/silicon/bullet_act(obj/item/projectile/P, def_zone) + var/totaldamage = P.damage if(P.original != src || P.firer != src) //try to block or reflect the bullet, can't do so when shooting oneself var/list/returnlist = list() var/returned = mob_run_block(P, P.damage, "the [P.name]", ATTACK_TYPE_PROJECTILE, P.armour_penetration, P.firer, def_zone, returnlist) @@ -128,22 +129,18 @@ if(returned & BLOCK_REDIRECTED) return BULLET_ACT_FORCE_PIERCE if(returned & BLOCK_SUCCESS) - P.on_hit(src, 100, def_zone) + P.on_hit(src, returnlist[BLOCK_RETURN_PROJECTILE_BLOCK_PERCENTAGE], def_zone) return BULLET_ACT_BLOCK + totaldamage = block_calculate_resultant_damage(totaldamage, returnlist) if((P.damage_type == BRUTE || P.damage_type == BURN)) - adjustBruteLoss(P.damage) - if(prob(P.damage*1.5)) - for(var/mob/living/M in buckled_mobs) - M.visible_message("[M] is knocked off of [src]!", - "You are knocked off of [src]!") - unbuckle_mob(M) - M.DefaultCombatKnockdown(40) - if(P.stun || P.knockdown) + adjustBruteLoss(totaldamage) + if((P.damage >= 10) || P.stun || P.knockdown || (P.stamina >= 20)) for(var/mob/living/M in buckled_mobs) - unbuckle_mob(M) M.visible_message("[M] is knocked off of [src] by the [P]!", "You are knocked off of [src] by the [P]!") - P.on_hit(src) + unbuckle_mob(M) + M.DefaultCombatKnockdown(40) + P.on_hit(src, 0, def_zone) return BULLET_ACT_HIT /mob/living/silicon/flash_act(intensity = 1, override_blindness_check = 0, affect_silicon = 0, visual = 0, type = /obj/screen/fullscreen/flash/static) diff --git a/code/modules/mob/living/simple_animal/animal_defense.dm b/code/modules/mob/living/simple_animal/animal_defense.dm index 1e56e91ae1..278bb37d0d 100644 --- a/code/modules/mob/living/simple_animal/animal_defense.dm +++ b/code/modules/mob/living/simple_animal/animal_defense.dm @@ -94,7 +94,7 @@ /mob/living/simple_animal/attack_animal(mob/living/simple_animal/M) . = ..() if(.) - var/damage = rand(M.melee_damage_lower, M.melee_damage_upper) + var/damage = . return attack_threshold_check(damage, M.melee_damage_type) /mob/living/simple_animal/attack_slime(mob/living/simple_animal/slime/M) diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index e41fa7c896..e8991df358 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -117,7 +117,7 @@ Move(get_step(src,chosen_dir)) face_atom(target) //Looks better if they keep looking at you when dodging -/mob/living/simple_animal/hostile/attacked_by(obj/item/I, mob/living/user) +/mob/living/simple_animal/hostile/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) if(stat == CONSCIOUS && !target && AIStatus != AI_OFF && !client && user) FindTarget(list(user), 1) return ..() diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm index 9d395ca5ff..c4f78b6e26 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/curse_blob.dm @@ -103,7 +103,7 @@ IGNORE_PROC_IF_NOT_TARGET(attack_slime) return BULLET_ACT_FORCE_PIERCE return ..() -/mob/living/simple_animal/hostile/asteroid/curseblob/attacked_by(obj/item/I, mob/living/L) +/mob/living/simple_animal/hostile/asteroid/curseblob/attacked_by(obj/item/I, mob/living/L, attackchain_flags = NONE, damage_multiplier = 1) if(L != set_target) L.changeNext_move(I.click_delay) //pre_attacked_by not called return diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index e14ff2f721..4c692e7175 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1086,3 +1086,10 @@ GLOBAL_VAR_INIT(exploit_warn_spam_prevention, 0) for(var/obj/item/I in held_items) if(I.item_flags & SLOWS_WHILE_IN_HAND) . += I.slowdown + +/** + * Mostly called by doUnEquip() + * Like item dropped() on mob side. + */ +/mob/proc/on_item_dropped(obj/item/I) + return diff --git a/code/modules/movespeed/modifiers/mobs.dm b/code/modules/movespeed/modifiers/mobs.dm index d20fc4c6c0..a2176ca95e 100644 --- a/code/modules/movespeed/modifiers/mobs.dm +++ b/code/modules/movespeed/modifiers/mobs.dm @@ -119,3 +119,7 @@ /datum/movespeed_modifier/liver_cirrhosis blacklisted_movetypes = FLOATING variable = TRUE + +/datum/movespeed_modifier/active_block + variable = TRUE + flags = IGNORE_NOSLOW diff --git a/code/modules/power/lighting.dm b/code/modules/power/lighting.dm index 3c3f3c9541..c18eebbb55 100644 --- a/code/modules/power/lighting.dm +++ b/code/modules/power/lighting.dm @@ -506,7 +506,7 @@ cell = null qdel(src) -/obj/machinery/light/attacked_by(obj/item/I, mob/living/user) +/obj/machinery/light/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) ..() if(status == LIGHT_BROKEN || status == LIGHT_EMPTY) if(on && (I.flags_1 & CONDUCT_1)) diff --git a/code/modules/projectiles/guns/ballistic/laser_gatling.dm b/code/modules/projectiles/guns/ballistic/laser_gatling.dm index 7029cd4071..c2dd5bb42d 100644 --- a/code/modules/projectiles/guns/ballistic/laser_gatling.dm +++ b/code/modules/projectiles/guns/ballistic/laser_gatling.dm @@ -53,6 +53,7 @@ ..() /obj/item/minigunpack/dropped(mob/user) + . = ..() if(armed) user.dropItemToGround(gun, TRUE) @@ -125,6 +126,7 @@ return /obj/item/gun/ballistic/minigun/dropped(mob/user) + . = ..() if(ammo_pack) ammo_pack.attach_gun(user) else @@ -144,4 +146,5 @@ . = ..() /obj/item/gun/ballistic/minigun/dropped(mob/living/user) + . = ..() ammo_pack.attach_gun(user) diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm index a4de6f94f5..319ec16345 100644 --- a/code/modules/projectiles/guns/ballistic/pistol.dm +++ b/code/modules/projectiles/guns/ballistic/pistol.dm @@ -115,6 +115,7 @@ icon_state = "flatgun" /obj/item/gun/ballistic/automatic/pistol/stickman/pickup(mob/living/user) + . = ..() to_chat(user, "As you try to pick up [src], it slips out of your grip..") if(prob(50)) to_chat(user, "..and vanishes from your vision! Where the hell did it go?") diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index a5938c2758..3705f7902c 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -162,7 +162,15 @@ /obj/item/projectile/proc/prehit(atom/target) return TRUE -/obj/item/projectile/proc/on_hit(atom/target, blocked = FALSE) +/** + * Called when we hit something. + * + * @params + * * target - what we hit + * * blocked - 0 to 100 percentage mitigation/block + * * def zone - where we hit if we hit a mob. + */ +/obj/item/projectile/proc/on_hit(atom/target, blocked = 0, def_zone) if(fired_from) SEND_SIGNAL(fired_from, COMSIG_PROJECTILE_ON_HIT, firer, target, Angle) var/turf/target_loca = get_turf(target) diff --git a/code/modules/vehicles/cars/car.dm b/code/modules/vehicles/cars/car.dm index 958a4e65c4..d45cb8d26f 100644 --- a/code/modules/vehicles/cars/car.dm +++ b/code/modules/vehicles/cars/car.dm @@ -49,7 +49,7 @@ mob_exit(M, silent) return TRUE -/obj/vehicle/sealed/car/attacked_by(obj/item/I, mob/living/user) +/obj/vehicle/sealed/car/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) if(!I.force) return FALSE if(occupants[user]) @@ -85,4 +85,4 @@ if(!silent) M.visible_message("[M] is forced into \the [src]!") M.forceMove(src) - add_occupant(M, VEHICLE_CONTROL_KIDNAPPED) \ No newline at end of file + add_occupant(M, VEHICLE_CONTROL_KIDNAPPED) diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm index 2b9890b863..cd21b61dbe 100644 --- a/code/modules/vehicles/cars/clowncar.dm +++ b/code/modules/vehicles/cars/clowncar.dm @@ -36,7 +36,7 @@ visible_message("[src] spews out a ton of space lube!") new /obj/effect/particle_effect/foam(loc) //YEET -/obj/vehicle/sealed/car/clowncar/attacked_by(obj/item/I, mob/living/user) +/obj/vehicle/sealed/car/clowncar/attacked_by(obj/item/I, mob/living/user, attackchain_flags = NONE, damage_multiplier = 1) . = ..() if(istype(I, /obj/item/reagent_containers/food/snacks/grown/banana)) var/obj/item/reagent_containers/food/snacks/grown/banana/banana = I @@ -129,4 +129,4 @@ icon_state = initial(icon_state) /obj/vehicle/sealed/car/clowncar/proc/StopDroppingOil() - droppingoil = FALSE \ No newline at end of file + droppingoil = FALSE diff --git a/icons/effects/block_parry.dmi b/icons/effects/block_parry.dmi new file mode 100644 index 0000000000..4a0ade5e61 Binary files /dev/null and b/icons/effects/block_parry.dmi differ diff --git a/sound/block_parry/block_metal1.ogg b/sound/block_parry/block_metal1.ogg new file mode 100644 index 0000000000..c0f98249cd Binary files /dev/null and b/sound/block_parry/block_metal1.ogg differ diff --git a/sound/block_parry/block_metal2.ogg b/sound/block_parry/block_metal2.ogg new file mode 100644 index 0000000000..58c1233cea Binary files /dev/null and b/sound/block_parry/block_metal2.ogg differ diff --git a/sound/block_parry/block_wood1.ogg b/sound/block_parry/block_wood1.ogg new file mode 100644 index 0000000000..a01f3dbe01 Binary files /dev/null and b/sound/block_parry/block_wood1.ogg differ diff --git a/sound/block_parry/block_wood2.ogg b/sound/block_parry/block_wood2.ogg new file mode 100644 index 0000000000..398a59e248 Binary files /dev/null and b/sound/block_parry/block_wood2.ogg differ diff --git a/sound/block_parry/sfx-parry.ogg b/sound/block_parry/sfx-parry.ogg new file mode 100644 index 0000000000..3429031bd9 Binary files /dev/null and b/sound/block_parry/sfx-parry.ogg differ diff --git a/tgstation.dme b/tgstation.dme index 9c24f3549c..39f79a0b71 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -48,7 +48,6 @@ #include "code\__DEFINES\events.dm" #include "code\__DEFINES\exports.dm" #include "code\__DEFINES\fantasy_affixes.dm" -#include "code\__DEFINES\flags.dm" #include "code\__DEFINES\food.dm" #include "code\__DEFINES\footsteps.dm" #include "code\__DEFINES\hud.dm" @@ -77,7 +76,6 @@ #include "code\__DEFINES\movespeed_modification.dm" #include "code\__DEFINES\nanites.dm" #include "code\__DEFINES\networks.dm" -#include "code\__DEFINES\obj_flags.dm" #include "code\__DEFINES\pinpointers.dm" #include "code\__DEFINES\pipe_construction.dm" #include "code\__DEFINES\pool.dm" @@ -119,10 +117,17 @@ #include "code\__DEFINES\vv.dm" #include "code\__DEFINES\wall_dents.dm" #include "code\__DEFINES\wires.dm" +#include "code\__DEFINES\_flags\_flags.dm" +#include "code\__DEFINES\_flags\item_flags.dm" +#include "code\__DEFINES\_flags\obj_flags.dm" #include "code\__DEFINES\admin\keybindings.dm" +#include "code\__DEFINES\combat\attack_types.dm" +#include "code\__DEFINES\combat\block.dm" +#include "code\__DEFINES\combat\block_parry.dm" #include "code\__DEFINES\dcs\flags.dm" #include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals.dm" +#include "code\__DEFINES\flags\do_after.dm" #include "code\__DEFINES\flags\shields.dm" #include "code\__DEFINES\mapping\maploader.dm" #include "code\__DEFINES\material\worth.dm" @@ -144,6 +149,7 @@ #include "code\__HELPERS\custom_holoforms.dm" #include "code\__HELPERS\dates.dm" #include "code\__HELPERS\dna.dm" +#include "code\__HELPERS\do_after.dm" #include "code\__HELPERS\donator_groupings.dm" #include "code\__HELPERS\files.dm" #include "code\__HELPERS\game.dm" @@ -2161,6 +2167,7 @@ #include "code\modules\keybindings\keybind\admin.dm" #include "code\modules\keybindings\keybind\carbon.dm" #include "code\modules\keybindings\keybind\client.dm" +#include "code\modules\keybindings\keybind\combat.dm" #include "code\modules\keybindings\keybind\emote.dm" #include "code\modules\keybindings\keybind\human.dm" #include "code\modules\keybindings\keybind\living.dm" @@ -2311,7 +2318,10 @@ #include "code\modules\mob\living\emote.dm" #include "code\modules\mob\living\life.dm" #include "code\modules\mob\living\living.dm" +#include "code\modules\mob\living\living_active_block.dm" +#include "code\modules\mob\living\living_active_parry.dm" #include "code\modules\mob\living\living_block.dm" +#include "code\modules\mob\living\living_blocking_parrying.dm" #include "code\modules\mob\living\living_defense.dm" #include "code\modules\mob\living\living_defines.dm" #include "code\modules\mob\living\living_mobility.dm"