diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index fb6fb43857c..b093e620c37 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -285,6 +285,9 @@ // /obj/item/gun signals +///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/process_fire (src, target, params, zone_override) #define COMSIG_MOB_FIRED_GUN "mob_fired_gun" ///called in /obj/item/gun/process_fire (user, target, params, zone_override) diff --git a/code/datums/components/scope.dm b/code/datums/components/scope.dm new file mode 100644 index 00000000000..693028a823c --- /dev/null +++ b/code/datums/components/scope.dm @@ -0,0 +1,178 @@ +/datum/component/scope + /// How far we can extend, with modifier of 1, up to our vision edge, higher numbers multiply. + var/range_modifier = 1 + /// Fullscreen object we use for tracking the shots. + var/atom/movable/screen/fullscreen/scope/tracker + +/datum/component/scope/Initialize(range_modifier) + if(!isgun(parent)) + return COMPONENT_INCOMPATIBLE + src.range_modifier = range_modifier + +/datum/component/scope/Destroy(force, silent) + if(tracker) + stop_zooming(tracker.marksman) + return ..() + +/datum/component/scope/RegisterWithParent() + RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_move) + RegisterSignal(parent, COMSIG_ITEM_AFTERATTACK_SECONDARY, .proc/on_secondary_afterattack) + RegisterSignal(parent, COMSIG_GUN_TRY_FIRE, .proc/on_gun_fire) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine) + +/datum/component/scope/UnregisterFromParent() + UnregisterSignal(parent, list( + COMSIG_MOVABLE_MOVED, + COMSIG_ITEM_AFTERATTACK_SECONDARY, + COMSIG_GUN_TRY_FIRE, + COMSIG_PARENT_EXAMINE, + )) + +/datum/component/scope/process(delta_time) + if(!tracker.marksman.client) + stop_zooming(tracker.marksman) + return + if(!length(tracker.marksman.client.keys_held & tracker.marksman.client.movement_keys)) + tracker.marksman.face_atom(tracker.given_turf) + animate(tracker.marksman.client, 0.2 SECONDS, easing = SINE_EASING, flags = EASE_OUT, 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 + + if(!tracker) + return + stop_zooming(tracker.marksman) + +/datum/component/scope/proc/on_secondary_afterattack(datum/source, atom/target, mob/user, proximity_flag, click_parameters) + SIGNAL_HANDLER + + if(tracker) + stop_zooming(user) + else + start_zooming(user) + return COMPONENT_SECONDARY_CANCEL_ATTACK_CHAIN + +/datum/component/scope/proc/on_gun_fire(obj/item/gun/source, mob/living/user, atom/target, flag, params) + SIGNAL_HANDLER + + if(!tracker?.given_turf || target == get_target(tracker.given_turf)) + return NONE + INVOKE_ASYNC(source, /obj/item/gun.proc/fire_gun, 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 + + examine_list += span_notice("You can scope in with right-click.") + +/** + * 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/object_targets = list() + 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.marksman.see_invisible) + continue + if(!possible_target.mouse_opacity) + continue + if(iseffect(possible_target)) + continue + if(ismob(possible_target)) + return possible_target + if(!possible_target.density) + non_dense_targets += possible_target + continue + object_targets += possible_target + for(var/obj/important_object as anything in object_targets) + return important_object + for(var/obj/unimportant_object as anything in non_dense_targets) + return unimportant_object + return target_turf + +/** + * We start zooming by hiding the mouse pointer, adding our tracker overlay and starting our processing. + * + * Arguments: + * * user: The mob we are starting zooming on. +*/ +/datum/component/scope/proc/start_zooming(mob/user) + if(!user.client) + return + user.client.mouse_override_icon = 'icons/effects/mouse_pointers/scope_hide.dmi' + user.update_mouse_pointer() + user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE) + tracker = user.overlay_fullscreen("scope", /atom/movable/screen/fullscreen/scope, 0) + tracker.range_modifier = range_modifier + tracker.marksman = user + tracker.RegisterSignal(user, COMSIG_MOVABLE_MOVED, /atom/movable/screen/fullscreen/scope.proc/on_move) + RegisterSignal(user, COMSIG_MOB_SWAP_HANDS, .proc/stop_zooming) + START_PROCESSING(SSfastprocess, src) + +/** + * 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 + + STOP_PROCESSING(SSfastprocess, src) + UnregisterSignal(user, COMSIG_MOB_SWAP_HANDS) + if(user.client) + animate(user.client, 0.2 SECONDS, pixel_x = 0, pixel_y = 0) + user.client.mouse_override_icon = null + user.update_mouse_pointer() + user.playsound_local(parent, 'sound/weapons/scope.ogg', 75, TRUE, frequency = -1) + tracker = null + user.clear_fullscreen("scope") + +/atom/movable/screen/fullscreen/scope + icon_state = "scope" + plane = HUD_PLANE + mouse_opacity = MOUSE_OPACITY_ICON + /// Multiplier for given_X an given_y. + var/range_modifier = 1 + /// The mob the scope is on. + var/mob/marksman + /// Pixel x we send to the scope component. + var/given_x = 0 + /// Pixel y we send to the scope component. + var/given_y = 0 + /// The turf we send to the scope component. + var/turf/given_turf + /// The coordinate on our mouseentered, for performance reasons. + COOLDOWN_DECLARE(coordinate_cooldown) + +/atom/movable/screen/fullscreen/scope/proc/on_move(atom/source, atom/oldloc, dir, forced) + SIGNAL_HANDLER + + if(!given_turf) + return + var/x_offset = source.loc.x - oldloc.x + var/y_offset = source.loc.y - oldloc.y + given_turf = locate(given_turf.x+x_offset, given_turf.y+y_offset, given_turf.z) + +/atom/movable/screen/fullscreen/scope/MouseEntered(location, control, params) + . = ..() + MouseMove(location, control, params) + +/atom/movable/screen/fullscreen/scope/MouseMove(location, control, params) + if(!marksman?.client || usr != marksman) + return + if(!COOLDOWN_FINISHED(src, coordinate_cooldown)) + return + COOLDOWN_START(src, coordinate_cooldown, 0.2 SECONDS) + var/list/modifiers = params2list(params) + var/icon_x = text2num(LAZYACCESS(modifiers, VIS_X)) + var/icon_y = text2num(LAZYACCESS(modifiers, VIS_Y)) + var/list/view = getviewsize(marksman.client.view) + given_x = round(range_modifier * (icon_x - view[1]*world.icon_size/2)) + given_y = round(range_modifier * (icon_y - view[2]*world.icon_size/2)) + given_turf = locate(marksman.x+round(given_x/world.icon_size, 1),marksman.y+round(given_y/world.icon_size, 1),marksman.z) diff --git a/code/game/objects/effects/temporary_visuals/projectiles/impact.dm b/code/game/objects/effects/temporary_visuals/projectiles/impact.dm index c504bb4f7b2..d5f37cca4c9 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/impact.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/impact.dm @@ -44,3 +44,6 @@ /obj/effect/projectile/impact/solar name = "solar impact" icon_state = "impact_solar" + +/obj/effect/projectile/impact/sniper + icon_state = "sniper" diff --git a/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm b/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm index a26141d80a8..65f0ba39767 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm @@ -35,3 +35,6 @@ /obj/effect/projectile/muzzle/solar icon_state = "muzzle_solar" + +/obj/effect/projectile/muzzle/sniper + icon_state = "sniper" diff --git a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm index cb1334aadca..05777826105 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm @@ -73,3 +73,6 @@ /obj/effect/projectile/tracer/laser/emitter name = "emitter beam" icon_state = "emitter" + +/obj/effect/projectile/tracer/sniper + icon_state = "sniper" diff --git a/code/modules/antagonists/heretic/items/hunter_rifle.dm b/code/modules/antagonists/heretic/items/hunter_rifle.dm index c43dbce8f93..d12ff4236ea 100644 --- a/code/modules/antagonists/heretic/items/hunter_rifle.dm +++ b/code/modules/antagonists/heretic/items/hunter_rifle.dm @@ -10,9 +10,10 @@ worn_icon_state = "moistprime" mag_type = /obj/item/ammo_box/magazine/internal/boltaction/lionhunter fire_sound = 'sound/weapons/gun/sniper/shot.ogg' - zoomable = TRUE - zoom_amt = 5 - zoom_out_amt = 3 + +/obj/item/gun/ballistic/rifle/lionhunter/Initialize(mapload) + . = ..() + AddComponent(/datum/component/scope, range_modifier = 1.25) /obj/item/ammo_box/magazine/internal/boltaction/lionhunter name = "lionhunter rifle internal magazine" diff --git a/code/modules/projectiles/ammunition/ballistic/sniper.dm b/code/modules/projectiles/ammunition/ballistic/sniper.dm index 3c191509cf7..3749520a522 100644 --- a/code/modules/projectiles/ammunition/ballistic/sniper.dm +++ b/code/modules/projectiles/ammunition/ballistic/sniper.dm @@ -18,3 +18,8 @@ name = ".50 penetrator round bullet casing" desc = "A .50 caliber penetrator round casing." projectile_type = /obj/projectile/bullet/p50/penetrator + +/obj/item/ammo_casing/p50/marksman + name = ".50 marksman round bullet casing" + desc = "A .50 caliber marksman round casing." + projectile_type = /obj/projectile/bullet/p50/marksman diff --git a/code/modules/projectiles/boxes_magazines/external/sniper.dm b/code/modules/projectiles/boxes_magazines/external/sniper.dm index ee249d70989..0cf429c1e00 100644 --- a/code/modules/projectiles/boxes_magazines/external/sniper.dm +++ b/code/modules/projectiles/boxes_magazines/external/sniper.dm @@ -23,3 +23,9 @@ desc = "An extremely powerful round capable of passing straight through cover and anyone unfortunate enough to be behind it." ammo_type = /obj/item/ammo_casing/p50/penetrator max_ammo = 5 + +/obj/item/ammo_box/magazine/sniper_rounds/marksman + name = "sniper rounds (marksman)" + desc = "An extremely fast sniper round able to pretty much instantly shoot through something." + ammo_type = /obj/item/ammo_casing/p50/marksman + max_ammo = 5 diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 83210d8fbb1..e3c7e979717 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -71,12 +71,6 @@ 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/zoom_out_amt = 0 - var/datum/action/toggle_scope_zoom/azoom var/pb_knockback = 0 /obj/item/gun/Initialize(mapload) @@ -85,7 +79,6 @@ pin = new pin(src) if(gun_light) alight = new(src) - build_zooming() /obj/item/gun/Destroy() if(isobj(pin)) //Can still be the initial path, then we skip @@ -96,8 +89,6 @@ QDEL_NULL(bayonet) if(chambered) //Not all guns are chambered (EMP'ed energy guns etc) QDEL_NULL(chambered) - if(azoom) - QDEL_NULL(azoom) if(isatom(suppressed)) //SUPPRESSED IS USED AS BOTH A TRUE/FALSE AND AS A REF, WHAT THE FUCKKKKKKKKKKKKKKKKK QDEL_NULL(suppressed) return ..() @@ -146,11 +137,6 @@ if(can_bayonet) . += "It has a bayonet lug on it." -/obj/item/gun/equipped(mob/living/user, slot) - . = ..() - if(zoomed && user.get_active_held_item() != src) - zoom(user, user.dir, FALSE) //we can only stay zoomed in if it's in our hands //yeah and we only unzoom if we're actually zoomed using the gun!! - //called after the gun has successfully fired its chambered ammo. /obj/item/gun/proc/process_chamber(empty_chamber = TRUE, from_firing = TRUE, chamber_next_round = TRUE) handle_chamber(empty_chamber, from_firing, chamber_next_round) @@ -225,6 +211,8 @@ return if(firing_burst) return + if(SEND_SIGNAL(src, COMSIG_GUN_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 @@ -603,18 +591,6 @@ update_appearance() update_action_buttons() -/obj/item/gun/pickup(mob/user) - ..() - if(azoom) - azoom.Grant(user) - -/obj/item/gun/dropped(mob/user) - . = ..() - if(azoom) - azoom.Remove(user) - if(zoomed) - zoom(user, user.dir, FALSE) - /obj/item/gun/update_overlays() . = ..() if(gun_light) @@ -687,66 +663,5 @@ /obj/item/gun/proc/before_firing(atom/target,mob/user) return -///////////// -// ZOOMING // -///////////// - -/datum/action/toggle_scope_zoom - name = "Toggle Scope" - check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED|AB_CHECK_IMMOBILE|AB_CHECK_LYING - icon_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "sniper_zoom" - var/obj/item/gun/gun = null - -/datum/action/toggle_scope_zoom/Trigger(trigger_flags) - . = ..() - if(!.) - return - gun.zoom(owner, owner.dir) - -/datum/action/toggle_scope_zoom/IsAvailable() - . = ..() - if(owner.get_active_held_item() != gun) - . = FALSE - if(!. && gun) - gun.zoom(owner, owner.dir, FALSE) - -/datum/action/toggle_scope_zoom/Remove(mob/living/L) - gun.zoom(L, L.dir, FALSE) - ..() - -/obj/item/gun/proc/rotate(atom/thing, old_dir, new_dir) - SIGNAL_HANDLER - - if(ismob(thing)) - var/mob/lad = thing - lad.client.view_size.zoomOut(zoom_out_amt, zoom_amt, new_dir) - -/obj/item/gun/proc/zoom(mob/living/user, direc, forced_zoom) - if(!user || !user.client) - return - - if(isnull(forced_zoom)) - zoomed = !zoomed - else - zoomed = forced_zoom - - if(zoomed) - RegisterSignal(user, COMSIG_ATOM_DIR_CHANGE, .proc/rotate) - user.client.view_size.zoomOut(zoom_out_amt, zoom_amt, direc) - else - UnregisterSignal(user, COMSIG_ATOM_DIR_CHANGE) - user.client.view_size.zoomIn() - return zoomed - -//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 - #undef FIRING_PIN_REMOVAL_DELAY #undef DUALWIELD_PENALTY_EXTRA_MULTIPLIER diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm index e02c2bfddbf..f11bc640c47 100644 --- a/code/modules/projectiles/guns/ballistic/automatic.dm +++ b/code/modules/projectiles/guns/ballistic/automatic.dm @@ -350,15 +350,16 @@ fire_delay = 40 burst_size = 1 w_class = WEIGHT_CLASS_NORMAL - zoomable = TRUE - zoom_amt = 10 //Long range, enough to see in front of you, but no tiles behind you. - zoom_out_amt = 5 slot_flags = ITEM_SLOT_BACK actions_types = list() mag_display = TRUE suppressor_x_offset = 3 suppressor_y_offset = 3 +/obj/item/gun/ballistic/automatic/sniper_rifle/Initialize(mapload) + . = ..() + AddComponent(/datum/component/scope, range_modifier = 2) + /obj/item/gun/ballistic/automatic/sniper_rifle/syndicate name = "syndicate sniper rifle" desc = "An illegally modified .50 cal sniper rifle with suppression compatibility. Quickscoping still doesn't work." diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 486e3b283a7..de6a269e464 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -94,6 +94,8 @@ var/ricochet_auto_aim_angle = 30 /// the angle of impact must be within this many degrees of the struck surface, set to 0 to allow any angle var/ricochet_incidence_leeway = 40 + /// Can our ricochet autoaim hit our firer? + var/ricochet_shoots_firer = TRUE ///If the object being hit can pass ths damage on to something else, it should not do it for this bullet var/force_hit = FALSE @@ -325,7 +327,7 @@ if(firer && HAS_TRAIT(firer, TRAIT_NICE_SHOT)) best_angle += NICE_SHOT_RICOCHET_BONUS for(var/mob/living/L in range(ricochet_auto_aim_range, src.loc)) - if(L.stat == DEAD || !is_in_sight(src, L)) + if(L.stat == DEAD || !is_in_sight(src, L) || (!ricochet_shoots_firer && L == firer)) continue var/our_angle = abs(closer_angle_difference(Angle, get_angle(src.loc, L.loc))) if(our_angle < best_angle) diff --git a/code/modules/projectiles/projectile/bullets/sniper.dm b/code/modules/projectiles/projectile/bullets/sniper.dm index c9403b65e16..db89bd72031 100644 --- a/code/modules/projectiles/projectile/bullets/sniper.dm +++ b/code/modules/projectiles/projectile/bullets/sniper.dm @@ -47,3 +47,29 @@ damage = 25 speed = 0.3 range = 16 + +/obj/projectile/bullet/p50/marksman + name = ".50 marksman round" + damage = 50 + paralyze = 0 + tracer_type = /obj/effect/projectile/tracer/sniper + impact_type = /obj/effect/projectile/impact/sniper + muzzle_type = /obj/effect/projectile/muzzle/sniper + hitscan = TRUE + impact_effect_type = null + hitscan_light_intensity = 3 + hitscan_light_range = 0.75 + hitscan_light_color_override = LIGHT_COLOR_YELLOW + muzzle_flash_intensity = 5 + muzzle_flash_range = 1 + muzzle_flash_color_override = LIGHT_COLOR_YELLOW + impact_light_intensity = 5 + impact_light_range = 1 + impact_light_color_override = LIGHT_COLOR_YELLOW + ricochets_max = 1 + ricochet_chance = 100 + ricochet_auto_aim_angle = 45 + ricochet_auto_aim_range = 15 + ricochet_incidence_leeway = 90 + ricochet_decay_damage = 1 + ricochet_shoots_firer = FALSE diff --git a/code/modules/uplink/uplink_items/nukeops.dm b/code/modules/uplink/uplink_items/nukeops.dm index 7ec57b8baad..c6934a95eeb 100644 --- a/code/modules/uplink/uplink_items/nukeops.dm +++ b/code/modules/uplink/uplink_items/nukeops.dm @@ -331,6 +331,12 @@ item = /obj/item/ammo_box/magazine/sniper_rounds/soporific cost = 6 +/datum/uplink_item/ammo/sniper/marksman + name = ".50 Marksman Magazine" + desc = "A 5-round magazine of marksman ammo designed for use with .50 sniper rifles. Blast your enemies with instant shots!" + item = /obj/item/ammo_box/magazine/sniper_rounds/marksman + cost = 5 + /datum/uplink_item/ammo/carbine name = "5.56mm Toploader Magazine" desc = "An additional 30-round 5.56mm magazine; suitable for use with the M-90gl carbine. \ diff --git a/icons/effects/mouse_pointers/scope_hide.dmi b/icons/effects/mouse_pointers/scope_hide.dmi new file mode 100644 index 00000000000..a6182e8fc5f Binary files /dev/null and b/icons/effects/mouse_pointers/scope_hide.dmi differ diff --git a/icons/hud/screen_full.dmi b/icons/hud/screen_full.dmi index f54a0bd9444..1e2a42b98e0 100644 Binary files a/icons/hud/screen_full.dmi and b/icons/hud/screen_full.dmi differ diff --git a/icons/obj/guns/projectiles_impact.dmi b/icons/obj/guns/projectiles_impact.dmi index fc005bd00d2..ae5e052287c 100644 Binary files a/icons/obj/guns/projectiles_impact.dmi and b/icons/obj/guns/projectiles_impact.dmi differ diff --git a/icons/obj/guns/projectiles_muzzle.dmi b/icons/obj/guns/projectiles_muzzle.dmi index 2ef59b8d2d3..9bc3869d8c9 100644 Binary files a/icons/obj/guns/projectiles_muzzle.dmi and b/icons/obj/guns/projectiles_muzzle.dmi differ diff --git a/icons/obj/guns/projectiles_tracer.dmi b/icons/obj/guns/projectiles_tracer.dmi index 8efd099a095..b4edd672ac9 100644 Binary files a/icons/obj/guns/projectiles_tracer.dmi and b/icons/obj/guns/projectiles_tracer.dmi differ 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 diff --git a/tgstation.dme b/tgstation.dme index 8802f945ff9..ed5a726e728 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -762,6 +762,7 @@ #include "code\datums\components\rename.dm" #include "code\datums\components\rot.dm" #include "code\datums\components\rotation.dm" +#include "code\datums\components\scope.dm" #include "code\datums\components\shell.dm" #include "code\datums\components\shielded.dm" #include "code\datums\components\shrink.dm"