From 9af6108d4d76234528f1eac47dbd20c33c32ea50 Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Thu, 19 Feb 2026 17:24:23 -0600 Subject: [PATCH 1/7] Combat lock --- code/_onclick/item_attack.dm | 5 ++ .../mob/living/carbon/human/_species.dm | 3 + code/modules/mob/living/living_defense.dm | 62 +++++++++++++++++++ code/modules/mob/living/living_defines.dm | 3 + code/modules/mob/living/living_movement.dm | 3 + 5 files changed, 76 insertions(+) diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 98d02dfbe4ca..cd9bde9a41db 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -320,6 +320,11 @@ if((attacking_item.item_flags & SURGICAL_TOOL) && !user.combat_mode && HAS_TRAIT(user, TRAIT_READY_TO_OPERATE)) wounding = CANT_WOUND + if(isliving(attacking_item.loc)) + var/mob/living/attacker = attacking_item.loc + attacker.combat_lock_on(src, TRUE) + combat_lock_on(attacker) + if(user != src) // This doesn't factor in armor, or most damage modifiers (physiology). Your mileage may vary if(check_block(attacking_item, damage, "the [attacking_item.name]", MELEE_ATTACK, attacking_item.armour_penetration, attacking_item.damtype)) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 33308a55f1b4..1af09b5bb5ab 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -913,6 +913,9 @@ GLOBAL_LIST_EMPTY(features_by_species) SEND_SIGNAL(target, COMSIG_HUMAN_GOT_PUNCHED, user, damage, attack_type, affecting, final_armor_block, kicking, limb_sharpness) SEND_SIGNAL(user, COMSIG_HUMAN_PUNCHED, target, damage, attack_type, affecting, final_armor_block, kicking, limb_sharpness) + user.combat_lock_on(target, TRUE) + target.combat_lock_on(user) + //If we rolled a punch high enough to hit our stun threshold, or our target is staggered and they have at least 40 damage+stamina loss, we knock them down //This does not work against opponents who are knockdown immune, such as from wearing riot armor. if(!HAS_TRAIT(src, TRAIT_BRAWLING_KNOCKDOWN_BLOCKED)) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 1c445c3f4ff4..5872decd4a70 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -181,6 +181,12 @@ ) if(hitting_projectile.dismemberment > 0 && !hitting_projectile.grazing) check_projectile_dismemberment(hitting_projectile, def_zone) + + if(isliving(hitting_projectile.firer)) + var/mob/living/firer = hitting_projectile.firer + firer.combat_lock_on(src, TRUE) + combat_lock_on(firer) + return BULLET_ACT_HIT /mob/living/check_projectile_armor(def_zone, obj/projectile/impacting_projectile, is_silent) @@ -697,6 +703,9 @@ shove_flags |= SHOVE_DIRECTIONAL_BLOCKED break + target.combat_lock_on(src, TRUE) + combat_lock_on(target) + if(shove_flags & SHOVE_CAN_HIT_SOMETHING) //Don't hit people through windows, ok? if(!(shove_flags & SHOVE_DIRECTIONAL_BLOCKED) && (SEND_SIGNAL(target_shove_turf, COMSIG_LIVING_DISARM_COLLIDE, src, target, shove_flags, weapon) & COMSIG_LIVING_SHOVE_HANDLED)) @@ -764,3 +773,56 @@ return TRUE return FALSE + +/mob/living/proc/combat_lock_on(mob/living/target, is_attacker = FALSE) + if(target == src) + return + var/target_dist = get_dist(src, target) + if(target_dist > 7) + return + if(!isnull(combat_target)) + if(!is_attacker && get_dist(src, combat_target) <= target_dist) + return + drop_combat_lock() + if(combat_mode) + face_atom(target) + + combat_target = target + RegisterSignal(target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) + RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_lock_moved)) + addtimer(CALLBACK(src, PROC_REF(drop_combat_lock_timer), REF(target)), 20 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME) + +/mob/living/proc/face_combat_target() + if(isnull(combat_target) || !combat_mode || pulledby || HAS_TRAIT(src, TRAIT_INCAPACITATED)) + return FALSE + if(isnull(ai_controller) && (isnull(mind) || !mind.active)) + return + if(!isturf(loc) || !isturf(combat_target.loc) || loc == combat_target.loc) + return FALSE + if(combat_target.alpha <= 50 || combat_target.invisibility > see_invisible) + return FALSE + var/combat_dist = get_dist(src, combat_target) + if(combat_dist > 7) + drop_combat_lock() + return FALSE + if(!(src in viewers(5, combat_target))) + return FALSE + face_atom(combat_target) + return TRUE + +/mob/living/proc/drop_combat_lock_timer(target_ref) + if(isnull(combat_target) || REF(combat_target) != target_ref) + return + drop_combat_lock() + +/mob/living/proc/drop_combat_lock() + SIGNAL_HANDLER + + UnregisterSignal(combat_target, COMSIG_QDELETING) + UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) + combat_target = null + set_dir_on_move = initial(set_dir_on_move) + +/mob/living/proc/combat_lock_moved(...) + SIGNAL_HANDLER + face_combat_target() diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index 776b22320414..86de81055d8c 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -278,3 +278,6 @@ /// First element is the current martial art - any other elements are "saved" for if they unlearn the first one /// Reference handling is done by the martial arts themselves var/list/datum/martial_art/martial_arts + + /// Current target we are engaged in combat with + VAR_PRIVATE/mob/living/combat_target diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm index c18dd622c22f..05827c0a1215 100644 --- a/code/modules/mob/living/living_movement.dm +++ b/code/modules/mob/living/living_movement.dm @@ -1,5 +1,8 @@ /mob/living/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) . = ..() + // only set dir to movement if we failed to face the target + set_dir_on_move = !face_combat_target() + update_turf_movespeed(loc) if(HAS_TRAIT(src, TRAIT_NEGATES_GRAVITY)) if(!isgroundlessturf(loc)) From d26ed26c764af93bb72ff57998576b050cd5013a Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Wed, 12 Aug 2026 21:18:20 -0500 Subject: [PATCH 2/7] Updates --- code/_onclick/item_attack.dm | 6 +- code/game/atoms_movable.dm | 3 + .../mob/living/carbon/human/_species.dm | 3 +- code/modules/mob/living/living_defense.dm | 157 +++++++++++++----- code/modules/mob/living/living_defines.dm | 3 - code/modules/mob/living/living_movement.dm | 3 - code/modules/vehicles/mecha/mecha_defense.dm | 1 + 7 files changed, 117 insertions(+), 59 deletions(-) diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 72db243f96fb..a9046b7b2bfd 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -341,12 +341,8 @@ if((attacking_item.item_flags & SURGICAL_TOOL) && !user.combat_mode && HAS_TRAIT(user, TRAIT_READY_TO_OPERATE)) wounding = CANT_WOUND - if(isliving(attacking_item.loc)) - var/mob/living/attacker = attacking_item.loc - attacker.combat_lock_on(src, TRUE) - combat_lock_on(attacker) - if(user != src) + user.combat_lock_on(src) // This doesn't factor in armor, or most damage modifiers (physiology). Your mileage may vary if(check_block(attacking_item, final_force, "\the [attacking_item]", MELEE_ATTACK, attacking_item.armour_penetration, attacking_item.damtype)) return 0 diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 92c203bf914f..52b865fb16fb 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1380,6 +1380,9 @@ SSthrowing.currentrun[src] = thrown_thing if (quickstart) thrown_thing.tick() + // Makes the thrower track the thrown thing as it flies + if (astype(thrower, /mob/living)?.combat_mode) + thrower.combat_lock_on(src, 1 SECONDS) /atom/movable/proc/handle_buckled_mob_movement(newloc, direct, glide_size_override) for(var/mob/living/buckled_mob as anything in buckled_mobs) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 84f74cfb19d0..b2da50fe5b2f 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -865,8 +865,7 @@ GLOBAL_LIST_EMPTY(features_by_species) SEND_SIGNAL(target, COMSIG_HUMAN_GOT_PUNCHED, user, damage, attack_type, affecting, final_armor_block, kicking, limb_sharpness) SEND_SIGNAL(user, COMSIG_HUMAN_PUNCHED, target, damage, attack_type, affecting, final_armor_block, kicking, limb_sharpness) - user.combat_lock_on(target, TRUE) - target.combat_lock_on(user) + user.combat_lock_on(target) //If we rolled a punch high enough to hit our stun threshold, or our target is staggered and they have at least 40 damage+stamina loss, we knock them down //This does not work against opponents who are knockdown immune, such as from wearing riot armor. diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index 73cba32dfafc..f7d2f514431e 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -186,11 +186,7 @@ if(hitting_projectile.dismemberment > 0 && !hitting_projectile.grazing) check_projectile_dismemberment(hitting_projectile, def_zone) - if(isliving(hitting_projectile.firer)) - var/mob/living/firer = hitting_projectile.firer - firer.combat_lock_on(src, TRUE) - combat_lock_on(firer) - + astype(hitting_projectile.firer, /mob/living)?.combat_lock_on(src) return BULLET_ACT_HIT /mob/living/check_projectile_armor(def_zone, obj/projectile/impacting_projectile, is_silent) @@ -245,6 +241,10 @@ SEND_SOUND(src, sound('sound/misc/ui_toggleoffcombat.ogg', volume = 25)) //Slightly modified version of the above /mob/living/hitby(atom/movable/AM, skipcatch, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum) + var/mob/thrown_by = thrown_item.thrownby?.resolve() + // Swaps to following the guy you hit with the thrown item + if(astype(thrown_by, /mob/living)?.combat_mode) + thrown_by.combat_lock_on(src, 10 SECONDS, override_existing = TRUE) if(!isitem(AM)) // Filled with made up numbers for non-items. if(check_block(AM, 30, "\the [AM.name]", THROWN_PROJECTILE_ATTACK, 0, BRUTE)) @@ -277,7 +277,6 @@ if(blocked) return TRUE - var/mob/thrown_by = thrown_item.thrownby?.resolve() if(thrown_by) log_combat(thrown_by, src, "threw and hit", thrown_item) else @@ -707,8 +706,7 @@ shove_flags |= SHOVE_DIRECTIONAL_BLOCKED break - target.combat_lock_on(src, TRUE) - combat_lock_on(target) + target.combat_lock_on(src) if(shove_flags & SHOVE_CAN_HIT_SOMETHING) //Don't hit people through windows, ok? @@ -778,55 +776,122 @@ return FALSE -/mob/living/proc/combat_lock_on(mob/living/target, is_attacker = FALSE) - if(target == src) - return - var/target_dist = get_dist(src, target) - if(target_dist > 7) +/** + * Locks onto a target, turning to face the mob so long as we're in combat mode + * + * Arguments + * * target - The target to lock onto + * * duration - How long to lock onto the target for + * * override_existing - If set to FALSE, existing targets will not be overridden unless they are further than the new target. + */ +/mob/living/proc/combat_lock_on(atom/movable/target, duration, override_existing = FALSE) + if(target == src || get_dist(src, target) > 7) return - if(!isnull(combat_target)) - if(!is_attacker && get_dist(src, combat_target) <= target_dist) - return - drop_combat_lock() - if(combat_mode) - face_atom(target) - combat_target = target - RegisterSignal(target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) - RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_lock_moved)) - addtimer(CALLBACK(src, PROC_REF(drop_combat_lock_timer), REF(target)), 20 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_DELETE_ME) + apply_status_effect(/datum/status_effect/combat_lock, target, duration, override_existing) + if(!isliving(target)) + return -/mob/living/proc/face_combat_target() - if(isnull(combat_target) || !combat_mode || pulledby || HAS_TRAIT(src, TRAIT_INCAPACITATED)) - return FALSE - if(isnull(ai_controller) && (isnull(mind) || !mind.active)) + // Immediately mirror combat lock if fighting an AI, makes it look like they're reacting to you like a player would. + var/mob/living/target_living = target + if(!isnull(target_living.ai_controller) && isnull(target_living.client)) + target_living.combat_lock_on(src, duration, override_existing) + +/datum/status_effect/combat_lock + id = "combat_lock" + tick_interval = -1 + duration = 20 SECONDS + status_type = STATUS_EFFECT_REFRESH + alert_type = null + on_remove_on_mob_delete = TRUE + /// Movable we struck and are locked onto + VAR_PRIVATE/atom/movable/combat_target + +/datum/status_effect/combat_lock/on_creation(mob/living/new_owner, atom/movable/combat_target, duration = 20 SECONDS, override_existing) + if(isnull(combat_target)) + stack_trace("Attempted to create a combat lock without a target!") + qdel(src) return - if(!isturf(loc) || !isturf(combat_target.loc) || loc == combat_target.loc) - return FALSE - if(combat_target.alpha <= 50 || combat_target.invisibility > see_invisible) - return FALSE - var/combat_dist = get_dist(src, combat_target) - if(combat_dist > 7) - drop_combat_lock() - return FALSE - if(!(src in viewers(5, combat_target))) + + src.duration = duration + src.combat_target = combat_target + RegisterSignal(combat_target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) + RegisterSignal(combat_target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_target_moved)) + return ..() + +/datum/status_effect/combat_lock/on_apply() + if(isnull(owner.ai_controller) && isnull(owner.client)) return FALSE - face_atom(combat_target) - return TRUE -/mob/living/proc/drop_combat_lock_timer(target_ref) - if(isnull(combat_target) || REF(combat_target) != target_ref) - return - drop_combat_lock() + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(owner_moved)) + face_combat_target() + return TRUE -/mob/living/proc/drop_combat_lock() - SIGNAL_HANDLER +/datum/status_effect/combat_lock/on_remove() + . = ..() + owner.set_dir_on_move = initial(owner.set_dir_on_move) +/datum/status_effect/combat_lock/Destroy() UnregisterSignal(combat_target, COMSIG_QDELETING) UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) combat_target = null - set_dir_on_move = initial(set_dir_on_move) + return ..() -/mob/living/proc/combat_lock_moved(...) +// Refresh refreshes duration - but then if a different, closer target is passed in, swap to that one instead. +/datum/status_effect/combat_lock/refresh(effect, atom/movable/other_lock, duration = 20 SECONDS, override_existing) + src.duration += duration + if(combat_target == other_lock || (!override_existing && get_dist(owner, combat_target) < get_dist(owner, other_lock))) + return + UnregisterSignal(combat_target, COMSIG_QDELETING) + UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) + combat_target = other_lock + RegisterSignal(combat_target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) + RegisterSignal(combat_target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_target_moved)) + face_combat_target() + +/datum/status_effect/combat_lock/proc/drop_combat_lock(datum/source) SIGNAL_HANDLER + // Target is gone, no more lock. + qdel(src) + +/datum/status_effect/combat_lock/proc/combat_target_moved(datum/source, ...) + SIGNAL_HANDLER + // They left the distance, drop the lock and let them reacquire it later. + if(get_dist(owner, combat_target) > 7) + qdel(src) + return + // Otherwise keep facing them wherever they are now. face_combat_target() + +/datum/status_effect/combat_lock/proc/owner_moved(datum/source, ...) + SIGNAL_HANDLER + // Prevents the mob from turning to face their direction if they successfully faced the target. + owner.set_dir_on_move = !face_combat_target() + +/datum/status_effect/combat_lock/proc/face_combat_target() + // - If we're a not AI controlled, we have to be in combat mode to face the target + // - Otherwise if we ARE an AI mob, then skip the combat mode check, because they can enter and exit it sporadically + if(isnull(owner.ai_controller)) + if(!owner.combat_mode) + return FALSE + else + if(isnull(owner.client)) + return FALSE + // - No turn if we're being pulled, let the puller handle it. + // - No turn if incap, because of course we can't... we're probably dead. + if(!isnull(owner.pulledby) || HAS_TRAIT(owner, TRAIT_INCAPACITATED)) + return FALSE + // - Turf checks are for nullspace shenanigans + // - Otherwise we can't face them if we're on the same tile + if(!isturf(owner.loc) || !isturf(combat_target.loc) || owner.loc == combat_target.loc) + return FALSE + // - Low alpha is intended to be harder to see, so don't let people aimbot them. 64 is picked as ~25% alpha. + // - Invisibility is obviously intended to make people incapable of being seen so don't let people aimbot them either + if(combat_target.alpha <= 64 || combat_target.invisibility > owner.see_invisible) + return FALSE + // - Blanket "can we even see see them" check, most expensive one (relatively) so it comes last. + if(!(owner in viewers(5, combat_target))) + return FALSE + + owner.face_atom(combat_target) + return TRUE diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index c5c933b03788..8783a5dc2959 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -288,9 +288,6 @@ /// Reference handling is done by the martial arts themselves var/list/datum/martial_art/martial_arts - /// Current target we are engaged in combat with - VAR_PRIVATE/mob/living/combat_target - /// List of smell datums we smelled recently, we get accustomed to it over time VAR_FINAL/list/recently_smelled /// Cooldown between smell attempts diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm index 05827c0a1215..c18dd622c22f 100644 --- a/code/modules/mob/living/living_movement.dm +++ b/code/modules/mob/living/living_movement.dm @@ -1,8 +1,5 @@ /mob/living/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) . = ..() - // only set dir to movement if we failed to face the target - set_dir_on_move = !face_combat_target() - update_turf_movespeed(loc) if(HAS_TRAIT(src, TRAIT_NEGATES_GRAVITY)) if(!isgroundlessturf(loc)) diff --git a/code/modules/vehicles/mecha/mecha_defense.dm b/code/modules/vehicles/mecha/mecha_defense.dm index 3d943284d62c..2afd5bbc525f 100644 --- a/code/modules/vehicles/mecha/mecha_defense.dm +++ b/code/modules/vehicles/mecha/mecha_defense.dm @@ -343,6 +343,7 @@ spray_blood(get_dir(user, src), rand(2, 4), list("[oil_name]" = oil_type)) oil_pool -= 2 + user.combat_lock_on(src) return damage_taken /obj/vehicle/sealed/mecha/attack_generic(mob/user, damage_amount, damage_type, damage_flag, effects, armor_penetration) From 36e2bb843a3a754c1dee054f3ee8d073a74aa40e Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Wed, 12 Aug 2026 21:26:29 -0500 Subject: [PATCH 3/7] Throwing --- code/game/atoms_movable.dm | 2 +- code/modules/mob/living/living_defense.dm | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 52b865fb16fb..4abae0b04bab 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1382,7 +1382,7 @@ thrown_thing.tick() // Makes the thrower track the thrown thing as it flies if (astype(thrower, /mob/living)?.combat_mode) - thrower.combat_lock_on(src, 1 SECONDS) + astype(thrower, /mob/living).combat_lock_on(src, 1 SECONDS, override_existing = TRUE) /atom/movable/proc/handle_buckled_mob_movement(newloc, direct, glide_size_override) for(var/mob/living/buckled_mob as anything in buckled_mobs) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index f7d2f514431e..f09a20671d3d 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -241,10 +241,6 @@ SEND_SOUND(src, sound('sound/misc/ui_toggleoffcombat.ogg', volume = 25)) //Slightly modified version of the above /mob/living/hitby(atom/movable/AM, skipcatch, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum) - var/mob/thrown_by = thrown_item.thrownby?.resolve() - // Swaps to following the guy you hit with the thrown item - if(astype(thrown_by, /mob/living)?.combat_mode) - thrown_by.combat_lock_on(src, 10 SECONDS, override_existing = TRUE) if(!isitem(AM)) // Filled with made up numbers for non-items. if(check_block(AM, 30, "\the [AM.name]", THROWN_PROJECTILE_ATTACK, 0, BRUTE)) @@ -263,6 +259,11 @@ if(thrown_item.thrownby == WEAKREF(src)) //No throwing stuff at yourself to trigger hit reactions return ..() + var/mob/thrown_by = thrown_item.thrownby?.resolve() + // Swaps to following the guy you hit with the thrown item + if(throwforce >= 5 && astype(thrown_by, /mob/living)?.combat_mode) + astype(thrown_by, /mob/living).combat_lock_on(src, 10 SECONDS, override_existing = TRUE) + if(check_block(AM, thrown_item.throwforce, "\the [thrown_item.name]", THROWN_PROJECTILE_ATTACK, 0, thrown_item.damtype)) hitpush = FALSE skipcatch = TRUE From 62fbf36ba9244f6fe20c52433c836dacb63eb558 Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Thu, 13 Aug 2026 13:44:36 -0500 Subject: [PATCH 4/7] Rename --- code/game/atoms_movable.dm | 2 +- code/modules/mob/living/living_defense.dm | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm index 4abae0b04bab..b456618ac865 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1382,7 +1382,7 @@ thrown_thing.tick() // Makes the thrower track the thrown thing as it flies if (astype(thrower, /mob/living)?.combat_mode) - astype(thrower, /mob/living).combat_lock_on(src, 1 SECONDS, override_existing = TRUE) + astype(thrower, /mob/living).combat_lock_on(src, 1 SECONDS, force_override_target = TRUE) /atom/movable/proc/handle_buckled_mob_movement(newloc, direct, glide_size_override) for(var/mob/living/buckled_mob as anything in buckled_mobs) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index f09a20671d3d..c39bb07efd3e 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -262,7 +262,7 @@ var/mob/thrown_by = thrown_item.thrownby?.resolve() // Swaps to following the guy you hit with the thrown item if(throwforce >= 5 && astype(thrown_by, /mob/living)?.combat_mode) - astype(thrown_by, /mob/living).combat_lock_on(src, 10 SECONDS, override_existing = TRUE) + astype(thrown_by, /mob/living).combat_lock_on(src, 10 SECONDS, force_override_target = TRUE) if(check_block(AM, thrown_item.throwforce, "\the [thrown_item.name]", THROWN_PROJECTILE_ATTACK, 0, thrown_item.damtype)) hitpush = FALSE @@ -783,20 +783,20 @@ * Arguments * * target - The target to lock onto * * duration - How long to lock onto the target for - * * override_existing - If set to FALSE, existing targets will not be overridden unless they are further than the new target. + * * force_override_target - If set to FALSE, existing targets will not be overridden unless they are further than the new target. */ -/mob/living/proc/combat_lock_on(atom/movable/target, duration, override_existing = FALSE) +/mob/living/proc/combat_lock_on(atom/movable/target, duration, force_override_target = FALSE) if(target == src || get_dist(src, target) > 7) return - apply_status_effect(/datum/status_effect/combat_lock, target, duration, override_existing) + apply_status_effect(/datum/status_effect/combat_lock, target, duration, force_override_target) if(!isliving(target)) return // Immediately mirror combat lock if fighting an AI, makes it look like they're reacting to you like a player would. var/mob/living/target_living = target if(!isnull(target_living.ai_controller) && isnull(target_living.client)) - target_living.combat_lock_on(src, duration, override_existing) + target_living.combat_lock_on(src, duration, force_override_target) /datum/status_effect/combat_lock id = "combat_lock" @@ -808,7 +808,7 @@ /// Movable we struck and are locked onto VAR_PRIVATE/atom/movable/combat_target -/datum/status_effect/combat_lock/on_creation(mob/living/new_owner, atom/movable/combat_target, duration = 20 SECONDS, override_existing) +/datum/status_effect/combat_lock/on_creation(mob/living/new_owner, atom/movable/combat_target, duration = 20 SECONDS, force_override_target) if(isnull(combat_target)) stack_trace("Attempted to create a combat lock without a target!") qdel(src) @@ -839,9 +839,9 @@ return ..() // Refresh refreshes duration - but then if a different, closer target is passed in, swap to that one instead. -/datum/status_effect/combat_lock/refresh(effect, atom/movable/other_lock, duration = 20 SECONDS, override_existing) +/datum/status_effect/combat_lock/refresh(effect, atom/movable/other_lock, duration = 20 SECONDS, force_override_target) src.duration += duration - if(combat_target == other_lock || (!override_existing && get_dist(owner, combat_target) < get_dist(owner, other_lock))) + if(combat_target == other_lock || (!force_override_target && get_dist(owner, combat_target) < get_dist(owner, other_lock))) return UnregisterSignal(combat_target, COMSIG_QDELETING) UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) From d886fb4a4ea5266c6726fa5ae4db0b8bff0a8ae2 Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Thu, 13 Aug 2026 14:03:23 -0500 Subject: [PATCH 5/7] Tweaks --- code/__DEFINES/living.dm | 2 + code/modules/mob/living/living_defense.dm | 124 +-------------- code/modules/vehicles/mecha/mecha_defense.dm | 1 - maplestation.dme | 1 + .../code/modules/mob/living/combat_lock.dm | 148 ++++++++++++++++++ 5 files changed, 153 insertions(+), 123 deletions(-) create mode 100644 maplestation_modules/code/modules/mob/living/combat_lock.dm diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm index 76ee12d826a0..555543f051e2 100644 --- a/code/__DEFINES/living.dm +++ b/code/__DEFINES/living.dm @@ -44,6 +44,8 @@ #define COMSIG_LIVING_CAN_ALLOW_THROUGH "living_can_allow_through" #define COMPONENT_LIVING_PASSABLE (1<<0) +#define COMSIG_LIVING_COMBAT_MODE_CHANGE "living_combat_mode_change" + /// Send when sharing body temperature to breath #define COMSIG_HUMAN_ON_HANDLE_BREATH_TEMPERATURE "human_on_handle_breath_temperature" /// Stops further processing diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index c39bb07efd3e..a3477d08f73c 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -231,8 +231,8 @@ return . = combat_mode combat_mode = new_mode - if(hud_used?.action_intent) - hud_used.action_intent.update_appearance() + SEND_SIGNAL(src, COMSIG_LIVING_COMBAT_MODE_CHANGE) + hud_used?.action_intent?.update_appearance() if(silent || !client?.prefs.read_preference(/datum/preference/toggle/sound_combatmode)) return if(combat_mode) @@ -776,123 +776,3 @@ return TRUE return FALSE - -/** - * Locks onto a target, turning to face the mob so long as we're in combat mode - * - * Arguments - * * target - The target to lock onto - * * duration - How long to lock onto the target for - * * force_override_target - If set to FALSE, existing targets will not be overridden unless they are further than the new target. - */ -/mob/living/proc/combat_lock_on(atom/movable/target, duration, force_override_target = FALSE) - if(target == src || get_dist(src, target) > 7) - return - - apply_status_effect(/datum/status_effect/combat_lock, target, duration, force_override_target) - if(!isliving(target)) - return - - // Immediately mirror combat lock if fighting an AI, makes it look like they're reacting to you like a player would. - var/mob/living/target_living = target - if(!isnull(target_living.ai_controller) && isnull(target_living.client)) - target_living.combat_lock_on(src, duration, force_override_target) - -/datum/status_effect/combat_lock - id = "combat_lock" - tick_interval = -1 - duration = 20 SECONDS - status_type = STATUS_EFFECT_REFRESH - alert_type = null - on_remove_on_mob_delete = TRUE - /// Movable we struck and are locked onto - VAR_PRIVATE/atom/movable/combat_target - -/datum/status_effect/combat_lock/on_creation(mob/living/new_owner, atom/movable/combat_target, duration = 20 SECONDS, force_override_target) - if(isnull(combat_target)) - stack_trace("Attempted to create a combat lock without a target!") - qdel(src) - return - - src.duration = duration - src.combat_target = combat_target - RegisterSignal(combat_target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) - RegisterSignal(combat_target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_target_moved)) - return ..() - -/datum/status_effect/combat_lock/on_apply() - if(isnull(owner.ai_controller) && isnull(owner.client)) - return FALSE - - RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(owner_moved)) - face_combat_target() - return TRUE - -/datum/status_effect/combat_lock/on_remove() - . = ..() - owner.set_dir_on_move = initial(owner.set_dir_on_move) - -/datum/status_effect/combat_lock/Destroy() - UnregisterSignal(combat_target, COMSIG_QDELETING) - UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) - combat_target = null - return ..() - -// Refresh refreshes duration - but then if a different, closer target is passed in, swap to that one instead. -/datum/status_effect/combat_lock/refresh(effect, atom/movable/other_lock, duration = 20 SECONDS, force_override_target) - src.duration += duration - if(combat_target == other_lock || (!force_override_target && get_dist(owner, combat_target) < get_dist(owner, other_lock))) - return - UnregisterSignal(combat_target, COMSIG_QDELETING) - UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) - combat_target = other_lock - RegisterSignal(combat_target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) - RegisterSignal(combat_target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_target_moved)) - face_combat_target() - -/datum/status_effect/combat_lock/proc/drop_combat_lock(datum/source) - SIGNAL_HANDLER - // Target is gone, no more lock. - qdel(src) - -/datum/status_effect/combat_lock/proc/combat_target_moved(datum/source, ...) - SIGNAL_HANDLER - // They left the distance, drop the lock and let them reacquire it later. - if(get_dist(owner, combat_target) > 7) - qdel(src) - return - // Otherwise keep facing them wherever they are now. - face_combat_target() - -/datum/status_effect/combat_lock/proc/owner_moved(datum/source, ...) - SIGNAL_HANDLER - // Prevents the mob from turning to face their direction if they successfully faced the target. - owner.set_dir_on_move = !face_combat_target() - -/datum/status_effect/combat_lock/proc/face_combat_target() - // - If we're a not AI controlled, we have to be in combat mode to face the target - // - Otherwise if we ARE an AI mob, then skip the combat mode check, because they can enter and exit it sporadically - if(isnull(owner.ai_controller)) - if(!owner.combat_mode) - return FALSE - else - if(isnull(owner.client)) - return FALSE - // - No turn if we're being pulled, let the puller handle it. - // - No turn if incap, because of course we can't... we're probably dead. - if(!isnull(owner.pulledby) || HAS_TRAIT(owner, TRAIT_INCAPACITATED)) - return FALSE - // - Turf checks are for nullspace shenanigans - // - Otherwise we can't face them if we're on the same tile - if(!isturf(owner.loc) || !isturf(combat_target.loc) || owner.loc == combat_target.loc) - return FALSE - // - Low alpha is intended to be harder to see, so don't let people aimbot them. 64 is picked as ~25% alpha. - // - Invisibility is obviously intended to make people incapable of being seen so don't let people aimbot them either - if(combat_target.alpha <= 64 || combat_target.invisibility > owner.see_invisible) - return FALSE - // - Blanket "can we even see see them" check, most expensive one (relatively) so it comes last. - if(!(owner in viewers(5, combat_target))) - return FALSE - - owner.face_atom(combat_target) - return TRUE diff --git a/code/modules/vehicles/mecha/mecha_defense.dm b/code/modules/vehicles/mecha/mecha_defense.dm index 2afd5bbc525f..3d943284d62c 100644 --- a/code/modules/vehicles/mecha/mecha_defense.dm +++ b/code/modules/vehicles/mecha/mecha_defense.dm @@ -343,7 +343,6 @@ spray_blood(get_dir(user, src), rand(2, 4), list("[oil_name]" = oil_type)) oil_pool -= 2 - user.combat_lock_on(src) return damage_taken /obj/vehicle/sealed/mecha/attack_generic(mob/user, damage_amount, damage_type, damage_flag, effects, armor_penetration) diff --git a/maplestation.dme b/maplestation.dme index 5d0fc3d1094b..4d765652e6fe 100644 --- a/maplestation.dme +++ b/maplestation.dme @@ -6670,6 +6670,7 @@ #include "maplestation_modules\code\modules\mob\basic\farm_animals\goat\goat_subtypes.dm" #include "maplestation_modules\code\modules\mob\dead\new_player\sprite_accessories.dm" #include "maplestation_modules\code\modules\mob\living\blood.dm" +#include "maplestation_modules\code\modules\mob\living\combat_lock.dm" #include "maplestation_modules\code\modules\mob\living\emote.dm" #include "maplestation_modules\code\modules\mob\living\impede_speech_verb.dm" #include "maplestation_modules\code\modules\mob\living\living_movement.dm" diff --git a/maplestation_modules/code/modules/mob/living/combat_lock.dm b/maplestation_modules/code/modules/mob/living/combat_lock.dm new file mode 100644 index 000000000000..b4d24aa4f145 --- /dev/null +++ b/maplestation_modules/code/modules/mob/living/combat_lock.dm @@ -0,0 +1,148 @@ + +/** + * Locks onto a target, turning to face the mob so long as we're in combat mode + * + * Arguments + * * target - The target to lock onto + * * duration - How long to lock onto the target for + * * force_override_target - If set to FALSE, existing targets will not be overridden unless they are further than the new target. + * * mirror_to_ai_mobs - If set to TRUE, will also lock the target onto the source mob if the target is an AI controlled mob. + */ +/mob/living/proc/combat_lock_on(atom/movable/target, duration, force_override_target = FALSE, mirror_to_ai_mobs = TRUE) + if(target == src || get_dist(src, target) > 7) + return + + apply_status_effect(/datum/status_effect/combat_lock, target, duration, force_override_target) + if(!isliving(target) || !mirror_to_ai_mobs) + return + + // Immediately mirror combat lock if fighting an AI, makes it look like they're reacting to you like a player would. + var/mob/living/target_living = target + if(!isnull(target_living.ai_controller) && isnull(target_living.client)) + target_living.combat_lock_on(src, duration, force_override_target, mirror_to_ai_mobs = FALSE) + +/datum/status_effect/combat_lock + id = "combat_lock" + tick_interval = -1 + duration = 20 SECONDS + status_type = STATUS_EFFECT_REFRESH + alert_type = null + on_remove_on_mob_delete = TRUE + /// Movable we struck and are locked onto + VAR_PRIVATE/atom/movable/combat_target + +/datum/status_effect/combat_lock/on_creation(mob/living/new_owner, atom/movable/combat_target, duration = 20 SECONDS, force_override_target) + if(isnull(combat_target)) + stack_trace("Attempted to create a combat lock without a target!") + qdel(src) + return + + src.duration = duration + src.combat_target = combat_target + RegisterSignals(combat_target, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING), PROC_REF(drop_combat_lock)) + RegisterSignal(combat_target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_target_moved)) + return ..() + +/datum/status_effect/combat_lock/on_apply() + if(isnull(owner.ai_controller) && isnull(owner.client)) + return FALSE + + RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(owner_moved)) + RegisterSignal(owner, COMSIG_LIVING_COMBAT_MODE_CHANGE, PROC_REF(combat_target_moved)) + face_combat_target() + return TRUE + +/datum/status_effect/combat_lock/on_remove() + . = ..() + UnregisterSignal(owner, list(COMSIG_MOVABLE_MOVED, COMSIG_LIVING_COMBAT_MODE_CHANGE)) + owner.set_dir_on_move = initial(owner.set_dir_on_move) + +/datum/status_effect/combat_lock/Destroy() + UnregisterSignal(combat_target, list(COMSIG_LIVING_DEATH, COMSIG_QDELETING, COMSIG_MOVABLE_MOVED)) + combat_target = null + return ..() + +// Refresh refreshes duration - but then if a different, closer target is passed in, swap to that one instead. +/datum/status_effect/combat_lock/refresh(effect, atom/movable/other_lock, duration = 20 SECONDS, force_override_target) + src.duration = min(src.duration + duration, world.time + duration) + if(combat_target == other_lock || (!force_override_target && get_dist(owner, combat_target) < get_dist(owner, other_lock))) + return + UnregisterSignal(combat_target, COMSIG_QDELETING) + UnregisterSignal(combat_target, COMSIG_MOVABLE_MOVED) + combat_target = other_lock + RegisterSignal(combat_target, COMSIG_QDELETING, PROC_REF(drop_combat_lock)) + RegisterSignal(combat_target, COMSIG_MOVABLE_MOVED, PROC_REF(combat_target_moved)) + face_combat_target() + +/datum/status_effect/combat_lock/proc/drop_combat_lock(datum/source) + SIGNAL_HANDLER + // Target is gone, no more lock. + qdel(src) + +/datum/status_effect/combat_lock/proc/combat_target_moved(datum/source, ...) + SIGNAL_HANDLER + // They left the distance, drop the lock and let them reacquire it later. + if(get_dist(owner, combat_target) > 7) + qdel(src) + return + // Otherwise keep facing them wherever they are now. + face_combat_target() + +/datum/status_effect/combat_lock/proc/owner_moved(datum/source, ...) + SIGNAL_HANDLER + // Prevents the mob from turning to face their direction if they successfully faced the target. + owner.set_dir_on_move = !face_combat_target() + +/datum/status_effect/combat_lock/proc/face_combat_target() + // - If we're a not AI controlled, we have to be in combat mode to face the target + // - Otherwise if we ARE an AI mob, then skip the combat mode check, because they can enter and exit it sporadically + if(isnull(owner.ai_controller)) + if(!owner.combat_mode) + return FALSE + else + if(isnull(owner.client)) + return FALSE + // - No turn if we're being pulled, let the puller handle it. + // - No turn if incap, because of course we can't... we're probably dead. + if(!isnull(owner.pulledby) || HAS_TRAIT(owner, TRAIT_INCAPACITATED)) + return FALSE + // - Turf checks are for nullspace shenanigans + // - Otherwise we can't face them if we're on the same tile + if(!isturf(owner.loc) || !isturf(combat_target.loc) || owner.loc == combat_target.loc) + return FALSE + // - Low alpha is intended to be harder to see, so don't let people aimbot them. 64 is picked as ~25% alpha. + // - Invisibility is obviously intended to make people incapable of being seen so don't let people aimbot them either + if(combat_target.alpha <= 64 || combat_target.invisibility > owner.see_invisible) + return FALSE + // - Blanket "can we even see see them" check, most expensive one (relatively) so it comes last. + if(!(owner in viewers(5, combat_target))) + return FALSE + + owner.face_atom(combat_target) + return TRUE + +// Locking on to random relevant targets. Could make this an element but it's whatever +/obj/machinery/porta_turret/attacked_by(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) + . = ..() + if(!.) + return + user.combat_lock_on(src, 5 SECONDS) + +/obj/structure/spawner/lavaland/goliath/attacked_by(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) + . = ..() + if(!.) + return + user.combat_lock_on(src, 5 SECONDS) + +/obj/vehicle/attacked_by(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) + . = ..() + if(!.) + return + user.combat_lock_on(src, 5 SECONDS) + +/obj/vehicle/sealed/mecha/attacked_by(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) + . = ..() + if(!.) + return + + user.combat_lock_on(src) From 99bd2e1a138a18390461f6b12c4570669ce9fdfe Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Thu, 13 Aug 2026 14:44:02 -0500 Subject: [PATCH 6/7] KC --- code/__DEFINES/living.dm | 3 +++ code/modules/assembly/flash.dm | 17 +++++++++------ .../mining/equipment/kinetic_crusher.dm | 1 + .../code/modules/mob/living/combat_lock.dm | 21 +++++++++++++++---- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm index 555543f051e2..cc6744531915 100644 --- a/code/__DEFINES/living.dm +++ b/code/__DEFINES/living.dm @@ -273,3 +273,6 @@ #define IS_PHYSICAL_DAMAGE(damage_type) (damage_type == BRUTE || damage_type == BURN) /// Damtype is intended to disable rather than kill #define IS_DISABLING_DAMAGE(damage_type) (damage_type == STAMINA || damage_type == PAIN) + +/// Weapon relies on positioning in some way, so we should be careful handing out combat locks to/from wielders of it +#define TRAIT_POSITION_BASED_WEAPON "position_based_weapon" diff --git a/code/modules/assembly/flash.dm b/code/modules/assembly/flash.dm index cf9cbd6b10e4..028d7064214a 100644 --- a/code/modules/assembly/flash.dm +++ b/code/modules/assembly/flash.dm @@ -27,6 +27,10 @@ var/cooldown = 0 var/last_trigger = 0 //Last time it was successfully triggered. +/obj/item/assembly/flash/Initialize(mapload) + . = ..() + ADD_TRAIT(src, TRAIT_POSITION_BASED_WEAPON, INNATE_TRAIT) + /obj/item/assembly/flash/suicide_act(mob/living/user) if(burnt_out) user.visible_message(span_suicide("[user] raises \the [src] up to [user.p_their()] eyes and activates it ... but it's burnt out!")) @@ -232,16 +236,17 @@ // Attacker lateral to the victim. return DEVIATION_PARTIAL -/obj/item/assembly/flash/attack(mob/living/M, mob/user) +/obj/item/assembly/flash/attack(mob/living/target_mob, mob/living/user) if(!try_use_flash(user)) return FALSE . = TRUE - if(iscarbon(M)) - flash_carbon(M, user, confusion_duration = 5 SECONDS, targeted = TRUE) + user.combat_lock_on(target_mob, 5 SECONDS) + if(iscarbon(target_mob)) + flash_carbon(target_mob, user, confusion_duration = 5 SECONDS, targeted = TRUE) return - if(issilicon(M)) - var/mob/living/silicon/robot/flashed_borgo = M + if(issilicon(target_mob)) + var/mob/living/silicon/robot/flashed_borgo = target_mob log_combat(user, flashed_borgo, "flashed", src) update_icon(ALL, TRUE) if(flashed_borgo.flash_act(affect_silicon = TRUE)) @@ -258,7 +263,7 @@ user.visible_message(span_warning("[user] fails to blind [flashed_borgo] with the flash!"), span_warning("You fail to blind [flashed_borgo] with the flash!")) return - user.visible_message(span_warning("[user] fails to blind [M] with the flash!"), span_warning("You fail to blind [M] with the flash!")) + user.visible_message(span_warning("[user] fails to blind [target_mob] with the flash!"), span_warning("You fail to blind [target_mob] with the flash!")) /obj/item/assembly/flash/attack_self(mob/living/carbon/user, flag = 0, emp = 0) if(holder) diff --git a/code/modules/mining/equipment/kinetic_crusher.dm b/code/modules/mining/equipment/kinetic_crusher.dm index a2b5f079f023..bcb4ea9fccb0 100644 --- a/code/modules/mining/equipment/kinetic_crusher.dm +++ b/code/modules/mining/equipment/kinetic_crusher.dm @@ -48,6 +48,7 @@ ) //technically it's huge and bulky, but this provides an incentive to use it AddComponent(/datum/component/two_handed, force_unwielded=0, force_wielded=20) + ADD_TRAIT(src, TRAIT_POSITION_BASED_WEAPON, INNATE_TRAIT) /obj/item/kinetic_crusher/Destroy() QDEL_LIST(trophies) diff --git a/maplestation_modules/code/modules/mob/living/combat_lock.dm b/maplestation_modules/code/modules/mob/living/combat_lock.dm index b4d24aa4f145..b10d9c236609 100644 --- a/maplestation_modules/code/modules/mob/living/combat_lock.dm +++ b/maplestation_modules/code/modules/mob/living/combat_lock.dm @@ -1,3 +1,5 @@ +#define IS_AI_MOB(mob) (!isnull(mob.ai_controller) && isnull(mob.client)) +#define IS_PLAYER(mob) (!isnull(mob.client) && isnull(mob.ai_controller)) /** * Locks onto a target, turning to face the mob so long as we're in combat mode @@ -11,6 +13,11 @@ /mob/living/proc/combat_lock_on(atom/movable/target, duration, force_override_target = FALSE, mirror_to_ai_mobs = TRUE) if(target == src || get_dist(src, target) > 7) return + // Avoid having AI mobs lock onto players using position based weapons at all (anti-frustration feature) + if(IS_AI_MOB(src)) + for(var/obj/item/weapon in astype(target, /mob/living)?.held_items) + if(HAS_TRAIT(weapon, TRAIT_POSITION_BASED_WEAPON)) + return apply_status_effect(/datum/status_effect/combat_lock, target, duration, force_override_target) if(!isliving(target) || !mirror_to_ai_mobs) @@ -18,7 +25,7 @@ // Immediately mirror combat lock if fighting an AI, makes it look like they're reacting to you like a player would. var/mob/living/target_living = target - if(!isnull(target_living.ai_controller) && isnull(target_living.client)) + if(IS_AI_MOB(target_living)) target_living.combat_lock_on(src, duration, force_override_target, mirror_to_ai_mobs = FALSE) /datum/status_effect/combat_lock @@ -44,7 +51,7 @@ return ..() /datum/status_effect/combat_lock/on_apply() - if(isnull(owner.ai_controller) && isnull(owner.client)) + if(!IS_AI_MOB(owner) && !IS_PLAYER(owner)) return FALSE RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(owner_moved)) @@ -96,12 +103,15 @@ /datum/status_effect/combat_lock/proc/face_combat_target() // - If we're a not AI controlled, we have to be in combat mode to face the target // - Otherwise if we ARE an AI mob, then skip the combat mode check, because they can enter and exit it sporadically - if(isnull(owner.ai_controller)) + if(IS_PLAYER(owner)) if(!owner.combat_mode) return FALSE else - if(isnull(owner.client)) + if(!IS_AI_MOB(owner)) return FALSE + for(var/obj/item/weapon in astype(combat_target, /mob/living)?.held_items) + if(HAS_TRAIT(weapon, TRAIT_POSITION_BASED_WEAPON)) + return FALSE // - No turn if we're being pulled, let the puller handle it. // - No turn if incap, because of course we can't... we're probably dead. if(!isnull(owner.pulledby) || HAS_TRAIT(owner, TRAIT_INCAPACITATED)) @@ -121,6 +131,9 @@ owner.face_atom(combat_target) return TRUE +#undef IS_AI_MOB +#undef IS_PLAYER + // Locking on to random relevant targets. Could make this an element but it's whatever /obj/machinery/porta_turret/attacked_by(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) . = ..() From c2387b75d1d2b5d9a72990335f5dca461b0662df Mon Sep 17 00:00:00 2001 From: MrMelbert Date: Thu, 13 Aug 2026 14:44:42 -0500 Subject: [PATCH 7/7] Tweak --- .../code/modules/mob/living/combat_lock.dm | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/maplestation_modules/code/modules/mob/living/combat_lock.dm b/maplestation_modules/code/modules/mob/living/combat_lock.dm index b10d9c236609..407775a47bee 100644 --- a/maplestation_modules/code/modules/mob/living/combat_lock.dm +++ b/maplestation_modules/code/modules/mob/living/combat_lock.dm @@ -20,13 +20,12 @@ return apply_status_effect(/datum/status_effect/combat_lock, target, duration, force_override_target) - if(!isliving(target) || !mirror_to_ai_mobs) - return - // Immediately mirror combat lock if fighting an AI, makes it look like they're reacting to you like a player would. var/mob/living/target_living = target - if(IS_AI_MOB(target_living)) - target_living.combat_lock_on(src, duration, force_override_target, mirror_to_ai_mobs = FALSE) + if(!istype(target_living) || !mirror_to_ai_mobs || !IS_AI_MOB(target_living)) + return + + target_living.combat_lock_on(src, duration, force_override_target, mirror_to_ai_mobs = FALSE) /datum/status_effect/combat_lock id = "combat_lock"