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/_onclick/drag_drop.dm b/code/_onclick/drag_drop.dm index 58c182036d..fa529d0bfd 100644 --- a/code/_onclick/drag_drop.dm +++ b/code/_onclick/drag_drop.dm @@ -74,9 +74,6 @@ /obj/item/proc/onMouseUp(object, location, params, mob) return -/obj/item/gun/CanItemAutoclick(object, location, params) - . = automatic - /atom/proc/IsAutoclickable() . = 1 diff --git a/code/datums/components/fullauto.dm b/code/datums/components/fullauto.dm new file mode 100644 index 0000000000..2d00783e30 --- /dev/null +++ b/code/datums/components/fullauto.dm @@ -0,0 +1,280 @@ +#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) + 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)) //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 + 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 = 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 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) + 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(semicd || shooter.stat || !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, "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.stat) + 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 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..9ab2be033a 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") @@ -867,10 +867,17 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( ip_intel = res.intel /client/Click(atom/object, atom/location, control, params, ignore_spam = FALSE, extra_info) - if(last_click > world.time - world.tick_lag) + /*if(last_click > world.time - world.tick_lag) return last_activity = world.time - last_click = world.time + last_click = world.time*/ + 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..91ff5f4fbf 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -875,13 +875,15 @@ GLOBAL_VAR_INIT(exploit_warn_spam_prevention, 0) L.alpha = lighting_alpha /mob/proc/update_mouse_pointer() - if (!client) + if(!client) return client.mouse_pointer_icon = initial(client.mouse_pointer_icon) - if (ismecha(loc)) + if(istype(loc, /obj/vehicle/sealed)) var/obj/vehicle/sealed/mecha/M = loc if(M.mouse_pointer) client.mouse_pointer_icon = M.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..06468685c5 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -92,7 +92,6 @@ /// 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 diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm index 4b871d59b2..6487b57a9e 100644 --- a/code/modules/projectiles/guns/ballistic/automatic.dm +++ b/code/modules/projectiles/guns/ballistic/automatic.dm @@ -304,17 +304,21 @@ 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/unrestricted pin = /obj/item/firing_pin +/obj/item/gun/ballistic/automatic/l6_saw/ComponentInitialize() + . = ..() + AddComponent(/datum/component/automatic_fire, 0.2 SECONDS) + /obj/item/gun/ballistic/automatic/l6_saw/examine(mob/user) . = ..() if(cover_open && magazine) 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