Adds a scope component, removes old zooming and adds sniper marksman ammo. (#66218)
Removes the old sniper rifle zoom, replaces it with a scope component. the scope activates on right click and lets your camera follow your mouse. https://streamable.com/2c63u4 (due to byond rounding some shots were weirdly missed in that video, its fixed now) Also adds sniper marksman ammo to the nukie uplink. It does slightly less damage, but it is hitscan and has one guaranteed ricochet shot, so you can shoot a wall and it could still hit someone.
@@ -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)
|
||||
|
||||
@@ -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 <b>right-click</b>.")
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -44,3 +44,6 @@
|
||||
/obj/effect/projectile/impact/solar
|
||||
name = "solar impact"
|
||||
icon_state = "impact_solar"
|
||||
|
||||
/obj/effect/projectile/impact/sniper
|
||||
icon_state = "sniper"
|
||||
|
||||
@@ -35,3 +35,6 @@
|
||||
|
||||
/obj/effect/projectile/muzzle/solar
|
||||
icon_state = "muzzle_solar"
|
||||
|
||||
/obj/effect/projectile/muzzle/sniper
|
||||
icon_state = "sniper"
|
||||
|
||||
@@ -73,3 +73,6 @@
|
||||
/obj/effect/projectile/tracer/laser/emitter
|
||||
name = "emitter beam"
|
||||
icon_state = "emitter"
|
||||
|
||||
/obj/effect/projectile/tracer/sniper
|
||||
icon_state = "sniper"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <b>bayonet</b> 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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. \
|
||||
|
||||
|
After Width: | Height: | Size: 241 B |
|
Before Width: | Height: | Size: 6.4 MiB After Width: | Height: | Size: 6.4 MiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -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"
|
||||
|
||||