From 2251ace22bd5ef4f2e3bf5adbe53f45dbb60dbfd Mon Sep 17 00:00:00 2001 From: Minty <120209597+MintyPhresh@users.noreply.github.com> Date: Sat, 1 Aug 2026 19:21:06 -0400 Subject: [PATCH] Adds an item to match Proteans and wearers (#5861) --- code/modules/loadout/loadout_helpers.dm | 10 +- code/modules/loadout/loadout_items.dm | 2 + .../modules/clothing/accessory/accessory.dm | 185 +++++++++++++++++- .../species/proteans/organs/protean_heart.dm | 2 +- .../proteans/organs/protean_stomach.dm | 1 + .../code/modules/loadout/categories/pocket.dm | 8 +- .../code/modules/vending/wardrobe.dm | 2 +- 7 files changed, 199 insertions(+), 11 deletions(-) diff --git a/code/modules/loadout/loadout_helpers.dm b/code/modules/loadout/loadout_helpers.dm index c4cbb429501..89bd2b32d6f 100644 --- a/code/modules/loadout/loadout_helpers.dm +++ b/code/modules/loadout/loadout_helpers.dm @@ -44,13 +44,13 @@ // BUBBER EDIT ADDITION BEGIN if(item.restricted_roles && equipping && !(equipping.title in item.restricted_roles)) if(preference_source.parent) - to_chat(preference_source.parent, span_warning("You were unable to get a loadout item([initial(item.item_path.name)]) due to job restrictions!")) + to_chat(preference_source.parent, span_warning("You were unable to get a loadout item ([initial(item.item_path.name)]) due to job restrictions!")) loadout_datums -= item continue if(item.blacklisted_roles && equipping && (equipping.title in item.blacklisted_roles)) if(preference_source.parent) - to_chat(preference_source.parent, span_warning("You were unable to get a loadout item([initial(item.item_path.name)]) due to job blacklists!")) + to_chat(preference_source.parent, span_warning("You were unable to get a loadout item ([initial(item.item_path.name)]) due to job blacklists!")) loadout_datums -= item continue @@ -60,6 +60,12 @@ loadout_datums -= item continue + if(item.blacklisted_species && (dna.species.id in item.blacklisted_species)) + if(preference_source.parent) + to_chat(preference_source.parent, span_warning("You were unable to get a loadout item ([initial(item.item_path.name)]) due to species blacklists!")) + loadout_datums -= item + continue + if(item.donator_only && !SSplayer_ranks.is_donator(preference_source?.parent)) if(preference_source.parent) to_chat(preference_source.parent, span_warning("You were unable to get a loadout item ([initial(item.item_path.name)]) due to donator restrictions!")) diff --git a/code/modules/loadout/loadout_items.dm b/code/modules/loadout/loadout_items.dm index 8d175624965..ee787a0cffc 100644 --- a/code/modules/loadout/loadout_items.dm +++ b/code/modules/loadout/loadout_items.dm @@ -70,6 +70,8 @@ GLOBAL_LIST_INIT(all_loadout_categories, init_loadout_categories()) var/list/blacklisted_roles /// If set, is a list of species which can get the loadout item var/list/restricted_species + /// If set, is a list of species which can't get the loadout item + var/list/blacklisted_species /// Whether the item is restricted to supporters var/donator_only /// Whether the item requires a specific season in order to be available diff --git a/modular_zubbers/code/modules/clothing/accessory/accessory.dm b/modular_zubbers/code/modules/clothing/accessory/accessory.dm index e5870520d06..ab187a53732 100644 --- a/modular_zubbers/code/modules/clothing/accessory/accessory.dm +++ b/modular_zubbers/code/modules/clothing/accessory/accessory.dm @@ -172,6 +172,185 @@ attachment_slot = NONE above_suit = TRUE +/// This list is for Protean Match tags, and stores how many tags are waiting to be matched. +GLOBAL_LIST_EMPTY_TYPED(protean_match_tag_pool, /obj/item/clothing/accessory/dogtags/protean_match) +/// This list is for Protean Match tags, and stores every tag that's currently assigned to someone. Intended to be used to prevent one person having multiple tags. +GLOBAL_LIST_EMPTY_TYPED(all_assigned_protean_match_tags, /obj/item/clothing/accessory/dogtags/protean_match) + +// The tag pool, contains either unmatched Wearers or unmatched Proteans, waiting for the opposite type to join and match them. +// It should never ever ever contain both, since if the second type tries to join the list it'll just get paired. +// So if we need to differentiate between if there's Proteans or Wearers in the pool, we just check the first entry. +#define POOL GLOB.protean_match_tag_pool +#define ASSIGNED_TAGS GLOB.all_assigned_protean_match_tags +#define UNASSIGNED "0" +#define IN_POOL "1" +#define MATCHED "2" + +/obj/item/clothing/accessory/dogtags/protean_match + name = "\improper Protean Match tag" + desc = "A small tag from NT's Protean Match program, which pairs Protean crew with interested wearers." + worn_icon = null // People shouldn't have to abandon 5 pixels of swag and aura to wear this + custom_premium_price = PAYCHECK_CREW * 0.4 + /// Name of the person assigned to the tag + var/assignee_name + /// the "true" name of the assignee, source.mind.name, used to keep one person from putting a bunch of their own tags in the pool. + var/assignee_truename + /// Is the person assigned to the tag a protean? TRUE if yes + var/assignee_protean + /// What step of the matching journey + var/match_progress = UNASSIGNED + var/datum/weakref/paired_tag_weakref + +///This is how a person can assign a tag to themself, or switch if they're in the candidate pool +/obj/item/clothing/accessory/dogtags/protean_match/attack_self(mob/user) + switch(match_progress) + if(UNASSIGNED) + assign(user) + // match_progress and sounds are set within assign() + if(IN_POOL) + to_chat(user, "You press the button on the tag, removing it from the Protean Match candidate pool and unclaiming it.") + playsound(src, "modular_skyrat/modules/emotes/sound/emotes/synth_no.ogg", 20, FALSE) + balloon_alert(user, "tag unassigned") + + unassign() + return + if(MATCHED) + var/obj/item/clothing/accessory/dogtags/protean_match/paired_tag = paired_tag_weakref?.resolve() + var/safety = (tgui_alert(user, "You are currently matched with [paired_tag?.assignee_name]. Are you sure you want to unclaim your tag? This will unassign both tags.", "Unclaim tag?", list("Unclaim", "Cancel"))) + if(safety == "Cancel" || !in_range(src, user)) + return + to_chat(user, "You press the button on the tag, unmatching with [paired_tag?.assignee_name] and exiting the Protean Match candidate pool.") + playsound(src, "modular_skyrat/modules/emotes/sound/emotes/synth_no.ogg", 20, FALSE) + balloon_alert(user, "unmatched tag") + + paired_tag.unassign("matched tag deactivated") + unassign() + return + +/obj/item/clothing/accessory/dogtags/protean_match/click_alt(mob/user) + if(length(POOL) ? (assignee_protean == POOL[1].assignee_protean) : TRUE) + playsound(src, "sound/machines/buzz/buzz-sigh.ogg", 20, FALSE) + balloon_alert(user, "no other valid matches in pool") + return + if(match_progress == MATCHED) + var/obj/item/clothing/accessory/dogtags/protean_match/paired_tag = paired_tag_weakref?.resolve() + var/safety = (tgui_alert(user, "You are currently matched with [paired_tag?.assignee_name]. Are you sure you want to attempt to match with someone else in the pool? This will unassign the other user's tag.", "Re-match tag?", list("Re-match", "Cancel"))) + if(safety == "Cancel" || !in_range(src, user)) + return + unassign() + assign(user) //This comes before the paired tag is unassigned so that it doesn't consider the existing pairing as a valid candidate + paired_tag.unassign("matched tag deactivated") + +/obj/item/clothing/accessory/dogtags/protean_match/Destroy() + POOL -= src + ASSIGNED_TAGS -= src + if(match_progress == MATCHED) + var/obj/item/clothing/accessory/dogtags/protean_match/paired_tag = paired_tag_weakref?.resolve() + paired_tag.unassign("paired tag destroyed") + return ..() + +///Filters out crew that can't wear Proteans or shouldn't have them. +/obj/item/clothing/accessory/dogtags/protean_match/proc/assign(mob/source) + . = TRUE + /// If this ends up not being null, the assignment fails and returns the reason + var/ineligibility_reason = null + var/mob/living/user + var/obj/item/card/id/user_id + var/user_id_name + + ///Runs through a bunch of reasons why you shouldn't get to be in the pool. If it finds one it sets the ineligibility_reason to a string, which makes the assignment fail + if(isliving(source)) + user = source + user_id = user.get_idcard(TRUE) + user_id_name = user_id?.get_displayed_name() + var/turf/station_check = get_turf(user) + + for(var/obj/item/clothing/accessory/dogtags/protean_match/tag as anything in ASSIGNED_TAGS) + if(user_id_name == tag.assignee_name) + ineligibility_reason = "existing tag has that name" + break + if(source.mind.name == tag.assignee_truename) //Checks the user's "true name" to prevent trolling with multiple cardboard IDs + ineligibility_reason = "one per person, please" + break + if(isnull(user_id)) + ineligibility_reason = "no ID found" + else if(!station_check || !is_station_level(station_check.z)) + ineligibility_reason = "not on station Z-level" + else if(user.has_quirk(/datum/quirk/equipping/entombed)) + ineligibility_reason = "MOD entombed" + else if(iscarbon(user)) + var/mob/living/carbon/carbon_user = user + if(carbon_user.dna.species.id == SPECIES_SNAIL) + ineligibility_reason = "snailperson" + else + ineligibility_reason = "invalid being" + + if(ineligibility_reason) + playsound(src, "sound/machines/buzz/buzz-sigh.ogg", 20, FALSE) + balloon_alert(source, "ineligible: [ineligibility_reason]") + return + +///From here, we assign a name and species to the tag, and try to match it. If it fails, adds it to the pool. + assignee_name = user_id_name + assignee_truename = source.mind.name + assignee_protean = isprotean(source) + ASSIGNED_TAGS += src + name += " - [assignee_name] ([assignee_protean ? "Protean" : "Wearer"])" + update_static_data_for_all_viewers() +//If you aren't the same as the other people in the pool, you get matched with one of them + if(length(POOL)) + if(assignee_protean != POOL[1].assignee_protean) + var/obj/item/clothing/accessory/dogtags/protean_match/matched_wearer_tag = pick(POOL) + matched_wearer_tag.handle_match(src) //Makes the match's tag ping + handle_match(matched_wearer_tag) //Makes our tag ping + return + // This should only run if the code block above fails + POOL += src + match_progress = IN_POOL + say("No match found, added to candidate pool! There are [(length(POOL) - 1) ? "[length(POOL) - 1]" : "no" ] other [assignee_protean ? "Proteans" : "Wearers"] in the pool.") + +///Handles the logistics of matching tags. Done by both sets of tags. +/obj/item/clothing/accessory/dogtags/protean_match/proc/handle_match(obj/item/clothing/accessory/dogtags/protean_match/other_tag) + match_progress = MATCHED + POOL -= src + playsound(src, "modular_skyrat/modules/emotes/sound/emotes/synth_yes.ogg", 20, FALSE) + say("Matched with [(other_tag.assignee_protean == TRUE) ? "Protean" : "Wearer"]: [other_tag.assignee_name]!") + paired_tag_weakref = other_tag.create_weakref() + +/// Used both when unmatching with a person and when leaving the match pool. +/obj/item/clothing/accessory/dogtags/protean_match/proc/unassign(unmatch_reason) + match_progress = UNASSIGNED + POOL -= src + ASSIGNED_TAGS -= src + assignee_name = null + assignee_truename = null + assignee_protean = null + name = initial(name) + paired_tag_weakref = null + update_static_data_for_all_viewers() + if(unmatch_reason) + say("You have been unmatched. Reason: [unmatch_reason].") + +/obj/item/clothing/accessory/dogtags/protean_match/examine(mob/user) + . = ..() + if(!(in_range(user, src))) + return + if(assignee_name) + . += span_notice("This set is assigned to [assignee_name], a [assignee_protean ? "Protean" : "Wearer"].") + switch(match_progress) + if(UNASSIGNED) + . += span_notice("You can join the program by using it in-hand.") + if(IN_POOL) + . += span_notice("It's currently in the candidate pool, alongside [length(POOL) - 1] others. Leave the pool by using it in-hand.") + if(MATCHED) + var/obj/item/clothing/accessory/dogtags/protean_match/paired_tag = paired_tag_weakref?.resolve() + . += span_notice("It's currently matched with [paired_tag.assignee_name], and can be unmatched by using it in-hand. To match with a different person in the queue, alt-click.") + +#undef POOL +#undef ASSIGNED_TAGS +#undef UNASSIGNED +#undef IN_POOL +#undef MATCHED /* Greyscaled Medals @@ -631,9 +810,3 @@ Potential future ideas: name = "\improper GalFed Official neckpin" desc = "A special golden neckpin to show true loyalty to the Federation." greyscale_colors = "#ffff66#0099ff" - -/obj/item/clothing/accessory/medal/gold/ - worn_icon_teshari = 'modular_zubbers/icons/mob/clothing/accessories_teshari.dmi' - -/obj/item/clothing/accessory/medal/gold/captain - worn_icon_teshari = 'modular_zubbers/icons/mob/clothing/accessories_teshari.dmi' diff --git a/modular_zubbers/code/modules/customization/species/proteans/organs/protean_heart.dm b/modular_zubbers/code/modules/customization/species/proteans/organs/protean_heart.dm index ec7bdf90c0a..81d8539a1dd 100644 --- a/modular_zubbers/code/modules/customization/species/proteans/organs/protean_heart.dm +++ b/modular_zubbers/code/modules/customization/species/proteans/organs/protean_heart.dm @@ -4,4 +4,4 @@ icon = PROTEAN_ORGAN_SPRITE icon_state = "orchestrator" organ_flags = ORGAN_ROBOTIC | ORGAN_NANOMACHINE - + custom_premium_price = PAYCHECK_COMMAND * 2 diff --git a/modular_zubbers/code/modules/customization/species/proteans/organs/protean_stomach.dm b/modular_zubbers/code/modules/customization/species/proteans/organs/protean_stomach.dm index 7e8296ea1f2..6a623c2d7ec 100644 --- a/modular_zubbers/code/modules/customization/species/proteans/organs/protean_stomach.dm +++ b/modular_zubbers/code/modules/customization/species/proteans/organs/protean_stomach.dm @@ -6,6 +6,7 @@ icon_state = "refactory" organ_flags = ORGAN_ROBOTIC | ORGAN_NANOMACHINE organ_traits = list(TRAIT_NOHUNGER) + custom_premium_price = PAYCHECK_COMMAND * 2 /// How much max metal can we hold at any given time (In sheets). This isn't using nutrition code because nutrition code gets weird without livers. var/metal_max = PROTEAN_STOMACH_FULL diff --git a/modular_zubbers/code/modules/loadout/categories/pocket.dm b/modular_zubbers/code/modules/loadout/categories/pocket.dm index 928f004fbd4..b6474dda073 100644 --- a/modular_zubbers/code/modules/loadout/categories/pocket.dm +++ b/modular_zubbers/code/modules/loadout/categories/pocket.dm @@ -29,9 +29,15 @@ item_path = /obj/item/storage/fancy/cigarettes/cigpack_mindbreaker /datum/loadout_item/pocket_items/paintingprinter - name = "instant painting printer" + name = "Instant Painting Printer" item_path = /obj/item/modular_computer/mini_painting_printer +/datum/loadout_item/pocket_items/protean_match + name = "Protean Match Tags" + item_path = /obj/item/clothing/accessory/dogtags/protean_match + blacklisted_species = list(SPECIES_SNAIL) //They can't wear proteans + blacklisted_roles = list(JOB_PRISONER) //no, prisoners do not get a spaceproof suit + /* * FLAGS */ diff --git a/modular_zubbers/code/modules/vending/wardrobe.dm b/modular_zubbers/code/modules/vending/wardrobe.dm index ef100bff548..38bf0adc362 100644 --- a/modular_zubbers/code/modules/vending/wardrobe.dm +++ b/modular_zubbers/code/modules/vending/wardrobe.dm @@ -137,8 +137,8 @@ premium = list( /obj/item/organ/stomach/protean = 1, /obj/item/organ/heart/protean = 1, + /obj/item/clothing/accessory/dogtags/protean_match = 6, ) - extra_price = PAYCHECK_COMMAND * 2.0 // This ensures the protean organs are ~200 credits each. /obj/machinery/vending/wardrobe/robo_wardrobe/Initialize(mapload) product_categories[1]["products"] += products