/**
* Component which allows you to attach a seclight to an item,
* be it a piece of clothing or a tool.
*/
/datum/component/seclite_attachable
dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
/// Whether we can remove the light with a screwdriver or not.
var/is_light_removable = TRUE
/// If passed, we wil simply update our item's icon_state when a light is attached.
/// Formatted as parent_base_state-[light_icon-state]-"on"
var/light_icon_state
/// If passed, we will add overlays to the item when a light is attached.
/// This is the icon file it grabs the overlay from.
var/light_overlay_icon
/// The state to take from the light overlay icon if supplied.
var/light_overlay
/// The X offset of our overlay if supplied.
var/overlay_x = 0
/// The Y offset of our overlay if supplied.
var/overlay_y = 0
// Internal vars.
/// A reference to the actual light that's attached.
var/obj/item/flashlight/seclite/light
/// A weakref to the item action we add with the light.
var/datum/weakref/toggle_action_ref
/// Static typecache of all lights we consider seclites (all lights we can attach).
var/static/list/valid_lights = typecacheof(list(/obj/item/flashlight/seclite))
/datum/component/seclite_attachable/Initialize(
obj/item/flashlight/seclite/starting_light,
is_light_removable = TRUE,
light_icon_state,
light_overlay_icon,
light_overlay,
overlay_x = 0,
overlay_y = 0,
)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
src.is_light_removable = is_light_removable
src.light_icon_state = light_icon_state
src.light_overlay_icon = light_overlay_icon
src.light_overlay = light_overlay
src.overlay_x = overlay_x
src.overlay_y = overlay_y
if(istype(starting_light))
add_light(starting_light)
/datum/component/seclite_attachable/Destroy(force)
if(light)
remove_light()
return ..()
// Inheriting component allows lights to be added externally to things which already have a mount.
/datum/component/seclite_attachable/InheritComponent(
datum/component/seclite_attachable/new_component,
original,
obj/item/flashlight/seclite/starting_light,
is_light_removable = TRUE,
light_icon_state,
light_overlay_icon,
light_overlay,
overlay_x,
overlay_y,
)
if(!original)
return
src.is_light_removable = is_light_removable
// For the rest of these arguments, default to what already exists
if(light_icon_state)
src.light_icon_state = light_icon_state
if(light_overlay_icon)
src.light_overlay_icon = light_overlay_icon
if(light_overlay)
src.light_overlay = light_overlay
if(overlay_x)
src.overlay_x = overlay_x
if(overlay_x)
src.overlay_y = overlay_y
if(istype(starting_light))
add_light(starting_light)
/datum/component/seclite_attachable/RegisterWithParent()
RegisterSignal(parent, COMSIG_OBJ_DECONSTRUCT, PROC_REF(on_parent_deconstructed))
RegisterSignal(parent, COMSIG_ATOM_EXITED, PROC_REF(on_light_exit))
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER), PROC_REF(on_screwdriver))
RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, PROC_REF(on_update_icon_state))
RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_update_overlays))
RegisterSignal(parent, COMSIG_ITEM_UI_ACTION_CLICK, PROC_REF(on_action_click))
RegisterSignal(parent, COMSIG_ATOM_ATTACKBY, PROC_REF(on_attackby))
RegisterSignal(parent, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
RegisterSignal(parent, COMSIG_ATOM_SABOTEUR_ACT, PROC_REF(on_hit_by_saboteur))
RegisterSignal(parent, COMSIG_QDELETING, PROC_REF(on_parent_deleted))
/datum/component/seclite_attachable/UnregisterFromParent()
UnregisterSignal(parent, list(
COMSIG_OBJ_DECONSTRUCT,
COMSIG_ATOM_EXITED,
COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER),
COMSIG_ATOM_UPDATE_ICON_STATE,
COMSIG_ATOM_UPDATE_OVERLAYS,
COMSIG_ITEM_UI_ACTION_CLICK,
COMSIG_ATOM_ATTACKBY,
COMSIG_ATOM_EXAMINE,
COMSIG_ATOM_SABOTEUR_ACT,
COMSIG_QDELETING,
))
/// Sets a new light as our current light for our parent.
/datum/component/seclite_attachable/proc/add_light(obj/item/flashlight/new_light, mob/attacher)
if(light)
CRASH("[type] tried to add a new light when it already had one.")
light = new_light
light.set_light_flags(light.light_flags | LIGHT_ATTACHED)
// We may already exist within in our parent's contents... But if we don't move it over now
if(light.loc != parent)
light.forceMove(parent)
// We already have an action for the light for some reason? Clean it up
if(toggle_action_ref?.resolve())
stack_trace("[type] - add_light had an existing toggle action when add_light was called.")
QDEL_NULL(toggle_action_ref)
// Make a new toggle light item action for our parent
var/obj/item/item_parent = parent
var/datum/action/item_action/toggle_seclight/toggle_action = item_parent.add_item_action(/datum/action/item_action/toggle_seclight)
toggle_action_ref = WEAKREF(toggle_action)
update_light()
/// Removes the current light from our parent.
/datum/component/seclite_attachable/proc/remove_light()
// Our action may be linked to our parent,
// but it's really sourced from our light. Get rid of it.
QDEL_NULL(toggle_action_ref)
// It is possible the light was removed by being deleted.
if(!QDELETED(light))
light.set_light_flags(light.light_flags & ~LIGHT_ATTACHED)
light.update_brightness()
light = null
update_light()
/// Toggles the light within on or off.
/// Returns TRUE if there is a light inside, FALSE otherwise.
/datum/component/seclite_attachable/proc/toggle_light(mob/user)
if(!light)
return FALSE
var/successful_toggle = light.toggle_light(user)
if(!successful_toggle)
return TRUE
user.balloon_alert(user, "[light.name] toggled [light.light_on ? "on":"off"]")
update_light()
return TRUE
/// Called after the a light is added, removed, or toggles.
/// Ensures all of our appearances look correct for the new light state.
/datum/component/seclite_attachable/proc/update_light()
var/obj/item/item_parent = parent
item_parent.update_appearance()
item_parent.update_item_action_buttons()
/// Signal proc for [COMSIG_ATOM_EXITED] that handles our light being removed or deleted from our parent.
/datum/component/seclite_attachable/proc/on_light_exit(obj/item/source, atom/movable/gone, direction)
SIGNAL_HANDLER
if(gone == light)
remove_light()
/// Signal proc for [COMSIG_OBJ_DECONSTRUCT] that drops our light to the ground if our parent is deconstructed.
/datum/component/seclite_attachable/proc/on_parent_deconstructed(obj/item/source, disassembled)
SIGNAL_HANDLER
// Our light is gone already - Probably destroyed by whatever destroyed our parent. Just remove it.
if(QDELETED(light) || !is_light_removable)
remove_light()
return
// We were deconstructed in any other way, so we can just drop the light on the ground (which removes it via signal).
light.forceMove(source.drop_location())
/// Signal proc for [COMSIG_QDELETING] that deletes our light if our parent is deleted.
/datum/component/seclite_attachable/proc/on_parent_deleted(obj/item/source)
SIGNAL_HANDLER
QDEL_NULL(light)
/// Signal proc for [COMSIG_ITEM_UI_ACTION_CLICK] that toggles our light on and off if our action button is clicked.
/datum/component/seclite_attachable/proc/on_action_click(obj/item/source, mob/user, datum/action)
SIGNAL_HANDLER
// This isn't OUR action specifically, we don't care.
if(!IS_WEAKREF_OF(action, toggle_action_ref))
return
// Toggle light fails = no light attached = shouldn't be possible
if(!toggle_light(user))
CRASH("[type] - on_action_click somehow both HAD AN ACTION and also HAD A TRIGGERABLE ACTION, without having an attached light.")
return COMPONENT_ACTION_HANDLED
/// Signal proc for [COMSIG_ATOM_ATTACKBY] that allows a user to attach a seclite by hitting our parent with it.
/datum/component/seclite_attachable/proc/on_attackby(obj/item/source, obj/item/attacking_item, mob/attacker, list/modifiers)
SIGNAL_HANDLER
if(!is_type_in_typecache(attacking_item, valid_lights))
return
if(light)
source.balloon_alert(attacker, "already has \a [light]!")
return
if(!attacker.transferItemToLoc(attacking_item, source))
return
add_light(attacking_item, attacker)
source.balloon_alert(attacker, "attached [attacking_item]")
return COMPONENT_NO_AFTERATTACK
/// Signal proc for [COMSIG_ATOM_TOOL_ACT] via [TOOL_SCREWDRIVER] that removes any attached seclite.
/datum/component/seclite_attachable/proc/on_screwdriver(obj/item/source, mob/user, obj/item/tool)
SIGNAL_HANDLER
if(!light || !is_light_removable)
return
INVOKE_ASYNC(src, PROC_REF(unscrew_light), source, user, tool)
return ITEM_INTERACT_BLOCKING
/// Invoked asyncronously from [proc/on_screwdriver]. Handles removing the light from our parent.
/datum/component/seclite_attachable/proc/unscrew_light(obj/item/source, mob/user, obj/item/tool)
tool?.play_tool_sound(source)
source.balloon_alert(user, "unscrewed [light]")
var/obj/item/flashlight/seclite/to_remove = light
// The forcemove here will call exited on the light, and automatically update our references / etc
to_remove.forceMove(source.drop_location())
if(source.Adjacent(user) && !issilicon(user))
user.put_in_hands(to_remove)
/// Signal proc for [COMSIG_ATOM_EXAMINE] that shows our item can have / does have a seclite attached.
/datum/component/seclite_attachable/proc/on_examine(obj/item/source, mob/examiner, list/examine_list)
SIGNAL_HANDLER
if(light)
examine_list += "It has \a [light] [is_light_removable ? "mounted on it with a few screws" : "permanently mounted on it"]."
else
examine_list += "It has a mounting point for a seclite."
/// Signal proc for [COMSIG_ATOM_UPDATE_OVERLAYS] that updates our parent with our seclite overlays, if we have some.
/datum/component/seclite_attachable/proc/on_update_overlays(obj/item/source, list/overlays)
SIGNAL_HANDLER
// No overlays to add, no reason to run
if(!light_overlay || !light_overlay_icon)
return
// No light, nothing to add
if(!light)
return
var/overlay_state = "[light_overlay][light.light_on ? "_on":""]"
var/mutable_appearance/flashlight_overlay = mutable_appearance(light_overlay_icon, overlay_state)
flashlight_overlay.pixel_w = overlay_x
flashlight_overlay.pixel_z = overlay_y
overlays += flashlight_overlay
/// Signal proc for [COMSIG_ATOM_UPDATE_ICON_STATE] that updates our parent's icon state, if we have one.
/datum/component/seclite_attachable/proc/on_update_icon_state(obj/item/source)
SIGNAL_HANDLER
// No icon state to set, no reason to run
if(!light_icon_state)
return
// Get the "base icon state" to work on
var/base_state = source.base_icon_state || initial(source.icon_state)
// Updates our icon state based on our light state.
if(light)
source.icon_state = "[base_state]-[light_icon_state][light.light_on ? "-on":""]"
// Reset their icon state when if we've got no light.
else if(source.icon_state != base_state)
// Yes, this might mess with other icon state alterations,
// but that's the downside of using icon states over overlays.
source.icon_state = base_state
//turns the light off for a few seconds.
/datum/component/seclite_attachable/proc/on_hit_by_saboteur(datum/source, disrupt_duration)
. = light.on_saboteur(source, disrupt_duration)
update_light()
return .