diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm index 3418dfcb119..ab0fbc9d781 100644 --- a/code/__DEFINES/dcs/signals/signals_object.dm +++ b/code/__DEFINES/dcs/signals/signals_object.dm @@ -126,12 +126,19 @@ #define COMSIG_ITEM_DROPPED "item_drop" ///from base of obj/item/pickup(): (/mob/taker) #define COMSIG_ITEM_PICKUP "item_pickup" +///from base of obj/item/on_outfit_equip(): (mob/equipper, visuals_only, slot) +#define COMSIG_ITEM_EQUIPPED_AS_OUTFIT "item_equip_as_outfit" /// Sebt from obj/item/ui_action_click(): (mob/user, datum/action) #define COMSIG_ITEM_UI_ACTION_CLICK "item_action_click" /// Return to prevent the default behavior (attack_selfing) from ocurring. #define COMPONENT_ACTION_HANDLED (1<<0) +/// Sent from obj/item/item_action_slot_check(): (mob/user, datum/action, slot) +#define COMSIG_ITEM_UI_ACTION_SLOT_CHECKED "item_action_slot_checked" + /// Return to prevent the default behavior (attack_selfing) from ocurring. + #define COMPONENT_ITEM_ACTION_SLOT_INVALID (1<<0) + ///from base of mob/living/carbon/attacked_by(): (mob/living/carbon/target, mob/living/user, hit_zone) #define COMSIG_ITEM_ATTACK_ZONE "item_attack_zone" ///from base of obj/item/hit_reaction(): (owner, hitby, attack_text, final_block_chance, damage, attack_type, damage_type) diff --git a/code/datums/components/toggle_attached_clothing.dm b/code/datums/components/toggle_attached_clothing.dm new file mode 100644 index 00000000000..5af2c187ff2 --- /dev/null +++ b/code/datums/components/toggle_attached_clothing.dm @@ -0,0 +1,207 @@ +/** + * Component which allows clothing to deploy a different kind of clothing onto you. + * The simplest example is hooded suits deploying hoods onto your head. + */ +/datum/component/toggle_attached_clothing + /// Instance of the item we're creating + var/obj/item/deployable + /// Action used to toggle deployment + var/datum/action/item_action/toggle_action + /// Typepath of what we're creating + var/deployable_type + /// Which slot this item equips into + var/equipped_slot + /// Name of toggle action + var/action_name = "" + /// If true, we delete our deployable on toggle rather than putting it in nullspace + var/destroy_on_removal + /// Current state of our deployable equipment + var/currently_deployed = FALSE + /// What should be added to the end of the parent icon state when equipment is deployed? Set to "" for no change + var/parent_icon_state_suffix = "" + /// Icon state for overlay to display over the parent item while deployable item is not deployed + var/down_overlay_state_suffix = "" + /// Overlay to display over the parent item while deployable item is not deployed + var/mutable_appearance/undeployed_overlay + /// Optional callback triggered before deploying, return TRUE to continue or FALSE to cancel + var/datum/callback/pre_creation_check + /// Optional callback triggered when we create our deployable equipment + var/datum/callback/on_created + /// Optional callback triggered when we have deployed our equipment + var/datum/callback/on_deployed + /// Optional callback triggered before we hide our equipment, before as we may delete it afterwards + var/datum/callback/on_removed + +/datum/component/toggle_attached_clothing/Initialize( + deployable_type, + equipped_slot, + action_name = "Toggle", + destroy_on_removal = FALSE, + parent_icon_state_suffix = "", + down_overlay_state_suffix = "", + datum/callback/pre_creation_check, + datum/callback/on_created, + datum/callback/on_deployed, + datum/callback/on_removed, +) + . = ..() + if (!isitem(parent)) + return COMPONENT_INCOMPATIBLE + if (!deployable_type || !equipped_slot) + return COMPONENT_INCOMPATIBLE // Not strictly true but INITIALIZE_HINT_QDEL doesn't work from components + src.deployable_type = deployable_type + src.equipped_slot = equipped_slot + src.destroy_on_removal = destroy_on_removal + src.parent_icon_state_suffix = parent_icon_state_suffix + src.down_overlay_state_suffix = down_overlay_state_suffix + src.pre_creation_check = pre_creation_check + src.on_created = on_created + src.on_deployed = on_deployed + src.on_removed = on_removed + + var/obj/item/clothing_parent = parent + toggle_action = new(parent) + toggle_action.name = action_name + clothing_parent.add_item_action(toggle_action) + + RegisterSignal(parent, COMSIG_ITEM_UI_ACTION_CLICK, PROC_REF(on_toggle_pressed)) + RegisterSignal(parent, COMSIG_ITEM_UI_ACTION_SLOT_CHECKED, PROC_REF(on_action_slot_checked)) + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_parent_equipped)) + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED_AS_OUTFIT, PROC_REF(on_parent_equipped_outfit)) + if (down_overlay_state_suffix) + var/overlay_state = "[initial(clothing_parent.icon_state)][down_overlay_state_suffix]" + undeployed_overlay = mutable_appearance(initial(clothing_parent.worn_icon), overlay_state, -SUIT_LAYER) + RegisterSignal(parent, COMSIG_ITEM_GET_WORN_OVERLAYS, PROC_REF(on_checked_overlays)) + clothing_parent.update_slot_icon() + + if (!destroy_on_removal) + create_deployable() + +/datum/component/toggle_attached_clothing/Destroy(force, silent) + if (deployable) + var/obj/item/parent_gear = parent + var/mob/living/carbon/human/wearer = parent_gear.loc + if (istype(wearer)) + wearer.dropItemToGround(deployable, force = TRUE, silent = TRUE) + QDEL_NULL(deployable) + QDEL_NULL(toggle_action) + QDEL_NULL(on_created) + QDEL_NULL(on_deployed) + QDEL_NULL(on_removed) + return ..() + +/// Toggle deployable when the UI button is clicked +/datum/component/toggle_attached_clothing/proc/on_toggle_pressed(obj/item/source, mob/user, datum/action) + SIGNAL_HANDLER + if (action != toggle_action) + return + toggle_deployable() + return COMPONENT_ACTION_HANDLED + +/// Called when action attempts to check what slot the item is worn in +/datum/component/toggle_attached_clothing/proc/on_action_slot_checked(obj/item/clothing/source, mob/user, datum/action, slot) + SIGNAL_HANDLER + if (action != toggle_action) + return + if (!(source.slot_flags & slot)) + return COMPONENT_ITEM_ACTION_SLOT_INVALID + +/// Apply an overlay while the item is not deployed +/datum/component/toggle_attached_clothing/proc/on_checked_overlays(obj/item/source, list/overlays, mutable_appearance/standing, isinhands, icon_file) + SIGNAL_HANDLER + if (isinhands || currently_deployed) + return + overlays += undeployed_overlay + +/// Deploys gear if it is hidden, hides it if it is deployed +/datum/component/toggle_attached_clothing/proc/toggle_deployable() + if (currently_deployed) + remove_deployable() + return + + var/obj/item/parent_gear = parent + if (!ishuman(parent_gear.loc)) + return + var/mob/living/carbon/human/wearer = parent_gear.loc + if (wearer.is_holding(parent_gear)) + parent_gear.balloon_alert(wearer, "wear it first!") + return + if (wearer.get_item_by_slot(equipped_slot)) + parent_gear.balloon_alert(wearer, "slot occupied!") + return + if (!deployable && !create_deployable()) + return + if (!wearer.equip_to_slot_if_possible(deployable, slot = equipped_slot)) + if(destroy_on_removal) + remove_deployable() + return + currently_deployed = TRUE + on_deployed?.Invoke(deployable) + if (parent_icon_state_suffix) + parent_gear.icon_state = "[initial(parent_gear.icon_state)][parent_icon_state_suffix]" + parent_gear.worn_icon_state = parent_gear.icon_state + parent_gear.update_slot_icon() + wearer.update_mob_action_buttons() + +/// Undeploy gear if it moves slots somehow +/datum/component/toggle_attached_clothing/proc/on_parent_equipped(obj/item/clothing/source, mob/equipper, slot) + SIGNAL_HANDLER + if (slot & equipped_slot) + return + remove_deployable() + +/// Display deployed if worn in an outfit +/datum/component/toggle_attached_clothing/proc/on_parent_equipped_outfit(obj/item/clothing/source, mob/equipper, visuals_only, slot) + SIGNAL_HANDLER + create_deployable() + toggle_deployable() + +/// Create our gear, returns true if we actually made anything +/datum/component/toggle_attached_clothing/proc/create_deployable() + if (deployable) + return FALSE + if (pre_creation_check && !pre_creation_check.Invoke()) + return FALSE + deployable = new deployable_type(parent) + if (!istype(deployable)) + stack_trace("Tried to create non-clothing item from toggled clothing.") + RegisterSignal(deployable, COMSIG_ITEM_DROPPED, PROC_REF(on_deployed_dropped)) + RegisterSignal(deployable, COMSIG_ITEM_EQUIPPED, PROC_REF(on_deployed_equipped)) + RegisterSignal(deployable, COMSIG_QDELETING, PROC_REF(on_deployed_destroyed)) + on_created?.Invoke(deployable) + return TRUE + +/// Undeploy gear if you drop it +/datum/component/toggle_attached_clothing/proc/on_deployed_dropped() + SIGNAL_HANDLER + remove_deployable() + +/// Undeploy gear if it moves slots somehow +/datum/component/toggle_attached_clothing/proc/on_deployed_equipped(obj/item/clothing/source, mob/equipper, slot) + SIGNAL_HANDLER + if (source.slot_flags & slot) + return + remove_deployable() + +/// Undeploy gear if it is deleted +/datum/component/toggle_attached_clothing/proc/on_deployed_destroyed() + SIGNAL_HANDLER + remove_deployable() + deployable = null + +/// Removes our deployed equipment from the wearer +/datum/component/toggle_attached_clothing/proc/remove_deployable() + deployable?.forceMove(parent) + if (!currently_deployed) + return + currently_deployed = FALSE + on_removed?.Invoke(deployable) + + var/obj/item/parent_gear = parent + if (destroy_on_removal) + QDEL_NULL(deployable) + else if (parent_icon_state_suffix) + parent_gear.icon_state = "[initial(parent_gear.icon_state)]" + parent_gear.worn_icon_state = parent_gear.icon_state + parent_gear.update_slot_icon() + parent_gear.update_item_action_buttons() diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 5d6f5273743..d92b930cd6d 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -678,7 +678,7 @@ /// Gives one of our item actions to a mob, when equipped to a certain slot /obj/item/proc/give_item_action(datum/action/action, mob/to_who, slot) // Some items only give their actions buttons when in a specific slot. - if(!item_action_slot_check(slot, to_who)) + if(!item_action_slot_check(slot, to_who, action) || !(SEND_SIGNAL(src, COMSIG_ITEM_UI_ACTION_SLOT_CHECKED, to_who, action, slot) & COMPONENT_ITEM_ACTION_SLOT_INVALID)) // There is a chance we still have our item action currently, // and are moving it from a "valid slot" to an "invalid slot". // So call Remove() here regardless, even if excessive. @@ -688,7 +688,7 @@ action.Grant(to_who) /// Sometimes we only want to grant the item's action if it's equipped in a specific slot. -/obj/item/proc/item_action_slot_check(slot, mob/user) +/obj/item/proc/item_action_slot_check(slot, mob/user, datum/action/action) if(slot & (ITEM_SLOT_BACKPACK|ITEM_SLOT_LEGCUFFED)) //these aren't true slots, so avoid granting actions there return FALSE return TRUE @@ -1222,9 +1222,9 @@ source_item?.reagents?.add_reagent(/datum/reagent/blood, 2) else if(custom_materials?.len) //if we've got materials, lets see whats in it - /// How many mats have we found? You can only be affected by two material datums by default + // How many mats have we found? You can only be affected by two material datums by default var/found_mats = 0 - /// How much of each material is in it? Used to determine if the glass should break + // How much of each material is in it? Used to determine if the glass should break var/total_material_amount = 0 for(var/mats in custom_materials) @@ -1239,7 +1239,7 @@ //if there's glass in it and the glass is more than 60% of the item, then we can shatter it if(custom_materials[GET_MATERIAL_REF(/datum/material/glass)] >= total_material_amount * 0.60) if(prob(66)) //66% chance to break it - /// The glass shard that is spawned into the source item + // The glass shard that is spawned into the source item var/obj/item/shard/broken_glass = new /obj/item/shard(loc) broken_glass.name = "broken [name]" broken_glass.desc = "This used to be \a [name], but it sure isn't anymore." @@ -1256,7 +1256,7 @@ span_warning("Eugh! Did I just bite into something?")) else if(w_class == WEIGHT_CLASS_TINY) //small items like soap or toys that don't have mat datums - /// victim's chest (for cavity implanting the item) + // victim's chest (for cavity implanting the item) var/obj/item/bodypart/chest/victim_cavity = victim.get_bodypart(BODY_ZONE_CHEST) if(victim_cavity.cavity_item) victim.vomit(5, FALSE, FALSE, distance = 0) @@ -1327,7 +1327,8 @@ /// Special stuff you want to do when an outfit equips this item. /obj/item/proc/on_outfit_equip(mob/living/carbon/human/outfit_wearer, visuals_only, item_slot) - return + SHOULD_CALL_PARENT(TRUE) + SEND_SIGNAL(src, COMSIG_ITEM_EQUIPPED_AS_OUTFIT, outfit_wearer, visuals_only, item_slot) /// Whether or not this item can be put into a storage item through attackby /obj/item/proc/attackby_storage_insert(datum/storage, atom/storage_holder, mob/user) diff --git a/code/modules/antagonists/cult/cult.dm b/code/modules/antagonists/cult/cult.dm index 49a6d2b2bdb..74b0aff0025 100644 --- a/code/modules/antagonists/cult/cult.dm +++ b/code/modules/antagonists/cult/cult.dm @@ -576,6 +576,7 @@ uniform = /obj/item/clothing/under/color/black suit = /obj/item/clothing/suit/hooded/cultrobes/alt + head = /obj/item/clothing/head/hooded/cult_hoodie/alt shoes = /obj/item/clothing/shoes/cult/alt r_hand = /obj/item/melee/blood_magic/stun diff --git a/code/modules/antagonists/cult/cult_items.dm b/code/modules/antagonists/cult/cult_items.dm index a39f76aefe5..72d02d6b0ca 100644 --- a/code/modules/antagonists/cult/cult_items.dm +++ b/code/modules/antagonists/cult/cult_items.dm @@ -213,7 +213,16 @@ Striking a noncultist, however, will tear their flesh."} heat_protection = CHEST|GROIN|LEGS|ARMS max_heat_protection_temperature = ARMOR_MAX_TEMP_PROTECT hoodtype = /obj/item/clothing/head/hooded/cult_hoodie + /// Whether the hood is flipped up + var/hood_up = FALSE +/// Called when the hood is worn +/obj/item/clothing/suit/hooded/cultrobes/on_hood_up(obj/item/clothing/head/hooded/hood) + hood_up = TRUE + +/// Called when the hood is hidden +/obj/item/clothing/suit/hooded/cultrobes/on_hood_down(obj/item/clothing/head/hooded/hood) + hood_up = FALSE /datum/armor/hooded_cultrobes melee = 40 diff --git a/code/modules/antagonists/heretic/heretic_antag.dm b/code/modules/antagonists/heretic/heretic_antag.dm index 8d75a1d9b2f..5c246833530 100644 --- a/code/modules/antagonists/heretic/heretic_antag.dm +++ b/code/modules/antagonists/heretic/heretic_antag.dm @@ -758,4 +758,5 @@ name = "Heretic (Preview only)" suit = /obj/item/clothing/suit/hooded/cultrobes/eldritch + head = /obj/item/clothing/head/hooded/cult_hoodie/eldritch r_hand = /obj/item/melee/touch_attack/mansus_fist diff --git a/code/modules/antagonists/heretic/items/heretic_armor.dm b/code/modules/antagonists/heretic/items/heretic_armor.dm index 577f901d021..e15e10c8335 100644 --- a/code/modules/antagonists/heretic/items/heretic_armor.dm +++ b/code/modules/antagonists/heretic/items/heretic_armor.dm @@ -109,26 +109,23 @@ // Let examiners know this works as a focus only if the hood is down . += span_notice("Allows you to cast heretic spells while the hood is down.") -/obj/item/clothing/suit/hooded/cultrobes/void/RemoveHood() - // This is before the hood actually goes down - // We only make it visible if the hood is being moved from up to down - if(hood_up) - make_visible() - +/obj/item/clothing/suit/hooded/cultrobes/void/on_hood_down(obj/item/clothing/head/hooded/hood) + make_visible() return ..() -/obj/item/clothing/suit/hooded/cultrobes/void/MakeHood() +/obj/item/clothing/suit/hooded/cultrobes/void/can_create_hood() if(!isliving(loc)) CRASH("[src] attempted to make a hood on a non-living thing: [loc]") - var/mob/living/wearer = loc - if(!IS_HERETIC_OR_MONSTER(wearer)) - loc.balloon_alert(loc, "you can't get the hood up!") - return + if(IS_HERETIC_OR_MONSTER(wearer)) + return TRUE - // When we make the hood, that means we're going invisible + loc.balloon_alert(loc, "can't get the hood up!") + return FALSE + +/obj/item/clothing/suit/hooded/cultrobes/void/on_hood_created(obj/item/clothing/head/hooded/hood) + . = ..() make_invisible() - return ..() /// Makes our cloak "invisible". Not the wearer, the cloak itself. /obj/item/clothing/suit/hooded/cultrobes/void/proc/make_invisible() diff --git a/code/modules/antagonists/traitor/datum_traitor.dm b/code/modules/antagonists/traitor/datum_traitor.dm index 59c92c6d172..9a81645716d 100644 --- a/code/modules/antagonists/traitor/datum_traitor.dm +++ b/code/modules/antagonists/traitor/datum_traitor.dm @@ -354,6 +354,7 @@ uniform = /obj/item/clothing/under/color/grey suit = /obj/item/clothing/suit/hooded/ablative + head = /obj/item/clothing/head/hooded/ablative gloves = /obj/item/clothing/gloves/color/yellow mask = /obj/item/clothing/mask/gas l_hand = /obj/item/melee/energy/sword diff --git a/code/modules/clothing/suits/ablativecoat.dm b/code/modules/clothing/suits/ablativecoat.dm index 98df168aa56..0a6967753e1 100644 --- a/code/modules/clothing/suits/ablativecoat.dm +++ b/code/modules/clothing/suits/ablativecoat.dm @@ -46,22 +46,18 @@ if (prob(hit_reflect_chance)) return TRUE -/obj/item/clothing/suit/hooded/ablative/ToggleHood() +/obj/item/clothing/suit/hooded/ablative/on_hood_up(obj/item/clothing/head/hooded/hood) . = ..() - if (!hood_up) - return var/mob/living/carbon/user = loc var/datum/atom_hud/hud = GLOB.huds[DATA_HUD_SECURITY_ADVANCED] ADD_TRAIT(user, TRAIT_SECURITY_HUD, HELMET_TRAIT) hud.show_to(user) - balloon_alert(user, "you put on the hood, and enable the hud") + balloon_alert(user, "hud enabled") -/obj/item/clothing/suit/hooded/ablative/RemoveHood() - if (!hood_up) - return ..() +/obj/item/clothing/suit/hooded/ablative/on_hood_down(obj/item/clothing/head/hooded/hood) var/mob/living/carbon/user = loc var/datum/atom_hud/sec_hud = GLOB.huds[DATA_HUD_SECURITY_ADVANCED] REMOVE_TRAIT(user, TRAIT_SECURITY_HUD, HELMET_TRAIT) sec_hud.hide_from(user) - balloon_alert(user, "you take off the hood, and disable the hud") + balloon_alert(user, "hud disabled") return ..() diff --git a/code/modules/clothing/suits/toggles.dm b/code/modules/clothing/suits/toggles.dm index 75d684333d3..1040bc03120 100644 --- a/code/modules/clothing/suits/toggles.dm +++ b/code/modules/clothing/suits/toggles.dm @@ -1,124 +1,61 @@ -//Hoods for winter coats and chaplain hoodie etc - +/// Subtype with support for hoods +/// You no longer actually need to extend this and can just add the component yourself without a lot of this boilerplate code /obj/item/clothing/suit/hooded - actions_types = list(/datum/action/item_action/toggle_hood) - var/obj/item/clothing/head/hooded/hood var/hoodtype = /obj/item/clothing/head/hooded/winterhood //so the chaplain hoodie or other hoodies can override this - ///Alternative mode for hiding the hood, instead of storing the hood in the suit it qdels it, useful for when you deal with hooded suit with storage. + /// Alternative mode for hiding the hood, instead of storing the hood in the suit it qdels it, useful for when you deal with hooded suit with storage. var/alternative_mode = FALSE - ///Whether the hood is flipped up - var/hood_up = FALSE /// What should be added to the end of the icon state when the hood is up? Set to "" for the suit sprite to not change at all var/hood_up_affix = "_t" - /// Are we zipped? Mostly relevant for wintercoats, leaving this here to simplify logic and so someone else can extend it if they ever wish to. - var/zipped = FALSE + /// Icon state added as a worn overlay while the hood is down, leave as "" for no overlay + var/hood_down_overlay_suffix = "" + /// Reference to hood object, if it exists + var/obj/item/clothing/head/hooded/hood /obj/item/clothing/suit/hooded/Initialize(mapload) . = ..() - if(!alternative_mode) - MakeHood() - + if (!hoodtype) + return + AddComponent(\ + /datum/component/toggle_attached_clothing,\ + deployable_type = hoodtype,\ + equipped_slot = ITEM_SLOT_HEAD,\ + action_name = "Toggle Hood",\ + destroy_on_removal = alternative_mode,\ + parent_icon_state_suffix = hood_up_affix,\ + down_overlay_state_suffix = hood_down_overlay_suffix, \ + pre_creation_check = CALLBACK(src, PROC_REF(can_create_hood)),\ + on_created = CALLBACK(src, PROC_REF(on_hood_created)),\ + on_deployed = CALLBACK(src, PROC_REF(on_hood_up)),\ + on_removed = CALLBACK(src, PROC_REF(on_hood_down)),\ + ) /obj/item/clothing/suit/hooded/Destroy() - . = ..() - QDEL_NULL(hood) - -/obj/item/clothing/suit/hooded/proc/MakeHood() - if(!hood) - var/obj/item/clothing/head/hooded/W = new hoodtype(src) - W.suit = src - hood = W - -/obj/item/clothing/suit/hooded/ui_action_click() - ToggleHood() - -/obj/item/clothing/suit/hooded/item_action_slot_check(slot, mob/user) - if(slot & (ITEM_SLOT_OCLOTHING|ITEM_SLOT_NECK)) - return TRUE - -/obj/item/clothing/suit/hooded/equipped(mob/user, slot) - if(!(slot & (ITEM_SLOT_OCLOTHING|ITEM_SLOT_NECK))) - RemoveHood() + hood = null return ..() -/obj/item/clothing/suit/hooded/on_outfit_equip(mob/living/carbon/human/outfit_wearer, visuals_only, item_slot) - if(visuals_only) - MakeHood() - ToggleHood() +/// Override to only create the hood conditionally +/obj/item/clothing/suit/hooded/proc/can_create_hood() + return TRUE -/obj/item/clothing/suit/hooded/proc/RemoveHood() - icon_state = "[initial(icon_state)]" - worn_icon_state = icon_state - zipped = FALSE - hood_up = FALSE +/// Called when the hood is instantiated +/obj/item/clothing/suit/hooded/proc/on_hood_created(obj/item/clothing/head/hooded/hood) + SHOULD_CALL_PARENT(TRUE) + src.hood = hood + RegisterSignal(hood, COMSIG_QDELETING, PROC_REF(on_hood_deleted)) - if(hood) - if(ishuman(hood.loc)) - var/mob/living/carbon/human/H = hood.loc - H.transferItemToLoc(hood, src, TRUE) - H.update_worn_oversuit() - else - hood.forceMove(src) +/// Called when hood is deleted +/obj/item/clothing/suit/hooded/proc/on_hood_deleted() + SIGNAL_HANDLER + SHOULD_CALL_PARENT(TRUE) + hood = null - if(alternative_mode) - QDEL_NULL(hood) +/// Called when the hood is worn +/obj/item/clothing/suit/hooded/proc/on_hood_up(obj/item/clothing/head/hooded/hood) + return - update_item_action_buttons() - -/obj/item/clothing/suit/hooded/dropped() - ..() - RemoveHood() - -/obj/item/clothing/suit/hooded/proc/ToggleHood() - if(!hood_up) - if(!ishuman(loc)) - return - var/mob/living/carbon/human/H = loc - if(H.is_holding(src)) - to_chat(H, span_warning("You must be wearing [src] to put up the hood!")) - return - if(H.head) - to_chat(H, span_warning("You're already wearing something on your head!")) - return - else - if(alternative_mode) - MakeHood() - if(!H.equip_to_slot_if_possible(hood,ITEM_SLOT_HEAD,0,0,1)) - if(alternative_mode) - RemoveHood() - return - hood_up = TRUE - icon_state = "[initial(icon_state)][hood_up_affix]" - worn_icon_state = icon_state - zipped = TRUE // Just to maintain the same behavior, and so we avoid any bugs that otherwise relied on this behavior of zipping the jacket when bringing up the hood - H.update_worn_oversuit() - H.update_mob_action_buttons() - else - RemoveHood() - -/obj/item/clothing/head/hooded - var/obj/item/clothing/suit/hooded/suit - - -/obj/item/clothing/head/hooded/Destroy() - suit = null - return ..() - -/obj/item/clothing/head/hooded/dropped() - ..() - if(suit) - suit.RemoveHood() - -/obj/item/clothing/head/hooded/equipped(mob/user, slot) - ..() - if(!(slot & ITEM_SLOT_HEAD)) - if(suit) - suit.RemoveHood() - else - qdel(src) - -// Toggle exosuits for different aesthetic styles (hoodies, suit jacket buttons, etc) -// Pretty much just a holder for `/datum/component/toggle_icon`. +/// Called when the hood is hidden +/obj/item/clothing/suit/hooded/proc/on_hood_down(obj/item/clothing/head/hooded/hood) + return /obj/item/clothing/suit/toggle /// The noun that is displayed to the user on toggle. EX: "Toggles the suit's [buttons]". diff --git a/code/modules/clothing/suits/wintercoats.dm b/code/modules/clothing/suits/wintercoats.dm index d53d937d9c2..725bfc02d24 100644 --- a/code/modules/clothing/suits/wintercoats.dm +++ b/code/modules/clothing/suits/wintercoats.dm @@ -11,8 +11,9 @@ min_cold_protection_temperature = FIRE_SUIT_MIN_TEMP_PROTECT allowed = list() armor_type = /datum/armor/hooded_wintercoat - /// The mutable_appearance of the associated hood, when it's down. Can be null when the hood is up. - var/mutable_appearance/hood_overlay + hood_down_overlay_suffix = "_hood" + /// How snug are we? + var/zipped = FALSE /datum/armor/hooded_wintercoat bio = 10 @@ -31,8 +32,14 @@ /obj/item/toy, ) - generate_hood_overlay() +/obj/item/clothing/suit/hooded/wintercoat/on_hood_up(obj/item/clothing/head/hooded/hood) + . = ..() + zipped = TRUE +/// Called when the hood is hidden +/obj/item/clothing/suit/hooded/wintercoat/on_hood_down(obj/item/clothing/head/hooded/hood) + . = ..() + zipped = FALSE /obj/item/clothing/suit/hooded/wintercoat/examine(mob/user) . = ..() @@ -54,19 +61,6 @@ var/mob/living/carbon/human/wearer = loc wearer.update_worn_oversuit() - -/// Helper proc to generate the `hood_overlay` associated to the wintercoat. -/obj/item/clothing/suit/hooded/wintercoat/proc/generate_hood_overlay() - hood_overlay = mutable_appearance(initial(worn_icon), "[initial(icon_state)]_hood", -SUIT_LAYER) - - -/obj/item/clothing/suit/hooded/wintercoat/worn_overlays(mutable_appearance/standing, isinhands) - . = ..() - - if(!isinhands && !hood_up) - . += hood_overlay - - /obj/item/clothing/head/hooded/winterhood name = "winter hood" desc = "A cozy winter hood attached to a heavy winter jacket." @@ -648,6 +642,7 @@ /obj/item/clothing/suit/hooded/wintercoat/custom name = "tailored winter coat" desc = "A heavy jacket made from 'synthetic' animal furs, with custom colors." + hood_down_overlay_suffix = "" greyscale_colors = "#ffffff#ffffff#808080#808080#808080#808080" greyscale_config = /datum/greyscale_config/winter_coats greyscale_config_worn = /datum/greyscale_config/winter_coats/worn @@ -657,13 +652,15 @@ //In case colors are changed after initialization /obj/item/clothing/suit/hooded/wintercoat/custom/set_greyscale(list/colors, new_config, new_worn_config, new_inhand_left, new_inhand_right) . = ..() - if(hood) - var/list/coat_colors = SSgreyscale.ParseColorString(greyscale_colors) - var/list/new_coat_colors = coat_colors.Copy(1,4) - hood.set_greyscale(new_coat_colors) //Adopt the suit's grayscale coloring for visual clarity. + if(!hood) + return + var/list/coat_colors = SSgreyscale.ParseColorString(greyscale_colors) + var/list/new_coat_colors = coat_colors.Copy(1,4) + hood.set_greyscale(new_coat_colors) //Adopt the suit's grayscale coloring for visual clarity. + hood.update_slot_icon() //But also keep old method in case the hood is (re-)created later -/obj/item/clothing/suit/hooded/wintercoat/custom/MakeHood() +/obj/item/clothing/suit/hooded/wintercoat/custom/on_hood_created(obj/item/clothing/head/hooded/hood) . = ..() var/list/coat_colors = (SSgreyscale.ParseColorString(greyscale_colors)) var/list/new_coat_colors = coat_colors.Copy(1,4) diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm index a8a2465c103..eaa3cf685bc 100644 --- a/code/modules/mod/mod_control.dm +++ b/code/modules/mod/mod_control.dm @@ -423,6 +423,7 @@ set_wearer(user) /obj/item/mod/control/on_outfit_equip(mob/living/carbon/human/outfit_wearer, visuals_only, item_slot) + . = ..() quick_activation() /obj/item/mod/control/doStrip(mob/stripper, mob/owner) diff --git a/code/modules/unit_tests/ablative_hud.dm b/code/modules/unit_tests/ablative_hud.dm index 62f9501180a..d7a40dc764b 100644 --- a/code/modules/unit_tests/ablative_hud.dm +++ b/code/modules/unit_tests/ablative_hud.dm @@ -4,11 +4,12 @@ /datum/unit_test/ablative_hood_hud/Run() var/mob/living/carbon/human/person = allocate(/mob/living/carbon/human/consistent) var/obj/item/clothing/suit/hooded/ablative/coat = allocate(/obj/item/clothing/suit/hooded/ablative) + var/datum/component/toggle_attached_clothing/hood = coat.GetComponent(/datum/component/toggle_attached_clothing) person.equip_to_slot(coat, ITEM_SLOT_OCLOTHING) TEST_ASSERT(!HAS_TRAIT(person, TRAIT_SECURITY_HUD), "Person already had a sechud before trying to equip the ablative hood.") - coat.ToggleHood() + hood.toggle_deployable() TEST_ASSERT(HAS_TRAIT(person, TRAIT_SECURITY_HUD), "Person toggled the ablative hood but didn't gain a sechud.") - coat.ToggleHood() + hood.toggle_deployable() TEST_ASSERT(!HAS_TRAIT(person, TRAIT_SECURITY_HUD), "Person lowered their ablative hood but still has a sechud.") // Check that player doesn't gain sec hud if the hood is toggled when already wearing a helmet @@ -17,9 +18,10 @@ /datum/unit_test/ablative_hood_hud_with_helmet/Run() var/mob/living/carbon/human/person = allocate(/mob/living/carbon/human/consistent) var/obj/item/clothing/suit/hooded/ablative/coat = allocate(/obj/item/clothing/suit/hooded/ablative) + var/datum/component/toggle_attached_clothing/hood = coat.GetComponent(/datum/component/toggle_attached_clothing) var/obj/item/clothing/head/helmet/hat = allocate(/obj/item/clothing/head/helmet) person.equip_to_slot(coat, ITEM_SLOT_OCLOTHING) person.equip_to_slot(hat, ITEM_SLOT_HEAD) TEST_ASSERT(!HAS_TRAIT(person, TRAIT_SECURITY_HUD), "Person already had a sechud before trying to equip the ablative hood.") - coat.ToggleHood() + hood.toggle_deployable() TEST_ASSERT(!HAS_TRAIT(person, TRAIT_SECURITY_HUD), "Person has gained a sechud from toggling the ablative hood despite already wearing a helmet.") diff --git a/tgstation.dme b/tgstation.dme index aaddcc66846..418dc92c6b2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1033,6 +1033,7 @@ #include "code\datums\components\tether.dm" #include "code\datums\components\thermite.dm" #include "code\datums\components\tippable.dm" +#include "code\datums\components\toggle_attached_clothing.dm" #include "code\datums\components\toggle_suit.dm" #include "code\datums\components\transforming.dm" #include "code\datums\components\trapdoor.dm"