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..e5e6dd845fe8 --- /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 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" +/// 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..3e39c04522ee --- /dev/null +++ b/maplestation_modules/code/datums/components/active_combat.dm @@ -0,0 +1,422 @@ +/* + * 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 + /// Callback that is called upon a successful parry + var/datum/callback/success_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, success_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 + src.success_callback = success_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 + 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) + + 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!")) + 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)) + 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 + 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) + 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..024336c979aa --- /dev/null +++ b/maplestation_modules/code/game/objects/items/parry_info.dm @@ -0,0 +1,183 @@ +// 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.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(), \ + 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), \ + ) + +/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 + * Cult sword + * Crowbar + * Cursed katana + * Energy katana + * Fireaxe + * Hierophant club + * Katana + * Kinetic Crusher + * Knives + * Pickass + * Toolboxes + * + * 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 000000000000..75be88437b2a Binary files /dev/null and b/maplestation_modules/icons/effects/defense_indicators.dmi differ diff --git a/maplestation_modules/sound/sfx-parry.ogg b/maplestation_modules/sound/sfx-parry.ogg new file mode 100644 index 000000000000..3429031bd94d Binary files /dev/null and b/maplestation_modules/sound/sfx-parry.ogg differ diff --git a/maplestation_modules/story_content/wollys_items/code/wollysitems.dm b/maplestation_modules/story_content/wollys_items/code/wollysitems.dm index 8e59cc11620b..8d5b81ddb1f8 100644 --- a/maplestation_modules/story_content/wollys_items/code/wollysitems.dm +++ b/maplestation_modules/story_content/wollys_items/code/wollysitems.dm @@ -8,7 +8,7 @@ since some of them are two per character or singleton, i'm gonna save space and name = "Maugrim" desc = "Hilda Brandt's longsword. It was christened after slaying a space-werewolf of the same name." // todo force = 18 // identical the the chappie claymore rod, but without anti-magic - block_chance = 30 + block_chance = 0 icon_state = "maugrim" icon = 'maplestation_modules/story_content/wollys_items/icons/obj/weapons.dmi' inhand_icon_state = "maugrim" @@ -62,6 +62,17 @@ since some of them are two per character or singleton, i'm gonna save space and EXAMINE_CHECK_SPECIES, /datum/species/ornithid) AddElement(/datum/element/bane, target_type = /mob/living/basic/heretic_summon, damage_multiplier = 0, added_damage = 2, requires_combat_mode = FALSE) // rare exhange if it ever even happens, nod to the character's specialization in anti-heresy + // Bamboo-hatted kim reference, perfect parries let the damage through but multiply force to dangerous levels + 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.2 SECONDS, stamina_multiplier = 0.5, perfect_stamina_multiplier = 0.75, 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_WHITE, projectile_window_multiplier = 0, \ + block_barrier_overrides = list(), \ + parry_effects = list(ACTIVE_COMBAT_PARRY = TRUE), \ + perfect_parry_effects = list(ACTIVE_COMBAT_PARRY = 1.3, ACTIVE_COMBAT_STAGGER = 2 SECONDS, ACTIVE_COMBAT_FORCED_DAMAGE = TRUE), \ + parry_miss_effects = list(ACTIVE_COMBAT_STAGGER = 3 SECONDS, ACTIVE_COMBAT_STAMINA = 12), \ + ) + /obj/item/melee/gehenna // matthew's sword when he's asset protection name = "Gehenna" desc = "The christened blade of Matthew Scoria."