Files
Batrachophreno 142862c8bc Container cleanup, standalone security panel (#20909)
Updated crate and locker sprites and icon overlay code to use a new
standalone 'securitypanel' overlay to lay the groundwork for, in the
future, being able to install/uninstall access-code locks on containers
just like you can in airlocks. For lockers, little visual change at all.
For crates, the security panel is now an overlay within the 'handle'
section of the crate rather than being a chunky box off to the side on
front. Locked/unlocked/emag sprites also updated for the new panel
location.
![Screenshot 2025-06-28
130646](https://github.com/user-attachments/assets/4b4469e7-5d62-42e0-9d9b-27c2f70a4239)
![Screenshot 2025-06-28
130944](https://github.com/user-attachments/assets/5741094f-9214-435a-8ef1-97e5c06b1c61)
![Screenshot 2025-06-28
130951](https://github.com/user-attachments/assets/3d9563b5-a89c-407d-9f8b-190d6382fb8b)

Moved closet.dmi, crate.dmi, guncabinet.dmi, safe.dmi, and
walllocker.dmi to a new subfolder /icons/obj/containers, and updated all
references to them in code, for sake of organization. Ideally (IMHO),
we'd differentiate structure-based storage containers from item-based
storage containers, but that's a larger organizational task and outside
of scope- this just moves storage structures into one common location.

Deleted sciguncabinet.dmi, as its contents had already been migrated to
guncabinet.dmi and it was no longer referenced anywhere in the codebase.
2026-01-14 17:38:00 +00:00

315 lines
9.8 KiB
Plaintext

/*
CONTAINS:
SAFES
FLOOR SAFES
*/
//SAFES
/obj/structure/safe
name = "safe"
desc = "A huge chunk of metal with a dial embedded in it. Fine print on the dial reads \"Scarborough Arms - 2 tumbler safe, guaranteed thermite resistant, explosion resistant, and assistant resistant.\""
icon = 'icons/obj/containers/safe.dmi'
icon_state = "safe"
anchored = 1
density = 1
var/broken = FALSE //is the tumbler broken into
var/open = 0 //is the safe open?
var/tumbler_1_pos //the tumbler position- from 0 to 71
var/tumbler_1_open //the tumbler position to open at- 0 to 71
var/tumbler_2_pos
var/tumbler_2_open
var/dial = 0 //where is the dial pointing?
var/space = 0 //the combined w_class of everything in the safe
var/maxspace = 24 //the maximum combined w_class of stuff in the safe
// Variables (Drill)
var/obj/item/thermal_drill/drill // The currently placed thermal drill, if any.
var/time_to_drill = 300 SECONDS // Drill duration of the current thermal drill.
var/last_drill_time = 0 // Last world.time the drill time was checked. Used to reduce time_to_drill accurately
var/image/drill_overlay // The drill overlay image to display during the drilling process.
var/drill_x_offset = -4 // The X pixel offset for the drill
var/drill_y_offset = -8 // The Y pixel offset for the drill
/obj/structure/safe/feedback_hints(mob/user, distance, is_adjacent)
. += ..()
if(broken)
. += SPAN_WARNING("\The [src]'s locking system has been drilled open!")
else if(time_to_drill < 300 SECONDS)
var/time_left = max(round(time_to_drill / 10), 0)
. += SPAN_WARNING("There are only [time_left] second\s of drilling left until \the [src] is broken!")
/obj/structure/safe/Initialize()
. = ..()
tumbler_1_pos = rand(0, 71)
tumbler_1_open = rand(0, 71)
tumbler_2_pos = rand(0, 71)
tumbler_2_open = min(71 , max( 1 , abs(tumbler_1_open + rand(-34, 34))))
for(var/obj/item/I in loc)
if(space >= maxspace)
return
if(I.w_class + space <= maxspace)
space += I.w_class
I.forceMove(src)
/obj/structure/safe/Destroy()
if(drill)
drill.soundloop.stop()
drill.forceMove(loc)
drill = null
return ..()
/obj/structure/safe/proc/check_unlocked(mob/user as mob, canhear)
if(user && canhear)
if(tumbler_1_pos == tumbler_1_open)
to_chat(user, SPAN_NOTICE("You hear a [pick("tonk", "krunk", "plunk")] from [src]."))
if(tumbler_2_pos == tumbler_2_open)
to_chat(user, SPAN_NOTICE("You hear a [pick("tink", "krink", "plink")] from [src]."))
if(tumbler_1_pos == tumbler_1_open && tumbler_2_pos == tumbler_2_open)
if(user) visible_message("<b>[pick("Spring", "Sprang", "Sproing", "Clunk", "Krunk")]!</b>")
return 1
return 0
/obj/structure/safe/proc/decrement(num)
num -= 1
if(num < 0)
num = 71
return num
/obj/structure/safe/proc/increment(num)
num += 1
if(num > 71)
num = 0
return num
/obj/structure/safe/update_icon()
if(open)
if(broken)
icon_state = "[initial(icon_state)]-open-broken"
else
icon_state = "[initial(icon_state)]-open"
else
if(broken)
icon_state = "[initial(icon_state)]-broken"
else
icon_state = initial(icon_state)
CutOverlays(drill_overlay)
if(istype(drill, /obj/item/thermal_drill))
var/drill_icon = istype(drill, /obj/item/thermal_drill/diamond_drill) ? "d" : "h"
var/state = "[initial(icon_state)]_[drill_icon]-drill-[(datum_flags & DF_ISPROCESSING) ? "on" : "off"]"
drill_overlay = image(icon = 'icons/effects/drill.dmi', icon_state = state, pixel_x = drill_x_offset, pixel_y = drill_y_offset)
AddOverlays(drill_overlay)
/obj/structure/safe/attack_hand(mob/user as mob)
if(drill)
switch(alert("What would you like to do?", "Thermal Drill", "Turn [(datum_flags & DF_ISPROCESSING) ? "Off" : "On"]", "Remove Drill", "Cancel"))
if("Turn On")
if(!drill || (datum_flags & DF_ISPROCESSING))
return
if(broken)
to_chat(user, SPAN_WARNING("\The [src] is already broken open!"))
return
if(do_after(user, 2 SECONDS))
if(!drill || (datum_flags & DF_ISPROCESSING))
return
if(broken)
return
last_drill_time = world.time
drill.soundloop.start()
START_PROCESSING(SSprocessing, src)
update_icon()
if("Turn Off")
if(!drill || !(datum_flags & DF_ISPROCESSING))
return
if(do_after(user, 2 SECONDS))
if(!drill || !(datum_flags & DF_ISPROCESSING))
return
drill.soundloop.stop()
STOP_PROCESSING(SSprocessing, src)
update_icon()
if("Remove Drill")
if(datum_flags & DF_ISPROCESSING)
to_chat(user, SPAN_WARNING("You cannot remove the drill while it's running!"))
else if(do_after(user, 2 SECONDS))
if(datum_flags & DF_ISPROCESSING)
return
user.put_in_hands(drill)
drill = null
update_icon()
if("Cancel")
return
else
user.set_machine(src)
var/dat = "<center>"
dat += "<a href='byond://?src=[REF(src)];open=1'>[open ? "Close" : "Open"] [src]</a>[drill || broken ? "" : " | <a href='byond://?src=[REF(src)];decrement=1'>-</a> [dial * 5] <a href='byond://?src=[REF(src)];increment=1'>+</a>"]"
if(open)
dat += "<table>"
for(var/i = contents.len, i>=1, i--)
var/obj/item/P = contents[i]
dat += "<tr><td><a href='byond://?src=[REF(src)];retrieve=[REF(P)]'>[P.name]</a></td></tr>"
dat += "</table></center>"
user << browse("<html><head><title>[name]</title></head><body>[dat]</body></html>", "window=safe;size=350x300")
/obj/structure/safe/Topic(href, href_list)
if(!ishuman(usr)) return
var/mob/living/carbon/human/user = usr
var/canhear = 0
if(istype(user.l_hand, /obj/item/clothing/accessory/stethoscope) || istype(user.r_hand, /obj/item/clothing/accessory/stethoscope))
canhear = 1
if(href_list["open"])
if(drill)
to_chat(user, SPAN_WARNING("You can't open \the [src] if there's a drill attached."))
return
if(broken || check_unlocked())
to_chat(user, SPAN_NOTICE("You [open ? "close" : "open"] [src]."))
open = !open
update_icon()
updateUsrDialog()
return
else
to_chat(user, SPAN_NOTICE("You can't [open ? "close" : "open"] [src], the lock is engaged!"))
return
if(href_list["decrement"])
dial = decrement(dial)
if(dial == tumbler_1_pos + 1 || dial == tumbler_1_pos - 71)
tumbler_1_pos = decrement(tumbler_1_pos)
if(canhear)
to_chat(user, SPAN_NOTICE("You hear a [pick("clack", "scrape", "clank")] from [src]."))
if(tumbler_1_pos == tumbler_2_pos + 37 || tumbler_1_pos == tumbler_2_pos - 35)
tumbler_2_pos = decrement(tumbler_2_pos)
if(canhear)
to_chat(user, SPAN_NOTICE("You hear a [pick("click", "chink", "clink")] from [src]."))
check_unlocked(user, canhear)
updateUsrDialog()
return
if(href_list["increment"])
dial = increment(dial)
if(dial == tumbler_1_pos - 1 || dial == tumbler_1_pos + 71)
tumbler_1_pos = increment(tumbler_1_pos)
if(canhear)
to_chat(user, SPAN_NOTICE("You hear a [pick("clack", "scrape", "clank")] from [src]."))
if(tumbler_1_pos == tumbler_2_pos - 37 || tumbler_1_pos == tumbler_2_pos + 35)
tumbler_2_pos = increment(tumbler_2_pos)
if(canhear)
to_chat(user, SPAN_NOTICE("You hear a [pick("click", "chink", "clink")] from [src]."))
check_unlocked(user, canhear)
updateUsrDialog()
return
if(href_list["retrieve"])
user << browse("", "window=safe") // Close the menu)
var/obj/item/P = locate(href_list["retrieve"]) in src
if(open)
if(P && in_range(src, user))
user.put_in_hands(P)
updateUsrDialog()
/obj/structure/safe/attackby(obj/item/attacking_item, mob/user)
if(open)
if(attacking_item.w_class + space <= maxspace)
space += attacking_item.w_class
user.drop_from_inventory(attacking_item, src)
to_chat(user, SPAN_NOTICE("You put [attacking_item] in [src]."))
updateUsrDialog()
return
else
to_chat(user, SPAN_NOTICE("[attacking_item] won't fit in [src]."))
return
else
if(istype(attacking_item, /obj/item/clothing/accessory/stethoscope))
attack_hand(user)
else if(istype(attacking_item, /obj/item/thermal_drill))
if(drill)
to_chat(user, SPAN_WARNING("There is already a drill attached!"))
else if(do_after(user, 2 SECONDS))
user.drop_from_inventory(attacking_item, src)
drill = attacking_item
update_icon()
else
to_chat(user, SPAN_WARNING("You can't put [attacking_item] into the safe while it is closed!"))
/obj/structure/safe/process()
if(!drill)
return
time_to_drill -= (world.time - last_drill_time) * drill.time_multiplier
last_drill_time = world.time
if(prob(15))
spark(loc, 3, GLOB.alldirs)
if(time_to_drill <= 0)
drill_open()
/obj/structure/safe/proc/drill_open()
broken = TRUE
drill.soundloop.stop()
STOP_PROCESSING(SSprocessing, src)
update_icon()
/obj/structure/safe/ex_act(severity)
return
//FLOOR SAFES
/obj/structure/safe/floor
name = "floor safe"
icon_state = "floorsafe"
density = 0
level = 1 //underfloor
layer = BELOW_OBJ_LAYER
drill_x_offset = -1
drill_y_offset = 20
/obj/structure/safe/floor/Initialize()
. = ..()
var/turf/T = loc
if(istype(T) && !T.is_plating())
hide(1)
update_icon()
/obj/structure/safe/floor/hide(var/intact)
set_invisibility(intact ? 101 : 0)
/obj/structure/safe/floor/hides_under_flooring()
return 1
//random station safe, may come with some different loot
/obj/structure/safe/station
name = "corporate safe"
/obj/structure/safe/station/Initialize()
. = ..()
new /obj/item/stack/telecrystal/twentyfive(src)
new /obj/random/highvalue/safe(src)
new /obj/random/highvalue/safe(src)
new /obj/random/highvalue/safe(src)
new /obj/random/highvalue/safe(src)
new /obj/random/highvalue/safe(src)
/obj/structure/safe/cash
name = "credit safe"
/obj/structure/safe/cash/Initialize()
. = ..()
new /obj/random/highvalue/cash(src)
new /obj/random/highvalue/cash(src)
new /obj/random/highvalue/cash(src)
/obj/structure/safe/highvalue
name = "valuables safe"
/obj/structure/safe/highvalue/Initialize()
. = ..()
new /obj/random/highvalue/no_weapon(src)
new /obj/random/highvalue/no_weapon(src)
new /obj/random/highvalue/no_weapon(src)
new /obj/random/highvalue/no_weapon(src)