diff --git a/code/game/objects/structures/stairs.dm b/code/game/objects/structures/stairs.dm index 6cf82aef603..31c65be2ec3 100644 --- a/code/game/objects/structures/stairs.dm +++ b/code/game/objects/structures/stairs.dm @@ -2,6 +2,9 @@ #define STAIR_TERMINATOR_NO 1 #define STAIR_TERMINATOR_YES 2 +/// Range within which stair indicators will appear for approaching mobs +#define STAIR_INDICATOR_RANGE 3 + // dir determines the direction of travel to go upwards // stairs require /turf/open/openspace as the tile above them to work, unless your stairs have 'force_open_above' set to TRUE // multiple stair objects can be chained together; the Z level transition will happen on the final stair object in the chain @@ -10,12 +13,22 @@ name = "stairs" icon = 'icons/obj/stairs.dmi' icon_state = "stairs" + base_icon_state = "stairs" anchored = TRUE move_resist = INFINITY + plane = FLOOR_PLANE + layer = ABOVE_OPEN_TURF_LAYER - var/force_open_above = FALSE // replaces the turf above this stair obj with /turf/open/openspace - var/terminator_mode = STAIR_TERMINATOR_AUTOMATIC - var/turf/listeningTo + /// If TRUE replaces the turf above this stair obj with /turf/open/openspace + var/force_open_above = FALSE + /// Determines if this stair is the last in a "chain" of stairs, ie next step is upstairs + VAR_FINAL/terminator_mode = STAIR_TERMINATOR_AUTOMATIC + /// Upstairs turf. Is observed for changes if force_open_above is TRUE (to re-open if necessary) + VAR_FINAL/turf/directly_above + /// If TRUE, we have left/middle/right sprites. + var/has_merged_sprites = TRUE + /// Lazyassoc list of weakef to mob viewing stair indicators to their images + VAR_PRIVATE/list/mob_to_image /obj/structure/stairs/north dir = NORTH @@ -31,49 +44,94 @@ /obj/structure/stairs/wood icon_state = "stairs_wood" + has_merged_sprites = FALSE /obj/structure/stairs/stone icon_state = "stairs_stone" + has_merged_sprites = FALSE /obj/structure/stairs/material icon_state = "stairs_material" material_flags = MATERIAL_EFFECTS | MATERIAL_ADD_PREFIX | MATERIAL_COLOR | MATERIAL_AFFECT_STATISTICS + has_merged_sprites = FALSE /obj/structure/stairs/Initialize(mapload) + . = ..() + GLOB.stairs += src if(force_open_above) force_open_above() build_signal_listener() update_surrounding() - var/static/list/loc_connections = list( - COMSIG_ATOM_EXIT = PROC_REF(on_exit), + var/static/list/exit_connections = list( + COMSIG_ATOM_EXIT = PROC_REF(on_exit_stairs), ) - AddElement(/datum/element/connect_loc, loc_connections) + AddElement(/datum/element/connect_loc, exit_connections) + + var/static/list/range_connections = list( + COMSIG_ATOM_ENTERED = PROC_REF(on_enter_range), + COMSIG_ATOM_EXITED = PROC_REF(on_exit_range), + ) + AddComponent(/datum/component/connect_range, tracked = src, connections = range_connections, range = STAIR_INDICATOR_RANGE) - return ..() /obj/structure/stairs/Destroy() - listeningTo = null + if(directly_above) + UnregisterSignal(directly_above, COMSIG_TURF_MULTIZ_NEW) + directly_above = null + for(var/climber_ref in mob_to_image) + clear_climber_image(climber_ref, instant = TRUE) GLOB.stairs -= src return ..() -/obj/structure/stairs/Move() //Look this should never happen but... +/obj/structure/stairs/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) //Look this should never happen but... . = ..() if(force_open_above) build_signal_listener() update_surrounding() +/// Updates the sprite and the sprites of neighboring stairs to reflect merged sprites /obj/structure/stairs/proc/update_surrounding() - update_appearance() - for(var/i in GLOB.cardinals) - var/turf/T = get_step(get_turf(src), i) - var/obj/structure/stairs/S = locate() in T - if(S) - S.update_appearance() + if(!has_merged_sprites) + return -/obj/structure/stairs/proc/on_exit(datum/source, atom/movable/leaving, direction) + update_appearance() + + for(var/obj/structure/stairs/stair in get_step(src, turn(dir, 90))) + stair.update_appearance() + + for(var/obj/structure/stairs/stair in get_step(src, turn(dir, -90))) + stair.update_appearance() + +/obj/structure/stairs/update_icon_state() + . = ..() + if(!has_merged_sprites) + return + + var/has_left_stairs = FALSE + var/has_right_stairs = FALSE + for(var/obj/structure/stairs/stair in get_step(src, turn(dir, 90))) + if(stair.dir == dir) + has_left_stairs = TRUE + break + + for(var/obj/structure/stairs/stair in get_step(src, turn(dir, -90))) + if(stair.dir == dir) + has_right_stairs = TRUE + break + + if(has_left_stairs && has_right_stairs) + icon_state = "[base_icon_state]-m" + else if(has_left_stairs) + icon_state = "[base_icon_state]-r" + else if(has_right_stairs) + icon_state = "[base_icon_state]-l" + else + icon_state = base_icon_state + +/obj/structure/stairs/proc/on_exit_stairs(datum/source, atom/movable/leaving, direction) SIGNAL_HANDLER if(leaving == src) @@ -85,19 +143,95 @@ leaving.Bump(src) return COMPONENT_ATOM_BLOCK_EXIT +#define POINT_X_COMPONENT(pdir) ((pdir & EAST) ? 2 : ((pdir & WEST) ? -2 : 0)) +#define POINT_Y_COMPONENT(pdir) ((pdir & SOUTH) ? 2 : ((pdir & NORTH) ? -2 : 0)) + +/obj/structure/stairs/proc/on_enter_range(datum/source, atom/movable/entered) + SIGNAL_HANDLER + + if(!isliving(entered)) + return + + var/mob/living/climber = entered + var/datum/weakref/climber_ref = WEAKREF(climber) + if(!climber.client || !climber.client.prefs.read_preference(/datum/preference/toggle/stair_indicator)) + return + if(climber.dir == REVERSE_DIR(dir)) + return // walking away + if(LAZYACCESS(mob_to_image, climber_ref)) + return // already see it + if(!(climber in viewers(STAIR_INDICATOR_RANGE + 1, src))) + return // can't see the staircase (+1 tile for some leeway) + if(!isopenturf(get_step_multiz(src, UP))) + return // no place to go up to + + var/image/pointing_image = get_pointing_image() + climber.client.images += pointing_image + pointing_image.alpha = 0 + animate(pointing_image, pixel_x = POINT_X_COMPONENT(dir), pixel_y = POINT_Y_COMPONENT(dir), time = 0.5 SECONDS, easing = SINE_EASING|EASE_OUT, loop = -1, tag = "point_xy") + animate(pixel_x = 0, pixel_y = 0, time = 0.5 SECONDS, easing = SINE_EASING|EASE_IN) + animate(pointing_image, alpha = 180, time = 0.75 SECONDS, tag = "point_fadein") + LAZYSET(mob_to_image, climber_ref, pointing_image) + +/obj/structure/stairs/proc/on_exit_range(datum/source, atom/movable/exited) + SIGNAL_HANDLER + + if(!isliving(exited)) + return + + var/datum/weakref/climber_ref = WEAKREF(exited) + if(!LAZYACCESS(mob_to_image, climber_ref)) + return // not seeing anything + if(exited in viewers(STAIR_INDICATOR_RANGE, src)) + return // still in range and can see the staircase + + clear_climber_image(climber_ref) + +/obj/structure/stairs/proc/clear_climber_image(datum/weakref/climber_ref, instant = FALSE) + var/image/pointing_image = LAZYACCESS(mob_to_image, climber_ref) + if(!pointing_image) + LAZYREMOVE(mob_to_image, climber_ref) // just in case + return + if(instant) + clear_climber_image_callback(climber_ref, pointing_image) + return + + animate(pointing_image, alpha = 0, time = 0.75 SECONDS, tag = "point_fadeout") + // note: the player won't see a new indicator until the image is fully a removed, so this timer also serves as a cooldown + addtimer(CALLBACK(src, PROC_REF(clear_climber_image_callback), climber_ref, pointing_image), 1.5 SECONDS, TIMER_UNIQUE) + +/obj/structure/stairs/proc/clear_climber_image_callback(datum/weakref/climber_ref, image/pointing_image) + PRIVATE_PROC(TRUE) + var/mob/living/climber = climber_ref?.resolve() + climber?.client?.images -= pointing_image + LAZYREMOVE(mob_to_image, climber_ref) + +/obj/structure/stairs/proc/get_pointing_image() + PROTECTED_PROC(TRUE) + var/image/point_image = image('icons/hud/screen_gen.dmi', src, "arrow_large_white_still") + point_image.color = COLOR_DARK_MODERATE_LIME_GREEN + point_image.appearance_flags |= KEEP_APART + point_image.transform = matrix().Turn(dir2angle(REVERSE_DIR(dir))) + point_image.layer = BELOW_MOB_LAYER + SET_PLANE(point_image, GAME_PLANE, src) + return point_image + +#undef POINT_X_COMPONENT +#undef POINT_Y_COMPONENT + /obj/structure/stairs/Cross(atom/movable/AM) if(isTerminator() && (get_dir(src, AM) == dir)) return FALSE return ..() /obj/structure/stairs/proc/stair_ascend(atom/movable/climber) - var/turf/checking = get_step_multiz(get_turf(src), UP) + var/turf/checking = get_step_multiz(src, UP) if(!istype(checking)) return // I'm only interested in if the pass is unobstructed, not if the mob will actually make it if(!climber.can_z_move(UP, get_turf(src), checking, z_move_flags = ZMOVE_ALLOW_BUCKLED)) return - var/turf/target = get_step_multiz(get_turf(src), (dir|UP)) + var/turf/target = get_step_multiz(src, dir|UP) if(istype(target) && !climber.can_z_move(DOWN, target, z_move_flags = ZMOVE_FALL_FLAGS)) //Don't throw them into a tile that will just dump them back down. climber.zMove(target = target, z_move_flags = ZMOVE_STAIRS_FLAGS) /// Moves anything that's being dragged by src or anything buckled to it to the stairs turf. @@ -113,22 +247,22 @@ if(var_name != NAMEOF(src, force_open_above)) return if(!var_value) - if(listeningTo) - UnregisterSignal(listeningTo, COMSIG_TURF_MULTIZ_NEW) - listeningTo = null + if(directly_above) + UnregisterSignal(directly_above, COMSIG_TURF_MULTIZ_NEW) + directly_above = null else build_signal_listener() force_open_above() /obj/structure/stairs/proc/build_signal_listener() - if(listeningTo) - UnregisterSignal(listeningTo, COMSIG_TURF_MULTIZ_NEW) - var/turf/open/openspace/T = get_step_multiz(get_turf(src), UP) + if(directly_above) + UnregisterSignal(directly_above, COMSIG_TURF_MULTIZ_NEW) + var/turf/open/openspace/T = get_step_multiz(src, UP) RegisterSignal(T, COMSIG_TURF_MULTIZ_NEW, PROC_REF(on_multiz_new)) - listeningTo = T + directly_above = T /obj/structure/stairs/proc/force_open_above() - var/turf/open/openspace/T = get_step_multiz(get_turf(src), UP) + var/turf/open/openspace/T = get_step_multiz(src, UP) if(T && !istype(T)) T.ChangeTurf(/turf/open/openspace, flags = CHANGETURF_INHERIT_AIR) @@ -136,7 +270,7 @@ SIGNAL_HANDLER if(dir == UP) - var/turf/open/openspace/T = get_step_multiz(get_turf(src), UP) + var/turf/open/openspace/T = get_step_multiz(src, UP) if(T && !istype(T)) T.ChangeTurf(/turf/open/openspace, flags = CHANGETURF_INHERIT_AIR) @@ -282,3 +416,5 @@ #undef STAIR_TERMINATOR_AUTOMATIC #undef STAIR_TERMINATOR_NO #undef STAIR_TERMINATOR_YES + +#undef STAIR_INDICATOR_RANGE diff --git a/code/modules/client/preferences/accessibility.dm b/code/modules/client/preferences/accessibility.dm index e7486719978..1cc413ffca5 100644 --- a/code/modules/client/preferences/accessibility.dm +++ b/code/modules/client/preferences/accessibility.dm @@ -18,3 +18,18 @@ default_value = FALSE savefile_key = "remove_double_click" savefile_identifier = PREFERENCE_PLAYER + +/// When toggled, enables staircase indicators +/datum/preference/toggle/stair_indicator + category = PREFERENCE_CATEGORY_GAME_PREFERENCES + default_value = TRUE + savefile_key = "stair_indicator" + savefile_identifier = PREFERENCE_PLAYER + +/datum/preference/toggle/stair_indicator/apply_to_client_updated(client/client, value) + if(value || !isliving(client.mob)) // only hide, showing is more trouble than it's worth + return + + var/datum/weakref/climber_ref = WEAKREF(client.mob) + for(var/obj/structure/stairs/stair as anything in GLOB.stairs) + stair.clear_climber_image(climber_ref) diff --git a/icons/obj/stairs.dmi b/icons/obj/stairs.dmi index a59e8f1fc9f..c541967a425 100644 Binary files a/icons/obj/stairs.dmi and b/icons/obj/stairs.dmi differ diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx index e4dd90a7142..dd91fa7c1d5 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/game_preferences/accessibility.tsx @@ -28,3 +28,13 @@ export const remove_double_click: FeatureToggle = { `, component: CheckboxInput, }; + +export const stair_indicator: FeatureToggle = { + name: 'Enable stair indicator', + category: 'ACCESSIBILITY', + description: ` + When toggled, staircases will have a visual indicator showing which + direction to walk to transition floors. + `, + component: CheckboxInput, +};