Files
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

527 lines
15 KiB
Plaintext

/*
Click code cleanup
~Sayu
*/
// 1 decisecond click delay (above and beyond mob/next_move)
//This is mainly modified by click code, to modify click delays elsewhere, use next_move and changeNext_move()
/mob/var/next_click = 0
// THESE DO NOT EFFECT THE BASE 1 DECISECOND DELAY OF NEXT_CLICK
/mob/var/next_move_adjust = 0 //Amount to adjust action/click delays by, + or -
/mob/var/next_move_modifier = 1 //Value to multiply action/click delays by
//Delays the mob's next click/action by num deciseconds
// eg: 10-3 = 7 deciseconds of delay
// eg: 10*0.5 = 5 deciseconds of delay
// DOES NOT EFFECT THE BASE 1 DECISECOND DELAY OF NEXT_CLICK
/mob/proc/changeNext_move(num)
next_move = world.time + ((num+next_move_adjust)*next_move_modifier)
/mob/living/changeNext_move(num)
var/mod = next_move_modifier
var/adj = next_move_adjust
for(var/datum/status_effect/effect as anything in status_effects)
mod *= effect.nextmove_modifier()
adj += effect.nextmove_adjust()
next_move = world.time + ((num + adj)*mod)
/**
* Before anything else, defer these calls to a per-mobtype handler. This allows us to
* remove istype() spaghetti code, but requires the addition of other handler procs to simplify it.
*
* Alternately, you could hardcode every mob's variation in a flat [/mob/proc/ClickOn] proc; however,
* that's a lot of code duplication and is hard to maintain.
*
* Note that this proc can be overridden, and is in the case of screen objects.
*/
/atom/Click(location, control, params)
if(flags_1 & INITIALIZED_1)
SEND_SIGNAL(src, COMSIG_CLICK, location, control, params, usr)
usr.ClickOn(src, params)
/atom/DblClick(location,control,params)
if(flags_1 & INITIALIZED_1)
usr.DblClickOn(src,params)
/atom/MouseWheel(delta_x,delta_y,location,control,params)
if(flags_1 & INITIALIZED_1)
usr.MouseWheelOn(src, delta_x, delta_y, params)
/**
* Standard mob ClickOn()
*
* After that, mostly just check your state, check whether you're holding an item,
* check whether you're adjacent to the target, then pass off the click to whoever is receiving it.
*
* The most common are:
* * [mob/proc/UnarmedAttack] (atom,adjacent) - used here only when adjacent, with no item in hand; in the case of humans, checks gloves
* * [atom/proc/attackby] (item,user) - used only when adjacent
* * [obj/item/proc/afterattack] (atom,user,adjacent,params) - used both ranged and adjacent
* * [mob/proc/RangedAttack] (atom,modifiers) - used only ranged, only used for tk and laser eyes but could be changed
*/
/mob/proc/ClickOn( atom/A, params )
if(world.time <= next_click)
return
next_click = world.time + 1
if(check_click_intercept(params,A) || HAS_TRAIT(src, TRAIT_NO_TRANSFORM))
return
var/list/modifiers = params2list(params)
if(SEND_SIGNAL(src, COMSIG_MOB_CLICKON, A, modifiers) & COMSIG_MOB_CANCEL_CLICKON)
return
if(LAZYACCESS(modifiers, BUTTON4) || LAZYACCESS(modifiers, BUTTON5))
return
if(LAZYACCESS(modifiers, SHIFT_CLICK))
if(LAZYACCESS(modifiers, MIDDLE_CLICK))
ShiftMiddleClickOn(A)
return
if(LAZYACCESS(modifiers, CTRL_CLICK))
CtrlShiftClickOn(A)
return
if (LAZYACCESS(modifiers, ALT_CLICK))
alt_shift_click_on(A)
return
ShiftClickOn(A)
return
if(LAZYACCESS(modifiers, MIDDLE_CLICK))
if(LAZYACCESS(modifiers, CTRL_CLICK))
CtrlMiddleClickOn(A)
else
MiddleClickOn(A, params)
return
if(LAZYACCESS(modifiers, ALT_CLICK)) // alt and alt-gr (rightalt)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
AltClickSecondaryOn(A)
else
AltClickOn(A)
return
if(LAZYACCESS(modifiers, CTRL_CLICK))
CtrlClickOn(A)
return
if(INCAPACITATED_IGNORING(src, INCAPABLE_RESTRAINTS|INCAPABLE_STASIS))
return
face_atom(A)
if(next_move > world.time) // in the year 2000...
return
if(!LAZYACCESS(modifiers, "catcher") && A.IsObscured())
return
if(HAS_TRAIT(src, TRAIT_HANDS_BLOCKED))
changeNext_move(CLICK_CD_HANDCUFFED) //Doing shit in cuffs shall be vey slow
UnarmedAttack(A, Adjacent(A), modifiers)
return
if(throw_mode)
if(throw_item(A))
changeNext_move(CLICK_CD_THROW)
return
var/obj/item/W = get_active_held_item()
if(W == A)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
W.attack_self_secondary(src, modifiers)
update_held_items()
return
else
W.attack_self(src, modifiers)
update_held_items()
return
//These are always reachable.
//User itself, current loc, and user inventory
if(A in DirectAccess())
if(W)
W.melee_attack_chain(src, A, modifiers)
else
if(ismob(A))
changeNext_move(CLICK_CD_MELEE)
UnarmedAttack(A, TRUE, modifiers)
return
//Can't reach anything else in lockers or other weirdness
if(!loc.AllowClick())
return
// In a storage item with a disassociated storage parent
var/obj/item/item_atom = A
if(istype(item_atom))
if((item_atom.item_flags & IN_STORAGE) && (item_atom.loc.flags_1 & HAS_DISASSOCIATED_STORAGE_1))
UnarmedAttack(item_atom, TRUE, modifiers)
//Standard reach turf to turf or reaching inside storage
if(A.IsReachableBy(src, W?.reach))
if(W)
W.melee_attack_chain(src, A, modifiers)
else
if(ismob(A))
changeNext_move(CLICK_CD_MELEE)
UnarmedAttack(A, TRUE, modifiers)
else
if(W)
A.base_ranged_item_interaction(src, W, modifiers)
else
if(LAZYACCESS(modifiers, RIGHT_CLICK))
ranged_secondary_attack(A, modifiers)
else
RangedAttack(A, modifiers)
/// Is the atom obscured by a PREVENT_CLICK_UNDER_1 object above it
/atom/proc/IsObscured()
SHOULD_BE_PURE(TRUE)
if(!isturf(loc)) //This only makes sense for things directly on turfs for now
return FALSE
var/turf/T = get_turf_pixel(src)
if(!T)
return FALSE
for(var/atom/movable/AM in T)
if(AM.flags_1 & PREVENT_CLICK_UNDER_1 && AM.density && AM.layer > layer)
return TRUE
return FALSE
/turf/IsObscured()
for(var/item in src)
var/atom/movable/AM = item
if(AM.flags_1 & PREVENT_CLICK_UNDER_1)
return TRUE
return FALSE
/**
* Returns TRUE if a movable can "Reach" this atom. This is defined as adjacency
*
* Args:
* * user: The movable trying to reach us.
* * reacher_range: How far the reacher can reach.
* * depth: How deep nested inside of an atom contents stack an object can be.
* * direct_access: Do not override. Used for recursion.
*/
/atom/proc/IsReachableBy(atom/movable/user, reacher_range = 1, depth = INFINITY, direct_access = user.DirectAccess())
SHOULD_NOT_OVERRIDE(TRUE)
if(isnull(user))
return FALSE
if(src in direct_access)
return TRUE
// This is a micro-opt, if any turf ever returns false from IsContainedAtomAccessible, change this.
if(isturf(loc) || isturf(src))
if(CheckReachableAdjacency(user, reacher_range))
return TRUE
depth--
if(depth <= 0)
return FALSE
if(isnull(loc) || isarea(loc))
return FALSE
if(!HAS_TRAIT(src, TRAIT_SKIP_BASIC_REACH_CHECK) && !loc.IsContainedAtomAccessible(src, user))
return FALSE
return loc.IsReachableBy(user, reacher_range, depth, direct_access)
/atom/proc/CheckReachableAdjacency(atom/movable/reacher, reacher_range)
if(reacher.Adjacent(src))
return TRUE
if(isliving(reacher))
var/mob/living/living_reacher = reacher
if(living_reacher.reach_length > reacher_range)
reacher_range = living_reacher.reach_length
return (reacher_range > 1) && RangedReachCheck(reacher, src, reacher_range)
/// Called by IsReachableBy() to check for ranged reaches.
/proc/RangedReachCheck(atom/movable/here, atom/movable/there, reach)
if(!here || !there)
return FALSE
if(reach <= 1)
return FALSE
// Prevent infinite loop.
if(istype(here, /obj/effect/abstract/reach_checker))
return FALSE
var/obj/effect/abstract/reach_checker/dummy = new(get_turf(here))
for(var/i in 1 to reach) //Limit it to that many tries
var/turf/T = get_step(dummy, get_dir(dummy, there))
if(there.IsReachableBy(dummy))
. = TRUE
break
if(!dummy.Move(T)) //we're blocked!
break
qdel(dummy)
/// Returns TRUE if an atom contained within our contents is reachable.
/atom/proc/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return TRUE
/atom/movable/IsContainedAtomAccessible(atom/contained, atom/movable/user)
return !!atom_storage
/atom/proc/DirectAccess()
return list(src, loc)
/mob/DirectAccess(atom/target)
return ..() + contents
/mob/living/DirectAccess(atom/target)
return ..() + get_all_contents()
/atom/proc/AllowClick()
return FALSE
/turf/AllowClick()
return TRUE
/proc/CheckToolReach(atom/movable/here, atom/movable/there, reach)
if(!here || !there)
return
var/turf/turf = get_turf(here)
if(turf.z != there.z)
return FALSE
switch(reach)
if(0)
return FALSE
if(1)
return FALSE //here.Adjacent(there)
if(2 to INFINITY)
var/obj/dummy = new(turf)
dummy.pass_flags |= PASSTABLE
dummy.SetInvisibility(INVISIBILITY_ABSTRACT)
for(var/i in 1 to reach) //Limit it to that many tries
var/turf/T = get_step(dummy, get_dir(dummy, there))
if(there.IsReachableBy(dummy))
qdel(dummy)
return TRUE
if(!dummy.Move(T)) //we're blocked!
qdel(dummy)
return
qdel(dummy)
/// Default behavior: ignore double clicks (the second click that makes the doubleclick call already calls for a normal click)
/mob/proc/DblClickOn(atom/A, params)
return
/**
* UnarmedAttack: The higest level of mob click chain discounting click itself.
*
* This handles, just "clicking on something" without an item. It translates
* into [atom/proc/attack_hand], [atom/proc/attack_animal] etc.
*
* Note: proximity_flag here is used to distinguish between normal usage (flag=1),
* and usage when clicking on things telekinetically (flag=0). This proc will
* not be called at ranged except with telekinesis.
*
* proximity_flag is not currently passed to attack_hand, and is instead used
* in human click code to allow glove touches only at melee range.
*
* modifiers is a lazy list of click modifiers this attack had,
* used for figuring out different properties of the click, mostly right vs left and such.
*/
/mob/proc/UnarmedAttack(atom/A, proximity_flag, list/modifiers)
if(ismob(A))
changeNext_move(CLICK_CD_MELEE)
return
/**
* Ranged unarmed attack:
*
* This currently is just a default for all mobs, involving
* laser eyes and telekinesis. You could easily add exceptions
* for things like ranged glove touches, spitting alien acid/neurotoxin,
* animals lunging, etc.
*/
/mob/proc/RangedAttack(atom/A, modifiers)
if(SEND_SIGNAL(src, COMSIG_MOB_ATTACK_RANGED, A, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN)
return TRUE
A.RangedAttackOn(src, modifiers)
/**
* Atom's version of RangedAttack, for when you want to do something when a mob clicks on this with more sanity than just Click()
*/
/atom/proc/RangedAttackOn(mob/attacker, list/modifiers)
return null
/**
* Ranged secondary attack
*
* If the same conditions are met to trigger RangedAttack but it is
* instead initialized via a right click, this will trigger instead.
* Useful for mobs that have their abilities mapped to right click.
*/
/mob/proc/ranged_secondary_attack(atom/target, modifiers)
if(SEND_SIGNAL(src, COMSIG_MOB_ATTACK_RANGED_SECONDARY, target, modifiers) & COMPONENT_CANCEL_ATTACK_CHAIN)
return TRUE
/**
* Middle click
* Mainly used for swapping hands
*/
/mob/proc/MiddleClickOn(atom/A, params)
. = SEND_SIGNAL(src, COMSIG_MOB_MIDDLECLICKON, A, params)
if(. & COMSIG_MOB_CANCEL_CLICKON)
return
swap_hand()
/**
* Shift click
* For most mobs, examine.
* This is overridden in ai.dm
*/
/mob/proc/ShiftClickOn(atom/A)
A.ShiftClick(src)
return
/atom/proc/ShiftClick(mob/user)
SEND_SIGNAL(src, COMSIG_SHIFT_CLICKED_ON, user)
var/shiftclick_flags = SEND_SIGNAL(user, COMSIG_CLICK_SHIFT, src)
if(shiftclick_flags & COMSIG_MOB_CANCEL_CLICKON)
return
if(user.client)
user.examinate(src)
/mob/proc/TurfAdjacent(turf/tile)
return tile.Adjacent(src)
/mob/proc/ShiftMiddleClickOn(atom/A)
src.pointed(A)
return
/*
Misc helpers
face_atom: turns the mob towards what you clicked on
*/
/// Simple helper to face what you clicked on, in case it should be needed in more than one place
/mob/proc/face_atom(atom/atom_to_face)
if( buckled || stat != CONSCIOUS || !atom_to_face || !x || !y || !atom_to_face.x || !atom_to_face.y )
return
var/dx = atom_to_face.x - x
var/dy = atom_to_face.y - y
if(!dx && !dy) // Wall items are graphically shifted but on the floor
if(atom_to_face.pixel_y > 16)
setDir(NORTH)
else if(atom_to_face.pixel_y < -16)
setDir(SOUTH)
else if(atom_to_face.pixel_x > 16)
setDir(EAST)
else if(atom_to_face.pixel_x < -16)
setDir(WEST)
return
if(abs(dx) < abs(dy))
if(dy > 0)
setDir(NORTH)
else
setDir(SOUTH)
else
if(dx > 0)
setDir(EAST)
else
setDir(WEST)
//debug
/atom/movable/screen/proc/scale_to(x1,y1)
if(!y1)
y1 = x1
var/matrix/M = new
M.Scale(x1,y1)
transform = M
/atom/movable/screen/click_catcher
icon = 'icons/hud/screen_gen.dmi'
icon_state = "catcher"
plane = CLICKCATCHER_PLANE
mouse_opacity = MOUSE_OPACITY_OPAQUE
screen_loc = "CENTER"
#define MAX_SAFE_BYOND_ICON_SCALE_TILES (MAX_SAFE_BYOND_ICON_SCALE_PX / ICON_SIZE_ALL)
#define MAX_SAFE_BYOND_ICON_SCALE_PX (33 * 32) //Not using world.icon_size on purpose. //Ok well I trust you
/atom/movable/screen/click_catcher/proc/UpdateGreed(view_size_x = 15, view_size_y = 15)
var/icon/newicon = icon('icons/hud/screen_gen.dmi', "catcher")
var/ox = min(MAX_SAFE_BYOND_ICON_SCALE_TILES, view_size_x)
var/oy = min(MAX_SAFE_BYOND_ICON_SCALE_TILES, view_size_y)
var/px = view_size_x * ICON_SIZE_X
var/py = view_size_y * ICON_SIZE_Y
var/sx = min(MAX_SAFE_BYOND_ICON_SCALE_PX, px)
var/sy = min(MAX_SAFE_BYOND_ICON_SCALE_PX, py)
newicon.Scale(sx, sy)
icon = newicon
screen_loc = "CENTER-[(ox-1)*0.5],CENTER-[(oy-1)*0.5]"
var/matrix/M = new
M.Scale(px/sx, py/sy)
transform = M
/atom/movable/screen/click_catcher/Initialize(mapload, datum/hud/hud_owner)
. = ..()
RegisterSignal(SSmapping, COMSIG_PLANE_OFFSET_INCREASE, PROC_REF(offset_increased))
offset_increased(SSmapping, 0, SSmapping.max_plane_offset)
// Draw to the lowest plane level offered
/atom/movable/screen/click_catcher/proc/offset_increased(datum/source, old_offset, new_offset)
SIGNAL_HANDLER
SET_PLANE_W_SCALAR(src, initial(plane), new_offset)
/atom/movable/screen/click_catcher/Click(location, control, params)
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, MIDDLE_CLICK) && iscarbon(usr))
var/mob/living/carbon/C = usr
C.swap_hand()
else
var/turf/click_turf = parse_caught_click_modifiers(modifiers, get_turf(usr.client ? usr.client.eye : usr), usr.client)
if (click_turf)
modifiers["catcher"] = TRUE
click_turf.Click(click_turf, control, list2params(modifiers))
. = 1
/// MouseWheelOn
/mob/proc/MouseWheelOn(atom/A, delta_x, delta_y, params)
SEND_SIGNAL(src, COMSIG_MOUSE_SCROLL_ON, A, delta_x, delta_y, params)
/mob/dead/observer/MouseWheelOn(atom/A, delta_x, delta_y, params)
var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, SHIFT_CLICK))
var/view = 0
if(delta_y > 0)
view = -1
else
view = 1
add_view_range(view)
/mob/proc/check_click_intercept(params,A)
//Client level intercept
if(client?.click_intercept)
if(call(client.click_intercept, "InterceptClickOn")(src, params, A))
return TRUE
//Mob level intercept
if(click_intercept)
if(call(click_intercept, "InterceptClickOn")(src, params, A))
return TRUE
return FALSE
#undef MAX_SAFE_BYOND_ICON_SCALE_TILES
#undef MAX_SAFE_BYOND_ICON_SCALE_PX