From 66936115327c3892bdf2dfbceef752869da00efe Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Wed, 23 Nov 2022 04:47:18 +0100 Subject: [PATCH] [MIRROR] Trophy case update [MDB IGNORE] (#17616) --- _maps/RandomZLevels/SnowCabin.dmm | 2 +- code/__DEFINES/access.dm | 2 +- code/__DEFINES/say.dm | 1 + code/controllers/subsystem/persistence.dm | 198 ++++++++---- code/game/objects/structures/displaycase.dm | 302 ++++++++++-------- code/modules/admin/admin_verbs.dm | 1 + code/modules/admin/trophy_manager.dm | 63 ++++ code/modules/photography/photos/album.dm | 2 +- code/modules/photography/photos/frame.dm | 4 +- .../modules/panicbunker/code/panicbunker.dm | 10 +- tgstation.dme | 1 + .../tgui/interfaces/TrophyAdminPanel.js | 78 +++++ tgui/packages/tgui/interfaces/Trophycase.js | 159 +++++++++ .../tgui/styles/interfaces/Trophycase.scss | 5 + tgui/packages/tgui/styles/main.scss | 1 + 15 files changed, 628 insertions(+), 201 deletions(-) create mode 100644 code/modules/admin/trophy_manager.dm create mode 100644 tgui/packages/tgui/interfaces/TrophyAdminPanel.js create mode 100644 tgui/packages/tgui/interfaces/Trophycase.js create mode 100644 tgui/packages/tgui/styles/interfaces/Trophycase.scss diff --git a/_maps/RandomZLevels/SnowCabin.dmm b/_maps/RandomZLevels/SnowCabin.dmm index c520bd9d675..66e63a1e580 100644 --- a/_maps/RandomZLevels/SnowCabin.dmm +++ b/_maps/RandomZLevels/SnowCabin.dmm @@ -3673,7 +3673,7 @@ /obj/effect/turf_decal/tile/blue/fourcorners, /obj/structure/displaycase{ start_showpiece_type = /obj/item/dice/d6/space; - trophy_message = "Stolen from dice collector before he could enjoy his day." + desc = "Contains an artifact stolen from dice collector before he could enjoy his day." }, /turf/open/floor/iron/dark, /area/awaymission/cabin/caves/mountain) diff --git a/code/__DEFINES/access.dm b/code/__DEFINES/access.dm index 1e41440f52d..a39d678f23a 100644 --- a/code/__DEFINES/access.dm +++ b/code/__DEFINES/access.dm @@ -128,7 +128,7 @@ #define ACCESS_CHAPEL_OFFICE "chapel_office" /// Access to the chapel's crematorium. #define ACCESS_CREMATORIUM "crematorium" -/// Access to the curator's private rooms in the Library, as well as access both into and out of the Library via Maintenance. +/// Access to the curator's private rooms in the Library and the trophy display cases, as well as access both into and out of the Library via Maintenance. #define ACCESS_LIBRARY "library" /// Access to the Bar, the Bar's Backroom, the bar sign, the bar robot portal, and the bar's vending machines. Some other bar-things too. #define ACCESS_BAR "bar" diff --git a/code/__DEFINES/say.dm b/code/__DEFINES/say.dm index 0883d6bd5dd..56289d16843 100644 --- a/code/__DEFINES/say.dm +++ b/code/__DEFINES/say.dm @@ -98,6 +98,7 @@ #define MAX_NAME_LEN 42 #define MAX_BROADCAST_LEN 512 #define MAX_CHARTER_LEN 80 +#define MAX_PLAQUE_LEN 144 // Audio/Visual Flags. Used to determine what sense are required to notice a message. #define MSG_VISUAL (1<<0) diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence.dm index d6d3ea7af87..2d6f145ac49 100644 --- a/code/controllers/subsystem/persistence.dm +++ b/code/controllers/subsystem/persistence.dm @@ -25,37 +25,40 @@ SUBSYSTEM_DEF(persistence) var/rounds_since_engine_exploded = 0 /datum/controller/subsystem/persistence/Initialize() - LoadPoly() + load_poly() load_wall_engravings() load_prisoner_tattoos() - LoadTrophies() - LoadRecentMaps() - LoadPhotoPersistence() - LoadRandomizedRecipes() - LoadPanicBunker() //SKYRAT EDIT ADDITION - PANICBUNKER + load_trophies() + load_recent_maps() + load_photo_persistence() + load_randomized_recipes() load_custom_outfits() load_delamination_counter() + load_panic_bunker() //SKYRAT EDIT ADDITION - PANICBUNKER load_adventures() return SS_INIT_SUCCESS +///Collects all data to persist. /datum/controller/subsystem/persistence/proc/collect_data() save_wall_engravings() save_prisoner_tattoos() - CollectTrophies() - CollectMaps() - SavePhotoPersistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION. - SaveRandomizedRecipes() - SavePanicBunker()//SKYRAT EDIT ADDITION - PANICBUNKER - SaveScars() + collect_trophies() + collect_maps() + save_photo_persistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION. + save_randomized_recipes() + save_scars() save_custom_outfits() save_delamination_counter() + save_panic_bunker() //SKYRAT EDIT ADDITION - PANICBUNKER -/datum/controller/subsystem/persistence/proc/LoadPoly() +///Loads up Poly's speech buffer. +/datum/controller/subsystem/persistence/proc/load_poly() for(var/mob/living/simple_animal/parrot/poly/P in GLOB.alive_mob_list) twitterize(P.speech_buffer, "polytalk") break //Who's been duping the bird?! +///Loads all engravings, and places a select amount in maintenance and the prison. /datum/controller/subsystem/persistence/proc/load_wall_engravings() var/json_file = file(ENGRAVING_SAVE_FILE) if(!fexists(json_file)) @@ -97,6 +100,7 @@ SUBSYSTEM_DEF(persistence) log_world("Loaded [successfully_loaded_engravings] engraved messages on map [SSmapping.config.map_name]") +///Saves all new engravings in the world. /datum/controller/subsystem/persistence/proc/save_wall_engravings() var/list/saved_data = list() @@ -136,6 +140,7 @@ SUBSYSTEM_DEF(persistence) return json +///Loads all tattoos, and select a few based on the amount of prisoner spawn positions. /datum/controller/subsystem/persistence/proc/load_prisoner_tattoos() var/json_file = file(PRISONER_TATTOO_SAVE_FILE) if(!fexists(json_file)) @@ -159,6 +164,7 @@ SUBSYSTEM_DEF(persistence) log_world("Loaded [prison_tattoos_to_use.len] prison tattoos") +///Saves all tattoos, so they can appear on prisoners in future rounds /datum/controller/subsystem/persistence/proc/save_prisoner_tattoos() var/json_file = file(PRISONER_TATTOO_SAVE_FILE) var/list/saved_data = list() @@ -190,27 +196,68 @@ SUBSYSTEM_DEF(persistence) return json - -/datum/controller/subsystem/persistence/proc/LoadTrophies() - if(fexists("data/npc_saves/TrophyItems.sav")) //legacy compatability to convert old format to new - var/savefile/S = new /savefile("data/npc_saves/TrophyItems.sav") - var/saved_json - S >> saved_json - if(!saved_json) - return - saved_trophies = json_decode(saved_json) - fdel("data/npc_saves/TrophyItems.sav") - else +/// Loads the trophies from the source file, and places a few in trophy display cases. +/datum/controller/subsystem/persistence/proc/load_trophies() + var/list/raw_saved_trophies = list() + if(fexists("data/npc_saves/TrophyItems.json")) var/json_file = file("data/npc_saves/TrophyItems.json") if(!fexists(json_file)) return var/list/json = json_decode(file2text(json_file)) if(!json) return - saved_trophies = json["data"] - SetUpTrophies(saved_trophies.Copy()) + raw_saved_trophies = json["data"] + fdel("data/npc_saves/TrophyItems.json") + else + var/json_file = file("data/trophy_items.json") + if(!fexists(json_file)) + return + var/list/json = json_decode(file2text(json_file)) + if(!json) + return + raw_saved_trophies = json["data"] -/datum/controller/subsystem/persistence/proc/LoadRecentMaps() + for(var/raw_json in raw_saved_trophies) + var/datum/trophy_data/parsed_trophy_data = new + parsed_trophy_data.load_from_json(raw_json) + saved_trophies += parsed_trophy_data + + set_up_trophies() + +///trophy data datum, for admin manipulation +/datum/trophy_data + ///path of the item the trophy will try to mimic, null if path_string is invalid + var/path + ///the message that appears under the item + var/message + ///the key of the one who placed the item in the trophy case + var/placer_key + +/datum/trophy_data/proc/load_from_json(list/json_data) + path = json_data["path"] + message = json_data["message"] + placer_key = json_data["placer_key"] + +/datum/trophy_data/proc/to_json() + var/list/new_data = list() + new_data["path"] = path + new_data["message"] = message + new_data["placer_key"] = placer_key + new_data["is_valid"] = text2path(path) ? TRUE : FALSE + return new_data + +/// Returns a list for the admin trophy panel. +/datum/controller/subsystem/persistence/proc/trophy_ui_data() + var/list/ui_data = list() + for(var/datum/trophy_data/data in saved_trophies) + var/list/pdata = data.to_json() + pdata["ref"] = REF(data) + ui_data += list(pdata) + + return ui_data + +/// Loads up the amount of times maps appeared to alter their appearance in voting and rotation. +/datum/controller/subsystem/persistence/proc/load_recent_maps() var/map_sav = FILE_RECENT_MAPS if(!fexists(FILE_RECENT_MAPS)) return @@ -231,44 +278,45 @@ SUBSYSTEM_DEF(persistence) if(run >= 2) //If run twice in the last KEEP_ROUNDS_MAP + 1 (including current) rounds, disable map for voting and rotation. blocked_maps += VM.map_name -/datum/controller/subsystem/persistence/proc/SetUpTrophies(list/trophy_items) - for(var/A in GLOB.trophy_cases) - var/obj/structure/displaycase/trophy/T = A - if (T.showpiece) - continue - T.added_roundstart = TRUE +/// Puts trophies into trophy cases. +/datum/controller/subsystem/persistence/proc/set_up_trophies() - var/trophy_data = pick_n_take(trophy_items) + var/list/valid_trophies = list() - if(!islist(trophy_data)) + for(var/datum/trophy_data/data in saved_trophies) + + if(!data) //sanity for incorrect deserialization continue - var/list/chosen_trophy = trophy_data - - if(!length(chosen_trophy)) //Malformed + var/path = text2path(data.path) + if(!path) //If the item no longer exist, ignore it continue - var/path = text2path(chosen_trophy["path"]) //If the item no longer exist, this returns null - if(!path) + valid_trophies += data + + for(var/obj/structure/displaycase/trophy/trophy_case in GLOB.trophy_cases) + if(!valid_trophies.len) + break + + if(trophy_case.showpiece) continue - T.showpiece = new /obj/item/showpiece_dummy(T, path) - T.trophy_message = chosen_trophy["message"] - T.placer_key = chosen_trophy["placer_key"] - T.update_appearance() + trophy_case.set_up_trophy(pick_n_take(valid_trophies)) -/datum/controller/subsystem/persistence/proc/GetPhotoAlbums() +///Loads up the photo album source file. +/datum/controller/subsystem/persistence/proc/get_photo_albums() var/album_path = file("data/photo_albums.json") if(fexists(album_path)) return json_decode(file2text(album_path)) -/datum/controller/subsystem/persistence/proc/GetPhotoFrames() +///Loads up the photo frames source file. +/datum/controller/subsystem/persistence/proc/get_photo_frames() var/frame_path = file("data/photo_frames.json") if(fexists(frame_path)) return json_decode(file2text(frame_path)) -/// Removes the identifier of a persitent photo frame from the json. -/datum/controller/subsystem/persistence/proc/RemovePhotoFrame(identifier) +/// Removes the identifier of a persistent photo frame from the json. +/datum/controller/subsystem/persistence/proc/remove_photo_frames(identifier) var/frame_path = file("data/photo_frames.json") if(!fexists(frame_path)) return @@ -280,7 +328,8 @@ SUBSYSTEM_DEF(persistence) fdel(frame_path) WRITE_FILE(frame_path, frame_json) -/datum/controller/subsystem/persistence/proc/LoadPhotoPersistence() +///Loads photo albums, and populates them; also loads and applies frames to picture frames. +/datum/controller/subsystem/persistence/proc/load_photo_persistence() var/album_path = file("data/photo_albums.json") var/frame_path = file("data/photo_frames.json") if(fexists(album_path)) @@ -303,7 +352,8 @@ SUBSYSTEM_DEF(persistence) if(json[PF.persistence_id]) PF.load_from_id(json[PF.persistence_id]) -/datum/controller/subsystem/persistence/proc/SavePhotoPersistence() +///Saves the contents of photo albums and the picture frames. +/datum/controller/subsystem/persistence/proc/save_photo_persistence() var/album_path = file("data/photo_albums.json") var/frame_path = file("data/photo_frames.json") @@ -339,13 +389,25 @@ SUBSYSTEM_DEF(persistence) WRITE_FILE(frame_path, frame_json) -/datum/controller/subsystem/persistence/proc/CollectTrophies() - var/json_file = file("data/npc_saves/TrophyItems.json") +///Collects trophies from all existing trophy cases. +/datum/controller/subsystem/persistence/proc/collect_trophies() + for(var/trophy_case in GLOB.trophy_cases) + save_trophy(trophy_case) + + var/json_file = file("data/trophy_items.json") var/list/file_data = list() - file_data["data"] = remove_duplicate_trophies(saved_trophies) + var/list/converted_data = list() + + for(var/datum/trophy_data/data in saved_trophies) + converted_data += list(data.to_json()) + + converted_data = remove_duplicate_trophies(converted_data) + + file_data["data"] = converted_data fdel(json_file) WRITE_FILE(json_file, json_encode(file_data)) +///gets the list of json trophies, and deletes the ones with an identical path and message /datum/controller/subsystem/persistence/proc/remove_duplicate_trophies(list/trophies) var/list/ukeys = list() . = list() @@ -357,15 +419,17 @@ SUBSYSTEM_DEF(persistence) . += list(trophy) ukeys[tkey] = TRUE -/datum/controller/subsystem/persistence/proc/SaveTrophy(obj/structure/displaycase/trophy/T) - if(!T.added_roundstart && T.showpiece) - var/list/data = list() - data["path"] = T.showpiece.type - data["message"] = T.trophy_message - data["placer_key"] = T.placer_key - saved_trophies += list(data) +///If there is a trophy in the trophy case, saved it, if the trophy was not a holo trophy and has a message attached. +/datum/controller/subsystem/persistence/proc/save_trophy(obj/structure/displaycase/trophy/trophy_case) + if(!trophy_case.holographic_showpiece && trophy_case.showpiece && trophy_case.trophy_message) + var/datum/trophy_data/data = new + data.path = trophy_case.showpiece.type + data.message = trophy_case.trophy_message + data.placer_key = trophy_case.placer_key + saved_trophies += data -/datum/controller/subsystem/persistence/proc/CollectMaps() +///Updates the list of the most recent maps. +/datum/controller/subsystem/persistence/proc/collect_maps() if(length(saved_maps) > KEEP_ROUNDS_MAP) //Get rid of extras from old configs. saved_maps.Cut(KEEP_ROUNDS_MAP+1) var/mapstosave = min(length(saved_maps)+1, KEEP_ROUNDS_MAP) @@ -380,7 +444,8 @@ SUBSYSTEM_DEF(persistence) fdel(json_file) WRITE_FILE(json_file, json_encode(file_data)) -/datum/controller/subsystem/persistence/proc/LoadRandomizedRecipes() +///Loads all randomized recipes. +/datum/controller/subsystem/persistence/proc/load_randomized_recipes() var/json_file = file("data/RandomizedChemRecipes.json") var/json if(fexists(json_file)) @@ -404,7 +469,8 @@ SUBSYSTEM_DEF(persistence) else log_game("Randomized recipe [randomized_type] resulted in conflicting recipes.") -/datum/controller/subsystem/persistence/proc/SaveRandomizedRecipes() +///Saves all randomized recipes. +/datum/controller/subsystem/persistence/proc/save_randomized_recipes() var/json_file = file("data/RandomizedChemRecipes.json") var/list/file_data = list() @@ -418,7 +484,8 @@ SUBSYSTEM_DEF(persistence) fdel(json_file) WRITE_FILE(json_file, json_encode(file_data)) -/datum/controller/subsystem/persistence/proc/SaveScars() +///Saves all scars for everyone's original characters +/datum/controller/subsystem/persistence/proc/save_scars() for(var/i in GLOB.joined_player_list) var/mob/living/carbon/human/ending_human = get_mob_by_ckey(i) if(!istype(ending_human) || !ending_human.mind?.original_character_slot_index || !ending_human.client?.prefs.read_preference(/datum/preference/toggle/persistent_scars)) @@ -434,7 +501,7 @@ SUBSYSTEM_DEF(persistence) else original_human.save_persistent_scars() - +///Loads the custom outfits of every admin. /datum/controller/subsystem/persistence/proc/load_custom_outfits() var/file = file("data/custom_outfits.json") if(!fexists(file)) @@ -456,6 +523,7 @@ SUBSYSTEM_DEF(persistence) continue GLOB.custom_outfits += outfit +///Saves each admin's custom outfit list /datum/controller/subsystem/persistence/proc/save_custom_outfits() var/file = file("data/custom_outfits.json") fdel(file) diff --git a/code/game/objects/structures/displaycase.dm b/code/game/objects/structures/displaycase.dm index 11e4733a200..aae51cbefe1 100644 --- a/code/game/objects/structures/displaycase.dm +++ b/code/game/objects/structures/displaycase.dm @@ -9,28 +9,28 @@ armor = list(MELEE = 30, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 10, BIO = 0, FIRE = 70, ACID = 100) max_integrity = 200 integrity_failure = 0.25 + ///The showpiece item inside the case var/obj/item/showpiece = null - var/obj/item/showpiece_type = null //This allows for showpieces that can only hold items if they're the same istype as this. + ///This allows for showpieces that can only hold items if they're the same istype as this. + var/obj/item/showpiece_type = null + ///Is the displaycase hooked up to a burglar alarm? var/alert = TRUE + ///Is the displaycase open at the moment? var/open = FALSE - var/openable = TRUE - var/custom_glass_overlay = FALSE ///If we have a custom glass overlay to use. + ///If we have a custom glass overlay to use. + var/custom_glass_overlay = FALSE var/obj/item/electronics/airlock/electronics - var/start_showpiece_type = null //add type for items on display - var/list/start_showpieces = list() //Takes sublists in the form of list("type" = /obj/item/bikehorn, "trophy_message" = "henk") - var/trophy_message = "" + ///Add type for items on display + var/start_showpiece_type = null + ///Displaycase is fixed by glass var/glass_fix = TRUE ///Represents a signel source of screaming when broken var/datum/alarm_handler/alarm_manager + ///Used for subtypes that have a UI in them. The examine on click while adjecent will not fire, as we already get a popup + var/autoexamine_while_closed = TRUE /obj/structure/displaycase/Initialize(mapload) . = ..() - if(start_showpieces.len && !start_showpiece_type) - var/list/showpiece_entry = pick(start_showpieces) - if (showpiece_entry && showpiece_entry["type"]) - start_showpiece_type = showpiece_entry["type"] - if (showpiece_entry["trophy_message"]) - trophy_message = showpiece_entry["trophy_message"] if(start_showpiece_type) showpiece = new start_showpiece_type (src) update_appearance() @@ -61,9 +61,8 @@ . += span_notice("Hooked up with an anti-theft system.") if(showpiece) . += span_notice("There's \a [showpiece] inside.") - if(trophy_message) - . += "The plaque reads:\n [trophy_message]" +///Removes the showpiece from the displaycase /obj/structure/displaycase/proc/dump() if(QDELETED(showpiece)) return @@ -124,27 +123,27 @@ . += "[initial(icon_state)]_closed" return -/obj/structure/displaycase/attackby(obj/item/W, mob/living/user, params) - if(W.GetID() && !broken && openable) +/obj/structure/displaycase/attackby(obj/item/tool, mob/living/user, params) + if(tool.GetID() && !broken) if(allowed(user)) - to_chat(user, span_notice("You [open ? "close":"open"] [src].")) + to_chat(user, span_notice("You [open ? "close":"open"] [src].")) toggle_lock(user) else to_chat(user, span_alert("Access denied.")) - else if(W.tool_behaviour == TOOL_WELDER && !user.combat_mode && !broken) + else if(tool.tool_behaviour == TOOL_WELDER && !user.combat_mode && !broken) if(atom_integrity < max_integrity) - if(!W.tool_start_check(user, amount=5)) + if(!tool.tool_start_check(user, amount=5)) return to_chat(user, span_notice("You begin repairing [src]...")) - if(W.use_tool(src, user, 40, amount=5, volume=50)) + if(tool.use_tool(src, user, 40, amount=5, volume=50)) atom_integrity = max_integrity update_appearance() to_chat(user, span_notice("You repair [src].")) else to_chat(user, span_warning("[src] is already in good condition!")) return - else if(!alert && W.tool_behaviour == TOOL_CROWBAR && openable) //Only applies to the lab cage and player made display cases + else if(!alert && tool.tool_behaviour == TOOL_CROWBAR) //Only applies to the lab cage and player made display cases if(broken) if(showpiece) to_chat(user, span_warning("Remove the displayed object first!")) @@ -153,35 +152,39 @@ qdel(src) else to_chat(user, span_notice("You start to [open ? "close":"open"] [src]...")) - if(W.use_tool(src, user, 20)) - to_chat(user, span_notice("You [open ? "close":"open"] [src].")) + if(tool.use_tool(src, user, 20)) + to_chat(user, span_notice("You [open ? "close":"open"] [src].")) toggle_lock(user) else if(open && !showpiece) - insert_showpiece(W, user) - else if(glass_fix && broken && istype(W, /obj/item/stack/sheet/glass)) - var/obj/item/stack/sheet/glass/G = W - if(G.get_amount() < 2) + insert_showpiece(tool, user) + return TRUE //cancel the attack chain, wether we successfully placed an item or not + else if(glass_fix && broken && istype(tool, /obj/item/stack/sheet/glass)) + var/obj/item/stack/sheet/glass/glass_sheet = tool + if(glass_sheet.get_amount() < 2) to_chat(user, span_warning("You need two glass sheets to fix the case!")) return to_chat(user, span_notice("You start fixing [src]...")) if(do_after(user, 20, target = src)) - G.use(2) + glass_sheet.use(2) broken = FALSE atom_integrity = max_integrity update_appearance() else return ..() -/obj/structure/displaycase/proc/insert_showpiece(obj/item/wack, mob/user) - if(showpiece_type && !istype(wack, showpiece_type)) +///Handles placing an item into the display case. Returns TRUE if the item failed to be placed inside the container, useful for descendants +/obj/structure/displaycase/proc/insert_showpiece(obj/item/new_showpiece, mob/user) + if(showpiece_type && !istype(new_showpiece, showpiece_type)) to_chat(user, span_notice("This doesn't belong in this kind of display.")) return TRUE - if(user.transferItemToLoc(wack, src)) - showpiece = wack - to_chat(user, span_notice("You put [wack] on display.")) + if(user.transferItemToLoc(new_showpiece, src)) + showpiece = new_showpiece + to_chat(user, span_notice("You put [new_showpiece] on display.")) update_appearance() +///Opens and closes the display case /obj/structure/displaycase/proc/toggle_lock(mob/user) + playsound(src, 'sound/machines/click.ogg', 20, TRUE) open = !open update_appearance() @@ -200,10 +203,12 @@ add_fingerprint(user) return else - //prevents remote "kicks" with TK + //prevents remote "kicks" with TK if (!Adjacent(user)) return if (!user.combat_mode) + if(!open && !autoexamine_while_closed) + return if(!user.is_blind()) user.examinate(src) return @@ -290,11 +295,17 @@ /obj/structure/displaycase/trophy name = "trophy display case" desc = "Store your trophies of accomplishment in here, and they will stay forever." - var/placer_key = "" - var/added_roundstart = TRUE - var/is_locked = TRUE integrity_failure = 0 - openable = FALSE + req_access = list(ACCESS_LIBRARY) + autoexamine_while_closed = FALSE + ///the key of the player who placed the item in the case + var/placer_key = "" + ///is the trophy a hologram, not a real item placed by a player? + var/holographic_showpiece = FALSE + ///are we about to edit + var/historian_mode = FALSE + ///the trophy message + var/trophy_message = "" /obj/structure/displaycase/trophy/Initialize(mapload) . = ..() @@ -304,93 +315,121 @@ GLOB.trophy_cases -= src return ..() +///Creates a showpiece dummy to display, using persistent data +/obj/structure/displaycase/trophy/proc/set_up_trophy(datum/trophy_data/chosen_trophy) + showpiece = new /obj/item/showpiece_dummy(src, text2path(chosen_trophy.path)) + trophy_message = trim(chosen_trophy.message, MAX_PLAQUE_LEN) + if(trophy_message == "") + trophy_message = trim(showpiece.desc, MAX_PLAQUE_LEN) + placer_key = trim(chosen_trophy.placer_key) + holographic_showpiece = TRUE + update_appearance() + /obj/structure/displaycase/trophy/attackby(obj/item/W, mob/living/user, params) - - if(!user.Adjacent(src)) //no TK museology + if(istype(W, /obj/item/key/displaycase)) + toggle_historian_mode(user) return - if(user.combat_mode) - return ..() - if(W.tool_behaviour == TOOL_WELDER && !broken) - return ..() - - if(user.is_holding_item_of_type(/obj/item/key/displaycase)) - if(added_roundstart) - is_locked = !is_locked - to_chat(user, span_notice("You [!is_locked ? "un" : ""]lock the case.")) - else - to_chat(user, span_warning("The lock is stuck shut!")) - return - - if(is_locked) - to_chat(user, span_warning("The case is shut tight with an old-fashioned physical lock. Maybe you should ask the curator for the key?")) - return - - if(!added_roundstart) - to_chat(user, span_warning("You've already put something new in this case!")) - return - - if(is_type_in_typecache(W, GLOB.blacklisted_cargo_types)) - to_chat(user, span_warning("The case rejects the [W]!")) - return - - for(var/a in W.get_all_contents()) - if(is_type_in_typecache(a, GLOB.blacklisted_cargo_types)) - to_chat(user, span_warning("The case rejects the [W]!")) - return - - if(user.transferItemToLoc(W, src)) - - if(showpiece) - to_chat(user, span_notice("You press a button, and [showpiece] descends into the floor of the case.")) - QDEL_NULL(showpiece) - - to_chat(user, span_notice("You insert [W] into the case.")) - showpiece = W - added_roundstart = FALSE - update_appearance() - - placer_key = user.ckey - - trophy_message = W.desc //default value - - var/chosen_plaque = tgui_input_text(user, "What would you like the plaque to say? Default value is item's description.", "Trophy Plaque", trophy_message) - if(chosen_plaque) - if(user.Adjacent(src)) - trophy_message = chosen_plaque - to_chat(user, span_notice("You set the plaque's text.")) - else - to_chat(user, span_warning("You are too far to set the plaque's text!")) - - SSpersistence.SaveTrophy(src) - return TRUE - - else - to_chat(user, span_warning("\The [W] is stuck to your hand, you can't put it in the [src.name]!")) - - return + return ..() /obj/structure/displaycase/trophy/dump() if (showpiece) - if(added_roundstart) - visible_message(span_danger("The [showpiece] crumbles to dust!")) - new /obj/effect/decal/cleanable/ash(loc) + if(holographic_showpiece) + visible_message(span_danger("[showpiece] fizzles and vanishes!")) + do_sparks(number = 1, cardinal_only = FALSE, source = src) QDEL_NULL(showpiece) + holographic_showpiece = FALSE else - return ..() + ..() + placer_key = "" + trophy_message = null + +/obj/structure/displaycase/trophy/insert_showpiece(obj/item/new_showpiece, mob/user) + if(..()) + return TRUE + if(showpiece == new_showpiece) + placer_key = user.ckey + +///Toggles the mode that shows the historian panel on the UI, enabling saving the looks and the trophy message of the current trophy +/obj/structure/displaycase/trophy/proc/toggle_historian_mode(mob/user) + historian_mode = !historian_mode + balloon_alert(user, "[historian_mode ? "enabled" : "disabled"] historian mode.") + playsound(src, 'sound/machines/twobeep.ogg', vary = 50) + SStgui.update_uis(src) + +/obj/structure/displaycase/trophy/toggle_lock(mob/user) + ..() + SStgui.close_uis(src) + +/obj/structure/displaycase/trophy/ui_data(mob/user) + var/list/data = list() + data["historian_mode"] = historian_mode + data["holographic_showpiece"] = holographic_showpiece + data["max_length"] = MAX_PLAQUE_LEN + data["has_showpiece"] = showpiece ? TRUE : FALSE + if(showpiece) + data["showpiece_name"] = capitalize(format_text(showpiece.name)) + data["showpiece_description"] = trophy_message ? format_text(trophy_message) : null + return data + +/obj/structure/displaycase/trophy/ui_static_data(mob/user) + var/list/data = list() + if(showpiece) + data["showpiece_icon"] = icon2base64(getFlatIcon(showpiece, no_anim=TRUE)) + return data + +/obj/structure/displaycase/trophy/ui_act(action, params) + . = ..() + if(.) + return + switch(action) + if("insert_key") + if(historian_mode) + return + var/obj/item/key/displaycase/trophy_key = usr.get_active_held_item() + if(istype(trophy_key)) + toggle_historian_mode(usr) + return TRUE + return + if("change_message") + if(showpiece && !holographic_showpiece) + var/new_trophy_message = tgui_input_text(usr, "Let's make history!", "Trophy Message", trophy_message, MAX_PLAQUE_LEN) + if(!new_trophy_message) + return + trophy_message = new_trophy_message + return TRUE + if("lock") + if(!historian_mode) + return + toggle_historian_mode(usr) + return TRUE + +/obj/structure/displaycase/trophy/ui_interact(mob/user, datum/tgui/ui) + if(open) + return + if(isliving(usr)) + var/mob/living/living_usr = usr + if(living_usr.combat_mode) + return + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "Trophycase", name) + ui.set_autoupdate(FALSE) + ui.open() /obj/item/key/displaycase name = "display case key" desc = "The key to the curator's display cases." /obj/item/showpiece_dummy - name = "Cheap replica" + name = "holographic replica" /obj/item/showpiece_dummy/Initialize(mapload, path) . = ..() - var/obj/item/I = path - name = initial(I.name) - icon = initial(I.icon) - icon_state = initial(I.icon_state) + var/obj/item/item_path = path + name = initial(item_path.name) + desc = initial(item_path.desc) + icon = initial(item_path.icon) + icon_state = initial(item_path.icon_state) /obj/structure/displaycase/forsale name = "vend-a-tray" @@ -403,6 +442,7 @@ alert = FALSE //No, we're not calling the fire department because someone stole your cookie. glass_fix = FALSE //Fixable with tools instead. pass_flags = PASSTABLE ///Can be placed and moved onto a table. + autoexamine_while_closed = FALSE ///The price of the item being sold. Altered by grab intent ID use. var/sale_price = 20 ///The Account which will receive payment for purchases. Set by the first ID to swipe the tray. @@ -417,6 +457,19 @@ if(!broken && !open) . += "[initial(icon_state)]_overlay" +/obj/structure/displaycase/forsale/insert_showpiece(obj/item/new_showpiece, mob/user) + if(..()) + return TRUE + update_static_data_for_all_viewers() + +/obj/structure/displaycase/forsale/dump() + ..() + update_static_data_for_all_viewers() + +/obj/structure/displaycase/forsale/toggle_lock() + ..() + SStgui.update_uis(src) + /obj/structure/displaycase/forsale/ui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) @@ -426,19 +479,18 @@ /obj/structure/displaycase/forsale/ui_data(mob/user) var/list/data = list() - var/register = FALSE - if(payments_acc) - register = TRUE - data["owner_name"] = payments_acc.account_holder - if(showpiece) - data["product_name"] = capitalize(showpiece.name) - var/base64 = icon2base64(icon(showpiece.icon, showpiece.icon_state)) - data["product_icon"] = base64 - data["registered"] = register + data["owner_name"] = payments_acc ? payments_acc.account_holder : null + data["product_name"] = showpiece ?capitalize(format_text(showpiece.name)) : null + data["registered"] = payments_acc ? TRUE : FALSE data["product_cost"] = sale_price data["tray_open"] = open return data +/obj/structure/displaycase/forsale/ui_static_data(mob/user) + var/list/data = list() + data["product_icon"] = showpiece ? icon2base64(getFlatIcon(showpiece, no_anim=TRUE)) : null + return data + /obj/structure/displaycase/forsale/ui_act(action, params) . = ..() if(.) @@ -471,16 +523,16 @@ to_chat(usr, span_notice("You do not possess the funds to purchase this.")) return TRUE else - account.adjust_money(-sale_price, "Display Case: [capitalize(showpiece)]") + account.adjust_money(-sale_price, "Display Case: [capitalize(showpiece.name)]") if(payments_acc) - payments_acc.adjust_money(sale_price, "Display Case: [capitalize(showpiece)]") + payments_acc.adjust_money(sale_price, "Display Case: [capitalize(showpiece.name)]") usr.put_in_hands(showpiece) to_chat(usr, span_notice("You purchase [showpiece] for [sale_price] credits.")) playsound(src, 'sound/effects/cashregister.ogg', 40, TRUE) flick("[initial(icon_state)]_vend", src) showpiece = null update_appearance() - SStgui.update_uis(src) + update_static_data_for_all_viewers() return TRUE if("Open") if(!payments_acc) @@ -492,7 +544,6 @@ playsound(src, 'sound/machines/buzz-sigh.ogg', 50, TRUE) return toggle_lock() - SStgui.update_uis(src) if("Register") if(payments_acc) return @@ -522,6 +573,7 @@ SStgui.update_uis(src) return TRUE . = TRUE + /obj/structure/displaycase/forsale/attackby(obj/item/I, mob/living/user, params) if(isidcard(I)) //Card Registration @@ -530,18 +582,16 @@ to_chat(user, span_warning("This ID card has no account registered!")) return if(payments_acc == potential_acc.registered_account) - playsound(src, 'sound/machines/click.ogg', 20, TRUE) toggle_lock() return if(istype(I, /obj/item/modular_computer)) return TRUE SStgui.update_uis(src) - . = ..() - + return ..() /obj/structure/displaycase/forsale/multitool_act(mob/living/user, obj/item/I) . = ..() - if(atom_integrity <= (integrity_failure * max_integrity)) + if(atom_integrity <= (integrity_failure * max_integrity)) to_chat(user, span_notice("You start recalibrating [src]'s hover field...")) if(do_after(user, 20, target = src)) broken = FALSE diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index a5c66814544..33cceb33497 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -96,6 +96,7 @@ GLOBAL_PROTECT(admin_verbs_admin) /client/proc/list_dna, /client/proc/list_fingerprints, /client/proc/message_pda, /*send a message to somebody on PDA*/ + /datum/admins/proc/trophy_manager, /client/proc/fax_panel, /*send a paper to fax*/ ) GLOBAL_LIST_INIT(admin_verbs_ban, list(/client/proc/unban_panel, /client/proc/ban_panel, /client/proc/stickybanpanel)) diff --git a/code/modules/admin/trophy_manager.dm b/code/modules/admin/trophy_manager.dm new file mode 100644 index 00000000000..6a45afb8e45 --- /dev/null +++ b/code/modules/admin/trophy_manager.dm @@ -0,0 +1,63 @@ +/datum/admins/proc/trophy_manager() + set name = "Trophy Manager" + set category = "Admin" + + if(!check_rights(R_ADMIN)) + return + var/datum/trophy_manager/ui = new(usr) + ui.ui_interact(usr) + +/// Trophy Admin Management Panel +/datum/trophy_manager + +/datum/trophy_manager/ui_state(mob/user) + return GLOB.admin_state + +/datum/trophy_manager/ui_close(mob/user) + qdel(src) + +/datum/trophy_manager/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TrophyAdminPanel") + ui.open() + +/datum/trophy_manager/ui_data(mob/user) + var/list/data = list() + data["trophies"] = SSpersistence.trophy_ui_data() + return data + +/datum/trophy_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + if (!check_rights(R_ADMIN)) + return + var/mob/user = usr + var/datum/trophy_data/trophy = locate(params["ref"]) in SSpersistence.saved_trophies + if(!trophy) + return + switch(action) + if("delete") + SSpersistence.saved_trophies -= trophy + log_admin("[key_name(user)] has deleted a trophy made by [trophy.placer_key].") + message_admins(span_notice("[key_name_admin(user)] has deleted trophy made by [trophy.placer_key].")) + return TRUE + if("edit_message") + var/old_message = trophy.message + var/new_message = tgui_input_text(user, "New trophy message?", "Message Editing", trophy.message, max_length = MAX_PLAQUE_LEN) + if(!new_message) + return + trophy.message = new_message + log_admin("[key_name(user)] has edited the message of trophy made by [trophy.placer_key] from \"[old_message]\" to \"[new_message]\".") + return TRUE + if("edit_path") + var/old_path = trophy.path + var/new_path = tgui_input_text(user, "New trophy path?", "Path Editing", trophy.path) + if(!new_path) + return + if(!text2path(new_path)) + to_chat(user, span_warning("Invalid path selected")) + return + trophy.path = new_path + log_admin("[key_name(user)] has edited the item path of trophy made by [trophy.placer_key] from \"[old_path]\" to \"[new_path]\".") + return TRUE diff --git a/code/modules/photography/photos/album.dm b/code/modules/photography/photos/album.dm index 64b6c07748e..4f11647eeb9 100644 --- a/code/modules/photography/photos/album.dm +++ b/code/modules/photography/photos/album.dm @@ -41,7 +41,7 @@ //Manual loading, DO NOT USE FOR HARDCODED/MAPPED IN ALBUMS. This is for if an album needs to be loaded mid-round from an ID. /obj/item/storage/photo_album/proc/persistence_load() - var/list/data = SSpersistence.GetPhotoAlbums() + var/list/data = SSpersistence.get_photo_albums() if(data[persistence_id]) populate_from_id_list(data[persistence_id]) diff --git a/code/modules/photography/photos/frame.dm b/code/modules/photography/photos/frame.dm index 543f8b74347..1f97fd1f07e 100644 --- a/code/modules/photography/photos/frame.dm +++ b/code/modules/photography/photos/frame.dm @@ -88,7 +88,7 @@ /obj/structure/sign/picture_frame/Destroy() LAZYREMOVE(SSpersistence.photo_frames, src) if(persistence_id && del_id_on_destroy) - SSpersistence.RemovePhotoFrame(persistence_id) + SSpersistence.remove_photo_frames(persistence_id) return ..() /obj/structure/sign/picture_frame/proc/get_photo_id() @@ -97,7 +97,7 @@ //Manual loading, DO NOT USE FOR HARDCODED/MAPPED IN ALBUMS. This is for if an album needs to be loaded mid-round from an ID. /obj/structure/sign/picture_frame/proc/persistence_load() - var/list/data = SSpersistence.GetPhotoFrames() + var/list/data = SSpersistence.get_photo_frames() if(data[persistence_id]) load_from_id(data[persistence_id]) diff --git a/modular_skyrat/modules/panicbunker/code/panicbunker.dm b/modular_skyrat/modules/panicbunker/code/panicbunker.dm index 96ee7c551b2..bfd6778bcbb 100644 --- a/modular_skyrat/modules/panicbunker/code/panicbunker.dm +++ b/modular_skyrat/modules/panicbunker/code/panicbunker.dm @@ -10,7 +10,7 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) GLOB.bunker_passthrough |= ckey(ckeytobypass) GLOB.bunker_passthrough[ckey(ckeytobypass)] = world.realtime - SSpersistence.SavePanicBunker() //we can do this every time, it's okay + SSpersistence.save_panic_bunker() //we can do this every time, it's okay log_admin("[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.") message_admins("[key_name_admin(usr)] has added [ckeytobypass] to the current round's bunker bypass list.") @@ -24,7 +24,7 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) return GLOB.bunker_passthrough -= ckey(ckeytobypass) - SSpersistence.SavePanicBunker() + SSpersistence.save_panic_bunker() log_admin("[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.") message_admins("[key_name_admin(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.") @@ -40,12 +40,12 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) GLOB.bunker_passthrough |= ckey(params) GLOB.bunker_passthrough[ckey(params)] = world.realtime - SSpersistence.SavePanicBunker() //we can do this every time, it's okay + SSpersistence.save_panic_bunker() //we can do this every time, it's okay log_admin("[sender.friendly_name] has added [params] to the current round's bunker bypass list.") message_admins("[sender.friendly_name] has added [params] to the current round's bunker bypass list.") return "[params] has been added to the current round's bunker bypass list." -/datum/controller/subsystem/persistence/proc/LoadPanicBunker() +/datum/controller/subsystem/persistence/proc/load_panic_bunker() var/bunker_path = file("data/bunker_passthrough.json") if(fexists(bunker_path)) var/list/json = json_decode(file2text(bunker_path)) @@ -54,7 +54,7 @@ GLOBAL_LIST_EMPTY(bunker_passthrough) if(daysSince(GLOB.bunker_passthrough[ckey]) >= CONFIG_GET(number/max_bunker_days)) GLOB.bunker_passthrough -= ckey -/datum/controller/subsystem/persistence/proc/SavePanicBunker() +/datum/controller/subsystem/persistence/proc/save_panic_bunker() var/json_file = file("data/bunker_passthrough.json") var/list/file_data = list() file_data["data"] = GLOB.bunker_passthrough diff --git a/tgstation.dme b/tgstation.dme index 0892495ff5c..865e0d2291e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2163,6 +2163,7 @@ #include "code\modules\admin\tag.dm" #include "code\modules\admin\team_panel.dm" #include "code\modules\admin\topic.dm" +#include "code\modules\admin\trophy_manager.dm" #include "code\modules\admin\whitelist.dm" #include "code\modules\admin\callproc\callproc.dm" #include "code\modules\admin\smites\bad_luck.dm" diff --git a/tgui/packages/tgui/interfaces/TrophyAdminPanel.js b/tgui/packages/tgui/interfaces/TrophyAdminPanel.js new file mode 100644 index 00000000000..8b6c235f0fc --- /dev/null +++ b/tgui/packages/tgui/interfaces/TrophyAdminPanel.js @@ -0,0 +1,78 @@ +import { decodeHtmlEntities } from 'common/string'; +import { useBackend } from '../backend'; +import { Button, Table } from '../components'; +import { Window } from '../layouts'; + +export const TrophyAdminPanel = (props, context) => { + const { act, data } = useBackend(context); + const { trophies } = data; + return ( + + + + + Path + + Message + + Placer Key + + + {!!trophies && + trophies.map((trophy) => ( + + + {decodeHtmlEntities(trophy.path)} + + + act('edit_path', { ref: trophy.ref })} + /> + + + {decodeHtmlEntities(trophy.message)} + + + act('edit_message', { ref: trophy.ref })} + /> + + + {decodeHtmlEntities(trophy.placer_key)} + + + act('delete', { ref: trophy.ref })} + /> + + + ))} + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/Trophycase.js b/tgui/packages/tgui/interfaces/Trophycase.js new file mode 100644 index 00000000000..b6aa4b33ea6 --- /dev/null +++ b/tgui/packages/tgui/interfaces/Trophycase.js @@ -0,0 +1,159 @@ +import { decodeHtmlEntities } from 'common/string'; +import { useBackend } from '../backend'; +import { Icon, Box, Button, Dimmer, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +export const Trophycase = (props, context) => { + const { act, data } = useBackend(context); + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +const HistorianPanel = (props, context) => { + const { act, data } = useBackend(context); + const { + has_showpiece, + historian_mode, + holographic_showpiece, + showpiece_description, + } = data; + + return ( + + {!historian_mode && ( + act('insert_key')} + /> + )} + {!!historian_mode && ( + + act('lock')} + /> + act('change_message')} + /> + + )} + {!!historian_mode && !!holographic_showpiece && ( + + A holographic trophy is already present. Replace it with a new trophy + to create a new recording. + + )} + {!!historian_mode && !has_showpiece && No trophies located.} + {!!historian_mode && + !!has_showpiece && + !holographic_showpiece && + !!showpiece_description && ( + + Recording has begun. Trophy data will be saved overnight, as long as + the trophy stays within an intact case. + + )} + {!!historian_mode && + !!has_showpiece && + !holographic_showpiece && + !showpiece_description && ( + + New trophy detected. Please record a description to begin archival. + + )} + + ); +}; + +const ShowpieceDescription = (props, context) => { + const { act, data } = useBackend(context); + const { + has_showpiece, + holographic_showpiece, + historian_mode, + max_length, + showpiece_description, + } = data; + return ( + + {!has_showpiece && ( + + This exhibit is empty. History awaits your contribution! + + )} + {!!holographic_showpiece && {showpiece_description}} + {!holographic_showpiece && !!has_showpiece && ( + + {showpiece_description + ? decodeHtmlEntities(showpiece_description) + : "This exhibit is under construction. Get the curator's key to finalize your contribution!"} + + )} + + ); +}; + +const ShowpieceImage = (props, context) => { + const { data } = useBackend(context); + const { showpiece_icon } = data; + return showpiece_icon ? ( + + + + ) : ( + + + + + + + + ); +}; + +const ShowpieceName = (props, context) => { + const { data } = useBackend(context); + const { showpiece_name } = data; + return ( + + + {showpiece_name + ? decodeHtmlEntities(showpiece_name) + : 'Under construction.'} + + + ); +}; diff --git a/tgui/packages/tgui/styles/interfaces/Trophycase.scss b/tgui/packages/tgui/styles/interfaces/Trophycase.scss new file mode 100644 index 00000000000..8b2972db4e5 --- /dev/null +++ b/tgui/packages/tgui/styles/interfaces/Trophycase.scss @@ -0,0 +1,5 @@ +.Trophycase-description { + word-break: break-word; + word-wrap: break-word; + font-style: italic; +} diff --git a/tgui/packages/tgui/styles/main.scss b/tgui/packages/tgui/styles/main.scss index 69ffdad1c6d..246c57c6770 100644 --- a/tgui/packages/tgui/styles/main.scss +++ b/tgui/packages/tgui/styles/main.scss @@ -67,6 +67,7 @@ @include meta.load-css('./interfaces/Safe.scss'); @include meta.load-css('./interfaces/TachyonArray.scss'); @include meta.load-css('./interfaces/Techweb.scss'); +@include meta.load-css('./interfaces/Trophycase.scss'); @include meta.load-css('./interfaces/RequestManager.scss'); @include meta.load-css('./interfaces/UtilityModulesPane.scss'); @include meta.load-css('./interfaces/Fabricator.scss');