Adds Character Loadout Tab to preferences (with just a small handful of items to start) (#83521)

## About The Pull Request

Adds a Character Loadout Tab to the preference menu

This tab lets you pick items to start the round with


![image](https://private-user-images.githubusercontent.com/51863163/336254447-c5f7eefa-c44c-418d-b48e-0409bb5bb982.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTgwNDAxMjMsIm5iZiI6MTcxODAzOTgyMywicGF0aCI6Ii81MTg2MzE2My8zMzYyNTQ0NDctYzVmN2VlZmEtYzQ0Yy00MThkLWI0OGUtMDQwOWJiNWJiOTgyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA2MTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNjEwVDE3MTcwM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWYxYWFmYjI2NDU0YjUyODg3NjBmM2VjZDg4YWQ1YjlhMThmODU3MDYyMzYwOGVmYTcxYmY2MDhjZWVmYjQ5ZTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.Y0_19Gisfp4yyUmLgW2atfKyneL7POWFRKNVgNWTbEs)

This also has some additional mechanics, such as being able to recolor
colorable items, rename certain items (such as plushies), set item skins
(such as the pride pin)


![image](https://github.com/tgstation/tgstation/assets/51863163/8a085d6c-a294-4538-95d2-ada902ab69b4)

## Why It's Good For The Game

This has been headcoder sanctioned for some time, just no one did it. So
here we are.

Allows players to add some additional customization to their characters.
Keeps us from cluttering the quirks list with quirks that do nothing but
grants items.

## Changelog

🆑 Melbert
add: Character Loadouts
del: Pride Pin quirk (it's in the Loadout menu now)
/🆑
This commit is contained in:
MrMelbert
2024-06-11 19:50:12 -05:00
committed by GitHub
parent 1a13b3a95a
commit d244c86ce6
44 changed files with 2192 additions and 99 deletions

View File

@@ -142,3 +142,17 @@
/// The key used for sprite accessories that should never actually be applied to the player.
#define SPRITE_ACCESSORY_NONE "None"
// Loadout
/// Used to make something not recolorable even if it's capable
#define DONT_GREYSCALE -1
// Loadout item info keys
// Changing these will break existing loadouts
/// Tracks GAGS color information
#define INFO_GREYSCALE "greyscale"
/// Used to set custom names
#define INFO_NAMED "name"
/// Used for specific alt-reskins, like the pride pin
#define INFO_RESKIN "reskin"
/// Handles which layer the item will be on, for accessories
#define INFO_LAYER "layer"

View File

@@ -1151,4 +1151,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Does this item bypass ranged armor checks?
#define TRAIT_BYPASS_RANGED_ARMOR "bypass_ranged_armor"
/// This item cannot be selected for or used by a theft objective (Spies, Traitors, etc.)
#define TRAIT_ITEM_OBJECTIVE_BLOCKED "item_objective_blocked"
// END TRAIT DEFINES

View File

@@ -522,6 +522,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_HAUNTED" = TRAIT_HAUNTED,
"TRAIT_HONKSPAMMING" = TRAIT_HONKSPAMMING,
"TRAIT_INNATELY_FANTASTICAL_ITEM" = TRAIT_INNATELY_FANTASTICAL_ITEM,
"TRAIT_ITEM_OBJECTIVE_BLOCKED" = TRAIT_ITEM_OBJECTIVE_BLOCKED,
"TRAIT_NEEDS_TWO_HANDS" = TRAIT_NEEDS_TWO_HANDS,
"TRAIT_NO_BARCODES" = TRAIT_NO_BARCODES,
"TRAIT_NO_STORAGE_INSERT" = TRAIT_NO_STORAGE_INSERT,

View File

@@ -557,7 +557,7 @@ SUBSYSTEM_DEF(job)
SEND_SIGNAL(equipping, COMSIG_JOB_RECEIVED, job)
equipping.mind?.set_assigned_role_with_greeting(job, player_client)
equipping.on_job_equipping(job)
equipping.on_job_equipping(job, player_client)
job.announce_job(equipping)
if(player_client?.holder)

View File

@@ -10,7 +10,7 @@ GLOBAL_LIST_INIT_TYPED(quirk_blacklist, /list/datum/quirk, list(
list(/datum/quirk/no_taste, /datum/quirk/vegetarian, /datum/quirk/deviant_tastes, /datum/quirk/gamer),
list(/datum/quirk/pineapple_liker, /datum/quirk/pineapple_hater, /datum/quirk/gamer),
list(/datum/quirk/alcohol_tolerance, /datum/quirk/light_drinker),
list(/datum/quirk/item_quirk/clown_enjoyer, /datum/quirk/item_quirk/mime_fan, /datum/quirk/item_quirk/pride_pin),
list(/datum/quirk/item_quirk/clown_enjoyer, /datum/quirk/item_quirk/mime_fan),
list(/datum/quirk/bad_touch, /datum/quirk/friendly),
list(/datum/quirk/extrovert, /datum/quirk/introvert),
list(/datum/quirk/prosthetic_limb, /datum/quirk/quadruple_amputee, /datum/quirk/transhumanist, /datum/quirk/body_purist),

View File

@@ -1,19 +0,0 @@
/datum/quirk/item_quirk/pride_pin
name = "Pride Pin"
desc = "Show off your pride with this changing pride pin!"
icon = FA_ICON_RAINBOW
value = 0
gain_text = span_notice("You feel fruity.")
lose_text = span_danger("You feel only slightly less fruity than before.")
medical_record_text = "Patient appears to be fruity."
/datum/quirk/item_quirk/pride_pin/add_unique(client/client_source)
var/obj/item/clothing/accessory/pride/pin = new(get_turf(quirk_holder))
var/pride_choice = client_source?.prefs?.read_preference(/datum/preference/choiced/pride_pin) || assoc_to_keys(GLOB.pride_pin_reskins)[1]
var/pride_reskin = GLOB.pride_pin_reskins[pride_choice]
pin.current_skin = pride_choice
pin.icon_state = pride_reskin
give_item_to_holder(pin, list(LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS))

View File

@@ -688,6 +688,9 @@ GLOBAL_LIST_EMPTY(possible_items)
var/list/all_items = M.current.get_all_contents() //this should get things in cheesewheels, books, etc.
for(var/obj/I in all_items) //Check for items
if(HAS_TRAIT(I, TRAIT_ITEM_OBJECTIVE_BLOCKED))
continue
if(istype(I, steal_target))
if(!targetinfo) //If there's no targetinfo, then that means it was a custom objective. At this point, we know you have the item, so return 1.
return TRUE

View File

@@ -5,7 +5,7 @@
//Contains the target item datums for Steal objectives.
/datum/objective_item
/// How the item is described in the objective
var/name = "A silly bike horn! Honk!"
var/name = "a silly bike horn! Honk!"
/// Typepath of item
var/targetitem = /obj/item/bikehorn
/// Valid containers that the target item can be in.
@@ -566,7 +566,7 @@
// A number of special early-game steal objectives intended to be used with the steal-and-destroy objective.
// They're basically items of utility or emotional value that may be found on many players or lying around the station.
/datum/objective_item/steal/traitor/insuls
name = "insulated gloves"
name = "some insulated gloves"
targetitem = /obj/item/clothing/gloves/color/yellow
excludefromjob = list(JOB_CARGO_TECHNICIAN, JOB_QUARTERMASTER, JOB_ATMOSPHERIC_TECHNICIAN, JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER)
item_owner = list(JOB_STATION_ENGINEER, JOB_CHIEF_ENGINEER)
@@ -578,7 +578,7 @@
return add_item_to_steal(src, /obj/item/clothing/gloves/color/yellow)
/datum/objective_item/steal/traitor/moth_plush
name = "cute moth plush toy"
name = "a cute moth plush toy"
targetitem = /obj/item/toy/plush/moth
excludefromjob = list(JOB_PSYCHOLOGIST, JOB_PARAMEDIC, JOB_CHEMIST, JOB_MEDICAL_DOCTOR, JOB_CHIEF_MEDICAL_OFFICER, JOB_CORONER)
exists_on_map = TRUE
@@ -589,7 +589,7 @@
return add_item_to_steal(src, /obj/item/toy/plush/moth)
/datum/objective_item/steal/traitor/lizard_plush
name = "cute lizard plush toy"
name = "a cute lizard plush toy"
targetitem = /obj/item/toy/plush/lizard_plushie
exists_on_map = TRUE
difficulty = 1
@@ -633,7 +633,7 @@
return add_item_to_steal(src, /obj/item/book/manual/wiki/security_space_law)
/datum/objective_item/steal/traitor/rpd
name = "rapid pipe dispenser"
name = "a rapid pipe dispenser"
targetitem = /obj/item/pipe_dispenser
excludefromjob = list(
JOB_ATMOSPHERIC_TECHNICIAN,
@@ -679,7 +679,7 @@
objective_type = OBJECTIVE_ITEM_TYPE_SPY
/datum/objective_item/steal/spy/lamarr
name = "The Research Director's pet headcrab"
name = "the Research Director's pet headcrab"
targetitem = /obj/item/clothing/mask/facehugger/lamarr
excludefromjob = list(JOB_RESEARCH_DIRECTOR)
exists_on_map = TRUE
@@ -809,7 +809,7 @@
return add_item_to_steal(src, /obj/item/stamp/head)
/datum/objective_item/steal/spy/sunglasses
name = "sunglasses"
name = "some sunglasses"
targetitem = /obj/item/clothing/glasses/sunglasses
excludefromjob = list(
JOB_CAPTAIN,
@@ -828,7 +828,7 @@
You can also obtain a pair from dissassembling hudglasses."
/datum/objective_item/steal/spy/ce_modsuit
name = "the cheif engineer's advanced MOD control unit"
name = "the chief engineer's advanced MOD control unit"
targetitem = /obj/item/mod/control/pre_equipped/advanced
excludefromjob = list(JOB_CHIEF_ENGINEER)
exists_on_map = TRUE

View File

@@ -531,6 +531,11 @@
desc = "An adorable stuffed toy that resembles a green lizardperson. This one fills you with nostalgia and soul."
greyscale_colors = "#66ff33#000000"
/obj/item/toy/plush/lizard_plushie/greyscale
desc = "An adorable stuffed toy that resembles a lizardperson. This one has been custom made."
greyscale_colors = "#d3d3d3#000000"
flags_1 = IS_PLAYER_COLORABLE_1
/obj/item/toy/plush/lizard_plushie/space
name = "space lizard plushie"
desc = "An adorable stuffed toy that resembles a very determined spacefaring lizardperson. To infinity and beyond, little guy."

View File

@@ -209,7 +209,11 @@ ADMIN_VERB(create_mapping_job_icons, R_DEBUG, "Generate job landmarks icons", "G
for(var/obj/item/I in D)
qdel(I)
randomize_human(D)
D.dress_up_as_job(JB, TRUE)
D.dress_up_as_job(
equipping = JB,
visual_only = TRUE,
consistent = TRUE,
)
var/icon/I = icon(getFlatIcon(D), frame = 1)
final.Insert(I, JB.title)
qdel(D)

View File

@@ -219,6 +219,8 @@
continue
if(!is_station_level(thing_turf.z) && !is_mining_level(thing_turf.z))
continue
if(HAS_TRAIT(existing_thing, TRAIT_ITEM_OBJECTIVE_BLOCKED))
continue
all_valid_existing_things += existing_thing
if(!length(all_valid_existing_things))
@@ -253,16 +255,14 @@
return FALSE
desired_item = pick(valid_possible_items)
// We need to do some snowflake for items that do exist vs generic items
var/list/obj/item/existing_items = GLOB.steal_item_handler.objectives_by_path[desired_item.targetitem]
var/obj/item/the_item = length(existing_items) ? pick(existing_items) : desired_item.targetitem
var/the_item_name = istype(the_item) ? the_item.name : initial(the_item.name)
name = "[the_item_name] [difficulty == SPY_DIFFICULTY_HARD ? "Grand ":""]Theft"
help = "Steal any [the_item_name][desired_item.steal_hint ? ": [desired_item.steal_hint]" : "."]"
name = "[desired_item.name] [difficulty == SPY_DIFFICULTY_HARD ? "Grand ":""]Theft"
help = "Steal [desired_item.name][desired_item.steal_hint ? ": [desired_item.steal_hint]" : "."]"
return TRUE
/datum/spy_bounty/objective_item/is_stealable(atom/movable/stealing)
return istype(stealing, desired_item.targetitem) && desired_item.check_special_completion(stealing)
return istype(stealing, desired_item.targetitem) \
&& !HAS_TRAIT(stealing, TRAIT_ITEM_OBJECTIVE_BLOCKED) \
&& desired_item.check_special_completion(stealing)
/datum/spy_bounty/objective_item/random_easy
difficulty = SPY_DIFFICULTY_EASY

View File

@@ -29,6 +29,8 @@ GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new())
/datum/objective_item_handler/proc/new_item_created(datum/source, obj/item/item)
SIGNAL_HANDLER
if(HAS_TRAIT(item, TRAIT_ITEM_OBJECTIVE_BLOCKED))
return
if(!generated_items)
item.add_stealing_item_objective()
return
@@ -224,6 +226,8 @@ GLOBAL_DATUM_INIT(steal_item_handler, /datum/objective_item_handler, new())
/datum/traitor_objective/steal_item/proc/handle_special_case(obj/item/source, obj/item/target)
SIGNAL_HANDLER
if(HAS_TRAIT(target, TRAIT_ITEM_OBJECTIVE_BLOCKED))
return COMPONENT_FORCE_FAIL_PLACEMENT
if(istype(target, target_item.targetitem))
if(!target_item.check_special_completion(target))
return COMPONENT_FORCE_FAIL_PLACEMENT

View File

@@ -225,8 +225,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
return TRUE
if ("rotate")
character_preview_view.dir = turn(character_preview_view.dir, -90)
character_preview_view.setDir(turn(character_preview_view.dir, -90))
return TRUE
if ("set_preference")
var/requested_preference_key = params["preference"]
@@ -351,6 +350,8 @@ GLOBAL_LIST_EMPTY(preferences_datums)
var/mob/living/carbon/human/dummy/body
/// The preferences this refers to
var/datum/preferences/preferences
/// Whether we show current job clothes or nude/loadout only
var/show_job_clothes = TRUE
/atom/movable/screen/map_view/char_preview/Initialize(mapload, datum/preferences/preferences)
. = ..()
@@ -368,16 +369,14 @@ GLOBAL_LIST_EMPTY(preferences_datums)
create_body()
else
body.wipe_state()
appearance = preferences.render_new_preview_appearance(body)
appearance = preferences.render_new_preview_appearance(body, show_job_clothes)
/atom/movable/screen/map_view/char_preview/proc/create_body()
QDEL_NULL(body)
body = new
// Without this, it doesn't show up in the menu
body.appearance_flags &= ~KEEP_TOGETHER
/datum/preferences/proc/create_character_profiles()
var/list/profiles = list()

View File

@@ -18,11 +18,16 @@
/// support the "use gender" option.
#define PREFERENCE_PRIORITY_BODY_TYPE 5
/// Equpping items based on preferences.
/// Should happen after species and body type to make sure it looks right.
/// Mostly redundant, but a safety net for saving/loading.
#define PREFERENCE_PRIORITY_LOADOUT 6
/// The priority at which names are decided, needed for proper randomization.
#define PREFERENCE_PRIORITY_NAMES 6
#define PREFERENCE_PRIORITY_NAMES 7
/// Preferences that aren't names, but change the name changes set by PREFERENCE_PRIORITY_NAMES.
#define PREFERENCE_PRIORITY_NAME_MODIFICATIONS 7
#define PREFERENCE_PRIORITY_NAME_MODIFICATIONS 8
/// The maximum preference priority, keep this updated, but don't use it for `priority`.
#define MAX_PREFERENCE_PRIORITY PREFERENCE_PRIORITY_NAME_MODIFICATIONS

View File

@@ -0,0 +1,27 @@
/**
* Move quirk items into loadout items
*
* If this is accompanied with removal of a quirk,
* you don't need to worry about handling that here -
* quirk sanitization happens AFTER migration
*/
/datum/preferences/proc/migrate_quirk_to_loadout(quirk_to_migrate, new_typepath, list/data_to_migrate)
ASSERT(istext(quirk_to_migrate) && ispath(new_typepath, /obj/item))
if(quirk_to_migrate in all_quirks)
add_loadout_item(new_typepath, data_to_migrate)
/// Helper for slotting in a new loadout item
/datum/preferences/proc/add_loadout_item(typepath, list/data = list())
PRIVATE_PROC(TRUE)
var/list/loadout_list = read_preference(/datum/preference/loadout) || list()
loadout_list[typepath] = data
write_preference(GLOB.preference_entries[/datum/preference/loadout], loadout_list)
/// Helper for removing a loadout item
/datum/preferences/proc/remove_loadout_item(typepath)
PRIVATE_PROC(TRUE)
var/list/loadout_list = read_preference(/datum/preference/loadout)
if(loadout_list?.Remove(typepath))
write_preference(GLOB.preference_entries[/datum/preference/loadout], loadout_list)

View File

@@ -1,16 +0,0 @@
/datum/preference/choiced/pride_pin
category = PREFERENCE_CATEGORY_SECONDARY_FEATURES
savefile_key = "pride_pin"
savefile_identifier = PREFERENCE_CHARACTER
/datum/preference/choiced/pride_pin/init_possible_values()
return assoc_to_keys(GLOB.pride_pin_reskins)
/datum/preference/choiced/pride_pin/is_accessible(datum/preferences/preferences)
if (!..(preferences))
return FALSE
return "Pride Pin" in preferences.all_quirks
/datum/preference/choiced/pride_pin/apply_to_human(mob/living/carbon/human/target, value)
return

View File

@@ -5,7 +5,7 @@
// You do not need to raise this if you are adding new values that have sane defaults.
// Only raise this value when changing the meaning/format/name/layout of an existing value
// where you would want the updater procs below to run
#define SAVEFILE_VERSION_MAX 44
#define SAVEFILE_VERSION_MAX 45
/*
SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Carn
@@ -104,6 +104,13 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
if (current_version < 43)
migrate_legacy_sound_toggles(savefile)
if (current_version < 45)
migrate_quirk_to_loadout(
quirk_to_migrate = "Pride Pin",
new_typepath = /obj/item/clothing/accessory/pride,
data_to_migrate = list(INFO_RESKIN = save_data?["pride_pin"]),
)
/// checks through keybindings for outdated unbound keys and updates them
/datum/preferences/proc/check_keybindings()
if(!parent)

View File

@@ -84,6 +84,8 @@
name = "flat cap"
desc = "A working man's cap."
icon_state = "beret_flat"
icon_preview = 'icons/obj/clothing/head/beret.dmi'
icon_state_preview = "beret_flat"
greyscale_config = /datum/greyscale_config/beret
greyscale_config_worn = /datum/greyscale_config/beret/worn
greyscale_colors = "#8F7654"

View File

@@ -385,6 +385,8 @@
name = "beret"
desc = "A beret, a mime's favorite headwear."
icon_state = "beret"
icon_preview = 'icons/obj/clothing/head/beret.dmi'
icon_state_preview = "beret"
dog_fashion = /datum/dog_fashion/head/beret
greyscale_config = /datum/greyscale_config/beret
greyscale_config_worn = /datum/greyscale_config/beret/worn

View File

@@ -398,6 +398,8 @@
/obj/item/clothing/neck/large_scarf
name = "large scarf"
icon_state = "large_scarf"
icon_preview = 'icons/obj/fluff/previews.dmi'
icon_state_preview = "scarf_large"
w_class = WEIGHT_CLASS_TINY
custom_price = PAYCHECK_CREW
greyscale_colors = "#C6C6C6#EEEEEE"

View File

@@ -175,8 +175,12 @@
name = "Pre-Approved Cyborg Candidate dogtag"
display = "This employee has been screened for negative mental traits to an acceptable level of accuracy, and is approved for the NT Cyborg program as an alternative to medical resuscitation."
/// Reskins for the pride pin accessory, mapped by display name to icon state
GLOBAL_LIST_INIT(pride_pin_reskins, list(
/obj/item/clothing/accessory/pride
name = "pride pin"
desc = "A Nanotrasen Diversity & Inclusion Center-sponsored holographic pin to show off your pride, reminding the crew of their unwavering commitment to equity, diversity, and inclusion!"
icon_state = "pride"
obj_flags = UNIQUE_RENAME | INFINITE_RESKIN
unique_reskin = list(
"Rainbow Pride" = "pride",
"Bisexual Pride" = "pride_bi",
"Pansexual Pride" = "pride_pan",
@@ -185,16 +189,9 @@ GLOBAL_LIST_INIT(pride_pin_reskins, list(
"Transgender Pride" = "pride_trans",
"Intersex Pride" = "pride_intersex",
"Lesbian Pride" = "pride_lesbian",
))
/obj/item/clothing/accessory/pride
name = "pride pin"
desc = "A Nanotrasen Diversity & Inclusion Center-sponsored holographic pin to show off your pride, reminding the crew of their unwavering commitment to equity, diversity, and inclusion!"
icon_state = "pride"
obj_flags = UNIQUE_RENAME | INFINITE_RESKIN
)
/obj/item/clothing/accessory/pride/setup_reskinning()
unique_reskin = GLOB.pride_pin_reskins
if(!check_setup_reskinning())
return

View File

@@ -173,7 +173,7 @@
spawned_human.mind.adjust_experience(i, roundstart_experience[i], TRUE)
/// Return the outfit to use
/datum/job/proc/get_outfit()
/datum/job/proc/get_outfit(consistent)
return outfit
/// Announce that this job as joined the round to all crew members.
@@ -188,12 +188,12 @@
return TRUE
/mob/living/proc/on_job_equipping(datum/job/equipping)
/mob/living/proc/on_job_equipping(datum/job/equipping, client/player_client)
return
#define VERY_LATE_ARRIVAL_TOAST_PROB 20
/mob/living/carbon/human/on_job_equipping(datum/job/equipping)
/mob/living/carbon/human/on_job_equipping(datum/job/equipping, client/player_client)
if(equipping.paycheck_department)
var/datum/bank_account/bank_account = new(real_name, equipping, dna.species.payday_modifier)
bank_account.payday(STARTING_PAYCHECKS, TRUE)
@@ -201,19 +201,24 @@
bank_account.replaceable = FALSE
add_mob_memory(/datum/memory/key/account, remembered_id = account_id)
dress_up_as_job(equipping)
dress_up_as_job(
equipping = equipping,
visual_only = FALSE,
player_client = player_client,
consistent = FALSE,
)
if(EMERGENCY_PAST_POINT_OF_NO_RETURN && prob(VERY_LATE_ARRIVAL_TOAST_PROB))
equip_to_slot_or_del(new /obj/item/food/griddle_toast(src), ITEM_SLOT_MASK)
#undef VERY_LATE_ARRIVAL_TOAST_PROB
/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE)
/mob/living/proc/dress_up_as_job(datum/job/equipping, visual_only = FALSE, client/player_client, consistent = FALSE)
return
/mob/living/carbon/human/dress_up_as_job(datum/job/equipping, visual_only = FALSE)
/mob/living/carbon/human/dress_up_as_job(datum/job/equipping, visual_only = FALSE, client/player_client, consistent = FALSE)
dna.species.pre_equip_species_outfit(equipping, src, visual_only)
equipOutfit(equipping.get_outfit(), visual_only)
equip_outfit_and_loadout(equipping.get_outfit(consistent), player_client?.prefs, visual_only)
/datum/job/proc/announce_head(mob/living/carbon/human/H, channels) //tells the given channel that the given mob is the new department head. See communications.dm for valid channels.
if(H && GLOB.announcement_systems.len)

View File

@@ -37,7 +37,9 @@ Assistant
rpg_title = "Lout"
config_tag = "ASSISTANT"
/datum/job/assistant/get_outfit()
/datum/job/assistant/get_outfit(consistent)
if(consistent)
return /datum/outfit/job/assistant/consistent
if(!HAS_TRAIT(SSstation, STATION_TRAIT_ASSISTANT_GIMMICKS))
return ..()

View File

@@ -0,0 +1,92 @@
/// Accessory Items (Moves overrided items to backpack)
/datum/loadout_category/accessories
category_name = "Accessory"
category_ui_icon = FA_ICON_VEST
type_to_generate = /datum/loadout_item/accessory
tab_order = /datum/loadout_category/head::tab_order + 3
/datum/loadout_item/accessory
abstract_type = /datum/loadout_item/accessory
/// Can we adjust this accessory to be above or below suits?
VAR_FINAL/can_be_layer_adjusted = FALSE
/datum/loadout_item/accessory/New()
. = ..()
if(ispath(item_path, /obj/item/clothing/accessory))
can_be_layer_adjusted = TRUE
/datum/loadout_item/accessory/get_ui_buttons()
if(!can_be_layer_adjusted)
return ..()
var/list/buttons = ..()
UNTYPED_LIST_ADD(buttons, list(
"label" = "Layer",
"act_key" = "set_layer",
"active_key" = INFO_LAYER,
"active_text" = "Above Suit",
"inactive_text" = "Below Suit",
))
return buttons
/datum/loadout_item/accessory/handle_loadout_action(datum/preference_middleware/loadout/manager, mob/user, action, params)
if(action == "set_layer")
return set_accessory_layer(manager, user)
return ..()
/datum/loadout_item/accessory/proc/set_accessory_layer(datum/preference_middleware/loadout/manager, mob/user)
if(!can_be_layer_adjusted)
return FALSE
var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
if(!loadout?[item_path])
return FALSE
if(isnull(loadout[item_path][INFO_LAYER]))
loadout[item_path][INFO_LAYER] = FALSE
loadout[item_path][INFO_LAYER] = !loadout[item_path][INFO_LAYER]
manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
return TRUE // Update UI
/datum/loadout_item/accessory/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
if(outfit.accessory)
LAZYADD(outfit.backpack_contents, outfit.accessory)
outfit.accessory = item_path
/datum/loadout_item/accessory/on_equip_item(
obj/item/clothing/accessory/equipped_item,
datum/preferences/preference_source,
list/preference_list,
mob/living/carbon/human/equipper,
visuals_only = FALSE,
)
. = ..()
if(istype(equipped_item))
equipped_item.above_suit = !!preference_list[item_path]?[INFO_LAYER]
. |= (ITEM_SLOT_OCLOTHING|ITEM_SLOT_ICLOTHING)
/datum/loadout_item/accessory/maid_apron
name = "Maid Apron"
item_path = /obj/item/clothing/accessory/maidapron
/datum/loadout_item/accessory/waistcoat
name = "Waistcoat"
item_path = /obj/item/clothing/accessory/waistcoat
/datum/loadout_item/accessory/pocket_protector
name = "Pocket Protector"
item_path = /obj/item/clothing/accessory/pocketprotector
/datum/loadout_item/accessory/full_pocket_protector
name = "Pocket Protector (Filled)"
item_path = /obj/item/clothing/accessory/pocketprotector/full
additional_displayed_text = list("Contains pens")
/datum/loadout_item/accessory/pride
name = "Pride Pin"
item_path = /obj/item/clothing/accessory/pride
can_be_reskinned = TRUE

View File

@@ -0,0 +1,55 @@
/// Glasses Slot Items (Moves overrided items to backpack)
/datum/loadout_category/glasses
category_name = "Glasses"
category_ui_icon = FA_ICON_GLASSES
type_to_generate = /datum/loadout_item/glasses
tab_order = /datum/loadout_category/head::tab_order + 1
/datum/loadout_item/glasses
abstract_type = /datum/loadout_item/glasses
/datum/loadout_item/glasses/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
if(outfit.glasses)
LAZYADD(outfit.backpack_contents, outfit.glasses)
outfit.glasses = item_path
/datum/loadout_item/glasses/prescription_glasses
name = "Glasses"
item_path = /obj/item/clothing/glasses/regular
additional_displayed_text = list("Prescription")
/datum/loadout_item/glasses/prescription_glasses/circle_glasses
name = "Circle Glasses"
item_path = /obj/item/clothing/glasses/regular/circle
/datum/loadout_item/glasses/prescription_glasses/hipster_glasses
name = "Hipster Glasses"
item_path = /obj/item/clothing/glasses/regular/hipster
/datum/loadout_item/glasses/prescription_glasses/jamjar_glasses
name = "Jamjar Glasses"
item_path = /obj/item/clothing/glasses/regular/jamjar
/datum/loadout_item/glasses/black_blindfold
name = "Black Blindfold"
item_path = /obj/item/clothing/glasses/blindfold
/datum/loadout_item/glasses/cold_glasses
name = "Cold Glasses"
item_path = /obj/item/clothing/glasses/cold
/datum/loadout_item/glasses/heat_glasses
name = "Heat Glasses"
item_path = /obj/item/clothing/glasses/heat
/datum/loadout_item/glasses/orange_glasses
name = "Orange Glasses"
item_path = /obj/item/clothing/glasses/orange
/datum/loadout_item/glasses/red_glasses
name = "Red Glasses"
item_path = /obj/item/clothing/glasses/red
/datum/loadout_item/glasses/eyepatch
name = "Eyepatch"
item_path = /obj/item/clothing/glasses/eyepatch

View File

@@ -0,0 +1,138 @@
/// Head Slot Items (Deletes overrided items)
/datum/loadout_category/head
category_name = "Head"
category_ui_icon = FA_ICON_HAT_COWBOY
type_to_generate = /datum/loadout_item/head
tab_order = 1
/datum/loadout_item/head
abstract_type = /datum/loadout_item/head
/datum/loadout_item/head/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
if(equipper.dna?.species?.outfit_important_for_life)
if(!visuals_only)
to_chat(equipper, "Your loadout helmet was not equipped directly due to your species outfit.")
LAZYADD(outfit.backpack_contents, item_path)
else
outfit.head = item_path
/datum/loadout_item/head/beanie
name = "Beanie (Colorable)"
item_path = /obj/item/clothing/head/beanie
/datum/loadout_item/head/fancy_cap
name = "Fancy Hat (Colorable)"
item_path = /obj/item/clothing/head/costume/fancy
/datum/loadout_item/head/red_beret
name = "Red Beret (Colorable)"
item_path = /obj/item/clothing/head/beret
/datum/loadout_item/head/black_cap
name = "Cap (Black)"
item_path = /obj/item/clothing/head/soft/black
/datum/loadout_item/head/blue_cap
name = "Cap (Blue)"
item_path = /obj/item/clothing/head/soft/blue
/datum/loadout_item/head/delinquent_cap
name = "Cap (Delinquent)"
item_path = /obj/item/clothing/head/costume/delinquent
/datum/loadout_item/head/green_cap
name = "Cap (Green)"
item_path = /obj/item/clothing/head/soft/green
/datum/loadout_item/head/grey_cap
name = "Cap (Grey)"
item_path = /obj/item/clothing/head/soft/grey
/datum/loadout_item/head/orange_cap
name = "Cap (Orange)"
item_path = /obj/item/clothing/head/soft/orange
/datum/loadout_item/head/purple_cap
name = "Cap (Purple)"
item_path = /obj/item/clothing/head/soft/purple
/datum/loadout_item/head/rainbow_cap
name = "Cap (Rainbow)"
item_path = /obj/item/clothing/head/soft/rainbow
/datum/loadout_item/head/red_cap
name = "Cap (Red)"
item_path = /obj/item/clothing/head/soft/red
/datum/loadout_item/head/white_cap
name = "Cap (White)"
item_path = /obj/item/clothing/head/soft
/datum/loadout_item/head/yellow_cap
name = "Cap (Yellow)"
item_path = /obj/item/clothing/head/soft/yellow
/datum/loadout_item/head/flatcap
name = "Cap (Flat)"
item_path = /obj/item/clothing/head/flatcap
/datum/loadout_item/head/beige_fedora
name = "Fedora (Beige)"
item_path = /obj/item/clothing/head/fedora/beige
/datum/loadout_item/head/black_fedora
name = "Fedora (Black)"
item_path = /obj/item/clothing/head/fedora
/datum/loadout_item/head/white_fedora
name = "Fedora (White)"
item_path = /obj/item/clothing/head/fedora/white
/datum/loadout_item/head/mail_cap
name = "Cap (Mail)"
item_path = /obj/item/clothing/head/costume/mailman
/datum/loadout_item/head/kitty_ears
name = "Kitty Ears"
item_path = /obj/item/clothing/head/costume/kitty
/datum/loadout_item/head/rabbit_ears
name = "Rabbit Ears"
item_path = /obj/item/clothing/head/costume/rabbitears
/datum/loadout_item/head/bandana
name = "Bandana Thin"
item_path = /obj/item/clothing/head/costume/tmc
/datum/loadout_item/head/rastafarian
name = "Cap (Rastafarian)"
item_path = /obj/item/clothing/head/rasta
/datum/loadout_item/head/top_hat
name = "Top Hat"
item_path = /obj/item/clothing/head/hats/tophat
/datum/loadout_item/head/bowler_hat
name = "Bowler Hat"
item_path = /obj/item/clothing/head/hats/bowler
/datum/loadout_item/head/bear_pelt
name = "Bear Pelt"
item_path = /obj/item/clothing/head/costume/bearpelt
/datum/loadout_item/head/ushanka
name ="Ushanka"
item_path = /obj/item/clothing/head/costume/ushanka
/datum/loadout_item/head/plague_doctor
name = "Cap (Plague Doctor)"
item_path = /obj/item/clothing/head/bio_hood/plague
/datum/loadout_item/head/rose
name = "Rose"
item_path = /obj/item/food/grown/rose
/datum/loadout_item/head/wig
name = "Wig"
item_path = /obj/item/clothing/head/wig/natural
additional_displayed_text = list("Hair Color")

View File

@@ -0,0 +1,33 @@
/// Inhand items (Moves overrided items to backpack)
/datum/loadout_category/inhands
category_name = "Inhand"
category_ui_icon = FA_ICON_BRIEFCASE
type_to_generate = /datum/loadout_item/inhand
tab_order = /datum/loadout_category/head::tab_order + 4
/datum/loadout_item/inhand
abstract_type = /datum/loadout_item/inhand
/datum/loadout_item/inhand/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
if(outfit.l_hand && !outfit.r_hand)
outfit.r_hand = item_path
else
if(outfit.l_hand)
LAZYADD(outfit.backpack_contents, outfit.l_hand)
outfit.l_hand = item_path
/datum/loadout_item/inhand/cane
name = "Cane"
item_path = /obj/item/cane
/datum/loadout_item/inhand/cane_white
name = "White Cane"
item_path = /obj/item/cane/white
/datum/loadout_item/inhand/briefcase
name = "Briefcase (Leather)"
item_path = /obj/item/storage/briefcase
/datum/loadout_item/inhand/briefcase_secure
name = "Briefcase (Secure)"
item_path = /obj/item/storage/briefcase/secure

View File

@@ -0,0 +1,36 @@
/// Neck Slot Items (Deletes overrided items)
/datum/loadout_category/neck
category_name = "Neck"
category_ui_icon = FA_ICON_USER_TIE
type_to_generate = /datum/loadout_item/neck
tab_order = /datum/loadout_category/head::tab_order + 2
/datum/loadout_item/neck
abstract_type = /datum/loadout_item/neck
/datum/loadout_item/neck/insert_path_into_outfit(datum/outfit/outfit, mob/living/carbon/human/equipper, visuals_only = FALSE)
outfit.neck = item_path
/datum/loadout_item/neck/scarf_greyscale
name = "Scarf (Colorable)"
item_path = /obj/item/clothing/neck/scarf
/datum/loadout_item/neck/greyscale_large
name = "Scarf (Large, Colorable)"
item_path = /obj/item/clothing/neck/large_scarf
/datum/loadout_item/neck/greyscale_larger
name = "Scarf (Larger, Colorable)"
item_path = /obj/item/clothing/neck/infinity_scarf
/datum/loadout_item/neck/necktie
name = "Necktie (Colorable)"
item_path = /obj/item/clothing/neck/tie
/datum/loadout_item/neck/necktie_disco
name = "Necktie (Ugly)"
item_path = /obj/item/clothing/neck/tie/horrible
/datum/loadout_item/neck/necktie_loose
name = "Necktie (Loose)"
item_path = /obj/item/clothing/neck/tie/detective

View File

@@ -0,0 +1,203 @@
/// Pocket items (Moved to backpack)
/datum/loadout_category/pocket
category_name = "Other"
category_ui_icon = FA_ICON_QUESTION
type_to_generate = /datum/loadout_item/pocket_items
tab_order = /datum/loadout_category/head::tab_order + 5
/// How many pocket items are allowed
VAR_PRIVATE/max_allowed = 2
/datum/loadout_category/pocket/New()
. = ..()
category_info = "([max_allowed] allowed)"
/datum/loadout_category/pocket/handle_duplicate_entires(
datum/preference_middleware/loadout/manager,
datum/loadout_item/conflicting_item,
datum/loadout_item/added_item,
list/datum/loadout_item/all_loadout_items,
)
var/list/datum/loadout_item/pocket_items/other_pocket_items = list()
for(var/datum/loadout_item/pocket_items/other_pocket_item in all_loadout_items)
other_pocket_items += other_pocket_item
if(length(other_pocket_items) >= max_allowed)
// We only need to deselect something if we're above the limit
// (And if we are we prioritize the first item found, FIFO)
manager.deselect_item(other_pocket_items[1])
return TRUE
/datum/loadout_item/pocket_items
abstract_type = /datum/loadout_item/pocket_items
/datum/loadout_item/pocket_items/on_equip_item(
obj/item/equipped_item,
datum/preferences/preference_source,
list/preference_list,
mob/living/carbon/human/equipper,
visuals_only = FALSE,
)
// Backpack items aren't created if it's a visual equipping, so don't do any on equip stuff. It doesn't exist.
if(visuals_only)
return NONE
return ..()
/datum/loadout_item/pocket_items/lipstick_black
name = "Lipstick (Black)"
item_path = /obj/item/lipstick/black
additional_displayed_text = list("Black")
/datum/loadout_item/pocket_items/lipstick_blue
name = "Lipstick (Blue)"
item_path = /obj/item/lipstick/blue
additional_displayed_text = list("Blue")
/datum/loadout_item/pocket_items/lipstick_green
name = "Lipstick (Green)"
item_path = /obj/item/lipstick/green
additional_displayed_text = list("Green")
/datum/loadout_item/pocket_items/lipstick_jade
name = "Lipstick (Jade)"
item_path = /obj/item/lipstick/jade
additional_displayed_text = list("Jade")
/datum/loadout_item/pocket_items/lipstick_purple
name = "Lipstick (Purple)"
item_path = /obj/item/lipstick/purple
additional_displayed_text = list("Purple")
/datum/loadout_item/pocket_items/lipstick_red
name = "Lipstick (Red)"
item_path = /obj/item/lipstick
additional_displayed_text = list("Red")
/datum/loadout_item/pocket_items/lipstick_white
name = "Lipstick (White)"
item_path = /obj/item/lipstick/white
additional_displayed_text = list("White")
/datum/loadout_item/pocket_items/plush
abstract_type = /datum/loadout_item/pocket_items/plush
can_be_named = TRUE
/datum/loadout_item/pocket_items/plush/bee
name = "Plush (Bee)"
item_path = /obj/item/toy/plush/beeplushie
/datum/loadout_item/pocket_items/plush/carp
name = "Plush (Carp)"
item_path = /obj/item/toy/plush/carpplushie
/datum/loadout_item/pocket_items/plush/lizard_greyscale
name = "Plush (Lizard, Colorable)"
item_path = /obj/item/toy/plush/lizard_plushie/greyscale
/datum/loadout_item/pocket_items/plush/lizard_random
name = "Plush (Lizard, Random)"
can_be_greyscale = DONT_GREYSCALE
item_path = /obj/item/toy/plush/lizard_plushie
additional_displayed_text = list("Random color")
/datum/loadout_item/pocket_items/plush/moth
name = "Plush (Moth)"
item_path = /obj/item/toy/plush/moth
/datum/loadout_item/pocket_items/plush/narsie
name = "Plush (Nar'sie)"
item_path = /obj/item/toy/plush/narplush
/datum/loadout_item/pocket_items/plush/nukie
name = "Plush (Nukie)"
item_path = /obj/item/toy/plush/nukeplushie
/datum/loadout_item/pocket_items/plush/peacekeeper
name = "Plush (Peacekeeper)"
item_path = /obj/item/toy/plush/pkplush
/datum/loadout_item/pocket_items/plush/plasmaman
name = "Plush (Plasmaman)"
item_path = /obj/item/toy/plush/plasmamanplushie
/datum/loadout_item/pocket_items/plush/ratvar
name = "Plush (Ratvar)"
item_path = /obj/item/toy/plush/ratplush
/datum/loadout_item/pocket_items/plush/rouny
name = "Plush (Rouny)"
item_path = /obj/item/toy/plush/rouny
/datum/loadout_item/pocket_items/plush/snake
name = "Plush (Snake)"
item_path = /obj/item/toy/plush/snakeplushie
/datum/loadout_item/pocket_items/card_binder
name = "Card Binder"
item_path = /obj/item/storage/card_binder
/datum/loadout_item/pocket_items/card_deck
name = "Playing Card Deck"
item_path = /obj/item/toy/cards/deck
/datum/loadout_item/pocket_items/kotahi_deck
name = "Kotahi Deck"
item_path = /obj/item/toy/cards/deck/kotahi
/datum/loadout_item/pocket_items/wizoff_deck
name = "Wizoff Deck"
item_path = /obj/item/toy/cards/deck/wizoff
/datum/loadout_item/pocket_items/dice_bag
name = "Dice Bag"
item_path = /obj/item/storage/dice
/datum/loadout_item/pocket_items/d1
name = "D1"
item_path = /obj/item/dice/d1
/datum/loadout_item/pocket_items/d2
name = "D2"
item_path = /obj/item/dice/d2
/datum/loadout_item/pocket_items/d4
name = "D4"
item_path = /obj/item/dice/d4
/datum/loadout_item/pocket_items/d6
name = "D6"
item_path = /obj/item/dice/d6
/datum/loadout_item/pocket_items/d6_ebony
name = "D6 (Ebony)"
item_path = /obj/item/dice/d6/ebony
/datum/loadout_item/pocket_items/d6_space
name = "D6 (Space)"
item_path = /obj/item/dice/d6/space
/datum/loadout_item/pocket_items/d8
name = "D8"
item_path = /obj/item/dice/d8
/datum/loadout_item/pocket_items/d10
name = "D10"
item_path = /obj/item/dice/d10
/datum/loadout_item/pocket_items/d12
name = "D12"
item_path = /obj/item/dice/d12
/datum/loadout_item/pocket_items/d20
name = "D20"
item_path = /obj/item/dice/d20
/datum/loadout_item/pocket_items/d100
name = "D100"
item_path = /obj/item/dice/d100
/datum/loadout_item/pocket_items/d00
name = "D00"
item_path = /obj/item/dice/d00

View File

@@ -0,0 +1,79 @@
/**
* # Loadout categories
*
* Loadout categories are singletons used to group loadout items together in the loadout screen.
*/
/datum/loadout_category
/// The name of the category, shown in the tabs
var/category_name
/// FontAwesome icon for the category
var/category_ui_icon
/// String to display on the top-right of a category tab
var/category_info
/// Order which they appear in the tabs, ties go alphabetically
var/tab_order = -1
/// What type of loadout items should be generated for this category?
var/type_to_generate
/// List of all loadout items in this category
VAR_FINAL/list/datum/loadout_item/associated_items
/datum/loadout_category/New()
. = ..()
associated_items = get_items()
for(var/datum/loadout_item/item as anything in associated_items)
if(GLOB.all_loadout_datums[item.item_path])
stack_trace("Loadout datum collision - [item.item_path] is shared between multiple loadout datums.")
GLOB.all_loadout_datums[item.item_path] = item
/datum/loadout_category/Destroy(force, ...)
if(!force)
stack_trace("QDEL called on loadout category [type]. This shouldn't ever happen. (Use FORCE if necessary.)")
return QDEL_HINT_LETMELIVE
associated_items.Cut()
return ..()
/// Return a list of all /datum/loadout_items in this category.
/datum/loadout_category/proc/get_items() as /list
var/list/all_items = list()
for(var/datum/loadout_item/found_type as anything in typesof(type_to_generate))
if(found_type == initial(found_type.abstract_type))
continue
if(!ispath(initial(found_type.item_path), /obj/item))
stack_trace("Loadout get_items(): Attempted to instantiate a loadout item ([found_type]) with an invalid or null typepath! (got path: [initial(found_type.item_path)])")
continue
var/datum/loadout_item/spawned_type = new found_type(src)
all_items += spawned_type
return all_items
/// Returns a list of all /datum/loadout_items in this category, formatted for UI use. Only ran once.
/datum/loadout_category/proc/items_to_ui_data() as /list
if(!length(associated_items))
return list()
var/list/formatted_list = list()
for(var/datum/loadout_item/item as anything in associated_items)
var/list/item_data = item.to_ui_data()
UNTYPED_LIST_ADD(formatted_list, item_data)
sortTim(formatted_list, /proc/cmp_assoc_list_name) // Alphabetizing
return formatted_list
/**
* Handles what happens when two items of this category are selected at once
*
* Return TRUE if it's okay to continue with adding the incoming item,
* or return FALSE to stop the new item from being added
*/
/datum/loadout_category/proc/handle_duplicate_entires(
datum/preference_middleware/loadout/manager,
datum/loadout_item/conflicting_item,
datum/loadout_item/added_item,
list/datum/loadout_item/all_loadout_items,
)
manager.deselect_item(conflicting_item)
return TRUE

View File

@@ -0,0 +1,80 @@
/**
* Equips this mob with a given outfit and loadout items as per the passed preferences.
*
* Loadout items override the pre-existing item in the corresponding slot of the job outfit.
* Some job items are preserved after being overridden - belt items, ear items, and glasses.
* The rest of the slots, the items are overridden completely and deleted.
*
* Species with special outfits are snowflaked to have loadout items placed in their bags instead of overriding the outfit.
*
* * outfit - the job outfit we're equipping
* * preference_source - the preferences to draw loadout items from.
* * visuals_only - whether we call special equipped procs, or if we just look like we equipped it
*/
/mob/living/carbon/human/proc/equip_outfit_and_loadout(
datum/outfit/outfit = /datum/outfit,
datum/preferences/preference_source,
visuals_only = FALSE,
)
if(isnull(preference_source))
return equipOutfit(outfit, visuals_only)
var/datum/outfit/equipped_outfit
if(ispath(outfit, /datum/outfit))
equipped_outfit = new outfit()
else if(istype(outfit, /datum/outfit))
equipped_outfit = outfit
else
CRASH("Invalid outfit passed to equip_outfit_and_loadout ([outfit])")
var/list/preference_list = preference_source.read_preference(/datum/preference/loadout)
var/list/loadout_datums = loadout_list_to_datums(preference_list)
// Slap our things into the outfit given
for(var/datum/loadout_item/item as anything in loadout_datums)
item.insert_path_into_outfit(equipped_outfit, src, visuals_only)
// Equip the outfit loadout items included
if(!equipped_outfit.equip(src, visuals_only))
return FALSE
// Handle any snowflake on_equips
var/list/new_contents = get_all_gear()
var/update = NONE
for(var/datum/loadout_item/item as anything in loadout_datums)
var/obj/item/equipped = locate(item.item_path) in new_contents
if(isnull(equipped))
continue
update |= item.on_equip_item(
equipped_item = equipped,
preference_source = preference_source,
preference_list = preference_list,
equipper = src,
visuals_only = visuals_only,
)
if(update)
update_clothing(update)
return TRUE
/**
* Takes a list of paths (such as a loadout list)
* and returns a list of their singleton loadout item datums
*
* loadout_list - the list being checked
*
* Returns a list of singleton datums
*/
/proc/loadout_list_to_datums(list/loadout_list) as /list
var/list/datums = list()
if(!length(GLOB.all_loadout_datums))
CRASH("No loadout datums in the global loadout list!")
for(var/path in loadout_list)
var/actual_datum = GLOB.all_loadout_datums[path]
if(!istype(actual_datum, /datum/loadout_item))
stack_trace("Could not find ([path]) loadout item in the global list of loadout datums!")
continue
datums += actual_datum
return datums

View File

@@ -0,0 +1,371 @@
/// 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 = FALSE
/// 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
/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)
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)
/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)
return set_name(manager, user)
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.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.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.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
return TRUE // update UI
/// Sets the name of the item.
/datum/loadout_item/proc/set_name(datum/preference_middleware/loadout/manager, mob/user)
var/list/loadout = manager.preferences.read_preference(/datum/preference/loadout)
var/input_name = tgui_input_text(
user = user,
message = "What name do you want to give the [name]? Leave blank to clear.",
title = "[name] name",
default = loadout?[item_path]?[INFO_NAMED], // plop in existing name (if any)
max_length = MAX_NAME_LEN,
)
if(QDELETED(src) || QDELETED(user) || QDELETED(manager) || QDELETED(manager.preferences))
return FALSE
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][INFO_NAMED] = input_name
else if(input_name == "")
loadout[item_path] -= INFO_NAMED
manager.preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
return FALSE // no update needed
/// 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.preferences.read_preference(/datum/preference/loadout)
if(!loadout?[item_path])
return FALSE
loadout[item_path][INFO_RESKIN] = reskin_to
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)
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, "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
if(can_be_named && item_details?[INFO_NAMED] && !visuals_only)
equipped_item.name = trim(item_details[INFO_NAMED], PREVENT_CHARACTER_TRIM_LOSS(MAX_NAME_LEN))
ADD_TRAIT(equipped_item, TRAIT_WAS_RENAMED, "Loadout")
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
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"
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,
))
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

View File

@@ -0,0 +1,120 @@
/datum/preference_middleware/loadout
action_delegations = list(
"clear_all_items" = PROC_REF(action_clear_all),
"pass_to_loadout_item" = PROC_REF(action_pass_to_loadout_item),
"rotate_dummy" = PROC_REF(action_rotate_model_dir),
"select_item" = PROC_REF(action_select_item),
"toggle_job_clothes" = PROC_REF(action_toggle_job_outfit),
"close_greyscale_menu" = PROC_REF(force_close_greyscale_menu),
)
/// Our currently open greyscaling menu.
VAR_FINAL/datum/greyscale_modify_menu/menu
/datum/preference_middleware/loadout/Destroy(force, ...)
QDEL_NULL(menu)
return ..()
/datum/preference_middleware/loadout/on_new_character(mob/user)
preferences.character_preview_view?.update_body()
/datum/preference_middleware/loadout/proc/action_select_item(list/params, mob/user)
PRIVATE_PROC(TRUE)
var/path_to_use = text2path(params["path"])
var/datum/loadout_item/interacted_item = GLOB.all_loadout_datums[path_to_use]
if(!istype(interacted_item))
stack_trace("Failed to locate desired loadout item (path: [params["path"]]) in the global list of loadout datums!")
return TRUE // update
if(params["deselect"])
deselect_item(interacted_item)
else
select_item(interacted_item)
return TRUE
/datum/preference_middleware/loadout/proc/action_clear_all(list/params, mob/user)
PRIVATE_PROC(TRUE)
preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], null)
return TRUE
/datum/preference_middleware/loadout/proc/action_toggle_job_outfit(list/params, mob/user)
PRIVATE_PROC(TRUE)
preferences.character_preview_view.show_job_clothes = !preferences.character_preview_view.show_job_clothes
preferences.character_preview_view.update_body()
return TRUE
/datum/preference_middleware/loadout/proc/action_rotate_model_dir(list/params, mob/user)
PRIVATE_PROC(TRUE)
switch(params["dir"])
if("left")
preferences.character_preview_view.setDir(turn(preferences.character_preview_view.dir, -90))
if("right")
preferences.character_preview_view.setDir(turn(preferences.character_preview_view.dir, 90))
/datum/preference_middleware/loadout/proc/action_pass_to_loadout_item(list/params, mob/user)
PRIVATE_PROC(TRUE)
var/path_to_use = text2path(params["path"])
var/datum/loadout_item/interacted_item = GLOB.all_loadout_datums[path_to_use]
if(!istype(interacted_item)) // no you cannot href exploit to spawn with a pulse rifle
stack_trace("Failed to locate desired loadout item (path: [params["path"]]) in the global list of loadout datums!")
return TRUE // update
if(interacted_item.handle_loadout_action(src, user, params["subaction"], params))
preferences.character_preview_view.update_body()
return TRUE
return FALSE
/// Select [path] item to [category_slot] slot.
/datum/preference_middleware/loadout/proc/select_item(datum/loadout_item/selected_item)
var/list/loadout = preferences.read_preference(/datum/preference/loadout)
var/list/datum/loadout_item/loadout_datums = loadout_list_to_datums(loadout)
for(var/datum/loadout_item/item as anything in loadout_datums)
if(item.category != selected_item.category)
continue
if(!item.category.handle_duplicate_entires(src, item, selected_item, loadout_datums))
return
LAZYSET(loadout, selected_item.item_path, list())
preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
/// Deselect [deselected_item].
/datum/preference_middleware/loadout/proc/deselect_item(datum/loadout_item/deselected_item)
var/list/loadout = preferences.read_preference(/datum/preference/loadout)
LAZYREMOVE(loadout, deselected_item.item_path)
preferences.update_preference(GLOB.preference_entries[/datum/preference/loadout], loadout)
/datum/preference_middleware/loadout/proc/register_greyscale_menu(datum/greyscale_modify_menu/open_menu)
src.menu = open_menu
RegisterSignal(menu, COMSIG_QDELETING, PROC_REF(cleanup_greyscale_menu))
/datum/preference_middleware/loadout/proc/cleanup_greyscale_menu()
SIGNAL_HANDLER
menu = null
/datum/preference_middleware/loadout/proc/force_close_greyscale_menu()
menu?.ui_close()
/datum/preference_middleware/loadout/get_ui_data(mob/user)
var/list/data = list()
data["job_clothes"] = preferences.character_preview_view.show_job_clothes
return data
/datum/preference_middleware/loadout/get_ui_static_data(mob/user)
var/list/data = list()
data["loadout_preview_view"] = preferences.character_preview_view.assigned_map
return data
/datum/preference_middleware/loadout/get_constant_data()
var/list/data = list()
var/list/loadout_tabs = list()
for(var/datum/loadout_category/category as anything in GLOB.all_loadout_categories)
var/list/cat_data = list(
"name" = category.category_name,
"category_icon" = category.category_ui_icon,
"category_info" = category.category_info,
"contents" = category.items_to_ui_data(),
)
UNTYPED_LIST_ADD(loadout_tabs, cat_data)
data["loadout_tabs"] = loadout_tabs
return data

View File

@@ -0,0 +1,59 @@
/datum/preference/loadout
savefile_key = "loadout_list"
savefile_identifier = PREFERENCE_CHARACTER
priority = PREFERENCE_PRIORITY_LOADOUT
can_randomize = FALSE
// Loadout preference is an assoc list [item_path] = [loadout item information list]
//
// it may look something like
// - list(/obj/item/glasses = list())
// or
// - list(/obj/item/plush/lizard = list("name" = "Tests-The-Loadout", "color" = "#FF0000"))
// Loadouts are applied with job equip code.
/datum/preference/loadout/apply_to_human(mob/living/carbon/human/target, value)
return
// Sanitize on load to ensure no invalid paths from older saves get in
/datum/preference/loadout/deserialize(input, datum/preferences/preferences)
return sanitize_loadout_list(input, preferences.parent?.mob)
// Default value is null - the loadout list is a lazylist
/datum/preference/loadout/create_default_value(datum/preferences/preferences)
return null
/datum/preference/loadout/is_valid(value)
return isnull(value) || islist(value)
/**
* Removes all invalid paths from loadout lists.
* This is a general sanitization for preference loading.
*
* Returns a list, or null if empty
*/
/datum/preference/loadout/proc/sanitize_loadout_list(list/passed_list, mob/optional_loadout_owner) as /list
var/list/sanitized_list
for(var/path in passed_list)
// Loading from json has each path in the list as a string that we need to convert back to typepath
var/obj/item/real_path = istext(path) ? text2path(path) : path
if(!ispath(real_path, /obj/item))
if(optional_loadout_owner)
to_chat(optional_loadout_owner, span_boldnotice("The following invalid item path was found \
in your character loadout: [real_path || "null"]. \
It has been removed, renamed, or is otherwise missing - \
You may want to check your loadout settings."))
continue
else if(!istype(GLOB.all_loadout_datums[real_path], /datum/loadout_item))
if(optional_loadout_owner)
to_chat(optional_loadout_owner, span_boldnotice("The following invalid loadout item was found \
in your character loadout: [real_path || "null"]. \
It has been removed, renamed, or is otherwise missing - \
You may want to check your loadout settings."))
continue
// Set into sanitize list using converted path key
var/list/data = passed_list[path]
LAZYSET(sanitized_list, real_path, LAZYLISTDUPLICATE(data))
return sanitized_list

View File

@@ -93,8 +93,9 @@
return preview_job
/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin)
var/datum/job/preview_job = get_highest_priority_job()
/datum/preferences/proc/render_new_preview_appearance(mob/living/carbon/human/dummy/mannequin, show_job_clothes = TRUE)
var/datum/job/no_job = SSjob.GetJobType(/datum/job/unassigned)
var/datum/job/preview_job = get_highest_priority_job() || no_job
if(preview_job)
// Silicons only need a very basic preview since there is no customization for them.
@@ -106,9 +107,13 @@
// Set up the dummy for its photoshoot
apply_prefs_to(mannequin, TRUE)
if(preview_job)
mannequin.job = preview_job.title
mannequin.dress_up_as_job(preview_job, TRUE)
mannequin.dress_up_as_job(
equipping = show_job_clothes ? preview_job : no_job,
visual_only = TRUE,
player_client = parent,
consistent = TRUE,
)
// Apply visual quirks
// Yes we do it every time because it needs to be done after job gear

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@@ -1727,7 +1727,6 @@
#include "code\datums\quirks\neutral_quirks\photographer.dm"
#include "code\datums\quirks\neutral_quirks\pineapple_hater.dm"
#include "code\datums\quirks\neutral_quirks\pineapple_liker.dm"
#include "code\datums\quirks\neutral_quirks\pride_pin.dm"
#include "code\datums\quirks\neutral_quirks\shifty_eyes.dm"
#include "code\datums\quirks\neutral_quirks\snob.dm"
#include "code\datums\quirks\neutral_quirks\transhumanist.dm"
@@ -3695,7 +3694,6 @@
#include "code\modules\client\preferences\pixel_size.dm"
#include "code\modules\client\preferences\playtime_reward_cloak.dm"
#include "code\modules\client\preferences\preferred_map.dm"
#include "code\modules\client\preferences\pride_pin.dm"
#include "code\modules\client\preferences\prisoner_crime.dm"
#include "code\modules\client\preferences\prosthetic_limb.dm"
#include "code\modules\client\preferences\prosthetic_organ.dm"
@@ -3730,6 +3728,7 @@
#include "code\modules\client\preferences\migrations\body_type_migration.dm"
#include "code\modules\client\preferences\migrations\convert_to_json_savefile.dm"
#include "code\modules\client\preferences\migrations\legacy_sound_toggles_migration.dm"
#include "code\modules\client\preferences\migrations\quirk_loadout_migration.dm"
#include "code\modules\client\preferences\migrations\tgui_prefs_migration.dm"
#include "code\modules\client\preferences\migrations\tts_blip_migration.dm"
#include "code\modules\client\preferences\species_features\basic.dm"
@@ -4403,6 +4402,17 @@
#include "code\modules\lighting\lighting_source.dm"
#include "code\modules\lighting\lighting_turf.dm"
#include "code\modules\lighting\static_lighting_area.dm"
#include "code\modules\loadout\loadout_categories.dm"
#include "code\modules\loadout\loadout_helpers.dm"
#include "code\modules\loadout\loadout_items.dm"
#include "code\modules\loadout\loadout_menu.dm"
#include "code\modules\loadout\loadout_preference.dm"
#include "code\modules\loadout\categories\accessories.dm"
#include "code\modules\loadout\categories\glasses.dm"
#include "code\modules\loadout\categories\heads.dm"
#include "code\modules\loadout\categories\inhands.dm"
#include "code\modules\loadout\categories\neck.dm"
#include "code\modules\loadout\categories\pocket.dm"
#include "code\modules\logging\log_category.dm"
#include "code\modules\logging\log_entry.dm"
#include "code\modules\logging\log_holder.dm"

View File

@@ -7,6 +7,7 @@ import { Window } from '../../layouts';
import { AntagsPage } from './AntagsPage';
import { PreferencesMenuData } from './data';
import { JobsPage } from './JobsPage';
import { LoadoutPage } from './loadout/index';
import { MainPage } from './MainPage';
import { PageButton } from './PageButton';
import { QuirksPage } from './QuirksPage';
@@ -18,6 +19,7 @@ enum Page {
Jobs,
Species,
Quirks,
Loadout,
}
const CharacterProfiles = (props: {
@@ -75,6 +77,11 @@ export const CharacterPreferenceWindow = (props) => {
case Page.Quirks:
pageContents = <QuirksPage />;
break;
case Page.Loadout:
pageContents = <LoadoutPage />;
break;
default:
exhaustiveCheck(currentPage);
}
@@ -116,6 +123,16 @@ export const CharacterPreferenceWindow = (props) => {
</PageButton>
</Stack.Item>
<Stack.Item grow>
<PageButton
currentPage={currentPage}
page={Page.Loadout}
setPage={setCurrentPage}
>
Loadout
</PageButton>
</Stack.Item>
<Stack.Item grow>
<PageButton
currentPage={currentPage}

View File

@@ -1,6 +1,7 @@
import { BooleanLike } from 'common/react';
import { sendAct } from '../../backend';
import { LoadoutCategory, LoadoutList } from './loadout/base';
import { Gender } from './preferences/gender';
export enum Food {
@@ -147,6 +148,8 @@ export type PreferencesMenuData = {
gender: Gender;
joblessrole: JoblessRole;
species: string;
loadout_list: LoadoutList;
job_clothes: BooleanLike;
};
randomization: Record<string, RandomSetting>;
@@ -191,6 +194,9 @@ export type ServerData = {
random: {
randomizable: string[];
};
loadout: {
loadout_tabs: LoadoutCategory[];
};
species: Record<string, Species>;
[otheyKey: string]: unknown;
};

View File

@@ -0,0 +1,147 @@
import { createSearch } from '../../../../common/string';
import { useBackend } from '../../../backend';
import {
Box,
Button,
DmIcon,
Flex,
Icon,
NoticeBox,
} from '../../../components';
import { LoadoutCategory, LoadoutItem, LoadoutManagerData } from './base';
export const ItemIcon = (props: { item: LoadoutItem; scale?: number }) => {
const { item, scale = 3 } = props;
const icon_to_use = item.icon;
const icon_state_to_use = item.icon_state;
if (!icon_to_use || !icon_state_to_use) {
return (
<Icon
name="question"
size={Math.round(scale * 2.5)}
color="red"
style={{
transform: `translateX(${scale * 2}px) translateY(${scale * 2}px)`,
}}
/>
);
}
return (
<DmIcon
fallback={<Icon name="spinner" spin color="gray" />}
icon={icon_to_use}
icon_state={icon_state_to_use}
style={{
transform: `scale(${scale}) translateX(${scale * 3}px) translateY(${
scale * 3
}px)`,
}}
/>
);
};
export const ItemDisplay = (props: {
active: boolean;
item: LoadoutItem;
scale?: number;
}) => {
const { act } = useBackend();
const { active, item, scale = 3 } = props;
const boxSize = `${scale * 32}px`;
return (
<Button
height={boxSize}
width={boxSize}
color={active ? 'green' : 'default'}
style={{ textTransform: 'capitalize', zIndex: '1' }}
tooltip={item.name}
tooltipPosition={'bottom'}
onClick={() =>
act('select_item', {
path: item.path,
deselect: active,
})
}
>
<Flex vertical>
<Flex.Item>
<ItemIcon item={item} scale={scale} />
</Flex.Item>
{item.information.length > 0 && (
<Flex.Item ml={-5.5} style={{ zIndex: '3' }}>
{item.information.map((info) => (
<Box
height="9px"
key={info}
fontSize="9px"
textColor={'darkgray'}
bold
>
{info}
</Box>
))}
</Flex.Item>
)}
</Flex>
</Button>
);
};
const ItemListDisplay = (props: { items: LoadoutItem[] }) => {
const { data } = useBackend<LoadoutManagerData>();
const { loadout_list } = data.character_preferences.misc;
return (
<Flex wrap>
{props.items.map((item) => (
<Flex.Item key={item.name} mr={2} mb={2}>
<ItemDisplay
item={item}
active={loadout_list && loadout_list[item.path] !== undefined}
/>
</Flex.Item>
))}
</Flex>
);
};
export const LoadoutTabDisplay = (props: {
category: LoadoutCategory | undefined;
}) => {
const { category } = props;
if (!category) {
return (
<NoticeBox>
Erroneous category detected! This is a bug, please report it.
</NoticeBox>
);
}
return <ItemListDisplay items={category.contents} />;
};
export const SearchDisplay = (props: {
loadout_tabs: LoadoutCategory[];
currentSearch: string;
}) => {
const { loadout_tabs, currentSearch } = props;
const search = createSearch(
currentSearch,
(loadout_item: LoadoutItem) => loadout_item.name,
);
const validLoadoutItems = loadout_tabs
.flatMap((tab) => tab.contents)
.filter(search)
.sort((a, b) => (a.name > b.name ? 1 : -1));
if (validLoadoutItems.length === 0) {
return <NoticeBox>No items found!</NoticeBox>;
}
return <ItemListDisplay items={validLoadoutItems} />;
};

View File

@@ -0,0 +1,219 @@
import { useBackend } from '../../../backend';
import {
Box,
Button,
Dimmer,
DmIcon,
Flex,
Icon,
LabeledList,
Section,
Stack,
} from '../../../components';
import { FAIcon, LoadoutItem, LoadoutManagerData, ReskinOption } from './base';
import { ItemIcon } from './ItemDisplay';
// Used in LoadoutItem to make buttons relating to how an item can be edited
export type LoadoutButton = {
label: string;
act_key?: string;
button_icon?: FAIcon;
button_text?: string;
active_key?: string;
active_text?: string;
inactive_text?: string;
tooltip_text?: string;
};
const LoadoutModifyButton = (props: {
button: LoadoutButton;
modifyItemDimmer: LoadoutItem;
}) => {
const { act, data } = useBackend<LoadoutManagerData>();
const { loadout_list } = data.character_preferences.misc;
const { button, modifyItemDimmer } = props;
const buttonIsActive =
button.active_key && loadout_list[modifyItemDimmer.path][button.active_key];
if (button.active_text && button.inactive_text) {
return (
<Button.Checkbox
tooltip={button.tooltip_text}
checked={buttonIsActive}
color={buttonIsActive ? 'green' : 'default'}
onClick={() => {
act('pass_to_loadout_item', {
subaction: button.act_key,
path: modifyItemDimmer.path,
});
}}
>
{buttonIsActive ? button.active_text : button.inactive_text}
</Button.Checkbox>
);
}
return (
<Button
icon={button.button_icon}
tooltip={button.tooltip_text}
disabled={!button.act_key}
color={buttonIsActive ? 'green' : 'default'}
onClick={() => {
act('pass_to_loadout_item', {
subaction: button.act_key,
path: modifyItemDimmer.path,
});
}}
>
{button.button_text}
</Button>
);
};
const LoadoutModifyButtons = (props: { modifyItemDimmer: LoadoutItem }) => {
const { act, data } = useBackend<LoadoutManagerData>();
const { loadout_list } = data.character_preferences.misc;
const { modifyItemDimmer } = props;
const isActive = (item: LoadoutItem, reskin: ReskinOption) => {
return loadout_list && loadout_list[item.path]['reskin']
? loadout_list[item.path]['reskin'] === reskin.name
: item.icon_state === reskin.skin_icon_state;
};
return (
<Stack>
<Stack.Item>
<LabeledList>
{!!modifyItemDimmer.reskins && (
<LabeledList.Item label="Styles" verticalAlign="middle">
<Flex wrap width="50%">
{modifyItemDimmer.reskins.map((reskin) => (
<Flex.Item key={reskin.tooltip} mr={1} mb={1}>
<Button
tooltip={reskin.tooltip}
color={
isActive(modifyItemDimmer, reskin) ? 'green' : 'default'
}
onClick={() => {
act('pass_to_loadout_item', {
subaction: 'set_skin',
path: modifyItemDimmer.path,
skin: reskin.name,
});
}}
>
{modifyItemDimmer.icon ? (
<DmIcon
fallback={<Icon name="spinner" spin color="gray" />}
icon={modifyItemDimmer.icon}
icon_state={reskin.skin_icon_state}
style={{
transform: `scale(2) translateY(2px)`,
}}
/>
) : (
// Should never happen, hopefully
<Box>{reskin.name}</Box>
)}
</Button>
</Flex.Item>
))}
</Flex>
</LabeledList.Item>
)}
{modifyItemDimmer.buttons.map((button) => (
<LabeledList.Item key={button.label} label={button.label}>
<LoadoutModifyButton
button={button}
modifyItemDimmer={modifyItemDimmer}
/>
</LabeledList.Item>
))}
</LabeledList>
</Stack.Item>
</Stack>
);
};
const LoadoutModifyItemDisplay = (props: { modifyItemDimmer: LoadoutItem }) => {
const { modifyItemDimmer } = props;
return (
<Stack vertical justify="center">
<Stack.Item>
<Box bold width="80px" textAlign="center">
{modifyItemDimmer.name}
</Box>
</Stack.Item>
<Stack.Item ml={-0.5}>
<ItemIcon item={modifyItemDimmer} scale={3} />
</Stack.Item>
</Stack>
);
};
export const LoadoutModifyDimmer = (props: {
modifyItemDimmer: LoadoutItem;
setModifyItemDimmer: (dimmer: LoadoutItem | null) => void;
}) => {
const { act } = useBackend();
const { modifyItemDimmer, setModifyItemDimmer } = props;
return (
<Dimmer style={{ zIndex: '100' }}>
<Stack
vertical
width="400px"
backgroundColor="#101010"
style={{
borderRadius: '2px',
position: 'relative',
display: 'inline-block',
padding: '5px',
}}
>
<Stack.Item height="20px">
<Flex justify="flex-end">
<Flex.Item>
<Button
icon="times"
color="red"
onClick={() => {
setModifyItemDimmer(null);
act('close_greyscale_menu');
}}
/>
</Flex.Item>
</Flex>
</Stack.Item>
<Stack.Item width="100%" height="100%">
<Flex>
<Flex.Item mr={1}>
<Section width="90px" height="160px">
<LoadoutModifyItemDisplay modifyItemDimmer={modifyItemDimmer} />
</Section>
</Flex.Item>
<Flex.Item width="310px">
<Section fill>
<LoadoutModifyButtons modifyItemDimmer={modifyItemDimmer} />
</Section>
</Flex.Item>
</Flex>
</Stack.Item>
<Stack.Item>
<Stack justify="center">
<Button
onClick={() => {
setModifyItemDimmer(null);
act('close_greyscale_menu');
}}
>
Done
</Button>
</Stack>
</Stack.Item>
</Stack>
</Dimmer>
);
};

View File

@@ -0,0 +1,46 @@
import { BooleanLike } from '../../../../common/react';
import { PreferencesMenuData } from '../data';
import { LoadoutButton } from './ModifyPanel';
// Generic types
export type DmIconFile = string;
export type DmIconState = string;
export type FAIcon = string;
export type typePath = string;
type LoadoutInfoKey = string;
type LoadoutInfoValue = string;
// Info about a loadout item (key to info, such as color, reskin, layer, etc)
type LoadoutListInfo = Record<LoadoutInfoKey, LoadoutInfoValue> | [];
// Typepath to info about the item
export type LoadoutList = Record<typePath, LoadoutListInfo>;
// Used for holding reskin information
export type ReskinOption = {
name: string;
tooltip: string;
skin_icon_state: DmIconState; // The icon is the same as the item icon
};
// Actual item passed in from the loadout
export type LoadoutItem = {
name: string;
path: typePath;
icon: DmIconFile | null;
icon_state: DmIconState | null;
buttons: LoadoutButton[];
reskins: ReskinOption[] | null;
information: string[];
};
// Category of items in the loadout
export type LoadoutCategory = {
name: string;
category_icon: FAIcon | null;
category_info: string | null;
contents: LoadoutItem[];
};
export type LoadoutManagerData = PreferencesMenuData & {
job_clothes: BooleanLike;
};

View File

@@ -0,0 +1,337 @@
import { Fragment, useState } from 'react';
import { useBackend } from '../../../backend';
import {
Box,
Button,
Divider,
Icon,
Input,
NoticeBox,
Section,
Stack,
Tabs,
} from '../../../components';
import { CharacterPreview } from '../../common/CharacterPreview';
import { ServerData } from '../data';
import { ServerPreferencesFetcher } from '../ServerPreferencesFetcher';
import {
LoadoutCategory,
LoadoutItem,
LoadoutManagerData,
typePath,
} from './base';
import { ItemIcon, LoadoutTabDisplay, SearchDisplay } from './ItemDisplay';
import { LoadoutModifyDimmer } from './ModifyPanel';
export const LoadoutPage = () => {
return (
<ServerPreferencesFetcher
render={(serverData) => {
if (!serverData) {
return <NoticeBox>Loading...</NoticeBox>;
}
const loadoutServerData: ServerData = serverData;
return (
<LoadoutPageInner
loadout_tabs={loadoutServerData.loadout.loadout_tabs}
/>
);
}}
/>
);
};
const LoadoutPageInner = (props: { loadout_tabs: LoadoutCategory[] }) => {
const { loadout_tabs } = props;
const [searchLoadout, setSearchLoadout] = useState('');
const [selectedTabName, setSelectedTab] = useState(loadout_tabs[0].name);
const [modifyItemDimmer, setModifyItemDimmer] = useState<LoadoutItem | null>(
null,
);
return (
<Stack vertical fill>
<Stack.Item>
{!!modifyItemDimmer && (
<LoadoutModifyDimmer
modifyItemDimmer={modifyItemDimmer}
setModifyItemDimmer={setModifyItemDimmer}
/>
)}
<Section
title="&nbsp;"
align="center"
buttons={
<Input
width="200px"
onInput={(_, value) => setSearchLoadout(value)}
placeholder="Search for an item..."
value={searchLoadout}
/>
}
>
<Tabs fluid align="center">
{loadout_tabs.map((curTab) => (
<Tabs.Tab
key={curTab.name}
selected={
searchLoadout.length <= 1 && curTab.name === selectedTabName
}
onClick={() => {
setSelectedTab(curTab.name);
setSearchLoadout('');
}}
>
<Box>
{curTab.category_icon && (
<Icon name={curTab.category_icon} mr={1} />
)}
{curTab.name}
</Box>
</Tabs.Tab>
))}
</Tabs>
</Section>
</Stack.Item>
<Stack.Item>
<LoadoutTabs
loadout_tabs={loadout_tabs}
currentTab={selectedTabName}
currentSearch={searchLoadout}
modifyItemDimmer={modifyItemDimmer}
setModifyItemDimmer={setModifyItemDimmer}
/>
</Stack.Item>
</Stack>
);
};
const LoadoutTabs = (props: {
loadout_tabs: LoadoutCategory[];
currentTab: string;
currentSearch: string;
modifyItemDimmer: LoadoutItem | null;
setModifyItemDimmer: (dimmer: LoadoutItem | null) => void;
}) => {
const {
loadout_tabs,
currentTab,
currentSearch,
modifyItemDimmer,
setModifyItemDimmer,
} = props;
const activeCategory = loadout_tabs.find((curTab) => {
return curTab.name === currentTab;
});
const searching = currentSearch.length > 1;
return (
<Stack fill height="550px">
<Stack.Item align="center" width="250px" height="100%">
<Stack vertical fill>
<Stack.Item height="60%">
<LoadoutPreviewSection />
</Stack.Item>
<Stack.Item grow>
<LoadoutSelectedSection
all_tabs={loadout_tabs}
modifyItemDimmer={modifyItemDimmer}
setModifyItemDimmer={setModifyItemDimmer}
/>
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow>
{searching || activeCategory?.contents ? (
<Section
title={searching ? 'Searching...' : 'Catalog'}
fill
scrollable
buttons={
activeCategory?.category_info ? (
<Box italic mt={0.5}>
{activeCategory.category_info}
</Box>
) : null
}
>
<Stack vertical>
<Stack.Item>
{searching ? (
<SearchDisplay
loadout_tabs={loadout_tabs}
currentSearch={currentSearch}
/>
) : (
<LoadoutTabDisplay category={activeCategory} />
)}
</Stack.Item>
</Stack>
</Section>
) : (
<Section fill>
<Box>No contents for selected tab.</Box>
</Section>
)}
</Stack.Item>
</Stack>
);
};
const typepathToLoadoutItem = (
typepath: typePath,
all_tabs: LoadoutCategory[],
) => {
// Maybe a bit inefficient, could be replaced with a hashmap?
for (const tab of all_tabs) {
for (const item of tab.contents) {
if (item.path === typepath) {
return item;
}
}
}
return null;
};
const LoadoutSelectedItem = (props: {
path: typePath;
all_tabs: LoadoutCategory[];
modifyItemDimmer: LoadoutItem | null;
setModifyItemDimmer: (dimmer: LoadoutItem | null) => void;
}) => {
const { all_tabs, path, modifyItemDimmer, setModifyItemDimmer } = props;
const { act } = useBackend();
const item = typepathToLoadoutItem(path, all_tabs);
if (!item) {
return null;
}
return (
<Stack align={'center'}>
<Stack.Item>
<ItemIcon item={item} scale={1} />
</Stack.Item>
<Stack.Item width="55%">{item.name}</Stack.Item>
{item.buttons.length ? (
<Stack.Item>
<Button
color="none"
width="32px"
onClick={() => {
setModifyItemDimmer(item);
}}
>
<Icon size={1.8} name="cogs" color="grey" />
</Button>
</Stack.Item>
) : (
<Stack.Item width="32px" /> // empty space
)}
<Stack.Item>
<Button
color="none"
width="32px"
onClick={() => act('select_item', { path: path, deselect: true })}
>
<Icon size={2.4} name="times" color="red" />
</Button>
</Stack.Item>
</Stack>
);
};
const LoadoutSelectedSection = (props: {
all_tabs: LoadoutCategory[];
modifyItemDimmer: LoadoutItem | null;
setModifyItemDimmer: (dimmer: LoadoutItem | null) => void;
}) => {
const { act, data } = useBackend<LoadoutManagerData>();
const { loadout_list } = data.character_preferences.misc;
const { all_tabs, modifyItemDimmer, setModifyItemDimmer } = props;
return (
<Section
title="&nbsp;"
scrollable
fill
buttons={
<Button.Confirm
icon="times"
color="red"
align="center"
disabled={!loadout_list || Object.keys(loadout_list).length === 0}
tooltip="Clears ALL selected items from all categories."
onClick={() => act('clear_all_items')}
>
Clear All
</Button.Confirm>
}
>
{loadout_list &&
Object.entries(loadout_list).map(([path, item]) => (
<Fragment key={path}>
<LoadoutSelectedItem
path={path}
all_tabs={all_tabs}
modifyItemDimmer={modifyItemDimmer}
setModifyItemDimmer={setModifyItemDimmer}
/>
<Divider />
</Fragment>
))}
</Section>
);
};
const LoadoutPreviewSection = () => {
const { act, data } = useBackend<LoadoutManagerData>();
return (
<Section
fill
title="&nbsp;"
buttons={
<Button.Checkbox
align="center"
checked={data.job_clothes}
onClick={() => act('toggle_job_clothes')}
>
Job Clothes
</Button.Checkbox>
}
>
<Stack vertical fill>
<Stack.Item grow align="center">
<CharacterPreview height="100%" id={data.character_preview_view} />
</Stack.Item>
<Stack.Divider />
<Stack.Item align="center">
<Stack>
<Stack.Item>
<Button
icon="chevron-left"
onClick={() =>
act('rotate_dummy', {
dir: 'left',
})
}
/>
</Stack.Item>
<Stack.Item>
<Button
icon="chevron-right"
onClick={() =>
act('rotate_dummy', {
dir: 'right',
})
}
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Section>
);
};

View File

@@ -1,7 +0,0 @@
import { FeatureChoiced } from '../base';
import { FeatureDropdownInput } from '../dropdowns';
export const pride_pin: FeatureChoiced = {
name: 'Pride Pin',
component: FeatureDropdownInput,
};