diff --git a/code/__DEFINES/dcs/signals/signals_clothing.dm b/code/__DEFINES/dcs/signals/signals_clothing.dm
new file mode 100644
index 00000000000..242a076615f
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_clothing.dm
@@ -0,0 +1,3 @@
+// /obj/item/clothing
+/// (/obj/item/clothing, visor_state) - When a clothing gets it's visor toggled.
+#define COMSIG_CLOTHING_VISOR_TOGGLE "clothing_visor_toggle"
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
index ca6cdae1200..244ef15c3f1 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
@@ -31,6 +31,10 @@
#define COMSIG_MOB_CLIENT_BLOCK_PRE_MOVE COMPONENT_MOVABLE_BLOCK_PRE_MOVE
/// From base of /client/Move()
#define COMSIG_MOB_CLIENT_MOVED "mob_client_moved"
+/// From base of /client/proc/change_view() (mob/source, new_size)
+#define COMSIG_MOB_CLIENT_CHANGE_VIEW "mob_client_change_view"
+/// From base of /mob/proc/reset_perspective() (mob/source)
+#define COMSIG_MOB_RESET_PERSPECTIVE "mob_reset_perspective"
///from mind/transfer_to. Sent to the receiving mob.
#define COMSIG_MOB_MIND_TRANSFERRED_INTO "mob_mind_transferred_into"
diff --git a/code/__DEFINES/fov.dm b/code/__DEFINES/fov.dm
new file mode 100644
index 00000000000..1a10b84be91
--- /dev/null
+++ b/code/__DEFINES/fov.dm
@@ -0,0 +1,11 @@
+/// Field of vision defines.
+#define FOV_90_DEGREES 90
+#define FOV_180_DEGREES 180
+#define FOV_270_DEGREES 270
+
+/// Base mask dimensions. They're like a client's view, only change them if you modify the mask to different dimensions.
+#define BASE_FOV_MASK_X_DIMENSION 15
+#define BASE_FOV_MASK_Y_DIMENSION 15
+
+/// Range at which FOV effects treat nearsightness as blind and play
+#define NEARSIGHTNESS_FOV_BLINDNESS 2
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index bfa6c686b35..1862341c13d 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -4,6 +4,9 @@
//NEVER HAVE ANYTHING BELOW THIS PLANE ADJUST IF YOU NEED MORE SPACE
#define LOWEST_EVER_PLANE -200
+#define FIELD_OF_VISION_BLOCKER_PLANE -199
+#define FIELD_OF_VISION_BLOCKER_RENDER_TARGET "*FIELD_OF_VISION_BLOCKER_RENDER_TARGET"
+
#define CLICKCATCHER_PLANE -99
#define PLANE_SPACE -95
@@ -19,8 +22,10 @@
#define FLOOR_PLANE -7
#define GAME_PLANE -4
+#define GAME_PLANE_FOV_HIDDEN -3
+#define ABOVE_GAME_PLANE -2
-#define MOUSE_TRANSPARENT_PLANE -3 //SKYRAT EDIT ADDITION - Pollution port
+#define MOUSE_TRANSPARENT_PLANE -1 //SKYRAT EDIT ADDITION - Pollution port
#define SPACE_LAYER 1.8
//#define TURF_LAYER 2 //For easy recordkeeping; this is a byond define
@@ -151,6 +156,7 @@
#define BLIND_LAYER 4
#define CRIT_LAYER 5
#define CURSE_LAYER 6
+#define FOV_EFFECTS_LAYER 10000 //Blindness effects are not layer 4, they lie to you
///--------------- FULLSCREEN RUNECHAT BUBBLES ------------
diff --git a/code/__DEFINES/~skyrat_defines/signals.dm b/code/__DEFINES/~skyrat_defines/signals.dm
index cb5bd681f23..70143539adb 100644
--- a/code/__DEFINES/~skyrat_defines/signals.dm
+++ b/code/__DEFINES/~skyrat_defines/signals.dm
@@ -70,3 +70,6 @@
#define COMSIG_START_FISHING "start_fishing"
//when someone pulls back their fishing rod
#define COMSIG_FINISH_FISHING "finish_fishing"
+
+/// From mob/living/*/set_combat_mode(): (new_state)
+#define COMSIG_LIVING_COMBAT_MODE_TOGGLE "living_combat_mode_toggle"
diff --git a/code/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm
index a952ad59853..8453f31e4d1 100644
--- a/code/_onclick/hud/rendering/plane_master.dm
+++ b/code/_onclick/hud/rendering/plane_master.dm
@@ -72,6 +72,23 @@
if(istype(mymob) && mymob.client?.prefs?.read_preference(/datum/preference/toggle/ambient_occlusion))
add_filter("AO", 1, drop_shadow_filter(x = 0, y = -2, size = 4, color = "#04080FAA"))
+/atom/movable/screen/plane_master/game_world_fov_hidden
+ name = "game world fov hidden plane master"
+ plane = GAME_PLANE_FOV_HIDDEN
+ render_relay_plane = GAME_PLANE
+ appearance_flags = PLANE_MASTER //should use client color
+ blend_mode = BLEND_OVERLAY
+
+/atom/movable/screen/plane_master/game_world_fov_hidden/Initialize()
+ . = ..()
+ add_filter("vision_cone", 1, alpha_mask_filter(render_source = FIELD_OF_VISION_BLOCKER_RENDER_TARGET, flags = MASK_INVERSE))
+
+/atom/movable/screen/plane_master/game_world_above
+ name = "above game world plane master"
+ plane = ABOVE_GAME_PLANE
+ render_relay_plane = GAME_PLANE
+ appearance_flags = PLANE_MASTER //should use client color
+ blend_mode = BLEND_OVERLAY
/atom/movable/screen/plane_master/massive_obj
name = "massive object plane master"
@@ -235,3 +252,10 @@
name = "fullscreen alert plane"
plane = FULLSCREEN_PLANE
render_relay_plane = RENDER_PLANE_NON_GAME
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+/atom/movable/screen/plane_master/field_of_vision_blocker
+ name = "field of vision blocker plane master"
+ plane = FIELD_OF_VISION_BLOCKER_PLANE
+ render_target = FIELD_OF_VISION_BLOCKER_RENDER_TARGET
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
diff --git a/code/_onclick/hud/rendering/plane_master_controller.dm b/code/_onclick/hud/rendering/plane_master_controller.dm
index 1b363a754c1..8ae132fd078 100644
--- a/code/_onclick/hud/rendering/plane_master_controller.dm
+++ b/code/_onclick/hud/rendering/plane_master_controller.dm
@@ -85,6 +85,8 @@ INITIALIZE_IMMEDIATE(/atom/movable/plane_master_controller)
controlled_planes = list(
FLOOR_PLANE,
GAME_PLANE,
+ GAME_PLANE_FOV_HIDDEN,
+ ABOVE_GAME_PLANE,
MASSIVE_OBJ_PLANE,
GHOST_PLANE,
POINT_PLANE,
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 16b5f54da8b..adcef045fbe 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -377,3 +377,5 @@
integer = FALSE // It is in hours, but just in case one wants to specify minutes.
/datum/config_entry/flag/sdql_spells
+
+/datum/config_entry/flag/native_fov
diff --git a/code/datums/components/clothing_fov_visor.dm b/code/datums/components/clothing_fov_visor.dm
new file mode 100644
index 00000000000..2ea793bd5fd
--- /dev/null
+++ b/code/datums/components/clothing_fov_visor.dm
@@ -0,0 +1,60 @@
+/// An element to add a FOV trait to the wearer, removing it when an item is unequipped, but only as long as the visor is up.
+/datum/component/clothing_fov_visor
+ /// What's the FOV angle of the trait we're applying to the wearer
+ var/fov_angle
+ /// Keeping track of the visor of our clothing.
+ var/visor_up = FALSE
+ /// Because of clothing code not being too good, we need keep track whether we are worn.
+ var/is_worn = FALSE
+
+/datum/component/clothing_fov_visor/Initialize(fov_angle)
+ . = ..()
+ if(!isclothing(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/obj/item/clothing/clothing_parent = parent
+ src.fov_angle = fov_angle
+ src.visor_up = clothing_parent.up //Initial values could vary, so we need to get it.
+
+/datum/component/clothing_fov_visor/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
+ RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/on_drop)
+ RegisterSignal(parent, COMSIG_CLOTHING_VISOR_TOGGLE, .proc/on_visor_toggle)
+
+/datum/component/clothing_fov_visor/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED, COMSIG_CLOTHING_VISOR_TOGGLE))
+ return ..()
+
+/// On dropping the item, remove the FoV trait if visor was down.
+/datum/component/clothing_fov_visor/proc/on_drop(datum/source, mob/living/dropper)
+ SIGNAL_HANDLER
+ is_worn = FALSE
+ if(visor_up)
+ return
+ dropper.remove_fov_trait(source.type, fov_angle)
+ dropper.update_fov()
+
+/// On equipping the item, add the FoV trait if visor isn't up.
+/datum/component/clothing_fov_visor/proc/on_equip(obj/item/source, mob/living/equipper, slot)
+ SIGNAL_HANDLER
+ if(!(source.slot_flags & slot)) //If EQUIPPED TO HANDS FOR EXAMPLE
+ return
+ is_worn = TRUE
+ if(visor_up)
+ return
+ equipper.add_fov_trait(source.type, fov_angle)
+ equipper.update_fov()
+
+/// On toggling the visor, we may want to add or remove FOV trait from the wearer.
+/datum/component/clothing_fov_visor/proc/on_visor_toggle(datum/source, visor_state)
+ SIGNAL_HANDLER
+ visor_up = visor_state
+ if(!is_worn)
+ return
+ var/obj/item/clothing/clothing_parent = parent
+ var/mob/living/wearer = clothing_parent.loc //This has to be the case due to equip/dropped keeping track.
+ if(visor_up)
+ wearer.remove_fov_trait(source.type, fov_angle)
+ wearer.update_fov()
+ else
+ wearer.add_fov_trait(source.type, fov_angle)
+ wearer.update_fov()
diff --git a/code/datums/components/fov_handler.dm b/code/datums/components/fov_handler.dm
new file mode 100644
index 00000000000..7ed738ba8c6
--- /dev/null
+++ b/code/datums/components/fov_handler.dm
@@ -0,0 +1,142 @@
+/// Component which handles Field of View masking for clients. FoV attributes are at /mob/living
+/datum/component/fov_handler
+ /// Currently applied x size of the fov masks
+ var/current_fov_x = BASE_FOV_MASK_X_DIMENSION
+ /// Currently applied y size of the fov masks
+ var/current_fov_y = BASE_FOV_MASK_Y_DIMENSION
+ /// Whether we are applying the masks now
+ var/applied_mask = FALSE
+ /// The angle of the mask we are applying
+ var/fov_angle = FOV_180_DEGREES
+ /// The blocker mask applied to a client's screen
+ var/atom/movable/screen/fov_blocker/blocker_mask
+ /// The shadow mask applied to a client's screen
+ var/atom/movable/screen/fov_shadow/visual_shadow
+
+/datum/component/fov_handler/Initialize(fov_type = FOV_180_DEGREES)
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/mob/living/mob_parent = parent
+ var/client/parent_client = mob_parent.client
+ if(!parent_client) //Love client volatility!!
+ qdel(src) //no QDEL hint for components, and we dont want this to print a warning regarding bad component application
+ return
+
+ blocker_mask = new
+ visual_shadow = new
+ //visual_shadow.alpha = parent_client?.prefs.read_preference(/datum/preference/numeric/fov_darkness) //SKYRAT EDIT REMOVAL
+ update_visual_shadow_alpha() //SKYRAT EDIT ADDITION
+ set_fov_angle(fov_type)
+ on_dir_change(mob_parent, mob_parent.dir, mob_parent.dir)
+ update_fov_size()
+ update_mask()
+
+/datum/component/fov_handler/Destroy()
+ if(applied_mask)
+ remove_mask()
+ if(blocker_mask) // In a case of early deletion due to volatile client
+ QDEL_NULL(blocker_mask)
+ if(visual_shadow) // In a case of early deletion due to volatile client
+ QDEL_NULL(visual_shadow)
+ return ..()
+
+/datum/component/fov_handler/proc/set_fov_angle(new_angle)
+ fov_angle = new_angle
+ blocker_mask.icon_state = "[fov_angle]"
+ visual_shadow.icon_state = "[fov_angle]_v"
+
+/// Updates the size of the FOV masks by comparing them to client view size.
+/datum/component/fov_handler/proc/update_fov_size()
+ SIGNAL_HANDLER
+ var/mob/parent_mob = parent
+ var/client/parent_client = parent_mob.client
+ if(!parent_client) //Love client volatility!!
+ return
+ var/list/view_size = getviewsize(parent_client.view)
+ if(view_size[1] == current_fov_x && view_size[2] == current_fov_y)
+ return
+ current_fov_x = BASE_FOV_MASK_X_DIMENSION
+ current_fov_y = BASE_FOV_MASK_Y_DIMENSION
+ var/matrix/new_matrix = new
+ var/x_shift = view_size[1] - current_fov_x
+ var/y_shift = view_size[2] - current_fov_y
+ var/x_scale = view_size[1] / current_fov_x
+ var/y_scale = view_size[2] / current_fov_y
+ current_fov_x = view_size[1]
+ current_fov_y = view_size[2]
+ visual_shadow.transform = blocker_mask.transform = new_matrix.Scale(x_scale, y_scale)
+ visual_shadow.transform = blocker_mask.transform = new_matrix.Translate(x_shift * 16, y_shift * 16)
+
+/// Updates the mask application to client by checking `stat` and `eye`
+/datum/component/fov_handler/proc/update_mask()
+ SIGNAL_HANDLER
+ var/mob/parent_mob = parent
+ var/client/parent_client = parent_mob.client
+ if(!parent_client) //Love client volatility!!
+ return
+ var/user_living = parent_mob != DEAD
+ var/atom/top_most_atom = get_atom_on_turf(parent_mob)
+ var/user_extends_eye = parent_client.eye != top_most_atom
+ var/should_apply_mask = user_living && !user_extends_eye
+
+ if(should_apply_mask == applied_mask)
+ return
+
+ if(should_apply_mask)
+ add_mask()
+ else
+ remove_mask()
+
+/datum/component/fov_handler/proc/remove_mask()
+ var/mob/parent_mob = parent
+ var/client/parent_client = parent_mob.client
+ if(!parent_client) //Love client volatility!!
+ return
+ applied_mask = FALSE
+ parent_client.screen -= blocker_mask
+ parent_client.screen -= visual_shadow
+
+/datum/component/fov_handler/proc/add_mask()
+ var/mob/parent_mob = parent
+ var/client/parent_client = parent_mob.client
+ if(!parent_client) //Love client volatility!!
+ return
+ applied_mask = TRUE
+ parent_client.screen += blocker_mask
+ parent_client.screen += visual_shadow
+
+/// When a direction of the user changes, so do the masks
+/datum/component/fov_handler/proc/on_dir_change(mob/source, old_dir, new_dir)
+ SIGNAL_HANDLER
+ blocker_mask.dir = new_dir
+ visual_shadow.dir = new_dir
+
+/// When a mob logs out, delete the component
+/datum/component/fov_handler/proc/mob_logout(mob/source)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/datum/component/fov_handler/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, .proc/on_dir_change)
+ RegisterSignal(parent, COMSIG_LIVING_DEATH, .proc/update_mask)
+ RegisterSignal(parent, COMSIG_LIVING_REVIVE, .proc/update_mask)
+ RegisterSignal(parent, COMSIG_MOB_CLIENT_CHANGE_VIEW, .proc/update_fov_size)
+ RegisterSignal(parent, COMSIG_MOB_RESET_PERSPECTIVE, .proc/update_mask)
+ RegisterSignal(parent, COMSIG_MOB_LOGOUT, .proc/mob_logout)
+ RegisterSignal(parent, COMSIG_LIVING_COMBAT_MODE_TOGGLE, .proc/update_visual_shadow_alpha) //SKYRAT EDIT ADDITION
+
+/datum/component/fov_handler/UnregisterFromParent()
+ . = ..()
+ UnregisterSignal(parent, list(COMSIG_MOB_RESET_PERSPECTIVE, COMSIG_ATOM_DIR_CHANGE, COMSIG_LIVING_DEATH, COMSIG_LIVING_REVIVE, COMSIG_MOB_LOGOUT))
+ UnregisterSignal(parent, COMSIG_LIVING_COMBAT_MODE_TOGGLE) //SKYRAT EDIT ADDITION
+
+//SKYRAT EDIT ADDITION BEGIN
+/// When toggling combat mode, we update the alpha of the shadow mask
+/datum/component/fov_handler/proc/update_visual_shadow_alpha()
+ SIGNAL_HANDLER
+ var/mob/living/parent_mob = parent
+ var/pref_to_read = parent_mob.combat_mode ? /datum/preference/numeric/fov_darkness : /datum/preference/numeric/out_of_combat_fov_darkness
+ var/target_alpha = parent_mob.client.prefs.read_preference(pref_to_read)
+ visual_shadow.alpha = target_alpha
+//SKYRAT EDIT ADDITION END
diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm
index e47c03a813c..a529025c967 100644
--- a/code/datums/elements/footstep.dm
+++ b/code/datums/elements/footstep.dm
@@ -117,7 +117,7 @@
return
playsound(source_loc, pick(footstep_sounds[turf_footstep][1]), footstep_sounds[turf_footstep][2] * volume, TRUE, footstep_sounds[turf_footstep][3] + e_range, falloff_distance = 1, vary = sound_vary)
-/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source)
+/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source, atom/oldloc, direction)
SIGNAL_HANDLER
if (SHOULD_DISABLE_FOOTSTEPS(source))
@@ -134,6 +134,7 @@
if(!source_loc)
return
+ play_fov_effect(source, 5, "footstep", direction, ignore_self = TRUE)
if ((source.wear_suit?.body_parts_covered | source.w_uniform?.body_parts_covered | source.shoes?.body_parts_covered) & FEET)
// we are wearing shoes
playsound(source_loc, pick(GLOB.footstep[source_loc.footstep][1]),
diff --git a/code/datums/elements/item_fov.dm b/code/datums/elements/item_fov.dm
new file mode 100644
index 00000000000..57d93f4f537
--- /dev/null
+++ b/code/datums/elements/item_fov.dm
@@ -0,0 +1,34 @@
+/// An element to unconditonally add a FOV trait to the wearer, removing it when an item is unequipped
+/datum/element/item_fov
+ element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
+ id_arg_index = 2
+ /// Angle of the FoV we will apply when someone wears the clothing this element is attached to.
+ var/fov_angle
+
+/datum/element/item_fov/Attach(datum/target, fov_angle)
+ . = ..()
+ if(!isitem(target))
+ return ELEMENT_INCOMPATIBLE
+ src.fov_angle = fov_angle
+
+ RegisterSignal(target, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
+ RegisterSignal(target, COMSIG_ITEM_DROPPED, .proc/on_drop)
+
+/datum/element/item_fov/Detach(datum/target)
+ UnregisterSignal(target, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED))
+ return ..()
+
+/// On dropping the item, remove the FoV trait.
+/datum/element/item_fov/proc/on_drop(datum/source, mob/living/dropper)
+ SIGNAL_HANDLER
+ dropper.remove_fov_trait(source.type, fov_angle)
+ dropper.update_fov()
+
+/// On equipping the item, add the FoV trait.
+/datum/element/item_fov/proc/on_equip(obj/item/source, mob/living/equipper, slot)
+ SIGNAL_HANDLER
+ if(!(source.slot_flags & slot)) //If EQUIPPED TO HANDS FOR EXAMPLE
+ return
+
+ equipper.add_fov_trait(source.type, fov_angle)
+ equipper.update_fov()
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 0a7a9a8b423..71f1eebacbc 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -2115,6 +2115,14 @@
//We inline a MAPTEXT() here, because there's no good way to statically add to a string like this
active_hud.screentip_text.maptext = "[name]"
+ ///SKYRAT EDIT ADDITION BEGIN
+ // Face directions on combat mode. No procs, no typechecks, just a var for speed
+ var/mob/user_mob = usr
+ if(user_mob.face_mouse)
+ user_mob.face_atom(src)
+ ///SKYRAT EDIT ADDITION END
+
+
/// Gets a merger datum representing the connected blob of objects in the allowed_types argument
/atom/proc/GetMergeGroup(id, list/allowed_types)
RETURN_TYPE(/datum/merger)
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 54d818ac86e..a72fbaf7352 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -81,6 +81,9 @@
/// The degree of pressure protection that mobs in list/contents have from the external environment, between 0 and 1
var/contents_pressure_protection = 0
+ /// Whether a user will face atoms on entering them with a mouse. Despite being a mob variable, it is here for performances //SKYRAT EDIT ADDITION
+ var/face_mouse = FALSE //SKYRAT EDIT ADDITION
+
/atom/movable/Initialize(mapload)
. = ..()
@@ -446,7 +449,7 @@
if(!direction)
direction = get_dir(src, newloc)
- if(set_dir_on_move)
+ if(set_dir_on_move && !face_mouse)
setDir(direction)
var/is_multi_tile_object = bound_width > 32 || bound_height > 32
@@ -573,7 +576,7 @@
moving_diagonally = SECOND_DIAG_STEP
. = step(src, SOUTH)
if(moving_diagonally == SECOND_DIAG_STEP)
- if(!. && set_dir_on_move)
+ if(!. && set_dir_on_move && !face_mouse)
setDir(first_step_dir)
else if (!inertia_moving)
inertia_next_move = world.time + inertia_move_delay
@@ -613,7 +616,7 @@
last_move = direct
- if(set_dir_on_move)
+ if(set_dir_on_move && !face_mouse)
setDir(direct)
if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) //movement failed due to buckled mob(s)
. = FALSE
@@ -1079,7 +1082,7 @@
return
-/atom/movable/proc/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect)
+/atom/movable/proc/do_attack_animation(atom/attacked_atom, visual_effect_icon, obj/item/used_item, no_effect, fov_effect = TRUE)
if(!no_effect && (visual_effect_icon || used_item))
do_item_attack_animation(attacked_atom, visual_effect_icon, used_item)
@@ -1103,6 +1106,9 @@
pixel_x_diff = -8
turn_dir = -1
+ if(fov_effect)
+ play_fov_effect(attacked_atom, 5, "attack")
+
var/matrix/initial_transform = matrix(transform)
var/matrix/rotated_transform = transform.Turn(15 * turn_dir)
animate(src, pixel_x = pixel_x + pixel_x_diff, pixel_y = pixel_y + pixel_y_diff, transform=rotated_transform, time = 1, easing=BACK_EASING|EASE_IN, flags = ANIMATION_PARALLEL)
diff --git a/code/game/objects/effects/effect_system/effects_smoke.dm b/code/game/objects/effects/effect_system/effects_smoke.dm
index a69a082c18e..f3f1f01cc71 100644
--- a/code/game/objects/effects/effect_system/effects_smoke.dm
+++ b/code/game/objects/effects/effect_system/effects_smoke.dm
@@ -9,6 +9,7 @@
pixel_x = -32
pixel_y = -32
opacity = FALSE
+ plane = ABOVE_GAME_PLANE
layer = FLY_LAYER
anchored = TRUE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
diff --git a/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm b/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm
index f0c25219c13..65654533e9e 100644
--- a/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm
+++ b/code/game/objects/effects/temporary_visuals/projectiles/projectile_effects.dm
@@ -3,6 +3,7 @@
icon = 'icons/obj/guns/projectiles.dmi'
icon_state = "nothing"
layer = ABOVE_MOB_LAYER
+ plane = GAME_PLANE_FOV_HIDDEN
anchored = TRUE
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
appearance_flags = 0
diff --git a/code/game/objects/structures/beds_chairs/chair.dm b/code/game/objects/structures/beds_chairs/chair.dm
index a9a5b582306..7d0fec987ce 100644
--- a/code/game/objects/structures/beds_chairs/chair.dm
+++ b/code/game/objects/structures/beds_chairs/chair.dm
@@ -186,6 +186,7 @@
/obj/structure/chair/comfy/Initialize(mapload)
armrest = GetArmrest()
armrest.layer = ABOVE_MOB_LAYER
+ armrest.plane = ABOVE_GAME_PLANE
return ..()
/obj/structure/chair/comfy/proc/GetArmrest()
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 076a5e0d940..fb6f32d32ae 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -203,6 +203,7 @@ GLOBAL_PROTECT(admin_verbs_debug)
/client/proc/cmd_sdql_spell_menu,
/client/proc/adventure_manager,
/client/proc/load_circuit,
+ /client/proc/cmd_admin_toggle_fov,
)
GLOBAL_LIST_INIT(admin_verbs_possess, list(/proc/possess, /proc/release))
GLOBAL_PROTECT(admin_verbs_possess)
diff --git a/code/modules/admin/verbs/fov.dm b/code/modules/admin/verbs/fov.dm
new file mode 100644
index 00000000000..f74ba6f8058
--- /dev/null
+++ b/code/modules/admin/verbs/fov.dm
@@ -0,0 +1,17 @@
+/client/proc/cmd_admin_toggle_fov()
+ set name = "Enable/Disable Field of View"
+ set category = "Debug"
+
+ if(!check_rights(R_ADMIN) || !check_rights(R_DEBUG))
+ return
+
+ var/on_off = CONFIG_GET(flag/native_fov)
+
+ message_admins("[key_name_admin(usr)] has [on_off ? "disabled" : "enabled"] the Native Field of View configuration..")
+ log_admin("[key_name(usr)] has [on_off ? "disabled" : "enabled"] the Native Field of View configuration.")
+ CONFIG_SET(flag/native_fov, !on_off)
+
+ SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggled Field of View", "[on_off ? "Enabled" : "Disabled"]"))
+
+ for(var/mob/living/mob in GLOB.player_list)
+ mob.update_fov()
diff --git a/code/modules/client/preferences/ambient_occlusion.dm b/code/modules/client/preferences/ambient_occlusion.dm
index a81efca00bd..33c87c188d7 100644
--- a/code/modules/client/preferences/ambient_occlusion.dm
+++ b/code/modules/client/preferences/ambient_occlusion.dm
@@ -5,6 +5,7 @@
savefile_identifier = PREFERENCE_PLAYER
/datum/preference/toggle/ambient_occlusion/apply_to_client(client/client, value)
+ /// Backdrop for the game world plane.
var/atom/movable/screen/plane_master/game_world/plane_master = locate() in client?.screen
if (!plane_master)
return
diff --git a/code/modules/client/preferences/fov_darkness.dm b/code/modules/client/preferences/fov_darkness.dm
new file mode 100644
index 00000000000..b0146e10b40
--- /dev/null
+++ b/code/modules/client/preferences/fov_darkness.dm
@@ -0,0 +1,17 @@
+/datum/preference/numeric/fov_darkness
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "fov_darkness"
+ savefile_identifier = PREFERENCE_PLAYER
+
+ minimum = 0
+ maximum = 255
+
+/datum/preference/numeric/fov_darkness/create_default_value()
+ return 255
+
+/datum/preference/numeric/fov_darkness/apply_to_client_updated(client/client, value)
+ if(client.mob)
+ var/datum/component/fov_handler/fov_component = client.mob.GetComponent(/datum/component/fov_handler)
+ if(!fov_component)
+ return
+ fov_component.visual_shadow.alpha = value
diff --git a/code/modules/clothing/clothing.dm b/code/modules/clothing/clothing.dm
index fa3d9b97726..8d88762f991 100644
--- a/code/modules/clothing/clothing.dm
+++ b/code/modules/clothing/clothing.dm
@@ -246,7 +246,7 @@
QDEL_NULL(moth_snack)
return ..()
-/obj/item/clothing/dropped(mob/user)
+/obj/item/clothing/dropped(mob/living/user)
..()
if(!istype(user))
return
@@ -254,6 +254,7 @@
for(var/trait in clothing_traits)
REMOVE_TRAIT(user, trait, "[CLOTHING_TRAIT] [REF(src)]")
+
if(LAZYLEN(user_vars_remembered))
for(var/variable in user_vars_remembered)
if(variable in user.vars)
@@ -261,7 +262,7 @@
user.vars[variable] = user_vars_remembered[variable]
user_vars_remembered = initial(user_vars_remembered) // Effectively this sets it to null.
-/obj/item/clothing/equipped(mob/user, slot)
+/obj/item/clothing/equipped(mob/living/user, slot)
. = ..()
if (!istype(user))
return
@@ -463,6 +464,7 @@ BLIND // can't see anything
/obj/item/clothing/proc/visor_toggling() //handles all the actual toggling of flags
up = !up
+ SEND_SIGNAL(src, COMSIG_CLOTHING_VISOR_TOGGLE, up)
clothing_flags ^= visor_flags
flags_inv ^= visor_flags_inv
flags_cover ^= initial(flags_cover)
@@ -474,6 +476,7 @@ BLIND // can't see anything
/obj/item/clothing/head/helmet/space/plasmaman/visor_toggling() //handles all the actual toggling of flags
up = !up
+ SEND_SIGNAL(src, COMSIG_CLOTHING_VISOR_TOGGLE, up)
clothing_flags ^= visor_flags
flags_inv ^= visor_flags_inv
icon_state = "[initial(icon_state)]"
diff --git a/code/modules/clothing/masks/gasmask.dm b/code/modules/clothing/masks/gasmask.dm
index 59a4d35d8e1..ef6c93a79bb 100644
--- a/code/modules/clothing/masks/gasmask.dm
+++ b/code/modules/clothing/masks/gasmask.dm
@@ -18,6 +18,7 @@
/obj/item/clothing/mask/gas/Initialize(mapload)
. = ..()
+ init_fov()
if(!max_filters || !starting_filter_type)
return
@@ -75,6 +76,10 @@
has_filter = FALSE
return filtered_breath
+/// Initializes the FoV component for the gas mask
+/obj/item/clothing/mask/gas/proc/init_fov()
+ AddComponent(/datum/component/clothing_fov_visor, FOV_90_DEGREES)
+
/**
* Getter for overall filter durability, takes into consideration all filters filter_status
*/
@@ -173,6 +178,9 @@
species_exception = list(/datum/species/golem/bananium)
var/list/clownmask_designs = list()
+/obj/item/clothing/mask/gas/clown_hat/init_fov()
+ return
+
/obj/item/clothing/mask/gas/clown_hat/plasmaman
starting_filter_type = /obj/item/gas_filter/plasmaman
@@ -219,6 +227,9 @@
resistance_flags = FLAMMABLE
species_exception = list(/datum/species/golem/bananium)
+/obj/item/clothing/mask/gas/sexyclown/init_fov()
+ return
+
/obj/item/clothing/mask/gas/mime
name = "mime mask"
desc = "The traditional mime's mask. It has an eerie facial posture."
@@ -232,6 +243,9 @@
species_exception = list(/datum/species/golem)
var/list/mimemask_designs = list()
+/obj/item/clothing/mask/gas/mime/init_fov()
+ return
+
/obj/item/clothing/mask/gas/mime/plasmaman
starting_filter_type = /obj/item/gas_filter/plasmaman
@@ -284,6 +298,9 @@
resistance_flags = FLAMMABLE
species_exception = list(/datum/species/golem)
+/obj/item/clothing/mask/gas/sexymime/init_fov()
+ return
+
/obj/item/clothing/mask/gas/cyborg
name = "cyborg visor"
desc = "Beep boop."
diff --git a/code/modules/mining/equipment/explorer_gear.dm b/code/modules/mining/equipment/explorer_gear.dm
index abb9bebce45..74bae5e91af 100644
--- a/code/modules/mining/equipment/explorer_gear.dm
+++ b/code/modules/mining/equipment/explorer_gear.dm
@@ -46,6 +46,9 @@
armor = list(MELEE = 10, BULLET = 5, LASER = 5, ENERGY = 5, BOMB = 0, BIO = 50, FIRE = 20, ACID = 40, WOUND = 5)
resistance_flags = FIRE_PROOF
+/obj/item/clothing/mask/gas/explorer/init_fov()
+ return
+
/obj/item/clothing/mask/gas/explorer/attack_self(mob/user)
adjustmask(user)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 9c3623379de..e9db404eb9c 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -10,6 +10,7 @@
faction += "[REF(src)]"
GLOB.mob_living_list += src
SSpoints_of_interest.make_point_of_interest(src)
+ update_fov()
/mob/living/ComponentInitialize()
. = ..()
@@ -753,7 +754,6 @@
adjustToxLoss(-20, TRUE, TRUE) //slime friendly
updatehealth()
grab_ghost()
- SEND_SIGNAL(src, COMSIG_LIVING_REVIVE, full_heal, admin_revive)
if(full_heal)
fully_heal(admin_revive = admin_revive)
if(stat == DEAD && can_be_revived()) //in some cases you can't revive (e.g. no brain)
@@ -776,6 +776,8 @@
else if(admin_revive)
updatehealth()
get_up(TRUE)
+ // The signal is called after everything else so components can properly check the updated values
+ SEND_SIGNAL(src, COMSIG_LIVING_REVIVE, full_heal, admin_revive)
/mob/living/proc/remove_CC()
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index d52027bd65a..aebd1fe71cd 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -77,6 +77,7 @@
return
. = combat_mode
combat_mode = new_mode
+ SEND_SIGNAL(src, COMSIG_LIVING_COMBAT_MODE_TOGGLE, new_mode) //SKYRAT EDIT ADDITION
if(hud_used?.action_intent)
hud_used.action_intent.update_appearance()
//SKYRAT EDIT ADDITION BEGIN
@@ -92,6 +93,7 @@
set_combat_indicator(TRUE)
else
set_combat_indicator(FALSE)
+ face_mouse = (client?.prefs?.read_preference(/datum/preference/toggle/face_cursor_combat_mode) && combat_mode) ? TRUE : FALSE
//SKYRAT EDIT ADDITION END
if(silent || !(client?.prefs.toggles & SOUND_COMBATMODE))
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index ba6394b3df0..d991d8e3dca 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -4,6 +4,7 @@
see_in_dark = 2
hud_possible = list(HEALTH_HUD,STATUS_HUD,ANTAG_HUD)
pressure_resistance = 10
+ plane = GAME_PLANE //SKYRAT EDIT CHANGE
hud_type = /datum/hud/living
@@ -171,3 +172,10 @@
var/body_position_pixel_x_offset = 0
///The x amount a mob's sprite should be offset due to the current position they're in
var/body_position_pixel_y_offset = 0
+
+ /// FOV view that is applied from either nativeness or traits
+ var/fov_view
+ /// Native FOV that will be applied if a config is enabled
+ var/native_fov = FOV_180_DEGREES //SKYRAT EDIT CHANGE
+ /// Lazy list of FOV traits that will apply a FOV view when handled.
+ var/list/fov_traits
diff --git a/code/modules/mob/living/living_fov.dm b/code/modules/mob/living/living_fov.dm
new file mode 100644
index 00000000000..96bcff79191
--- /dev/null
+++ b/code/modules/mob/living/living_fov.dm
@@ -0,0 +1,136 @@
+/// Is `observed_atom` in a mob's field of view? This takes blindness, nearsightness and FOV into consideration
+/mob/living/proc/in_fov(atom/observed_atom, ignore_self = FALSE)
+ if(ignore_self && observed_atom == src)
+ return TRUE
+ if(is_blind())
+ return FALSE
+ . = FALSE
+ var/turf/my_turf = get_turf(src) //Because being inside contents of something will cause our x,y to not be updated
+ // If turf doesn't exist, then we wouldn't get a fov check called by `play_fov_effect` or presumably other new stuff that might check this.
+ // ^ If that case has changed and you need that check, add it.
+ var/rel_x = observed_atom.x - my_turf.x
+ var/rel_y = observed_atom.y - my_turf.y
+ if(fov_view && observed_atom.plane == GAME_PLANE_FOV_HIDDEN) //SKYRAT EDIT CHANGE
+ if(rel_x >= -1 && rel_x <= 1 && rel_y >= -1 && rel_y <= 1) //Cheap way to check inside that 3x3 box around you
+ return TRUE //Also checks if both are 0 to stop division by zero
+
+ // Get the vector length so we can create a good directional vector
+ var/vector_len = sqrt(abs(rel_x) ** 2 + abs(rel_y) ** 2)
+
+ /// Getting a direction vector
+ var/dir_x
+ var/dir_y
+ switch(dir)
+ if(SOUTH)
+ dir_x = 0
+ dir_y = -vector_len
+ if(NORTH)
+ dir_x = 0
+ dir_y = vector_len
+ if(EAST)
+ dir_x = vector_len
+ dir_y = 0
+ if(WEST)
+ dir_x = -vector_len
+ dir_y = 0
+
+ ///Calculate angle
+ var/angle = arccos((dir_x * rel_x + dir_y * rel_y) / (sqrt(dir_x**2 + dir_y**2) * sqrt(rel_x**2 + rel_y**2)))
+
+ /// Calculate vision angle and compare
+ var/vision_angle = (360 - fov_view) / 2
+ if(angle < vision_angle)
+ . = TRUE
+ else
+ . = TRUE
+
+ // Handling nearsightnedness
+ if(. && HAS_TRAIT(src, TRAIT_NEARSIGHT))
+ //Checking if our dude really is suffering from nearsightness! (very nice nearsightness code)
+ if(iscarbon(src))
+ var/mob/living/carbon/carbon_me = src
+ if(carbon_me.glasses)
+ var/obj/item/clothing/glasses/glass = carbon_me.glasses
+ if(glass.vision_correction)
+ return
+ if((rel_x >= NEARSIGHTNESS_FOV_BLINDNESS || rel_x <= -NEARSIGHTNESS_FOV_BLINDNESS) || (rel_y >= NEARSIGHTNESS_FOV_BLINDNESS || rel_y <= -NEARSIGHTNESS_FOV_BLINDNESS))
+ return FALSE
+
+/// Updates the applied FOV value and applies the handler to client if able
+/mob/living/proc/update_fov()
+ var/highest_fov
+ if(CONFIG_GET(flag/native_fov))
+ highest_fov = native_fov
+ for(var/trait_type in fov_traits)
+ var/fov_type = fov_traits[trait_type]
+ if(fov_type > highest_fov)
+ highest_fov = fov_type
+ fov_view = highest_fov
+ update_fov_client()
+
+/// Updates the FOV for the client.
+/mob/living/proc/update_fov_client()
+ if(!client)
+ return
+ var/datum/component/fov_handler/fov_component = GetComponent(/datum/component/fov_handler)
+ if(fov_view)
+ if(!fov_component)
+ AddComponent(/datum/component/fov_handler, fov_view)
+ else
+ fov_component.set_fov_angle(fov_view)
+ else if(fov_component)
+ qdel(fov_component)
+
+/// Adds a trait which limits a user's FOV
+/mob/living/proc/add_fov_trait(source, type)
+ LAZYINITLIST(fov_traits)
+ fov_traits[source] = type
+ update_fov()
+
+/// Removes a trait which limits a user's FOV
+/mob/living/proc/remove_fov_trait(source, type)
+ if(!fov_traits) //Clothing equip/unequip is bad code and invokes this several times
+ return
+ fov_traits -= source
+ UNSETEMPTY(fov_traits)
+ update_fov()
+
+/// Plays a visual effect representing a sound cue for people with vision obstructed by FOV or blindness
+/proc/play_fov_effect(atom/center, range, icon_state, dir = SOUTH, ignore_self = FALSE, angle = 0)
+ var/turf/anchor_point = get_turf(center)
+ var/image/fov_image
+ for(var/mob/living/living_mob in get_hearers_in_view(range, center))
+ var/client/mob_client = living_mob.client
+ if(!mob_client)
+ continue
+ if(HAS_TRAIT(living_mob, TRAIT_DEAF)) //Deaf people can't hear sounds so no sound indicators
+ continue
+ if(living_mob.in_fov(center, ignore_self))
+ continue
+ if(!fov_image) //Make the image once we found one recipient to receive it
+ fov_image = image(icon = 'icons/effects/fov/fov_effects.dmi', icon_state = icon_state, loc = anchor_point)
+ fov_image.plane = FULLSCREEN_PLANE
+ fov_image.layer = FOV_EFFECTS_LAYER
+ fov_image.dir = dir
+ fov_image.appearance_flags = RESET_COLOR | RESET_TRANSFORM
+ if(angle)
+ var/matrix/matrix = new
+ matrix.Turn(angle)
+ fov_image.transform = matrix
+ fov_image.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ mob_client.images += fov_image
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_image_from_client, fov_image, mob_client), 30)
+
+/atom/movable/screen/fov_blocker
+ icon = 'icons/effects/fov/field_of_view.dmi'
+ icon_state = "90"
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ plane = FIELD_OF_VISION_BLOCKER_PLANE
+ screen_loc = "BOTTOM,LEFT"
+
+/atom/movable/screen/fov_shadow
+ icon = 'icons/effects/fov/field_of_view.dmi'
+ icon_state = "90_v"
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ plane = ABOVE_LIGHTING_PLANE
+ screen_loc = "BOTTOM,LEFT"
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index bbe97d6aaef..7d34fab011a 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -26,3 +26,5 @@
changeling.regain_powers()
med_hud_set_status()
+
+ update_fov_client()
diff --git a/code/modules/mob/living/silicon/ai/ai.dm b/code/modules/mob/living/silicon/ai/ai.dm
index 3e0df35925e..0a5124a9e2a 100644
--- a/code/modules/mob/living/silicon/ai/ai.dm
+++ b/code/modules/mob/living/silicon/ai/ai.dm
@@ -30,6 +30,7 @@
mob_size = MOB_SIZE_LARGE
radio = /obj/item/radio/headset/silicon/ai
can_buckle_to = FALSE
+ native_fov = null
var/battery = 200 //emergency power if the AI's APC is off
var/list/network = list("ss13")
var/obj/machinery/camera/current
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
index 11ab84a2743..27fbb7aaaa3 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
@@ -263,6 +263,7 @@
eyeblur = 0
damage_type = BRUTE
pass_flags = PASSTABLE
+ plane = GAME_PLANE
var/explode_hit_objects = TRUE
/obj/projectile/colossus/can_hit_target(atom/target, direct_target = FALSE, ignore_loc = FALSE, cross_failed = FALSE)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 235e529658a..0553b5a6664 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -449,6 +449,7 @@
client.perspective = EYE_PERSPECTIVE
client.eye = loc
return 1
+ SEND_SIGNAL(src, COMSIG_MOB_RESET_PERSPECTIVE)
/**
* Examine a mob
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 7d75a7323e6..e1995fe115a 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -501,6 +501,7 @@
return FALSE
//SKYRAT EDIT ADDITION END
m_intent = MOVE_INTENT_RUN
+ plane = (m_intent == MOVE_INTENT_WALK) ? GAME_PLANE_FOV_HIDDEN : GAME_PLANE //SKYRAT EDIT ADDITION
if(hud_used?.static_inventory)
for(var/atom/movable/screen/mov_intent/selector in hud_used.static_inventory)
selector.update_appearance()
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index e4d495add93..a32ede8a9d7 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -12,6 +12,7 @@
wound_bonus = CANT_WOUND // can't wound by default
generic_canpass = FALSE
blocks_emissive = EMISSIVE_BLOCK_GENERIC
+ plane = GAME_PLANE_FOV_HIDDEN
//The sound this plays on impact.
var/hitsound = 'sound/weapons/pierce.ogg'
var/hitsound_wall = ""
@@ -689,6 +690,7 @@
trajectory = new(starting.x, starting.y, starting.z, pixel_x, pixel_y, Angle, SSprojectiles.global_pixel_speed)
last_projectile_move = world.time
fired = TRUE
+ play_fov_effect(starting, 6, "gunfire", dir = NORTH, angle = Angle)
SEND_SIGNAL(src, COMSIG_PROJECTILE_FIRE)
if(hitscan)
process_hitscan()
diff --git a/code/modules/vehicles/_vehicle.dm b/code/modules/vehicles/_vehicle.dm
index 8f652cc92d2..aa14db17c2a 100644
--- a/code/modules/vehicles/_vehicle.dm
+++ b/code/modules/vehicles/_vehicle.dm
@@ -5,6 +5,7 @@
icon_state = "fuckyou"
max_integrity = 300
armor = list(MELEE = 30, BULLET = 30, LASER = 30, ENERGY = 0, BOMB = 30, BIO = 0, FIRE = 60, ACID = 60)
+ plane = GAME_PLANE_FOV_HIDDEN
density = TRUE
anchored = FALSE
blocks_emissive = EMISSIVE_BLOCK_GENERIC
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index 1c68ac359cf..0df887e66f6 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -702,7 +702,7 @@
if(dir != direction && !strafe || forcerotate || keyheld)
if(dir != direction && !(mecha_flags & QUIET_TURNS) && !step_silent)
playsound(src,turnsound,40,TRUE)
- setDir(direction)
+ set_dir_mecha(direction)
return TRUE
set_glide_size(DELAY_TO_GLIDE_SIZE(movedelay))
@@ -711,7 +711,7 @@
if(phasing)
use_power(phasing_energy_drain)
if(strafe)
- setDir(olddir)
+ set_dir_mecha(olddir)
/obj/vehicle/sealed/mecha/Bump(atom/obstacle)
@@ -976,7 +976,7 @@
newoccupant.update_mouse_pointer()
add_fingerprint(newoccupant)
log_message("[newoccupant] moved in as pilot.", LOG_MECHA)
- setDir(dir_in)
+ set_dir_mecha(dir_in)
playsound(src, 'sound/machines/windowdoor.ogg', 50, TRUE)
if(!internal_damage)
SEND_SOUND(newoccupant, sound('sound/mecha/nominal.ogg',volume=50))
@@ -1023,7 +1023,7 @@
brain_mob.reset_perspective(src)
brain_mob.remote_control = src
brain_mob.update_mouse_pointer()
- setDir(dir_in)
+ set_dir_mecha(dir_in)
log_message("[brain_obj] moved in as pilot.", LOG_MECHA)
if(!internal_damage)
SEND_SOUND(brain_obj, sound('sound/mecha/nominal.ogg',volume=50))
@@ -1090,7 +1090,7 @@
remove_occupant(ejector)
mmi.set_mecha(null)
mmi.update_appearance()
- setDir(dir_in)
+ set_dir_mecha(dir_in)
return ..()
@@ -1253,3 +1253,9 @@
for(var/occupant in occupants)
remove_action_type_from_mob(/datum/action/vehicle/sealed/mecha/mech_toggle_lights, occupant)
return COMPONENT_BLOCK_LIGHT_EATER
+
+/// Sets the direction of the mecha and all of its occcupents, required for FOV. Alternatively one could make a recursive contents registration and register topmost direction changes in the fov component
+/obj/vehicle/sealed/mecha/proc/set_dir_mecha(new_dir)
+ setDir(new_dir)
+ for(var/mob/living/occupant as anything in occupants)
+ occupant.setDir(new_dir)
diff --git a/config/game_options.txt b/config/game_options.txt
index 04ab833bf54..e9d801b8dc9 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -502,3 +502,6 @@ MAXFINE 2000
## Warning: SDQL is a powerful tool and can break many things or expose security sensitive information.
## Giving players access to it has major security concerns, be careful and deliberate when using this feature.
#SDQL_SPELLS
+
+## Whether native FoV is enabled for all people.
+#NATIVE_FOV
diff --git a/icons/effects/fov/field_of_view.dmi b/icons/effects/fov/field_of_view.dmi
new file mode 100644
index 00000000000..8086773d140
Binary files /dev/null and b/icons/effects/fov/field_of_view.dmi differ
diff --git a/icons/effects/fov/fov_effects.dmi b/icons/effects/fov/fov_effects.dmi
new file mode 100644
index 00000000000..d5b7de38a45
Binary files /dev/null and b/icons/effects/fov/fov_effects.dmi differ
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/face_cursor_combat_mode.dm b/modular_skyrat/master_files/code/modules/client/preferences/face_cursor_combat_mode.dm
new file mode 100644
index 00000000000..1bb5fb99fe6
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/face_cursor_combat_mode.dm
@@ -0,0 +1,5 @@
+/datum/preference/toggle/face_cursor_combat_mode
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ default_value = TRUE
+ savefile_key = "face_cursor_combat_mode"
+ savefile_identifier = PREFERENCE_PLAYER
diff --git a/modular_skyrat/master_files/code/modules/client/preferences/out_of_combat_fov_darkness.dm b/modular_skyrat/master_files/code/modules/client/preferences/out_of_combat_fov_darkness.dm
new file mode 100644
index 00000000000..78a1c6708d6
--- /dev/null
+++ b/modular_skyrat/master_files/code/modules/client/preferences/out_of_combat_fov_darkness.dm
@@ -0,0 +1,10 @@
+/datum/preference/numeric/out_of_combat_fov_darkness
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "out_of_combat_fov_darkness"
+ savefile_identifier = PREFERENCE_PLAYER
+
+ minimum = 0
+ maximum = 255
+
+/datum/preference/numeric/out_of_combat_fov_darkness/create_default_value()
+ return 0
diff --git a/tgstation.dme b/tgstation.dme
index 5f95dd0dfe2..b0f3ba02cf6 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -73,6 +73,7 @@
#include "code\__DEFINES\food.dm"
#include "code\__DEFINES\footsteps.dm"
#include "code\__DEFINES\forensics.dm"
+#include "code\__DEFINES\fov.dm"
#include "code\__DEFINES\hud.dm"
#include "code\__DEFINES\icon_smoothing.dm"
#include "code\__DEFINES\id_cards.dm"
@@ -179,6 +180,7 @@
#include "code\__DEFINES\dcs\signals\signals_area.dm"
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_circuit.dm"
+#include "code\__DEFINES\dcs\signals\signals_clothing.dm"
#include "code\__DEFINES\dcs\signals\signals_container.dm"
#include "code\__DEFINES\dcs\signals\signals_customizable.dm"
#include "code\__DEFINES\dcs\signals\signals_cytology.dm"
@@ -672,6 +674,7 @@
#include "code\datums\components\caltrop.dm"
#include "code\datums\components\chasm.dm"
#include "code\datums\components\clickbox.dm"
+#include "code\datums\components\clothing_fov_visor.dm"
#include "code\datums\components\codeword_hearing.dm"
#include "code\datums\components\combustible_flooder.dm"
#include "code\datums\components\connect_loc_behalf.dm"
@@ -694,6 +697,7 @@
#include "code\datums\components\explodable.dm"
#include "code\datums\components\faction_granter.dm"
#include "code\datums\components\forensics.dm"
+#include "code\datums\components\fov_handler.dm"
#include "code\datums\components\fullauto.dm"
#include "code\datums\components\gas_leaker.dm"
#include "code\datums\components\geiger_sound.dm"
@@ -906,6 +910,7 @@
#include "code\datums\elements\forced_gravity.dm"
#include "code\datums\elements\haunted.dm"
#include "code\datums\elements\honkspam.dm"
+#include "code\datums\elements\item_fov.dm"
#include "code\datums\elements\item_scaling.dm"
#include "code\datums\elements\kneecapping.dm"
#include "code\datums\elements\kneejerk.dm"
@@ -1839,6 +1844,7 @@
#include "code\modules\admin\verbs\diagnostics.dm"
#include "code\modules\admin\verbs\ert.dm"
#include "code\modules\admin\verbs\fix_air.dm"
+#include "code\modules\admin\verbs\fov.dm"
#include "code\modules\admin\verbs\fps.dm"
#include "code\modules\admin\verbs\getlogs.dm"
#include "code\modules\admin\verbs\ghost_pool_protection.dm"
@@ -2272,6 +2278,7 @@
#include "code\modules\client\preferences\buttons_locked.dm"
#include "code\modules\client\preferences\clothing.dm"
#include "code\modules\client\preferences\darkened_flash.dm"
+#include "code\modules\client\preferences\fov_darkness.dm"
#include "code\modules\client\preferences\fps.dm"
#include "code\modules\client\preferences\gender.dm"
#include "code\modules\client\preferences\ghost.dm"
@@ -2951,6 +2958,7 @@
#include "code\modules\mob\living\living.dm"
#include "code\modules\mob\living\living_defense.dm"
#include "code\modules\mob\living\living_defines.dm"
+#include "code\modules\mob\living\living_fov.dm"
#include "code\modules\mob\living\living_movement.dm"
#include "code\modules\mob\living\living_say.dm"
#include "code\modules\mob\living\living_update_icons.dm"
@@ -4177,11 +4185,13 @@
#include "modular_skyrat\master_files\code\modules\client\preferences\cursed_shit.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\delete_sparks.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\erp_preferences.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\face_cursor_combat_mode.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\flavor_text.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\laugh.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\loadout_override_preference.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\looc.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\mutant_parts.dm"
+#include "modular_skyrat\master_files\code\modules\client\preferences\out_of_combat_fov_darkness.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\scream.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\skin_tone.dm"
#include "modular_skyrat\master_files\code\modules\client\preferences\tgui_prefs_migration.dm"
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/fov_darkness.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/fov_darkness.tsx
new file mode 100644
index 00000000000..1315cc88fba
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/fov_darkness.tsx
@@ -0,0 +1,8 @@
+import { Feature, FeatureNumberInput } from "../base";
+
+export const fov_darkness: Feature = {
+ name: "Field of view darkness",
+ category: "GAMEPLAY",
+ description: "The density of darkness of field of vision cones you may have by wearing restrictive eye cover.",
+ component: FeatureNumberInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/face_cursor_combat_mode.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/face_cursor_combat_mode.tsx
new file mode 100644
index 00000000000..283a9225dfc
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/face_cursor_combat_mode.tsx
@@ -0,0 +1,12 @@
+import { multiline } from "common/string";
+import { CheckboxInput, FeatureToggle } from "../../base";
+
+export const face_cursor_combat_mode: FeatureToggle = {
+ name: "Face cursor with combat mode",
+ category: "GAMEPLAY",
+ description: multiline`
+ When toggled, you will now face towards the cursor
+ with combat mode enabled.
+ `,
+ component: CheckboxInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/out_of_combat_fov_darkness.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/out_of_combat_fov_darkness.tsx
new file mode 100644
index 00000000000..235869413a0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/skyrat/out_of_combat_fov_darkness.tsx
@@ -0,0 +1,7 @@
+import { Feature, FeatureNumberInput } from "../../base";
+
+export const out_of_combat_fov_darkness: Feature = {
+ name: "Out of Combat Field of View Darkness",
+ category: "GAMEPLAY",
+ component: FeatureNumberInput,
+};