Files
EnterTheJake 0bd4b0820f Adds TGMC Tactical maps to the game and to Nuclear Operatives! (#96068)
<!-- Write **BELOW** The Headers and **ABOVE** The comments else it may
not be viewable. -->
<!-- You can view Contributing.MD for a detailed description of the pull
request process. -->

## About The Pull Request

Brought to you by Team Powercrèpe.(same group of people that delivered
the Heretic Overhaul)

This PR ports the TGMC Tacmaps and grants em to our NukeOps.

Code by Me, @Xander3359, @Arturlang, @Absolucy.

Mapping by @RipGrayson 

Sprites by @KillerOrcaCora

Writing bits by @necromanceranne

Tacmaps are essentially dynamically generated minimaps,

<img width="1711" height="1335" alt="Tac map examples"
src="https://github.com/user-attachments/assets/f936fac0-fb7c-4b84-970d-7195b4995ca7"
/>



<img width="1274" height="714" alt="Tac-Maps Icons"
src="https://github.com/user-attachments/assets/0f04e388-af4b-4955-be7a-04650cfd746b"
/>






As you can see from the screenshots above, departments are classified by
color, hovering your cursor over an area or a team mate, will display
their name, the system does allow the user to switch between Multi-Z,
and lastly you can draw and add labels to the map, although the last
option is only available to the NukeOps Leader and OW agents.


The functionality is tied to the Syndicate implants nukies start with,
Cayenne, Syndie borgs and Mechs also have the ability to toggle the map.

Likewise, the tacmaps will display the positioning of the following in
real time!

- Nuke ops agents

- Syndie borgs and Mechs

- Cayenne

- Location of the nuclear fission device (and beer nuke)

- Location of the Nuclear Authentication Disk.

<img width="1274" height="714" alt="SkyBridge OFF"
src="https://github.com/user-attachments/assets/3915e7d7-644a-4e9a-b213-997834dde2ea"
/>

<img width="1274" height="714" alt="SkyBridge ON"
src="https://github.com/user-attachments/assets/13748290-fd16-42ba-a9c4-a6b4f7e69c79"
/>

<img width="566" height="459" alt="Holo table done"
src="https://github.com/user-attachments/assets/1a43c5b7-ee51-4794-9c09-16b4fb50e2ad"
/>






https://github.com/user-attachments/assets/caf38538-ef04-4330-992c-3137a67ea0d0






Lastly The tactiical map implant cannot be be used on the nuke ops
base,If the team wishes to interact with the map they'll have to
Interact with the "Reconnaisance Platform" (Sprite by Orcacora) in the
briefing room, which has seen some mapping changes (Brought to you by
TheLastGrayson).



<!-- Describe The Pull Request. Please be sure every change is
documented or this can delay review and even discourage maintainers from
merging your PR! -->

## Why It's Good For The Game

Originally this feature was planned for the Upcoming Contractor Rework
our team is currently working on, but given the massive size of it all,
we decided to atomize it.

But without that consideration in mind, I think it makes sense for
Nuclear operatives of all antagonists to have the means to Recon and
plot an assault plan onto the station and actually be able to know the
whereabouts of their teammates.

Obviously this feature has a lot of other potential applications, we
have wanted to have functional Maps for a very long time, if someone
wants to give it a go once the feature is merged, by all means.

<!-- Argue for the merits of your changes and how they benefit the game,
especially if they are controversial and/or far reaching. If you can't
actually explain WHY what you are doing will improve the game, then it
probably isn't good for the game in the first place. -->

## Changelog

<!-- If your PR modifies aspects of the game that can be concretely
observed by players or admins you should add a changelog. If your change
does NOT meet this description, remove this section. Be sure to properly
mark your PRs to prevent unnecessary GBP loss. You can read up on GBP
and its effects on PRs in the tgstation guides for contributors. Please
note that maintainers freely reserve the right to remove and add tags
should they deem it appropriate. You can attempt to finagle the system
all you want, but it's best to shoot for clear communication right off
the bat. -->

🆑
add: Tactical Maps have been added to the game!
add: Nuclear Operatives implants now grant the ability to open a
Minimap, this map will display and show the names of various areas of
the station, and the positioning of the Nukeops team,borgs,mechs,Cayenne
and the nuke disk in real time!
add: Nukie leaders and OW agents can also draw and apply labels to the
map.
add: the "Recoinassance Platform" has been added to the Nukie Base, it
allows displaying of the Tactical maps while on the Syndicate Base Z
level.
map: The briefing room in the NukeOps base has been remapped to better
accomodate the new Holographic table.
map: Changed the front shutters on the Syndicate Infiltrator to the more
fitting Syndicate Shutters.
/🆑

<!-- Both 🆑's are required for the changelog to work! You can put
your name to the right of the first 🆑 if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

---------

Co-authored-by: Xander3359 <66163761+Xander3359@users.noreply.github.com>
Co-authored-by: Artur Lang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: RipGrayson <49290523+RipGrayson@users.noreply.github.com>
Co-authored-by: necromanceranne <40847847+necromanceranne@users.noreply.github.com>
2026-06-09 16:38:00 +02:00

471 lines
16 KiB
Plaintext

#define STAIR_TERMINATOR_AUTOMATIC 0
#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
/// Minimum tile spacing between stair minimap blips
#define STAIR_BLIP_MIN_DISTANCE 2
// 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
/obj/structure/stairs
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
/// 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
/// Current atoms used as this stair's minimap blip targets.
var/list/minimap_blip_targets
/// Lazyassoc list of weakef to mob viewing stair indicators to their images
VAR_PRIVATE/list/mob_to_image
/obj/structure/stairs/north
dir = NORTH
/obj/structure/stairs/south
dir = SOUTH
/obj/structure/stairs/east
dir = EAST
/obj/structure/stairs/west
dir = WEST
/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()
update_minimap_blip()
var/static/list/exit_connections = list(
COMSIG_ATOM_EXIT = PROC_REF(on_exit_stairs),
)
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)
/obj/structure/stairs/Destroy()
clear_minimap_blips()
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/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()
if(!has_merged_sprites)
return
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()
update_minimap_blip()
/obj/structure/stairs/proc/update_minimap_blip()
var/bottom_state = isTerminator() ? "stairs_up" : "stairs_down"
var/top_state = (bottom_state == "stairs_up") ? "stairs_down" : "stairs_up"
var/turf/current_turf = get_turf(src)
clear_minimap_blips()
if(isnull(current_turf))
return
add_minimap_blip_if_valid(current_turf, bottom_state)
add_minimap_blip_if_valid(get_step_multiz(current_turf, UP), top_state)
/obj/structure/stairs/proc/clear_minimap_blips()
if(!islist(minimap_blip_targets))
return
for(var/atom/target as anything in minimap_blip_targets)
remove_minimap_blip(MINIMAP_STAIR_BLIP, target)
LAZYCLEARLIST(minimap_blip_targets)
/obj/structure/stairs/proc/add_minimap_blip_if_valid(atom/target, state)
if(isnull(target))
return
if(!should_place_minimap_blip(target))
return
var/atom/movable/screen/minimap_element/blip/blip = get_minimap_blip(MINIMAP_STAIR_BLIP, target)
if(!isnull(blip))
blip.icon_state = state
else
add_minimap_blip(target, MINIMAP_STAIR_BLIP, state)
LAZYADD(minimap_blip_targets, target)
/obj/structure/stairs/proc/should_place_minimap_blip(atom/target)
var/turf/target_turf = get_turf(target)
if(isnull(target_turf))
return FALSE
if(length(get_minimap_blips_in_area(MINIMAP_STAIR_BLIP, target_turf, STAIR_BLIP_MIN_DISTANCE)))
return FALSE
return TRUE
/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)
return //Let's not block ourselves.
if(!isobserver(leaving) && isTerminator() && direction == dir)
leaving.set_currently_z_moving(CURRENTLY_Z_ASCENDING)
INVOKE_ASYNC(src, PROC_REF(stair_ascend), leaving)
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(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(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.
climber.pulling?.move_from_pull(climber, loc, climber.glide_size)
for(var/mob/living/buckled as anything in climber.buckled_mobs)
buckled.pulling?.move_from_pull(buckled, loc, buckled.glide_size)
/obj/structure/stairs/vv_edit_var(var_name, var_value)
. = ..()
if(!.)
return
if(var_name != NAMEOF(src, force_open_above))
return
if(!var_value)
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(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))
directly_above = T
/obj/structure/stairs/proc/force_open_above()
var/turf/open/openspace/T = get_step_multiz(src, UP)
if(T && !istype(T))
T.ChangeTurf(/turf/open/openspace, flags = CHANGETURF_INHERIT_AIR)
/obj/structure/stairs/proc/on_multiz_new(turf/source, dir)
SIGNAL_HANDLER
if(dir == UP)
var/turf/open/openspace/T = get_step_multiz(src, UP)
if(T && !istype(T))
T.ChangeTurf(/turf/open/openspace, flags = CHANGETURF_INHERIT_AIR)
/obj/structure/stairs/intercept_zImpact(list/falling_movables, levels = 1)
. = ..()
// falling from a higher z level onto stairs
if(levels != 1 || !isTerminator())
return
for(var/mob/living/guy in falling_movables)
if(!can_fall_down_stairs(guy))
continue
to_chat(guy, span_warning("You fall down [src]!"))
on_fall(guy)
. |= FALL_INTERCEPTED | FALL_NO_MESSAGE | FALL_RETAIN_PULL
/// Will the passed mob tumble down the stairs instead of walking?
/obj/structure/stairs/proc/can_fall_down_stairs(mob/living/falling)
if(falling.buckled || falling.pulledby)
return FALSE
if(falling.stat >= UNCONSCIOUS) // if you shove someone unconscious down the stairs, they'd probably roll
return TRUE
if(falling.has_status_effect(/datum/status_effect/staggered)) // off balance
return TRUE
return FALSE
/// What happens when a mob tumbles down the stairs
/obj/structure/stairs/proc/on_fall(mob/living/falling)
falling.AdjustParalyzed(2 SECONDS)
falling.adjust_staggered(2 SECONDS)
falling.AdjustKnockdown(5 SECONDS)
falling.spin(1 SECONDS, 0.25 SECONDS)
falling.apply_damage(rand(4, 8), BRUTE, spread_damage = TRUE)
GLOB.move_manager.move_towards(falling, get_ranged_target_turf(src, REVERSE_DIR(dir), 2), delay = 0.4 SECONDS, timeout = 1 SECONDS)
/obj/structure/stairs/proc/isTerminator() //If this is the last stair in a chain and should move mobs up
if(terminator_mode != STAIR_TERMINATOR_AUTOMATIC)
return (terminator_mode == STAIR_TERMINATOR_YES)
var/turf/T = get_turf(src)
if(!T)
return FALSE
var/turf/them = get_step(T, dir)
if(!them)
return FALSE
for(var/obj/structure/stairs/S in them)
if(S.dir == dir)
return FALSE
return TRUE
/obj/structure/stairs_frame
name = "stairs frame"
desc = "Everything you need to call something a staircase, aside from the stuff you actually step on."
icon = 'icons/obj/stairs.dmi'
icon_state = "stairs_frame"
density = FALSE
anchored = FALSE
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 5)
/// What type of stack will this drop on deconstruction?
var/frame_stack = /obj/item/stack/rods
/// How much of frame_stack should this drop on deconstruction?
var/frame_stack_amount = 10
/obj/structure/stairs_frame/wood
name = "wooden stairs frame"
desc = "Everything you need to build a staircase, minus the actual stairs. This one is made of wood."
frame_stack = /obj/item/stack/sheet/mineral/wood
custom_materials = list(/datum/material/wood = SHEET_MATERIAL_AMOUNT * 10)
/obj/structure/stairs_frame/Initialize(mapload)
. = ..()
AddElement(/datum/element/simple_rotation)
/obj/structure/stairs_frame/examine(mob/living/carbon/human/user)
. = ..()
if(anchored)
. += span_notice("The frame is anchored and can be made into proper stairs with 10 sheets of material.")
else
. += span_notice("The frame will need to be secured with a wrench before it can be completed.")
/obj/structure/stairs_frame/wrench_act(mob/living/user, obj/item/used_tool)
user.balloon_alert_to_viewers("securing stairs frame", "securing frame")
used_tool.play_tool_sound(src)
if(!used_tool.use_tool(src, user, 3 SECONDS))
return TRUE
if(anchored)
anchored = FALSE
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
return TRUE
anchored = TRUE
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
return TRUE
/obj/structure/stairs_frame/wrench_act_secondary(mob/living/user, obj/item/used_tool)
to_chat(user, span_notice("You start disassembling [src]..."))
used_tool.play_tool_sound(src)
if(!used_tool.use_tool(src, user, 3 SECONDS))
return TRUE
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
deconstruct(TRUE)
return TRUE
/obj/structure/stairs_frame/atom_deconstruct(disassembled = TRUE)
new frame_stack(get_turf(src), frame_stack_amount)
/obj/structure/stairs_frame/attackby(obj/item/attacked_by, mob/user, list/modifiers, list/attack_modifiers)
if(!isstack(attacked_by))
return ..()
if(!anchored)
user.balloon_alert(user, "secure frame first")
return TRUE
var/obj/item/stack/material = attacked_by
if(material.stairs_type)
if(material.get_amount() < 10)
to_chat(user, span_warning("You need ten [material.name] sheets to do this!"))
return
if(locate(/obj/structure/stairs) in loc)
to_chat(user, span_warning("There's already stairs built here!"))
return
to_chat(user, span_notice("You start adding [material] to [src]..."))
if(!do_after(user, 10 SECONDS, target = src) || !material.use(10) || (locate(/obj/structure/table) in loc))
return
make_new_stairs(material.stairs_type)
else if(istype(material, /obj/item/stack/sheet))
if(material.get_amount() < 10)
to_chat(user, span_warning("You need ten sheets to do this!"))
return
if(locate(/obj/structure/stairs) in loc)
to_chat(user, span_warning("There's already stairs built here!"))
return
to_chat(user, span_notice("You start adding [material] to [src]..."))
if(!do_after(user, 10 SECONDS, target = src) || !material.use(10) || (locate(/obj/structure/table) in loc))
return
var/list/material_list = list()
if(material.material_type)
material_list[material.material_type] = SHEET_MATERIAL_AMOUNT * 10
make_new_stairs(/obj/structure/stairs/material, material_list)
return TRUE
/obj/structure/stairs_frame/proc/make_new_stairs(stairs_type, custom_materials)
var/obj/structure/stairs/new_stairs = new stairs_type(loc)
new_stairs.setDir(dir)
if(custom_materials)
new_stairs.set_custom_materials(custom_materials)
qdel(src)
#undef STAIR_TERMINATOR_AUTOMATIC
#undef STAIR_TERMINATOR_NO
#undef STAIR_TERMINATOR_YES
#undef STAIR_INDICATOR_RANGE
#undef STAIR_BLIP_MIN_DISTANCE