diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 0568963ccf3..3cb1af15965 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -530,6 +530,7 @@
///from base of obj/item/hit_reaction(): (list/args)
#define COMSIG_ITEM_HIT_REACT "item_hit_react"
#define COMPONENT_BLOCK_SUCCESSFUL (1 << 0)
+ #define COMPONENT_BLOCK_PERFECT (1 << 2)
///called on item when crossed by something (): (/atom/movable, mob/living/crossed)
#define COMSIG_ITEM_WEARERCROSSED "wearer_crossed"
///called on item when microwaved (): (obj/machinery/microwave/M)
diff --git a/code/datums/components/parry.dm b/code/datums/components/parry.dm
index ea3e41e65bb..6488de14a01 100644
--- a/code/datums/components/parry.dm
+++ b/code/datums/components/parry.dm
@@ -12,6 +12,8 @@
var/parryable_attack_types
/// the time between parry attempts
var/parry_cooldown
+ ///Do we wish to mute the parry sound?
+ var/no_parry_sound
/datum/component/parry/RegisterWithParent()
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(equipped))
@@ -26,7 +28,7 @@
if(ismob(I.loc))
UnregisterSignal(I.loc, COMSIG_LIVING_RESIST)
-/datum/component/parry/Initialize(_stamina_constant = 0, _stamina_coefficient = 0, _parry_time_out_time = PARRY_DEFAULT_TIMEOUT, _parryable_attack_types = ALL_ATTACK_TYPES, _parry_cooldown = 2 SECONDS)
+/datum/component/parry/Initialize(_stamina_constant = 0, _stamina_coefficient = 0, _parry_time_out_time = PARRY_DEFAULT_TIMEOUT, _parryable_attack_types = ALL_ATTACK_TYPES, _parry_cooldown = 2 SECONDS, _no_parry_sound = FALSE)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
@@ -34,6 +36,7 @@
stamina_constant = _stamina_constant
stamina_coefficient = _stamina_coefficient
parry_cooldown = _parry_cooldown
+ no_parry_sound = _no_parry_sound
if(islist(_parryable_attack_types))
parryable_attack_types = _parryable_attack_types
else
@@ -61,6 +64,7 @@
/datum/component/parry/proc/attempt_parry(datum/source, mob/living/carbon/human/owner, atom/movable/hitby, damage = 0, attack_type = MELEE_ATTACK)
SIGNAL_HANDLER
+ var/was_perfect = FALSE
if(!(attack_type in parryable_attack_types))
return
var/time_since_parry = world.time - time_parried
@@ -80,17 +84,22 @@
var/stamina_damage = stamina_coefficient * (((time_since_parry / parry_time_out_time) + armour_penetration_percentage / 100) * (damage + armour_penetration_flat)) + stamina_constant
- var/sound_to_play
- if(attack_type == PROJECTILE_ATTACK)
- sound_to_play = pick('sound/weapons/effects/ric1.ogg', 'sound/weapons/effects/ric2.ogg', 'sound/weapons/effects/ric3.ogg', 'sound/weapons/effects/ric4.ogg', 'sound/weapons/effects/ric5.ogg')
- else
- sound_to_play = 'sound/weapons/parry.ogg'
+ if(!no_parry_sound)
+ var/sound_to_play
+ if(attack_type == PROJECTILE_ATTACK)
+ sound_to_play = pick('sound/weapons/effects/ric1.ogg', 'sound/weapons/effects/ric2.ogg', 'sound/weapons/effects/ric3.ogg', 'sound/weapons/effects/ric4.ogg', 'sound/weapons/effects/ric5.ogg')
+ else
+ sound_to_play = 'sound/weapons/parry.ogg'
- playsound(owner, sound_to_play, clamp(stamina_damage, 40, 120))
+ playsound(owner, sound_to_play, clamp(stamina_damage, 40, 120))
+ if(time_since_parry <= parry_time_out_time * 0.5) // a perfect parry
+ was_perfect = TRUE
owner.adjustStaminaLoss(stamina_damage)
if(owner.getStaminaLoss() < 100)
- return COMPONENT_BLOCK_SUCCESSFUL
+ if(!was_perfect)
+ return COMPONENT_BLOCK_SUCCESSFUL
+ return (COMPONENT_BLOCK_SUCCESSFUL | COMPONENT_BLOCK_PERFECT)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 1c16ce13611..21308002d7f 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -81,7 +81,8 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons
var/breakouttime = 0
var/flags_cover = 0 //for flags such as GLASSESCOVERSEYES
- var/hit_reaction_chance = 0 //If you want to have something unrelated to blocking/armour piercing etc. Maybe not needed, but trying to think ahead/allow more freedom
+ /// Used to give a reaction chance on hit that is not a block. If less than 0, will remove the block message, allowing overides.
+ var/hit_reaction_chance = 0
// Needs to be in /obj/item because corgis can wear a lot of
// non-clothing items
@@ -398,9 +399,11 @@ GLOBAL_DATUM_INIT(welding_sparks, /mutable_appearance, mutable_appearance('icons
return ..()
/obj/item/proc/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
- if((SEND_SIGNAL(src, COMSIG_ITEM_HIT_REACT, owner, hitby, damage, attack_type) & COMPONENT_BLOCK_SUCCESSFUL) || prob(final_block_chance))
- owner.visible_message("[owner] blocks [attack_text] with [src]!")
- return TRUE
+ var/signal_result = SEND_SIGNAL(src, COMSIG_ITEM_HIT_REACT, owner, hitby, damage, attack_type) + prob(final_block_chance)
+ if(signal_result != 0)
+ if(hit_reaction_chance >= 0) //Normally used for non blocking hit reactions, but also used for displaying block message on actual blocks
+ owner.visible_message("[owner] blocks [attack_text] with [src]!")
+ return signal_result
return FALSE
// Generic use proc. Depending on the item, it uses up fuel, charges, sheets, etc.
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index 2cf35534ed2..bec8484be9e 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -32,8 +32,12 @@ emp_act
return -1
//Shields
- if(check_shields(P, P.damage, "the [P.name]", PROJECTILE_ATTACK, P.armour_penetration_flat, P.armour_penetration_percentage))
+ var/shield_check_result = check_shields(P, P.damage, "the [P.name]", PROJECTILE_ATTACK, P.armour_penetration_flat, P.armour_penetration_percentage)
+ if(shield_check_result == 1)
return 2
+ else if(shield_check_result == -1)
+ P.reflect_back(src)
+ return -1
if(mind?.martial_art?.deflection_chance) //Some martial arts users can deflect projectiles!
if(!IS_HORIZONTAL(src) && !HAS_TRAIT(src, TRAIT_HULK) && mind.martial_art.try_deflect(src)) //But only if they're not lying down, and hulks can't do it
@@ -219,9 +223,11 @@ emp_act
/mob/living/carbon/human/proc/check_shields(atom/AM, damage, attack_text = "the attack", attack_type = MELEE_ATTACK, armour_penetration_flat = 0, armour_penetration_percentage = 0)
var/obj/item/shield = get_best_shield()
-
- if(shield?.hit_reaction(src, AM, attack_text, 0, damage, attack_type))
+ var/shield_result = shield?.hit_reaction(src, AM, attack_text, 0, damage, attack_type)
+ if(shield_result >= 1)
return TRUE
+ if(shield_result == -1)
+ return -1
if(wear_suit && wear_suit.hit_reaction(src, AM, attack_text, 0, damage, attack_type))
return TRUE
diff --git a/code/modules/research/designs/weapon_designs.dm b/code/modules/research/designs/weapon_designs.dm
index a729dae4d2c..50065876b82 100644
--- a/code/modules/research/designs/weapon_designs.dm
+++ b/code/modules/research/designs/weapon_designs.dm
@@ -280,3 +280,14 @@
materials = list(MAT_GOLD = 5000, MAT_URANIUM = 4000, MAT_METAL = 5000, MAT_TITANIUM = 2000, MAT_BLUESPACE = 2000)
build_path = /obj/item/weaponcrafting/gunkit/u_ionsilencer
category = list("Weapons")
+
+/datum/design/v1_arm
+ name = "Vortex arm implant shell"
+ desc = "A shell to make an arm able to parry, reflect, and boost the power of incoming projectiles."
+ id = "v1_arm"
+ req_tech = list("combat" = 7, "magnets" = 6, "engineering" = 6, "biotech" = 7)
+ build_type = PROTOLATHE
+ materials = list(MAT_GOLD = 5000, MAT_URANIUM = 4000, MAT_METAL = 10000, MAT_TITANIUM = 2000, MAT_BLUESPACE = 2000)
+ reagents_list = list("blood" = 50)
+ build_path = /obj/item/v1_arm_shell
+ category = list("Weapons")
diff --git a/code/modules/surgery/organs/augments_arms.dm b/code/modules/surgery/organs/augments_arms.dm
index 9ac62b05403..49c46c6cbf8 100644
--- a/code/modules/surgery/organs/augments_arms.dm
+++ b/code/modules/surgery/organs/augments_arms.dm
@@ -411,3 +411,127 @@
contents = newlist(/obj/item/mop/advanced)
action_icon = list(/datum/action/item_action/organ_action/toggle = 'icons/obj/janitor.dmi')
action_icon_state = list(/datum/action/item_action/organ_action/toggle = "advmop")
+
+/obj/item/organ/internal/cyberimp/arm/v1_arm
+ name = "vortex feedback arm implant"
+ desc = "An implant, that when deployed surrounds the users arm in armor and circuitry, allowing them to redirect nearby projectiles with feedback from the vortex anomaly core."
+ origin_tech = "combat=6;magnets=6;biotech=6;engineering=6"
+ icon = 'icons/obj/items.dmi'
+ icon_state = "v1_arm"
+ parent_organ = "l_arm" //Left arm by default
+ slot = "l_arm_device"
+
+ contents = newlist(/obj/item/shield/v1_arm)
+ action_icon = list(/datum/action/item_action/organ_action/toggle = 'icons/obj/items.dmi')
+ action_icon_state = list(/datum/action/item_action/organ_action/toggle = "v1_arm")
+ var/disabled = FALSE
+
+/obj/item/organ/internal/cyberimp/arm/v1_arm/emp_act(severity)
+ if(emp_proof && !disabled)
+ return
+ disabled = TRUE
+ addtimer(VARSET_CALLBACK(src, disabled, FALSE), 10 SECONDS)
+
+/obj/item/organ/internal/cyberimp/arm/v1_arm/Extend(obj/item/item)
+ if(disabled)
+ to_chat(owner, "Your arm fails to extend!")
+ return FALSE
+ ..()
+
+/obj/item/organ/internal/cyberimp/arm/v1_arm/Retract()
+ if(disabled)
+ to_chat(owner, "Your arm fails to retract!")
+ return FALSE
+ ..()
+
+/obj/item/shield/v1_arm
+ name = "vortex feedback arm" //format is they are holding x
+ desc = "A modification to a users arm, allowing them to use a vortex core energy feedback, to parry, reflect, and even empower projectile attack. Rumors that it runs on the users blood are unconfirmed"
+ icon_state = "v1_arm"
+ item_state = "v1_arm"
+ sprite_sheets_inhand = list("Drask" = 'icons/mob/clothing/species/drask/held.dmi', "Vox" = 'icons/mob/clothing/species/vox/held.dmi')
+ force = 20 //bonk, not sharp
+ attack_verb = list("slamed", "punched", "parried", "judged", "styled on", "disrespected", "interupted", "gored")
+ hitsound = 'sound/effects/bang.ogg'
+ light_power = 3
+ light_range = 0
+ light_color = "#9933ff"
+ hit_reaction_chance = -1
+ /// The damage the reflected projectile will be increased by
+ var/reflect_damage_boost = 10
+ /// The cap of the reflected damage. Damage will not be increased above 50, however it will not be reduced to 50 either.
+ var/reflect_damage_cap = 50
+ var/disabled = FALSE
+ var/force_when_disabled = 5 //still basically a metal pipe, just hard to move
+
+
+/obj/item/shield/v1_arm/emp_act(severity)
+ if(disabled)
+ return
+ to_chat(loc, "Your arm seises up!")
+ disabled = TRUE
+ force = force_when_disabled
+ addtimer(CALLBACK(src, PROC_REF(reboot)), 10 SECONDS)
+
+/obj/item/shield/v1_arm/proc/reboot()
+ disabled = FALSE
+ force = initial(force)
+
+/obj/item/shield/v1_arm/add_parry_component()
+ AddComponent(/datum/component/parry, _stamina_constant = 2, _stamina_coefficient = 0.35, _parryable_attack_types = ALL_ATTACK_TYPES, _parry_cooldown = (1 / 3) SECONDS, _no_parry_sound = TRUE) // 0.3333 seconds of cooldown for 75% uptime, countered by ions and plasma pistols
+
+/obj/item/shield/v1_arm/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
+ if(disabled)
+ return FALSE
+ // Hit by a melee weapon or blocked a projectile
+ . = ..()
+ if(!.) // they did not block the attack
+ return
+ if(. == 1) // a normal block
+ owner.visible_message("[owner] blocks [attack_text] with [src]!")
+ playsound(src, 'sound/weapons/effects/ric3.ogg', 100, TRUE)
+ return TRUE
+
+ set_light(3)
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, set_light), 0), 0.25 SECONDS)
+
+ if(istype(hitby, /obj/item/projectile))
+ var/obj/item/projectile/P = hitby
+ if(P.shield_buster || istype(P, /obj/item/projectile/ion)) //EMP's and unpariable attacks, after all.
+ return FALSE
+ if(P.reflectability == REFLECTABILITY_NEVER) //only 1 magic spell does this, but hey, needed
+ owner.visible_message("[owner] blocks [attack_text] with [src]!")
+ playsound(src, 'sound/weapons/effects/ric3.ogg', 100, TRUE)
+ return TRUE
+
+ P.damage = clamp((P.damage + 10), P.damage, reflect_damage_cap)
+ var/sound = pick('sound/effects/explosion1.ogg', 'sound/effects/explosion2.ogg', 'sound/effects/meteorimpact.ogg')
+ P.hitsound = sound
+ P.hitsound_wall = sound
+ P.add_overlay("parry")
+ playsound(src, 'sound/weapons/v1_parry.ogg', 100, TRUE)
+ owner.visible_message("[owner] parries [attack_text] with [src]!")
+ add_attack_logs(P.firer, src, "hit by [P.type] but got parried by [src]")
+ return -1
+
+ owner.visible_message("[owner] parries [attack_text] with [src]!")
+ playsound(src, 'sound/weapons/v1_parry.ogg', 100, TRUE)
+ if(isitem(hitby)) //Thrown items
+ var/obj/item/TT = hitby
+ addtimer(CALLBACK(TT, TYPE_PROC_REF(/atom/movable, throw_at), locateUID(TT.thrownby), 15, 15, owner), 0.1 SECONDS) //yeet that shit right back
+ return TRUE
+ melee_attack_chain(owner, hitby)
+ return TRUE
+
+/obj/item/v1_arm_shell
+ name = "vortex feedback arm implant frame"
+ desc = "An implant awaiting installation of a vortex anomaly core"
+ icon_state = "v1_arm"
+
+/obj/item/v1_arm_shell/attackby(obj/item/I, mob/user, params)
+ if(istype(I, /obj/item/assembly/signaler/anomaly/vortex))
+ to_chat(user, "You insert [I] into the back of the hand, and the implant begins to boot up.")
+ new /obj/item/organ/internal/cyberimp/arm/v1_arm(get_turf(src))
+ qdel(src)
+ qdel(I)
+ return ..()
diff --git a/icons/mob/clothing/species/drask/held.dmi b/icons/mob/clothing/species/drask/held.dmi
index 51e93e98469..fea2228adec 100644
Binary files a/icons/mob/clothing/species/drask/held.dmi and b/icons/mob/clothing/species/drask/held.dmi differ
diff --git a/icons/mob/clothing/species/vox/held.dmi b/icons/mob/clothing/species/vox/held.dmi
index 853076c23ae..973f8aa94cc 100644
Binary files a/icons/mob/clothing/species/vox/held.dmi and b/icons/mob/clothing/species/vox/held.dmi differ
diff --git a/icons/mob/inhands/items_lefthand.dmi b/icons/mob/inhands/items_lefthand.dmi
index a0dfef8af58..c8db062af90 100644
Binary files a/icons/mob/inhands/items_lefthand.dmi and b/icons/mob/inhands/items_lefthand.dmi differ
diff --git a/icons/mob/inhands/items_righthand.dmi b/icons/mob/inhands/items_righthand.dmi
index 5d7537c3a20..12f380147fa 100644
Binary files a/icons/mob/inhands/items_righthand.dmi and b/icons/mob/inhands/items_righthand.dmi differ
diff --git a/icons/obj/items.dmi b/icons/obj/items.dmi
index af0a38e7590..5371bccda14 100644
Binary files a/icons/obj/items.dmi and b/icons/obj/items.dmi differ
diff --git a/icons/obj/projectiles.dmi b/icons/obj/projectiles.dmi
index bc4eac79664..a042e446cf7 100644
Binary files a/icons/obj/projectiles.dmi and b/icons/obj/projectiles.dmi differ
diff --git a/sound/weapons/v1_parry.ogg b/sound/weapons/v1_parry.ogg
new file mode 100644
index 00000000000..5179015ffaf
Binary files /dev/null and b/sound/weapons/v1_parry.ogg differ