Files
Bubberstation/code/datums/components/ranged_mob_full_auto.dm
T
JacquerelandGitHub 6454b5ec9b Automatic fire for mobs (#79502)
This adds a component you can slap onto mobs with ranged attacks which
lets them fire repeatedly with the mouse held down.
I applied it to a couple of existing mobs, to be honest we don't
currently have any which _need_ this but I will add it to the Ranged
Holoparasite (which really does need it) depending on which PR gets
merged first.
2023-11-09 07:32:34 +00:00

204 lines
7.3 KiB
Plaintext

#define AUTOFIRE_MOUSEUP 1
#define AUTOFIRE_MOUSEDOWN 0
/// Allows a mob to autofire by holding down the cursor
/datum/component/ranged_mob_full_auto
/// Delay before attempting to fire again, note that this is just when we make attempts and is separate from mob's actual firing cooldown
var/autofire_shot_delay
/// Our client for click tracking
var/client/clicker
/// Are we currently firing?
var/is_firing = FALSE
/// This seems hacky but there can be two MouseDown() without a MouseUp() in between if the user holds click and uses alt+tab, printscreen or similar.
var/awaiting_status = AUTOFIRE_MOUSEDOWN
/// What are we currently shooting at?
var/atom/target
/// Where are we currently shooting at?
var/turf/target_loc
/// When will we next try to shoot?
COOLDOWN_DECLARE(next_shot_cooldown)
/datum/component/ranged_mob_full_auto/Initialize(autofire_shot_delay = 0.5 SECONDS)
. = ..()
if (!isliving(parent))
return COMPONENT_INCOMPATIBLE
src.autofire_shot_delay = autofire_shot_delay
var/mob/living/living_parent = parent
if (isnull(living_parent.client))
return
on_gained_client(parent)
/datum/component/ranged_mob_full_auto/RegisterWithParent()
RegisterSignal(parent, COMSIG_MOB_LOGIN, PROC_REF(on_gained_client))
RegisterSignal(parent, COMSIG_MOB_LOGOUT, PROC_REF(on_lost_client))
/datum/component/ranged_mob_full_auto/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT))
/datum/component/ranged_mob_full_auto/process(seconds_per_tick)
if (!try_shooting())
return PROCESS_KILL
/// Try and take a shot, returns false if we are unable to do so and should stop trying
/datum/component/ranged_mob_full_auto/proc/try_shooting()
if (!is_firing)
return FALSE
if (!COOLDOWN_FINISHED(src, next_shot_cooldown))
return TRUE // Don't fire but also keep processing
var/mob/living/living_parent = parent
if (isnull(target) || get_turf(target) != target_loc) // Target moved or got destroyed since we last aimed.
set_target(target_loc)
target = target_loc // So we keep firing on the emptied tile until we move our mouse and find a new target.
if (get_dist(living_parent, target) <= 0)
set_target(get_step(living_parent, living_parent.dir)) // Shoot in the direction faced if the mouse is on the same tile as we are.
target_loc = target
else if (!in_view_range(living_parent, target))
stop_firing()
return FALSE // Can't see shit
living_parent.face_atom(target)
COOLDOWN_START(src, next_shot_cooldown, autofire_shot_delay)
living_parent.RangedAttack(target)
return TRUE
/// Setter for reference handling
/datum/component/ranged_mob_full_auto/proc/set_target(atom/new_target)
if (!isnull(target))
UnregisterSignal(target, COMSIG_QDELETING)
target = new_target
if (!isnull(target))
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(on_target_deleted))
/// Don't hang references
/datum/component/ranged_mob_full_auto/proc/on_target_deleted()
SIGNAL_HANDLER
set_target(null)
/// When we gain a client, start tracking clicks
/datum/component/ranged_mob_full_auto/proc/on_gained_client(mob/living/source)
SIGNAL_HANDLER
clicker = source.client
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(on_mouse_down))
/// When we lose our client, stop functioning
/datum/component/ranged_mob_full_auto/proc/on_lost_client(mob/living/source)
SIGNAL_HANDLER
if (!isnull(clicker))
UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDOWN, COMSIG_CLIENT_MOUSEDRAG, COMSIG_CLIENT_MOUSEUP))
stop_firing()
clicker = null
/// On mouse down start shooting!
/datum/component/ranged_mob_full_auto/proc/on_mouse_down(client/source, atom/target, turf/location, control, params)
SIGNAL_HANDLER
if (awaiting_status != AUTOFIRE_MOUSEDOWN)
return // Avoid a double mousedown with no mouseup
var/list/modifiers = params2list(params)
if (LAZYACCESS(modifiers, SHIFT_CLICK))
return
if (LAZYACCESS(modifiers, CTRL_CLICK))
return
if (LAZYACCESS(modifiers, MIDDLE_CLICK))
return
if (LAZYACCESS(modifiers, RIGHT_CLICK))
return
if (LAZYACCESS(modifiers, ALT_CLICK))
return
var/mob/living/living_parent = parent
if (!isturf(living_parent.loc) || living_parent.Adjacent(target))
return
if (isnull(location) || istype(target, /atom/movable/screen)) // Clicking on a screen object.
if (target.plane != CLICKCATCHER_PLANE) // The clickcatcher is a special case. We want the click to trigger then, under it.
return // If we click and drag on our worn backpack, for example, we want it to open instead.
set_target(parse_caught_click_modifiers(modifiers, get_turf(source.eye), source))
params = list2params(modifiers)
if (isnull(target))
CRASH("Failed to get the turf under clickcatcher")
awaiting_status = AUTOFIRE_MOUSEUP
source.click_intercept_time = world.time // From this point onwards Click() will no longer be triggered.
if (is_firing)
stop_firing()
set_target(target)
target_loc = get_turf(target)
INVOKE_ASYNC(src, PROC_REF(start_firing))
/// Start tracking mouse movement and processing our shots
/datum/component/ranged_mob_full_auto/proc/start_firing()
if (is_firing)
return
is_firing = TRUE
if (!try_shooting()) // First one is immediate
stop_firing()
return
clicker.mouse_override_icon = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
clicker.mouse_pointer_icon = clicker.mouse_override_icon
START_PROCESSING(SSprojectiles, src)
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEUP, PROC_REF(on_mouse_up))
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG, PROC_REF(on_mouse_drag))
/// When the mouse moved let's try and shift our aim
/datum/component/ranged_mob_full_auto/proc/on_mouse_drag(client/source, atom/src_object, atom/over_object, turf/src_location, turf/over_location, src_control, over_control, params)
SIGNAL_HANDLER
if (!isnull(over_location))
set_target(over_object)
target_loc = get_turf(over_object)
return
//This happens when the mouse is over an inventory or screen object, or on entering deep darkness, for example.
var/list/modifiers = params2list(params)
var/new_target = parse_caught_click_modifiers(modifiers, get_turf(source.eye), source)
params = list2params(modifiers)
if (!isnull(new_target))
set_target(new_target)
target_loc = new_target
return
if (QDELETED(target)) //No new target acquired, and old one was deleted, get us out of here.
stop_firing()
CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]. Old target was incidentally QDELETED.")
set_target(get_turf(target)) //If previous target wasn't a turf, let's turn it into one to avoid locking onto a potentially moving target.
target_loc = target
CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]")
/// When the mouse is released we should stop
/datum/component/ranged_mob_full_auto/proc/on_mouse_up()
SIGNAL_HANDLER
if (awaiting_status != AUTOFIRE_MOUSEUP)
return
stop_firing()
return COMPONENT_CLIENT_MOUSEUP_INTERCEPT
/// Stop watching our mouse and processing shots
/datum/component/ranged_mob_full_auto/proc/stop_firing()
if (!is_firing)
return
is_firing = FALSE
set_target(null)
target_loc = null
STOP_PROCESSING(SSprojectiles, src)
awaiting_status = AUTOFIRE_MOUSEDOWN
if (isnull(clicker))
return
UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDRAG, COMSIG_CLIENT_MOUSEUP))
clicker.mouse_override_icon = null
clicker.mouse_pointer_icon = null
#undef AUTOFIRE_MOUSEUP
#undef AUTOFIRE_MOUSEDOWN