diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_living.dm
index a016c26000f..3857ea06129 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_living.dm
@@ -113,3 +113,6 @@
#define COMSIG_LIVING_PREBITE_SELF "living_prebite"
#define COMSIG_LIVING_POSTBITE_SELF "living_postbite"
+
+/// From [mob/living/MiddleClickOn] before a middle mouse intent is performed
+#define COMSIG_MOB_PRE_SPECIAL_MIDDLE "pre_special_middle"
diff --git a/code/__DEFINES/dcs/signals/signals_objectives.dm b/code/__DEFINES/dcs/signals/signals_objectives.dm
index fa9613fafb7..ec7ad33a7c7 100644
--- a/code/__DEFINES/dcs/signals/signals_objectives.dm
+++ b/code/__DEFINES/dcs/signals/signals_objectives.dm
@@ -52,6 +52,8 @@
#define COMSIG_MOB_BUTCHERED "mob_butchered"
/// from /datum/species/proc/kicked() (mob/user, mob/target, zone_hit, damage_blocked)
#define COMSIG_MOB_KICK "mob_kick"
+/// from /datum/species/proc/kicked() (mob/user, mob/target, zone_hut, damage_blocked)
+#define COMSIG_MOB_KICKED "mob_kicked"
/// from /obj/structure/closet/dirthole/attackby() (mob/user)
#define COMSIG_GRAVE_ROBBED "grave_robbed"
/// from /datum/action/cooldown/spell/find_flaw/cast() (datum/charflaw/flaw, mob/target)
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index b0d4ebfaff0..22d52691ca3 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -184,13 +184,6 @@
/area/flick_overlay_view(mutable_appearance/display, duration)
return
-/proc/flick_overlay_view(image/I, atom/target, duration) //wrapper for the above, flicks to everyone who can see the target atom
- var/list/viewing = list()
- for(var/mob/M as anything in viewers(target))
- if(M.client)
- viewing += M.client
- flick_overlay(I, viewing, duration)
-
/proc/get_active_player_count(alive_check = 0, afk_check = 0, human_check = 0)
// Get active players who are playing in the round
var/active_players = 0
diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm
index 9c10622410c..10ee518d316 100644
--- a/code/_onclick/other_mobs.dm
+++ b/code/_onclick/other_mobs.dm
@@ -223,93 +223,94 @@
/mob/living/MiddleClickOn(atom/A, list/modifiers)
- ..()
+ . = ..()
if(!mmb_intent)
if(!A.Adjacent(src))
return
A.MiddleClick(src, modifiers)
- else
- switch(mmb_intent.type)
- if(INTENT_KICK)
- if(src.usable_legs < 2)
- return
- if(!A.Adjacent(src))
+ return
+
+ SEND_SIGNAL(src, COMSIG_MOB_PRE_SPECIAL_MIDDLE, A)
+
+ switch(mmb_intent.type)
+ if(INTENT_KICK)
+ if(src.usable_legs < 2)
+ return
+ if(!A.Adjacent(src))
+ return
+ if(A == src)
+ var/list/mobs_here = list()
+ for(var/mob/M in get_turf(src))
+ if(M.invisibility || M == src)
+ continue
+ mobs_here += M
+ if(mobs_here.len)
+ A = pick(mobs_here)
+ if(A == src) //auto aim couldn't select another target
return
- if(A == src)
- var/list/mobs_here = list()
- for(var/mob/M in get_turf(src))
- if(M.invisibility || M == src)
- continue
- mobs_here += M
- if(mobs_here.len)
- A = pick(mobs_here)
- if(A == src) //auto aim couldn't select another target
+ if(IsOffBalanced())
+ to_chat(src, span_warning("I haven't regained my balance yet."))
+ return
+ changeNext_move(mmb_intent.clickcd)
+ face_atom(A)
+
+ if(ismob(A))
+ var/mob/living/M = A
+ if(src.used_intent)
+
+ do_attack_animation(M, visual_effect_icon = ATTACK_EFFECT_KICK, used_item = FALSE, atom_bounce = TRUE)
+ playsound(src, pick(PUNCHWOOSH), 100, FALSE, -1)
+
+ sleep(src.used_intent.swingdelay)
+ if(QDELETED(src) || QDELETED(M))
+ return
+ if(!M.Adjacent(src))
+ return
+ if(src.incapacitated(IGNORE_GRAB))
return
- if(IsOffBalanced())
- to_chat(src, span_warning("I haven't regained my balance yet."))
- return
- changeNext_move(mmb_intent.clickcd)
- face_atom(A)
-
- if(ismob(A))
- var/mob/living/M = A
- if(src.used_intent)
-
- do_attack_animation(M, visual_effect_icon = ATTACK_EFFECT_KICK, used_item = FALSE, atom_bounce = TRUE)
- playsound(src, pick(PUNCHWOOSH), 100, FALSE, -1)
-
- sleep(src.used_intent.swingdelay)
- if(QDELETED(src) || QDELETED(M))
- return
- if(!M.Adjacent(src))
- return
- if(src.incapacitated(IGNORE_GRAB))
- return
- if(M.checkmiss(src))
- return
- if(M.checkdefense(src.used_intent, src))
- return
if(M.checkmiss(src))
return
- if(!M.checkdefense(mmb_intent, src))
- if(ishuman(M))
- var/mob/living/carbon/human/H = M
- H.dna.species.kicked(src, H)
- else
- M.onkick(src)
- OffBalance(15) // Off balance for human enemies moved to dna.species.onkick
- else
- A.onkick(src)
- OffBalance(10)
- return
- if(INTENT_JUMP)
- jump_action(A)
- if(INTENT_BITE)
- if(!A.Adjacent(src))
- return
- if(A == src)
- return
- if(src.incapacitated(IGNORE_GRAB))
- return
- if(stat != CONSCIOUS)
- return
- if(is_mouth_covered())
- to_chat(src, span_warning("My mouth is blocked."))
- return
- if(HAS_TRAIT(src, TRAIT_NO_BITE))
- to_chat(src, span_warning("I can't bite."))
- return
- if(iscarbon(src))
- var/mob/living/carbon/C = src
- if(C.mouth)
- to_chat(src, span_warning("My mouth has something in it."))
+ if(M.checkdefense(src.used_intent, src))
return
- changeNext_move(mmb_intent.clickcd)
- face_atom(A)
- bite(A)
+ if(M.checkmiss(src))
+ return
+ if(!M.checkdefense(mmb_intent, src))
+ if(ishuman(M))
+ var/mob/living/carbon/human/H = M
+ H.dna.species.kicked(src, H)
+ else
+ M.onkick(src)
+ OffBalance(15) // Off balance for human enemies moved to dna.species.onkick
+ else
+ A.onkick(src)
+ OffBalance(10)
+ if(INTENT_JUMP)
+ jump_action(A)
+ if(INTENT_BITE)
+ if(!A.Adjacent(src))
return
- if(INTENT_STEAL)
- steal_action(A)
+ if(A == src)
+ return
+ if(src.incapacitated(IGNORE_GRAB))
+ return
+ if(stat != CONSCIOUS)
+ return
+ if(is_mouth_covered())
+ to_chat(src, span_warning("My mouth is blocked."))
+ return
+ if(HAS_TRAIT(src, TRAIT_NO_BITE))
+ to_chat(src, span_warning("I can't bite."))
+ return
+ if(iscarbon(src))
+ var/mob/living/carbon/C = src
+ if(C.mouth)
+ to_chat(src, span_warning("My mouth has something in it."))
+ return
+ changeNext_move(mmb_intent.clickcd)
+ face_atom(A)
+ bite(A)
+ if(INTENT_STEAL)
+ steal_action(A)
//Return TRUE to cancel other attack hand effects that respect it.
/atom/proc/attack_hand(mob/user, list/modifiers)
@@ -524,6 +525,10 @@
to_chat(src, span_warning("That's too high for me..."))
return
+ if(has_status_effect(/datum/status_effect/debuff/exposed))
+ to_chat(src, span_warning("I'm exposed and lost my footing! I can't jump!"))
+ return FALSE
+
changeNext_move(mmb_intent?.clickcd ? mmb_intent.clickcd : CLICK_CD_MELEE)
face_atom(A)
diff --git a/code/datums/status_effects/debuffs.dm b/code/datums/status_effects/debuffs.dm
index d40cc9784c0..2c3360940bb 100644
--- a/code/datums/status_effects/debuffs.dm
+++ b/code/datums/status_effects/debuffs.dm
@@ -488,36 +488,59 @@
msg_stage++
-/// Prevents use of weapon special attacks
-/datum/status_effect/debuff/specialcd
- id = "specialcd"
- alert_type = /atom/movable/screen/alert/status_effect/debuff/specialcd
- duration = 30 SECONDS
- status_type = STATUS_EFFECT_UNIQUE
+/// Prevent clicks for the duration of the ability
+/datum/status_effect/debuff/clickcd
+ id = "clickcd"
+ alert_type = /atom/movable/screen/alert/status_effect/debuff/clickcd
+ duration = 3 SECONDS
+
+/datum/status_effect/debuff/clickcd/on_creation(mob/living/new_owner, duration_override, ...)
+ new_owner.changeNext_move(duration)
+ return ..()
-/atom/movable/screen/alert/status_effect/debuff/specialcd
- name = "Special Manouevre Cooldown"
+/atom/movable/screen/alert/status_effect/debuff/clickcd
+ name = "Action Delayed"
+ desc = "I cannot take another action."
+ icon_state = "clickcd"
+
+/datum/status_effect/debuff/clashcd
+ id = "clashcd"
+ alert_type = /atom/movable/screen/alert/status_effect/debuff/clashcd
+ duration = 20 SECONDS
+
+/atom/movable/screen/alert/status_effect/debuff/clashcd
+ name = "Riposte / Guard Cooldown"
desc = "I used it. I must wait."
- icon_state = "specialcd"
+ icon_state = "guardcd"
/datum/status_effect/debuff/exposed
- id = "nofeint"
+ id = "exposed"
alert_type = /atom/movable/screen/alert/status_effect/debuff/exposed
duration = 10 SECONDS
mob_overlay_icon_state = "eff_exposed"
/atom/movable/screen/alert/status_effect/debuff/exposed
name = "Exposed"
- desc = "My defenses are exposed. I can be hit through my parry and dodge!"
+ desc = "My defenses are completely exposed. I can be hit through my parry and dodge to great effect!"
icon_state = "exposed"
+/datum/status_effect/debuff/vulnerable
+ id = "vulnerable"
+ alert_type = /atom/movable/screen/alert/status_effect/debuff/vulnerable
+ duration = 10 SECONDS
+ mob_overlay_icon_state = "eff_vulnerable"
+
+/atom/movable/screen/alert/status_effect/debuff/vulnerable
+ name = "Vulnerable"
+ desc = "A mistake. I can be hit through my parry and dodge to a lighter effect!"
+ icon_state = "vulnerable"
+
/datum/status_effect/debuff/feinted
id = "feinted"
alert_type = /atom/movable/screen/alert/status_effect/debuff/feinted
- duration = 5 SECONDS
+ duration = 30 SECONDS
/atom/movable/screen/alert/status_effect/debuff/feinted
-
name = "Feinted"
desc = span_boldwarning("I've been feinted. It won't happen again so soon.")
icon_state = "feinted"
@@ -525,13 +548,25 @@
/datum/status_effect/debuff/feintcd
id = "feintcd"
alert_type = /atom/movable/screen/alert/status_effect/debuff/feintcd
- duration = 10 SECONDS
+ duration = 30 SECONDS
/atom/movable/screen/alert/status_effect/debuff/feintcd
name = "Feint Cooldown"
desc = span_warning("I have feinted recently, my opponents will be wary.")
icon_state = "feinted"
+/// Prevents use of weapon special attacks
+/datum/status_effect/debuff/specialcd
+ id = "specialcd"
+ alert_type = /atom/movable/screen/alert/status_effect/debuff/specialcd
+ duration = 30 SECONDS
+ status_type = STATUS_EFFECT_UNIQUE
+
+/atom/movable/screen/alert/status_effect/debuff/specialcd
+ name = "Special Manouevre Cooldown"
+ desc = "I used it. I must wait."
+ icon_state = "specialcd"
+
/// 2 Speed reduction for 8 seconds + slow
/datum/status_effect/debuff/hobbled
id = "hobbled"
@@ -551,3 +586,4 @@
/datum/status_effect/debuff/hobbled/on_remove()
. = ..()
owner?.remove_movespeed_modifier(MOVESPEED_ID_STATUS_EFFECT(id))
+
diff --git a/code/datums/status_effects/rogue/roguebuff.dm b/code/datums/status_effects/rogue/roguebuff.dm
index 7ba6f5ea428..7476b9e3d58 100644
--- a/code/datums/status_effects/rogue/roguebuff.dm
+++ b/code/datums/status_effects/rogue/roguebuff.dm
@@ -17,6 +17,7 @@
if(iscarbon(owner))
var/mob/living/carbon/C = owner
C.add_stress(/datum/stress_event/drunk)
+
/datum/status_effect/buff/drunk/on_remove()
. = ..()
if(iscarbon(owner))
diff --git a/code/datums/status_effects/status_effect.dm b/code/datums/status_effects/status_effect.dm
index 80cf4b78819..0a2e484b2e2 100644
--- a/code/datums/status_effects/status_effect.dm
+++ b/code/datums/status_effects/status_effect.dm
@@ -36,7 +36,7 @@
var/list/effectedstats = list()
/// Variables to create a mob overlay if applicable
- var/mob_overlay_icon
+ var/mob_overlay_icon = 'icons/mob/mob_effects.dmi'
var/mob_overlay_icon_state
var/mob_overlay_layer
diff --git a/code/datums/wounds/_wound.dm b/code/datums/wounds/_wound.dm
index 04e13b2b0bc..23c9b7ddd9f 100644
--- a/code/datums/wounds/_wound.dm
+++ b/code/datums/wounds/_wound.dm
@@ -424,7 +424,6 @@ GLOBAL_LIST_INIT(primordial_wounds, init_primordial_wounds())
return
to_chat(human_owner, span_danger("I feel horrible... REALLY horrible..."))
MOBTIMER_SET(human_owner, MT_PUKE)
- human_owner.vomit(1, blood = TRUE, stun = FALSE)
werewolf_infection_timer = addtimer(CALLBACK(src, PROC_REF(wake_werewolf)), werewolf_infection_time, TIMER_STOPPABLE)
severity = WOUND_SEVERITY_BIOHAZARD
if(bodypart_owner)
diff --git a/code/game/atom/atoms_movable.dm b/code/game/atom/atoms_movable.dm
index 645c2cc0d61..180303f8471 100644
--- a/code/game/atom/atoms_movable.dm
+++ b/code/game/atom/atoms_movable.dm
@@ -1113,13 +1113,13 @@
/atom/movable/proc/do_item_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, animation_type = ATTACK_ANIMATION_SWIPE)
if (visual_effect_icon)
- var/image/attack_image = image(icon = 'icons/effects/effects.dmi', icon_state = visual_effect_icon)
- attack_image.plane = attacked_atom.plane + 1
+ var/mutable_appearance/attack_appearance = mutable_appearance('icons/effects/effects.dmi', visual_effect_icon)
+ attack_appearance.plane = GAME_PLANE
// Scale the icon.
- attack_image.transform *= 0.4
+ attack_appearance.transform *= 0.4
// The icon should not rotate.
- attack_image.appearance_flags = APPEARANCE_UI
- var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_image, 1 SECONDS)
+ attack_appearance.appearance_flags = APPEARANCE_UI
+ var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_appearance, 1 SECONDS)
var/matrix/copy_transform = new(initial(transform))
attack.dir = get_dir(src, attacked_atom)
animate(
@@ -1140,16 +1140,16 @@
if (!used_item)
return
- var/image/attack_image = image(icon = used_item, icon_state = used_item.icon_state)
- attack_image.plane = attacked_atom.plane + 1
- attack_image.pixel_w = used_item.pixel_x + used_item.pixel_w
- attack_image.pixel_z = used_item.pixel_y + used_item.pixel_z
+ var/mutable_appearance/attack_appearance = mutable_appearance(used_item.icon, used_item.icon_state)
+ attack_appearance.plane = GAME_PLANE
+ attack_appearance.pixel_w = used_item.pixel_x + used_item.pixel_w
+ attack_appearance.pixel_z = used_item.pixel_y + used_item.pixel_z
// Scale the icon.
- attack_image.transform *= 0.5
+ attack_appearance.transform *= 0.5
// The icon should not rotate.
- attack_image.appearance_flags = APPEARANCE_UI
+ attack_appearance.appearance_flags = APPEARANCE_UI
- var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_image, 1 SECONDS)
+ var/atom/movable/flick_visual/attack = attacked_atom.flick_overlay_view(attack_appearance, 1 SECONDS)
var/matrix/copy_transform = new(transform)
var/x_sign = 0
var/y_sign = 0
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index b652a48abfd..310216e239e 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -1357,6 +1357,10 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
wdefense -= 1
user.update_a_intents()
+/obj/item/proc/is_wielded()
+ var/datum/component/two_handed/two_handed = GetComponent(/datum/component/two_handed)
+ return two_handed?.wielded
+
/obj/item/proc/toggle_altgrip(mob/user, override_state)
if(!alt_intents)
return
@@ -1428,7 +1432,7 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e
if(!istype(loc, /turf))
return
source = loc
- var/image/pickup_animation = image(icon = src, layer = layer + 0.1)
+ var/mutable_appearance/pickup_animation = mutable_appearance(icon, icon_state, layer = layer + 0.1)
pickup_animation.plane = GAME_PLANE
pickup_animation.transform.Scale(0.75)
pickup_animation.appearance_flags = APPEARANCE_UI_IGNORE_ALPHA
diff --git a/code/game/objects/items/weapons/rmb_intents.dm b/code/game/objects/items/weapons/rmb_intents.dm
index 3e87b88914c..7191d46d6f6 100644
--- a/code/game/objects/items/weapons/rmb_intents.dm
+++ b/code/game/objects/items/weapons/rmb_intents.dm
@@ -17,65 +17,7 @@
return potential
/datum/rmb_intent/proc/special_attack(mob/living/user, atom/target)
- if(!user)
- return FALSE
-
- if(!user.Adjacent(target))
- return FALSE
-
- if(user.incapacitated(IGNORE_GRAB))
- return FALSE
-
- var/mob/living/L = get_target(target)
- if(!istype(L))
- return FALSE
-
- user.changeNext_move(CLICK_CD_FAST)
- playsound(user, 'sound/combat/feint.ogg', 100, TRUE)
- user.visible_message(span_danger("[user] feints an attack at [target]!"))
- var/perc = 50
- if(user.mind)
- var/obj/item/I = user.get_active_held_item()
- var/ourskill = 0
- var/theirskill = 0
- if(I)
- if(I.associated_skill)
- ourskill = user.get_skill_level(I.associated_skill, TRUE)
- if(L.mind)
- I = L.get_active_held_item()
- if(I?.associated_skill)
- theirskill = L.get_skill_level(I.associated_skill, TRUE)
- perc += (ourskill - theirskill) * 15 //skill is of the essence
- perc += (user.STAINT - L.STAINT) * 10 //but it's also mostly a mindgame
- perc += (user.STASPD - L.STASPD) * 5 //yet a speedy feint is hard to counter
- perc += (user.STAPER - L.STAPER) * 5 //a good eye helps
-
- if(!user.cmode)
- perc = 0
-
- if(L.has_status_effect(/datum/status_effect/debuff/exposed))
- perc = 0
-
- if(user.has_status_effect(/datum/status_effect/debuff/feintcd))
- perc -= rand(10,30)
-
- user.apply_status_effect(/datum/status_effect/debuff/feintcd)
- perc = CLAMP(perc, 0, 90) //no zero risk superfeinting
-
- if(prob(perc)) //feint intent increases the immobilize duration significantly
- if(istype(user.rmb_intent, /datum/rmb_intent/feint))
- L.changeNext_move(10)
- L.Immobilize(15)
- else
- L.changeNext_move(4)
- L.Immobilize(5)
- L.apply_status_effect(/datum/status_effect/debuff/exposed, 5 SECONDS)
- to_chat(user, span_notice("[L] fell for my feint attack!"))
- to_chat(L, span_danger("I fall for [user]'s feint attack!"))
- else if(user.client?.prefs.showrolls)
- to_chat(user, span_warning("[L] did not fall for my feint... [perc]%"))
-
- return TRUE
+ return
/datum/rmb_intent/aimed
name = "aimed"
@@ -127,13 +69,104 @@
/datum/rmb_intent/feint
name = "feint"
- desc = "(RMB WHILE DEFENSE IS ACTIVE) A deceptive half-attack with no follow-through, meant to force your opponent to open their guard. Useless against someone who is dodging."
+ desc = "(RMB WHILE IN COMBAT MODE) A deceptive half-attack with no follow-through, meant to force your opponent to open their guard.."
icon_state = "rmbfeint"
def_bonus = 10
+ /// Duration of the feint expose / vulnerable effect
+ var/feint_duration = 7.5 SECONDS
-/datum/status_effect/debuff/riposted
- id = "riposted"
- duration = 30
+/datum/rmb_intent/feint/special_attack(mob/living/user, atom/target)
+ if(!user)
+ return FALSE
+
+ if(user == target)
+ return FALSE
+
+ if(user.incapacitated(IGNORE_GRAB))
+ return FALSE
+
+ if(user.has_status_effect(/datum/status_effect/debuff/feintcd))
+ return FALSE
+
+ var/mob/living/defender = get_target(target)
+ if(!istype(defender))
+ return FALSE
+
+ var/obj/item/attacker_item = user.get_active_held_item()
+ if(!attacker_item && !user.Adjacent(target))
+ return FALSE
+
+ if(get_dist(user, target) > user.used_intent?.reach)
+ return FALSE
+
+ user.visible_message(
+ span_danger("[user] feints an attack at [defender]!"),
+ span_userdanger("I feint an attack at [defender]!"),
+ )
+
+ var/perc = 50
+ var/ourskill = 0
+ var/theirskill = 0
+ var/skill_factor = 0
+
+
+ if(attacker_item?.associated_skill)
+ ourskill = user.get_skill_level(attacker_item.associated_skill)
+
+ var/obj/item/defender_item = defender.get_active_held_item()
+ if(defender_item?.associated_skill)
+ theirskill = defender.get_skill_level(defender_item.associated_skill)
+
+ perc += (ourskill - theirskill) * 12 //skill is of the essence
+ perc += (user.STAINT - defender.STAINT) * 8 //but it's also mostly a mindgame
+ perc += (user.STASPD - defender.STASPD) * 3 //yet a speedy feint is hard to counter
+ perc += (user.STAPER - defender.STAPER) * 3 //a good eye helps
+
+ skill_factor = (ourskill - theirskill) / 2
+
+ var/special_message
+ var/cooldown_override = 20 SECONDS
+
+ if(defender.has_status_effect(/datum/status_effect/debuff/exposed) || \
+ defender.has_status_effect(/datum/status_effect/debuff/vulnerable))
+ perc = 0
+
+ if(defender.is_blind() || !defender.can_see_cone(user))
+ perc = 0
+ cooldown_override = 5 SECONDS
+ special_message = span_warning("They need to see me for me to feint them!")
+
+ perc = CLAMP(perc, 0, 90)
+
+ if(!prob(perc))
+ playsound(user, 'sound/combat/feint.ogg', 100, TRUE)
+ if(user.client?.prefs.showrolls)
+ to_chat(user, span_warning("[defender.p_they(TRUE)] did not fall for my feint... [perc]%"))
+ user.apply_status_effect(/datum/status_effect/debuff/feintcd)
+ if(special_message)
+ to_chat(user, special_message)
+ return TRUE
+
+ if(defender.has_status_effect(/datum/status_effect/buff/clash))
+ defender.remove_status_effect(/datum/status_effect/buff/clash)
+ defender.balloon_alert(user, "guard interrupted!")
+
+ var/effect_to_apply = defender.mind ? /datum/status_effect/debuff/vulnerable : /datum/status_effect/debuff/exposed
+
+ defender.apply_status_effect(effect_to_apply, feint_duration)
+ defender.apply_status_effect(/datum/status_effect/debuff/clickcd, max(1.5 SECONDS + skill_factor, 2.5 SECONDS))
+ defender.Immobilize(0.5 SECONDS)
+ defender.adjust_stamina(defender.stamina * 0.1)
+ defender.Slowdown(2)
+
+ user.changeNext_move(CLICK_CD_FAST) //We don't want the feint effect to be popped instantly.
+ user.apply_status_effect(/datum/status_effect/debuff/feintcd, cooldown_override)
+
+ to_chat(user, span_notice("[defender.p_they(TRUE)] fell for my feint attack!"))
+ to_chat(defender, span_danger("I fall for [user.p_their()] feint attack!"))
+ playsound(user, 'sound/combat/riposte.ogg', 100, TRUE)
+
+ return TRUE
/datum/rmb_intent/riposte
name = "defend"
@@ -141,6 +174,30 @@
icon_state = "rmbdef"
def_bonus = 10
+/datum/rmb_intent/riposte/special_attack(mob/living/user, atom/target)
+ if(user.has_status_effect(/datum/status_effect/buff/clash))
+ return FALSE
+
+ if(user.has_status_effect(/datum/status_effect/debuff/clashcd))
+ return FALSE
+
+ if(!user.get_active_held_item()) //Nothing in our hand to Guard with.
+ return FALSE
+
+ if(user.incapacitated()) //Not usable while grabs are in play.
+ return FALSE
+
+ if(user.IsImmobilized() || user.IsOffBalanced()) //Not usable while we're offbalanced or immobilized
+ return FALSE
+
+ if(user.m_intent == MOVE_INTENT_RUN)
+ to_chat(user, span_warning("I can't focus on this while running."))
+ return FALSE
+
+ user.apply_status_effect(/datum/status_effect/buff/clash)
+
+ return TRUE
+
/datum/rmb_intent/guard
name = "guarde"
desc = "(RMB WHILE DEFENSE IS ACTIVE) Raise your weapon, ready to attack any creature who moves onto the space you are guarding."
diff --git a/code/modules/combat/clash/clash.dm b/code/modules/combat/clash/clash.dm
new file mode 100644
index 00000000000..e18f6a04ba3
--- /dev/null
+++ b/code/modules/combat/clash/clash.dm
@@ -0,0 +1,172 @@
+// File for clashing procs see [/datum/status_effect/buff/clash]
+
+/// Clash with the attacker, either resulting in a disarm, a riposte or a strike
+/mob/living/proc/process_clash(mob/living/user)
+ if(user == src)
+ return
+
+ var/obj/item/our_item = get_active_held_item()
+ var/obj/item/their_item = user.get_active_held_item()
+
+ if(!their_item) //The opponent is trying to rawdog us with their bare hands while we have Guard up. We get a free attack on their active hand.
+ var/obj/item/bodypart/affecting = user.get_bodypart("[(user.active_hand_index % 2 == 0) ? "r" : "l" ]_arm")
+ var/force = get_complex_damage(our_item, src)
+ var/armor_block = user.run_armor_check(BODY_ZONE_PRECISE_L_HAND, used_intent.item_damage_type, armor_penetration = used_intent.penfactor, damage = force)
+ if(user.apply_damage(force, our_item.damtype, affecting, armor_block))
+ visible_message(span_suicide("[src] gores [user]'s hands with \the [our_item]!"))
+ affecting?.bodypart_attacked_by(used_intent.blade_class, force, crit_message = TRUE)
+ else
+ visible_message(span_suicide("[src] clashes into [user]'s hands with \the [our_item]!"))
+
+ playsound(src, pick(used_intent.hitsound), 80)
+ remove_status_effect(/datum/status_effect/buff/clash)
+ return
+
+ if(user.has_status_effect(/datum/status_effect/buff/clash))
+ clash(user, our_item, their_item)
+ return
+
+ var/damage = get_complex_damage(our_item, src, their_item.blade_dulling)
+ if(our_item.wbalance < 0)
+ damage *= 1.5
+
+ their_item.take_damage(max(damage, 1), BRUTE, our_item.damage_type)
+ visible_message(span_suicide("[src] ripostes [user] with \the [our_item]!"))
+ span_notice("[capitalize(user.p_theyre())] exposed!")
+ playsound(src, 'sound/combat/clash_struck.ogg', 100)
+
+ user.apply_status_effect(/datum/status_effect/debuff/exposed, 3 SECONDS)
+ user.apply_status_effect(/datum/status_effect/debuff/clickcd, 3 SECONDS)
+
+ remove_status_effect(/datum/status_effect/buff/clash)
+
+/// Decide who gets disarmed if both combants have clash enabled
+/mob/living/proc/clash(mob/living/user, obj/item/our_item, obj/item/their_item)
+ var/instantloss = FALSE
+ var/instantwin = FALSE
+
+ //Stat checks. Basic comparison.
+ var/strdiff = STASTR - user.STASTR
+ var/perdiff = STAPER - user.STAPER
+ var/spddiff = STASPD - user.STASPD
+ var/fordiff = STALUC - user.STALUC
+ var/intdiff = STAINT - user.STAINT
+
+ var/list/statdiffs = list(strdiff, perdiff, spddiff, fordiff, intdiff)
+
+ //Skill check, very simple. If you're more skilled with your weapon than the opponent is with theirs -> +10% to disarm or vice-versa.
+ var/skilldiff
+ if(our_item.associated_skill)
+ skilldiff = get_skill_level(our_item.associated_skill)
+ else
+ instantloss = TRUE //We are Guarding with a book or something -- no chance for us.
+
+ if(their_item.associated_skill)
+ skilldiff = skilldiff - user.get_skill_level(their_item.associated_skill)
+ else
+ instantwin = TRUE //THEY are Guarding with a book or something -- no chance for them.
+
+ //Weapon checks.
+ var/lengthdiff = our_item.wlength - their_item.wlength //The longer the weapon the better.
+ var/wieldeddiff = our_item.is_wielded() - their_item.is_wielded() //If ours is wielded but theirs is not.
+ var/weightdiff = our_item.wbalance < their_item.wbalance //If our weapon is heavy-balanced and theirs is not.
+ var/wildcard = rand(-1, 1)
+
+ var/list/wepdiffs = list(lengthdiff, wieldeddiff, weightdiff)
+
+ var/prob_us = 0
+ var/prob_opp = 0
+
+ //Stat checks only matter if their difference is 2 or more.
+ for(var/statdiff in statdiffs)
+ if(statdiff >= 2)
+ prob_us += 10
+ else if(statdiff <= -2)
+ prob_opp += 10
+
+ for(var/wepdiff in wepdiffs)
+ if(wepdiff > 0)
+ prob_us += 10
+ else if(wepdiff < 0)
+ prob_opp += 10
+
+ //Wildcard modifier that can go either way or to neither.
+ if(wildcard > 0)
+ prob_us += 10
+ else if(wildcard < 0 )
+ prob_opp += 10
+
+ //Small bonus to the first one to strike in a Clash.
+ var/initiator_bonus = rand(5, 10)
+ prob_us += initiator_bonus
+
+ if((!instantloss && !instantwin) || (instantloss && instantwin)) //We are both using normal weapons OR we're both using memes. Either way, proceed as normal.
+ visible_message(span_boldwarning("[src] and [user] clash!"))
+ flash_fullscreen("whiteflash")
+ user.flash_fullscreen("whiteflash")
+ var/datum/effect_system/spark_spread/S = new()
+ var/turf/front = get_step(src,src.dir)
+ S.set_up(1, 1, front)
+ S.start()
+ var/success
+ if(prob(prob_us))
+ user.show_overhead_indicator('icons/mob/overhead_effects.dmi', "clashtwo", 1 SECONDS, y_offset = 24, sound = 'sound/combat/clash_disarm_us.ogg')
+ disarm_weapon()
+ Slowdown(5)
+ success = TRUE
+ if(prob(prob_opp))
+ user.disarm_weapon()
+ user.Slowdown(5)
+ show_overhead_indicator('icons/mob/overhead_effects.dmi', "clashtwo", 1 SECONDS, y_offset = 24, sound = 'sound/combat/clash_disarm_opp.ogg')
+ success = TRUE
+ if(!success)
+ to_chat(src, span_warningbig("Draw! Opponent's chances were... [prob_opp]%"))
+ to_chat(user, span_warningbig("Draw! Opponent's chances were... [prob_us]%"))
+ playsound(src, 'sound/combat/clash_draw.ogg', 100, TRUE)
+ else if(instantloss)
+ disarm_weapon()
+ else if(instantwin)
+ user.disarm_weapon()
+
+ remove_status_effect(/datum/status_effect/buff/clash)
+ user.remove_status_effect(/datum/status_effect/buff/clash)
+
+/// Proc that will try to throw the src's held I and throw it 2 - 4 tiles to their side.
+/mob/living/proc/disarm_weapon()
+ var/obj/item/disarmed = get_active_held_item()
+ if(!disarmed)
+ return
+
+ if(!canUnEquip(disarmed))
+ return
+
+ visible_message(
+ span_suicide("[src] is disarmed!"),
+ span_boldwarning("I'm disarmed!"),
+ )
+
+ var/turnangle = (prob(50) ? 270 : 90)
+ var/turndir = turn(dir, turnangle)
+ var/dist = rand(2, 4)
+ var/current_turf = get_turf(src)
+ var/target_turf = get_ranged_target_turf(current_turf, turndir, dist)
+
+ throw_item(target_turf, FALSE)
+ apply_status_effect(/datum/status_effect/debuff/clickcd, 3 SECONDS)
+
+///Proc that cancels Riposte with a small stamina penalty, unless it's an extreme case.
+/mob/living/proc/bad_guard(message, custom_value, cheesy = FALSE)
+ adjust_stamina(((maximum_stamina * (custom_value ? custom_value : 20)) / 100))
+ if(cheesy) //We tried to hit someone with Riposte (Not Limb Guard) up. Unfortunately this must be super punishing to prevent cheese.
+ adjust_energy(-((max_energy * (custom_value ? custom_value : 20)) / 100))
+ Immobilize(2 SECONDS)
+ if(message)
+ to_chat(src, message)
+ INVOKE_ASYNC(src, PROC_REF(emote), "strain", forced = TRUE)
+ remove_status_effect(/datum/status_effect/buff/clash)
+ // remove_status_effect(/datum/status_effect/buff/clash/limbguard)
+
+/mob/living/carbon/human/species/human/northern/clasher/Initialize()
+ . = ..()
+ put_in_hands(new /obj/item/weapon/sword/sabre)
+ apply_status_effect(/datum/status_effect/buff/clash, 1 HOURS)
diff --git a/code/modules/combat/clash/status_effects.dm b/code/modules/combat/clash/status_effects.dm
new file mode 100644
index 00000000000..3619be4ecd8
--- /dev/null
+++ b/code/modules/combat/clash/status_effects.dm
@@ -0,0 +1,127 @@
+/datum/status_effect/buff/clash
+ id = "clash"
+ duration = 6 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/buff/clash
+ /// Reference to the overlay to remove it
+ var/mutable_appearance/clash_overlay
+
+ /// Signals that cancel the clash
+ var/static/list/interrupt_signals = list(
+ COMSIG_ATOM_BULLET_ACT, // Any projectile
+ COMSIG_ATOM_HITBY, // Thrown items
+ COMSIG_MOB_SWAPPING_HANDS, // Swapping and twohanding
+ COMSIG_MOB_KICKED, // getting kicked
+ SIGNAL_ADDTRAIT(TRAIT_KNOCKEDOUT),
+ SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
+ SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
+ SIGNAL_ADDTRAIT(TRAIT_FLOORED),
+ SIGNAL_ADDTRAIT(TRAIT_PACIFISM),
+ )
+
+ /// Signals that punish the owner and cancel the clash
+ var/static/list/punishmment_signals = list(
+ COMSIG_MOB_SPELL_ACTIVATED, // Trying to cast
+ COMSIG_MOB_PRE_SPECIAL_MIDDLE, // Before: kick/bite/jump/etc
+ COMSIG_MOB_FIRED_GUN, // Shooting a gun (We can clash with them)
+ )
+
+/datum/status_effect/buff/clash/on_creation(mob/living/new_owner, duration_override, ...)
+ . = ..()
+
+ RegisterSignal(new_owner, COMSIG_ATOM_ATTACKBY, PROC_REF(attacked_item))
+ RegisterSignal(new_owner, COMSIG_ATOM_ATTACK_HAND, PROC_REF(attacked_hand))
+
+ RegisterSignal(new_owner, interrupt_signals, PROC_REF(cancel_clash))
+ RegisterSignal(new_owner, punishmment_signals, PROC_REF(cancel_punish_clash))
+
+/datum/status_effect/buff/clash/on_apply()
+ . = ..()
+ if(!ishuman(owner))
+ return
+
+ clash_overlay = mutable_appearance('icons/mob/mob_effects.dmi', "eff_riposte_trans", ABOVE_ALL_MOB_LAYER)
+ clash_overlay.pixel_y = 20
+
+ owner.add_overlay(clash_overlay)
+
+/datum/status_effect/buff/clash/on_remove()
+ . = ..()
+ if(!owner)
+ clash_overlay = null
+ return
+
+ UnregisterSignal(owner, COMSIG_MOB_ITEM_ATTACK)
+ UnregisterSignal(owner, COMSIG_ATOM_ATTACK_HAND)
+
+ UnregisterSignal(owner, interrupt_signals)
+ UnregisterSignal(owner, punishmment_signals)
+
+ owner.cut_overlay(clash_overlay)
+ clash_overlay = null
+
+ owner.apply_status_effect(/datum/status_effect/debuff/clashcd)
+
+/datum/status_effect/buff/clash/tick()
+ if(QDELETED(src))
+ return
+
+ if(!owner.get_active_held_item() || owner.is_blind())
+ owner.bad_guard()
+
+/datum/status_effect/buff/clash/proc/attacked_item(mob/living/victim, obj/item/weapon, mob/living/assailant, list/modifiers)
+ SIGNAL_HANDLER
+
+ if(QDELETED(src) || !owner)
+ return
+
+ if(!weapon)
+ return
+
+ // Attacker has Guard / Clash active, and is hitting us who doesn't. Cheesing a 'free' hit with a defensive buff is a no-no. You get punished.
+ if(assailant.has_status_effect(/datum/status_effect/buff/clash) && !victim.has_status_effect(/datum/status_effect/buff/clash))
+ assailant.bad_guard(span_suicide("I tried to strike while focused on defense whole! It drains me!"), cheesy = TRUE)
+ return
+
+ // var/weapon_range = victim.used_intent?.reach
+ // if(get_dist(victim, assailant) > weapon_range)
+ // cancel_clash() // If we are getting stabbed by a spear, we can't clash unless we can match
+ // return
+
+ if(victim.dir == REVERSE_DIR(get_dir(victim, assailant)))
+ cancel_clash() // Attacked from behind
+ return
+
+ victim.process_clash(assailant)
+
+ return COMPONENT_NO_ATTACK
+
+/datum/status_effect/buff/clash/proc/attacked_hand(mob/living/victim, mob/living/assailant)
+ SIGNAL_HANDLER
+
+ // Attacker has Guard / Clash active, and is hitting us who doesn't. Cheesing a 'free' hit with a defensive buff is a no-no. You get punished.
+ if(assailant.has_status_effect(/datum/status_effect/buff/clash) && !victim.has_status_effect(/datum/status_effect/buff/clash))
+ assailant.bad_guard(span_suicide("I tried to strike while focused on defense whole! It drains me!"), cheesy = TRUE)
+ return
+
+ if(victim.dir == REVERSE_DIR(get_dir(victim, assailant)))
+ cancel_clash() // Attacked from behind
+ return
+
+ victim.process_clash(assailant)
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/datum/status_effect/buff/clash/proc/cancel_clash()
+ SIGNAL_HANDLER
+
+ owner.bad_guard(span_userdanger("My focus was interrupted!"))
+
+/datum/status_effect/buff/clash/proc/cancel_punish_clash()
+ SIGNAL_HANDLER
+
+ owner.bad_guard(span_userdanger("My focus was heavily interrupted!"), cheesy = TRUE)
+
+/atom/movable/screen/alert/status_effect/buff/clash
+ name = "Ready to Clash"
+ desc = span_notice("I am on guard, and ready to clash. If I am hit, I will successfully defend. Attacking will make me lose my focus.")
+ icon_state = "clash"
diff --git a/code/modules/combat/special_intents.dm b/code/modules/combat/special_intents.dm
index 36d92d9f182..c42084a0ae5 100644
--- a/code/modules/combat/special_intents.dm
+++ b/code/modules/combat/special_intents.dm
@@ -47,7 +47,7 @@
continue
if(victim.body_position == LYING_DOWN)
continue
- victim.apply_status_effect(/datum/status_effect/debuff/exposed, 5 SECONDS)
+ victim.apply_status_effect(/datum/status_effect/debuff/exposed, 4 SECONDS)
apply_generic_weapon_damage(user, parent, victim, damage, SLASH, target_zone, damage_class = BCLASS_CUT)
/datum/special_intent/shin_swipe
diff --git a/code/modules/crafting/alchemy/essence_machines/essence_splitter.dm b/code/modules/crafting/alchemy/essence_machines/essence_splitter.dm
index 65f146db77d..5c050bf631a 100644
--- a/code/modules/crafting/alchemy/essence_machines/essence_splitter.dm
+++ b/code/modules/crafting/alchemy/essence_machines/essence_splitter.dm
@@ -196,7 +196,7 @@
addtimer(CALLBACK(src, PROC_REF(finish_bulk_splitting), all_precursors, user), process_time)
/obj/machinery/essence/splitter/proc/finish_bulk_splitting(list/precursors, mob/living/user)
- flick_overlay_view(image(icon, src, "split", ABOVE_MOB_LAYER), 1.2 SECONDS)
+ flick_overlay_view(mutable_appearance(icon, "split", ABOVE_MOB_LAYER), 1.2 SECONDS)
var/efficiency_bonus = GLOB.thaumic_research.get_research_bonus(/datum/thaumic_research_node/splitter_efficiency)
var/list/total_produced = list()
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 0a1616da314..46505d7c913 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -252,10 +252,10 @@
I.add_mob_blood(src)
return TRUE //successful attack
-//ATTACK HAND IGNORING PARENT RETURN VALUE
/mob/living/carbon/attack_hand(mob/living/carbon/human/user)
- if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_HAND, user) & COMPONENT_CANCEL_ATTACK_CHAIN)
- . = TRUE
+ . = ..()
+ if(.)
+ return TRUE
if(!lying_attack_check(user))
return FALSE
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index db26e4d5eef..1a61a33bb3d 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -408,7 +408,7 @@
addtimer(CALLBACK(src, PROC_REF(end_electrocution_animation), electrocution_skeleton_anim), anim_duration)
else //or just do a generic animation
- flick_overlay_view(image(icon,src,"electrocuted_generic",ABOVE_MOB_LAYER), src, anim_duration)
+ flick_overlay_view(mutable_appearance(icon, "electrocuted_generic", ABOVE_MOB_LAYER), anim_duration)
/mob/living/carbon/human/proc/end_electrocution_animation(mutable_appearance/MA)
remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, "#000000")
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index 2e35614cea2..84186abd7a5 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -263,8 +263,10 @@
return dna.species.spec_attacked_by(I, user, affecting, used_intent, src, useder, accurate)
/mob/living/carbon/human/attack_hand(mob/user)
- if(..()) //to allow surgery to return properly.
- return
+ . = ..()
+ if(.)
+ return TRUE
+
if(ishuman(user))
var/mob/living/carbon/human/H = user
dna.species.spec_attack_hand(H, src)
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index 620ae5571c6..4c0bc3dec75 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -1759,6 +1759,8 @@ GLOBAL_LIST_EMPTY(roundstart_species)
affecting.bodypart_attacked_by(BCLASS_BLUNT, damage, user, selzone)
SEND_SIGNAL(user, COMSIG_MOB_KICK, target, selzone, damage_blocked)
+ SEND_SIGNAL(target, COMSIG_MOB_KICKED, user, selzone, damage_blocked)
+
playsound(target, 'sound/combat/hits/kick/kick.ogg', 100, TRUE, -1)
target.lastattacker = user.real_name
target.lastattackerckey = user.ckey
diff --git a/code/modules/mob/living/carbon/rogfatstam.dm b/code/modules/mob/living/carbon/rogfatstam.dm
index 750b2bd528e..6f445ad58ee 100644
--- a/code/modules/mob/living/carbon/rogfatstam.dm
+++ b/code/modules/mob/living/carbon/rogfatstam.dm
@@ -92,9 +92,9 @@
if(m_intent == MOVE_INTENT_RUN) //can't sprint at full fatigue
toggle_rogmove_intent(MOVE_INTENT_WALK, TRUE)
if(!emote_override)
- emote("fatigue", forced = force_emote)
+ INVOKE_ASYNC(src, PROC_REF(emote), "fatigue", forced = force_emote)
else
- emote(emote_override, forced = force_emote)
+ INVOKE_ASYNC(src, PROC_REF(emote), emote_override, forced = force_emote)
set_eye_blur_if_lower(4 SECONDS)
last_fatigued = world.time + 30 //extra time before fatigue regen sets in
stop_attack()
@@ -137,7 +137,7 @@
if(!heart_attacking)
var/mob/living/carbon/C = src
C.visible_message(C, "[C] clutches at [C.p_their()] chest!") // Other people know something is wrong.
- emote("breathgasp", forced = TRUE)
+ INVOKE_ASYNC(src, PROC_REF(emote), "breathgasp", forced = TRUE)
shake_camera(src, 1, 3)
set_eye_blur_if_lower(80 SECONDS)
var/stuffy = list("ZIZO GRABS MY WEARY HEART!","ARGH! MY HEART BEATS NO MORE!","NO... MY HEART HAS BEAT IT'S LAST!","MY HEART HAS GIVEN UP!","MY HEART BETRAYS ME!","THE METRONOME OF MY LIFE STILLS!")
diff --git a/code/modules/mob/living/dodge.dm b/code/modules/mob/living/dodge.dm
index 3be0cbf35d4..9773977be88 100644
--- a/code/modules/mob/living/dodge.dm
+++ b/code/modules/mob/living/dodge.dm
@@ -6,6 +6,9 @@
* @return TRUE if dodge successful, FALSE otherwise
*/
/mob/living/proc/attempt_dodge(datum/intent/intenty, mob/living/user, can_dodge_see = TRUE)
+ if(!candodge)
+ return FALSE
+
if(intenty && !intenty.candodge)
return FALSE
@@ -18,6 +21,9 @@
if(has_status_effect(/datum/status_effect/debuff/exposed))
return FALSE
+ if(has_status_effect(/datum/status_effect/debuff/vulnerable))
+ return FALSE
+
if(world.time < last_dodge + dodgetime && !istype(rmb_intent, /datum/rmb_intent/riposte))
return FALSE
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index 3662546aeb2..6fc187e8982 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -92,22 +92,19 @@
. = FALSE
/datum/emote/living/custom/run_emote(mob/user, params, type_override = null, intentional = FALSE)
- if(!can_run_emote(user, TRUE, intentional))
+ if(QDELETED(user))
return FALSE
- else if(QDELETED(user))
+
+ if(!can_run_emote(user, TRUE, intentional))
return FALSE
- else if(user.client && user.client.prefs.muted & MUTE_IC)
+
+ if(user.client?.prefs.muted & MUTE_IC)
to_chat(user, "I cannot send IC messages (muted).")
return FALSE
- else if(!params)
- var/custom_emote = copytext(sanitize(input("What does your character do?") as text|null), 1, MAX_MESSAGE_LEN)
- if(custom_emote && !check_invalid(user, custom_emote))
- message = custom_emote
- emote_type = EMOTE_VISIBLE
- else
- message = params
- if(type_override)
- emote_type = type_override
+
+ message = params
+ if(type_override)
+ emote_type = type_override
. = ..()
message = null
emote_type = EMOTE_VISIBLE
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 8e5addb84ad..b72a844275e 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1261,23 +1261,20 @@
if(stat)
return
surrendering = 1
- if(alert(src, "Yield in surrender?",,"YES","NO") == "YES")
- record_round_statistic(STATS_YIELDS)
- changeNext_move(CLICK_CD_EXHAUSTED)
- var/image/flaggy = image('icons/effects/effects.dmi',src,"surrender",ABOVE_MOB_LAYER)
- flaggy.appearance_flags = RESET_TRANSFORM|KEEP_APART
- flaggy.transform = null
- flaggy.pixel_y = 12
- flick_overlay_view(flaggy, 150)
- drop_all_held_items()
- Stun(150)
- src.visible_message("[src] yields!")
- playsound(src, 'sound/misc/surrender.ogg', 100, FALSE, -1)
- toggle_cmode()
- sleep(150)
- log_attack("[key_name(src)] has yielded!")
- surrendering = 0
+ if(!alert(src, "Yield in surrender?",,"YES","NO") == "YES")
+ return
+ record_round_statistic(STATS_YIELDS)
+ changeNext_move(CLICK_CD_EXHAUSTED)
+ var/mutable_appearance/flaggy = mutable_appearance('icons/effects/effects.dmi', "surrender", ABOVE_MOB_LAYER, appearance_flags = RESET_TRANSFORM|KEEP_APART)
+ flaggy.pixel_y = 12
+ flick_overlay_view(flaggy, 150)
+ drop_all_held_items()
+ Stun(15 SECONDS)
+ visible_message("[src] yields!")
+ playsound(src, 'sound/misc/surrender.ogg', 100, FALSE, -1)
+ toggle_cmode()
+ addtimer(VARSET_CALLBACK(src, surrendering, FALSE), 15 SECONDS)
/mob/proc/stop_attack(message = FALSE)
if(atkswinging)
diff --git a/code/modules/mob/living/overhead_effects.dm b/code/modules/mob/living/overhead_effects.dm
index 2871da94f11..c065b63c05b 100644
--- a/code/modules/mob/living/overhead_effects.dm
+++ b/code/modules/mob/living/overhead_effects.dm
@@ -1,75 +1,109 @@
//By DREAMKEEP, Vide Noir https://github.com/EaglePhntm.
//GRAPHICS & SOUNDS INCLUDED:
//DARKEST DUNGEON (STRESS, RELIEF, AFFLICTION)
-/mob/living/carbon/proc/play_overhead_indicator(icon_path, overlay_name, clear_time, overlay_layer, public = FALSE, soundin = null)
- if(!ishuman(src))
+/**
+ * Show an appearance over the head of this mob.
+ *
+ * Arguments
+ * * icon - icon file to use.
+ * * icon_state - icon_state to use.
+ * * duration - how long to show the icon for.
+ * * layer - layer to use.
+ * * public - If True all clients can see the icon.
+ * * sound - If set, play this when we show the icon.
+ * * can_see_cb - Callback to test if someone can see and hear even if not public.
+ */
+/mob/living/proc/show_overhead_indicator(icon, icon_state, duration, layer = ABOVE_ALL_MOB_LAYER, plane = FLOAT_PLANE, y_offset = 12, public = FALSE, sound = null, datum/callback/can_see_cb = null)
+ if(!icon_exists_or_scream(icon, icon_state))
return
- if(!client)
- return
- if(!COOLDOWN_FINISHED(src, stress_indicator))
+
+ var/mutable_appearance/appearance = mutable_appearance(icon, icon_state, layer, plane, appearance_flags = RESET_COLOR)
+ appearance.pixel_y = y_offset
+
+ if(public && sound)
+ playsound(src, sound, 15, FALSE, extrarange = -1, ignore_walls = FALSE)
+ else if(sound)
+ playsound_local(src, sound, 15, FALSE)
+
+ if(client)
+ flick_overlay(appearance, client, duration)
+
+ for(var/mob/viewer as anything in viewers(src))
+ var/client/client = viewer.client
+ if(!client)
+ continue
+ if(public)
+ flick_overlay(appearance, client, duration)
+ else if(can_see_cb)
+ var/can_see = can_see_cb.Invoke(src, viewer)
+ if(can_see)
+ playsound_local(viewer, sound, 15, FALSE)
+ flick_overlay(appearance, client, duration)
+
+/mob/living/carbon/human/show_overhead_indicator(icon, icon_state, duration = 3 SECONDS, layer = ABOVE_ALL_MOB_LAYER, y_offset = 12, public = FALSE, sound = null, datum/callback/can_see_cb = null)
+ if(!icon_exists_or_scream(icon, icon_state))
return
+
+ var/list/offsets
+
var/datum/species/species = dna?.species
- if(!species)
- return
- var/mob/living/carbon/human/H = src
- if(stat < UNCONSCIOUS)
- COOLDOWN_START(src, stress_indicator, 8 SECONDS)
+ if(species)
+ var/use_female_sprites = MALE_SPRITES
+ if(species.sexes)
+ if(gender == FEMALE && !species.swap_female_clothes || gender == MALE && species.swap_male_clothes)
+ use_female_sprites = FEMALE_SPRITES
+
+ if(use_female_sprites)
+ offsets = (age == AGE_CHILD) ? species.offset_features_child : species.offset_features_f
+ else
+ offsets = (age == AGE_CHILD) ? species.offset_features_child : species.offset_features_m
- var/list/offsets
+ var/mutable_appearance/appearance = mutable_appearance(icon, icon_state, layer, appearance_flags = RESET_COLOR)
+ appearance.pixel_y = y_offset
+ if(length(offsets) && LAZYACCESS(offsets, OFFSET_HEAD))
+ appearance.pixel_y += offsets[OFFSET_HEAD][1]
+ appearance.pixel_y+= offsets[OFFSET_HEAD][2]
+
+ if(public && sound)
+ playsound(src, sound, 15, FALSE, extrarange = -1, ignore_walls = FALSE)
+ else if(sound)
+ playsound_local(src, sound, 15, FALSE)
+
+ if(client)
+ flick_overlay(appearance, client, duration)
+
+ for(var/mob/viewer as anything in viewers(src))
+ var/client/client = viewer.client
+ if(!client)
+ continue
if(public)
- var/use_female_sprites = MALE_SPRITES
- if(species)
- if(species.sexes)
- if(H.gender == FEMALE && !species.swap_female_clothes || H.gender == MALE && species.swap_male_clothes)
- use_female_sprites = FEMALE_SPRITES
-
- if(use_female_sprites)
- offsets = (H.age == AGE_CHILD) ? species.offset_features_child : species.offset_features_f
- else
- offsets = (H.age == AGE_CHILD) ? species.offset_features_child : species.offset_features_m
-
- var/mutable_appearance/appearance = mutable_appearance(icon_path, overlay_name, overlay_layer)
- if(LAZYACCESS(offsets, OFFSET_HEAD))
- appearance.pixel_x += offsets[OFFSET_HEAD][1]
- appearance.pixel_y += offsets[OFFSET_HEAD][2] + 12
- appearance.appearance_flags = RESET_COLOR
- overlays_standing[OBJ_LAYER] = appearance
- apply_overlay(OBJ_LAYER)
- addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, cut_overlay), appearance), clear_time)
- playsound(src, soundin, 15, FALSE, extrarange = -1, ignore_walls = FALSE)
- else
- var/list/can_see = list()
- for(var/mob/M in viewers(src))
- if(HAS_TRAIT(M, TRAIT_EMPATH))
- can_see += M
- if(soundin)
- M.playsound_local(get_turf(src), soundin, 15, FALSE)
-
- vis_contents += new /obj/effect/temp_visual/stress_event(null, can_see, icon_path, overlay_name, offsets)
-
-/obj/effect/temp_visual/stress_event
- icon = 'icons/mob/overhead_effects.dmi'
- icon_state = null
- duration = 25
- layer = BELOW_MOB_LAYER
- vis_flags = VIS_INHERIT_PLANE|VIS_INHERIT_ID
-
-/obj/effect/temp_visual/stress_event/Initialize(mapload, list/seers, path, iname, list/offsets)
- . = ..()
- var/image/I = image(icon = path, icon_state = iname, layer = ABOVE_MOB_LAYER, loc = src)
- I.alpha = 255
- I.appearance_flags = RESET_ALPHA
- if(LAZYACCESS(offsets, OFFSET_HEAD))
- I.pixel_x += offsets[OFFSET_HEAD][1]
- I.pixel_y += offsets[OFFSET_HEAD][2] + 12
- add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/People, iname, I, seers)
+ flick_overlay(appearance, client, duration)
+ else if(can_see_cb)
+ var/can_see = can_see_cb.Invoke(src, viewer)
+ if(can_see)
+ viewer.playsound_local(src, sound, 15, FALSE)
+ flick_overlay(appearance, client, duration)
+
+// Everything about this makes me sad
/mob/living/carbon/proc/play_stress_indicator()
- play_overhead_indicator('icons/mob/overhead_effects.dmi', "stress", 20, OBJ_LAYER, soundin = 'sound/ddstress.ogg')
+ if(!COOLDOWN_FINISHED(src, stress_indicator))
+ return
+ show_overhead_indicator('icons/mob/overhead_effects.dmi', "stress", 2 SECONDS, sound = 'sound/ddstress.ogg', can_see_cb = CALLBACK(src, PROC_REF(is_empath)))
+ COOLDOWN_START(src, stress_indicator, 8 SECONDS)
/mob/living/carbon/proc/play_relief_indicator()
- play_overhead_indicator('icons/mob/overhead_effects.dmi', "relief", 20, OBJ_LAYER, soundin = 'sound/ddrelief.ogg')
+ if(!COOLDOWN_FINISHED(src, stress_indicator))
+ return
+ show_overhead_indicator('icons/mob/overhead_effects.dmi', "relief", 2 SECONDS, sound = 'sound/ddrelief.ogg', can_see_cb = CALLBACK(src, PROC_REF(is_empath)))
+ COOLDOWN_START(src, stress_indicator, 8 SECONDS)
/mob/living/carbon/proc/play_mental_break_indicator()
- play_overhead_indicator('icons/mob/overhead_effects.dmi', "mentalbreak", 30, OBJ_LAYER, soundin = 'sound/stressaffliction.ogg')
+ if(!COOLDOWN_FINISHED(src, stress_indicator))
+ return
+ show_overhead_indicator('icons/mob/overhead_effects.dmi', "mentalbreak", 3 SECONDS, sound = 'sound/stressaffliction.ogg', can_see_cb = CALLBACK(src, PROC_REF(is_empath)))
+ COOLDOWN_START(src, stress_indicator, 8 SECONDS)
+
+/mob/living/carbon/proc/is_empath()
+ return HAS_TRAIT(src, TRAIT_EMPATH)
diff --git a/code/modules/mob/living/parry.dm b/code/modules/mob/living/parry.dm
index 67cbfecb6fc..52cb8c0fd1d 100644
--- a/code/modules/mob/living/parry.dm
+++ b/code/modules/mob/living/parry.dm
@@ -18,6 +18,9 @@
if(has_status_effect(/datum/status_effect/debuff/exposed))
return FALSE
+ if(has_status_effect(/datum/status_effect/debuff/vulnerable))
+ return FALSE
+
if(world.time < last_parry + setparrytime && !istype(rmb_intent, /datum/rmb_intent/riposte))
return FALSE
diff --git a/icons/mob/mob_effects.dmi b/icons/mob/mob_effects.dmi
new file mode 100644
index 00000000000..c84b7d74a36
Binary files /dev/null and b/icons/mob/mob_effects.dmi differ
diff --git a/icons/mob/overhead_effects.dmi b/icons/mob/overhead_effects.dmi
index 02a72a16ba4..31d4cdd0e83 100644
Binary files a/icons/mob/overhead_effects.dmi and b/icons/mob/overhead_effects.dmi differ
diff --git a/sound/combat/clash_disarm_opp.ogg b/sound/combat/clash_disarm_opp.ogg
new file mode 100644
index 00000000000..3398fbc8a09
Binary files /dev/null and b/sound/combat/clash_disarm_opp.ogg differ
diff --git a/sound/combat/clash_disarm_us.ogg b/sound/combat/clash_disarm_us.ogg
new file mode 100644
index 00000000000..b1a1075da63
Binary files /dev/null and b/sound/combat/clash_disarm_us.ogg differ
diff --git a/sound/combat/clash_draw.ogg b/sound/combat/clash_draw.ogg
new file mode 100644
index 00000000000..bd4e1b1ba4d
Binary files /dev/null and b/sound/combat/clash_draw.ogg differ
diff --git a/sound/combat/clash_struck.ogg b/sound/combat/clash_struck.ogg
new file mode 100644
index 00000000000..9749d200bf6
Binary files /dev/null and b/sound/combat/clash_struck.ogg differ
diff --git a/vanderlin.dme b/vanderlin.dme
index b435ec729e7..ca20a3d12d7 100644
--- a/vanderlin.dme
+++ b/vanderlin.dme
@@ -2399,6 +2399,8 @@
#include "code\modules\clothing\wrists\misc.dm"
#include "code\modules\combat\_special_intent.dm"
#include "code\modules\combat\special_intents.dm"
+#include "code\modules\combat\clash\clash.dm"
+#include "code\modules\combat\clash\status_effects.dm"
#include "code\modules\cooking\NeuFood.dm"
#include "code\modules\cooking\Teas_and_Brews.dm"
#include "code\modules\cooking\cooked\NeuFood_pies.dm"