From 025930404f5afe8429f95576225d03dea0b13e03 Mon Sep 17 00:00:00 2001 From: SmArtKar Date: Sun, 4 Aug 2024 18:46:04 +0300 Subject: [PATCH 1/3] Implements active parrying as a mechanic --- maplestation.dme | 4 + maplestation_modules/code/__DEFINES/combat.dm | 40 ++ .../code/__DEFINES/signals.dm | 3 + .../controllers/subsystem/mouse_entered.dm | 10 + .../code/datums/components/active_combat.dm | 412 ++++++++++++++++++ .../game/objects/items/captain_weapons.dm | 28 +- .../code/game/objects/items/parry_info.dm | 165 +++++++ .../icons/effects/defense_indicators.dmi | Bin 0 -> 485 bytes maplestation_modules/sound/sfx-parry.ogg | Bin 0 -> 22373 bytes .../wollys_items/code/wollysitems.dm | 13 +- 10 files changed, 673 insertions(+), 2 deletions(-) create mode 100644 maplestation_modules/code/__DEFINES/combat.dm create mode 100644 maplestation_modules/code/controllers/subsystem/mouse_entered.dm create mode 100644 maplestation_modules/code/datums/components/active_combat.dm create mode 100644 maplestation_modules/code/game/objects/items/parry_info.dm create mode 100644 maplestation_modules/icons/effects/defense_indicators.dmi create mode 100644 maplestation_modules/sound/sfx-parry.ogg diff --git a/maplestation.dme b/maplestation.dme index c8b23bd24c0d..a5b6628e3a4b 100644 --- a/maplestation.dme +++ b/maplestation.dme @@ -6062,6 +6062,7 @@ #include "maplestation_modules\code\__DEFINES\antag_defines.dm" #include "maplestation_modules\code\__DEFINES\captain_weapons.dm" #include "maplestation_modules\code\__DEFINES\colors.dm" +#include "maplestation_modules\code\__DEFINES\combat.dm" #include "maplestation_modules\code\__DEFINES\DNA.dm" #include "maplestation_modules\code\__DEFINES\examine_defines.dm" #include "maplestation_modules\code\__DEFINES\is_helpers.dm" @@ -6086,8 +6087,10 @@ #include "maplestation_modules\code\controllers\configuration\entries\autotransfer.dm" #include "maplestation_modules\code\controllers\subsystem\autotransfer.dm" #include "maplestation_modules\code\controllers\subsystem\economy.dm" +#include "maplestation_modules\code\controllers\subsystem\mouse_entered.dm" #include "maplestation_modules\code\controllers\subsystem\pain_subsystem.dm" #include "maplestation_modules\code\datums\shuttles.dm" +#include "maplestation_modules\code\datums\components\active_combat.dm" #include "maplestation_modules\code\datums\components\easy_ignite.dm" #include "maplestation_modules\code\datums\components\knockoff_move.dm" #include "maplestation_modules\code\datums\components\limbless_aid.dm" @@ -6149,6 +6152,7 @@ #include "maplestation_modules\code\game\objects\items\cards_ids.dm" #include "maplestation_modules\code\game\objects\items\holy_weapons.dm" #include "maplestation_modules\code\game\objects\items\locker_spawners.dm" +#include "maplestation_modules\code\game\objects\items\parry_info.dm" #include "maplestation_modules\code\game\objects\items\plushes.dm" #include "maplestation_modules\code\game\objects\items\toys.dm" #include "maplestation_modules\code\game\objects\items\weaponry.dm" diff --git a/maplestation_modules/code/__DEFINES/combat.dm b/maplestation_modules/code/__DEFINES/combat.dm new file mode 100644 index 000000000000..a11d62626189 --- /dev/null +++ b/maplestation_modules/code/__DEFINES/combat.dm @@ -0,0 +1,40 @@ +// Directional behaviors + +/// Can block/parry attacks from any direction +#define ACTIVE_COMBAT_OMNIDIRECTIONAL 0 +/// Can block/parry attacks from the direction you're facing + diagonals. Default behavior +#define ACTIVE_COMBAT_FACING 1 +/// Only can block/parry attacks from the directions you're facing - cardinals only +#define ACTIVE_COMBAT_CARDINAL_FACING 2 + +// Effects for successfull blocks +// Also used on self upon failing to parry/block +/// Click on the attacker upon blocking for a parry, list assoc should be TRUE or a damage multiplier +#define ACTIVE_COMBAT_PARRY "active_combat_parry" +/// Shoves the attacker, list assoc should be TRUE +#define ACTIVE_COMBAT_SHOVE "active_combat_shove" +/// Evades to the side, if sides are occupied evades back, list assoc should be TRUE +#define ACTIVE_COMBAT_EVADE "active_combat_evade" +/// Knocks the attacker down, list assoc is duration. Can be used in failed parries +#define ACTIVE_COMBAT_KNOCKDOWN "active_combat_knockdown" +/// Staggers the attacker, list assoc is duration. Can be used in failed parries +#define ACTIVE_COMBAT_STAGGER "active_combat_stagger" +/// Deals stamina to the attacker, list assoc is amount. Can be used in failed parries +#define ACTIVE_COMBAT_STAMINA "active_combat_stamina" +/// Make an emote, list assoc is emote itself. Can be used in failed parries +#define ACTIVE_COMBAT_EMOTE "active_combat_emote" +/// Reflect a projectile if hit by one at whatever the user is currently hovering their mouse over, list assoc is TRUE +#define ACTIVE_COMBAT_REFLECT_PROJECTILE "active_combat_reflect_projectile" +/// Makes the damage go through even if the parry was good enough, list assoc is TRUE +#define ACTIVE_COMBAT_FORCED_DAMAGE "active_combat_forced_damage" + +// Parry states +#define ACTIVE_COMBAT_INACTIVE 0 +#define ACTIVE_COMBAT_PREPARED 1 +#define ACTIVE_COMBAT_RECOVERING 2 + +// Parry returns +#define PARRY_FAILURE NONE +#define PARRY_SUCCESS (1<<0) +#define PARRY_FULL_BLOCK (1<<1) +#define PARRY_RETALIATE (1<<2) diff --git a/maplestation_modules/code/__DEFINES/signals.dm b/maplestation_modules/code/__DEFINES/signals.dm index 59edbe51490e..29820b0845b0 100644 --- a/maplestation_modules/code/__DEFINES/signals.dm +++ b/maplestation_modules/code/__DEFINES/signals.dm @@ -24,3 +24,6 @@ /// A carbon drank some caffeine. (signal, caffeine_content) #define COMSIG_CARBON_DRINK_CAFFEINE "carbon_drink_caffeine" + +/// A living mob pressed the active block button +#define COMSIG_LIVING_ACTIVE_BLOCK_KEYBIND "living_active_block_keybind" diff --git a/maplestation_modules/code/controllers/subsystem/mouse_entered.dm b/maplestation_modules/code/controllers/subsystem/mouse_entered.dm new file mode 100644 index 000000000000..68b5148df8d1 --- /dev/null +++ b/maplestation_modules/code/controllers/subsystem/mouse_entered.dm @@ -0,0 +1,10 @@ +/datum/controller/subsystem/mouse_entered + var/list/sustained_hovers = list() + +/atom/MouseEntered(location, control, params) + . = ..() + SSmouse_entered.sustained_hovers[usr.client] = src + +/atom/MouseExited(location, control, params) + . = ..() + SSmouse_entered.sustained_hovers[usr.client] = null diff --git a/maplestation_modules/code/datums/components/active_combat.dm b/maplestation_modules/code/datums/components/active_combat.dm new file mode 100644 index 000000000000..4c90c4b1df49 --- /dev/null +++ b/maplestation_modules/code/datums/components/active_combat.dm @@ -0,0 +1,412 @@ +/* + * Active Combat component + * Allows weapons its attached to to perform active blocking/evading/parrying + * Timing the parry perfectly can cause additional effects, such as shoving the opponent or retaliating in melee + */ + +/datum/keybinding/living/active_combat + hotkey_keys = list("N") + name = "active_combat" + full_name = "Active Parry" + description = "Press to attempt to block/parry." + keybind_signal = COMSIG_LIVING_ACTIVE_BLOCK_KEYBIND + +/datum/component/active_combat + /// Flags which this item (if parent is an item) needs to be in to be able to parry + var/inventory_flags = ITEM_SLOT_HANDS + + /// Decides which directions you can block from + var/block_directions = ACTIVE_COMBAT_FACING + + // These dictate the animation, kinda + /// How long does it take for parry to become active + var/windup_timer = 0.2 SECONDS + /// Window during which you can parry. Includes perfect parrying window, after which the effects start to diminish + var/parry_window = 0.75 SECONDS + /// Window during the parry is "perfect" + var/perfect_parry_window = 0.25 SECONDS + + /// Effects applied after parrying a hit + var/list/parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE) + /// Alternative list of effects for perfect parrying. If null, falls back to parry_effects + var/list/perfect_parry_effects = null + /// Effects used on self upon missing a parry + var/list/parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5) + + /// Multiplier for converting damage into stamina damage + var/stamina_multiplier = 0.5 + /// Multiplier for stamina damage for a perfect parry + var/perfect_stamina_multiplier = 0.33 + + /// How much damage (in percentage) is blocked by a perfect parry + /// If block_barrier or higher, hit is negated completely + var/damage_blocked = 1 + /// How much damage block is lost by the end of the parry window + var/damage_block_imperfect_loss = 0.5 + /// Maximum amount of damage to be blocked from a single hit + var/maximum_damage_blocked = 25 + + /// How efficient the block has to be to fully block an attack + var/block_barrier = 1 + /// Overrides for block_barrier for different attack types + var/list/block_barrier_overrides = list() + + /// For how long user's clicks are blocked after failing a parry + var/parry_miss_cooldown = 0.4 SECONDS + + /// Icon used by VFX + var/icon_state = "block" + /// Color used by VFX + var/effect_color = "#5EB4FF" + + /// Window multiplier for projectiles. Set to 0 to disable projectile blocking/parrying + var/projectile_window_multiplier = 0 + + /// Callback to determine valid parries + var/datum/callback/parry_callback + + // Internal variables + /// Last time the user pressed the parry keybind + var/last_keypress = 0 + /// Current state + var/state = ACTIVE_COMBAT_INACTIVE + /// Timer for failing the parry + var/parry_fail_timer + /// Tick during which we parried last time + var/parry_tick = 0 + /// How much damage to negate off the next hit *this tick* + var/damage_negation_mult = 1 + /// Current user + var/mob/living/carbon/cur_user + /// VFX + var/obj/effect/temp_visual/active_combat/visual_effect + +/datum/component/active_combat/Initialize(inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.2 SECONDS, \ + parry_window = 0.75 SECONDS, perfect_parry_window = 0.25 SECONDS, parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE), perfect_parry_effects = null, \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1, damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, \ + block_barrier = 1, block_barrier_overrides = list(), parry_miss_cooldown = 0.4 SECONDS, icon_state = "block", effect_color = "#5EB4FF", \ + projectile_window_multiplier = 0, parry_callback) + + if(!iscarbon(parent) && !isitem(parent)) + return COMPONENT_INCOMPATIBLE + + src.inventory_flags = inventory_flags + src.block_directions = block_directions + src.windup_timer = windup_timer + src.parry_window = parry_window + src.perfect_parry_window = perfect_parry_window + src.parry_effects = parry_effects + src.perfect_parry_effects = perfect_parry_effects + src.parry_miss_effects = parry_miss_effects + src.stamina_multiplier = stamina_multiplier + src.perfect_stamina_multiplier = perfect_stamina_multiplier + src.damage_blocked = damage_blocked + src.damage_block_imperfect_loss = damage_block_imperfect_loss + src.maximum_damage_blocked = maximum_damage_blocked + src.block_barrier = block_barrier + src.block_barrier_overrides = block_barrier_overrides + src.parry_miss_cooldown = parry_miss_cooldown + src.icon_state = icon_state + src.effect_color = effect_color + src.projectile_window_multiplier = projectile_window_multiplier + src.parry_callback = parry_callback + +/datum/component/active_combat/RegisterWithParent() + if (ismob(parent)) + register_to_mob(parent) + RegisterSignal(parent, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(check_block)) + return + + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equip)) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop)) + RegisterSignal(parent, COMSIG_ITEM_HIT_REACT, PROC_REF(on_hit_react)) + +/datum/component/active_combat/UnregisterFromParent() + if (ismob(parent)) + unregister_mob(parent) + UnregisterSignal(parent, COMSIG_LIVING_CHECK_BLOCK) + return + + UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED, COMSIG_ITEM_HIT_REACT)) + +/datum/component/active_combat/proc/on_equip(atom/source, mob/equipper, slot) + SIGNAL_HANDLER + + if(!(inventory_flags & slot)) + unregister_mob(equipper) + return + + register_to_mob(equipper) + +/datum/component/active_combat/proc/on_drop(atom/source, mob/user) + SIGNAL_HANDLER + unregister_mob(user) + +/datum/component/active_combat/proc/register_to_mob(mob/living/carbon/user) + last_keypress = 0 + state = ACTIVE_COMBAT_INACTIVE + cur_user = user + RegisterSignal(user, COMSIG_LIVING_ACTIVE_BLOCK_KEYBIND, PROC_REF(on_keybind)) + RegisterSignal(user, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(modify_damage)) + +/datum/component/active_combat/proc/unregister_mob(mob/living/carbon/user) + UnregisterSignal(user, list(COMSIG_LIVING_ACTIVE_BLOCK_KEYBIND, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS)) + cur_user = null + last_keypress = 0 + state = ACTIVE_COMBAT_INACTIVE + user.remove_movespeed_modifier(/datum/movespeed_modifier/active_combat) + if (!isnull(parry_fail_timer)) + deltimer(parry_fail_timer) + STOP_PROCESSING(SSfastprocess, src) + +/datum/component/active_combat/proc/on_keybind(mob/living/carbon/source) + SIGNAL_HANDLER + + if (state != ACTIVE_COMBAT_INACTIVE) + to_chat(source, span_warning("You cannot do this right now!")) + return + + if(source.get_timed_status_effect_duration(/datum/status_effect/staggered)) + to_chat(source, span_warning("You're too off balance to parry!")) + return + + last_keypress = world.time + state = ACTIVE_COMBAT_PREPARED + parry_fail_timer = addtimer(CALLBACK(src, PROC_REF(failed_parry)), parry_window, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE) + START_PROCESSING(SSfastprocess, src) + source.changeNext_move(windup_timer + parry_window) + playsound(source, 'maplestation_modules/sound/sfx-parry.ogg', 120, TRUE) + visual_effect = new(source, icon_state, windup_timer + parry_window, windup_timer) + source.add_movespeed_modifier(/datum/movespeed_modifier/active_combat) + + if (!isnull(source.client)) + source.face_atom(SSmouse_entered.sustained_hovers[source.client]) + +/datum/component/active_combat/process() + if (!isnull(cur_user.client)) + cur_user.face_atom(SSmouse_entered.sustained_hovers[cur_user.client]) + +/datum/component/active_combat/proc/on_hit_react(obj/item/source, mob/living/carbon/owner, atom/movable/hitby, attack_text, final_block_chance, damage, attack_type, damage_type) + SIGNAL_HANDLER + + if (last_keypress + windup_timer + parry_window * (isprojectile(hitby) ? projectile_window_multiplier : 1) < world.time || state != ACTIVE_COMBAT_PREPARED) + return + + var/parry_return = run_parry(owner, source, hitby, damage, attack_type, damage_type) + + if (!parry_return) + return + + if (!(parry_return & PARRY_RETALIATE)) + playsound(src, source.block_sound, BLOCK_SOUND_VOLUME, TRUE) + + if (parry_return & PARRY_FULL_BLOCK) + owner.visible_message(span_danger("[owner] [(parry_return & PARRY_RETALIATE) ? "parries" : "blocks"] [attack_text] with [source]!")) + return COMPONENT_HIT_REACTION_BLOCK + +/datum/component/active_combat/proc/run_parry(mob/living/carbon/user, atom/source, atom/movable/hitby, damage, attack_type, damage_type) + if (last_keypress + windup_timer > world.time) + failed_parry(TRUE) + return PARRY_FAILURE + + if (!isnull(user.client)) + user.face_atom(SSmouse_entered.sustained_hovers[user.client]) // In case SSfastprocess didn't tick yet and we moved last tick + + if (block_directions == ACTIVE_COMBAT_CARDINAL_FACING) + if (user.dir != get_dir(user, hitby)) + failed_parry() // You eat shit if you get backstabbed while parrying + return PARRY_FAILURE + else if (block_directions == ACTIVE_COMBAT_FACING) + if (!(user.dir & get_dir(user, hitby))) + failed_parry() + return PARRY_FAILURE + + if (!isnull(parry_callback) && !parry_callback.Invoke(hitby)) + failed_parry(TRUE) + return PARRY_FAILURE + + var/window_mult = isprojectile(hitby) ? projectile_window_multiplier : 1 + var/is_perfect = last_keypress + windup_timer + perfect_parry_window * window_mult > world.time + var/parry_loss = (world.time - (last_keypress + windup_timer + perfect_parry_window * window_mult)) / ((parry_window - perfect_parry_window) * window_mult) + parry_tick = world.time + last_keypress = 0 + state = ACTIVE_COMBAT_INACTIVE + if (!isnull(parry_fail_timer)) + deltimer(parry_fail_timer) + STOP_PROCESSING(SSfastprocess, src) + user.changeNext_move(0) + user.remove_movespeed_modifier(/datum/movespeed_modifier/active_combat) + + var/list/effects = is_perfect && !isnull(perfect_parry_effects) ? perfect_parry_effects : parry_effects + var/damage_negation = is_perfect ? damage_blocked : (damage_blocked - parry_loss * damage_block_imperfect_loss) + var/effect_mult = clamp(damage_negation, 0, 1) + var/barrier = (attack_type in block_barrier_overrides) ? block_barrier_overrides[attack_type] : block_barrier + + damage_negation_mult = (damage <= maximum_damage_blocked) ? clamp(1 - damage_negation, 0, 1) : (1 - (maximum_damage_blocked * clamp(damage_negation, 0, 1)) / damage) + + var/atom/movable/hitter = hitby + var/parry_flags = PARRY_SUCCESS + var/use_rush_effect = FALSE + + if (isprojectile(hitby)) + var/obj/projectile/hit_proj = hitby + if (!isnull(hit_proj.firer)) + hitter = hit_proj.firer + + if (LAZYACCESS(effects, ACTIVE_COMBAT_REFLECT_PROJECTILE)) + hit_proj.firer = user + hit_proj.set_angle(get_angle(user, hitter)) + damage_negation_mult = barrier + + if (LAZYACCESS(effects, ACTIVE_COMBAT_PARRY) && !isprojectile(hitter)) + playsound(user, 'sound/weapons/parry.ogg', BLOCK_SOUND_VOLUME, TRUE) + parry_flags |= PARRY_RETALIATE + INVOKE_ASYNC(src, PROC_REF(retaliate), user, source, hitter, effects) + use_rush_effect = TRUE + + if (LAZYACCESS(effects, ACTIVE_COMBAT_EVADE)) + var/dir_attempts = prob(50) ? list(90, 270, 180) : list(270, 90, 180) + for (var/dir_attempt in dir_attempts) + if (user.Move(get_step(user, turn(user.dir, dir_attempt)))) + if (isprojectile(hitby)) + damage_negation_mult = barrier + user.visible_message(span_warning("[user] weaves out of [hitby]'s way!"), span_notice("You weave out of [hitby]'s way!")) + break + + if (LAZYACCESS(effects, ACTIVE_COMBAT_EMOTE)) + INVOKE_ASYNC(user, TYPE_PROC_REF(/mob, emote), LAZYACCESS(effects, ACTIVE_COMBAT_EMOTE)) + + if (isliving(hitter) && user.CanReach(hitter)) + var/mob/living/victim = hitter + + if (LAZYACCESS(effects, ACTIVE_COMBAT_SHOVE)) + user.disarm(victim, source) + use_rush_effect = TRUE + + if (LAZYACCESS(effects, ACTIVE_COMBAT_KNOCKDOWN)) + victim.Knockdown(LAZYACCESS(effects, ACTIVE_COMBAT_KNOCKDOWN) * effect_mult) + use_rush_effect = TRUE + + if (LAZYACCESS(effects, ACTIVE_COMBAT_STAGGER)) + victim.adjust_staggered_up_to(LAZYACCESS(effects, ACTIVE_COMBAT_STAGGER) * effect_mult, 10 SECONDS) + use_rush_effect = TRUE + + if (LAZYACCESS(effects, ACTIVE_COMBAT_STAMINA)) + victim.apply_damage(LAZYACCESS(effects, ACTIVE_COMBAT_STAMINA) * effect_mult, STAMINA) + use_rush_effect = TRUE + + if (is_perfect) + visual_effect.add_filter("perfect_parry", 2, list("type" = "outline", "color" = "#ffffffAF", "size" = 2)) + if (use_rush_effect) + visual_effect.rush_at(hitter) + else + visual_effect.fadeout() + + user.apply_damage(damage * (is_perfect ? perfect_stamina_multiplier : stamina_multiplier), STAMINA) // ngl itd be funny if you got stamcritted from parrying a meteor + if (damage_negation >= barrier && !LAZYACCESS(effects, ACTIVE_COMBAT_FORCED_DAMAGE)) + parry_flags |= PARRY_FULL_BLOCK + return parry_flags + +/datum/component/active_combat/proc/failed_parry(harmless = FALSE) + last_keypress = 0 + state = ACTIVE_COMBAT_RECOVERING + STOP_PROCESSING(SSfastprocess, src) + cur_user.changeNext_move(parry_miss_cooldown) + addtimer(CALLBACK(src, PROC_REF(recover_parry)), parry_miss_cooldown) + cur_user.remove_movespeed_modifier(/datum/movespeed_modifier/active_combat) + + if (harmless) + return + + if (LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_EMOTE)) + INVOKE_ASYNC(cur_user, TYPE_PROC_REF(/mob, emote), LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_EMOTE)) + + if (LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_KNOCKDOWN)) + cur_user.Knockdown(LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_KNOCKDOWN)) + + if (LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_STAGGER)) + cur_user.adjust_staggered_up_to(LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_STAGGER), 10 SECONDS) + + if (LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_STAMINA)) + cur_user.apply_damage(LAZYACCESS(parry_miss_effects, ACTIVE_COMBAT_STAMINA), STAMINA) + +/datum/component/active_combat/proc/retaliate(mob/living/carbon/user, atom/source, atom/movable/hitter, list/effects) + var/held_index = null + var/old_force = null + if (isitem(source)) + var/obj/item/source_item = source + if (source != user.get_active_held_item()) + held_index = user.active_hand_index + user.swap_hand(user.get_held_index_of_item(source)) + old_force = source_item.force + source_item.force *= LAZYACCESS(effects, ACTIVE_COMBAT_PARRY) + + user.ClickOn(hitter) + + if (!isnull(held_index)) + user.swap_hand(held_index) + + if (!isnull(old_force)) + var/obj/item/source_item = source + source_item.force = old_force + +/datum/component/active_combat/proc/recover_parry() + state = ACTIVE_COMBAT_INACTIVE + +/datum/component/active_combat/proc/modify_damage(mob/living/carbon/human/source, list/damage_mods, damage_amount, damagetype, def_zone, sharpness, attack_direction, obj/item/attacking_item) + SIGNAL_HANDLER + + if (parry_tick == world.time) + damage_mods += damage_negation_mult + parry_tick = 0 + +/datum/component/active_combat/proc/check_block(mob/living/carbon/source, atom/hitby, damage, attack_text, attack_type, armour_penetration, damage_type, attack_flag) + SIGNAL_HANDLER + + if (last_keypress + windup_timer + parry_window * (isprojectile(hitby) ? projectile_window_multiplier : 1) < world.time || state != ACTIVE_COMBAT_PREPARED) + return + + var/parry_return = run_parry(source, null, hitby, damage, attack_type, damage_type) + + if (parry_return & PARRY_FULL_BLOCK) + source.visible_message(span_danger("[source] [(parry_return & PARRY_RETALIATE) ? "parries" : "blocks"] [attack_text]!")) + return COMPONENT_HIT_REACTION_BLOCK + +/obj/effect/temp_visual/active_combat + name = "blocking glow" + icon = 'maplestation_modules/icons/effects/defense_indicators.dmi' + icon_state = "block" + layer = ABOVE_ALL_MOB_LAYER + +/obj/effect/temp_visual/active_combat/Initialize(mapload, icon_state, max_duration, warmup) + . = ..() + var/atom/movable/owner = loc + owner.vis_contents += src + src.icon_state = icon_state + pixel_y = -12 + update_appearance() + var/matrix/nmatrix = matrix() + nmatrix.Scale(0.2, 0.2) + transform = nmatrix + animate(src, transform = matrix(), pixel_y = 0, time = warmup, easing = SINE_EASING|EASE_IN) + deltimer(timerid) + timerid = addtimer(CALLBACK(src, PROC_REF(fadeout)), max_duration, TIMER_UNIQUE|TIMER_STOPPABLE) + +/obj/effect/temp_visual/active_combat/proc/fadeout() + var/matrix/nmatrix = matrix() + nmatrix.Scale(2, 2) + animate(src, transform = nmatrix, alpha = 0, time = 0.3 SECONDS, easing = SINE_EASING|EASE_OUT) + if (timerid) + deltimer(timerid) + timerid = QDEL_IN_STOPPABLE(src, 0.3 SECONDS) + +/obj/effect/temp_visual/active_combat/proc/rush_at(atom/movable/target) + var/atom/movable/owner = loc + animate(src, pixel_x = sin(get_angle(owner, target)) * world.icon_size * get_dist(owner, target) * 1.5, pixel_y = cos(get_angle(owner, target)) * world.icon_size * get_dist(owner, target) * 1.5, alpha = 0, time = 0.5 SECONDS) + if (timerid) + deltimer(timerid) + timerid = QDEL_IN_STOPPABLE(src, 0.5 SECONDS) + +/datum/movespeed_modifier/active_combat + multiplicative_slowdown = 0.5 diff --git a/maplestation_modules/code/game/objects/items/captain_weapons.dm b/maplestation_modules/code/game/objects/items/captain_weapons.dm index c2d0a83ed543..2def630b08b3 100644 --- a/maplestation_modules/code/game/objects/items/captain_weapons.dm +++ b/maplestation_modules/code/game/objects/items/captain_weapons.dm @@ -1,10 +1,25 @@ GLOBAL_VAR(captain_weapon_picked) //Weapons for the captain to use in melee. + +/obj/item/melee/sabre + block_chance = 0 + /obj/item/melee/sabre/Initialize(mapload) . = ..() if(!GLOB.captain_weapon_picked) AddComponent(/datum/component/subtype_picker, GLOB.captain_weapons, CALLBACK(src, PROC_REF(on_captain_weapon_picked))) + // Larger window, smaller perfect parries, no windup and has a bit of a leeway for full blocks for imperfect parries + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0 SECONDS, \ + parry_window = 1.0 SECONDS, perfect_parry_window = 0.25 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1.3, \ + damage_block_imperfect_loss = 0.6, maximum_damage_blocked = 25, block_barrier = 1, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_LIGHT_ORANGE, projectile_window_multiplier = 0.5, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = 1.2, ACTIVE_COMBAT_STAGGER = 3 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + ) + ///Probably doesn't need to be a proc, but this is used when the captain's weapon is chosen to make sure you can keep picking the sabre over and over. Has to be a global list so that its on the next weapon. /obj/item/melee/sabre/proc/on_captain_weapon_picked(obj/item/melee/sabre/captain_weapon_picked) GLOB.captain_weapon_picked = captain_weapon_picked @@ -35,7 +50,18 @@ GLOBAL_VAR(captain_weapon_picked) active_force = 25 active_throwforce = 35 //Its a fucking spear of a sword armour_penetration = 50 - block_chance = 10 //Compared to the sabre's 50, yikes. + +/obj/item/melee/energy/sword/captain_rapier/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.1 SECONDS, \ + parry_window = 0.5 SECONDS, perfect_parry_window = 0.2 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, block_barrier = 1, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_LIGHT_ORANGE, projectile_window_multiplier = 1, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = 1.2, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE, ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 4 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + ) /obj/item/melee/energy/sword/captain_rapier/afterattack(atom/target, mob/user, proximity) . = ..() diff --git a/maplestation_modules/code/game/objects/items/parry_info.dm b/maplestation_modules/code/game/objects/items/parry_info.dm new file mode 100644 index 000000000000..39aae2ad8cd2 --- /dev/null +++ b/maplestation_modules/code/game/objects/items/parry_info.dm @@ -0,0 +1,165 @@ +// Extremely bad values, however: funny +/obj/item/cane/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.3 SECONDS, \ + parry_window = 0.5 SECONDS, perfect_parry_window = 0.2 SECONDS, stamina_multiplier = 1, perfect_stamina_multiplier = 0.33, damage_blocked = 1, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 10, block_barrier = 1, parry_miss_cooldown = 0.4 SECONDS, icon_state = "block", \ + effect_color = COLOR_WHITE, projectile_window_multiplier = 0, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_SHOVE = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_SHOVE = TRUE, ACTIVE_COMBAT_KNOCKDOWN = 1 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 10), \ + ) + +// Some windup but roughly on-par with defaults. Only parries in spars +/obj/item/ceremonial_blade + block_chance = 0 + +/obj/item/ceremonial_blade/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.1 SECONDS, \ + parry_window = 0.75 SECONDS, perfect_parry_window = 0.25 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1.2, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, block_barrier = 1, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = "#5EB4FF", projectile_window_multiplier = 0, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + parry_callback = CALLBACK(src, PROC_REF(can_parry)), \ + ) + +/obj/item/ceremonial_blade/proc/can_parry(atom/target) + return HAS_TRAIT(target, TRAIT_SPARRING) + +// Tricky but rewarding +/obj/item/nullrod/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.2 SECONDS, \ + parry_window = 0.6 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1.2, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, block_barrier = 0.8, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_YELLOW, projectile_window_multiplier = 0.3, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_STAMINA = 15), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 12), \ + ) + +/obj/item/vorpalscythe/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.2 SECONDS, \ + parry_window = 0.6 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1.2, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, block_barrier = 0.8, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_RED, projectile_window_multiplier = 0.3, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_STAMINA = 15), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 12), \ + ) + +// Fast, efficient and redirects projectiles +/obj/item/highfrequencyblade + block_chance = 0 + +/obj/item/highfrequencyblade/Initialize(mapload) + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_OMNIDIRECTIONAL, windup_timer = 0 SECONDS, \ + parry_window = 1.2 SECONDS, perfect_parry_window = 0.2 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 2, \ + damage_block_imperfect_loss = 1.5, maximum_damage_blocked = 25, block_barrier = 0.8, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_BLUE, projectile_window_multiplier = 1, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE, ACTIVE_COMBAT_KNOCKDOWN = 1.5 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAMINA = 4), \ + ) + +// Reflects projectiles but punishes failed parries +/obj/item/melee/energy/sword + block_chance = 15 + +/obj/item/melee/energy/sword/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.1 SECONDS, \ + parry_window = 1 SECONDS, perfect_parry_window = 0.25 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1.2, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, block_barrier = 0.8, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_RED_LIGHT, projectile_window_multiplier = 1, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = 1.2, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE, ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 4 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + parry_callback = CALLBACK(src, PROC_REF(can_parry)), \ + ) + +/obj/item/melee/energy/sword/proc/can_parry(atom/target) + return HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) + +/obj/item/dualsaber + block_chance = 25 + +/obj/item/dualsaber/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_OMNIDIRECTIONAL, windup_timer = 0 SECONDS, \ + parry_window = 1.5 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 2, \ + damage_block_imperfect_loss = 1.5, maximum_damage_blocked = 35, block_barrier = 0.6, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = COLOR_RED_LIGHT, projectile_window_multiplier = 1, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE, ACTIVE_COMBAT_EMOTE = "spin"), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = 1.2, ACTIVE_COMBAT_REFLECT_PROJECTILE = TRUE, ACTIVE_COMBAT_EMOTE = "spin", ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 2 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + parry_callback = CALLBACK(src, PROC_REF(can_parry)), \ + ) + +/obj/item/dualsaber/proc/can_parry(atom/target) + return HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) + +/obj/item/shield + block_chance = 25 // Git gud, amateur + +/obj/item/shield/buckler + block_chance = 15 + +/obj/item/shield/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.15 SECONDS, \ + parry_window = 1.5 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 2, \ + damage_block_imperfect_loss = 1.5, maximum_damage_blocked = 35, block_barrier = 0.6, parry_miss_cooldown = 0.4 SECONDS, icon_state = "block", \ + effect_color = COLOR_RED_LIGHT, projectile_window_multiplier = 1, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_SHOVE = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_SHOVE = TRUE, ACTIVE_COMBAT_KNOCKDOWN = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + ) + +/* TODOS + * + * Claymore + * Cult sword + * Crowbar + * Cursed katana + * Energy katana + * Fireaxe + * Hierophant club + * Katana + * Kinetic Crusher + * Knives + * Pickass + * + * Baseball bats + * Batons + * Stunbatons + * Telebatons + * Heretic Blade + */ + +/* + * Template with "default" parry values + + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.2 SECONDS, \ + parry_window = 0.75 SECONDS, perfect_parry_window = 0.25 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1.2, \ + damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, block_barrier = 1, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \ + effect_color = "#5EB4FF", projectile_window_multiplier = 0, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE, ACTIVE_COMBAT_STAGGER = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + ) + +*/ diff --git a/maplestation_modules/icons/effects/defense_indicators.dmi b/maplestation_modules/icons/effects/defense_indicators.dmi new file mode 100644 index 0000000000000000000000000000000000000000..75be88437b2a0b37ba5f558350cc2809fe984575 GIT binary patch literal 485 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=^;IDeB`&GO$wiq3C7Jno3=9>F zt$o&9tp)-Nuit2jy_J7|{Z@yPSXFyQnD8H#NulfW12zcnmYk84K{hnU(VvGJ5hW}1_ z73cU^3LWLnGA3P=dGN1d*X^%K{GYt`?PFmm-|%SF6NZK*Vn1}M|9{lc-4I>F@NoaT zcHtVo7wk8>!Z{~gV7hDOWZ1GSzR}L<&D~tqg0>5(%v%(DSZ+AK*!A1{<@;+4T1+s@ z85kN8IVG5pxNHq?47aK^EUNF^%JAka=Q2hOy*TCr@khN`Evz?8sQ2)cIB+vzOODlQ zmIW1V>RVY?iJa#WU=w)vX_ZXZ6pnzeG7R=@2;&$eg!NS-g+J`~*vttM^>p=fS?83{ F1OSR5x+wqv literal 0 HcmV?d00001 diff --git a/maplestation_modules/sound/sfx-parry.ogg b/maplestation_modules/sound/sfx-parry.ogg new file mode 100644 index 0000000000000000000000000000000000000000..3429031bd94d9a78e47a5b689cc10be342127e2d GIT binary patch literal 22373 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 as Date: Sun, 4 Aug 2024 20:58:37 +0300 Subject: [PATCH 2/3] eshield + evade --- maplestation_modules/code/__DEFINES/combat.dm | 2 +- .../code/datums/components/active_combat.dm | 12 +++++++++++- .../code/game/objects/items/parry_info.dm | 19 ++++++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/maplestation_modules/code/__DEFINES/combat.dm b/maplestation_modules/code/__DEFINES/combat.dm index a11d62626189..e5e6dd845fe8 100644 --- a/maplestation_modules/code/__DEFINES/combat.dm +++ b/maplestation_modules/code/__DEFINES/combat.dm @@ -13,7 +13,7 @@ #define ACTIVE_COMBAT_PARRY "active_combat_parry" /// Shoves the attacker, list assoc should be TRUE #define ACTIVE_COMBAT_SHOVE "active_combat_shove" -/// Evades to the side, if sides are occupied evades back, list assoc should be TRUE +/// Evades to the side, if sides are occupied evades back, list assoc should be TRUE or maximum distance for the evade #define ACTIVE_COMBAT_EVADE "active_combat_evade" /// Knocks the attacker down, list assoc is duration. Can be used in failed parries #define ACTIVE_COMBAT_KNOCKDOWN "active_combat_knockdown" diff --git a/maplestation_modules/code/datums/components/active_combat.dm b/maplestation_modules/code/datums/components/active_combat.dm index 4c90c4b1df49..3e39c04522ee 100644 --- a/maplestation_modules/code/datums/components/active_combat.dm +++ b/maplestation_modules/code/datums/components/active_combat.dm @@ -64,6 +64,8 @@ /// Callback to determine valid parries var/datum/callback/parry_callback + /// Callback that is called upon a successful parry + var/datum/callback/success_callback // Internal variables /// Last time the user pressed the parry keybind @@ -86,7 +88,7 @@ parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 1, damage_block_imperfect_loss = 0.5, maximum_damage_blocked = 25, \ block_barrier = 1, block_barrier_overrides = list(), parry_miss_cooldown = 0.4 SECONDS, icon_state = "block", effect_color = "#5EB4FF", \ - projectile_window_multiplier = 0, parry_callback) + projectile_window_multiplier = 0, parry_callback, success_callback) if(!iscarbon(parent) && !isitem(parent)) return COMPONENT_INCOMPATIBLE @@ -111,6 +113,7 @@ src.effect_color = effect_color src.projectile_window_multiplier = projectile_window_multiplier src.parry_callback = parry_callback + src.success_callback = success_callback /datum/component/active_combat/RegisterWithParent() if (ismob(parent)) @@ -242,6 +245,7 @@ var/damage_negation = is_perfect ? damage_blocked : (damage_blocked - parry_loss * damage_block_imperfect_loss) var/effect_mult = clamp(damage_negation, 0, 1) var/barrier = (attack_type in block_barrier_overrides) ? block_barrier_overrides[attack_type] : block_barrier + var/turf/user_turf = get_turf(user) damage_negation_mult = (damage <= maximum_damage_blocked) ? clamp(1 - damage_negation, 0, 1) : (1 - (maximum_damage_blocked * clamp(damage_negation, 0, 1)) / damage) @@ -272,6 +276,10 @@ if (isprojectile(hitby)) damage_negation_mult = barrier user.visible_message(span_warning("[user] weaves out of [hitby]'s way!"), span_notice("You weave out of [hitby]'s way!")) + if (LAZYACCESS(effects, ACTIVE_COMBAT_EVADE) > 1) + for (var/evade_count in 1 to (LAZYACCESS(effects, ACTIVE_COMBAT_EVADE) - 1)) + if (!user.Move(get_step(user, turn(user.dir, dir_attempt)))) + break break if (LAZYACCESS(effects, ACTIVE_COMBAT_EMOTE)) @@ -306,6 +314,8 @@ user.apply_damage(damage * (is_perfect ? perfect_stamina_multiplier : stamina_multiplier), STAMINA) // ngl itd be funny if you got stamcritted from parrying a meteor if (damage_negation >= barrier && !LAZYACCESS(effects, ACTIVE_COMBAT_FORCED_DAMAGE)) parry_flags |= PARRY_FULL_BLOCK + if (!isnull(success_callback)) + success_callback.Invoke(user, source, hitby, hitter, is_perfect, parry_loss, effect_mult, user_turf, parry_flags) return parry_flags /datum/component/active_combat/proc/failed_parry(harmless = FALSE) diff --git a/maplestation_modules/code/game/objects/items/parry_info.dm b/maplestation_modules/code/game/objects/items/parry_info.dm index 39aae2ad8cd2..4927fe7924a0 100644 --- a/maplestation_modules/code/game/objects/items/parry_info.dm +++ b/maplestation_modules/code/game/objects/items/parry_info.dm @@ -119,7 +119,7 @@ /obj/item/shield/Initialize(mapload) . = ..() AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.15 SECONDS, \ - parry_window = 1.5 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 2, \ + parry_window = 1.5 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.2, damage_blocked = 2, \ damage_block_imperfect_loss = 1.5, maximum_damage_blocked = 35, block_barrier = 0.6, parry_miss_cooldown = 0.4 SECONDS, icon_state = "block", \ effect_color = COLOR_RED_LIGHT, projectile_window_multiplier = 1, \ block_barrier_overrides = list(), \ @@ -128,6 +128,22 @@ parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ ) +/obj/item/shield/energy/Initialize(mapload) + . = ..() + AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_FACING, windup_timer = 0.15 SECONDS, \ + parry_window = 1.5 SECONDS, perfect_parry_window = 0.3 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.2, damage_blocked = 2, \ + damage_block_imperfect_loss = 1.5, maximum_damage_blocked = 35, block_barrier = 0.6, parry_miss_cooldown = 0.4 SECONDS, icon_state = "block", \ + effect_color = COLOR_BLUE_LIGHT, projectile_window_multiplier = 1, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_SHOVE = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_SHOVE = TRUE, ACTIVE_COMBAT_KNOCKDOWN = 2 SECONDS), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 5), \ + parry_callback = CALLBACK(src, PROC_REF(can_parry)), \ + ) + +/obj/item/shield/energy/proc/can_parry(atom/target) + return HAS_TRAIT(src, TRAIT_TRANSFORM_ACTIVE) + /* TODOS * * Claymore @@ -141,6 +157,7 @@ * Kinetic Crusher * Knives * Pickass + * Toolboxes * * Baseball bats * Batons From 72c40a1b2604af01df506f224e53e4fc8fb7ad78 Mon Sep 17 00:00:00 2001 From: SmArtKar Date: Sat, 17 Aug 2024 08:59:04 +0300 Subject: [PATCH 3/3] CI --- maplestation_modules/code/game/objects/items/parry_info.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/maplestation_modules/code/game/objects/items/parry_info.dm b/maplestation_modules/code/game/objects/items/parry_info.dm index 4927fe7924a0..024336c979aa 100644 --- a/maplestation_modules/code/game/objects/items/parry_info.dm +++ b/maplestation_modules/code/game/objects/items/parry_info.dm @@ -61,6 +61,7 @@ block_chance = 0 /obj/item/highfrequencyblade/Initialize(mapload) + . = ..() AddComponent(/datum/component/active_combat, inventory_flags = ITEM_SLOT_HANDS, block_directions = ACTIVE_COMBAT_OMNIDIRECTIONAL, windup_timer = 0 SECONDS, \ parry_window = 1.2 SECONDS, perfect_parry_window = 0.2 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.33, damage_blocked = 2, \ damage_block_imperfect_loss = 1.5, maximum_damage_blocked = 25, block_barrier = 0.8, parry_miss_cooldown = 0.4 SECONDS, icon_state = "counter", \