mirror of
https://github.com/SPLURT-Station/S.P.L.U.R.T-Station-13.git
synced 2025-12-10 18:02:57 +00:00
355 lines
14 KiB
Plaintext
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 ..()
|