From f105159acb46e9e9d4131f149aa2c1c1da6d5850 Mon Sep 17 00:00:00 2001 From: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com> Date: Tue, 14 May 2024 05:26:04 -0400 Subject: [PATCH] Ports the scope component from TG and new scope eyes (#24288) * Ports the scope component from TG. * scope eyes * now in scope * fixes lwap canceling other scopes * Update code/datums/components/scope.dm Co-authored-by: Charlie Nolan * c-c-c-changes * Apply suggestions from code review Co-authored-by: Farie82 * Update scope.dm * moves it off the gun * Update code/datums/components/scope.dm Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com> * fixes stealth conflict * do_after_once --------- Signed-off-by: Qwertytoforty <52090703+Qwertytoforty@users.noreply.github.com> Co-authored-by: Charlie Nolan Co-authored-by: Farie82 Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> --- code/__DEFINES/dcs/signals.dm | 9 +- code/__DEFINES/mob_defines.dm | 1 + code/__DEFINES/zoom.dm | 5 + code/__HELPERS/trait_helpers.dm | 1 + code/_globalvars/traits.dm | 1 + code/datums/action.dm | 3 +- code/datums/components/scope.dm | 273 ++++++++++++++++++ code/datums/status_effects/neutral.dm | 19 +- code/datums/uplink_items/uplink_general.dm | 10 + code/modules/projectiles/gun.dm | 110 +------ code/modules/projectiles/guns/energy/laser.dm | 40 +-- .../projectiles/guns/projectile/sniper.dm | 10 +- code/modules/projectiles/guns/rocket.dm | 7 +- .../research/designs/medical_designs.dm | 11 + code/modules/surgery/organs/autosurgeon.dm | 3 + code/modules/surgery/organs/eyes.dm | 32 ++ icons/mob/screen_scope.dmi | Bin 0 -> 6428 bytes paradise.dme | 2 + sound/weapons/scope.ogg | Bin 0 -> 7241 bytes 19 files changed, 380 insertions(+), 157 deletions(-) create mode 100644 code/__DEFINES/zoom.dm create mode 100644 code/datums/components/scope.dm create mode 100644 icons/mob/screen_scope.dmi create mode 100644 sound/weapons/scope.ogg 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 0000000000000000000000000000000000000000..03ca5665dbfeaa2c369da708866ce9a7e5ebcdb9 GIT binary patch literal 6428 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 F{2Ruo_9*}W literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..37d70b6ff4c3df868c64265a09075587d74837a5 GIT binary patch literal 7241 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