mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-06-23 07:05:13 +01:00
5243b00d6b
## About The Pull Request Revamps Echolocation effect - Echolocation no longer pulses, at least, not as noticeably. The world around you is consistently black and white, and objects will fade into view as you approach or fade out of view as you move away. - Echolocaters are now really blind. But you can still "see", so long as you're not also deaf. - Echolocaters can now "hear" mobs through walls, ie, see mobs through walls like thermal vision. - While echolocating you have the option of selectively ignoring things which would make the screen busier - currently, blood, items, and floor objects like vents or holopads. https://github.com/user-attachments/assets/d559482c-e6e2-4b97-8387-54bca4074db0 https://github.com/user-attachments/assets/aeddd805-230c-4e82-b93f-1e537f4186eb ## Why It's Good For The Game Echolocation is such a cool effect but it currently is basically unplayable This revamp aims to make it more playable, particularly by making it update constantly so you can react easier. ## Changelog 🆑 Melbert balance: Revamped echolocation effect entirely. Now updates constantly, rather than on a cooldown. balance: Echolocaters can filter out certain objects that they would really prefer not to see at the moment. balance: Echolocaters are mechanically (but not visually) blind, which means they have flash immunity and whatnot. Because of course they are. balance: Echolocater lose their ability to echolocate if they've gone deaf. Then you're truly fully blind. balance: Echolocaters can see (or rather, hear) mobs through walls. refactor: Refactored Prosopagnosia to make the effect a bit more generic. /🆑 --------- Co-authored-by: Jordan Dominion <dominion@tgstation13.org>
350 lines
12 KiB
Plaintext
350 lines
12 KiB
Plaintext
/datum/component/echolocation
|
|
/// Radius of our echolocation.
|
|
var/echo_range = 5
|
|
/// Time for the image to start fading out.
|
|
var/image_expiry_time = 0.7 SECONDS
|
|
/// Time for the image to fade in.
|
|
var/fade_in_time = 0.2 SECONDS
|
|
/// Time for the image to fade out and delete itself.
|
|
var/fade_out_time = 0.3 SECONDS
|
|
|
|
/// Assoc list of world.time to atom ref to active image
|
|
VAR_PRIVATE/list/active_images = list()
|
|
/// All the saved appearances, keyed by icon-icon_state.
|
|
VAR_PRIVATE/static/list/saved_appearances = list()
|
|
|
|
/// Typepache of atom types that will be highlighted with an image on ABOVE_GAME_PLANE.
|
|
var/list/highlighted_paths
|
|
/// Typepache of atom types that will have an image generated on WALL_PLANE,
|
|
/// so they stick out from the floor but they don't obstruct game objects.
|
|
var/list/background_paths
|
|
/// Typecache of overlay icon states to the typecache of types that get that overlay.
|
|
var/list/overlay_states
|
|
|
|
/// Combined typecache of all (UNCHANGING) types we want to echolocate, for faster checking.
|
|
VAR_PRIVATE/list/combined_filter
|
|
|
|
/// The focus action for adjusting echolocation settings.
|
|
var/datum/action/echolocation_focus/focus
|
|
|
|
/// A matrix that turns everything except #ffffff into pure blackness, used for our images (the outlines are #ffffff).
|
|
VAR_PRIVATE/list/black_white_matrix
|
|
/// List of planes we apply our filters to.
|
|
VAR_PRIVATE/list/planes
|
|
|
|
/// Check every few seconds if we lost our hearing, disable sight_bypass if so (turning us really blind).
|
|
COOLDOWN_DECLARE(deafness_check)
|
|
|
|
/datum/component/echolocation/Initialize(
|
|
echo_range = src.echo_range,
|
|
image_expiry_time = src.image_expiry_time,
|
|
fade_in_time = src.fade_in_time,
|
|
fade_out_time = src.fade_out_time,
|
|
echo_icon,
|
|
)
|
|
. = ..()
|
|
var/mob/living/echolocator = parent
|
|
if(!istype(echolocator))
|
|
return COMPONENT_INCOMPATIBLE
|
|
|
|
overlay_states = list(
|
|
"danger" = typecacheof(
|
|
list(
|
|
/turf/closed/wall/space,
|
|
/turf/open/chasm,
|
|
/turf/open/floor/fakepit,
|
|
/turf/open/floor/fakespace,
|
|
/turf/open/lava,
|
|
/turf/open/openspace,
|
|
/turf/open/space,
|
|
),
|
|
),
|
|
"door" = typecacheof(
|
|
list(
|
|
/obj/machinery/door,
|
|
/obj/structure/mineral_door,
|
|
),
|
|
)
|
|
)
|
|
highlighted_paths = list()
|
|
background_paths = typecacheof(list(/obj/structure/bed, /obj/structure/table))
|
|
|
|
combined_filter = list()
|
|
for(var/state, typecache in overlay_states)
|
|
combined_filter |= typecache
|
|
combined_filter |= background_paths
|
|
|
|
black_white_matrix = list(85, 85, 85, 0, 85, 85, 85, 0, 85, 85, 85, 0, 0, 0, 0, 1, -254, -254, -254, 0)
|
|
planes = list(ABOVE_GAME_PLANE, FLOOR_PLANE, GAME_PLANE, WALL_PLANE)
|
|
|
|
focus = new(src)
|
|
focus.Grant(parent)
|
|
|
|
src.echo_range = echo_range
|
|
src.image_expiry_time = image_expiry_time
|
|
src.fade_in_time = fade_in_time
|
|
src.fade_out_time = fade_out_time
|
|
|
|
ADD_TRAIT(echolocator, TRAIT_ECHOLOCATOR, ECHOLOCATION_TRAIT)
|
|
deafness_check()
|
|
RegisterSignals(echolocator, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)), PROC_REF(deafness_check))
|
|
echolocator.become_blind(ECHOLOCATION_TRAIT)
|
|
echolocator.overlay_fullscreen(ECHOLOCATION_TRAIT, /atom/movable/screen/fullscreen/echo, echo_icon)
|
|
echolocator.apply_status_effect(/datum/status_effect/grouped/see_no_names, ECHOLOCATION_TRAIT)
|
|
START_PROCESSING(SSfastprocess, src)
|
|
|
|
if(echolocator.hud_used)
|
|
hud_created()
|
|
RegisterSignal(echolocator, COMSIG_MOB_HUD_CREATED, PROC_REF(hud_created))
|
|
echolocator.update_sight()
|
|
|
|
/datum/component/echolocation/Destroy(force)
|
|
STOP_PROCESSING(SSfastprocess, src)
|
|
var/mob/living/echolocator = parent
|
|
|
|
REMOVE_TRAIT(echolocator, TRAIT_ECHOLOCATOR, ECHOLOCATION_TRAIT)
|
|
REMOVE_TRAIT(echolocator, TRAIT_SIGHT_BYPASS, ECHOLOCATION_TRAIT)
|
|
UnregisterSignal(echolocator, list(SIGNAL_ADDTRAIT(TRAIT_DEAF), SIGNAL_REMOVETRAIT(TRAIT_DEAF)))
|
|
echolocator.cure_blind(ECHOLOCATION_TRAIT)
|
|
echolocator.clear_fullscreen(ECHOLOCATION_TRAIT)
|
|
echolocator.remove_status_effect(/datum/status_effect/grouped/see_no_names, ECHOLOCATION_TRAIT)
|
|
QDEL_NULL(focus)
|
|
|
|
for(var/tplane in planes)
|
|
for(var/atom/movable/screen/plane_master/game_plane as anything in echolocator.hud_used?.get_true_plane_masters(tplane))
|
|
game_plane.remove_filter("[ECHOLOCATION_TRAIT]_color")
|
|
game_plane.remove_filter("[ECHOLOCATION_TRAIT]_outline")
|
|
|
|
UnregisterSignal(echolocator, COMSIG_MOB_HUD_CREATED)
|
|
echolocator.update_sight()
|
|
|
|
for(var/time, image_list in active_images)
|
|
for(var/atom_ref, echo_image in image_list)
|
|
echolocator.client?.images -= echo_image
|
|
|
|
return ..()
|
|
|
|
/// Add or remove SIGHT_BYPASS depending on if we are deaf or not
|
|
/datum/component/echolocation/proc/deafness_check(...)
|
|
SIGNAL_HANDLER
|
|
|
|
if(HAS_TRAIT(parent, TRAIT_DEAF))
|
|
REMOVE_TRAIT(parent, TRAIT_SIGHT_BYPASS, ECHOLOCATION_TRAIT)
|
|
else
|
|
ADD_TRAIT(parent, TRAIT_SIGHT_BYPASS, ECHOLOCATION_TRAIT)
|
|
|
|
/// If the mob had no hud when they gained echolocation we need to apply the effect now
|
|
/datum/component/echolocation/proc/hud_created(...)
|
|
SIGNAL_HANDLER
|
|
|
|
var/mob/living/echolocator = parent
|
|
for(var/tplane in planes)
|
|
for(var/atom/movable/screen/plane_master/game_plane as anything in echolocator.hud_used.get_true_plane_masters(tplane))
|
|
game_plane.add_filter("[ECHOLOCATION_TRAIT]_color", 1, color_matrix_filter(black_white_matrix))
|
|
game_plane.add_filter("[ECHOLOCATION_TRAIT]_outline", 1, outline_filter(size = 1, color = COLOR_WHITE))
|
|
|
|
/datum/component/echolocation/process()
|
|
var/mob/living/echolocator = parent
|
|
if(echolocator.stat == DEAD)
|
|
return
|
|
COOLDOWN_START(src, deafness_check, 0. SECONDS)
|
|
echolocate()
|
|
|
|
/datum/component/echolocation/proc/echolocate()
|
|
var/mob/living/echolocator = parent
|
|
var/list/filtered = list()
|
|
|
|
for(var/atom/seen_atom as anything in dview(echo_range, get_turf(echolocator.client?.eye || echolocator), invis_flags = echolocator.see_invisible))
|
|
if(!seen_atom.alpha)
|
|
continue
|
|
if(!is_type_in_typecache(seen_atom, combined_filter) && !is_type_in_typecache(seen_atom, highlighted_paths))
|
|
continue
|
|
filtered += seen_atom
|
|
|
|
if(!length(filtered))
|
|
return
|
|
|
|
var/list/known_refs = list()
|
|
for(var/time, image_list in active_images)
|
|
for(var/atom_ref, echo_image in image_list)
|
|
known_refs[atom_ref] = time
|
|
|
|
var/current_time = "[world.time]"
|
|
active_images[current_time] = list()
|
|
|
|
for(var/atom/filtered_atom as anything in filtered)
|
|
// if we are already showing an image for this atom, just update its time so it sticks around longer
|
|
var/atom_ref = REF(filtered_atom)
|
|
if(known_refs[atom_ref])
|
|
var/old_time = known_refs[atom_ref]
|
|
var/image/old_image = active_images[old_time][atom_ref]
|
|
active_images[old_time] -= atom_ref
|
|
active_images[current_time][atom_ref] = old_image
|
|
if(!length(active_images[old_time]))
|
|
active_images -= old_time
|
|
// if they are mid fade, cancel it
|
|
animate(old_image, time = 0, alpha = 255)
|
|
continue
|
|
|
|
|
|
var/image/final_image
|
|
|
|
// check for special overlays first
|
|
for(var/overlay_state, overlay_typecache in overlay_states)
|
|
if(!is_type_in_typecache(filtered_atom, overlay_typecache))
|
|
continue
|
|
|
|
var/image/special_overlay = saved_appearances[overlay_state]
|
|
if(!special_overlay)
|
|
special_overlay = image('icons/effects/echolocate.dmi', null, overlay_state)
|
|
special_overlay.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
|
saved_appearances[overlay_state] = special_overlay
|
|
|
|
final_image = image(special_overlay)
|
|
break
|
|
|
|
// or generate a new image for this atom
|
|
if(isnull(final_image))
|
|
var/image/found_appearance = saved_appearances["[filtered_atom.icon]-[filtered_atom.icon_state]"]
|
|
if(isnull(found_appearance))
|
|
found_appearance = new(filtered_atom)
|
|
if(filtered_atom.icon && filtered_atom.icon_state)
|
|
saved_appearances["[filtered_atom.icon]-[filtered_atom.icon_state]"] = found_appearance
|
|
found_appearance.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
|
|
|
|
final_image = image(found_appearance)
|
|
final_image.dir = filtered_atom.dir
|
|
|
|
if(isturf(filtered_atom))
|
|
// We don't want to mess with you, you can stay on floor or wall plane or whatever
|
|
SET_PLANE(final_image, filtered_atom.plane, filtered_atom)
|
|
else if(is_type_in_typecache(filtered_atom, background_paths) || PLANE_TO_TRUE(final_image.plane) == FLOOR_PLANE)
|
|
// I am being evil here and using wall plane due to being in-between of game plane and floor plane
|
|
// Why? Because we need background/floor objects to have their own layering, otherwise the effect is blended in wrong
|
|
// These objects will scarcely interact with real walls so it's... fine
|
|
final_image.layer = ABOVE_NORMAL_TURF_LAYER
|
|
SET_PLANE(final_image, WALL_PLANE, filtered_atom)
|
|
else
|
|
// Must be on above game plane to separate the effects from game objects
|
|
SET_PLANE(final_image, ABOVE_GAME_PLANE, filtered_atom)
|
|
|
|
// Setting loc so we should disregard pixel offsets
|
|
final_image.pixel_w = 0
|
|
final_image.pixel_x = 0
|
|
final_image.pixel_y = 0
|
|
final_image.pixel_z = 0
|
|
final_image.loc = filtered_atom
|
|
// fade in
|
|
if(fade_in_time > 0)
|
|
final_image.alpha = 0
|
|
animate(final_image, alpha = 255, time = fade_in_time)
|
|
|
|
active_images[current_time] ||= list()
|
|
active_images[current_time][atom_ref] = final_image
|
|
|
|
echolocator.client?.images += final_image
|
|
|
|
addtimer(CALLBACK(src, PROC_REF(fade_images), current_time), image_expiry_time)
|
|
|
|
/datum/component/echolocation/proc/fade_images(from_time)
|
|
if(fade_out_time <= 0)
|
|
cleanup_images(from_time)
|
|
return
|
|
|
|
for(var/atom_ref, echo_image in active_images[from_time])
|
|
animate(echo_image, alpha = 0, time = fade_out_time)
|
|
|
|
addtimer(CALLBACK(src, PROC_REF(cleanup_images), from_time), fade_out_time + 0.5 SECONDS)
|
|
|
|
/datum/component/echolocation/proc/cleanup_images(from_time)
|
|
var/mob/living/echolocator = parent
|
|
for(var/atom_ref, echo_image in active_images[from_time])
|
|
echolocator.client?.images -= echo_image
|
|
active_images -= from_time
|
|
|
|
/atom/movable/screen/fullscreen/echo
|
|
icon_state = "echo"
|
|
layer = 30
|
|
plane = PLANE_SPACE_PARALLAX
|
|
show_when_dead = TRUE
|
|
|
|
/atom/movable/screen/fullscreen/echo/Initialize(mapload, datum/hud/hud_owner)
|
|
. = ..()
|
|
particles = new /particles/echo()
|
|
|
|
/atom/movable/screen/fullscreen/echo/Destroy()
|
|
QDEL_NULL(particles)
|
|
return ..()
|
|
|
|
|
|
/datum/action/echolocation_focus
|
|
name = "Echolocation Focus"
|
|
desc = "Focus your echolocation to reveal more details or ignore certain objects."
|
|
|
|
/// Assoc list of option name to typecache list
|
|
var/static/list/options
|
|
/// List of currently selected option names
|
|
var/list/selected_options
|
|
|
|
/datum/action/echolocation_focus/New(Target)
|
|
. = ..()
|
|
if(!options)
|
|
var/list/all_floor_objects = list(/obj/machinery/atmospherics/components)
|
|
for(var/obj/floor_type as anything in typesof(/obj/machinery, /obj/structure))
|
|
if(initial(floor_type.plane) == FLOOR_PLANE)
|
|
all_floor_objects += floor_type
|
|
|
|
options = list()
|
|
options["Blood"] = typecacheof(/obj/effect/decal/cleanable/blood)
|
|
options["Items"] = typecacheof(/obj/item)
|
|
options["Floor Objects"] = typecacheof(all_floor_objects)
|
|
|
|
selected_options = list(options[1], options[2])
|
|
update_echocomp()
|
|
|
|
/datum/action/echolocation_focus/proc/update_echocomp()
|
|
var/datum/component/echolocation/echo_comp = target
|
|
echo_comp.highlighted_paths.Cut()
|
|
for(var/option_name in selected_options)
|
|
echo_comp.highlighted_paths |= options[option_name]
|
|
|
|
/datum/action/echolocation_focus/Trigger(mob/clicker, trigger_flags)
|
|
. = ..()
|
|
if(!.)
|
|
return
|
|
|
|
ui_interact(clicker)
|
|
|
|
/datum/action/echolocation_focus/ui_status(mob/user, datum/ui_state/state)
|
|
return IsAvailable() ? UI_INTERACTIVE : UI_CLOSE
|
|
|
|
/datum/action/echolocation_focus/ui_interact(mob/user, datum/tgui/ui)
|
|
ui = SStgui.try_update_ui(user, src, ui)
|
|
if(isnull(ui))
|
|
ui = new(user, src, "EcholocationFocus")
|
|
ui.open()
|
|
|
|
/datum/action/echolocation_focus/ui_data(mob/user)
|
|
var/list/data = list()
|
|
|
|
data["selected_options"] = selected_options
|
|
data["all_options"] = assoc_to_keys(options)
|
|
|
|
return data
|
|
|
|
/datum/action/echolocation_focus/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
|
|
. = ..()
|
|
if(.)
|
|
return
|
|
|
|
if(action != "toggle")
|
|
return
|
|
|
|
var/toggled_option = params["option"]
|
|
if(toggled_option in selected_options)
|
|
selected_options -= toggled_option
|
|
else
|
|
selected_options += toggled_option
|
|
update_echocomp()
|
|
return TRUE
|