Files
Bubberstation/code/datums/components/fullauto.dm
TemporalOroboros 6cf97ad50e Fixes aiming at the click catcher. (#64147)
My PR to fix pixel aiming broke aiming when you were aiming at obscured turfs.
The click catcher was properly modifying the click target to be the turf under the click location,
but it was passing the `ICON_X` and `ICON_Y` variables on unmodified.
This means that instead of being measured from the bottom left corner of the turf you clicked on they were being measured from
roughly the bottom left corner of your screen.

This makes the click relevant click parsing proc also update the ICON_X and ICON_Y values of the click modifiers.
2022-01-19 13:51:56 -08:00

287 lines
11 KiB
Plaintext

#define AUTOFIRE_MOUSEUP 0
#define AUTOFIRE_MOUSEDOWN 1
/datum/component/automatic_fire
var/client/clicker
var/mob/living/shooter
var/atom/target
var/turf/target_loc //For dealing with locking on targets due to BYOND engine limitations (the mouse input only happening when mouse moves).
var/autofire_stat = AUTOFIRE_STAT_IDLE
var/mouse_parameters
var/autofire_shot_delay = 0.3 SECONDS //Time between individual shots.
var/mouse_status = AUTOFIRE_MOUSEUP //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.
COOLDOWN_DECLARE(next_shot_cd)
/datum/component/automatic_fire/Initialize(_autofire_shot_delay)
. = ..()
if(!isgun(parent))
return COMPONENT_INCOMPATIBLE
var/obj/item/gun = parent
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/wake_up)
if(_autofire_shot_delay)
autofire_shot_delay = _autofire_shot_delay
if(autofire_stat == AUTOFIRE_STAT_IDLE && ismob(gun.loc))
var/mob/user = gun.loc
wake_up(src, user)
/datum/component/automatic_fire/Destroy()
autofire_off()
return ..()
/datum/component/automatic_fire/process(delta_time)
if(autofire_stat != AUTOFIRE_STAT_FIRING)
STOP_PROCESSING(SSprojectiles, src)
return
process_shot()
/datum/component/automatic_fire/proc/wake_up(datum/source, mob/user, slot)
SIGNAL_HANDLER
if(autofire_stat == AUTOFIRE_STAT_ALERT)
return //We've updated the firemode. No need for more.
if(autofire_stat == AUTOFIRE_STAT_FIRING)
stop_autofiring() //Let's stop shooting to avoid issues.
return
if(iscarbon(user))
var/mob/living/carbon/arizona_ranger = user
if(arizona_ranger.is_holding(parent))
autofire_on(arizona_ranger.client)
// There is a gun and there is a user wielding it. The component now waits for the mouse click.
/datum/component/automatic_fire/proc/autofire_on(client/usercli)
SIGNAL_HANDLER
if(autofire_stat != AUTOFIRE_STAT_IDLE)
return
autofire_stat = AUTOFIRE_STAT_ALERT
if(!QDELETED(usercli))
clicker = usercli
shooter = clicker.mob
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDOWN, .proc/on_mouse_down)
if(!QDELETED(shooter))
RegisterSignal(shooter, COMSIG_MOB_LOGOUT, .proc/autofire_off)
UnregisterSignal(shooter, COMSIG_MOB_LOGIN)
RegisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_ITEM_DROPPED), .proc/autofire_off)
parent.RegisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, /obj/item/gun/.proc/autofire_bypass_check)
parent.RegisterSignal(parent, COMSIG_AUTOFIRE_SHOT, /obj/item/gun/.proc/do_autofire)
/datum/component/automatic_fire/proc/autofire_off(datum/source)
SIGNAL_HANDLER
if(autofire_stat == AUTOFIRE_STAT_IDLE)
return
if(autofire_stat == AUTOFIRE_STAT_FIRING)
stop_autofiring()
autofire_stat = AUTOFIRE_STAT_IDLE
if(!QDELETED(clicker))
UnregisterSignal(clicker, list(COMSIG_CLIENT_MOUSEDOWN, COMSIG_CLIENT_MOUSEUP, COMSIG_CLIENT_MOUSEDRAG))
mouse_status = AUTOFIRE_MOUSEUP //In regards to the component there's no click anymore to care about.
clicker = null
if(!QDELETED(shooter))
RegisterSignal(shooter, COMSIG_MOB_LOGIN, .proc/on_client_login)
UnregisterSignal(shooter, COMSIG_MOB_LOGOUT)
UnregisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_ITEM_DROPPED))
shooter = null
parent.UnregisterSignal(parent, COMSIG_AUTOFIRE_SHOT)
parent.UnregisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN)
/datum/component/automatic_fire/proc/on_client_login(mob/source)
SIGNAL_HANDLER
if(!source.client)
return
if(source.is_holding(parent))
autofire_on(source.client)
/datum/component/automatic_fire/proc/on_mouse_down(client/source, atom/_target, turf/location, control, params)
SIGNAL_HANDLER
var/list/modifiers = params2list(params) //If they're shift+clicking, for example, let's not have them accidentally shoot.
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
if(source.mob.throw_mode)
return
if(!isturf(source.mob.loc)) //No firing inside lockers and stuff.
return
if(get_dist(source.mob, _target) < 2) //Adjacent clicking.
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.
_target = parse_caught_click_modifiers(modifiers, get_turf(source.eye), source)
params = list2params(modifiers)
if(!_target)
CRASH("Failed to get the turf under clickcatcher")
if(SEND_SIGNAL(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, source, _target, location, control, params) & COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS)
return
source.click_intercept_time = world.time //From this point onwards Click() will no longer be triggered.
if(autofire_stat == (AUTOFIRE_STAT_IDLE))
CRASH("on_mouse_down() called with [autofire_stat] autofire_stat")
if(autofire_stat == AUTOFIRE_STAT_FIRING)
stop_autofiring() //This can happen if we click and hold and then alt+tab, printscreen or other such action. MouseUp won't be called then and it will keep autofiring.
target = _target
target_loc = get_turf(target)
mouse_parameters = params
INVOKE_ASYNC(src, .proc/start_autofiring)
//Dakka-dakka
/datum/component/automatic_fire/proc/start_autofiring()
if(autofire_stat == AUTOFIRE_STAT_FIRING)
return
autofire_stat = AUTOFIRE_STAT_FIRING
clicker.mouse_override_icon = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
clicker.mouse_pointer_icon = clicker.mouse_override_icon
if(mouse_status == AUTOFIRE_MOUSEUP) //See mouse_status definition for the reason for this.
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEUP, .proc/on_mouse_up)
mouse_status = AUTOFIRE_MOUSEDOWN
RegisterSignal(shooter, COMSIG_MOB_SWAP_HANDS, .proc/stop_autofiring)
if(isgun(parent))
var/obj/item/gun/shoota = parent
if(!shoota.on_autofire_start(shooter)) //This is needed because the minigun has a do_after before firing and signals are async.
stop_autofiring()
return
if(autofire_stat != AUTOFIRE_STAT_FIRING)
return //Things may have changed while on_autofire_start() was being processed, due to do_after's sleep.
if(!process_shot()) //First shot is processed instantly.
return //If it fails, such as when the gun is empty, then there's no need to schedule a second shot.
START_PROCESSING(SSprojectiles, src)
RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG, .proc/on_mouse_drag)
/datum/component/automatic_fire/proc/on_mouse_up(datum/source, atom/object, turf/location, control, params)
SIGNAL_HANDLER
UnregisterSignal(clicker, COMSIG_CLIENT_MOUSEUP)
mouse_status = AUTOFIRE_MOUSEUP
if(autofire_stat == AUTOFIRE_STAT_FIRING)
stop_autofiring()
return COMPONENT_CLIENT_MOUSEUP_INTERCEPT
/datum/component/automatic_fire/proc/stop_autofiring(datum/source, atom/object, turf/location, control, params)
SIGNAL_HANDLER
if(autofire_stat != AUTOFIRE_STAT_FIRING)
return
STOP_PROCESSING(SSprojectiles, src)
autofire_stat = AUTOFIRE_STAT_ALERT
if(clicker)
clicker.mouse_override_icon = null
clicker.mouse_pointer_icon = clicker.mouse_override_icon
UnregisterSignal(clicker, COMSIG_CLIENT_MOUSEDRAG)
if(!QDELETED(shooter))
UnregisterSignal(shooter, COMSIG_MOB_SWAP_HANDS)
target = null
target_loc = null
mouse_parameters = null
/datum/component/automatic_fire/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)) //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)
mouse_parameters = params
if(!new_target)
if(QDELETED(target)) //No new target acquired, and old one was deleted, get us out of here.
stop_autofiring()
CRASH("on_mouse_drag failed to get the turf under screen object [over_object.type]. Old target was incidentally QDELETED.")
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]")
target = new_target
target_loc = new_target
return
target = over_object
target_loc = get_turf(over_object)
mouse_parameters = params
/datum/component/automatic_fire/proc/process_shot()
if(autofire_stat != AUTOFIRE_STAT_FIRING)
return FALSE
if(!COOLDOWN_FINISHED(src, next_shot_cd))
return TRUE
if(QDELETED(target) || get_turf(target) != target_loc) //Target moved or got destroyed since we last aimed.
target = target_loc //So we keep firing on the emptied tile until we move our mouse and find a new target.
if(get_dist(shooter, target) <= 0)
target = get_step(shooter, shooter.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(shooter, target))
stop_autofiring() //Elvis has left the building.
return FALSE
shooter.face_atom(target)
var/next_delay = autofire_shot_delay
if(HAS_TRAIT(shooter, TRAIT_DOUBLE_TAP))
next_delay = round(next_delay * 0.5)
COOLDOWN_START(src, next_shot_cd, next_delay)
if(SEND_SIGNAL(parent, COMSIG_AUTOFIRE_SHOT, target, shooter, mouse_parameters) & COMPONENT_AUTOFIRE_SHOT_SUCCESS)
return TRUE
stop_autofiring()
return FALSE
// Gun procs.
/obj/item/gun/proc/on_autofire_start(mob/living/shooter)
if(semicd || shooter.incapacitated() || !can_trigger_gun(shooter))
return FALSE
if(!can_shoot())
shoot_with_empty_chamber(shooter)
return FALSE
var/obj/item/bodypart/other_hand = shooter.has_hand_for_held_index(shooter.get_inactive_hand_index())
if(weapon_weight == WEAPON_HEAVY && (shooter.get_inactive_held_item() || !other_hand))
to_chat(shooter, span_warning("You need two hands to fire [src]!"))
return FALSE
return TRUE
/obj/item/gun/proc/autofire_bypass_check(datum/source, client/clicker, atom/target, turf/location, control, params)
SIGNAL_HANDLER
if(clicker.mob.get_active_held_item() != src)
return COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS
/obj/item/gun/proc/do_autofire(datum/source, atom/target, mob/living/shooter, params)
SIGNAL_HANDLER
if(semicd || shooter.incapacitated())
return NONE
if(!can_shoot())
shoot_with_empty_chamber(shooter)
return NONE
INVOKE_ASYNC(src, .proc/do_autofire_shot, source, target, shooter, params)
return COMPONENT_AUTOFIRE_SHOT_SUCCESS //All is well, we can continue shooting.
/obj/item/gun/proc/do_autofire_shot(datum/source, atom/target, mob/living/shooter, params)
var/obj/item/gun/akimbo_gun = shooter.get_inactive_held_item()
var/bonus_spread = 0
if(istype(akimbo_gun) && weapon_weight < WEAPON_MEDIUM)
if(akimbo_gun.weapon_weight < WEAPON_MEDIUM && akimbo_gun.can_trigger_gun(shooter))
bonus_spread = dual_wield_spread
addtimer(CALLBACK(akimbo_gun, /obj/item/gun.proc/process_fire, target, shooter, TRUE, params, null, bonus_spread), 1)
process_fire(target, shooter, TRUE, params, null, bonus_spread)
#undef AUTOFIRE_MOUSEUP
#undef AUTOFIRE_MOUSEDOWN