diff --git a/code/__DEFINES/combat.dm b/code/__DEFINES/combat.dm
index a6d411a8dd..6dae40ce07 100644
--- a/code/__DEFINES/combat.dm
+++ b/code/__DEFINES/combat.dm
@@ -206,6 +206,19 @@
///Time to spend without clicking on other things required for your shots to become accurate.
#define GUN_AIMING_TIME (2 SECONDS)
+//Autofire component
+/// 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)
+
+#define COMSIG_AUTOFIRE_ONMOUSEDOWN "autofire_onmousedown"
+ #define COMPONENT_AUTOFIRE_ONMOUSEDOWN_BYPASS (1<<0)
+#define COMSIG_AUTOFIRE_SHOT "autofire_shot"
+ #define COMPONENT_AUTOFIRE_SHOT_SUCCESS (1<<0)
+
//Object/Item sharpness
#define SHARP_NONE 0
#define SHARP_EDGED 1
@@ -261,3 +274,4 @@
* a "inefficiently" prefix will be added to the message.
*/
#define FEEBLE_ATTACK_MSG_THRESHOLD 0.5
+
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 9de0dbe7c5..7a54b8482d 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -321,6 +321,12 @@
///from base of mob/AltClickOn(): (atom/A)
#define COMSIG_MOB_ALTCLICKON "mob_altclickon"
+//Gun signals
+///When a gun is switched to automatic fire mode
+#define COMSIG_GUN_AUTOFIRE_SELECTED "gun_autofire_selected"
+///When a gun is switched off of automatic fire mode
+#define COMSIG_GUN_AUTOFIRE_DESELECTED "gun_autofire_deselected"
+
// Lighting:
///from base of [atom/proc/set_light]: (l_range, l_power, l_color, l_on)
#define COMSIG_ATOM_SET_LIGHT "atom_set_light"
@@ -348,12 +354,22 @@
#define COMSIG_ATOM_UPDATE_LIGHT_FLAGS "atom_update_light_flags"
// /client signals
-#define COMSIG_MOB_CLIENT_LOGIN "mob_client_login" //sent when a mob/login() finishes: (client)
#define COMSIG_MOB_CLIENT_LOGOUT "mob_client_logout" //sent when a mob/logout() starts: (client)
#define COMSIG_MOB_CLIENT_MOVE "mob_client_move" //sent when client/Move() finishes with no early returns: (client, direction, n, oldloc)
#define COMSIG_MOB_CLIENT_CHANGE_VIEW "mob_client_change_view" //from base of /client/change_view(): (client, old_view, view)
#define COMSIG_MOB_CLIENT_MOUSEMOVE "mob_client_mousemove" //from base of /client/MouseMove(): (object, location, control, params)
+///sent when a mob/login() finishes: (client)
+#define COMSIG_MOB_CLIENT_LOGIN "comsig_mob_client_login"
+//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"
+
+
// /mob/living signals
#define COMSIG_LIVING_REGENERATE_LIMBS "living_regenerate_limbs" //from base of /mob/living/regenerate_limbs(): (noheal, excluded_limbs)
#define COMSIG_LIVING_RESIST "living_resist" //from base of mob/living/resist() (/mob/living)
diff --git a/code/__DEFINES/gun.dm b/code/__DEFINES/gun.dm
new file mode 100644
index 0000000000..05ce5118a3
--- /dev/null
+++ b/code/__DEFINES/gun.dm
@@ -0,0 +1,3 @@
+#define SELECT_SEMI_AUTOMATIC 1
+#define SELECT_BURST_SHOT 2
+#define SELECT_FULLY_AUTOMATIC 3
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index a88f40a19f..6607b91695 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -388,6 +388,23 @@ GLOBAL_LIST_INIT(pda_reskins, list(PDA_SKIN_CLASSIC = 'icons/obj/pda.dmi', PDA_S
#define BEAT_SLOW 2
#define BEAT_NONE 0
+//Mouse buttons pressed/held/released
+#define RIGHT_CLICK "right"
+#define MIDDLE_CLICK "middle"
+#define LEFT_CLICK "left"
+
+//Keys held down during the mouse action
+#define CTRL_CLICK "ctrl"
+#define ALT_CLICK "alt"
+#define SHIFT_CLICK "shift"
+
+//Pixel coordinates within the icon, in the icon's coordinate space
+#define ICON_X "icon-x"
+#define ICON_Y "icon-y"
+
+//Pixel coordinates in screen_loc format ("[tile_x]:[pixel_x],[tile_y]:[pixel_y]")
+#define SCREEN_LOC "screen-loc"
+
//https://secure.byond.com/docs/ref/info.html#/atom/var/mouse_opacity
#define MOUSE_OPACITY_TRANSPARENT 0
#define MOUSE_OPACITY_ICON 1
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 428784e953..73b2dd3006 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -673,6 +673,27 @@ Turf and target are separate in case you want to teleport some distance from a t
if(final_x || final_y)
return locate(final_x, final_y, T.z)
+///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 null
+
+ var/screen_loc = splittext(LAZYACCESS(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
+
//Finds the distance between two atoms, in pixels
//centered = FALSE counts from turf edge to edge
//centered = TRUE counts from turf center to turf center
diff --git a/code/_onclick/drag_drop.dm b/code/_onclick/drag_drop.dm
index 58c182036d..a698706f1a 100644
--- a/code/_onclick/drag_drop.dm
+++ b/code/_onclick/drag_drop.dm
@@ -23,23 +23,31 @@
SEND_SIGNAL(src, COMSIG_MOUSEDROPPED_ONTO, dropping, user)
return
-
-/client/MouseDown(object, location, control, params)
- if (mouse_down_icon)
+/client/MouseDown(datum/object, location, control, params)
+ if(!control)
+ return
+ 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], TRUE)
+ Click(selected_target[1], location, control, selected_target[2])
sleep(delay)
active_mousedown_item = mob.canMobMousedown(object, location, params)
if(active_mousedown_item)
active_mousedown_item.onMouseDown(object, location, params, mob)
/client/MouseUp(object, location, control, params)
- if (mouse_up_icon)
+ if(!control)
+ return
+ 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
if(active_mousedown_item)
@@ -74,9 +82,6 @@
/obj/item/proc/onMouseUp(object, location, params, mob)
return
-/obj/item/gun/CanItemAutoclick(object, location, params)
- . = automatic
-
/atom/proc/IsAutoclickable()
. = 1
@@ -110,6 +115,7 @@
selected_target[2] = params
if(active_mousedown_item)
active_mousedown_item.onMouseDrag(src_object, over_object, src_location, over_location, params, mob)
+ SEND_SIGNAL(src, COMSIG_CLIENT_MOUSEDRAG, src_object, over_object, src_location, over_location, src_control, over_control, params)
/obj/item/proc/onMouseDrag(src_object, over_object, src_location, over_location, params, mob)
return
diff --git a/code/datums/action.dm b/code/datums/action.dm
index ac8c909bd2..304aa47baa 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -23,6 +23,8 @@
var/icon_icon = 'icons/mob/actions.dmi' //This is the file for the ACTION icon
var/button_icon_state = "default" //And this is the state for the action icon
var/mob/owner
+ ///List of all mobs that are viewing our action button -> A unique movable for them to view.
+ var/list/viewers = list()
/datum/action/New(Target)
link_to(Target)
@@ -121,6 +123,11 @@
return FALSE
return TRUE
+/datum/action/proc/UpdateButtons(status_only, force)
+ for(var/datum/hud/hud in viewers)
+ var/atom/movable/screen/movable/button = viewers[hud]
+ UpdateButtonIcon(button, status_only, force)
+
/datum/action/proc/UpdateButtonIcon(status_only = FALSE, force = FALSE)
if(!button)
return
@@ -217,6 +224,8 @@
name = "Toggle Hood"
/datum/action/item_action/toggle_firemode
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "fireselect_no"
name = "Toggle Firemode"
/datum/action/item_action/rcl_col
diff --git a/code/datums/components/fullauto.dm b/code/datums/components/fullauto.dm
new file mode 100644
index 0000000000..4b9c25db91
--- /dev/null
+++ b/code/datums/components/fullauto.dm
@@ -0,0 +1,278 @@
+#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)
+ RegisterSignal(parent, COMSIG_GUN_AUTOFIRE_SELECTED, .proc/wake_up)
+ RegisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_ITEM_DROPPED, COMSIG_GUN_AUTOFIRE_DESELECTED), .proc/autofire_off)
+ if(_autofire_shot_delay)
+ autofire_shot_delay = _autofire_shot_delay
+ if(ismob(gun.loc))
+ var/mob/user = gun.loc
+ wake_up(src, user)
+
+
+/datum/component/automatic_fire/Destroy()
+ UnregisterSignal(parent, list(COMSIG_PARENT_PREQDELETED, COMSIG_ITEM_DROPPED, COMSIG_GUN_AUTOFIRE_DESELECTED))
+ autofire_off()
+ return ..()
+
+/datum/component/automatic_fire/process(delta_time)
+ if(!(autofire_stat & AUTOFIRE_STAT_FIRING))
+ STOP_PROCESSING(SSprojectiles, src)
+ return
+
+ if(!COOLDOWN_FINISHED(src, next_shot_cd))
+ 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
+
+ var/obj/item/gun/G = parent
+
+ if(iscarbon(user))
+ var/mob/living/carbon/shooter = user
+ if(shooter.is_holding(parent) && G.fire_select == SELECT_FULLY_AUTOMATIC)
+ autofire_on(shooter.client)
+ else
+ autofire_off()
+
+// 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_ALERT|AUTOFIRE_STAT_FIRING))
+ return
+ autofire_stat = AUTOFIRE_STAT_ALERT
+ clicker = usercli
+ shooter = clicker.mob
+ RegisterSignal(clicker, COMSIG_CLIENT_MOUSEDOWN, .proc/on_mouse_down)
+ RegisterSignal(shooter, COMSIG_MOB_CLIENT_LOGOUT, .proc/autofire_off)
+ if(!QDELETED(shooter))
+ UnregisterSignal(shooter, COMSIG_MOB_CLIENT_LOGIN)
+ 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
+ RegisterSignal(shooter, COMSIG_MOB_CLIENT_LOGIN, .proc/on_client_login)
+ if(!QDELETED(shooter))
+ UnregisterSignal(shooter, COMSIG_MOB_CLIENT_LOGOUT)
+ 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)
+ 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.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)) //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 = params2turf(modifiers["screen-loc"], get_turf(source.eye), source)
+ 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
+ start_autofiring()
+
+
+//Dakka-dakka
+/datum/component/automatic_fire/proc/start_autofiring()
+ if(autofire_stat == AUTOFIRE_STAT_FIRING)
+ return //Already pew-pewing.
+ 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
+ switch(autofire_stat)
+ if(AUTOFIRE_STAT_IDLE, AUTOFIRE_STAT_ALERT)
+ 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 = params2turf(modifiers["screen-loc"], get_turf(source.eye), source)
+ 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
+ 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)
+ COOLDOWN_START(src, next_shot_cd, autofire_shot_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(!can_shoot(shooter) || !can_trigger_gun(shooter) || semicd)
+ 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, "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_DOES_SLEEP
+ if(!can_shoot())
+ shoot_with_empty_chamber(shooter)
+ return NONE
+ 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)
+ return COMPONENT_AUTOFIRE_SHOT_SUCCESS //All is well, we can continue shooting.
+
+#undef AUTOFIRE_MOUSEUP
+#undef AUTOFIRE_MOUSEDOWN
diff --git a/code/modules/cargo/centcom_podlauncher.dm b/code/modules/cargo/centcom_podlauncher.dm
index e4060de1a2..557061e5a6 100644
--- a/code/modules/cargo/centcom_podlauncher.dm
+++ b/code/modules/cargo/centcom_podlauncher.dm
@@ -567,7 +567,8 @@
else if(picking_dropoff_turf)
holder.mouse_up_icon = 'icons/effects/mouse_pointers/supplypod_pickturf.dmi' //Icon for when mouse is released
holder.mouse_down_icon = 'icons/effects/mouse_pointers/supplypod_pickturf_down.dmi' //Icon for when mouse is pressed
- holder.mouse_pointer_icon = holder.mouse_up_icon //Icon for idle mouse (same as icon for when released)
+ holder.mouse_override_icon = holder.mouse_up_icon //Icon for idle mouse (same as icon for when released)
+ holder.mouse_pointer_icon = holder.mouse_override_icon
holder.click_intercept = src //Create a click_intercept so we know where the user is clicking
else
var/mob/holder_mob = holder.mob
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 294fdcc30b..c1399ce646 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -22,6 +22,8 @@
///Contains admin info. Null if client is not an admin.
var/datum/admins/holder = null
var/datum/click_intercept = null // Needs to implement InterceptClickOn(user,params,atom) proc
+ ///Time when the click was intercepted
+ var/click_intercept_time = 0
var/AI_Interact = 0
var/jobbancache = null //Used to cache this client's jobbans to save on DB queries
@@ -78,6 +80,8 @@
//These two vars are used to make a special mouse cursor, with a unique icon for clicking
var/mouse_up_icon = null
var/mouse_down_icon = null
+ ///used to override the mouse cursor so it doesnt get reset
+ var/mouse_override_icon = null
var/ip_intel = "Disabled"
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 66bb714b99..6ca963df73 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -422,7 +422,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if( (world.address == address || !address) && !GLOB.host )
GLOB.host = key
world.update_status()
-
+
if(holder)
add_admin_verbs()
var/admin_memo_note = get_message_output("memo")
@@ -871,6 +871,16 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
return
last_activity = world.time
last_click = world.time
+ //fullauto stuff
+ /*
+ if(!control)
+ return
+ */
+ if(click_intercept_time)
+ if(click_intercept_time >= world.time)
+ click_intercept_time = 0 //Reset and return. Next click should work, but not this one.
+ return
+ click_intercept_time = 0 //Just reset. Let's not keep re-checking forever.
var/list/L = params2list(params)
if(L["drag"])
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 7ef26c7818..507a94b613 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -878,10 +878,12 @@ GLOBAL_VAR_INIT(exploit_warn_spam_prevention, 0)
if (!client)
return
client.mouse_pointer_icon = initial(client.mouse_pointer_icon)
- if (ismecha(loc))
- var/obj/vehicle/sealed/mecha/M = loc
- if(M.mouse_pointer)
- client.mouse_pointer_icon = M.mouse_pointer
+ if(istype(loc, /obj/vehicle/sealed))
+ var/obj/vehicle/sealed/mecha/E = loc
+ if(E.mouse_pointer)
+ client.mouse_pointer_icon = E.mouse_pointer
+ if(client.mouse_override_icon)
+ client.mouse_pointer_icon = client.mouse_override_icon
/mob/proc/is_literate()
return 0
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 7f1f43efc3..236143dc43 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -22,6 +22,7 @@
var/ranged_attack_speed = CLICK_CD_RANGE
var/melee_attack_speed = CLICK_CD_MELEE
+ var/gun_flags = NONE
var/fire_sound = "gunshot"
var/suppressed = null //whether or not a message is displayed when fired
var/can_suppress = FALSE
@@ -32,6 +33,7 @@
trigger_guard = TRIGGER_GUARD_NORMAL //trigger guard on the weapon, hulks can't fire them with their big meaty fingers
var/sawn_desc = null //description change if weapon is sawn-off
var/sawn_off = FALSE
+ var/firing_burst = 0 //Prevent the weapon from firing again while already firing
/// can we be put into a turret
var/can_turret = TRUE
@@ -57,6 +59,8 @@
var/burst_spread = 0 //Spread induced by the gun itself during burst fire per iteration. Only checked if spread is 0.
var/randomspread = 1 //Set to 0 for shotguns. This is used for weapons that don't fire all their bullets at once.
var/inaccuracy_modifier = 1
+ var/semicd = 0 //cooldown handler
+ var/dual_wield_spread = 24 //additional spread when dual wielding
lefthand_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
righthand_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
@@ -87,27 +91,64 @@
var/zoom_out_amt = 0
var/datum/action/item_action/toggle_scope_zoom/azoom
+ //Firemodes
+ var/datum/action/item_action/toggle_firemode/firemode_action
+ /// Current fire selection, can choose between burst, single, and full auto.
+ var/fire_select = SELECT_SEMI_AUTOMATIC
+ var/fire_select_index = 1
+ /// What modes does this weapon have? Put SELECT_FULLY_AUTOMATIC in here to enable fully automatic behaviours.
+ var/list/fire_select_modes = list(SELECT_SEMI_AUTOMATIC)
+ /// if i`1t has an icon for a selector switch indicating current firemode.
+ var/selector_switch_icon = FALSE
+
var/dualwield_spread_mult = 1 //dualwield spread multiplier
/// Just 'slightly' snowflakey way to modify projectile damage for projectiles fired from this gun.
var/projectile_damage_multiplier = 1
- var/automatic = 0 //can gun use it, 0 is no, anything above 0 is the delay between clicks in ds
-
/// directional recoil multiplier
var/dir_recoil_amp = 10
+/obj/item/gun/ui_action_click(mob/user, action)
+ if(istype(action, /datum/action/item_action/toggle_firemode))
+ fire_select()
+ else if(istype(action, /datum/action/item_action/toggle_scope_zoom))
+ zoom(user, user.dir)
+ else if(istype(action, alight))
+ toggle_gunlight()
+ else
+ ..()
+
/obj/item/gun/Initialize(mapload)
. = ..()
- if(no_pin_required)
- pin = null
- else if(pin)
+ if(pin)
pin = new pin(src)
+
if(gun_light)
- alight = new (src)
+ alight = new(src)
+
if(zoomable)
azoom = new (src)
+ if(burst_size > 1 && !(SELECT_BURST_SHOT in fire_select_modes))
+ fire_select_modes.Add(SELECT_BURST_SHOT)
+ else if(burst_size <= 1 && (SELECT_BURST_SHOT in fire_select_modes))
+ fire_select_modes.Remove(SELECT_BURST_SHOT)
+
+ burst_size = 1
+
+ sortList(fire_select_modes, /proc/cmp_numeric_asc)
+
+ if(fire_select_modes.len > 1)
+ firemode_action = new(src)
+ firemode_action.button_icon_state = "fireselect_[fire_select]"
+ firemode_action.UpdateButtonIcon()
+
+/obj/item/gun/ComponentInitialize()
+ . = ..()
+ if(SELECT_FULLY_AUTOMATIC in fire_select_modes)
+ AddComponent(/datum/component/automatic_fire, fire_delay)
+
/obj/item/gun/Destroy()
if(pin)
QDEL_NULL(pin)
@@ -117,6 +158,10 @@
QDEL_NULL(bayonet)
if(chambered)
QDEL_NULL(chambered)
+ if(azoom)
+ QDEL_NULL(azoom)
+ if(firemode_action)
+ QDEL_NULL(firemode_action)
return ..()
/obj/item/gun/examine(mob/user)
@@ -142,6 +187,41 @@
else if(can_bayonet)
. += "It has a bayonet lug on it."
+/obj/item/gun/proc/fire_select()
+ var/mob/living/carbon/human/user = usr
+
+ var/max_mode = fire_select_modes.len
+
+ if(max_mode <= 1)
+ to_chat(user, "[src] is not capable of switching firemodes!")
+ return
+
+ fire_select_index = 1 + fire_select_index % max_mode //Magic math to cycle through this shit!
+
+ fire_select = fire_select_modes[fire_select_index]
+
+ switch(fire_select)
+ if(SELECT_SEMI_AUTOMATIC)
+ burst_size = 1
+ fire_delay = 0
+ SEND_SIGNAL(src, COMSIG_GUN_AUTOFIRE_DESELECTED, user)
+ to_chat(user, "You switch [src] to semi-automatic.")
+ if(SELECT_BURST_SHOT)
+ burst_size = initial(burst_size)
+ fire_delay = initial(fire_delay)
+ SEND_SIGNAL(src, COMSIG_GUN_AUTOFIRE_DESELECTED, user)
+ to_chat(user, "You switch [src] to [burst_size]-round burst.")
+ if(SELECT_FULLY_AUTOMATIC)
+ burst_size = 1
+ SEND_SIGNAL(src, COMSIG_GUN_AUTOFIRE_SELECTED, user)
+ to_chat(user, "You switch [src] to automatic.")
+
+ playsound(user, 'sound/weapons/empty.ogg', 100, TRUE)
+ update_appearance()
+ firemode_action.button_icon_state = "fireselect_[fire_select]"
+ firemode_action.UpdateButtonIcon()
+ return TRUE
+
/obj/item/gun/equipped(mob/living/user, slot)
. = ..()
if(zoomed && user.get_active_held_item() != src)
@@ -567,12 +647,6 @@
gun_light = new_light
-/obj/item/gun/ui_action_click(mob/user, action)
- if(istype(action, /datum/action/item_action/toggle_scope_zoom))
- zoom(user, user.dir)
- else if(istype(action, alight))
- toggle_gunlight()
-
/obj/item/gun/proc/toggle_gunlight()
if(!gun_light)
return
diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm
index 4b871d59b2..4d08520246 100644
--- a/code/modules/projectiles/guns/ballistic/automatic.dm
+++ b/code/modules/projectiles/guns/ballistic/automatic.dm
@@ -5,8 +5,8 @@
var/automatic_burst_overlay = TRUE
can_suppress = TRUE
burst_size = 3
- burst_shot_delay = 2
- actions_types = list(/datum/action/item_action/toggle_firemode)
+ fire_delay = 2
+ fire_select_modes = list(SELECT_SEMI_AUTOMATIC, SELECT_BURST_SHOT, SELECT_FULLY_AUTOMATIC)
/obj/item/gun/ballistic/automatic/proto
name = "\improper Nanotrasen Saber SMG"
@@ -15,6 +15,7 @@
fire_sound = "sound/weapons/gunshot_smg_alt.ogg"
mag_type = /obj/item/ammo_box/magazine/smgm9mm
pin = null
+ burst_size = 1
/obj/item/gun/ballistic/automatic/proto/unrestricted
pin = /obj/item/firing_pin
@@ -55,34 +56,6 @@
else
to_chat(user, "You cannot seem to get \the [src] out of your hands!")
-/obj/item/gun/ballistic/automatic/ui_action_click(mob/user, action)
- if(istype(action, /datum/action/item_action/toggle_firemode))
- burst_select()
- else
- return ..()
-
-/obj/item/gun/ballistic/automatic/proc/burst_select()
- var/mob/living/carbon/human/user = usr
- select = !select
- if(!select)
- disable_burst()
- to_chat(user, "You switch to semi-automatic.")
- else
- enable_burst()
- to_chat(user, "You switch to [burst_size]-rnd burst.")
-
- playsound(user, 'sound/weapons/empty.ogg', 100, 1)
- update_icon()
- for(var/X in actions)
- var/datum/action/A = X
- A.UpdateButtonIcon()
-
-/obj/item/gun/ballistic/automatic/proc/enable_burst()
- burst_size = initial(burst_size)
-
-/obj/item/gun/ballistic/automatic/proc/disable_burst()
- burst_size = 1
-
/obj/item/gun/ballistic/automatic/can_shoot()
return get_ammo()
@@ -136,18 +109,10 @@
knife_y_offset = 12
automatic_burst_overlay = FALSE
-/obj/item/gun/ballistic/automatic/wt550/enable_burst()
- . = ..()
- spread = 15
-
/obj/item/gun/ballistic/automatic/wt550/afterattack()
. = ..()
empty_alarm()
-/obj/item/gun/ballistic/automatic/wt550/disable_burst()
- . = ..()
- spread = 0
-
/obj/item/gun/ballistic/automatic/wt550/update_icon_state()
icon_state = "wt550[magazine ? "-[CEILING(((get_ammo(FALSE) / magazine.max_ammo) * 20) /4, 1)*4]" : "-0"]" //Sprites only support up to 20.
@@ -211,6 +176,7 @@
/obj/item/gun/ballistic/automatic/m90/update_icon_state()
icon_state = "[initial(icon_state)][magazine ? "" : "-e"]"
+/*
/obj/item/gun/ballistic/automatic/m90/burst_select()
var/mob/living/carbon/human/user = usr
switch(select)
@@ -228,6 +194,7 @@
playsound(user, 'sound/weapons/empty.ogg', 100, 1)
update_icon()
return
+*/
/obj/item/gun/ballistic/automatic/tommygun
name = "\improper Thompson SMG"
@@ -304,14 +271,17 @@
slot_flags = 0
mag_type = /obj/item/ammo_box/magazine/mm712x82
weapon_weight = WEAPON_HEAVY
- var/cover_open = FALSE
can_suppress = FALSE
- burst_size = 3
- burst_shot_delay = 1
+ burst_size = 1
+ actions_types = list()
spread = 7
pin = /obj/item/firing_pin/implant/pindicate
- automatic_burst_overlay = FALSE
+ var/cover_open = FALSE
+/obj/item/gun/ballistic/automatic/l6_saw/Initialize()
+ . = ..()
+ AddElement(/datum/element/update_icon_updates_onmob)
+ AddComponent(/datum/component/automatic_fire, 0.2 SECONDS)
/obj/item/gun/ballistic/automatic/l6_saw/unrestricted
pin = /obj/item/firing_pin
diff --git a/code/modules/projectiles/guns/ballistic/launchers.dm b/code/modules/projectiles/guns/ballistic/launchers.dm
index c53366c4f5..10a6eea89d 100644
--- a/code/modules/projectiles/guns/ballistic/launchers.dm
+++ b/code/modules/projectiles/guns/ballistic/launchers.dm
@@ -39,7 +39,7 @@
mag_type = /obj/item/ammo_box/magazine/m75
burst_size = 1
fire_delay = 0
- actions_types = list()
+ fire_select_modes = list(SELECT_SEMI_AUTOMATIC)
casing_ejector = FALSE
/obj/item/gun/ballistic/automatic/gyropistol/update_icon_state()
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index 98b654aadb..bd6f203882 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -7,7 +7,7 @@
can_suppress = TRUE
burst_size = 1
fire_delay = 0
- actions_types = list()
+ fire_select_modes = list(SELECT_SEMI_AUTOMATIC)
automatic_burst_overlay = FALSE
/obj/item/gun/ballistic/automatic/pistol/no_mag
@@ -104,7 +104,7 @@
mag_type = /obj/item/ammo_box/magazine/pistolm9mm
burst_size = 3
fire_delay = 2
- actions_types = list(/datum/action/item_action/toggle_firemode)
+ fire_select_modes = list(SELECT_SEMI_AUTOMATIC, SELECT_BURST_SHOT, SELECT_FULLY_AUTOMATIC)
/obj/item/gun/ballistic/automatic/pistol/stickman
name = "flat gun"
@@ -137,7 +137,7 @@
burst_size = 1
can_suppress = FALSE
w_class = WEIGHT_CLASS_NORMAL
- actions_types = list()
+ fire_select_modes = list(SELECT_SEMI_AUTOMATIC)
fire_sound = 'sound/weapons/noscope.ogg'
spread = 20 //damn thing has no rifling.
automatic_burst_overlay = FALSE
diff --git a/code/modules/projectiles/guns/energy/laser_gatling.dm b/code/modules/projectiles/guns/energy/laser_gatling.dm
index 16a977515c..65d525b638 100644
--- a/code/modules/projectiles/guns/energy/laser_gatling.dm
+++ b/code/modules/projectiles/guns/energy/laser_gatling.dm
@@ -101,8 +101,6 @@
slot_flags = null
w_class = WEIGHT_CLASS_HUGE
custom_materials = null
- automatic = 0.5
- fire_delay = 2
ammo_type = list(
/obj/item/ammo_casing/energy/laser
)
diff --git a/icons/effects/mouse_pointers/weapon_pointer.dmi b/icons/effects/mouse_pointers/weapon_pointer.dmi
new file mode 100644
index 0000000000..b5070062c0
Binary files /dev/null and b/icons/effects/mouse_pointers/weapon_pointer.dmi differ
diff --git a/icons/mob/actions/actions_items.dmi b/icons/mob/actions/actions_items.dmi
index 10627cf66d..2eb92f4a45 100644
Binary files a/icons/mob/actions/actions_items.dmi and b/icons/mob/actions/actions_items.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 0e59f9a7dd..e2248b9704 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -56,6 +56,7 @@
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\food.dm"
#include "code\__DEFINES\footsteps.dm"
+#include "code\__DEFINES\gun.dm"
#include "code\__DEFINES\hud.dm"
#include "code\__DEFINES\instruments.dm"
#include "code\__DEFINES\integrated_electronics.dm"
@@ -536,6 +537,7 @@
#include "code\datums\components\field_of_vision.dm"
#include "code\datums\components\footstep.dm"
#include "code\datums\components\fried.dm"
+#include "code\datums\components\fullauto.dm"
#include "code\datums\components\gps.dm"
#include "code\datums\components\honkspam.dm"
#include "code\datums\components\identification.dm"