From 5bb6078def29eb43a6989f74bf328906a2857c3c Mon Sep 17 00:00:00 2001 From: SandPoot Date: Thu, 22 Apr 2021 20:39:21 -0300 Subject: [PATCH 1/4] upload files --- code/__DEFINES/vv.dm | 3 + code/__HELPERS/icons.dm | 5 +- code/datums/dna.dm | 5 +- code/datums/outfit.dm | 351 +++++++++++++++--- code/modules/admin/admin_verbs.dm | 4 +- code/modules/admin/outfit_editor.dm | 196 ++++++++++ code/modules/admin/outfit_manager.dm | 73 ++++ code/modules/admin/outfits.dm | 32 ++ code/modules/admin/verbs/debug.dm | 66 ++-- code/modules/admin/verbs/randomverbs.dm | 2 - code/modules/admin/verbs/selectequipment.dm | 226 +++++++++++ code/modules/client/preferences.dm | 2 + code/modules/client/preferences_savefile.dm | 11 + code/modules/clothing/chameleon.dm | 3 +- code/modules/mob/living/carbon/human/dummy.dm | 48 ++- code/modules/mob/living/carbon/human/human.dm | 10 +- .../mob/living/carbon/human/human_helpers.dm | 6 + tgstation.dme | 4 + tgui/packages/common/collections.js | 4 + tgui/packages/tgui/interfaces/ListInput.js | 206 ++++++++++ tgui/packages/tgui/interfaces/OutfitEditor.js | 146 ++++++++ .../packages/tgui/interfaces/OutfitManager.js | 81 ++++ .../tgui/interfaces/SelectEquipment.js | 214 +++++++++++ 23 files changed, 1578 insertions(+), 120 deletions(-) create mode 100644 code/modules/admin/outfit_editor.dm create mode 100644 code/modules/admin/outfit_manager.dm create mode 100644 code/modules/admin/outfits.dm create mode 100644 code/modules/admin/verbs/selectequipment.dm create mode 100644 tgui/packages/tgui/interfaces/ListInput.js create mode 100644 tgui/packages/tgui/interfaces/OutfitEditor.js create mode 100644 tgui/packages/tgui/interfaces/OutfitManager.js create mode 100644 tgui/packages/tgui/interfaces/SelectEquipment.js diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index 99a2e9d0ab..a04f02bd6a 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -136,3 +136,6 @@ // paintings #define VV_HK_REMOVE_PAINTING "remove_painting" + +//outfits +#define VV_HK_TO_OUTFIT_EDITOR "outfit_editor" diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index 1d37f639bf..ce6bbf48c3 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1065,10 +1065,9 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") + COMPILE_OVERLAYS(body) for(var/D in showDirs) - body.setDir(D) - COMPILE_OVERLAYS(body) - var/icon/partial = getFlatIcon(body) + var/icon/partial = getFlatIcon(body, defdir=D) out_icon.Insert(partial,dir=D) humanoid_icon_cache[icon_id] = out_icon diff --git a/code/datums/dna.dm b/code/datums/dna.dm index b6b93bdddc..248b669ab1 100644 --- a/code/datums/dna.dm +++ b/code/datums/dna.dm @@ -329,12 +329,13 @@ uni_identity = generate_uni_identity() unique_enzymes = generate_unique_enzymes() -/datum/dna/proc/initialize_dna(newblood_type) +/datum/dna/proc/initialize_dna(newblood_type, skip_index = FALSE) if(newblood_type) blood_type = newblood_type unique_enzymes = generate_unique_enzymes() uni_identity = generate_uni_identity() - generate_dna_blocks() + if(!skip_index) //I hate this + generate_dna_blocks() features = random_features(species?.id, holder?.gender) diff --git a/code/datums/outfit.dm b/code/datums/outfit.dm index da379b9851..0b46629365 100755 --- a/code/datums/outfit.dm +++ b/code/datums/outfit.dm @@ -1,71 +1,185 @@ +/** + * # Outfit datums + * + * This is a clean system of applying outfits to mobs, if you need to equip someone in a uniform + * this is the way to do it cleanly and properly. + * + * You can also specify an outfit datum on a job to have it auto equipped to the mob on join + * + * /mob/living/carbon/human/proc/equipOutfit(outfit) is the mob level proc to equip an outfit + * and you pass it the relevant datum outfit + * + * outfits can also be saved as json blobs downloadable by a client and then can be uploaded + * by that user to recreate the outfit, this is used by admins to allow for custom event outfits + * that can be restored at a later date + */ /datum/outfit + ///Name of the outfit (shows up in the equip admin verb) var/name = "Naked" - var/uniform = null - var/suit = null - var/toggle_helmet = TRUE - var/back = null - var/belt = null - var/gloves = null - var/shoes = null - var/head = null - var/mask = null - var/neck = null - var/ears = null - var/glasses = null + /// Type path of item to go in the idcard slot var/id = null - var/l_pocket = null - var/r_pocket = null + + /// Type path of item to go in uniform slot + var/uniform = null + + /// Type path of item to go in suit slot + var/suit = null + + /** + * Type path of item to go in suit storage slot + * + * (make sure it's valid for that suit) + */ var/suit_store = null - var/r_hand = null + + /// Type path of item to go in back slot + var/back = null + + /** + * list of items that should go in the backpack of the user + * + * Format of this list should be: list(path=count,otherpath=count) + */ + var/list/backpack_contents = null + + /// Type path of item to go in belt slot + var/belt = null + + /// Type path of item to go in ears slot + var/ears = null + + /// Type path of item to go in the glasses slot + var/glasses = null + + /// Type path of item to go in gloves slot + var/gloves = null + + /// Type path of item to go in head slot + var/head = null + + /// Type path of item to go in mask slot + var/mask = null + + /// Type path of item to go in neck slot + var/neck = null + + /// Type path of item to go in shoes slot + var/shoes = null + + /// Type path of item for left pocket slot + var/l_pocket = null + + /// Type path of item for right pocket slot + var/r_pocket = null + + ///Type path of item to go in the right hand var/l_hand = null - var/internals_slot = null //ID of slot containing a gas tank - var/list/backpack_contents = null // In the list(path=count,otherpath=count) format - var/box // Internals box. Will be inserted at the start of backpack_contents - var/list/implants = null + + //Type path of item to go in left hand + var/r_hand = null + + /// Any clothing accessory item var/accessory = null - var/can_be_admin_equipped = TRUE // Set to FALSE if your outfit requires runtime parameters - var/list/chameleon_extras //extra types for chameleon outfit changes, mostly guns + /// Internals box. Will be inserted at the start of backpack_contents + var/box + /** + * extra types for chameleon outfit changes, mostly guns + * + * Format of this list is (typepath, typepath, typepath) + * + * These are all added and returns in the list for get_chamelon_diguise_info proc + */ + var/list/chameleon_extras + + /** + * Any implants the mob should start implanted with + * + * Format of this list is (typepath, typepath, typepath) + */ + var/list/implants = null + + ///ID of the slot containing a gas tank + var/internals_slot = null + + /// Should the toggle helmet proc be called on the helmet during equip + var/toggle_helmet = TRUE + + /// Any undershirt. While on humans it is a string, here we use paths to stay consistent with the rest of the equips. + var/datum/sprite_accessory/undershirt = null + +/** + * Called at the start of the equip proc + * + * Override to change the value of the slots depending on client prefs, species and + * other such sources of change + * + * Extra Arguments + * * visualsOnly true if this is only for display (in the character setup screen) + * + * If visualsOnly is true, you can omit any work that doesn't visually appear on the character sprite + */ /datum/outfit/proc/pre_equip(mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source) //to be overridden for customization depending on client prefs,species etc return +/** + * Called after the equip proc has finished + * + * All items are on the mob at this point, use this proc to toggle internals + * fiddle with id bindings and accesses etc + * + * Extra Arguments + * * visualsOnly true if this is only for display (in the character setup screen) + * + * If visualsOnly is true, you can omit any work that doesn't visually appear on the character sprite + */ /datum/outfit/proc/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source) //to be overridden for toggling internals, id binding, access etc return +/** + * Equips all defined types and paths to the mob passed in + * + * Extra Arguments + * * visualsOnly true if this is only for display (in the character setup screen) + * + * If visualsOnly is true, you can omit any work that doesn't visually appear on the character sprite + */ /datum/outfit/proc/equip(mob/living/carbon/human/H, visualsOnly = FALSE, client/preference_source) pre_equip(H, visualsOnly, preference_source) //Start with uniform,suit,backpack for additional slots if(uniform) - H.equip_to_slot_or_del(new uniform(H),SLOT_W_UNIFORM) + H.equip_to_slot_or_del(new uniform(H), SLOT_W_UNIFORM, TRUE) if(suit) - H.equip_to_slot_or_del(new suit(H),SLOT_WEAR_SUIT) + H.equip_to_slot_or_del(new suit(H), SLOT_WEAR_SUIT, TRUE) if(back) - H.equip_to_slot_or_del(new back(H),SLOT_BACK) + H.equip_to_slot_or_del(new back(H), SLOT_BACK, TRUE) if(belt) - H.equip_to_slot_or_del(new belt(H),SLOT_BELT) + H.equip_to_slot_or_del(new belt(H), SLOT_BELT, TRUE) if(gloves) - H.equip_to_slot_or_del(new gloves(H),SLOT_GLOVES) + H.equip_to_slot_or_del(new gloves(H), SLOT_GLOVES, TRUE) if(shoes) - H.equip_to_slot_or_del(new shoes(H),SLOT_SHOES) + H.equip_to_slot_or_del(new shoes(H), SLOT_SHOES, TRUE) if(head) - H.equip_to_slot_or_del(new head(H),SLOT_HEAD) + H.equip_to_slot_or_del(new head(H), SLOT_HEAD, TRUE) if(mask) - H.equip_to_slot_or_del(new mask(H),SLOT_WEAR_MASK) + H.equip_to_slot_or_del(new mask(H), SLOT_WEAR_MASK, TRUE) if(neck) - H.equip_to_slot_or_del(new neck(H),SLOT_NECK) + H.equip_to_slot_or_del(new neck(H), SLOT_NECK, TRUE) if(ears) - H.equip_to_slot_or_del(new ears(H),SLOT_EARS) + H.equip_to_slot_or_del(new ears(H), SLOT_EARS, TRUE) if(glasses) - H.equip_to_slot_or_del(new glasses(H),SLOT_GLASSES) + H.equip_to_slot_or_del(new glasses(H), SLOT_GLASSES, TRUE) if(id) - H.equip_to_slot_or_del(new id(H),SLOT_WEAR_ID) + H.equip_to_slot_or_del(new id(H), SLOT_WEAR_ID, TRUE) if(suit_store) - H.equip_to_slot_or_del(new suit_store(H),SLOT_S_STORE) + H.equip_to_slot_or_del(new suit_store(H), SLOT_S_STORE, TRUE) + if(undershirt) + H.undershirt = initial(undershirt.name) if(accessory) var/obj/item/clothing/under/U = H.w_uniform @@ -81,9 +195,9 @@ if(!visualsOnly) // Items in pockets or backpack don't show up on mob's icon. if(l_pocket) - H.equip_to_slot_or_del(new l_pocket(H),SLOT_L_STORE) + H.equip_to_slot_or_del(new l_pocket(H), SLOT_L_STORE, TRUE) if(r_pocket) - H.equip_to_slot_or_del(new r_pocket(H),SLOT_R_STORE) + H.equip_to_slot_or_del(new r_pocket(H), SLOT_R_STORE, TRUE) if(box) if(!backpack_contents) @@ -97,7 +211,7 @@ if(!isnum(number))//Default to 1 number = 1 for(var/i in 1 to number) - H.equip_to_slot_or_del(new path(H),SLOT_IN_BACKPACK) + H.equip_to_slot_or_del(new path(H), SLOT_IN_BACKPACK, TRUE) if(!H.head && toggle_helmet && istype(H.wear_suit, /obj/item/clothing/suit/space/hardsuit)) var/obj/item/clothing/suit/space/hardsuit/HS = H.wear_suit @@ -112,55 +226,178 @@ H.update_action_buttons_icon() if(implants) for(var/implant_type in implants) - var/obj/item/implant/I = new implant_type + var/obj/item/implant/I = new implant_type(H) I.implant(H, null, TRUE) H.update_body() return TRUE +/** + * Apply a fingerprint from the passed in human to all items in the outfit + * + * Used for forensics setup when the mob is first equipped at roundstart + * essentially calls add_fingerprint to every defined item on the human + * + */ /datum/outfit/proc/apply_fingerprints(mob/living/carbon/human/H) if(!istype(H)) return if(H.back) - H.back.add_fingerprint(H,1) //The 1 sets a flag to ignore gloves + H.back.add_fingerprint(H, ignoregloves = TRUE) for(var/obj/item/I in H.back.contents) - I.add_fingerprint(H,1) + I.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_id) - H.wear_id.add_fingerprint(H,1) + H.wear_id.add_fingerprint(H, ignoregloves = TRUE) if(H.w_uniform) - H.w_uniform.add_fingerprint(H,1) + H.w_uniform.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_suit) - H.wear_suit.add_fingerprint(H,1) + H.wear_suit.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_mask) - H.wear_mask.add_fingerprint(H,1) + H.wear_mask.add_fingerprint(H, ignoregloves = TRUE) if(H.wear_neck) - H.wear_neck.add_fingerprint(H,1) + H.wear_neck.add_fingerprint(H, ignoregloves = TRUE) if(H.head) - H.head.add_fingerprint(H,1) + H.head.add_fingerprint(H, ignoregloves = TRUE) if(H.shoes) - H.shoes.add_fingerprint(H,1) + H.shoes.add_fingerprint(H, ignoregloves = TRUE) if(H.gloves) - H.gloves.add_fingerprint(H,1) + H.gloves.add_fingerprint(H, ignoregloves = TRUE) if(H.ears) - H.ears.add_fingerprint(H,1) + H.ears.add_fingerprint(H, ignoregloves = TRUE) if(H.glasses) - H.glasses.add_fingerprint(H,1) + H.glasses.add_fingerprint(H, ignoregloves = TRUE) if(H.belt) - H.belt.add_fingerprint(H,1) + H.belt.add_fingerprint(H, ignoregloves = TRUE) for(var/obj/item/I in H.belt.contents) - I.add_fingerprint(H,1) + I.add_fingerprint(H, ignoregloves = TRUE) if(H.s_store) - H.s_store.add_fingerprint(H,1) + H.s_store.add_fingerprint(H, ignoregloves = TRUE) if(H.l_store) - H.l_store.add_fingerprint(H,1) + H.l_store.add_fingerprint(H, ignoregloves = TRUE) if(H.r_store) - H.r_store.add_fingerprint(H,1) + H.r_store.add_fingerprint(H, ignoregloves = TRUE) for(var/obj/item/I in H.held_items) - I.add_fingerprint(H,1) - return 1 + I.add_fingerprint(H, ignoregloves = TRUE) + return TRUE +/// Return a list of all the types that are required to disguise as this outfit type /datum/outfit/proc/get_chameleon_disguise_info() var/list/types = list(uniform, suit, back, belt, gloves, shoes, head, mask, neck, ears, glasses, id, l_pocket, r_pocket, suit_store, r_hand, l_hand) types += chameleon_extras listclearnulls(types) return types + +/// Return a json list of this outfit +/datum/outfit/proc/get_json_data() + . = list() + .["outfit_type"] = type + .["name"] = name + .["uniform"] = uniform + .["suit"] = suit + .["toggle_helmet"] = toggle_helmet + .["back"] = back + .["belt"] = belt + .["gloves"] = gloves + .["shoes"] = shoes + .["head"] = head + .["mask"] = mask + .["neck"] = neck + .["ears"] = ears + .["glasses"] = glasses + .["id"] = id + .["l_pocket"] = l_pocket + .["r_pocket"] = r_pocket + .["suit_store"] = suit_store + .["r_hand"] = r_hand + .["l_hand"] = l_hand + .["internals_slot"] = internals_slot + .["backpack_contents"] = backpack_contents + .["box"] = box + .["implants"] = implants + .["accessory"] = accessory + +/// Copy most vars from another outfit to this one +/datum/outfit/proc/copy_from(datum/outfit/target) + name = target.name + uniform = target.uniform + suit = target.suit + toggle_helmet = target.toggle_helmet + back = target.back + belt = target.belt + gloves = target.gloves + shoes = target.shoes + head = target.head + mask = target.mask + neck = target.neck + ears = target.ears + glasses = target.glasses + id = target.id + l_pocket = target.l_pocket + r_pocket = target.r_pocket + suit_store = target.suit_store + r_hand = target.r_hand + l_hand = target.l_hand + internals_slot = target.internals_slot + backpack_contents = target.backpack_contents + box = target.box + implants = target.implants + accessory = target.accessory + +/// Prompt the passed in mob client to download this outfit as a json blob +/datum/outfit/proc/save_to_file(mob/admin) + var/stored_data = get_json_data() + var/json = json_encode(stored_data) + //Kinda annoying but as far as i can tell you need to make actual file. + var/f = file("data/TempOutfitUpload") + fdel(f) + WRITE_FILE(f,json) + admin << ftp(f,"[name].json") + +/// Create an outfit datum from a list of json data +/datum/outfit/proc/load_from(list/outfit_data) + //This could probably use more strict validation + name = outfit_data["name"] + uniform = text2path(outfit_data["uniform"]) + suit = text2path(outfit_data["suit"]) + toggle_helmet = outfit_data["toggle_helmet"] + back = text2path(outfit_data["back"]) + belt = text2path(outfit_data["belt"]) + gloves = text2path(outfit_data["gloves"]) + shoes = text2path(outfit_data["shoes"]) + head = text2path(outfit_data["head"]) + mask = text2path(outfit_data["mask"]) + neck = text2path(outfit_data["neck"]) + ears = text2path(outfit_data["ears"]) + glasses = text2path(outfit_data["glasses"]) + id = text2path(outfit_data["id"]) + l_pocket = text2path(outfit_data["l_pocket"]) + r_pocket = text2path(outfit_data["r_pocket"]) + suit_store = text2path(outfit_data["suit_store"]) + r_hand = text2path(outfit_data["r_hand"]) + l_hand = text2path(outfit_data["l_hand"]) + internals_slot = outfit_data["internals_slot"] + var/list/backpack = outfit_data["backpack_contents"] + backpack_contents = list() + for(var/item in backpack) + var/itype = text2path(item) + if(itype) + backpack_contents[itype] = backpack[item] + box = text2path(outfit_data["box"]) + var/list/impl = outfit_data["implants"] + implants = list() + for(var/I in impl) + var/imptype = text2path(I) + if(imptype) + implants += imptype + accessory = text2path(outfit_data["accessory"]) + return TRUE + +/datum/outfit/vv_get_dropdown() + . = ..() + VV_DROPDOWN_OPTION("", "---") + VV_DROPDOWN_OPTION(VV_HK_TO_OUTFIT_EDITOR, "Outfit Editor") + +/datum/outfit/vv_do_topic(list/href_list) + . = ..() + if(href_list[VV_HK_TO_OUTFIT_EDITOR]) + usr.client.open_outfit_editor(src) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index f22ceb6a2d..96eb4e4a0f 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -93,7 +93,7 @@ GLOBAL_PROTECT(admin_verbs_ban) GLOBAL_LIST_INIT(admin_verbs_sounds, list(/client/proc/play_local_sound, /client/proc/play_sound, /client/proc/manual_play_web_sound, /client/proc/set_round_end_sound)) GLOBAL_PROTECT(admin_verbs_sounds) GLOBAL_LIST_INIT(admin_verbs_fun, list( - /client/proc/cmd_admin_dress, + /client/proc/cmd_select_equipment, /client/proc/cmd_admin_gib_self, /client/proc/drop_bomb, /client/proc/set_dynex_scale, @@ -232,7 +232,7 @@ GLOBAL_LIST_INIT(admin_verbs_hideable, list( /client/proc/play_local_sound, /client/proc/play_sound, /client/proc/set_round_end_sound, - /client/proc/cmd_admin_dress, + /client/proc/cmd_select_equipment, /client/proc/cmd_admin_gib_self, /client/proc/drop_bomb, /client/proc/drop_dynex_bomb, diff --git a/code/modules/admin/outfit_editor.dm b/code/modules/admin/outfit_editor.dm new file mode 100644 index 0000000000..9a99d8b20e --- /dev/null +++ b/code/modules/admin/outfit_editor.dm @@ -0,0 +1,196 @@ + +/client/proc/open_outfit_editor(datum/outfit/target) + var/datum/outfit_editor/ui = new(usr, target) + ui.ui_interact(usr) + +#define OUTFIT_EDITOR_NAME "Outfit-O-Tron 9000" +/datum/outfit_editor + var/client/owner + + var/dummy_key + + var/datum/outfit/drip + +/datum/outfit_editor/New(user, datum/outfit/target) + owner = CLIENT_FROM_VAR(user) + + if(ispath(target)) + drip = new /datum/outfit + drip.copy_from(new target) + else if(istype(target)) + drip = target + else + drip = new /datum/outfit + drip.name = "New Outfit" + +/datum/outfit_editor/ui_state(mob/user) + return GLOB.admin_state + +/datum/outfit_editor/ui_status(mob/user, datum/ui_state/state) + if(QDELETED(drip)) + return UI_CLOSE + return ..() + +/datum/outfit_editor/ui_close(mob/user) + clear_human_dummy(dummy_key) + qdel(src) + +/datum/outfit_editor/proc/init_dummy() + dummy_key = "outfit_editor_[owner]" + generate_dummy_lookalike(dummy_key, owner.mob) + unset_busy_human_dummy(dummy_key) + +/datum/outfit_editor/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "OutfitEditor", OUTFIT_EDITOR_NAME) + ui.open() + ui.set_autoupdate(FALSE) + +/datum/outfit_editor/proc/entry(data) + if(ispath(data, /obj/item)) + var/obj/item/item = data + return list( + "path" = item, + "name" = initial(item.name), + "desc" = initial(item.desc), + // at this point initializing the item is probably faster tbh + "sprite" = icon2base64(icon(initial(item.icon), initial(item.icon_state))), + ) + + return data + +/datum/outfit_editor/proc/serialize_outfit() + var/list/outfit_slots = drip.get_json_data() + . = list() + for(var/key in outfit_slots) + var/val = outfit_slots[key] + . += list("[key]" = entry(val)) + +/datum/outfit_editor/ui_data(mob/user) + var/list/data = list() + + data["outfit"] = serialize_outfit() + data["saveable"] = !GLOB.custom_outfits.Find(drip) + + if(!dummy_key) + init_dummy() + var/icon/dummysprite = get_flat_human_icon(null, + dummy_key = dummy_key, + showDirs = list(SOUTH), + outfit_override = drip) + data["dummy64"] = icon2base64(dummysprite) + + return data + + +/datum/outfit_editor/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + . = TRUE + + var/slot = params["slot"] + switch(action) + if("click") + choose_item(slot) + if("ctrlClick") + choose_any_item(slot) + if("clear") + if(drip.vars.Find(slot)) + drip.vars[slot] = null + + if("rename") + var/newname = stripped_input(owner, "What do you want to name this outfit?", OUTFIT_EDITOR_NAME) + if(newname) + drip.name = newname + if("save") + GLOB.custom_outfits |= drip + SStgui.update_user_uis(owner.mob) + if("delete") + GLOB.custom_outfits -= drip + SStgui.update_user_uis(owner.mob) + if("vv") + owner.debug_variables(drip) + + +/datum/outfit_editor/proc/set_item(slot, obj/item/choice) + if(!choice) + return + if(!ispath(choice)) + alert(owner, "Invalid item", OUTFIT_EDITOR_NAME, "oh no") + return + if(initial(choice.icon_state) == null) //hacky check copied from experimentor code + var/msg = "Warning: This item's icon_state is null, indicating it is very probably not actually a usable item." + if(alert(owner, msg, OUTFIT_EDITOR_NAME, "Use it anyway", "Cancel") != "Use it anyway") + return + + if(drip.vars.Find(slot)) + drip.vars[slot] = choice + +/datum/outfit_editor/proc/choose_any_item(slot) + var/obj/item/choice = pick_closest_path(FALSE) + + if(!choice) + return + + set_item(slot, choice) + +//this proc will try to give a good selection of items that the user can choose from +//it does *not* give a selection of all items that can fit in a slot because lag; +//most notably the hand and pocket slots because they accept pretty much anything +//also stuff that fits in the belt and back slots are scattered pretty much all over the place +/datum/outfit_editor/proc/choose_item(slot) + var/list/options = list() + + switch(slot) + if("head") + options = typesof(/obj/item/clothing/head) + if("glasses") + options = typesof(/obj/item/clothing/glasses) + if("ears") + options = typesof(/obj/item/radio/headset) + + if("neck") + options = typesof(/obj/item/clothing/neck) + if("mask") + options = typesof(/obj/item/clothing/mask) + + if("uniform") + options = typesof(/obj/item/clothing/under) + if("suit") + options = typesof(/obj/item/clothing/suit) + if("gloves") + options = typesof(/obj/item/clothing/gloves) + + if("suit_store") + var/obj/item/clothing/suit/suit = drip.suit + if(suit) + suit = new suit //initial() doesn't like lists + options = suit.allowed + if(!options.len) //nothing will happen, but don't let the user think it's broken + to_chat(owner, "No options available for the current suit.") + + if("belt") + options = typesof(/obj/item/storage/belt) + if("id") + options = typesof(/obj/item/card/id) + + if("l_hand") + choose_any_item(slot) + if("back") + options = typesof(/obj/item/storage/backpack) + if("r_hand") + choose_any_item(slot) + + if("l_pocket") + choose_any_item(slot) + if("shoes") + options = typesof(/obj/item/clothing/shoes) + if("r_pocket") + choose_any_item(slot) + + if(length(options)) + set_item(slot, tgui_input_list(owner, "Choose an item", OUTFIT_EDITOR_NAME, options)) + + +#undef OUTFIT_EDITOR_NAME diff --git a/code/modules/admin/outfit_manager.dm b/code/modules/admin/outfit_manager.dm new file mode 100644 index 0000000000..9d20b64547 --- /dev/null +++ b/code/modules/admin/outfit_manager.dm @@ -0,0 +1,73 @@ +/client/proc/outfit_manager() + set category = "Debug" + set name = "Outfit Manager" + + if(!check_rights(R_DEBUG)) + return + var/datum/outfit_manager/ui = new(usr) + ui.ui_interact(usr) + + +/datum/outfit_manager + var/client/owner + +/datum/outfit_manager/New(user) + owner = CLIENT_FROM_VAR(user) + +/datum/outfit_manager/ui_state(mob/user) + return GLOB.admin_state + +/datum/outfit_manager/ui_close(mob/user) + qdel(src) + +/datum/outfit_manager/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "OutfitManager") + ui.open() + +/datum/outfit_manager/proc/entry(datum/outfit/outfit) + var/vv = FALSE + var/datum/outfit/varedit/varoutfit = outfit + if(istype(varoutfit)) + vv = length(varoutfit.vv_values) + return list( + "name" = "[outfit.name] [vv ? "(VV)" : ""]", + "ref" = REF(outfit), + ) + +/datum/outfit_manager/ui_data(mob/user) + var/list/data = list() + + var/list/outfits = list() + for(var/datum/outfit/custom_outfit in GLOB.custom_outfits) + outfits += list(entry(custom_outfit)) + data["outfits"] = outfits + + return data + +/datum/outfit_manager/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + . = TRUE + + switch(action) + if("new") + owner.open_outfit_editor(new /datum/outfit) + if("load") + owner.holder.load_outfit(owner.mob) + if("copy") + var/datum/outfit/outfit = tgui_input_list(owner, "Pick an outfit to copy from", "Outfit Manager", subtypesof(/datum/outfit)) + if(ispath(outfit)) + owner.open_outfit_editor(new outfit) + + var/datum/outfit/target_outfit = locate(params["outfit"]) + if(!istype(target_outfit)) + return + switch(action) //wow we're switching through action again this is horrible optimization smh + if("edit") + owner.open_outfit_editor(target_outfit) + if("save") + owner.holder.save_outfit(owner.mob, target_outfit) + if("delete") + owner.holder.delete_outfit(owner.mob, target_outfit) diff --git a/code/modules/admin/outfits.dm b/code/modules/admin/outfits.dm new file mode 100644 index 0000000000..1b615e3d62 --- /dev/null +++ b/code/modules/admin/outfits.dm @@ -0,0 +1,32 @@ +GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits + +/datum/admins/proc/save_outfit(mob/admin, datum/outfit/O) + O.save_to_file(admin) + SStgui.update_user_uis(admin) + +/datum/admins/proc/delete_outfit(mob/admin, datum/outfit/O) + GLOB.custom_outfits -= O + qdel(O) + to_chat(admin,"Outfit deleted.") + SStgui.update_user_uis(admin) + +/datum/admins/proc/load_outfit(mob/admin) + var/outfit_file = input("Pick outfit json file:", "File") as null|file + if(!outfit_file) + return + var/filedata = file2text(outfit_file) + var/json = json_decode(filedata) + if(!json) + to_chat(admin,"JSON decode error.") + return + var/otype = text2path(json["outfit_type"]) + if(!ispath(otype,/datum/outfit)) + to_chat(admin,"Malformed/Outdated file.") + return + var/datum/outfit/O = new otype + if(!O.load_from(json)) + to_chat(admin,"Malformed/Outdated file.") + return + GLOB.custom_outfits += O + SStgui.update_user_uis(admin) + diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 2c1ddc4e0a..a123d73e62 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -484,74 +484,52 @@ set name = "Test Areas (ALL)" cmd_admin_areatest(FALSE) -/client/proc/cmd_admin_dress(mob/M in GLOB.mob_list) - set category = "Admin.Events" - set name = "Select equipment" - if(!(ishuman(M) || isobserver(M))) - alert("Invalid mob") - return - - var/dresscode = robust_dress_shop() - - if(!dresscode) - return - - var/delete_pocket - var/mob/living/carbon/human/H - if(isobserver(M)) - H = M.change_mob_type(/mob/living/carbon/human, null, null, TRUE) - else - H = M - if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No") - delete_pocket = TRUE - - SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! - for (var/obj/item/I in H.get_equipped_items(delete_pocket)) - qdel(I) - if(dresscode != "Naked") - H.equipOutfit(dresscode) - - H.regenerate_icons() - - log_admin("[key_name(usr)] changed the equipment of [key_name(H)] to [dresscode].") - message_admins("[key_name_admin(usr)] changed the equipment of [ADMIN_LOOKUPFLW(H)] to [dresscode].") - /client/proc/robust_dress_shop() - var/list/outfits = list("Cancel","Naked","Custom","As Job...") - var/list/paths = subtypesof(/datum/outfit) - typesof(/datum/outfit/job) + + var/list/baseoutfits = list("Naked","Custom","As Job...", "As Plasmaman...") + var/list/outfits = list() + var/list/paths = subtypesof(/datum/outfit) - typesof(/datum/outfit/job) - typesof(/datum/outfit/plasmaman) + for(var/path in paths) var/datum/outfit/O = path //not much to initalize here but whatever - if(initial(O.can_be_admin_equipped)) - outfits[initial(O.name)] = path + outfits[initial(O.name)] = path - var/dresscode = input("Select outfit", "Robust quick dress shop") as null|anything in outfits + var/dresscode = input("Select outfit", "Robust quick dress shop") as null|anything in baseoutfits + sortList(outfits) if (isnull(dresscode)) return if (outfits[dresscode]) dresscode = outfits[dresscode] - if(dresscode == "Cancel") - return - if (dresscode == "As Job...") var/list/job_paths = subtypesof(/datum/outfit/job) var/list/job_outfits = list() for(var/path in job_paths) var/datum/outfit/O = path - if(initial(O.can_be_admin_equipped)) - job_outfits[initial(O.name)] = path + job_outfits[initial(O.name)] = path - dresscode = input("Select job equipment", "Robust quick dress shop") as null|anything in job_outfits + dresscode = input("Select job equipment", "Robust quick dress shop") as null|anything in sortList(job_outfits) dresscode = job_outfits[dresscode] if(isnull(dresscode)) return + if (dresscode == "As Plasmaman...") + var/list/plasmaman_paths = typesof(/datum/outfit/plasmaman) + var/list/plasmaman_outfits = list() + for(var/path in plasmaman_paths) + var/datum/outfit/O = path + plasmaman_outfits[initial(O.name)] = path + + dresscode = input("Select plasmeme equipment", "Robust quick dress shop") as null|anything in sortList(plasmaman_outfits) + dresscode = plasmaman_outfits[dresscode] + if(isnull(dresscode)) + return + if (dresscode == "Custom") var/list/custom_names = list() for(var/datum/outfit/D in GLOB.custom_outfits) custom_names[D.name] = D - var/selected_name = input("Select outfit", "Robust quick dress shop") as null|anything in custom_names + var/selected_name = input("Select outfit", "Robust quick dress shop") as null|anything in sortList(custom_names) dresscode = custom_names[selected_name] if(isnull(dresscode)) return diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index 199004abed..7e7bac9ff0 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -879,8 +879,6 @@ Traitors and the like can also be revived with the previous role mostly intact. message_admins("[ADMIN_LOOKUPFLW(usr)] [N.timing ? "activated" : "deactivated"] a nuke at [ADMIN_VERBOSEJMP(N)].") SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Nuke", "[N.timing]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! -GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits - /client/proc/create_outfits() set category = "Debug" set name = "Create Custom Outfit" diff --git a/code/modules/admin/verbs/selectequipment.dm b/code/modules/admin/verbs/selectequipment.dm new file mode 100644 index 0000000000..4184fed68f --- /dev/null +++ b/code/modules/admin/verbs/selectequipment.dm @@ -0,0 +1,226 @@ +/client/proc/cmd_select_equipment(mob/target in GLOB.mob_list) + set category = "Admin.Events" + set name = "Select equipment" + + + var/datum/select_equipment/ui = new(usr, target) + ui.ui_interact(usr) + +/* + * This is the datum housing the select equipment UI. + * + * You may notice some oddities about the way outfits are passed to the UI and vice versa here. + * That's because it handles both outfit typepaths (for normal outfits) *and* outfit objects (for custom outfits). + * + * Custom outfits need to be objects as they're created in runtime. + * "Then just handle the normal outfits as objects too and simplify the handling" - you may say. + * There are about 300 outfit types at the time of writing this. Initializing all of these to objects would be a huge waste. + * + */ + +/datum/select_equipment + var/client/user + var/mob/target_mob + + var/dummy_key + + //static list to share all the outfit typepaths between all instances of this datum. + var/static/list/cached_outfits + + //a typepath if the selected outfit is a normal outfit; + //an object if the selected outfit is a custom outfit + var/datum/outfit/selected_outfit = /datum/outfit + //serializable string for the UI to keep track of which outfit is selected + var/selected_identifier = "/datum/outfit" + +/datum/select_equipment/New(_user, mob/target) + user = CLIENT_FROM_VAR(_user) + + if(!ishuman(target) && !isobserver(target)) + alert("Invalid mob") + return + target_mob = target + +/datum/select_equipment/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "SelectEquipment", "Select Equipment") + ui.open() + ui.set_autoupdate(FALSE) + +/datum/select_equipment/ui_state(mob/user) + return GLOB.admin_state + +/datum/select_equipment/ui_status(mob/user, datum/ui_state/state) + if(QDELETED(target_mob)) + return UI_CLOSE + return ..() + +/datum/select_equipment/ui_close(mob/user) + clear_human_dummy(dummy_key) + qdel(src) + +/datum/select_equipment/proc/init_dummy() + dummy_key = "selectequipmentUI_[target_mob]" + generate_dummy_lookalike(dummy_key, target_mob) + unset_busy_human_dummy(dummy_key) + return + +/** + * Packs up data about an outfit as an assoc list to send to the UI as an outfit entry. + * + * Args: + * * category (string) - The tab it will be under + * + * * identifier (typepath or ref) - This will sent this back to ui_act to preview or spawn in an outfit. + * * Must be unique between all entries. + * + * * name (string) - Will be the text on the button + * + * * priority (bool)(optional) - If True, the UI will sort the entry to the top, right below favorites. + * + * * custom_entry (bool)(optional) - Send the identifier with a "ref" keyword instead of "path", + * * for the UI to tell apart custom outfits from normal ones. + * + * Returns (list) An outfit entry + */ + +/datum/select_equipment/proc/outfit_entry(category, identifier, name, priority=FALSE, custom_entry=FALSE) + if(custom_entry) + return list("category" = category, "ref" = identifier, "name" = name, "priority" = priority) + return list("category" = category, "path" = identifier, "name" = name, "priority" = priority) + +/datum/select_equipment/proc/make_outfit_entries(category="General", list/outfit_list) + var/list/entries = list() + for(var/path as anything in outfit_list) + var/datum/outfit/outfit = path + entries += list(outfit_entry(category, path, initial(outfit.name))) + return entries + +//GLOB.custom_outfits lists outfit *objects* so we'll need to do some custom handling for it +/datum/select_equipment/proc/make_custom_outfit_entries(list/outfit_list) + var/list/entries = list() + for(var/datum/outfit/outfit as anything in outfit_list) + entries += list(outfit_entry("Custom", REF(outfit), outfit.name, custom_entry=TRUE)) //it's either this or special handling on the UI side + return entries + +/datum/select_equipment/ui_data(mob/user) + var/list/data = list() + if(!dummy_key) + init_dummy() + + var/icon/dummysprite = get_flat_human_icon(null, + dummy_key = dummy_key, + outfit_override = selected_outfit) + data["icon64"] = icon2base64(dummysprite) + data["name"] = target_mob + + var/datum/preferences/prefs = user?.client?.prefs + data["favorites"] = list() + if(prefs) + data["favorites"] = prefs.favorite_outfits + + var/list/custom + custom += make_custom_outfit_entries(GLOB.custom_outfits) + data["custom_outfits"] = custom + data["current_outfit"] = selected_identifier + return data + + +/datum/select_equipment/ui_static_data(mob/user) + var/list/data = list() + if(!cached_outfits) + cached_outfits = list() + cached_outfits += list(outfit_entry("General", /datum/outfit, "Naked", priority=TRUE)) + cached_outfits += make_outfit_entries("General", subtypesof(/datum/outfit) - typesof(/datum/outfit/job) - typesof(/datum/outfit/plasmaman)) + cached_outfits += make_outfit_entries("Jobs", typesof(/datum/outfit/job)) + cached_outfits += make_outfit_entries("Plasmamen Outfits", typesof(/datum/outfit/plasmaman)) + + data["outfits"] = cached_outfits + return data + + +/datum/select_equipment/proc/resolve_outfit(text) + + var/path = text2path(text) + if(ispath(path, /datum/outfit)) + return path + + else //don't bail yet - could be a custom outfit + var/datum/outfit/custom_outfit = locate(text) + if(istype(custom_outfit)) + return custom_outfit + + +/datum/select_equipment/ui_act(action, params) + if(..()) + return + . = TRUE + switch(action) + if("preview") + var/datum/outfit/new_outfit = resolve_outfit(params["path"]) + + if(ispath(new_outfit)) //got a typepath - that means we're dealing with a normal outfit + selected_identifier = new_outfit //these are keyed by type + //by the way, no, they can't be keyed by name because many of them have duplicate names + + else if(istype(new_outfit)) //got an initialized object - means it's a custom outfit + selected_identifier = REF(new_outfit) //and the outfit will be keyed by its ref (cause its type will always be /datum/outfit) + + else //we got nothing and should bail + return + + selected_outfit = new_outfit + + if("applyoutfit") + var/datum/outfit/new_outfit = resolve_outfit(params["path"]) + if(new_outfit && ispath(new_outfit)) //initialize it + new_outfit = new new_outfit + if(!istype(new_outfit)) + return + user.admin_apply_outfit(target_mob, new_outfit) + + if("customoutfit") + user.outfit_manager() + + if("togglefavorite") + var/datum/outfit/outfit_path = resolve_outfit(params["path"]) + if(!ispath(outfit_path)) //we do *not* want custom outfits (i.e objects) here, they're not even persistent + return + + if(user.prefs.favorite_outfits.Find(outfit_path)) //already there, remove it + user.prefs.favorite_outfits -= outfit_path + else //not there, add it + user.prefs.favorite_outfits += outfit_path + user.prefs.save_preferences() + +/client/proc/admin_apply_outfit(mob/target, dresscode) + if(!ishuman(target) && !isobserver(target)) + alert("Invalid mob") + return + + if(!dresscode) + return + + var/delete_pocket + var/mob/living/carbon/human/human_target + if(isobserver(target)) + human_target = target.change_mob_type(/mob/living/carbon/human, delete_old_mob = TRUE) + else + human_target = target + if(human_target.l_store || human_target.r_store || human_target.s_store) //saves a lot of time for admins and coders alike + if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No") + delete_pocket = TRUE + + SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + for(var/obj/item/item in human_target.get_equipped_items(delete_pocket)) + qdel(item) + if(dresscode != "Naked") + human_target.equipOutfit(dresscode) + + human_target.regenerate_icons() + + log_admin("[key_name(usr)] changed the equipment of [key_name(human_target)] to [dresscode].") + message_admins("[key_name_admin(usr)] changed the equipment of [ADMIN_LOOKUPFLW(human_target)] to [dresscode].") + + return dresscode diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index 3e4b962bc4..81d267fcf1 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -227,6 +227,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/persistent_scars = TRUE ///If we want to broadcast deadchat connect/disconnect messages var/broadcast_login_logout = TRUE + ///What outfit typepaths we've favorited in the SelectEquipment menu + var/list/favorite_outfits = list() /// We have 5 slots for persistent scars, if enabled we pick a random one to load (empty by default) and scars at the end of the shift if we survived as our original person var/list/scars_list = list("1" = "", "2" = "", "3" = "", "4" = "", "5" = "") /// Which of the 5 persistent scar slots we randomly roll to load for this round, if enabled. Actually rolled in [/datum/preferences/proc/load_character(slot)] diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index b8959d93aa..864bf5335a 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -385,6 +385,15 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car S["auto_ooc"] >> auto_ooc S["no_tetris_storage"] >> no_tetris_storage + //favorite outfits + S["favorite_outfits"] >> favorite_outfits + + var/list/parsed_favs = list() + for(var/typetext in favorite_outfits) + var/datum/outfit/path = text2path(typetext) + if(ispath(path)) //whatever typepath fails this check probably doesn't exist anymore + parsed_favs += path + favorite_outfits = uniqueList(parsed_favs) //try to fix any outdated data if necessary if(needs_update >= 0) @@ -434,6 +443,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car no_tetris_storage = sanitize_integer(no_tetris_storage, 0, 1, initial(no_tetris_storage)) key_bindings = sanitize_islist(key_bindings, list()) modless_key_bindings = sanitize_islist(modless_key_bindings, list()) + favorite_outfits = SANITIZE_LIST(favorite_outfits) verify_keybindings_valid() // one of these days this will runtime and you'll be glad that i put it in a different proc so no one gets their saves wiped @@ -535,6 +545,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car WRITE_FILE(S["pda_skin"], pda_skin) WRITE_FILE(S["key_bindings"], key_bindings) WRITE_FILE(S["modless_key_bindings"], modless_key_bindings) + WRITE_FILE(S["favorite_outfits"], favorite_outfits) //citadel code WRITE_FILE(S["screenshake"], screenshake) diff --git a/code/modules/clothing/chameleon.dm b/code/modules/clothing/chameleon.dm index 6e570e595a..008772663d 100644 --- a/code/modules/clothing/chameleon.dm +++ b/code/modules/clothing/chameleon.dm @@ -83,8 +83,7 @@ standard_outfit_options = list() for(var/path in subtypesof(/datum/outfit/job)) var/datum/outfit/O = path - if(initial(O.can_be_admin_equipped)) - standard_outfit_options[initial(O.name)] = path + standard_outfit_options[initial(O.name)] = path sortTim(standard_outfit_options, /proc/cmp_text_asc) outfit_options = standard_outfit_options diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index ed1ba3852f..3074abd070 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -24,7 +24,12 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy) /mob/living/carbon/human/dummy/proc/wipe_state() delete_equipment() icon_render_key = null - cut_overlays() + cut_overlays(TRUE) + +/mob/living/carbon/human/dummy/setup_human_dna() + create_dna(src) + randomize_human(src) + dna.initialize_dna(skip_index = TRUE) //Skip stuff that requires full round init. //Inefficient pooling/caching way. GLOBAL_LIST_EMPTY(human_dummy_list) @@ -42,13 +47,48 @@ GLOBAL_LIST_EMPTY(dummy_mob_list) D = new GLOB.human_dummy_list[slotkey] = D GLOB.dummy_mob_list += D + else + D.regenerate_icons() //they were cut in wipe_state() D.in_use = TRUE return D -/proc/unset_busy_human_dummy(slotnumber) - if(!slotnumber) +/proc/generate_dummy_lookalike(slotkey, mob/target) + if(!istype(target)) + return generate_or_wait_for_human_dummy(slotkey) + + var/mob/living/carbon/human/dummy/copycat = generate_or_wait_for_human_dummy(slotkey) + + if(iscarbon(target)) + var/mob/living/carbon/carbon_target = target + carbon_target.dna.transfer_identity(copycat, transfer_SE = TRUE) + + if(ishuman(target)) + var/mob/living/carbon/human/human_target = target + human_target.copy_clothing_prefs(copycat) + + copycat.updateappearance(icon_update=TRUE, mutcolor_update=TRUE, mutations_overlay_update=TRUE) + else + //even if target isn't a carbon, if they have a client we can make the + //dummy look like what their human would look like based on their prefs + target?.client?.prefs?.copy_to(copycat, icon_updates=TRUE, roundstart_checks=FALSE) + + return copycat + +/proc/unset_busy_human_dummy(slotkey) + if(!slotkey) return - var/mob/living/carbon/human/dummy/D = GLOB.human_dummy_list[slotnumber] + var/mob/living/carbon/human/dummy/D = GLOB.human_dummy_list[slotkey] if(istype(D)) D.wipe_state() D.in_use = FALSE + +/proc/clear_human_dummy(slotkey) + if(!slotkey) + return + + var/mob/living/carbon/human/dummy/dummy = GLOB.human_dummy_list[slotkey] + + GLOB.human_dummy_list -= slotkey + if(istype(dummy)) + GLOB.dummy_mob_list -= dummy + qdel(dummy) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 8660e115a6..c2014cbe41 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -14,10 +14,7 @@ //initialize limbs first create_bodyparts() - //initialize dna. for spawned humans; overwritten by other code - create_dna(src) - randomize_human(src) - dna.initialize_dna() + setup_human_dna() if(dna.species) set_species(dna.species.type) @@ -36,6 +33,11 @@ RegisterSignal(src, COMSIG_COMPONENT_CLEAN_ACT, /atom.proc/clean_blood) GLOB.human_list += src +/mob/living/carbon/human/proc/setup_human_dna() + //initialize dna. for spawned humans; overwritten by other code + create_dna(src) + randomize_human(src) + dna.initialize_dna() /mob/living/carbon/human/ComponentInitialize() . = ..() diff --git a/code/modules/mob/living/carbon/human/human_helpers.dm b/code/modules/mob/living/carbon/human/human_helpers.dm index 3e65bb6e66..7e3b2ab015 100644 --- a/code/modules/mob/living/carbon/human/human_helpers.dm +++ b/code/modules/mob/living/carbon/human/human_helpers.dm @@ -176,3 +176,9 @@ /mob/living/carbon/human/get_biological_state() return dna.species.get_biological_state() + +///copies over clothing preferences like underwear to another human +/mob/living/carbon/human/proc/copy_clothing_prefs(mob/living/carbon/human/destination) + destination.underwear = underwear + destination.undershirt = undershirt + destination.socks = socks diff --git a/tgstation.dme b/tgstation.dme index d13020e2e5..017bd393e7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1416,6 +1416,9 @@ #include "code\modules\admin\ipintel.dm" #include "code\modules\admin\IsBanned.dm" #include "code\modules\admin\NewBan.dm" +#include "code\modules\admin\outfit_editor.dm" +#include "code\modules\admin\outfit_manager.dm" +#include "code\modules\admin\outfits.dm" #include "code\modules\admin\permissionedit.dm" #include "code\modules\admin\player_panel.dm" #include "code\modules\admin\sound_emitter.dm" @@ -1456,6 +1459,7 @@ #include "code\modules\admin\verbs\randomverbs.dm" #include "code\modules\admin\verbs\reestablish_db_connection.dm" #include "code\modules\admin\verbs\secrets.dm" +#include "code\modules\admin\verbs\selectequipment.dm" #include "code\modules\admin\verbs\shuttlepanel.dm" #include "code\modules\admin\verbs\spawnobjasmob.dm" #include "code\modules\admin\verbs\tripAI.dm" diff --git a/tgui/packages/common/collections.js b/tgui/packages/common/collections.js index 7b4540e730..5c0b3d0893 100644 --- a/tgui/packages/common/collections.js +++ b/tgui/packages/common/collections.js @@ -171,6 +171,8 @@ export const sortBy = (...iterateeFns) => array => { return mappedArray; }; +export const sort = sortBy(); + /** * A fast implementation of reduce. */ @@ -235,6 +237,8 @@ export const uniqBy = iterateeFn => array => { return result; }; +export const uniq = uniqBy(); + /** * Creates an array of grouped elements, the first of which contains * the first elements of the given arrays, the second of which contains diff --git a/tgui/packages/tgui/interfaces/ListInput.js b/tgui/packages/tgui/interfaces/ListInput.js new file mode 100644 index 0000000000..721ac4f5ac --- /dev/null +++ b/tgui/packages/tgui/interfaces/ListInput.js @@ -0,0 +1,206 @@ +/** + * @file + * @copyright 2020 watermelon914 (https://github.com/watermelon914) + * @license MIT + */ + +import { clamp01 } from 'common/math'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Section, Input, Stack } from '../components'; +import { Window } from '../layouts'; + +const ARROW_KEY_UP = 38; +const ARROW_KEY_DOWN = 40; + +let lastScrollTime = 0; + +export const ListInput = (props, context) => { + const { act, data } = useBackend(context); + const { + title, + message, + buttons, + timeout, + } = data; + + // Search + const [showSearchBar, setShowSearchBar] = useLocalState( + context, 'search_bar', false); + const [displayedArray, setDisplayedArray] = useLocalState( + context, 'displayed_array', buttons); + + // KeyPress + const [searchArray, setSearchArray] = useLocalState( + context, 'search_array', []); + const [searchIndex, setSearchIndex] = useLocalState( + context, 'search_index', 0); + const [lastCharCode, setLastCharCode] = useLocalState( + context, 'last_char_code', null); + + // Selected Button + const [selectedButton, setSelectedButton] = useLocalState( + context, 'selected_button', buttons[0]); + + const handleKeyDown = e => { + e.preventDefault(); + if (lastScrollTime > performance.now()) { + return; + } + lastScrollTime = performance.now() + 125; + + if (e.keyCode === ARROW_KEY_UP || e.keyCode === ARROW_KEY_DOWN) { + let direction = 1; + if (e.keyCode === ARROW_KEY_UP) direction = -1; + + let index = 0; + for (index; index < buttons.length; index++) { + if (buttons[index] === selectedButton) break; + } + index += direction; + if (index < 0) index = buttons.length - 1; + else if (index >= buttons.length) index = 0; + setSelectedButton(buttons[index]); + setLastCharCode(null); + document.getElementById(buttons[index]).focus(); + return; + } + + const charCode = String.fromCharCode(e.keyCode).toLowerCase(); + if (!charCode) return; + + let foundValue; + if (charCode === lastCharCode && searchArray.length > 0) { + const nextIndex = searchIndex + 1; + + if (nextIndex < searchArray.length) { + foundValue = searchArray[nextIndex]; + setSearchIndex(nextIndex); + } + else { + foundValue = searchArray[0]; + setSearchIndex(0); + } + } + else { + const resultArray = displayedArray.filter(value => + value.substring(0, 1).toLowerCase() === charCode + ); + + if (resultArray.length > 0) { + setSearchArray(resultArray); + setSearchIndex(0); + foundValue = resultArray[0]; + } + } + + if (foundValue) { + setLastCharCode(charCode); + setSelectedButton(foundValue); + document.getElementById(foundValue).focus(); + } + }; + + return ( + + {timeout !== undefined && } + + + +
{ + setShowSearchBar(!showSearchBar); + setDisplayedArray(buttons); + }} + /> + )}> + {displayedArray.map(button => ( + + ))} +
+
+ {showSearchBar && ( + + setDisplayedArray( + buttons.filter(val => ( + val.toLowerCase().search(value.toLowerCase()) !== -1 + )) + )} + /> + + )} + + + + + + {currItem?.sprite && ( + <> + + act("clear", { slot })} /> + + )} + + + {currItem?.name || "Empty"} + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/OutfitManager.js b/tgui/packages/tgui/interfaces/OutfitManager.js new file mode 100644 index 0000000000..2ffe8cd520 --- /dev/null +++ b/tgui/packages/tgui/interfaces/OutfitManager.js @@ -0,0 +1,81 @@ +import { useBackend } from '../backend'; +import { Button, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +export const OutfitManager = (props, context) => { + const { act, data } = useBackend(context); + const { outfits } = data; + return ( + + +
+
+
+
+ ); +}; + diff --git a/tgui/packages/tgui/interfaces/SelectEquipment.js b/tgui/packages/tgui/interfaces/SelectEquipment.js new file mode 100644 index 0000000000..e7abf709bc --- /dev/null +++ b/tgui/packages/tgui/interfaces/SelectEquipment.js @@ -0,0 +1,214 @@ +import { filter, map, sortBy, uniq } from 'common/collections'; +import { flow } from 'common/fp'; +import { createSearch } from 'common/string'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, Icon, Input, Section, Stack, Tabs } from '../components'; +import { Window } from '../layouts'; + +// here's an important mental define: +// custom outfits give a ref keyword instead of path +const getOutfitKey = outfit => outfit.path || outfit.ref; + +const useOutfitTabs = (context, categories) => { + return useLocalState(context, 'selected-tab', categories[0]); +}; + +export const SelectEquipment = (props, context) => { + const { act, data } = useBackend(context); + const { + name, + icon64, + current_outfit, + favorites, + } = data; + + const isFavorited = entry => favorites?.includes(entry.path); + + const outfits = map(entry => ({ + ...entry, + favorite: isFavorited(entry), + }))([ + ...data.outfits, + ...data.custom_outfits, + ]); + + // even if no custom outfits were sent, we still want to make sure there's + // at least a 'Custom' tab so the button to create a new one pops up + const categories = uniq([ + ...outfits.map(entry => entry.category), + 'Custom', + ]); + const [tab] = useOutfitTabs(context, categories); + + const [searchText, setSearchText] = useLocalState( + context, 'searchText', ''); + const searchFilter = createSearch(searchText, entry => ( + entry.name + entry.path + )); + + const visibleOutfits = flow([ + filter(entry => entry.category === tab), + filter(searchFilter), + sortBy( + entry => !entry.favorite, + entry => !entry.priority, + entry => entry.name + ), + ])(outfits); + + const getOutfitEntry = current_outfit => outfits.find(outfit => ( + getOutfitKey(outfit) === current_outfit + )); + + const currentOutfitEntry = getOutfitEntry(current_outfit); + + return ( + + + + + + + setSearchText(value)} /> + + + + + + + + + + + + +
+ +
+
+ +
+ +
+
+
+
+
+
+
+ ); +}; + +const DisplayTabs = (props, context) => { + const { categories } = props; + const [tab, setTab] = useOutfitTabs(context, categories); + return ( + + {categories.map(category => ( + setTab(category)}> + {category} + + ))} + + ); +}; + +const OutfitDisplay = (props, context) => { + const { act, data } = useBackend(context); + const { current_outfit } = data; + const { entries, currentTab } = props; + return ( +
+ {entries.map(entry => ( + + )} +
+ ); +}; + +const CurrentlySelectedDisplay = (props, context) => { + const { act, data } = useBackend(context); + const { current_outfit } = data; + const { entry } = props; + return ( + + {entry?.path && ( + + act('togglefavorite', { + path: entry.path, + })} /> + + )} + + + Currently selected: + + + {entry?.name} + + + + + + + ); +}; From 90661d36879ab877fd8ce5eb7da04501a4185b80 Mon Sep 17 00:00:00 2001 From: SandPoot Date: Sat, 24 Apr 2021 21:37:01 -0300 Subject: [PATCH 2/4] upload files --- code/__HELPERS/icons.dm | 4 ++-- code/modules/admin/verbs/selectequipment.dm | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index ce6bbf48c3..c9b011d0e4 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1051,7 +1051,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) return 0 //For creating consistent icons for human looking simple animals -/proc/get_flat_human_icon(icon_id, datum/job/J, datum/preferences/prefs, dummy_key, showDirs = GLOB.cardinals, outfit_override = null) +/proc/get_flat_human_icon(icon_id, datum/job/J, datum/preferences/prefs, dummy_key, showDirs = GLOB.cardinals, outfit_override = null, no_anim = FALSE) var/static/list/humanoid_icon_cache = list() if(!icon_id || !humanoid_icon_cache[icon_id]) var/mob/living/carbon/human/dummy/body = generate_or_wait_for_human_dummy(dummy_key) @@ -1067,7 +1067,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") COMPILE_OVERLAYS(body) for(var/D in showDirs) - var/icon/partial = getFlatIcon(body, defdir=D) + var/icon/partial = getFlatIcon(body, defdir=D, no_anim) out_icon.Insert(partial,dir=D) humanoid_icon_cache[icon_id] = out_icon diff --git a/code/modules/admin/verbs/selectequipment.dm b/code/modules/admin/verbs/selectequipment.dm index 4184fed68f..eb75df9ac1 100644 --- a/code/modules/admin/verbs/selectequipment.dm +++ b/code/modules/admin/verbs/selectequipment.dm @@ -111,7 +111,8 @@ var/icon/dummysprite = get_flat_human_icon(null, dummy_key = dummy_key, - outfit_override = selected_outfit) + outfit_override = selected_outfit, + no_anim = TRUE) data["icon64"] = icon2base64(dummysprite) data["name"] = target_mob From c2b7140ddd67147a6bcd088c46d35d1135e87112 Mon Sep 17 00:00:00 2001 From: SandPoot Date: Sat, 24 Apr 2021 21:43:12 -0300 Subject: [PATCH 3/4] upload icons.dm --- code/__HELPERS/icons.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm index c9b011d0e4..fabe70c929 100644 --- a/code/__HELPERS/icons.dm +++ b/code/__HELPERS/icons.dm @@ -1067,7 +1067,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types) var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing") COMPILE_OVERLAYS(body) for(var/D in showDirs) - var/icon/partial = getFlatIcon(body, defdir=D, no_anim) + var/icon/partial = getFlatIcon(body, defdir = D, no_anim = no_anim) out_icon.Insert(partial,dir=D) humanoid_icon_cache[icon_id] = out_icon From 44c2a9eecdc5b062a5a8de1161f820f8a2f096f1 Mon Sep 17 00:00:00 2001 From: CitadelStationBot Date: Sun, 25 Apr 2021 18:10:11 -0500 Subject: [PATCH 4/4] Automatic changelog generation for PR #14642 [ci skip] --- html/changelogs/AutoChangeLog-pr-14642.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 html/changelogs/AutoChangeLog-pr-14642.yml diff --git a/html/changelogs/AutoChangeLog-pr-14642.yml b/html/changelogs/AutoChangeLog-pr-14642.yml new file mode 100644 index 0000000000..675d5418d5 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-14642.yml @@ -0,0 +1,5 @@ +author: "Trigg, stylemistake and SandPoot" +delete-after: True +changes: + - admin: "Admins just got a new TGUI Select Equipment menu +tweak: Prevents the window from creating sprites for any animated version there might be. (this guarantees consistant sprite size/amount)"