diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 1d4d33ac264..ea134741bec 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -717,9 +717,12 @@
// /obj/item/gun signals
-/// 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/fire_gun (user, target, flag, params)
+#define COMSIG_GUN_TRY_FIRE "gun_try_fire"
+ #define COMPONENT_CANCEL_GUN_FIRE (1<<0)
+///called in /obj/item/gun/afterattack (user, target, flag, params)
+#define COMSIG_MOB_TRY_FIRE "mob_fired_gun"
+///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"
diff --git a/code/__DEFINES/mob_defines.dm b/code/__DEFINES/mob_defines.dm
index 5d57157cfb4..5c1d026c3ae 100644
--- a/code/__DEFINES/mob_defines.dm
+++ b/code/__DEFINES/mob_defines.dm
@@ -298,6 +298,7 @@
#define HEALTH_HUD_OVERRIDE_HEALTHY 3
// Eye protection
#define FLASH_PROTECTION_VERYVUNERABLE -4
+#define FLASH_PROTECTION_EXTRA_SENSITIVE -2
#define FLASH_PROTECTION_SENSITIVE -1
#define FLASH_PROTECTION_NONE 0
#define FLASH_PROTECTION_FLASH 1
diff --git a/code/__DEFINES/zoom.dm b/code/__DEFINES/zoom.dm
new file mode 100644
index 00000000000..f36a97019aa
--- /dev/null
+++ b/code/__DEFINES/zoom.dm
@@ -0,0 +1,5 @@
+///How the scope component is toggled.
+/// Wielding the object with both hands toggles the zoom. Requires the two-handed component to work.
+#define ZOOM_METHOD_WIELD 1
+/// Activated by clicking an item action button specified by the `item_action_type` var.
+#define ZOOM_METHOD_ITEM_ACTION 2
diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm
index aeb341bc7ea..3cb5928179a 100644
--- a/code/__HELPERS/trait_helpers.dm
+++ b/code/__HELPERS/trait_helpers.dm
@@ -223,6 +223,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_CAN_VIEW_HEALTH "can_view_health" // Also used for /Stat
#define TRAIT_MAGPULSE "magnetificent" // Used for anything that is magboot related
#define TRAIT_NOSLIP "noslip"
+#define TRAIT_SCOPED "user_scoped"
#define TRAIT_MEPHEDRONE_ADAPTED "mephedrone_adapted" // Trait that changes the ending effects of twitch leaving your system
#define TRAIT_NOKNOCKDOWNSLOWDOWN "noknockdownslowdown" //If this person has this trait, they are not slowed via knockdown, but they can be hit by bullets like a self knockdown
#define TRAIT_CAN_STRIP "can_strip" // This mob can strip other mobs.
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 4bd180b8222..2e1fc13f5db 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -88,6 +88,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_FORCED_STANDING" = TRAIT_FORCED_STANDING,
"TRAIT_NOSLIP" = TRAIT_NOSLIP,
"TRAIT_MAGPULSE" = TRAIT_MAGPULSE,
+ "TRAIT_SCOPED" = TRAIT_SCOPED,
"TRAIT_MEPHEDRONE_ADAPTED" = TRAIT_MEPHEDRONE_ADAPTED,
"TRAIT_NOKNOCKDOWNSLOWDOWN" = TRAIT_NOKNOCKDOWNSLOWDOWN,
"TRAIT_CAN_STRIP" = TRAIT_CAN_STRIP
diff --git a/code/datums/action.dm b/code/datums/action.dm
index eb6a1a19005..0a099de316c 100644
--- a/code/datums/action.dm
+++ b/code/datums/action.dm
@@ -78,6 +78,8 @@
/datum/action/proc/Trigger(left_click = TRUE)
if(!IsAvailable())
return FALSE
+ if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER)
+ return FALSE
return TRUE
/datum/action/proc/AltTrigger()
@@ -570,7 +572,6 @@
var/obj/item/clothing/shoes/magboots/gravity/G = target
G.dash(usr)
-
///prset for organ actions
/datum/action/item_action/organ_action
check_flags = AB_CHECK_CONSCIOUS
diff --git a/code/datums/components/scope.dm b/code/datums/components/scope.dm
new file mode 100644
index 00000000000..0763330d752
--- /dev/null
+++ b/code/datums/components/scope.dm
@@ -0,0 +1,273 @@
+///A component that allows players to use the item to zoom out. Mainly intended for firearms, but now works with other items too.
+/datum/component/scope
+ /// How far the view can be moved from the player. At 1, it can be moved by the player's view distance; other values scale linearly.
+ var/range_modifier = 1
+ /// Fullscreen object we use for tracking.
+ var/atom/movable/screen/fullscreen/stretch/cursor_catcher/scope/tracker
+ /// The owner of the tracker's ckey. For comparing with the current owner mob, in case the client has left it (e.g. ghosted).
+ var/tracker_owner_ckey
+ /// The method which we zoom in and out
+ var/zoom_method = ZOOM_METHOD_ITEM_ACTION
+ /// if not null, an item action will be added. Redundant if the mode is ZOOM_METHOD_RIGHT_CLICK or ZOOM_METHOD_WIELD.
+ var/item_action_type
+ /// Time to scope up, if you want the scope to take time to boot up. Used by the LWAP
+ var/time_to_scope
+ /// Do we let the user scope and click on the middle of their screen?
+ var/allow_middle_click = FALSE
+ /// Do we have the scope cancel on move?
+ var/movement_cancels_scope = FALSE
+
+/datum/component/scope/Initialize(range_modifier = 1, zoom_method = ZOOM_METHOD_ITEM_ACTION, item_action_type = /datum/action/zoom, time_to_scope = 0, allow_middle_click = FALSE, movement_cancels_scope = FALSE)
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+ src.range_modifier = range_modifier
+ src.zoom_method = zoom_method
+ src.item_action_type = item_action_type
+ src.time_to_scope = time_to_scope
+ src.allow_middle_click = allow_middle_click
+ src.movement_cancels_scope = movement_cancels_scope
+
+
+/datum/component/scope/Destroy(force)
+ if(is_zoomed_in())
+ stop_zooming(tracker.owner)
+ return ..()
+
+/datum/component/scope/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) //Checks for being removed for person, not mob movement
+ if(zoom_method == ZOOM_METHOD_WIELD)
+ RegisterSignal(parent, SIGNAL_ADDTRAIT(TRAIT_WIELDED), PROC_REF(on_wielded))
+ RegisterSignal(parent, SIGNAL_REMOVETRAIT(TRAIT_WIELDED), PROC_REF(on_unwielded))
+ if(item_action_type)
+ var/obj/item/parent_item = parent
+ var/datum/action/scope = new item_action_type(parent)
+ parent_item.actions += scope
+ RegisterSignal(scope, COMSIG_ACTION_TRIGGER, PROC_REF(on_action_trigger))
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine))
+ if(istype(parent, /obj/item/gun))
+ RegisterSignal(parent, COMSIG_GUN_TRY_FIRE, PROC_REF(on_gun_fire))
+
+/datum/component/scope/UnregisterFromParent()
+ if(item_action_type)
+ var/obj/item/parent_item = parent
+ var/datum/action/scope = locate(item_action_type) in parent_item.actions
+ parent_item.actions -= scope
+ UnregisterSignal(parent, list(
+ COMSIG_MOVABLE_MOVED,
+ SIGNAL_ADDTRAIT(TRAIT_WIELDED),
+ SIGNAL_REMOVETRAIT(TRAIT_WIELDED),
+ COMSIG_GUN_TRY_FIRE,
+ COMSIG_PARENT_EXAMINE,
+ ))
+
+/datum/component/scope/process()
+ var/mob/user_mob = tracker.owner
+ var/client/user_client = user_mob.client
+ if(!user_client)
+ stop_zooming(user_mob)
+ return
+ tracker.calculate_params()
+ animate(user_client, world.tick_lag, pixel_x = tracker.given_x, pixel_y = tracker.given_y)
+
+/datum/component/scope/proc/on_move(atom/movable/source, atom/oldloc, dir, forced)
+ SIGNAL_HANDLER // COMSIG_MOVABLE_MOVED
+
+ if(!is_zoomed_in())
+ return
+ if(source.loc != tracker.owner) //Dropped.
+ to_chat(tracker.owner, "[parent]'s scope is overloaded by movement and shuts down!")
+ stop_zooming(tracker.owner)
+
+/datum/component/scope/proc/on_action_trigger(datum/action/source)
+ SIGNAL_HANDLER // COMSIG_ACTION_TRIGGER
+ var/obj/item/item = source.target
+ var/mob/living/user = item.loc
+ if(is_internal_organ(item))
+ var/obj/item/organ/internal/O = item
+ user = O.owner
+ if(is_zoomed_in())
+ stop_zooming(user)
+ else
+ INVOKE_ASYNC(src, PROC_REF(zoom), user)
+
+/datum/component/scope/proc/on_wielded(obj/item/source, trait)
+ SIGNAL_HANDLER // SIGNAL_ADDTRAIT(TRAIT_WIELDED)
+ var/mob/living/user = source.loc
+ INVOKE_ASYNC(src, PROC_REF(zoom), user)
+
+/datum/component/scope/proc/on_unwielded(obj/item/source, trait)
+ SIGNAL_HANDLER // SIGNAL_REMOVETRAIT(TRAIT_WIELDED)
+ var/mob/living/user = source.loc
+ stop_zooming(user)
+
+/datum/component/scope/proc/on_gun_fire(obj/item/gun/source, mob/living/user, atom/target, flag, params)
+ SIGNAL_HANDLER // COMSIG_GUN_TRY_FIRE
+ if(!tracker?.given_turf || target == get_target(tracker.given_turf))
+ return NONE
+ INVOKE_ASYNC(source, TYPE_PROC_REF(/obj/item, afterattack), get_target(tracker.given_turf), user)
+ return COMPONENT_CANCEL_GUN_FIRE
+
+/datum/component/scope/proc/on_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER // COMSIG_PARENT_EXAMINE
+
+ var/scope = istype(parent, /obj/item/gun) ? "scope in" : "zoom out"
+ switch(zoom_method)
+ if(ZOOM_METHOD_WIELD)
+ examine_list += "You can [scope] by wielding it with both hands."
+
+/**
+ * We find and return the best target to hit on a given turf.
+ *
+ * Arguments:
+ * * target_turf: The turf we are looking for targets on.
+*/
+/datum/component/scope/proc/get_target(turf/target_turf)
+ var/list/non_dense_targets = list()
+ for(var/atom/movable/possible_target in target_turf)
+ if(possible_target.layer <= PROJECTILE_HIT_THRESHHOLD_LAYER)
+ continue
+ if(possible_target.invisibility > tracker.owner.see_invisible)
+ continue
+ if(!possible_target.mouse_opacity)
+ continue
+ if(iseffect(possible_target))
+ continue
+ if(ismob(possible_target))
+ if(possible_target == tracker.owner)
+ continue
+ return possible_target
+ if(!possible_target.density)
+ non_dense_targets += possible_target
+ continue
+ return possible_target
+ if(length(non_dense_targets))
+ return non_dense_targets[1]
+ return target_turf
+
+/**
+ * We start zooming by adding our tracker overlay and starting our processing.
+ *
+ * Arguments:
+ * * user: The mob we are starting zooming on.
+*/
+/datum/component/scope/proc/zoom(mob/user)
+ if(isnull(user.client))
+ return
+ if(HAS_TRAIT(user, TRAIT_SCOPED))
+ to_chat(user, "You are already zoomed in!")
+ return
+ if(time_to_scope)
+ if(!do_after_once(user, time_to_scope, target = parent))
+ return
+ user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE)
+ tracker = user.overlay_fullscreen("scope", /atom/movable/screen/fullscreen/stretch/cursor_catcher/scope, istype(parent, /obj/item/gun))
+ tracker.assign_to_mob(user, range_modifier)
+ if(movement_cancels_scope)
+ RegisterSignal(user, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ if(allow_middle_click)
+ RegisterSignal(tracker, COMSIG_CLICK, PROC_REF(generic_click))
+ tracker_owner_ckey = user.ckey
+ if(user.is_holding(parent))
+ RegisterSignals(user, list(COMSIG_CARBON_SWAP_HANDS, COMSIG_PARENT_QDELETING), PROC_REF(stop_zooming))
+ else // The item is likely worn.
+ RegisterSignal(user, COMSIG_PARENT_QDELETING, PROC_REF(stop_zooming))
+ var/static/list/capacity_signals = list(
+ COMSIG_LIVING_STATUS_PARALYSE,
+ COMSIG_LIVING_STATUS_STUN,
+ )
+ RegisterSignals(user, capacity_signals, PROC_REF(on_incapacitated))
+ START_PROCESSING(SSprojectiles, src)
+ ADD_TRAIT(user, TRAIT_SCOPED, "[UID(src)]")
+ if(istype(parent, /obj/item/gun))
+ var/obj/item/gun/G = parent
+ G.on_scope_success(user)
+ return TRUE
+
+/datum/component/scope/proc/on_incapacitated(mob/living/source, amount = 0, ignore_canstun = FALSE)
+ SIGNAL_HANDLER // COMSIG_LIVING_STATUS_PARALYSE, COMSIG_LIVING_STATUS_STUN
+
+ if(amount > 0)
+ stop_zooming(source)
+
+/datum/component/scope/proc/generic_click(/obj/source, location, control, params)
+ SIGNAL_HANDLER // COMSIG_CLICK
+ INVOKE_ASYNC(tracker.owner, TYPE_PROC_REF(/mob, ClickOn), get_target(tracker.given_turf), params)
+
+/**
+ * We stop zooming, canceling processing, resetting stuff back to normal and deleting our tracker.
+ *
+ * Arguments:
+ * * user: The mob we are canceling zooming on.
+*/
+/datum/component/scope/proc/stop_zooming(mob/user)
+ SIGNAL_HANDLER // COMSIG_CARBON_SWAP_HANDS, COMSIG_PARENT_QDELETING
+
+ if(!HAS_TRAIT(user, TRAIT_SCOPED))
+ return
+
+ STOP_PROCESSING(SSprojectiles, src)
+ UnregisterSignal(user, list(
+ COMSIG_LIVING_STATUS_PARALYSE,
+ COMSIG_LIVING_STATUS_STUN,
+ COMSIG_CARBON_SWAP_HANDS,
+ COMSIG_PARENT_QDELETING,
+ ))
+ REMOVE_TRAIT(user, TRAIT_SCOPED, "[UID(src)]")
+
+ user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE, frequency = -1)
+ user.clear_fullscreen("scope")
+
+ // if the client has ended up in another mob, find that mob so we can fix their cursor
+ var/mob/true_user
+ if(user.ckey != tracker_owner_ckey)
+ true_user = get_mob_by_ckey(tracker_owner_ckey)
+
+ if(!isnull(true_user))
+ user = true_user
+
+ if(user.client)
+ animate(user.client, 0.2 SECONDS, pixel_x = 0, pixel_y = 0)
+ UnregisterSignal(user, COMSIG_MOVABLE_MOVED)
+ QDEL_NULL(tracker)
+ tracker_owner_ckey = null
+ if(istype(parent, /obj/item/gun))
+ var/obj/item/gun/G = parent
+ G.on_scope_end(user)
+
+/datum/component/scope/proc/is_zoomed_in()
+ return !!tracker
+
+/atom/movable/screen/fullscreen/stretch/cursor_catcher/scope
+ icon = 'icons/mob/screen_scope.dmi'
+ icon_state = "scope"
+ /// Multiplier for given_X an given_y.
+ var/range_modifier = 1
+
+/atom/movable/screen/fullscreen/stretch/cursor_catcher/scope/assign_to_mob(mob/new_owner, range_modifier)
+ src.range_modifier = range_modifier
+ return ..()
+
+/atom/movable/screen/fullscreen/stretch/cursor_catcher/scope/Click(location, control, params)
+ if(usr == owner)
+ calculate_params()
+ SEND_SIGNAL(src, COMSIG_CLICK, location, control, params)
+
+ return ..()
+
+/atom/movable/screen/fullscreen/stretch/cursor_catcher/scope/calculate_params()
+ var/list/modifiers = params2list(mouse_params)
+ var/icon_x = text2num(LAZYACCESS(modifiers, "vis-x"))
+ if(isnull(icon_x))
+ icon_x = text2num(LAZYACCESS(modifiers, "icon-x"))
+ var/icon_y = text2num(LAZYACCESS(modifiers, "vis-y"))
+ if(isnull(icon_y))
+ icon_y = text2num(LAZYACCESS(modifiers, "icon-y"))
+ given_x = round(range_modifier * (icon_x - view_list[1] * world.icon_size / 2))
+ given_y = round(range_modifier * (icon_y - view_list[2] * world.icon_size / 2))
+ given_turf = locate(owner.x + round(given_x / world.icon_size, 1), owner.y + round(given_y / world.icon_size, 1), owner.z)
+
+
+/datum/action/zoom
+ name = "Toggle Scope"
+ check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_RESTRAINED|AB_CHECK_STUNNED|AB_CHECK_LYING
+ button_icon_state = "sniper_zoom"
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index 1a0339564de..275aa7bda3c 100644
--- a/code/datums/status_effects/neutral.dm
+++ b/code/datums/status_effects/neutral.dm
@@ -218,26 +218,11 @@
tick_interval = 4
/// The number of people the gun has locked on to. Caps at 10 for sanity.
var/locks = 0
- /// What direction the owner was in when using the scope.
- var/owner_dir = 0
-
-/datum/status_effect/lwap_scope/on_creation(mob/living/new_owner, stored_dir = 0)
- owner_dir = stored_dir
- return ..()
/datum/status_effect/lwap_scope/tick()
locks = 0
- var/turf/owner_turf = get_turf(owner)
- var/scope_turf
- for(var/turf/T in RANGE_EDGE_TURFS(7, owner_turf))
- if(get_dir(owner, T) != owner_dir)
- continue
- if(T in range(owner, 6))
- continue
- scope_turf = T
- break
- if(scope_turf)
- for(var/mob/living/L in range(10, scope_turf))
+ for(var/atom/movable/screen/fullscreen/stretch/cursor_catcher/scope/our_scope in owner.client.screen)
+ for(var/mob/living/L in range(10, our_scope.given_turf))
if(locks >= LWAP_LOCK_CAP)
return
if(L == owner || L.stat == DEAD || isslime(L) || ismonkeybasic(L)) //xenobio moment
diff --git a/code/datums/uplink_items/uplink_general.dm b/code/datums/uplink_items/uplink_general.dm
index 9f8b899c38c..d470f30593d 100644
--- a/code/datums/uplink_items/uplink_general.dm
+++ b/code/datums/uplink_items/uplink_general.dm
@@ -853,6 +853,16 @@ GLOBAL_LIST_INIT(uplink_items, subtypesof(/datum/uplink_item))
item = /obj/item/autosurgeon/organ/syndicate/razorwire
cost = 20
+/datum/uplink_item/cyber_implants/scope_eyes
+ name = "Hardened Kaleido Optics Eyes Autoimplanter"
+ desc = "These cybernetic eye implants will let you zoom in on far away objects. \
+ Many users find it disorienting, and find it hard to interact with things near them when active. \
+ This pair has been hardened for special operations personnel."
+ reference = "KOE"
+ item = /obj/item/autosurgeon/organ/syndicate/scope_eyes
+ cost = 20
+
+
// POINTLESS BADASSERY
/datum/uplink_item/badass
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 7baf9bb6cc7..b4b071e6f61 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -67,22 +67,14 @@
var/flight_x_offset = 0
var/flight_y_offset = 0
- //Zooming
- var/zoomable = FALSE //whether the gun generates a Zoom action on creation
- var/zoomed = FALSE //Zoom toggle
- var/zoom_amt = 3 //Distance in TURFs to move the user's screen forward (the "zoom" effect)
- var/datum/action/toggle_scope_zoom/azoom
-
/obj/item/gun/Initialize(mapload)
. = ..()
- build_zooming()
ADD_TRAIT(src, TRAIT_CAN_POINT_WITH, ROUNDSTART_TRAIT)
appearance_flags |= KEEP_TOGETHER
/obj/item/gun/Destroy()
QDEL_NULL(bayonet)
QDEL_NULL(chambered)
- QDEL_NULL(azoom)
QDEL_NULL(gun_light)
return ..()
@@ -154,6 +146,10 @@
/obj/item/gun/afterattack(atom/target, mob/living/user, flag, params)
if(firing_burst)
return
+ if(SEND_SIGNAL(src, COMSIG_GUN_TRY_FIRE, user, target, flag, params) & COMPONENT_CANCEL_GUN_FIRE)
+ return
+ if(SEND_SIGNAL(src, COMSIG_MOB_TRY_FIRE, user, target, flag, params) & COMPONENT_CANCEL_GUN_FIRE)
+ return
if(flag) //It's adjacent, is the user, or is on the user's person
if(target in user.contents) //can't shoot stuff inside us.
return
@@ -414,11 +410,6 @@
visible_message("[src]'s light fades and turns off.")
-/obj/item/gun/dropped(mob/user)
- ..()
- zoom(user,FALSE)
- if(azoom)
- azoom.Remove(user)
/obj/item/gun/AltClick(mob/user)
..()
@@ -485,93 +476,8 @@
process_fire(target, user, 1, params)
-/////////////
-// ZOOMING //
-/////////////
+/obj/item/gun/proc/on_scope_success()
+ return
-/datum/action/toggle_scope_zoom
- name = "Toggle Scope"
- check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_RESTRAINED|AB_CHECK_STUNNED|AB_CHECK_LYING
- button_icon_state = "sniper_zoom"
- var/obj/item/gun/gun = null
-
-/datum/action/toggle_scope_zoom/Destroy()
- gun = null
- return ..()
-
-/datum/action/toggle_scope_zoom/Trigger(left_click)
- gun.zoom(owner)
-
-/datum/action/toggle_scope_zoom/IsAvailable()
- . = ..()
- if(!. && gun)
- gun.zoom(owner, FALSE)
-
-/datum/action/toggle_scope_zoom/Remove(mob/living/L)
- gun.zoom(L, FALSE)
- ..()
-
-/obj/item/gun/proc/zoom(mob/living/user, forced_zoom)
- if(!user || !user.client)
- return
-
- switch(forced_zoom)
- if(FALSE)
- zoomed = FALSE
- if(TRUE)
- zoomed = TRUE
- else
- zoomed = !zoomed
-
- if(zoomed)
- var/_x = 0
- var/_y = 0
- switch(user.dir)
- if(NORTH)
- _y = zoom_amt
- if(EAST)
- _x = zoom_amt
- if(SOUTH)
- _y = -zoom_amt
- if(WEST)
- _x = -zoom_amt
-
- user.client.pixel_x = world.icon_size*_x
- user.client.pixel_y = world.icon_size*_y
- else
- user.client.pixel_x = 0
- user.client.pixel_y = 0
-
-
-//Proc, so that gun accessories/scopes/etc. can easily add zooming.
-/obj/item/gun/proc/build_zooming()
- if(azoom)
- return
-
- if(zoomable)
- azoom = new()
- azoom.gun = src
- RegisterSignal(src, COMSIG_ITEM_EQUIPPED, PROC_REF(ZoomGrantCheck))
-
-/**
- * Proc which will be called when the gun receives the `COMSIG_ITEM_EQUIPPED` signal.
- *
- * This happens if the mob picks up the gun, or equips it to any of their slots.
- * If the slot is anything other than either of their hands (such as the back slot), un-zoom them, and `Remove` the zoom action button from the mob.
- * Otherwise, `Grant` the mob the zoom action button.
- *
- * Arguments:
- * * source - the gun that got equipped, which is `src`.
- * * user - the mob equipping the gun.
- * * slot - the slot the gun is getting equipped to.
- */
-/obj/item/gun/proc/ZoomGrantCheck(datum/source, mob/user, slot)
- // Checks if the gun got equipped into either of the user's hands.
- if(slot != SLOT_HUD_RIGHT_HAND && slot != SLOT_HUD_LEFT_HAND)
- // If its not in their hands, un-zoom, and remove the zoom action button.
- zoom(user, FALSE)
- azoom.Remove(user)
- return FALSE
-
- // The gun is equipped in their hands, give them the zoom ability.
- azoom.Grant(user)
+/obj/item/gun/proc/on_scope_end()
+ return
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index 3d194b08510..fddd8eacaf4 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -126,39 +126,21 @@
weapon_weight = WEAPON_HEAVY
origin_tech = "combat=6;magnets=6;powerstorage=4"
ammo_type = list(/obj/item/ammo_casing/energy/laser/sniper, /obj/item/ammo_casing/energy/laser/sniper/pierce)
- zoomable = TRUE
- zoom_amt = 7
shaded_charge = TRUE
- /// Is the scope fully online or not?
- var/scope_active = FALSE
- var/stored_dir
execution_speed = 8 SECONDS
-/obj/item/gun/energy/lwap/zoom(mob/living/user, forced_zoom)
+/obj/item/gun/energy/lwap/Initialize(mapload)
. = ..()
- if(!ishuman(user))
- return
- var/mob/living/carbon/human/H = user
- stored_dir = H.dir
- if(scope_active && !zoomed)
- select_fire(H)
- H.remove_status_effect(STATUS_EFFECT_LWAPSCOPE)
- scope_active = FALSE
- return
- if(zoomed && do_after(user, 3 SECONDS, target = src))
- if(zoomed && !scope_active) //We check after to be sure.
- scope_active = TRUE
- to_chat(user, "SCOPE_CREEPER_[rand(1, 9999)] Online.")
- select_fire(H)
- H.apply_status_effect(STATUS_EFFECT_LWAPSCOPE, stored_dir)
- return
- if(zoomed)
- zoom(user, FALSE) //Moved while scope was booting, so we unzoom
+ AddComponent(/datum/component/scope, range_modifier = 2, time_to_scope = 3 SECONDS, movement_cancels_scope = TRUE)
-/obj/item/gun/energy/lwap/on_mob_move(dir, mob/user)
- if(scope_active)
- to_chat(user, "[src]'s scope is overloaded by movement and shuts down!")
- zoom(user, FALSE)
+/obj/item/gun/energy/lwap/on_scope_success(mob/living/user)
+ to_chat(user, "SCOPE_CREEPER_[rand(1, 9999)] Online.")
+ select_fire(user)
+ user.apply_status_effect(STATUS_EFFECT_LWAPSCOPE)
+
+/obj/item/gun/energy/lwap/on_scope_end(mob/living/user)
+ select_fire(user)
+ user.remove_status_effect(STATUS_EFFECT_LWAPSCOPE)
/obj/item/gun/energy/lwap/attack_self()
return //no manual ammo changing.
@@ -273,7 +255,7 @@
return
/// Special version given by the Safety Override upgrade and as a standard module for ERT engiborgs.
-/obj/item/gun/energy/emitter/cyborg/proto
+/obj/item/gun/energy/emitter/cyborg/proto
name = "mobile proto-emitter"
desc = "An emitter removed from its base and attached to a laser cannon frame. This one operates on unoptimised software, reducing its effectiveness."
ammo_type = list(/obj/item/ammo_casing/energy/emitter/cyborg/proto)
diff --git a/code/modules/projectiles/guns/projectile/sniper.dm b/code/modules/projectiles/guns/projectile/sniper.dm
index 43781e87570..f8d317464e0 100644
--- a/code/modules/projectiles/guns/projectile/sniper.dm
+++ b/code/modules/projectiles/guns/projectile/sniper.dm
@@ -15,14 +15,18 @@
can_unsuppress = TRUE
can_suppress = TRUE
w_class = WEIGHT_CLASS_NORMAL
- zoomable = TRUE
- zoom_amt = 7 //Long range, enough to see in front of you, but no tiles behind you.
slot_flags = SLOT_FLAG_BACK
actions_types = list()
execution_speed = 8 SECONDS
+ var/zoomable = TRUE
+
+/obj/item/gun/projectile/automatic/sniper_rifle/Initialize(mapload)
+ . = ..()
+ if(zoomable)
+ AddComponent(/datum/component/scope, range_modifier = 2)
/obj/item/gun/projectile/automatic/sniper_rifle/process_fire(atom/target, mob/living/user, message = TRUE, params, zone_override, bonus_spread = 0)
- if(istype(chambered.BB, /obj/item/projectile/bullet/sniper) && !zoomed)
+ if(istype(chambered.BB, /obj/item/projectile/bullet/sniper) && !HAS_TRAIT(user, TRAIT_SCOPED))
var/obj/item/projectile/bullet/sniper/S = chambered.BB
if(S.non_zoom_spread)
to_chat(user, "[src] must be zoomed in to fire this ammunition accurately!")
diff --git a/code/modules/projectiles/guns/rocket.dm b/code/modules/projectiles/guns/rocket.dm
index 1a6755a59e2..1e3f111479c 100644
--- a/code/modules/projectiles/guns/rocket.dm
+++ b/code/modules/projectiles/guns/rocket.dm
@@ -13,11 +13,14 @@
fire_sound = 'sound/weapons/blastcannon.ogg'
fire_delay = 40
recoil = 2
- zoomable = TRUE
- zoom_amt = 7
var/missile_speed = 2
var/missile_range = 30
+
+/obj/item/gun/rocketlauncher/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/scope, range_modifier = 2)
+
/obj/item/gun/rocketlauncher/examine(mob/user)
. = ..()
. += "It is currently [chambered ? "" : "un"]loaded."
diff --git a/code/modules/research/designs/medical_designs.dm b/code/modules/research/designs/medical_designs.dm
index 476fa574fcf..ff3c7b741b4 100644
--- a/code/modules/research/designs/medical_designs.dm
+++ b/code/modules/research/designs/medical_designs.dm
@@ -502,6 +502,17 @@
build_path = /obj/item/organ/internal/eyes/cybernetic/thermals
category = list("Medical")
+/datum/design/cyberimp_scope
+ name = "Kaleido Optics Implant"
+ desc = "These cybernetic eye implants will let you zoom in on far away objects. Many users find it disorienting, and find it hard to interact with things near them when active."
+ id = "ci-scope"
+ req_tech = list("materials" = 6, "programming" = 4, "biotech" = 7, "magnets" = 5,"plasmatech" = 4)
+ build_type = PROTOLATHE | MECHFAB
+ construction_time = 60
+ materials = list(MAT_METAL = 600, MAT_GLASS = 600, MAT_SILVER = 600, MAT_GOLD = 600, MAT_PLASMA = 1000, MAT_DIAMOND = 2000)
+ build_path = /obj/item/organ/internal/eyes/cybernetic/scope
+ category = list("Medical")
+
/datum/design/cyberimp_antidrop
name = "Anti-Drop Implant"
desc = "This cybernetic brain implant will allow you to force your hand muscles to contract, preventing item dropping. Twitch ear to toggle."
diff --git a/code/modules/surgery/organs/autosurgeon.dm b/code/modules/surgery/organs/autosurgeon.dm
index 69678826def..427b07d51f3 100644
--- a/code/modules/surgery/organs/autosurgeon.dm
+++ b/code/modules/surgery/organs/autosurgeon.dm
@@ -141,6 +141,9 @@
/obj/item/autosurgeon/organ/syndicate/xray_eyes
starting_organ = /obj/item/organ/internal/eyes/cybernetic/xray/hardened
+/obj/item/autosurgeon/organ/syndicate/scope_eyes
+ starting_organ = /obj/item/organ/internal/eyes/cybernetic/scope/hardened
+
/obj/item/autosurgeon/organ/syndicate/anti_stam
starting_organ = /obj/item/organ/internal/cyberimp/brain/anti_stam/hardened
diff --git a/code/modules/surgery/organs/eyes.dm b/code/modules/surgery/organs/eyes.dm
index 373303286c6..a7b13fe9aff 100644
--- a/code/modules/surgery/organs/eyes.dm
+++ b/code/modules/surgery/organs/eyes.dm
@@ -167,6 +167,38 @@
emp_proof = TRUE
origin_tech = "materials=6;programming=5;biotech=6;magnets=6;syndicate=3"
+/obj/item/organ/internal/eyes/cybernetic/scope
+ name = "\improper Kaleido Optics eyes"
+ desc = "These cybernetic eye implants will let you zoom in on far away objects. Many users find it disorienting, and find it hard to interact with things near them when active."
+ eye_color = "#6f00ff"
+ flash_protect = FLASH_PROTECTION_EXTRA_SENSITIVE
+ origin_tech = "materials=5;programming=4;biotech=4;magnets=4"
+ var/scope_range = 0.8 //Only used in initialize. Greatly nerfed zoom range, since you are not taking the time zoom delay the lwap has.
+ var/active = FALSE
+
+/obj/item/organ/internal/eyes/cybernetic/scope/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/scope, range_modifier = scope_range, item_action_type = /datum/action/item_action/organ_action/toggle, allow_middle_click = TRUE)
+
+/obj/item/organ/internal/eyes/cybernetic/scope/insert(mob/living/carbon/human/M, special)
+ . = ..()
+ flash_protect = FLASH_PROTECTION_NONE //Resets it to none, so we can just flip to inital each time it is used.
+
+/obj/item/organ/internal/eyes/cybernetic/scope/ui_action_click(mob/user, actiontype)
+ active = !active
+ if(active)
+ flash_protect = initial(flash_protect)
+ else
+ flash_protect = FLASH_PROTECTION_NONE
+
+/obj/item/organ/internal/eyes/cybernetic/scope/hardened
+ name = "\improper Hardened Kaleido Optics eyes"
+ desc = "These cybernetic eye implants will let you zoom in on far away objects. Many users find it disorienting, and find it hard to interact with things near them when active. This pair has been hardened for special operations personnel, and has enhanced zoom functionality."
+ flash_protect = FLASH_PROTECTION_SENSITIVE
+ origin_tech = "materials=6;programming=5;biotech=6;magnets=6;syndicate=3"
+ scope_range = 1.25
+ emp_proof = TRUE
+
/obj/item/organ/internal/eyes/cybernetic/flashlight
name = "flashlight eyes"
desc = "It's two flashlights rigged together with some wire. Why would you put these in someone's head?"
diff --git a/icons/mob/screen_scope.dmi b/icons/mob/screen_scope.dmi
new file mode 100644
index 00000000000..03ca5665dbf
Binary files /dev/null and b/icons/mob/screen_scope.dmi differ
diff --git a/paradise.dme b/paradise.dme
index 427bca5bcd2..4528e95bb9f 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -135,6 +135,7 @@
#include "code\__DEFINES\vv.dm"
#include "code\__DEFINES\wires_defines.dm"
#include "code\__DEFINES\zlevel_defines.dm"
+#include "code\__DEFINES\zoom.dm"
#include "code\__DEFINES\dcs\dcs_flags.dm"
#include "code\__DEFINES\dcs\dcs_helpers.dm"
#include "code\__DEFINES\dcs\signals.dm"
@@ -420,6 +421,7 @@
#include "code\datums\components\persistent_overlay.dm"
#include "code\datums\components\proximity_monitor.dm"
#include "code\datums\components\radioactive.dm"
+#include "code\datums\components\scope.dm"
#include "code\datums\components\shielded.dm"
#include "code\datums\components\slippery.dm"
#include "code\datums\components\spawner.dm"
diff --git a/sound/weapons/scope.ogg b/sound/weapons/scope.ogg
new file mode 100644
index 00000000000..37d70b6ff4c
Binary files /dev/null and b/sound/weapons/scope.ogg differ