diff --git a/code/game/objects/structures/safe.dm b/code/game/objects/structures/safe.dm
index e659bbeedd9..4d5f0996cd1 100644
--- a/code/game/objects/structures/safe.dm
+++ b/code/game/objects/structures/safe.dm
@@ -6,50 +6,70 @@ SAFE INTERNALS
SAFE CODES
*/
+#define DRILL_SPARK_CHANCE 15
+#define DRILL_TIME 300 SECONDS
+#define SOUND_CHANCE 10
+
GLOBAL_LIST_EMPTY(safes)
-//SAFES
+/**
+ * # Safe
+ *
+ * A locked container that can only be opened by entering a combination through a dial.
+ */
/obj/structure/safe
- name = "\improper Safe"
+ name = "safe"
desc = "A huge chunk of metal with a dial embedded in it. Fine print on the dial reads \"Scarborough Arms tumbler safe, guaranteed thermite resistant, explosion resistant, and assistant resistant.\""
icon = 'icons/obj/structures.dmi'
icon_state = "safe"
-
anchored = TRUE
density = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ACID_PROOF
-
+ // Settings
+ /// The maximum combined w_class of stuff in the safe.
+ var/maxspace = 24
+ /// The amount of tumblers that will be generated.
+ var/number_of_tumblers = 3
+ // Variables
+ /// Whether the safe is open or not.
var/open = FALSE
+ /// Whether the safe is locked or not.
var/locked = TRUE
- var/dial = 0 // The position the dial is pointing to.
-
- var/number_of_tumblers = 3 // The amount of tumblers that will be generated.
- var/list/tumblers = list() // The list of tumbler dial positions that need to be hit.
- var/current_tumbler_index = 1 // The index in the tumblers list of the tumbler dial position that needs to be hit.
-
- 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.
-
+ /// The position the dial is pointing to.
+ var/dial = 0
+ /// The list of tumbler dial positions that need to be hit.
+ var/list/tumblers = list()
+ /// The index in the tumblers list of the tumbler dial position that needs to be hit.
+ var/current_tumbler_index = 1
+ /// The combined w_class of everything in the safe.
+ var/space = 0
+ // Variables (Drill)
+ /// The currently placed thermal drill, if any.
var/obj/item/thermal_drill/drill = null
+ /// The [/proc/addtimer] handle for the current thermal drill.
var/drill_timer
+ /// Drill duration of the current thermal drill.
var/time_to_drill
- var/image/bar
+ /// The world.time at which drilling started.
var/drill_start_time
- var/drill_x_offset = -13
- var/drill_y_offset = -3
- var/known_by = list()
-
- var/image/progress_bar
+ /// The drill overlay image to display during the drilling process.
var/image/drill_overlay
+ /// The progress bar image to display during the drilling process.
+ var/image/progress_bar
+ /// The X pixel offset for the drilling progress bar.
+ var/drill_x_offset = -13
+ /// The Y pixel offset for the drilling progress bar.
+ var/drill_y_offset = -3
+ /// Used by [/obj/item/paper/safe_code] to tell the codes through a paper spawned on map load.
+ var/known_by = list()
/obj/structure/safe/Initialize(mapload)
. = ..()
-
GLOB.safes += src
-
+ // Combination generation
for(var/i in 1 to number_of_tumblers)
tumblers.Add(rand(0, 99))
-
+ // Put as many items on our turf inside as possible
for(var/obj/item/I in loc)
if(space >= maxspace)
return
@@ -59,25 +79,26 @@ GLOBAL_LIST_EMPTY(safes)
/obj/structure/safe/Destroy()
GLOB.safes -= src
- drill?.soundloop?.stop()
- drill?.forceMove(loc)
- drill = null
+ if(drill)
+ drill.soundloop.stop()
+ drill.forceMove(loc)
+ drill = null
- qdel(progress_bar)
- qdel(drill_overlay)
+ QDEL_NULL(progress_bar)
+ QDEL_NULL(drill_overlay)
return ..()
/obj/structure/safe/process()
- if(drill_timer)
- cut_overlay(progress_bar)
- progress_bar = image('icons/effects/progessbar.dmi', src, "prog_bar_[round((((world.time - drill_start_time) / time_to_drill) * 100), 5)]", HUD_LAYER)
- add_overlay(progress_bar)
- if(prob(15))
- drill.spark_system.start()
+ if(!drill_timer)
+ return
+ cut_overlay(progress_bar)
+ progress_bar = image('icons/effects/progessbar.dmi', src, "prog_bar_[round((((world.time - drill_start_time) / time_to_drill) * 100), 5)]", HUD_LAYER)
+ add_overlay(progress_bar)
+ if(prob(DRILL_SPARK_CHANCE))
+ drill.spark_system.start()
/obj/structure/safe/examine(mob/user)
. = ..()
-
. += "This model appears to have [number_of_tumblers] tumblers."
if(open)
. += "The inside of the the door has numbers written on it: [get_combination()]"
@@ -91,28 +112,6 @@ GLOBAL_LIST_EMPTY(safes)
/obj/structure/safe/examine_status(mob/user)
return
-/obj/structure/safe/proc/check_unlocked()
- if(current_tumbler_index > number_of_tumblers)
- locked = FALSE
- visible_message("[pick("Spring", "Sprang", "Sproing", "Clunk", "Krunk")]!")
- return TRUE
-
- locked = TRUE
- return FALSE
-
-/obj/structure/safe/proc/get_combination()
- var/combination = ""
- var/looped = 0
-
- for(var/tumbler in tumblers)
- looped++
- combination += "[tumbler]"
-
- if(looped < LAZYLEN(tumblers))
- combination += ", "
-
- return combination
-
/obj/structure/safe/update_icon()
if(open)
if(broken)
@@ -133,18 +132,14 @@ GLOBAL_LIST_EMPTY(safes)
if(istype(drill, /obj/item/thermal_drill))
var/drill_icon = istype(drill, /obj/item/thermal_drill/diamond_drill) ? "d" : "h"
- if(drill_timer)
- drill_overlay = image(icon = 'icons/effects/drill.dmi', icon_state = "[initial(icon_state)]_[drill_icon]-drill-on", pixel_x = drill_x_offset, pixel_y = drill_y_offset)
- else
- drill_overlay = image(icon = 'icons/effects/drill.dmi', icon_state = "[initial(icon_state)]_[drill_icon]-drill-off", pixel_x = drill_x_offset, pixel_y = drill_y_offset)
-
+ var/state = "[initial(icon_state)]_[drill_icon]-drill-[drill_timer ? "on" : "off"]"
+ drill_overlay = image(icon = 'icons/effects/drill.dmi', icon_state = state, pixel_x = drill_x_offset, pixel_y = drill_y_offset)
add_overlay(drill_overlay)
/obj/structure/safe/attack_ghost(mob/user)
if(..() || drill)
return TRUE
-
- ui_interact(user)
+ tgui_interact(user)
/obj/structure/safe/attack_hand(mob/user)
if(..())
@@ -168,7 +163,7 @@ GLOBAL_LIST_EMPTY(safes)
STOP_PROCESSING(SSobj, src)
if("Remove Drill")
if(drill_timer)
- to_chat(user, "You cant remove the drill while it's running!")
+ to_chat(user, "You cannot remove the drill while it's running!")
else if(do_after(user, 2 SECONDS, target = src))
user.put_in_hands(drill)
drill = null
@@ -176,145 +171,14 @@ GLOBAL_LIST_EMPTY(safes)
if("Cancel")
return
else
- ui_interact(user)
-
-/obj/structure/safe/ui_interact(mob/user, ui_key = "main", datum/nanoui/ui = null, force_open = 1)
- ui = SSnanoui.try_update_ui(user, src, ui_key, ui, force_open)
- if(!ui)
- ui = new(user, src, ui_key, "safe.tmpl", name, 600, 750)
- ui.open()
- ui.set_auto_update(1)
-
-/obj/structure/safe/ui_data(mob/user, ui_key = "main", datum/topic_state/state = GLOB.default_state)
- var/data[0]
- var/list/contents_names = list()
- if(open)
- for(var/obj/O in contents)
- contents_names[++contents_names.len] = list("name" = O.name, "index" = contents.Find(O), "sprite" = O.icon_state)
- user << browse_rsc(icon(O.icon, O.icon_state), "[O.icon_state].png")
-
- data["dial"] = dial
- data["open"] = open
- data["locked"] = locked
- data["rotation"] = "[-dial * 3.6]deg"
- data["contents"] = contents_names
-
- return data
-
-/obj/structure/safe/proc/notify_user(user, canhear, sounds, total_ticks, current_tick)
- if(!canhear)
- return
-
- if(current_tick == 2)
- to_chat(user, "The sounds from [src] are too fast and blend together.")
-
- if(total_ticks == 1 || prob(10))
- to_chat(user, "You hear a [pick(sounds)] from [src].")
-
-/obj/structure/safe/Topic(href, href_list)
- if(..())
- return TRUE
-
- var/mob/user = usr
- if(!user.IsAdvancedToolUser() && !isobserver(user))
- to_chat(user, "You're not able to operate the safe.")
- return
-
- var/canhear = FALSE
- if(ishuman(user))
- var/mob/living/carbon/human/H = user
- if(H.can_hear() && H.is_in_hands(/obj/item/clothing/accessory/stethoscope))
- canhear = TRUE
-
- if(href_list["open"])
- if(check_unlocked() || open || broken)
- to_chat(user, "You [open ? "close" : "open"] [src].")
- open = !open
- update_icon()
- else
- to_chat(user, "You can't open [src], as its lock is engaged!")
-
- . = TRUE
-
- if(href_list["turnright"])
- if(open)
- return
-
- if(broken)
- to_chat(user, "The dial will not turn, as the mechanism is destroyed.")
- return
-
- var/ticks = text2num(href_list["turnright"])
- for(var/i = 1 to ticks)
- dial = WRAP(dial - 1, 0, 100)
-
- var/invalid_turn = current_tumbler_index % 2 == 0 || current_tumbler_index > number_of_tumblers
- if(invalid_turn) // The moment you turn the wrong way or go too far, the tumblers reset
- current_tumbler_index = 1
-
- if(!invalid_turn && dial == tumblers[current_tumbler_index])
- notify_user(user, canhear, list("tink", "krink", "plink"), ticks, i)
- current_tumbler_index++
- else
- notify_user(user, canhear, list("clack", "scrape", "clank"), ticks, i)
-
- sleep(1)
- check_unlocked(user, canhear)
- SSnanoui.update_uis(src)
-
- . = TRUE
-
- if(href_list["turnleft"])
- if(open)
- return
-
- if(broken)
- to_chat(user, "The dial will not turn, as the mechanism is destroyed.")
- return
-
- var/ticks = text2num(href_list["turnleft"])
- for(var/i = 1 to ticks)
- dial = WRAP(dial + 1, 0, 100)
-
- var/invalid_turn = current_tumbler_index % 2 != 0 || current_tumbler_index > number_of_tumblers
- if(invalid_turn) // The moment you turn the wrong way or go too far, the tumblers reset
- current_tumbler_index = 1
-
- if(!invalid_turn && dial == tumblers[current_tumbler_index])
- notify_user(user, canhear, list("tonk", "krunk", "plunk"), ticks, i)
- current_tumbler_index++
- else
- notify_user(user, canhear, list("click", "chink", "clink"), ticks, i)
-
- sleep(1)
- check_unlocked(user, canhear)
- SSnanoui.update_uis(src)
-
- . = TRUE
-
- if(href_list["retrieve"])
- var/index = text2num(href_list["retrieve"])
- if(index > 0 && index <= contents.len)
- var/obj/item/P = contents[index]
- if(open)
- if(P && in_range(src, user))
- user.put_in_hands(P)
- space -= P.w_class
- . = TRUE
-
-/obj/structure/safe/proc/drill_open()
- broken = TRUE
- drill_timer = null
- drill.soundloop.stop()
- update_icon()
- STOP_PROCESSING(SSobj, src)
+ tgui_interact(user)
/obj/structure/safe/attackby(obj/item/I, mob/user, params)
if(open)
if(broken && istype(I, /obj/item/safe_internals) && do_after(user, 2 SECONDS, target = src))
to_chat(user, "You replace the broken mechanism.")
qdel(I)
- broken = !broken
+ broken = FALSE
update_icon()
else if(I.w_class + space <= maxspace)
if(!user.drop_item())
@@ -323,11 +187,12 @@ GLOBAL_LIST_EMPTY(safes)
space += I.w_class
I.forceMove(src)
to_chat(user, "You put [I] in [src].")
+ SStgui.update_uis(src)
else
- to_chat(user, "[I] won't fit in [src].")
+ to_chat(user, "[I] won't fit in [src].")
else
if(istype(I, /obj/item/clothing/accessory/stethoscope))
- to_chat(user, "Hold [I] in one of your hands while you manipulate the dial!")
+ attack_hand(user)
return
else if(istype(I, /obj/item/thermal_drill))
if(drill)
@@ -338,13 +203,159 @@ GLOBAL_LIST_EMPTY(safes)
return
I.forceMove(src)
drill = I
- time_to_drill = 300 SECONDS * drill.time_multiplier
+ time_to_drill = DRILL_TIME * drill.time_multiplier
update_icon()
else
to_chat(user, "You can't put [I] into the safe while it is closed!")
return
-//FLOOR SAFES
+/obj/structure/safe/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = TRUE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_physical_state)
+ var/datum/asset/safe_assets = get_asset_datum(/datum/asset/simple/safe)
+ safe_assets.send(user)
+
+ ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
+ if(!ui)
+ ui = new(user, src, ui_key, "Safe", name, 600, 750)
+ ui.open()
+ ui.set_autoupdate(FALSE)
+
+/obj/structure/safe/tgui_data(mob/user)
+ var/list/data = list()
+ data["dial"] = dial
+ data["open"] = open
+ data["locked"] = locked
+
+ if(open)
+ var/list/contents_names = list()
+ data["contents"] = contents_names
+ for(var/obj/O in contents)
+ contents_names[++contents_names.len] = list("name" = O.name, "sprite" = O.icon_state)
+ user << browse_rsc(icon(O.icon, O.icon_state), "[O.icon_state].png")
+
+ return data
+
+/obj/structure/safe/tgui_act(action, params)
+ if(..())
+ return
+
+ if(!usr.IsAdvancedToolUser() && !isobserver(usr))
+ to_chat(usr, "You are not able to operate the safe.")
+ return
+
+ var/canhear = FALSE
+ if(ishuman(usr))
+ var/mob/living/carbon/human/H = usr
+ if(H.can_hear() && H.is_in_hands(/obj/item/clothing/accessory/stethoscope))
+ canhear = TRUE
+
+ . = TRUE
+ switch(action)
+ if("open")
+ if(check_unlocked() || open || broken)
+ to_chat(usr, "You [open ? "close" : "open"] [src].")
+ open = !open
+ update_icon()
+ else
+ to_chat(usr, "You cannot open [src], as its lock is engaged!")
+ if("turnright")
+ if(open)
+ return
+ if(broken)
+ to_chat(usr, "The dial will not turn, as the mechanism is destroyed!")
+ return
+ var/ticks = text2num(params["num"])
+ for(var/i = 1 to ticks)
+ dial = WRAP(dial - 1, 0, 100)
+
+ var/invalid_turn = current_tumbler_index % 2 == 0 || current_tumbler_index > number_of_tumblers
+ if(invalid_turn) // The moment you turn the wrong way or go too far, the tumblers reset
+ current_tumbler_index = 1
+
+ if(!invalid_turn && dial == tumblers[current_tumbler_index])
+ notify_user(usr, canhear, list("tink", "krink", "plink"), ticks, i)
+ current_tumbler_index++
+ else
+ notify_user(usr, canhear, list("clack", "scrape", "clank"), ticks, i)
+ check_unlocked()
+ if("turnleft")
+ if(open)
+ return
+ if(broken)
+ to_chat(usr, "The dial will not turn, as the mechanism is destroyed!")
+ return
+ var/ticks = text2num(params["num"])
+ for(var/i = 1 to ticks)
+ dial = WRAP(dial + 1, 0, 100)
+
+ var/invalid_turn = current_tumbler_index % 2 != 0 || current_tumbler_index > number_of_tumblers
+ if(invalid_turn) // The moment you turn the wrong way or go too far, the tumblers reset
+ current_tumbler_index = 1
+
+ if(!invalid_turn && dial == tumblers[current_tumbler_index])
+ notify_user(usr, canhear, list("tonk", "krunk", "plunk"), ticks, i)
+ current_tumbler_index++
+ else
+ notify_user(usr, canhear, list("click", "chink", "clink"), ticks, i)
+ check_unlocked()
+ if("retrieve")
+ if(!open)
+ return
+ var/index = text2num(params["index"])
+ if(!ISINDEXSAFE(contents, index))
+ return
+ var/obj/item/I = contents[index]
+ if(I && in_range(src, usr))
+ usr.put_in_hands(I)
+ space -= I.w_class
+ else
+ return FALSE
+
+ add_fingerprint(usr)
+
+/**
+ * Called every dial turn to determine whether the safe should unlock or not.
+ */
+/obj/structure/safe/proc/check_unlocked()
+ if(current_tumbler_index > number_of_tumblers)
+ locked = FALSE
+ visible_message("[pick("Spring", "Sprang", "Sproing", "Clunk", "Krunk")]!")
+ return TRUE
+ locked = TRUE
+ return FALSE
+
+/**
+ * Called every dial turn to provide feedback if possible.
+ */
+/obj/structure/safe/proc/notify_user(user, canhear, sounds, total_ticks, current_tick)
+ if(!canhear)
+ return
+
+ if(current_tick == 2)
+ to_chat(user, "The sounds from [src] are too fast and blend together.")
+ if(total_ticks == 1 || prob(SOUND_CHANCE))
+ to_chat(user, "You hear a [pick(sounds)] from [src].")
+
+/**
+ * Returns the combination to unlock the safe as text.
+ */
+/obj/structure/safe/proc/get_combination()
+ return jointext(tumblers, ", ")
+
+/**
+ * Called when the current thermal drill has finished drilling.
+ */
+/obj/structure/safe/proc/drill_open()
+ broken = TRUE
+ drill_timer = null
+ drill.soundloop.stop()
+ update_icon()
+ STOP_PROCESSING(SSobj, src)
+
+/**
+ * # Floor Safe
+ *
+ * Like a safe, but without density. Can be hidden with flooring.
+ */
/obj/structure/safe/floor
name = "floor safe"
icon_state = "floorsafe"
@@ -362,11 +373,21 @@ GLOBAL_LIST_EMPTY(safes)
/obj/structure/safe/floor/hide(intact)
invisibility = intact ? INVISIBILITY_MAXIMUM : 0
+/**
+ * # Safe Internals
+ *
+ * Can be used to replace a safe's broken mechanism.
+ */
/obj/item/safe_internals
name = "safe internals"
- desc = "The mechanism and locking bolts for a Scarborough Arms - 2 tumbler safe"
+ desc = "The mechanism and locking bolts for a Scarborough Arms - 2 tumbler safe."
icon_state = "safe_internals"
+/**
+ * # Safe Codes
+ *
+ * Contains the (generated on map load) codes for all publicly known safes.
+ */
/obj/item/paper/safe_code
name = "safe codes"
var/owner
@@ -384,3 +405,7 @@ GLOBAL_LIST_EMPTY(safes)
info += "
The combination for the safe located in the [get_area(S).name] is: [S.get_combination()]
"
info_links = info
update_icon()
+
+#undef DRILL_SPARK_CHANCE
+#undef DRILL_TIME
+#undef SOUND_CHANCE
diff --git a/code/modules/client/asset_cache.dm b/code/modules/client/asset_cache.dm
index e39c62e5cd7..969afc3ba53 100644
--- a/code/modules/client/asset_cache.dm
+++ b/code/modules/client/asset_cache.dm
@@ -386,3 +386,9 @@ GLOBAL_LIST_EMPTY(asset_datums)
"Delta_nanomap_z1.png" = 'icons/_nanomaps/Delta_nanomap_z1.png',
"MetaStation_nanomap_z1.png" = 'icons/_nanomaps/MetaStation_nanomap_z1.png',
)
+
+/datum/asset/simple/safe
+ verify = FALSE
+ assets = list(
+ "safe_dial.png" = 'icons/safe_dial.png'
+ )
diff --git a/icons/safe_dial.png b/icons/safe_dial.png
new file mode 100644
index 00000000000..de24f16b670
Binary files /dev/null and b/icons/safe_dial.png differ
diff --git a/nano/templates/safe.tmpl b/nano/templates/safe.tmpl
deleted file mode 100644
index 15e8eddff86..00000000000
--- a/nano/templates/safe.tmpl
+++ /dev/null
@@ -1,63 +0,0 @@
-
