Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code/__DEFINES/living.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions code/_onclick/item_attack.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions code/game/atoms_movable.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 11 additions & 6 deletions code/modules/assembly/flash.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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!"))
Expand Down Expand Up @@ -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))
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions code/modules/mining/equipment/kinetic_crusher.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions code/modules/mob/living/carbon/human/_species.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
14 changes: 11 additions & 3 deletions code/modules/mob/living/living_defense.dm
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions maplestation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
160 changes: 160 additions & 0 deletions maplestation_modules/code/modules/mob/living/combat_lock.dm
Original file line number Diff line number Diff line change
@@ -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)
Loading