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"