Files
Bubberstation/code/datums/components/fov_handler.dm
Azarak 680ca7d3b9 Field of View and Blindness improvements [bounty + upstream push] (#63312)
About The Pull Request

I have received exemption from the freeze by Oranges.
This PR was originally done for Skyrat-tg, as a bounty. Oranges took interest and Skyrat people approved taking it upstream so I did. (original PR Skyrat-SS13/Skyrat-tg#9912 )
Features:
This PR adds "field of view" mechanics, which by default don't do anything.
Once you pick up and wear a gas mask you'll notice a cone behind you.
That cone will hide all mobs, projectiles and projectile effects.
HOWEVER... hidden things in the cone (or for people who are blind or nearsighted) will play a visual effect.

The fov cone shadow density can be adjusted

Technicalities:
Originally a component, but by Watermelon's advice I have made this a datumized behaviour instead. (semi-component) This way it is faster, less memory use etc. Once again a component
People have native fov, which can be enabled by a config, by default it's off. (or by an admin verb)
People have fov traits, which add the fov impairings to you, gas masks do that now. All clothes have a support for possibly applying them.
Moves all things that should be hidden onto a new plane which is just above GAME_PLANE, that was we can efficiently filter them out of clients views.
Being dead or extending your eye view (xenobio console) will disable the FOV mask.

This PR was brought to you by Skyrat paying me aswell as rotational mathematics


Why It's Good For The Game

Oranges approves so it's good.
Changelog

cl
add: Gas Masks now limit your field of view
add: Added field of view mechanics. It's darkness can be tweaked by a pref
add: Blind, nearsighted and fov-limited people will now see sounds as visual effects.
/cl
2021-12-14 15:52:52 +13:00

129 lines
4.7 KiB
Plaintext

/// 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))