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 0c6af0d5963..1880fcdf14d 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 827bdd23701..6a1d422fba6 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,6 +22,7 @@ #define FLOOR_PLANE -7 #define GAME_PLANE -4 +#define GAME_PLANE_FOV_HIDDEN -3 #define SPACE_LAYER 1.8 //#define TURF_LAYER 2 //For easy recordkeeping; this is a byond define @@ -147,6 +151,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/_onclick/hud/rendering/plane_master.dm b/code/_onclick/hud/rendering/plane_master.dm index a952ad59853..351dbe3145e 100644 --- a/code/_onclick/hud/rendering/plane_master.dm +++ b/code/_onclick/hud/rendering/plane_master.dm @@ -72,6 +72,13 @@ 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 + +/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/massive_obj name = "massive object plane master" @@ -235,3 +242,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/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm index 5da4cde7132..ebf26629151 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..0402913f1d9 --- /dev/null +++ b/code/datums/components/fov_handler.dm @@ -0,0 +1,128 @@ +/// 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) + 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/user_extends_eye = parent_client.eye != parent_mob + 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) + +/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)) 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_movable.dm b/code/game/atoms_movable.dm index 8fa17b08f95..2c777dc51cf 100644 --- a/code/game/atoms_movable.dm +++ b/code/game/atoms_movable.dm @@ -1076,7 +1076,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) @@ -1100,6 +1100,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/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/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index afe03698bfb..529a45a32e5 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -189,6 +189,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..8890923eab1 --- /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 = "Admin.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..b0acafe8cc5 100644 --- a/code/modules/client/preferences/ambient_occlusion.dm +++ b/code/modules/client/preferences/ambient_occlusion.dm @@ -5,8 +5,6 @@ savefile_identifier = PREFERENCE_PLAYER /datum/preference/toggle/ambient_occlusion/apply_to_client(client/client, value) - var/atom/movable/screen/plane_master/game_world/plane_master = locate() in client?.screen - if (!plane_master) - return - - plane_master.backdrop(client.mob) + /// Backdrop for all game world planes. + for(var/atom/movable/screen/plane_master/game_world/game_world_plane_master in client?.screen) + game_world_plane_master.backdrop(client.mob) 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 6d58bbb341c..7a502ae1ae6 100644 --- a/code/modules/clothing/clothing.dm +++ b/code/modules/clothing/clothing.dm @@ -243,7 +243,7 @@ QDEL_NULL(moth_snack) return ..() -/obj/item/clothing/dropped(mob/user) +/obj/item/clothing/dropped(mob/living/user) ..() if(!istype(user)) return @@ -251,6 +251,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) @@ -258,7 +259,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 @@ -460,6 +461,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) @@ -471,6 +473,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 e1faf4d2567..06c6bf20f5d 100644 --- a/code/modules/clothing/masks/gasmask.dm +++ b/code/modules/clothing/masks/gasmask.dm @@ -19,6 +19,7 @@ /obj/item/clothing/mask/gas/Initialize(mapload) . = ..() + init_fov() if(!max_filters || !starting_filter_type) return @@ -76,6 +77,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 */ @@ -178,6 +183,9 @@ tint = 0 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 @@ -225,6 +233,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." @@ -239,6 +250,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 @@ -293,6 +307,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 8fbbf14f31a..cfbd214ad84 100644 --- a/code/modules/mining/equipment/explorer_gear.dm +++ b/code/modules/mining/equipment/explorer_gear.dm @@ -47,6 +47,9 @@ resistance_flags = FIRE_PROOF tint = 0.25 +/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/death.dm b/code/modules/mob/living/death.dm index 540e3e1262d..66b194ccc4c 100644 --- a/code/modules/mob/living/death.dm +++ b/code/modules/mob/living/death.dm @@ -91,6 +91,7 @@ med_hud_set_status() stop_pulling() + SEND_SIGNAL(src, COMSIG_LIVING_DEATH, gibbed) SEND_GLOBAL_SIGNAL(COMSIG_GLOB_MOB_DEATH, src, gibbed) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index bc08490ac8f..b29a6948b09 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() . = ..() @@ -705,7 +706,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) @@ -728,6 +728,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_defines.dm b/code/modules/mob/living/living_defines.dm index ba6394b3df0..0f8d45789b0 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_FOV_HIDDEN 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_90_DEGREES + /// 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..a9776304d45 --- /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) + 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 096a1fd3a7f..3313ffdac56 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 6a3da2611f4..c21ce134c8c 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 092acbbd44e..9b151234572 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -437,6 +437,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/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 95e93037f3c..1d4264c9b13 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -13,6 +13,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 = "" @@ -690,6 +691,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 199914229be..68790f54022 100644 --- a/config/game_options.txt +++ b/config/game_options.txt @@ -470,3 +470,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/tgstation.dme b/tgstation.dme index 9c071405b93..0e5fd79b99b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -74,6 +74,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" @@ -180,6 +181,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" @@ -627,6 +629,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" @@ -649,6 +652,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" @@ -860,6 +864,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" @@ -1792,6 +1797,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" @@ -2224,6 +2230,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" @@ -2903,6 +2910,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" 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, +};