diff --git a/code/__DEFINES/combat_defines.dm b/code/__DEFINES/combat_defines.dm
index ee2ec5345af..01e0c8990c7 100644
--- a/code/__DEFINES/combat_defines.dm
+++ b/code/__DEFINES/combat_defines.dm
@@ -157,3 +157,10 @@
#define STATUS_EFFECT_CONSTANT * 20
#define IS_HORIZONTAL(x) x.body_position
+
+/// Compatible firemode is in the gun. Wait until it's held in the user hands.
+#define AUTOFIRE_STAT_IDLE (1<<0)
+/// Gun is active and in the user hands. Wait until user does a valid click.
+#define AUTOFIRE_STAT_ALERT (1<<1)
+/// Gun is shooting.
+#define AUTOFIRE_STAT_FIRING (1<<2)
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 05150f92552..585f6c5365f 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -219,8 +219,17 @@
/////////////////
-///from base of client/Click(): (atom/target, atom/location, control, params, mob/user)
+// /client signals
+
+/// from base of client/Click(): (atom/target, atom/location, control, params, mob/user)
#define COMSIG_CLIENT_CLICK "atom_client_click"
+/// from base of client/MouseDown(): (/client, object, location, control, params)
+#define COMSIG_CLIENT_MOUSEDOWN "client_mousedown"
+/// from base of client/MouseUp(): (/client, object, location, control, params)
+#define COMSIG_CLIENT_MOUSEUP "client_mouseup"
+ #define COMPONENT_CLIENT_MOUSEUP_INTERCEPT (1<<0)
+/// from base of client/MouseUp(): (/client, object, location, control, params)
+#define COMSIG_CLIENT_MOUSEDRAG "client_mousedrag"
///from base of area/Entered(): (/area)
#define COMSIG_ENTER_AREA "enter_area"
@@ -708,11 +717,16 @@
// /obj/item/gun signals
-///called in /obj/item/gun/process_fire (user, target, params, zone_override)
+/// called in /obj/item/gun/process_fire (user, target, params, zone_override)
#define COMSIG_MOB_FIRED_GUN "mob_fired_gun"
-
-///called in /obj/item/gun/process_fire (user, target)
+/// called in /obj/item/gun/process_fire (user, target)
#define COMSIG_GUN_FIRED "gun_fired"
+/// called in /datum/component/automatic_fire/proc/on_mouse_down: (client/clicker, atom/target, turf/location, control, params)
+#define COMSIG_AUTOFIRE_ONMOUSEDOWN "autofire_onmousedown"
+ #define COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS (1<<0)
+/// called in /datum/component/automatic_fire/proc/process_shot(): (atom/target, mob/living/shooter, allow_akimbo, params)
+#define COMSIG_AUTOFIRE_SHOT "autofire_shot"
+ #define COMPONENT_AUTOFIRE_SHOT_SUCCESS (1<<0)
// /obj/item/grenade signals
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 4f808a9ad0a..62d3df6514e 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -58,6 +58,10 @@
#define isclothing(A) (istype(A, /obj/item/clothing))
+#define isprojectile(A) (istype(A, /obj/item/projectile))
+
+#define isgun(A) (istype(A, /obj/item/gun))
+
#define is_pen(W) (istype(W, /obj/item/pen) || istype(W, /obj/item/flashlight/pen))
#define is_pda(W) (istype(W, /obj/item/pda))
diff --git a/code/__HELPERS/mob_helpers.dm b/code/__HELPERS/mob_helpers.dm
index 42a2c024560..cb4632129d9 100644
--- a/code/__HELPERS/mob_helpers.dm
+++ b/code/__HELPERS/mob_helpers.dm
@@ -609,7 +609,7 @@ GLOBAL_LIST_EMPTY(do_after_once_tracker)
var/viewX
var/viewY
if(isnum(view))
- var/totalviewrange = 1 + 2 * view
+ var/totalviewrange = (view < 0 ? -1 : 1) + 2 * view
viewX = totalviewrange
viewY = totalviewrange
else if(istext(view))
@@ -625,6 +625,12 @@ GLOBAL_LIST_EMPTY(do_after_once_tracker)
return list(viewX, viewY)
+/proc/in_view_range(mob/user, atom/A)
+ var/list/view_range = getviewsize(user.client.view)
+ var/turf/source = get_turf(user)
+ var/turf/target = get_turf(A)
+ return ISINRANGE(target.x, source.x - view_range[1], source.x + view_range[1]) && ISINRANGE(target.y, source.y - view_range[2], source.y + view_range[2])
+
//Used in chemical_mob_spawn. Generates a random mob based on a given gold_core_spawnable value.
/proc/create_random_mob(spawn_location, mob_class = HOSTILE_SPAWN)
var/static/list/mob_spawn_meancritters = list() // list of possible hostile mobs
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 579be0eb83a..34a232d4a13 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -1951,6 +1951,27 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
pixel_x = initialpixelx
pixel_y = initialpixely
+/// Returns a turf based on text inputs, original turf and viewing client
+/proc/parse_caught_click_modifiers(list/modifiers, turf/origin, client/viewing_client)
+ if(!modifiers)
+ return
+
+ var/screen_loc = splittext(modifiers["screen-loc"], ",")
+ var/list/actual_view = getviewsize(viewing_client ? viewing_client.view : world.view)
+ var/click_turf_x = splittext(screen_loc[1], ":")
+ var/click_turf_y = splittext(screen_loc[2], ":")
+ var/click_turf_z = origin.z
+
+ var/click_turf_px = text2num(click_turf_x[2])
+ var/click_turf_py = text2num(click_turf_y[2])
+ click_turf_x = origin.x + text2num(click_turf_x[1]) - round(actual_view[1] / 2) - 1
+ click_turf_y = origin.y + text2num(click_turf_y[1]) - round(actual_view[2] / 2) - 1
+
+ var/turf/click_turf = locate(clamp(click_turf_x, 1, world.maxx), clamp(click_turf_y, 1, world.maxy), click_turf_z)
+ LAZYSET(modifiers, "icon-x", "[(click_turf_px - click_turf.pixel_x) + ((click_turf_x - click_turf.x) * world.icon_size)]")
+ LAZYSET(modifiers, "icon-y", "[(click_turf_py - click_turf.pixel_y) + ((click_turf_y - click_turf.y) * world.icon_size)]")
+ return click_turf
+
/proc/params2turf(scr_loc, turf/origin, client/C)
if(!scr_loc)
return null
diff --git a/code/_onclick/drag_drop.dm b/code/_onclick/drag_drop.dm
index 84f87e5bfd5..9f40c657b95 100644
--- a/code/_onclick/drag_drop.dm
+++ b/code/_onclick/drag_drop.dm
@@ -47,4 +47,71 @@ actually if you do something in that proc like changing user location or whateve
to inform the game this action was expected and its fine
*/
/atom/proc/MouseDrop_T(atom/dropping, mob/user, params) // return TRUE if you want to prevent us click the object after it
- return
+ return FALSE
+
+/client/MouseDown(datum/object, location, control, params)
+ if(QDELETED(object)) // Yep, you can click on qdeleted things before they have time to nullspace. Fun.
+ return
+ SEND_SIGNAL(src, COMSIG_CLIENT_MOUSEDOWN, object, location, control, params)
+ if(mouse_down_icon)
+ mouse_pointer_icon = mouse_down_icon
+ var/delay = mob.CanMobAutoclick(object, location, params)
+ if(delay)
+ selected_target[1] = object
+ selected_target[2] = params
+ while(selected_target[1])
+ Click(selected_target[1], location, control, selected_target[2])
+ sleep(delay)
+
+/client/MouseUp(object, location, control, params)
+ if(SEND_SIGNAL(src, COMSIG_CLIENT_MOUSEUP, object, location, control, params) & COMPONENT_CLIENT_MOUSEUP_INTERCEPT)
+ click_intercept_time = world.time
+ if(mouse_up_icon)
+ mouse_pointer_icon = mouse_up_icon
+ selected_target[1] = null
+
+/mob/proc/CanMobAutoclick(object, location, params)
+ return FALSE
+
+/mob/living/carbon/CanMobAutoclick(atom/object, location, params)
+ if(!object.IsAutoclickable())
+ return FALSE
+ var/obj/item/active_item = get_active_hand()
+ return active_item?.CanItemAutoclick(object, location, params)
+
+/obj/item/proc/CanItemAutoclick(object, location, params)
+ return FALSE
+
+/atom/proc/IsAutoclickable()
+ return TRUE
+
+/obj/screen/IsAutoclickable()
+ return FALSE
+
+/obj/screen/click_catcher/IsAutoclickable()
+ return TRUE
+
+// The actual arguments of this:
+// MouseDrag(src_object as null|atom in usr.client,
+// over_object as null|atom in usr.client,
+// src_location as null|turf|text in usr.client,
+// over_location as null|turf|text in usr.client,
+// src_control as text, over_control as text, params as text)
+/client/MouseDrag(src_object, atom/over_object, src_location, turf/over_location, src_control, over_control, params)
+ var/list/modifiers = params2list(params)
+ if(!drag_start) // If we're just starting to drag
+ drag_start = world.time
+ drag_details = modifiers.Copy()
+ mouseParams = params
+ mouse_location_UID = isturf(over_location) ? over_location.UID() : over_location
+ mouse_object_UID = over_object?.UID()
+ if(selected_target[1] && over_object?.IsAutoclickable())
+ selected_target[1] = over_object
+ selected_target[2] = params
+ SEND_SIGNAL(src, COMSIG_CLIENT_MOUSEDRAG, src_object, over_object, src_location, over_location, src_control, over_control, params)
+ return ..()
+
+/client/MouseDrop(atom/src_object, atom/over_object, atom/src_location, atom/over_location, src_control, over_control, params)
+ ..()
+ drag_start = 0
+ drag_details = null
diff --git a/code/datums/components/fullauto.dm b/code/datums/components/fullauto.dm
new file mode 100644
index 00000000000..cfd4da69ce1
--- /dev/null
+++ b/code/datums/components/fullauto.dm
@@ -0,0 +1,325 @@
+#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
+ /// Time between individual shots.
+ var/autofire_shot_delay = 0.3 SECONDS
+ /// 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/mouse_status = AUTOFIRE_MOUSEUP
+ /// Should dual wielding be allowed?
+ var/allow_akimbo
+
+ /// windup autofire vars
+ /// Whether the delay between shots increases over time, simulating a spooling weapon
+ var/windup_autofire = FALSE
+ /// the reduction to shot delay for windup
+ var/current_windup_reduction = 0
+ /// the percentage of autfire_shot_delay that is added to current_windup_reduction
+ var/windup_autofire_reduction_multiplier = 0.3
+ /// How high of a reduction that current_windup_reduction can reach
+ var/windup_autofire_cap = 0.3
+ /// How long it takes for weapons that have spooled-up to reset back to the original firing speed
+ var/windup_spindown = 3 SECONDS
+ /// Timer for tracking the spindown reset timings
+ var/timerid
+ COOLDOWN_DECLARE(next_shot_cd)
+
+/datum/component/automatic_fire/Initialize(autofire_shot_delay, windup_autofire, windup_autofire_reduction_multiplier, windup_autofire_cap, windup_spindown, allow_akimbo = TRUE)
+ . = ..()
+ if(!isgun(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/obj/item/gun = parent
+ RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(wake_up))
+ if(autofire_shot_delay)
+ src.autofire_shot_delay = autofire_shot_delay
+ src.allow_akimbo = allow_akimbo
+ if(windup_autofire)
+ src.windup_autofire = windup_autofire
+ src.windup_autofire_reduction_multiplier = windup_autofire_reduction_multiplier
+ src.windup_autofire_cap = windup_autofire_cap
+ src.windup_spindown = windup_spindown
+ 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(seconds_per_tick)
+ 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 // COMSIG_ITEM_EQUIPPED
+
+ 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(user.is_holding(parent))
+ autofire_on(user.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/user_client)
+ if(autofire_stat != AUTOFIRE_STAT_IDLE)
+ return
+ autofire_stat = AUTOFIRE_STAT_ALERT
+ if(!QDELETED(user_client))
+ clicker = user_client
+ shooter = user_client.mob
+ RegisterSignal(user_client, COMSIG_CLIENT_MOUSEDOWN, PROC_REF(on_mouse_down))
+ if(!QDELETED(shooter))
+ RegisterSignal(shooter, COMSIG_MOB_LOGOUT, PROC_REF(autofire_off))
+ UnregisterSignal(shooter, COMSIG_MOB_LOGIN)
+ RegisterSignal(parent, list(COMSIG_PARENT_QDELETING, COMSIG_ITEM_DROPPED), PROC_REF(autofire_off))
+ parent.RegisterSignal(src, COMSIG_AUTOFIRE_ONMOUSEDOWN, TYPE_PROC_REF(/obj/item/gun, autofire_bypass_check))
+ parent.RegisterSignal(parent, COMSIG_AUTOFIRE_SHOT, TYPE_PROC_REF(/obj/item/gun, do_autofire))
+
+/datum/component/automatic_fire/proc/autofire_off(datum/source)
+ SIGNAL_HANDLER // COMSIG_MOB_LOGOUT
+
+ 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_REF(on_client_login))
+ UnregisterSignal(shooter, COMSIG_MOB_LOGOUT)
+ UnregisterSignal(parent, list(COMSIG_PARENT_QDELETING, 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 // COMSIG_MOB_LOGIN
+ 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 // COMSIG_CLIENT_MOUSEDOWN
+
+ var/list/modifiers = params2list(params) // If they're shift+clicking, for example, let's not have them accidentally shoot.
+
+ if(modifiers["shift"])
+ return
+ if(modifiers["ctrl"])
+ return
+ if(modifiers["middle"])
+ return
+ if(modifiers["right"])
+ return
+ if(modifiers["alt"])
+ return
+ if(source.mob.in_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, /obj/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_REF(start_autofiring))
+
+/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_REF(on_mouse_up))
+ mouse_status = AUTOFIRE_MOUSEDOWN
+
+ RegisterSignal(shooter, COMSIG_MOB_SWAPPED_HANDS, PROC_REF(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_REF(on_mouse_drag))
+
+/datum/component/automatic_fire/proc/on_mouse_up(datum/source, atom/object, turf/location, control, params)
+ SIGNAL_HANDLER // COMSIG_CLIENT_MOUSEUP
+
+ 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 // COMSIG_MOB_SWAPPED_HANDS
+
+ 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_SWAPPED_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 // COMSIG_CLIENT_MOUSEDRAG
+
+ 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(windup_autofire)
+ next_delay = clamp(next_delay - current_windup_reduction, round(autofire_shot_delay * windup_autofire_cap), autofire_shot_delay)
+ current_windup_reduction = (current_windup_reduction + round(autofire_shot_delay * windup_autofire_reduction_multiplier))
+ timerid = addtimer(CALLBACK(src, PROC_REF(windup_reset), FALSE), windup_spindown, TIMER_UNIQUE|TIMER_OVERRIDE|TIMER_STOPPABLE)
+
+ COOLDOWN_START(src, next_shot_cd, next_delay)
+
+ if(SEND_SIGNAL(parent, COMSIG_AUTOFIRE_SHOT, target, shooter, allow_akimbo, mouse_parameters) & COMPONENT_AUTOFIRE_SHOT_SUCCESS)
+ return TRUE
+
+ stop_autofiring()
+ return FALSE
+
+/// Reset for our windup, resetting everything back to initial values after a variable set amount of time (determined by var/windup_spindown).
+/datum/component/automatic_fire/proc/windup_reset(deltimer)
+ current_windup_reduction = initial(current_windup_reduction)
+ if(deltimer && timerid)
+ deltimer(timerid)
+
+// 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/other_hand = shooter.get_organ(shooter.hand ? BODY_ZONE_PRECISE_R_HAND : BODY_ZONE_PRECISE_L_HAND)
+ if(weapon_weight == WEAPON_HEAVY && (shooter.get_inactive_hand() || !other_hand))
+ to_chat(shooter, "You need to use both hands!")
+ return FALSE
+
+ return TRUE
+
+/obj/item/gun/proc/autofire_bypass_check(datum/source, client/clicker, atom/target, turf/location, control, params)
+ SIGNAL_HANDLER // COMSIG_AUTOFIRE_ONMOUSEDOWN
+
+ if(clicker.mob.get_active_hand() != src)
+ return COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS
+
+/obj/item/gun/proc/do_autofire(datum/source, atom/target, mob/living/shooter, allow_akimbo, params)
+ SIGNAL_HANDLER // COMSIG_AUTOFIRE_SHOT
+
+ if(semicd || shooter.incapacitated())
+ return NONE
+
+ if(!can_shoot())
+ shoot_with_empty_chamber(shooter)
+ return NONE
+
+ INVOKE_ASYNC(src, PROC_REF(do_autofire_shot), source, target, shooter, allow_akimbo, 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, allow_akimbo, params)
+ var/obj/item/gun/akimbo_gun = shooter.get_inactive_hand()
+ var/bonus_spread = 0
+ if(isgun(akimbo_gun) && weapon_weight < WEAPON_MEDIUM && allow_akimbo)
+ if(akimbo_gun.weapon_weight < WEAPON_MEDIUM && akimbo_gun.can_trigger_gun(shooter))
+ bonus_spread = dual_wield_spread
+ addtimer(CALLBACK(akimbo_gun, TYPE_PROC_REF(/obj/item/gun, 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
diff --git a/code/datums/components/shielded.dm b/code/datums/components/shielded.dm
index effad26c8fe..adbe78c3b7f 100644
--- a/code/datums/components/shielded.dm
+++ b/code/datums/components/shielded.dm
@@ -158,7 +158,7 @@
var/charge_loss = 1 // how many charges do we lose
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
if(P.shield_buster)
charge_loss = 3
diff --git a/code/datums/components/squeak.dm b/code/datums/components/squeak.dm
index 7445e577605..c2aef66bcaa 100644
--- a/code/datums/components/squeak.dm
+++ b/code/datums/components/squeak.dm
@@ -76,7 +76,7 @@
var/obj/item/I = AM
if(I.flags & ABSTRACT)
return
- else if(istype(AM, /obj/item/projectile))
+ else if(isprojectile(AM))
var/obj/item/projectile/P = AM
if(P.original != parent)
return
diff --git a/code/game/gamemodes/cult/blood_magic.dm b/code/game/gamemodes/cult/blood_magic.dm
index 30370c7d905..5a62abbd8c1 100644
--- a/code/game/gamemodes/cult/blood_magic.dm
+++ b/code/game/gamemodes/cult/blood_magic.dm
@@ -546,7 +546,7 @@
var/input_rune_key = tgui_input_list(user, "Choose a rune to teleport to", "Rune to Teleport to", potential_runes) //we know what key they picked
var/obj/effect/rune/teleport/actual_selected_rune = potential_runes[input_rune_key] //what rune does that key correspond to?
- if(QDELETED(src) || !user || user.l_hand != src && user.r_hand != src || user.incapacitated() || !actual_selected_rune)
+ if(QDELETED(src) || !user || !user.is_holding(src) || user.incapacitated() || !actual_selected_rune)
return
if(HAS_TRAIT(user, TRAIT_FLOORED))
diff --git a/code/game/gamemodes/cult/cult_items.dm b/code/game/gamemodes/cult/cult_items.dm
index f72ed0d0b15..f8a3ca79d77 100644
--- a/code/game/gamemodes/cult/cult_items.dm
+++ b/code/game/gamemodes/cult/cult_items.dm
@@ -466,7 +466,7 @@
if(IS_CULTIST(owner) && !owner.holy_check()) // Cultist holding the shield
// Hit by a projectile
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
var/shatter_chance = 0 // Percent chance of the shield shattering on a projectile hit
var/threshold // Depends on the damage Type (Brute or Burn)
diff --git a/code/game/machinery/deployable.dm b/code/game/machinery/deployable.dm
index f3e3025c7ab..63b1325a260 100644
--- a/code/game/machinery/deployable.dm
+++ b/code/game/machinery/deployable.dm
@@ -56,7 +56,7 @@
/obj/structure/barricade/CanPass(atom/movable/mover, turf/target)//So bullets will fly over and stuff.
if(locate(/obj/structure/barricade) in get_turf(mover))
return TRUE
- else if(istype(mover, /obj/item/projectile))
+ else if(isprojectile(mover))
if(!anchored)
return TRUE
var/obj/item/projectile/proj = mover
@@ -449,7 +449,7 @@
/obj/structure/barricade/dropwall/firewall/Crossed(atom/movable/AM, oldloc)
. = ..()
- if(!istype(AM, /obj/item/projectile))
+ if(!isprojectile(AM))
return
var/obj/item/projectile/P = AM
P.immolate ++
diff --git a/code/game/machinery/shieldgen.dm b/code/game/machinery/shieldgen.dm
index 3ad64115567..4010ba26453 100644
--- a/code/game/machinery/shieldgen.dm
+++ b/code/game/machinery/shieldgen.dm
@@ -547,7 +547,7 @@
if(istype(mover) && mover.checkpass(PASSGLASS))
return prob(20)
else
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return prob(10)
else
return !density
@@ -563,7 +563,7 @@
var/mob/living/M = mover
if("syndicate" in M.faction)
return TRUE
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return FALSE
return ..(mover, target, height)
diff --git a/code/game/machinery/transformer.dm b/code/game/machinery/transformer.dm
index b72b6a7b714..1b84d8faef3 100644
--- a/code/game/machinery/transformer.dm
+++ b/code/game/machinery/transformer.dm
@@ -223,7 +223,7 @@
sleep(30)
/obj/machinery/transformer/xray/proc/scan_rec(obj/item/I)
- if(istype(I, /obj/item/gun))
+ if(isgun(I))
return TRUE
if(istype(I, /obj/item/transfer_valve))
return TRUE
diff --git a/code/game/objects/effects/spiders.dm b/code/game/objects/effects/spiders.dm
index 08703285cc2..3005942594c 100644
--- a/code/game/objects/effects/spiders.dm
+++ b/code/game/objects/effects/spiders.dm
@@ -47,7 +47,7 @@
if(prob(50))
to_chat(mover, "You get stuck in [src] for a moment.")
return FALSE
- else if(istype(mover, /obj/item/projectile))
+ else if(isprojectile(mover))
return prob(30)
return TRUE
diff --git a/code/game/objects/items/devices/taperecorder.dm b/code/game/objects/items/devices/taperecorder.dm
index 2e8f73059fd..463a69a18d3 100644
--- a/code/game/objects/items/devices/taperecorder.dm
+++ b/code/game/objects/items/devices/taperecorder.dm
@@ -75,7 +75,7 @@
/obj/item/taperecorder/attack_hand(mob/user)
if(loc == user)
if(mytape)
- if(user.l_hand != src && user.r_hand != src)
+ if(!user.is_holding(src))
..()
return
eject(user)
diff --git a/code/game/objects/items/his_grace.dm b/code/game/objects/items/his_grace.dm
index 7766452893d..12d5a908dce 100644
--- a/code/game/objects/items/his_grace.dm
+++ b/code/game/objects/items/his_grace.dm
@@ -110,7 +110,7 @@
go_rabid()
return
- if(!(src in list(master.l_hand, master.r_hand)))
+ if(!master.is_holding(src))
go_rabid()
return
diff --git a/code/game/objects/items/sport.dm b/code/game/objects/items/sport.dm
index a0fe0d7f43b..862cdeecce2 100644
--- a/code/game/objects/items/sport.dm
+++ b/code/game/objects/items/sport.dm
@@ -46,7 +46,7 @@
. = ..()
var/mob/living/carbon/human/M = hit_atom
if(ishuman(hit_atom) && (M.wear_suit?.type in suit_types))
- if(M.r_hand == src || M.l_hand == src)
+ if(M.is_holding(src))
return
playsound(src, 'sound/items/dodgeball.ogg', 50, 1)
M.KnockDown(6 SECONDS)
@@ -86,7 +86,7 @@
return
/obj/structure/holohoop/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
- if(isitem(AM) && !istype(AM, /obj/item/projectile))
+ if(isitem(AM) && !isprojectile(AM))
if(prob(50) || HAS_TRAIT(throwingdatum.thrower, TRAIT_BADASS))
AM.forceMove(get_turf(src))
visible_message("Swish! [AM] lands in [src].")
diff --git a/code/game/objects/items/weapons/flamethrower.dm b/code/game/objects/items/weapons/flamethrower.dm
index a16963b648a..67bc3f4994e 100644
--- a/code/game/objects/items/weapons/flamethrower.dm
+++ b/code/game/objects/items/weapons/flamethrower.dm
@@ -42,7 +42,7 @@
var/turf/location = loc
if(ismob(location))
var/mob/M = location
- if(M.l_hand == src || M.r_hand == src)
+ if(M.is_holding(src))
location = M.loc
if(isturf(location)) //start a fire if possible
igniter.flamethrower_process(location)
diff --git a/code/game/objects/items/weapons/holy_weapons.dm b/code/game/objects/items/weapons/holy_weapons.dm
index 0cf30e2db5e..a117746c570 100644
--- a/code/game/objects/items/weapons/holy_weapons.dm
+++ b/code/game/objects/items/weapons/holy_weapons.dm
@@ -360,7 +360,7 @@
if(ishuman(loc))
var/mob/living/carbon/human/our_location = loc
if(istype(our_location))
- if(src != our_location.l_hand && src != our_location.r_hand)
+ if(!our_location.is_holding(src))
return
if(our_location.Adjacent(attacking_atom)) // with a buddy we deal 12 damage :D
our_location.do_attack_animation(attacking_atom, used_item = src)
diff --git a/code/game/objects/items/weapons/melee/energy_melee_weapons.dm b/code/game/objects/items/weapons/melee/energy_melee_weapons.dm
index 22acf04add8..0313f5ae46a 100644
--- a/code/game/objects/items/weapons/melee/energy_melee_weapons.dm
+++ b/code/game/objects/items/weapons/melee/energy_melee_weapons.dm
@@ -240,7 +240,7 @@
. = ..()
if(!.) // they did not block the attack
return
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
if(P.reflectability == REFLECTABILITY_NEVER) //only 1 magic spell does this, but hey, needed
owner.visible_message("[owner] blocks [attack_text] with [src]!")
diff --git a/code/game/objects/items/weapons/shields.dm b/code/game/objects/items/weapons/shields.dm
index e2d1b0fe3c7..398b6c4d4ed 100644
--- a/code/game/objects/items/weapons/shields.dm
+++ b/code/game/objects/items/weapons/shields.dm
@@ -86,7 +86,7 @@
return
/obj/item/shield/energy/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
if(P.shield_buster && active)
toggle(owner, TRUE)
diff --git a/code/game/objects/items/weapons/storage/briefcase.dm b/code/game/objects/items/weapons/storage/briefcase.dm
index ed9a96a2b93..77cc3c0f361 100644
--- a/code/game/objects/items/weapons/storage/briefcase.dm
+++ b/code/game/objects/items/weapons/storage/briefcase.dm
@@ -43,7 +43,7 @@
/obj/item/storage/briefcase/false_bottomed/afterattack(atom/A, mob/user, flag, params)
..()
- if(stored_item && istype(stored_item, /obj/item/gun) && !Adjacent(A))
+ if(stored_item && isgun(stored_item) && !Adjacent(A))
var/obj/item/gun/stored_gun = stored_item
stored_gun.afterattack(A, user, flag, params)
diff --git a/code/game/objects/structures/fence.dm b/code/game/objects/structures/fence.dm
index e544227d383..1a2535814c0 100644
--- a/code/game/objects/structures/fence.dm
+++ b/code/game/objects/structures/fence.dm
@@ -61,7 +61,7 @@
/obj/structure/fence/CanPass(atom/movable/mover, turf/target)
if(istype(mover) && mover.checkpass(PASSFENCE))
return TRUE
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return TRUE
if(!density)
return TRUE
diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm
index 6585976999e..1c0bc9cd224 100644
--- a/code/game/objects/structures/girders.dm
+++ b/code/game/objects/structures/girders.dm
@@ -410,7 +410,7 @@
if(istype(mover) && mover.checkpass(PASSGRILLE))
return prob(girderpasschance)
else
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return prob(girderpasschance)
else
return 0
diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm
index fbc538af4a1..4097219ef38 100644
--- a/code/game/objects/structures/grille.dm
+++ b/code/game/objects/structures/grille.dm
@@ -123,7 +123,7 @@
return TRUE
if(istype(mover) && mover.checkpass(PASSGRILLE))
return TRUE
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return (prob(30) || !density)
/obj/structure/grille/CanPathfindPass(obj/item/card/id/ID, dir, caller, no_id = FALSE)
diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm
index ff67ffde85a..5acfde9058a 100644
--- a/code/game/objects/structures/railings.dm
+++ b/code/game/objects/structures/railings.dm
@@ -102,7 +102,7 @@
/obj/structure/railing/CanPass(atom/movable/mover, turf/target)
if(istype(mover) && mover.checkpass(PASSFENCE))
return TRUE
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return TRUE
if(ismob(mover))
var/mob/living/M = mover
@@ -130,7 +130,7 @@
var/mob/living/M = O
if(istype(O) && O.checkpass(PASSFENCE))
return TRUE
- if(istype(O, /obj/item/projectile))
+ if(isprojectile(O))
return TRUE
if(istype(M))
if(M.flying || M.floating || (IS_HORIZONTAL(M) && HAS_TRAIT(M, TRAIT_CONTORTED_BODY)))
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index aa06fa2055c..d47f82b5e02 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -62,6 +62,9 @@
var/datum/click_intercept/click_intercept = null
+ /// Time when the click was intercepted
+ var/click_intercept_time = 0
+
//datum that controls the displaying and hiding of tooltips
var/datum/tooltip/tooltips
@@ -132,6 +135,27 @@
var/list/active_keybindings = list()
/// The client's movement keybindings to directions, which work regardless of modifiers.
var/list/movement_kb_dirs = list()
+
+ /// Used to make a special mouse cursor, this one for mouse up icon
+ var/mouse_up_icon = null
+ /// Used to make a special mouse cursor, this one for mouse up icon
+ var/mouse_down_icon = null
+ /// Used to override the mouse cursor so it doesnt get reset
+ var/mouse_override_icon = null
+
+ /// Autoclick list of two elements, first being the clicked thing, second being the parameters.
+ var/list/atom/selected_target[2]
+ /// Used in MouseDrag to preserve the original mouse click parameters
+ var/mouseParams = ""
+ /// Used in MouseDrag to preserve the last mouse-entered location.
+ var/mouse_location_UID
+ /// Used in MouseDrag to preserve the last mouse-entered object.
+ var/mouse_object_UID
+ /// When we started the currently active drag
+ var/drag_start = 0
+ /// The params we passed at the start of the drag, in list form
+ var/list/drag_details
+
/// The client's currently moused over datum, limited to movable and stored as UID
var/atom/movable/moused_over
diff --git a/code/modules/clothing/head/misc_special.dm b/code/modules/clothing/head/misc_special.dm
index f21c1b3fbc3..9cc9981ea28 100644
--- a/code/modules/clothing/head/misc_special.dm
+++ b/code/modules/clothing/head/misc_special.dm
@@ -75,7 +75,7 @@
var/turf/location = loc
if(ismob(location))
var/mob/living/carbon/human/M = location
- if(M.l_hand == src || M.r_hand == src || M.head == src)
+ if(M.is_holding(src) || M.head == src)
location = M.loc
if(isturf(location))
diff --git a/code/modules/clothing/suits/armor_suits.dm b/code/modules/clothing/suits/armor_suits.dm
index 9a5dda61757..f57c9fecf16 100644
--- a/code/modules/clothing/suits/armor_suits.dm
+++ b/code/modules/clothing/suits/armor_suits.dm
@@ -476,7 +476,7 @@
/obj/item/clothing/suit/armor/reactive/proc/reaction_check(hitby)
if(prob(hit_reaction_chance))
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
if(istype(P, /obj/item/projectile/ion))
return FALSE
diff --git a/code/modules/clothing/under/accessories/holster.dm b/code/modules/clothing/under/accessories/holster.dm
index ac798acda90..e31e66f186a 100644
--- a/code/modules/clothing/under/accessories/holster.dm
+++ b/code/modules/clothing/under/accessories/holster.dm
@@ -36,7 +36,7 @@
to_chat(user, "There is already a [holstered] holstered here!")
return
- if(!istype(I, /obj/item/gun))
+ if(!isgun(I))
to_chat(user, "Only guns can be holstered!")
return
diff --git a/code/modules/food_and_drinks/drinks/drinks/cans.dm b/code/modules/food_and_drinks/drinks/drinks/cans.dm
index c98487ba69c..e797c5ef7b6 100644
--- a/code/modules/food_and_drinks/drinks/drinks/cans.dm
+++ b/code/modules/food_and_drinks/drinks/drinks/cans.dm
@@ -49,7 +49,7 @@
if(can_opened)
to_chat(H, "You can't shake up an already opened drink!")
return
- if(src == H.l_hand || src == H.r_hand)
+ if(H.is_holding(src))
can_shake = FALSE
addtimer(CALLBACK(src, PROC_REF(reset_shakable)), 1 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE)
to_chat(H, "You start shaking up [src].")
diff --git a/code/modules/mining/lavaland/loot/tendril_loot.dm b/code/modules/mining/lavaland/loot/tendril_loot.dm
index fc5a2570944..2055b5e736a 100644
--- a/code/modules/mining/lavaland/loot/tendril_loot.dm
+++ b/code/modules/mining/lavaland/loot/tendril_loot.dm
@@ -57,7 +57,7 @@
/obj/item/shared_storage/attack_self(mob/living/carbon/user)
if(!iscarbon(user))
return
- if(src == user.l_hand || src == user.r_hand)
+ if(user.is_holding(src))
bag?.open(user)
else
..()
diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm
index bf9bfc53edf..65386ffc6de 100644
--- a/code/modules/mining/lavaland/necropolis_chests.dm
+++ b/code/modules/mining/lavaland/necropolis_chests.dm
@@ -223,7 +223,7 @@
/obj/item/clothing/head/hooded/berserker/hit_reaction(mob/living/carbon/human/owner, atom/movable/hitby, attack_text = "the attack", final_block_chance = 0, damage = 0, attack_type = MELEE_ATTACK)
if(berserk_active)
return
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
if(P.damage_type == STAMINA)
return //no disabler rage
diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm
index 6938a3e3451..59786362d25 100644
--- a/code/modules/mob/living/carbon/human/human_mob.dm
+++ b/code/modules/mob/living/carbon/human/human_mob.dm
@@ -1448,8 +1448,7 @@ Eyes need to have significantly high darksight to shine unless the mob has the X
/mob/living/carbon/human/singularity_pull(S, current_size)
..()
if(current_size >= STAGE_THREE)
- var/list/handlist = list(l_hand, r_hand)
- for(var/obj/item/hand in handlist)
+ for(var/obj/item/hand in get_both_hands(src))
if(prob(current_size * 5) && hand.w_class >= ((11-current_size)/2) && unEquip(hand))
step_towards(hand, src)
to_chat(src, "\The [S] pulls \the [hand] from your grip!")
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
index 7e98435c073..f59b35b46e2 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/hierophant.dm
@@ -605,7 +605,7 @@ Difficulty: Hard
return FALSE
if(mover == caster.pulledby)
return TRUE
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
var/obj/item/projectile/P = mover
if(P.firer == caster)
return TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/terror_spiders/actions.dm b/code/modules/mob/living/simple_animal/hostile/terror_spiders/actions.dm
index 4d4e048ce32..5c1a666f9b1 100644
--- a/code/modules/mob/living/simple_animal/hostile/terror_spiders/actions.dm
+++ b/code/modules/mob/living/simple_animal/hostile/terror_spiders/actions.dm
@@ -181,7 +181,7 @@
if(!(M.mobility_flags & MOBILITY_MOVE))
return TRUE
return prob(80)
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return prob(20)
return ..()
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index f7f5e5a38c8..2f7ac040189 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -6,7 +6,7 @@
if(height==0)
return 1
- if(istype(mover, /obj/item/projectile))
+ if(isprojectile(mover))
return projectile_hit_check(mover)
if(mover.throwing)
return (!density || horizontal || (mover.throwing.thrower == src))
diff --git a/code/modules/power/engines/singularity/singularity.dm b/code/modules/power/engines/singularity/singularity.dm
index 3fcf4600a74..f9da157db0d 100644
--- a/code/modules/power/engines/singularity/singularity.dm
+++ b/code/modules/power/engines/singularity/singularity.dm
@@ -519,7 +519,7 @@
/obj/effect/abstract/proximity_checker/singulo/Crossed(atom/movable/AM, oldloc)
. = ..()
- if(!istype(AM, /obj/item/projectile))
+ if(!isprojectile(AM))
return
var/obj/item/projectile/P = AM
var/distance = distance_to_singulo
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 0e33fbc6ef4..b5365256fe8 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -35,6 +35,8 @@
var/semicd = 0 //cooldown handler
var/execution_speed = 6 SECONDS
var/weapon_weight = WEAPON_LIGHT
+ /// Additional spread when dual wielding.
+ var/dual_wield_spread = 24
var/list/restricted_species
var/spread = 0
@@ -201,7 +203,7 @@
continue
else if(G.can_trigger_gun(user))
if(!HAS_TRAIT(user, TRAIT_BADASS))
- bonus_spread += 24 * G.weapon_weight
+ bonus_spread += dual_wield_spread * G.weapon_weight
loop_counter++
addtimer(CALLBACK(G, PROC_REF(process_fire), target, user, 1, params, null, bonus_spread), loop_counter)
diff --git a/code/modules/projectiles/guns/energy_guns.dm b/code/modules/projectiles/guns/energy_guns.dm
index 67f2d45398c..5980eaad8ec 100644
--- a/code/modules/projectiles/guns/energy_guns.dm
+++ b/code/modules/projectiles/guns/energy_guns.dm
@@ -200,7 +200,7 @@
if(can_shoot())
user.visible_message("[user] is putting the barrel of [src] in [user.p_their()] mouth. It looks like [user.p_theyre()] trying to commit suicide!")
sleep(25)
- if(user.l_hand == src || user.r_hand == src)
+ if(user.is_holding(src))
user.visible_message("[user] melts [user.p_their()] face off with [src]!")
playsound(loc, fire_sound, 50, 1, -1)
var/obj/item/ammo_casing/energy/shot = ammo_type[select]
diff --git a/code/modules/projectiles/guns/projectile_gun.dm b/code/modules/projectiles/guns/projectile_gun.dm
index 37d17ed38d4..25dc5282cb4 100644
--- a/code/modules/projectiles/guns/projectile_gun.dm
+++ b/code/modules/projectiles/guns/projectile_gun.dm
@@ -148,7 +148,7 @@
if(loc == user)
if(suppressed && can_unsuppress)
var/obj/item/suppressor/S = suppressed
- if(user.l_hand != src && user.r_hand != src)
+ if(!user.is_holding(src))
..()
return
to_chat(user, "You unscrew [suppressed] from [src].")
@@ -198,7 +198,7 @@
if(chambered && chambered.BB && !chambered.BB.nodamage)
user.visible_message("[user] is putting the barrel of [src] in [user.p_their()] mouth. It looks like [user.p_theyre()] trying to commit suicide!")
sleep(25)
- if(user.l_hand == src || user.r_hand == src)
+ if(user.is_holding(src))
process_fire(user, user, 0, zone_override = "head")
user.visible_message("[user] blows [user.p_their()] brains out with [src]!")
return BRUTELOSS
diff --git a/code/modules/projectiles/projectile/magic_projectiles.dm b/code/modules/projectiles/projectile/magic_projectiles.dm
index 1d21a315982..8ccc59ffc30 100644
--- a/code/modules/projectiles/projectile/magic_projectiles.dm
+++ b/code/modules/projectiles/projectile/magic_projectiles.dm
@@ -357,7 +357,7 @@ GLOBAL_LIST_INIT(wabbajack_docile_animals, list(
qdel(src)
else
var/obj/O = change
- if(istype(O, /obj/item/gun))
+ if(isgun(O))
new /mob/living/simple_animal/hostile/mimic/copy/ranged(O.loc, O, firer)
else
new /mob/living/simple_animal/hostile/mimic/copy(O.loc, O, firer)
diff --git a/code/modules/recycling/disposal.dm b/code/modules/recycling/disposal.dm
index 38b67f3c086..f273631c1f5 100644
--- a/code/modules/recycling/disposal.dm
+++ b/code/modules/recycling/disposal.dm
@@ -524,7 +524,7 @@
/obj/machinery/disposal/CanPass(atom/movable/mover, turf/target, height=0)
if(isitem(mover) && mover.throwing)
var/obj/item/I = mover
- if(istype(I, /obj/item/projectile))
+ if(isprojectile(I))
return
if(prob(75) || (istype(mover.throwing.thrower) && (HAS_TRAIT(mover.throwing.thrower, TRAIT_BADASS) || HAS_TRAIT(mover.throwing.thrower, TRAIT_NEVER_MISSES_DISPOSALS))))
I.forceMove(src)
diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm
index a1258bf72d9..8fae073f154 100644
--- a/code/modules/recycling/sortingmachinery.dm
+++ b/code/modules/recycling/sortingmachinery.dm
@@ -285,7 +285,7 @@
return
/obj/machinery/disposal/deliveryChute/Bumped(atom/movable/AM) //Go straight into the chute
- if(istype(AM, /obj/item/projectile) || isAI(AM) || QDELETED(AM))
+ if(isprojectile(AM) || isAI(AM) || QDELETED(AM))
return
switch(dir)
if(NORTH)
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index fde9a420793..7ba2fec07b2 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -539,7 +539,7 @@
H.AIStatus = AI_OFF
H.LoseTarget()
stopped_atoms |= M
- else if(istype(A, /obj/item/projectile))
+ else if(isprojectile(A))
var/obj/item/projectile/P = A
P.paused = TRUE
stopped_atoms |= P
diff --git a/code/modules/surgery/organs/augments_arms.dm b/code/modules/surgery/organs/augments_arms.dm
index 0088aaa7fdc..2c7806b9dce 100644
--- a/code/modules/surgery/organs/augments_arms.dm
+++ b/code/modules/surgery/organs/augments_arms.dm
@@ -690,7 +690,7 @@
set_light(3)
addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, set_light), 0), 0.25 SECONDS)
- if(istype(hitby, /obj/item/projectile))
+ if(isprojectile(hitby))
var/obj/item/projectile/P = hitby
if(P.shield_buster || istype(P, /obj/item/projectile/ion)) //EMP's and unpariable attacks, after all.
return FALSE
diff --git a/icons/effects/mouse_pointers/weapon_pointer.dmi b/icons/effects/mouse_pointers/weapon_pointer.dmi
new file mode 100644
index 00000000000..b5070062c0b
Binary files /dev/null and b/icons/effects/mouse_pointers/weapon_pointer.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 395bb3eb17f..912489efe04 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -406,6 +406,7 @@
#include "code\datums\components\edit_complainer.dm"
#include "code\datums\components\emissive_blocker.dm"
#include "code\datums\components\footstep.dm"
+#include "code\datums\components\fullauto.dm"
#include "code\datums\components\label.dm"
#include "code\datums\components\largeobjecttransparency.dm"
#include "code\datums\components\material_container.dm"