Files
Bubberstation/code/datums/components/toggle_attached_clothing.dm
MrMelbert 135a09182b Refactors obscured (#92779)
## About The Pull Request

Fixes #85028

Obscured flags and covered flags are tracked on carbons, updated as
items are equipped and unequipped. It's that shrimple.

Closes #92760

Just removes the species exception checks for not making sense

Also refactors handcuffs / legcuffs removal. In all of these situations
they were hardcoded when they could easily just use an inventory proc to
work.

## Why It's Good For The Game

Stops a million excessive calls to `check_obscured_slots`

Makes obscured behavior more consistent

Makes obscured behavior easier to use

Cleans up human rendering (There was some cursed stuff before with
render item -> updated obscured -> update body -> cause side effects)

## Changelog

🆑 Melbert
del: Golems which somehow manage to grow wings and somehow manage to
equip something that covers their jumpsuit can no longer fly.
(Seriously, this will not affect anyone)
refactor: Refactored clothing obscurity entirely. Items should be a
loooot more consistent and what covers what, and should update a lot
snappier. As always, report any oddities, like mysteriously disappearing
articles of clothing, hair, or species parts
refactored: Refactored handcuffs and legcuffs a bit, report any odd
situations with cuffs like getting stuck restrained
/🆑
2025-09-07 09:24:34 +02:00

218 lines
8.1 KiB
Plaintext

/**
* 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_REDUNDANT
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_POST_UNEQUIP, PROC_REF(remove_deployable))
RegisterSignal(parent, COMSIG_ITEM_EQUIPPED_AS_OUTFIT, PROC_REF(on_parent_equipped_outfit))
if (down_overlay_state_suffix)
var/overlay_state = "[initial(clothing_parent.post_init_icon_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)
unequip_deployable()
QDEL_NULL(deployable)
QDEL_NULL(toggle_action)
undeployed_overlay = null
pre_creation_check = null
on_created = null
on_deployed = null
on_removed = null
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.post_init_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()
SIGNAL_HANDLER
unequip_deployable()
if (!currently_deployed)
return
currently_deployed = FALSE
on_removed?.Invoke(deployable)
var/obj/item/parent_gear = parent
if(destroy_on_removal)
QDEL_NULL(deployable)
if(parent_icon_state_suffix)
parent_gear.icon_state = "[initial(parent_gear.post_init_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()
/// Removes an equipped deployable atom upon its retraction or destruction
/datum/component/toggle_attached_clothing/proc/unequip_deployable()
if (!deployable)
return
if (!ishuman(deployable.loc))
deployable.forceMove(parent)
return
var/mob/living/carbon/human/wearer = deployable.loc
wearer.transferItemToLoc(deployable, parent, force = TRUE, silent = TRUE)