diff --git a/code/__DEFINES/living.dm b/code/__DEFINES/living.dm index 76ee12d826a0..cc6744531915 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 @@ -271,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/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 45a3144e9cad..a9046b7b2bfd 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -342,6 +342,7 @@ wounding = CANT_WOUND 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..b456618ac865 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) + 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/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/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 3da1d6a6f0d7..b2da50fe5b2f 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -865,6 +865,8 @@ 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) + //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 51c21009a66e..a3477d08f73c 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -185,6 +185,8 @@ ) if(hitting_projectile.dismemberment > 0 && !hitting_projectile.grazing) check_projectile_dismemberment(hitting_projectile, def_zone) + + 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) @@ -229,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) @@ -257,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, force_override_target = TRUE) + if(check_block(AM, thrown_item.throwforce, "\the [thrown_item.name]", THROWN_PROJECTILE_ATTACK, 0, thrown_item.damtype)) hitpush = FALSE skipcatch = TRUE @@ -271,7 +278,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 @@ -701,6 +707,8 @@ shove_flags |= SHOVE_DIRECTIONAL_BLOCKED break + target.combat_lock_on(src) + 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)) 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..407775a47bee --- /dev/null +++ b/maplestation_modules/code/modules/mob/living/combat_lock.dm @@ -0,0 +1,160 @@ +#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 + * + * 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 + // 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) + // 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(!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" + 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(!IS_AI_MOB(owner) && !IS_PLAYER(owner)) + 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(IS_PLAYER(owner)) + if(!owner.combat_mode) + return FALSE + else + 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)) + 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 + +#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) + . = ..() + 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)