Files
S.P.L.U.R.T-Station-13/code/datums/components/field_of_vision.dm
DeltaFire 6aa906395c not a gaius reference
hhhh let me just replace all the ex_acts missing some args and adjust them
2021-11-14 21:34:59 +01:00

355 lines
14 KiB
Plaintext

#define CENTERED_RENDER_SOURCE(img, atom, FoV) \
atom.render_target = atom.render_target || ref(atom);\
img.render_source = atom.render_target;\
if(atom.icon){\
var/_cached_sizes = FoV.width_n_height_offsets[atom.icon];\
if(!_cached_sizes){\
var/icon/_I = icon(atom.icon);\
var/list/L = list();\
L += (_I.Width() - world.icon_size)/2;\
L += (_I.Height() - world.icon_size)/2;\
_cached_sizes = FoV.width_n_height_offsets[atom.icon] = L\
}\
img.pixel_x = _cached_sizes[1];\
img.pixel_y = _cached_sizes[2];\
img.loc = atom\
}
#define REGISTER_NESTED_LOCS(source, list, comsig, proc) \
for(var/k in get_nested_locs(source)){\
var/atom/_A = k;\
RegisterSignal(_A, comsig, proc);\
list += _A\
}
#define UNREGISTER_NESTED_LOCS(list, comsig, index) \
for(var/k in index to length(list)){\
var/atom/_A = list[k];\
UnregisterSignal(_A, comsig);\
list -= _A\
}
/**
* Field of Vision component. Does totally what you probably think it does,
* ergo preventing players from seeing what's behind them.
*/
/datum/component/field_of_vision
can_transfer = TRUE
/**
* That special invisible, almost neigh indestructible movable
* that holds both shadow cone mask and image and follows the player around.
*/
var/atom/movable/fov_holder/fov
///The current screen size this field of vision is meant to fit for.
var/current_fov_size = list(15, 15)
///How much is the cone rotated clockwise, purely backend. Please use rotate_shadow_cone() if you must.
var/angle = 0
/// Used to scale the shadow cone when rotating it to fit over the edges of the screen.
var/rot_scale = 1
/// The inner angle of this cone, right hardset to 90, 180, or 270 degrees, until someone figures out a way to make it dynamic.
var/shadow_angle = FOV_90_DEGREES
/// The mask portion of the cone, placed on a * render target plane so while not visible it still applies the filter.
var/image/shadow_mask
/// The visual portion of the cone, placed on the highest layer of the wall plane
var/image/visual_shadow
/**
* An image whose render_source is kept up to date to prevent the mob (or the topmost movable holding it) from being hidden by the mask.
* Will make it use vis_contents instead once a few byonds bugs with images and vis contents are fixed.
*/
var/image/owner_mask
/**
* A circle image used to somewhat uncover the adjacent portion of the shadow cone, making mobs and objects behind us somewhat visible.
* The owner mask is still required for those mob going over the default 32x32 px size btw.
*/
var/image/adj_mask
/// A list of nested locations the mob is in, to ensure the above image works correctly.
var/list/nested_locs = list()
/**
* A static list of offsets based on icon width and height, because render sources are centered unlike most other visuals,
* and that gives us some problems when the icon is larger or smaller than world.icon_size
*/
var/static/list/width_n_height_offsets = list()
/datum/component/field_of_vision/Initialize(fov_type = FOV_90_DEGREES, _angle = 0)
if(!ismob(parent))
return COMPONENT_INCOMPATIBLE
angle = _angle
shadow_angle = fov_type
/datum/component/field_of_vision/RegisterWithParent()
. = ..()
var/mob/M = parent
if(M.client)
generate_fov_holder(M, angle)
RegisterSignal(M, COMSIG_MOB_CLIENT_LOGIN, .proc/on_mob_login)
RegisterSignal(M, COMSIG_MOB_CLIENT_LOGOUT, .proc/on_mob_logout)
RegisterSignal(M, COMSIG_MOB_GET_VISIBLE_MESSAGE, .proc/on_visible_message)
RegisterSignal(M, COMSIG_MOB_EXAMINATE, .proc/on_examinate)
RegisterSignal(M, COMSIG_MOB_FOV_VIEW, .proc/on_fov_view)
RegisterSignal(M, COMSIG_MOB_CLIENT_CHANGE_VIEW, .proc/on_change_view)
RegisterSignal(M, COMSIG_MOB_RESET_PERSPECTIVE, .proc/on_reset_perspective)
RegisterSignal(M, COMSIG_MOB_FOV_VIEWER, .proc/is_viewer)
/datum/component/field_of_vision/UnregisterFromParent()
. = ..()
var/mob/M = parent
if(!QDELETED(fov))
if(M.client)
UnregisterSignal(M, list(COMSIG_ATOM_DIR_CHANGE, COMSIG_MOVABLE_MOVED, COMSIG_MOB_DEATH, COMSIG_LIVING_REVIVE))
M.client.images -= owner_mask
M.client.images -= shadow_mask
M.client.images -= visual_shadow
M.client.images -= adj_mask
qdel(fov, TRUE) // Forced.
fov = null
QDEL_NULL(owner_mask)
QDEL_NULL(adj_mask)
if(length(nested_locs))
UNREGISTER_NESTED_LOCS(nested_locs, COMSIG_MOVABLE_MOVED, 1)
UnregisterSignal(M, list(COMSIG_MOB_CLIENT_LOGIN, COMSIG_MOB_CLIENT_LOGOUT,
COMSIG_MOB_GET_VISIBLE_MESSAGE, COMSIG_MOB_EXAMINATE,
COMSIG_MOB_FOV_VIEW, COMSIG_MOB_RESET_PERSPECTIVE,
COMSIG_MOB_CLIENT_CHANGE_VIEW, COMSIG_MOB_FOV_VIEWER))
/**
* Generates the holder and images (if not generated yet) and adds them to client.images.
* Run when the component is registered to a player mob, or upon login.
*/
/datum/component/field_of_vision/proc/generate_fov_holder(mob/M, _angle = 0)
if(QDELETED(fov))
fov = new(get_turf(M))
fov.icon_state = "[shadow_angle]"
fov.dir = M.dir
shadow_mask = image('icons/misc/field_of_vision.dmi', fov, "[shadow_angle]", FIELD_OF_VISION_LAYER)
shadow_mask.plane = FIELD_OF_VISION_PLANE
visual_shadow = image('icons/misc/field_of_vision.dmi', fov, "[shadow_angle]_v", FIELD_OF_VISION_LAYER)
visual_shadow.plane = FIELD_OF_VISION_VISUAL_PLANE
owner_mask = new
owner_mask.appearance_flags = RESET_TRANSFORM
owner_mask.plane = FIELD_OF_VISION_BLOCKER_PLANE
adj_mask = image('icons/misc/field_of_vision.dmi', fov, "adj_mask", FIELD_OF_VISION_LAYER)
adj_mask.appearance_flags = RESET_TRANSFORM
adj_mask.plane = FIELD_OF_VISION_BLOCKER_PLANE
if(_angle)
rotate_shadow_cone(_angle)
fov.alpha = M.stat == DEAD ? 0 : 255
RegisterSignal(M, COMSIG_MOB_DEATH, .proc/hide_fov)
RegisterSignal(M, COMSIG_LIVING_REVIVE, .proc/show_fov)
RegisterSignal(M, COMSIG_ATOM_DIR_CHANGE, .proc/on_dir_change)
RegisterSignal(M, COMSIG_MOVABLE_MOVED, .proc/on_mob_moved)
RegisterSignal(M, COMSIG_ROBOT_UPDATE_ICONS, .proc/manual_centered_render_source)
var/atom/A = M
if(M.loc && !isturf(M.loc))
REGISTER_NESTED_LOCS(M, nested_locs, COMSIG_MOVABLE_MOVED, .proc/on_loc_moved)
A = nested_locs[nested_locs.len]
CENTERED_RENDER_SOURCE(owner_mask, A, src)
M.client.images += shadow_mask
M.client.images += visual_shadow
M.client.images += owner_mask
M.client.images += adj_mask
if(M.client.view != "[current_fov_size[1]]x[current_fov_size[2]]")
resize_fov(current_fov_size, getviewsize(M.client.view))
///Rotates the shadow cone to a certain degree. Backend shenanigans.
/datum/component/field_of_vision/proc/rotate_shadow_cone(new_angle)
var/simple_degrees = SIMPLIFY_DEGREES(new_angle - angle)
var/to_scale = cos(simple_degrees) * sin(simple_degrees)
if(to_scale)
var/old_rot_scale = rot_scale
rot_scale = 1 + to_scale
if(old_rot_scale != rot_scale)
visual_shadow.transform = shadow_mask.transform = shadow_mask.transform.Scale(rot_scale/old_rot_scale)
visual_shadow.transform = shadow_mask.transform = shadow_mask.transform.Turn(fov.transform, simple_degrees)
/**
* Resizes the shadow to match the current screen size.
* Run when the client view size is changed, or if the player has a viewsize different than "15x15" on login/comp registration.
*/
/datum/component/field_of_vision/proc/resize_fov(list/old_view, list/view)
current_fov_size = view
var/old_size = max(old_view[1], old_view[2])
var/new_size = max(view[1], view[2])
if(old_size == new_size) //longest edges are still of the same length.
return
visual_shadow.transform = shadow_mask.transform = shadow_mask.transform.Scale(new_size/old_size)
/datum/component/field_of_vision/proc/on_mob_login(mob/source, client/client)
generate_fov_holder(source, angle)
/datum/component/field_of_vision/proc/on_mob_logout(mob/source, client/client)
UnregisterSignal(source, list(COMSIG_ATOM_DIR_CHANGE, COMSIG_MOVABLE_MOVED, COMSIG_MOB_DEATH,
COMSIG_LIVING_REVIVE, COMSIG_ROBOT_UPDATE_ICONS))
if(length(nested_locs))
UNREGISTER_NESTED_LOCS(nested_locs, COMSIG_MOVABLE_MOVED, 1)
/datum/component/field_of_vision/proc/on_dir_change(mob/source, old_dir, new_dir)
fov.dir = new_dir
///Hides the shadow, other visibility comsig procs will take it into account. Called when the mob dies.
/datum/component/field_of_vision/proc/hide_fov(mob/source)
fov.alpha = 0
/// Shows the shadow. Called when the mob is revived.
/datum/component/field_of_vision/proc/show_fov(mob/source)
fov.alpha = 255
/// Hides the shadow when looking through other items, shows it otherwise.
/datum/component/field_of_vision/proc/on_reset_perspective(mob/source, atom/target)
if(source.client.eye == source || source.client.eye == source.loc)
fov.alpha = 255
else
fov.alpha = 0
/// Called when the client view size is changed.
/datum/component/field_of_vision/proc/on_change_view(mob/source, client, list/old_view, list/view)
resize_fov(old_view, view)
/**
* Called when the owner mob moves around. Used to keep shadow located right behind us,
* As well as modify the owner mask to match the topmost item.
*/
/datum/component/field_of_vision/proc/on_mob_moved(mob/source, atom/oldloc, dir, forced)
var/turf/T
if(!isturf(source.loc)) //Recalculate all nested locations.
UNREGISTER_NESTED_LOCS( nested_locs, COMSIG_MOVABLE_MOVED, 1)
REGISTER_NESTED_LOCS(source, nested_locs, COMSIG_MOVABLE_MOVED, .proc/on_loc_moved)
var/atom/movable/topmost = nested_locs[nested_locs.len]
T = topmost.loc
CENTERED_RENDER_SOURCE(owner_mask, topmost, src)
else
T = source.loc
if(length(nested_locs))
UNREGISTER_NESTED_LOCS(nested_locs, COMSIG_MOVABLE_MOVED, 1)
CENTERED_RENDER_SOURCE(owner_mask, source, src)
if(T)
fov.forceMove(T, harderforce = TRUE)
/// Pretty much like the above, but meant for other movables the mob is stored in (bodybags, boxes, mechs etc).
/datum/component/field_of_vision/proc/on_loc_moved(atom/movable/source, atom/oldloc, dir, forced)
if(isturf(source.loc) && isturf(oldloc)) //This is the case of the topmost movable loc moving around the world, skip.
fov.forceMove(source.loc, harderforce = TRUE)
return
var/atom/movable/prev_topmost = nested_locs[nested_locs.len]
if(prev_topmost != source)
UNREGISTER_NESTED_LOCS(nested_locs, COMSIG_MOVABLE_MOVED, nested_locs.Find(source) + 1)
REGISTER_NESTED_LOCS(source, nested_locs, COMSIG_MOVABLE_MOVED, .proc/on_loc_moved)
var/atom/movable/topmost = nested_locs[nested_locs.len]
if(topmost != prev_topmost)
CENTERED_RENDER_SOURCE(owner_mask, topmost, src)
if(topmost.loc)
fov.forceMove(topmost.loc, harderforce = TRUE)
/// A hacky comsig proc for things that somehow decide to change icon on the go. may make a change_icon_file() proc later but...
/datum/component/field_of_vision/proc/manual_centered_render_source(mob/source, old_icon)
if(!isturf(source.loc))
return
CENTERED_RENDER_SOURCE(owner_mask, source, src)
#undef CENTERED_RENDER_SOURCE
#undef REGISTER_NESTED_LOCS
#undef UNREGISTER_NESTED_LOCS
/**
* Byond doc is not entirely correct on the integrated arctan() proc.
* When both x and y are negative, the output is also negative, cycling clockwise instead of counter-clockwise.
* That's also why I am extensively using the SIMPLIFY_DEGREES macro here.
*
* Overall this is the main macro that calculates wheter a target is within the shadow cone angle or not.
*/
#define FOV_ANGLE_CHECK(mob, target, zero_x_y_statement, success_statement) \
var/turf/T1 = get_turf(target);\
var/turf/T2 = get_turf(mob);\
if(!T1 || !T2){\
zero_x_y_statement\
}\
var/_x = (T1.x - T2.x);\
var/_y = (T1.y - T2.y);\
if(ISINRANGE(_x, -1, 1) && ISINRANGE(_y, -1, 1)){\
zero_x_y_statement\
}\
var/dir = (mob.dir & (EAST|WEST)) || mob.dir;\
var/_degree = -angle;\
var/_half = shadow_angle/2;\
switch(dir){\
if(EAST){\
_degree += 180;\
}\
if(NORTH){\
_degree += 270;\
}\
if(SOUTH){\
_degree += 90;\
}\
}\
var/_min = SIMPLIFY_DEGREES(_degree - _half);\
var/_max = SIMPLIFY_DEGREES(_degree + _half);\
if((_min > _max) ? !ISINRANGE(SIMPLIFY_DEGREES(arctan(_x, _y)), _max, _min) : ISINRANGE(SIMPLIFY_DEGREES(arctan(_x, _y)), _min, _max)){\
success_statement;\
}
/datum/component/field_of_vision/proc/on_examinate(mob/source, atom/target)
if(fov.alpha)
FOV_ANGLE_CHECK(source, target, return, return COMPONENT_DENY_EXAMINATE|COMPONENT_EXAMINATE_BLIND)
/datum/component/field_of_vision/proc/on_visible_message(mob/source, atom/target, message, range, list/ignored_mobs)
if(fov.alpha)
FOV_ANGLE_CHECK(source, target, return, return COMPONENT_NO_VISIBLE_MESSAGE)
/datum/component/field_of_vision/proc/on_fov_view(mob/source, list/atoms)
if(!fov.alpha)
return
for(var/k in atoms)
var/atom/A = k
FOV_ANGLE_CHECK(source, A, continue, atoms -= A)
/datum/component/field_of_vision/proc/is_viewer(mob/source, atom/center, depth, list/viewers_list)
if(fov.alpha)
FOV_ANGLE_CHECK(source, center, return, viewers_list -= source)
#undef FOV_ANGLE_CHECK
/**
* The shadow cone's mask and visual images holder which can't locate inside the mob,
* lest they inherit the mob opacity and cause a lot of hindrance
*/
/atom/movable/fov_holder
name = "field of vision holder"
pixel_x = -224 //the image is about 480x480 px, ergo 15 tiles (480/32) big, and we gotta center it.
pixel_y = -224
mouse_opacity = MOUSE_OPACITY_TRANSPARENT
plane = FIELD_OF_VISION_PLANE
anchored = TRUE
/atom/movable/fov_holder/ConveyorMove()
return
/atom/movable/fov_holder/has_gravity(turf/T)
return FALSE
/atom/movable/fov_holder/ex_act(severity, target, origin)
return FALSE
/atom/movable/fov_holder/singularity_act()
return
/atom/movable/fov_holder/singularity_pull()
return
/atom/movable/fov_holder/blob_act()
return
/atom/movable/fov_holder/onTransitZ()
return
/// Prevents people from moving these after creation, because they shouldn't be.
/atom/movable/fov_holder/forceMove(atom/destination, no_tp=FALSE, harderforce = FALSE)
if(harderforce)
return ..()
/// Last but not least, these shouldn't be deleted by anything but the component itself
/atom/movable/fov_holder/Destroy(force = FALSE)
if(!force)
return QDEL_HINT_LETMELIVE
return ..()