Files
Bubberstation/code/modules/loadout/loadout_items.dm
Rimi Nosha 6d8f4d1805 [SEMI-MODULAR] Multiple Loadout Presets! (#2540)
## About The Pull Request

Tin. Also made some QoL tweaks while I was at it!

Comprehensively:
- Removes the defunct job checkbox and replaces it with a dropdown.
- Adds a preference update message, cause it's otherwise silent.
- Stops the TGUI prefs migration message from showing when changing to a
brand new character.
- Adds the ability to create (nameable) loadout presets.
- The default preset can't be renamed or deleted. This was to make my
life easier, or I'd spend another day on this, which I'm not prepared to
do.
- Automatically COPIES your old loadout to the new system's default
loadout. The old preference value is ENTIRELY UNCHANGED, which should
mean this is entirely safe. **Back up the server playersave data,
regardless, though.**
- Fixes a funny bug where the blacklisted and whitelisted roles text was
the same.

## There's a default limit of 12 entries (including default.) I'm happy
to bump this up at maintainer request.

## Why It's Good For The Game

This is a thing folk have always wanted since the dawn of loadouts, and
the new loadout system is *surprisingly* hackable, so I did it!

Now you can set up multiple outfits for whatever occasion you want!

## Proof Of Testing
<details>
<summary>Screenshots/Videos</summary>


![image](https://github.com/user-attachments/assets/6cddb219-0c8e-44f1-aefe-d3f36a261555)



https://github.com/user-attachments/assets/9d4bd6f3-0dc7-4aaa-a6ad-989fc9cb987a

The total allowed loadout count, added after the video, so here's a
screencap instead!

![image](https://github.com/user-attachments/assets/0ffd72a4-ed4c-4b6d-8d77-83c0545e1171)

Renaming


https://github.com/user-attachments/assets/236d99c2-1bcf-4836-b32c-4bb4dd6227e0

</details>

## Changelog
🆑
add: Loadout presets! Make multiple loadouts, be able to switch between
them in a couple of clicks!
qol: Replaced the defunct job checkbox on the loadout page with a
working dropdown.
/🆑
2024-12-07 20:53:57 +01:00

445 lines
16 KiB
Plaintext

/// Global list of ALL loadout datums instantiated.
/// Loadout datums are created by loadout categories.
GLOBAL_LIST_EMPTY(all_loadout_datums)
/// Global list of all loadout categories
/// Doesn't really NEED to be a global but we need to init this early for preferences,
/// as the categories instantiate all the loadout datums
GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories())
/// Inits the global list of loadout category singletons
/// Also inits loadout item singletons
/proc/init_loadout_categories()
var/list/loadout_categories = list()
for(var/category_type in subtypesof(/datum/loadout_category))
loadout_categories += new category_type()
sortTim(loadout_categories, /proc/cmp_loadout_categories)
return loadout_categories
/proc/cmp_loadout_categories(datum/loadout_category/A, datum/loadout_category/B)
var/a_order = A::tab_order
var/b_order = B::tab_order
if(a_order == b_order)
return cmp_text_asc(A::category_name, B::category_name)
return cmp_numeric_asc(a_order, b_order)
/**
* # Loadout item datum
*
* Singleton that holds all the information about each loadout items, and how to equip them.
*/
/datum/loadout_item
/// The category of the loadout item. Set automatically in New
VAR_FINAL/datum/loadout_category/category
/// Displayed name of the loadout item.
/// Defaults to the item's name if unset.
var/name
/// Whether this item has greyscale support.
/// Only works if the item is compatible with the GAGS system of coloring.
/// Set automatically to TRUE for all items that have the flag [IS_PLAYER_COLORABLE_1].
/// If you really want it to not be colorable set this to [DONT_GREYSCALE]
var/can_be_greyscale = FALSE
/// Whether this item can be renamed.
/// I recommend you apply this sparingly becuase it certainly can go wrong (or get reset / overridden easily)
var/can_be_named = TRUE // SKYRAT EDIT
/// Whether this item can be reskinned.
/// Only works if the item has a "unique reskin" list set.
var/can_be_reskinned = FALSE
/// The abstract parent of this loadout item, to determine which items to not instantiate
var/abstract_type = /datum/loadout_item
/// The actual item path of the loadout item.
var/obj/item/item_path
/// Lazylist of additional "information" text to display about this item.
var/list/additional_displayed_text
/// Icon file (DMI) for the UI to use for preview icons.
/// Set automatically if null
var/ui_icon
/// Icon state for the UI to use for preview icons.
/// Set automatically if null
var/ui_icon_state
/// Reskin options of this item if it can be reskinned.
VAR_FINAL/list/cached_reskin_options
// BUBBER EDIT ADDITION START
/// If set, it's a list containing ckeys which only can get the item
var/list/ckeywhitelist
/// If set, is a list of job names of which can get the loadout item
var/list/restricted_roles
/// If set, is a list of job names of which can't get the loadout item
var/list/blacklisted_roles
/// If set, is a list of species which can get the loadout item
var/list/restricted_species
/// Whether the item is restricted to supporters
var/donator_only
/// Whether the item requires a specific season in order to be available
var/required_season = null
/// If the item won't appear when the ERP config is disabled
var/erp_item = FALSE
// BUBBER EDIT END
/datum/loadout_item/New(category)
src.category = category
if(can_be_greyscale == DONT_GREYSCALE)
can_be_greyscale = FALSE
else if((item_path::flags_1 & IS_PLAYER_COLORABLE_1) && item_path::greyscale_config && item_path::greyscale_colors)
can_be_greyscale = TRUE
if(isnull(name))
name = item_path::name
if(isnull(ui_icon) && isnull(ui_icon_state))
ui_icon = item_path::icon_preview || item_path::icon
ui_icon_state = item_path::icon_state_preview || item_path::icon_state
if(can_be_reskinned)
var/obj/item/dummy_item = new item_path()
if(!length(dummy_item.unique_reskin))
can_be_reskinned = FALSE
stack_trace("Loadout item [item_path] has can_be_reskinned set to TRUE but has no unique reskins.")
else
cached_reskin_options = dummy_item.unique_reskin.Copy()
qdel(dummy_item)
// SKYRAT EDIT ADDITION
// Let's sanitize in case somebody inserted the player's byond name instead of ckey in canonical form
if(ckeywhitelist)
for (var/i = 1, i <= length(ckeywhitelist), i++)
ckeywhitelist[i] = ckey(ckeywhitelist[i])
// SKYRAT EDIT END
/datum/loadout_item/Destroy(force, ...)
if(force)
stack_trace("QDEL called on loadout item [type]. This shouldn't ever happen. (Use FORCE if necessary.)")
return QDEL_HINT_LETMELIVE
GLOB.all_loadout_datums -= item_path
return ..()
/**
* Takes in an action from a loadout manager and applies it
*
* Useful for subtypes of loadout items with unique actions
*
* Return TRUE to force an update to the UI / character preview
*/
/datum/loadout_item/proc/handle_loadout_action(datum/preference_middleware/loadout/manager, mob/user, action, params)
SHOULD_CALL_PARENT(TRUE)
switch(action)
if("select_color")
if(can_be_greyscale)
return set_item_color(manager, user)
if("set_name")
if(can_be_named)
// SKYRAT EDIT BEGIN - Description
return set_name(manager, user, INFO_NAMED)
if("set_desc")
if(can_be_named)
return set_name(manager, user, INFO_DESCRIBED)
// SKYRAT EDIT END
if("set_skin")
return set_skin(manager, user, params)
return TRUE
/// Opens up the GAGS editing menu.
/datum/loadout_item/proc/set_item_color(datum/preference_middleware/loadout/manager, mob/user)
if(manager.menu)
return FALSE
var/list/loadout = manager.get_current_loadout() // BUBBER EDIT: Multiple loadout presets: ORIGINAL: var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
var/list/allowed_configs = list()
if(initial(item_path.greyscale_config))
allowed_configs += "[initial(item_path.greyscale_config)]"
if(initial(item_path.greyscale_config_worn))
allowed_configs += "[initial(item_path.greyscale_config_worn)]"
if(initial(item_path.greyscale_config_inhand_left))
allowed_configs += "[initial(item_path.greyscale_config_inhand_left)]"
if(initial(item_path.greyscale_config_inhand_right))
allowed_configs += "[initial(item_path.greyscale_config_inhand_right)]"
var/datum/greyscale_modify_menu/menu = new(
manager,
user,
allowed_configs,
CALLBACK(src, PROC_REF(set_slot_greyscale), manager),
starting_icon_state = initial(item_path.icon_state),
starting_config = initial(item_path.greyscale_config),
starting_colors = loadout?[item_path]?[INFO_GREYSCALE] || initial(item_path.greyscale_colors),
)
manager.register_greyscale_menu(menu)
menu.ui_interact(user)
return TRUE
/// Callback for GAGS menu to set this item's color.
/datum/loadout_item/proc/set_slot_greyscale(datum/preference_middleware/loadout/manager, datum/greyscale_modify_menu/open_menu)
if(!istype(open_menu))
CRASH("set_slot_greyscale called without a greyscale menu!")
var/list/loadout = manager.get_current_loadout() // BUBBER EDIT: Multiple loadout presets: ORIGINAL: var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
if(!loadout?[item_path])
return FALSE
var/list/colors = open_menu.split_colors
if(!colors)
return FALSE
loadout[item_path][INFO_GREYSCALE] = colors.Join("")
manager.save_current_loadout(loadout) // BUBBER EDIT: Multiple loadout presets: ORIGINAL: manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
return TRUE // update UI
/// Sets the name of the item.
// SKYRAT EDIT BEGIN - Adds descriptions with minimal copypaste
// Generally, if this conflicts, name_slot is to be put anywhere where INFO_NAMED appears in tgcode
/datum/loadout_item/proc/set_name(datum/preference_middleware/loadout/manager, mob/user, name_slot = INFO_NAMED)
var/isname = (name_slot == INFO_NAMED)
var/list/loadout = manager.get_current_loadout() // BUBBER EDIT: Multiple loadout presets: ORIGINAL: var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
var/input_name = tgui_input_text(
user = user,
message = "What [isname ? "name" : "description"] do you want to give the [name]? Leave blank to clear.",
title = "[name] [isname ? "name" : "description"]",
default = loadout?[item_path]?[name_slot], // plop in existing name (if any)
max_length = isname ? MAX_NAME_LEN : MAX_DESC_LEN,
)
if(QDELETED(src) || QDELETED(user) || QDELETED(manager) || QDELETED(manager.preferences))
return FALSE
loadout = manager.get_current_loadout() // BUBBER EDIT: Multiple loadout presets: ORIGINAL: loadout = manager.preferences.read_preference(/datum/preference/loadout) // Make sure no shenanigans happened
if(!loadout?[item_path])
return FALSE
if(input_name)
loadout[item_path][name_slot] = input_name
else if(input_name == "")
loadout[item_path] -= name_slot
manager.save_current_loadout(loadout) // BUBBER EDIT: Multiple loadout presets: ORIGINAL: manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
return FALSE // no update needed
// SKYRAT EDIT END
/// Used for reskinning an item to an alt skin.
/datum/loadout_item/proc/set_skin(datum/preference_middleware/loadout/manager, mob/user, params)
if(!can_be_reskinned)
return FALSE
var/reskin_to = params["skin"]
if(!cached_reskin_options[reskin_to])
return FALSE
var/list/loadout = manager.get_current_loadout() // BUBBER EDIT: Multiple loadout presets: ORIGINAL: var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
if(!loadout?[item_path])
return FALSE
loadout[item_path][INFO_RESKIN] = reskin_to
manager.save_current_loadout(loadout) // BUBBER EDIT: Multiple loadout presets: ORIGINAL: manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
return TRUE // always update UI
/**
* Place our [item_path] into the passed [outfit].
*
* By default, just adds the item into the outfit's backpack contents, if non-visual.
*
* Arguments:
* * outfit - The outfit we're equipping our items into.
* * equipper - If we're equipping out outfit onto a mob at the time, this is the mob it is equipped on. Can be null.
* * visual - If TRUE, then our outfit is only for visual use (for example, a preview).
*/
/datum/loadout_item/proc/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE, loadout_placement_preference) // SKYRAT EDIT CHANGE - Added loadout_placement_preference
if(!visuals_only)
LAZYADD(outfit.backpack_contents, item_path)
/**
* Called When the item is equipped on [equipper].
*
* At this point the item is in the mob's contents
*
* Arguments:
* * preference_source - the datum/preferences our loadout item originated from - cannot be null
* * equipper - the mob we're equipping this item onto - cannot be null
* * visuals_only - whether or not this is only concerned with visual things (not backpack, not renaming, etc)
* * preference_list - what the raw loadout list looks like in the preferences
*
* Return a bitflag of slot flags to update
*/
/datum/loadout_item/proc/on_equip_item(
obj/item/equipped_item,
datum/preferences/preference_source,
list/preference_list,
mob/living/carbon/human/equipper,
visuals_only = FALSE,
)
ASSERT(!isnull(equipped_item))
if(!visuals_only)
ADD_TRAIT(equipped_item, TRAIT_ITEM_OBJECTIVE_BLOCKED, TRAIT_SOURCE_LOADOUT)
var/list/item_details = preference_list[item_path]
var/update_flag = NONE
if(can_be_greyscale && item_details?[INFO_GREYSCALE])
equipped_item.set_greyscale(item_details[INFO_GREYSCALE])
update_flag |= equipped_item.slot_flags
// SKYRAT EDIT BEGIN - DESCRIPTIONS~
if(can_be_named && !visuals_only)
var/renamed = 0
if(item_details?[INFO_NAMED])
equipped_item.name = trim(item_details[INFO_NAMED], PREVENT_CHARACTER_TRIM_LOSS(MAX_NAME_LEN))
renamed = 1
if(item_details?[INFO_DESCRIBED])
equipped_item.desc = trim(item_details[INFO_DESCRIBED], PREVENT_CHARACTER_TRIM_LOSS(MAX_DESC_LEN))
renamed = 1
if(renamed)
ADD_TRAIT(equipped_item, TRAIT_WAS_RENAMED, TRAIT_SOURCE_LOADOUT)
equipped_item.AddElement(/datum/element/examined_when_worn)
SEND_SIGNAL(equipped_item, COMSIG_NAME_CHANGED)
// SKYRAT EDIT END
if(can_be_reskinned && item_details?[INFO_RESKIN])
var/skin_chosen = item_details[INFO_RESKIN]
if(skin_chosen in equipped_item.unique_reskin)
equipped_item.current_skin = skin_chosen
equipped_item.icon_state = equipped_item.unique_reskin[skin_chosen]
if(istype(equipped_item, /obj/item/clothing/accessory))
// Snowflake handing for accessories, because we need to update the thing it's attached to instead
if(isclothing(equipped_item.loc))
var/obj/item/clothing/under/attached_to = equipped_item.loc
attached_to.update_accessory_overlay()
update_flag |= (ITEM_SLOT_OCLOTHING|ITEM_SLOT_ICLOTHING)
else
update_flag |= equipped_item.slot_flags
else
// Not valid, update the preference
item_details -= INFO_RESKIN
preference_source.write_preference(GLOB.preference_entries[/datum/preference/loadout], preference_list)
return update_flag
/**
* Returns a formatted list of data for this loadout item.
*/
/datum/loadout_item/proc/to_ui_data() as /list
SHOULD_CALL_PARENT(TRUE)
var/list/formatted_item = list()
formatted_item["name"] = name
formatted_item["path"] = item_path
formatted_item["information"] = get_item_information()
formatted_item["buttons"] = get_ui_buttons()
formatted_item["reskins"] = get_reskin_options()
formatted_item["icon"] = ui_icon
formatted_item["icon_state"] = ui_icon_state
// SKYRAT EDIT BEGIN - Extra loadout stuff
formatted_item["ckey_whitelist"] = ckeywhitelist
formatted_item["donator_only"] = donator_only
formatted_item["restricted_roles"] = restricted_roles
formatted_item["blacklisted_roles"] = blacklisted_roles
// SKYRAT EDIT END
return formatted_item
/**
* Returns a list of information to display about this item in the loadout UI.
*
* These should be short strings, sub 14 characters generally.
*/
/datum/loadout_item/proc/get_item_information() as /list
SHOULD_CALL_PARENT(TRUE)
var/list/displayed_text = list()
displayed_text += (additional_displayed_text || list())
if(can_be_greyscale)
displayed_text += "Recolorable"
if(can_be_named)
displayed_text += "Renamable"
if(can_be_reskinned)
displayed_text += "Reskinnable"
// SKYRAT EDIT ADDITION
if(donator_only)
displayed_text += "Donator only"
if(ckeywhitelist)
displayed_text += "Unique"
if(restricted_roles || blacklisted_roles)
displayed_text += "Role restricted"
if(restricted_species)
displayed_text += "Species restricted"
// SKYRAT EDIT ADDITION
return displayed_text
/**
* Returns a list of buttons that are shown in the loadout UI for customizing this item.
*
* Buttons contain
* - 'L'abel: The text displayed beside the button
* - act_key: The key that is sent to the loadout manager when the button is clicked,
* for use in handle_loadout_action
* - button_icon: The FontAwesome icon to display on the button
* - active_key: In the loadout UI, this key is checked in the user's loadout list for this item
* to determine if the button is 'active' (green) or not (blue).
* - active_text: Optional, if provided, the button appears to be a checkbox and this text is shown when 'active'
* - inactive_text: Optional, if provided, the button appears to be a checkbox and this text is shown when not 'active'
*/
/datum/loadout_item/proc/get_ui_buttons() as /list
SHOULD_CALL_PARENT(TRUE)
var/list/button_list = list()
if(can_be_greyscale)
UNTYPED_LIST_ADD(button_list, list(
"label" = "Recolor",
"act_key" = "select_color",
"button_icon" = FA_ICON_PALETTE,
"active_key" = INFO_GREYSCALE,
))
if(can_be_named)
UNTYPED_LIST_ADD(button_list, list(
"label" = "Rename",
"act_key" = "set_name",
"button_icon" = FA_ICON_PEN,
"active_key" = INFO_NAMED,
))
// SKYRAT EDIT BEGIN - Descriptions
UNTYPED_LIST_ADD(button_list, list(
"label" = "Change description",
"act_key" = "set_desc",
"button_icon" = FA_ICON_PEN,
"active_key" = INFO_NAMED,
))
// SKYRAT EDIT END
return button_list
/**
* Returns a list of options this item can be reskinned into.
*/
/datum/loadout_item/proc/get_reskin_options() as /list
if(!can_be_reskinned)
return null
var/list/reskins = list()
for(var/skin in cached_reskin_options)
UNTYPED_LIST_ADD(reskins, list(
"name" = skin,
"tooltip" = skin,
"skin_icon_state" = cached_reskin_options[skin],
))
return reskins