Files
Bubberstation/code/game/objects/items/devices/multitool.dm
T
SmArtKar 16aef3a2fd Completely refactors HUD element management and datumizes inventory HUDs (#95119)
## About The Pull Request

This is a port/revival of Kapu's
https://github.com/DaedalusDock/daedalusdock/pull/883
By god, please TM this for a while, as HUDs are rather volatile and I
might've missed something (also the original PR had harddel issues, so
we should probably be on the lookout for those)

Instead of being stored in a metric ton of separate variables, all HUD
elements are now kept in a ``key -> element`` assoc list, and separate
category lists have been turned into a single ``group_key -> list of
elements`` assoc list for easier management.
This massively simplifies HUD creation and management, and allows us to
sanely dynamically modify HUDs without having to keep track of our
elements ourselves (harddel fuel)

I've also noticed that plasma vessels had... interesting, to say the
least, way of managing their HUD and in humans were unable to display
it, which I've changed (the element itself is displayed below stamina in
non-aliens, as latter occupies the spot where you'd normally see it)
Also fixes a bunch of minor unlikely to occur issues with HUD not
updating when it should've sometimes.

## Why It's Good For The Game

The two most important results of this is that A) we can fix the issue
with items larger than 32x32 not displaying properly in inventories (in
a separate PR) and B) this paves the way for datumized inventory slots,
although that is a separate nightmare
Some of this code is also actually over a decade old, and is an absolute
nightmare to work with.

## Changelog
🆑
qol: Non-aliens with an implanted plasma vessel now see their plasma
level in their HUD instead of just the stat panel
refactor: Refactored the entirety of HUD management code, report if
anything breaks!
/🆑

---------

Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
2026-04-02 15:20:55 -04:00

346 lines
11 KiB
Plaintext

#define PROXIMITY_NONE ""
#define PROXIMITY_ON_SCREEN "_red"
#define PROXIMITY_NEAR "_yellow"
/**
* Multitool -- A multitool is used for hacking electronic devices.
*
*/
/obj/item/multitool
name = "multitool"
desc = "Used for pulsing wires to test which to cut. Not recommended by doctors. You can activate it in-hand to locate the nearest APC."
icon = 'icons/obj/devices/tool.dmi'
icon_state = "multitool"
inhand_icon_state = "multitool"
icon_angle = -90
lefthand_file = 'icons/mob/inhands/equipment/tools_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/tools_righthand.dmi'
force = 5
w_class = WEIGHT_CLASS_SMALL
tool_behaviour = TOOL_MULTITOOL
throwforce = 0
throw_range = 7
throw_speed = 3
drop_sound = 'sound/items/handling/tools/multitool_drop.ogg'
pickup_sound = 'sound/items/handling/tools/multitool_pickup.ogg'
custom_materials = list(
/datum/material/iron =SMALL_MATERIAL_AMOUNT * 0.5,
/datum/material/glass =SMALL_MATERIAL_AMOUNT * 0.2
)
custom_premium_price = PAYCHECK_CREW * 3
toolspeed = 1
usesound = 'sound/items/weapons/empty.ogg'
var/datum/buffer // simple machine buffer for device linkage
var/mode = 0
var/apc_scanner = TRUE
COOLDOWN_DECLARE(next_apc_scan)
/obj/item/multitool/Destroy()
if(buffer)
remove_buffer(buffer)
return ..()
/obj/item/multitool/examine(mob/user)
. = ..()
. += span_notice("Its buffer [buffer ? "contains [buffer]." : "is empty."]")
/obj/item/multitool/attack_self(mob/user, list/modifiers)
. = ..()
if(. || !apc_scanner)
return
scan_apc(user)
/obj/item/multitool/attack_self_secondary(mob/user, modifiers)
. = ..()
if(. || !apc_scanner)
return
scan_apc(user)
/obj/item/multitool/proc/scan_apc(mob/user)
if(!COOLDOWN_FINISHED(src, next_apc_scan))
return
COOLDOWN_START(src, next_apc_scan, 2 SECONDS)
var/area/local_area = get_area(src)
var/obj/machinery/power/apc/power_controller = local_area.apc
if(!power_controller)
user.balloon_alert(user, "couldn't find apc!")
return
var/dist = get_dist(src, power_controller)
var/dir = get_dir(user, power_controller)
var/balloon_message
var/arrow_color
switch(dist)
if (0)
user.balloon_alert(user, "found apc!")
return
if(1 to 5)
arrow_color = COLOR_GREEN
if(6 to 10)
arrow_color = COLOR_YELLOW
if(11 to 15)
arrow_color = COLOR_ORANGE
else
arrow_color = COLOR_RED
user.balloon_alert(user, balloon_message)
var/datum/hud/user_hud = user.hud_used
if(!user_hud)
return
var/atom/movable/screen/multitool_arrow/arrow = user_hud.add_screen_object(/atom/movable/screen/multitool_arrow, HUD_MULTITOOL_ARROW, HUD_GROUP_INFO, update_screen = TRUE)
arrow.color = arrow_color
arrow.transform = matrix(dir2angle(dir), MATRIX_ROTATE)
QDEL_IN(arrow, 1.5 SECONDS)
/atom/movable/screen/multitool_arrow
icon = 'icons/effects/96x96.dmi'
icon_state = "multitool_arrow"
pixel_x = -32
pixel_y = -32
screen_loc = around_player
/atom/movable/screen/multitool_arrow/Destroy()
var/datum/hud/our_hud = hud
. = ..()
if (!QDELETED(our_hud))
INVOKE_ASYNC(our_hud, TYPE_PROC_REF(/datum/hud, show_hud), our_hud.hud_version)
/obj/item/multitool/suicide_act(mob/living/carbon/user)
user.visible_message(span_suicide("[user] puts the [src] to [user.p_their()] chest. It looks like [user.p_theyre()] trying to pulse [user.p_their()] heart off!"))
return OXYLOSS//there's a reason it wasn't recommended by doctors
/**
* Sets the multitool internal object buffer
*
* Arguments:
* * buffer - the new object to assign to the multitool's buffer
*/
/obj/item/multitool/proc/set_buffer(datum/buffer)
if(src.buffer)
UnregisterSignal(src.buffer, COMSIG_QDELETING)
remove_buffer(src.buffer)
src.buffer = buffer
if(!QDELETED(buffer))
RegisterSignal(buffer, COMSIG_QDELETING, PROC_REF(remove_buffer))
/**
* Called when the buffer's stored object is deleted
*
* This proc does not clear the buffer of the multitool, it is here to
* handle the deletion of the object the buffer references
*/
/obj/item/multitool/proc/remove_buffer(datum/source)
SIGNAL_HANDLER
SEND_SIGNAL(src, COMSIG_MULTITOOL_REMOVE_BUFFER, source)
buffer = null
// Syndicate device disguised as a multitool; it will turn red when an AI camera is nearby.
/obj/item/multitool/ai_detect
apc_scanner = FALSE
/// How close the AI is to us
var/detect_state = PROXIMITY_NONE
/// Range at which the closest AI makes the multitool glow red
var/rangealert = 8 //Glows red when inside
/// Range at which the closest AI makes the multitool glow yellow
var/rangewarning = 20 //Glows yellow when inside
/// Is our HUD on
var/hud_on = FALSE
// static scan stuff
/// hud object that the fake static images use
var/obj/effect/overlay/ai_detect_hud/camera_unseen/hud_obj
/// fake static image
var/list/image/static_images = list()
/// the client that we shoved those images to
var/datum/weakref/static_viewer
/// timerid for the timer that makes em disappear
var/static_disappear_timer
/// cooldown for actually doing a static scan
COOLDOWN_DECLARE(static_scan_cd)
/obj/item/multitool/ai_detect/examine(mob/user)
. = ..()
if(!hud_on)
return
. += span_notice("You can right-click to scan for nearby unseen spots. They will be shown for exactly 8 seconds due to battery limitations.")
switch(detect_state)
if(PROXIMITY_NONE)
. += span_green("No AI should be currently looking at you. Keep on your clandestine activities.")
if(PROXIMITY_NEAR)
. += span_warning("An AI is getting uncomfortably close. Maybe time to drop what youre doing.")
if(PROXIMITY_ON_SCREEN)
. += span_danger("An AI is (probably) looking at you. You should probably hide this.")
/obj/item/multitool/ai_detect/Destroy()
if(hud_on && ismob(loc))
remove_hud(loc)
cleanup_static()
return ..()
/obj/item/multitool/ai_detect/attack_self(mob/user, modifiers)
. = ..()
if(.)
return
toggle_hud(user)
/obj/item/multitool/ai_detect/attack_self_secondary(mob/user, modifiers)
. = ..()
if(.)
return
scan_unseen(user)
/obj/item/multitool/ai_detect/equipped(mob/living/carbon/human/user, slot)
. = ..()
if(hud_on)
show_hud(user)
/obj/item/multitool/ai_detect/dropped(mob/living/carbon/human/user)
. = ..()
if(hud_on)
remove_hud(user)
cleanup_static()
/obj/item/multitool/ai_detect/update_icon_state()
. = ..()
icon_state = "[initial(icon_state)][detect_state]"
/obj/item/multitool/ai_detect/process()
var/old_detect_state = detect_state
multitool_detect()
if(detect_state != old_detect_state)
update_appearance()
/obj/item/multitool/ai_detect/proc/toggle_hud(mob/user)
hud_on = !hud_on
if(user)
to_chat(user, span_notice("You toggle the ai detection feature on [src] [hud_on ? "on" : "off"]."))
if(hud_on)
START_PROCESSING(SSfastprocess, src)
show_hud(user)
else
STOP_PROCESSING(SSfastprocess, src)
detect_state = PROXIMITY_NONE
update_appearance(UPDATE_ICON)
remove_hud(user)
/obj/item/multitool/ai_detect/proc/show_hud(mob/user)
var/datum/atom_hud/hud = GLOB.huds[DATA_HUD_AI_DETECT]
hud.show_to(user)
/obj/item/multitool/ai_detect/proc/remove_hud(mob/user)
var/datum/atom_hud/hud = GLOB.huds[DATA_HUD_AI_DETECT]
hud.hide_from(user)
/obj/item/multitool/ai_detect/proc/multitool_detect()
var/turf/our_turf = get_turf(src)
detect_state = PROXIMITY_NONE
for(var/mob/eye/camera/ai/AI_eye in GLOB.camera_eyes)
if(!AI_eye.ai_detector_visible)
continue
var/turf/ai_turf = get_turf(AI_eye)
var/distance = get_dist(our_turf, ai_turf)
if(distance == -1) //get_dist() returns -1 for distances greater than 127 (and for errors, so assume -1 is just max range)
if(ai_turf == our_turf)
detect_state = PROXIMITY_ON_SCREEN
break
continue
if(distance < rangealert) //ai should be able to see us
detect_state = PROXIMITY_ON_SCREEN
break
if(distance < rangewarning) //ai can't see us but is close
detect_state = PROXIMITY_NEAR
/obj/item/multitool/ai_detect/proc/scan_unseen(mob/user)
if(isnull(user?.client)) // the monkey incident of 2564
return
if(!COOLDOWN_FINISHED(src, static_scan_cd))
balloon_alert(user, "recharging!")
return
cleanup_static()
var/turf/our_turf = get_turf(src)
var/list/datum/camerachunk/chunks = surrounding_chunks(our_turf)
if(!hud_obj)
hud_obj = new()
SET_PLANE_W_SCALAR(hud_obj, PLANE_TO_TRUE(hud_obj.plane), GET_TURF_PLANE_OFFSET(our_turf))
var/list/new_images = list()
for(var/datum/camerachunk/chunk as anything in chunks)
for(var/turf/seen_turf as anything in chunk.obscuredTurfs)
var/image/img = image(loc = seen_turf, layer = ABOVE_ALL_MOB_LAYER)
img.vis_contents += hud_obj
SET_PLANE(img, GAME_PLANE, seen_turf)
new_images += img
user.client.images |= new_images
static_viewer = WEAKREF(user.client)
balloon_alert(user, "nearby unseen spots shown")
static_disappear_timer = addtimer(CALLBACK(src, PROC_REF(cleanup_static)), 8 SECONDS, TIMER_STOPPABLE)
COOLDOWN_START(src, static_scan_cd, 4 SECONDS)
// copied from camera chunks but we are doing a really big edge case here though
/obj/item/multitool/ai_detect/proc/surrounding_chunks(turf/epicenter)
. = list()
var/static_range = /mob/eye/camera/ai::static_visibility_range
var/x1 = max(1, epicenter.x - static_range)
var/y1 = max(1, epicenter.y - static_range)
var/x2 = min(world.maxx, epicenter.x + static_range)
var/y2 = min(world.maxy, epicenter.y + static_range)
for(var/x = x1; x <= x2; x += CHUNK_SIZE)
for(var/y = y1; y <= y2; y += CHUNK_SIZE)
var/datum/camerachunk/chunk = SScameras.generate_chunk(x, y, epicenter.z)
// removing cameras in build mode didnt affect it and i guess it needs an AI eye to update so we have to do this manually
// unless we only want to see static in a jank manner only if an eye updates it
chunk?.force_update(only_if_necessary = FALSE) // UPDATE THE FUCK NOW
. |= chunk
/obj/item/multitool/ai_detect/proc/cleanup_static()
if(isnull(hud_obj)) //we never did anything
return
var/client/viewer = static_viewer?.resolve()
viewer?.images -= static_images
static_images.Cut()
QDEL_NULL(hud_obj)
viewer = null
deltimer(static_disappear_timer)
static_disappear_timer = null
/obj/item/multitool/abductor
name = "alien multitool"
desc = "An omni-technological interface."
icon = 'icons/obj/antags/abductor.dmi'
icon_state = "multitool"
inside_belt_icon_state = "multitool_alien"
custom_materials = list(/datum/material/iron = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/silver = SHEET_MATERIAL_AMOUNT * 1.25, /datum/material/plasma = SHEET_MATERIAL_AMOUNT * 2.5, /datum/material/titanium = SHEET_MATERIAL_AMOUNT, /datum/material/diamond = SHEET_MATERIAL_AMOUNT)
toolspeed = 0.1
/obj/item/multitool/cyborg
name = "electronic multitool"
desc = "Optimised version of a regular multitool. Streamlines processes handled by its internal microchip."
icon = 'icons/obj/items_cyborg.dmi'
icon_state = "toolkit_engiborg_multitool"
icon_angle = 0
toolspeed = 0.5
#undef PROXIMITY_NEAR
#undef PROXIMITY_NONE
#undef PROXIMITY_ON_SCREEN