mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-11 10:11:09 +00:00
## About The Pull Request Put a ! where there should not be. Whoops. Fixes https://github.com/tgstation/tgstation/issues/92395 Fixes https://github.com/tgstation/tgstation/issues/92348 ## Why It's Good For The Game Bug fix and working feature good ## Changelog 🆑 fix: Action button binding works again /🆑
455 lines
17 KiB
Plaintext
455 lines
17 KiB
Plaintext
/**
|
|
* # Action system
|
|
*
|
|
* A simple base for an modular behavior attached to atom or datum.
|
|
*/
|
|
/datum/action
|
|
/// The name of the action
|
|
var/name = "Generic Action"
|
|
/// The description of what the action does, shown in button tooltips
|
|
var/desc
|
|
/// The target the action is attached to. If the target datum is deleted, the action is as well.
|
|
/// Set in New() via the proc link_to(). PLEASE set a target if you're making an action
|
|
var/datum/target
|
|
/// Where any buttons we create should be by default. Accepts screen_loc and location defines
|
|
var/default_button_position = SCRN_OBJ_IN_LIST
|
|
/// This is who currently owns the action, and most often, this is who is using the action if it is triggered
|
|
/// This can be the same as "target" but is not ALWAYS the same - this is set and unset with Grant() and Remove()
|
|
var/mob/owner
|
|
/// If False, the owner of this action does not get a hud and cannot activate it on their own
|
|
var/owner_has_control = TRUE
|
|
/// Flags that will determine of the owner / user of the action can... use the action
|
|
var/check_flags = NONE
|
|
/// Whether the button becomes transparent when it can't be used, or just reddened
|
|
var/transparent_when_unavailable = TRUE
|
|
///List of all mobs that are viewing our action button -> A unique movable for them to view.
|
|
var/list/viewers = list()
|
|
/// If TRUE, this action button will be shown to observers / other mobs who view from this action's owner's eyes.
|
|
/// Used in [/mob/proc/show_other_mob_action_buttons]
|
|
var/show_to_observers = TRUE
|
|
/// If observers can click this action at any time, regardless of the owner
|
|
var/allow_observer_click = FALSE
|
|
|
|
/// The style the button's tooltips appear to be
|
|
var/buttontooltipstyle = ""
|
|
|
|
/// This is the file for the BACKGROUND underlay icon of the button
|
|
var/background_icon = 'icons/mob/actions/backgrounds.dmi'
|
|
/// This is the icon state state for the BACKGROUND underlay icon of the button
|
|
/// (If set to ACTION_BUTTON_DEFAULT_BACKGROUND, uses the hud's default background)
|
|
var/background_icon_state = ACTION_BUTTON_DEFAULT_BACKGROUND
|
|
|
|
/// This is the file for the icon that appears on the button
|
|
var/button_icon = 'icons/hud/actions.dmi'
|
|
/// This is the icon state for the icon that appears on the button
|
|
var/button_icon_state = "default"
|
|
|
|
/// This is the file for any FOREGROUND overlay icons on the button (such as borders)
|
|
var/overlay_icon = 'icons/mob/actions/backgrounds.dmi'
|
|
/// This is the icon state for any FOREGROUND overlay icons on the button (such as borders)
|
|
var/overlay_icon_state
|
|
|
|
/// full key we are bound to
|
|
var/full_key
|
|
|
|
/// Toggles whether this action is usable or not
|
|
var/action_disabled = FALSE
|
|
/// Can this action be shared with our rider?
|
|
var/can_be_shared = TRUE
|
|
|
|
/datum/action/New(Target)
|
|
link_to(Target)
|
|
|
|
/// Links the passed target to our action, registering any relevant signals
|
|
/datum/action/proc/link_to(Target)
|
|
target = Target
|
|
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(clear_ref), override = TRUE)
|
|
|
|
if(isatom(target))
|
|
RegisterSignal(target, COMSIG_ATOM_UPDATED_ICON, PROC_REF(on_target_icon_update))
|
|
|
|
if(istype(target, /datum/mind))
|
|
RegisterSignal(target, COMSIG_MIND_TRANSFERRED, PROC_REF(on_target_mind_swapped))
|
|
|
|
/datum/action/Destroy()
|
|
if(owner)
|
|
Remove(owner)
|
|
target = null
|
|
QDEL_LIST_ASSOC_VAL(viewers) // Qdel the buttons in the viewers list **NOT THE HUDS**
|
|
return ..()
|
|
|
|
/// Signal proc that clears any references based on the owner or target deleting
|
|
/// If the owner's deleted, we will simply remove from them, but if the target's deleted, we will self-delete
|
|
/datum/action/proc/clear_ref(datum/ref)
|
|
SIGNAL_HANDLER
|
|
if(ref == owner)
|
|
Remove(owner)
|
|
if(ref == target)
|
|
qdel(src)
|
|
|
|
/// Grants the action to the passed mob, making it the owner
|
|
/datum/action/proc/Grant(mob/grant_to)
|
|
if(isnull(grant_to))
|
|
Remove(owner)
|
|
return
|
|
if(grant_to == owner)
|
|
return // We already have it
|
|
var/mob/previous_owner = owner
|
|
owner = grant_to
|
|
if(!isnull(previous_owner))
|
|
Remove(previous_owner)
|
|
SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, owner)
|
|
SEND_SIGNAL(owner, COMSIG_MOB_GRANTED_ACTION, src)
|
|
RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(clear_ref), override = TRUE)
|
|
|
|
// Register some signals based on our check_flags
|
|
// so that our button icon updates when relevant
|
|
if(check_flags & AB_CHECK_CONSCIOUS)
|
|
RegisterSignal(owner, COMSIG_MOB_STATCHANGE, PROC_REF(update_status_on_signal))
|
|
if(check_flags & AB_CHECK_INCAPACITATED)
|
|
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED), SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED)), PROC_REF(update_status_on_signal))
|
|
if(check_flags & AB_CHECK_IMMOBILE)
|
|
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED)), PROC_REF(update_status_on_signal))
|
|
if(check_flags & AB_CHECK_HANDS_BLOCKED)
|
|
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED)), PROC_REF(update_status_on_signal))
|
|
if(check_flags & AB_CHECK_LYING)
|
|
RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, PROC_REF(update_status_on_signal))
|
|
if(check_flags & AB_CHECK_PHASED)
|
|
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED), SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED)), PROC_REF(update_status_on_signal))
|
|
if(check_flags & AB_CHECK_OPEN_TURF)
|
|
RegisterSignal(owner, COMSIG_MOVABLE_MOVED, PROC_REF(update_status_on_signal))
|
|
if(owner_has_control)
|
|
RegisterSignal(grant_to, COMSIG_MOB_KEYDOWN, PROC_REF(keydown), override = TRUE)
|
|
GiveAction(grant_to)
|
|
|
|
/// Remove the passed mob from being owner of our action
|
|
/datum/action/proc/Remove(mob/remove_from)
|
|
SHOULD_CALL_PARENT(TRUE)
|
|
|
|
for(var/datum/hud/hud in viewers)
|
|
if(!hud.mymob)
|
|
continue
|
|
HideFrom(hud.mymob)
|
|
LAZYREMOVE(remove_from?.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
|
|
viewers = list()
|
|
UnregisterSignal(remove_from, COMSIG_MOB_KEYDOWN)
|
|
|
|
if(isnull(owner))
|
|
return
|
|
SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
|
|
SEND_SIGNAL(owner, COMSIG_MOB_REMOVED_ACTION, src)
|
|
UnregisterSignal(owner, COMSIG_QDELETING)
|
|
|
|
// Clean up our check_flag signals
|
|
UnregisterSignal(owner, list(
|
|
COMSIG_LIVING_SET_BODY_POSITION,
|
|
COMSIG_MOB_STATCHANGE,
|
|
COMSIG_MOVABLE_MOVED,
|
|
SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED),
|
|
SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
|
|
SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
|
|
SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED),
|
|
SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED),
|
|
SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED),
|
|
SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED),
|
|
SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED),
|
|
))
|
|
|
|
if(target == owner)
|
|
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(clear_ref))
|
|
if (owner == remove_from)
|
|
owner = null
|
|
|
|
/// Actually triggers the effects of the action.
|
|
/// Called when the on-screen button is clicked, for example.
|
|
/datum/action/proc/Trigger(mob/clicker, trigger_flags)
|
|
if(!(trigger_flags & TRIGGER_FORCE_AVAILABLE) && !IsAvailable(feedback = TRUE))
|
|
return FALSE
|
|
if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER)
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/**
|
|
* Whether our action is currently available to use or not
|
|
* * feedback - If true this is being called to check if we have any messages to show to the owner
|
|
*/
|
|
/datum/action/proc/IsAvailable(feedback = FALSE)
|
|
if(!owner)
|
|
return FALSE
|
|
if(action_disabled)
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_HANDS_BLOCKED) && HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED))
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "hands blocked!")
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_IMMOBILE) && HAS_TRAIT(owner, TRAIT_IMMOBILIZED))
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "can't move!")
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_INCAPACITATED) && HAS_TRAIT(owner, TRAIT_INCAPACITATED))
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "incapacitated!")
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_LYING) && isliving(owner))
|
|
var/mob/living/action_owner = owner
|
|
if(action_owner.body_position == LYING_DOWN)
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "must stand up!")
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_CONSCIOUS) && owner.stat != CONSCIOUS)
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "unconscious!")
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED))
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "incorporeal!")
|
|
return FALSE
|
|
if((check_flags & AB_CHECK_OPEN_TURF) && !isopenturf(owner.loc))
|
|
if (feedback)
|
|
owner.balloon_alert(owner, "not enough space!")
|
|
return FALSE
|
|
return TRUE
|
|
|
|
/// Builds / updates all buttons we have shared or given out
|
|
/datum/action/proc/build_all_button_icons(update_flags = ALL, force)
|
|
for(var/datum/hud/hud as anything in viewers)
|
|
build_button_icon(viewers[hud], update_flags, force)
|
|
|
|
/**
|
|
* Builds the icon of the button.
|
|
*
|
|
* Concept:
|
|
* - Underlay (Background icon)
|
|
* - Icon (button icon)
|
|
* - Maptext
|
|
* - Overlay (Background border)
|
|
*
|
|
* button - which button we are modifying the icon of
|
|
* force - whether we're forcing a full update
|
|
*/
|
|
/datum/action/proc/build_button_icon(atom/movable/screen/movable/action_button/button, update_flags = ALL, force = FALSE)
|
|
if(!button)
|
|
return
|
|
|
|
button.actiontooltipstyle = buttontooltipstyle
|
|
|
|
if(update_flags & UPDATE_BUTTON_NAME)
|
|
update_button_name(button, force)
|
|
|
|
if(update_flags & UPDATE_BUTTON_BACKGROUND)
|
|
apply_button_background(button, force)
|
|
|
|
if(update_flags & UPDATE_BUTTON_ICON)
|
|
apply_button_icon(button, force)
|
|
|
|
if(update_flags & UPDATE_BUTTON_OVERLAY)
|
|
apply_button_overlay(button, force)
|
|
|
|
if(update_flags & UPDATE_BUTTON_STATUS)
|
|
update_button_status(button, force)
|
|
|
|
/**
|
|
* Updates the name and description of the button to match our action name and discription.
|
|
*
|
|
* current_button - what button are we editing?
|
|
* force - whether an update is forced regardless of existing status
|
|
*/
|
|
/datum/action/proc/update_button_name(atom/movable/screen/movable/action_button/button, force = FALSE)
|
|
button.name = name
|
|
if(desc)
|
|
button.desc = desc
|
|
|
|
/**
|
|
* Creates the background underlay for the button
|
|
*
|
|
* current_button - what button are we editing?
|
|
* force - whether an update is forced regardless of existing status
|
|
*/
|
|
/datum/action/proc/apply_button_background(atom/movable/screen/movable/action_button/current_button, force = FALSE)
|
|
if(!background_icon || !background_icon_state || (current_button.active_underlay_icon_state == background_icon_state && !force))
|
|
return
|
|
|
|
// What icons we use for our background
|
|
var/list/icon_settings = list(
|
|
// The icon file
|
|
"bg_icon" = background_icon,
|
|
// The icon state, if is_action_active() returns FALSE
|
|
"bg_state" = background_icon_state,
|
|
// The icon state, if is_action_active() returns TRUE
|
|
"bg_state_active" = background_icon_state,
|
|
)
|
|
|
|
// If background_icon_state is ACTION_BUTTON_DEFAULT_BACKGROUND instead use our hud's action button scheme
|
|
if(background_icon_state == ACTION_BUTTON_DEFAULT_BACKGROUND && owner?.hud_used)
|
|
icon_settings = owner.hud_used.get_action_buttons_icons()
|
|
|
|
// Determine which icon to use
|
|
var/used_icon_key = is_action_active(current_button) ? "bg_state_active" : "bg_state"
|
|
|
|
// Make the underlay
|
|
current_button.underlays.Cut()
|
|
current_button.underlays += image(icon = icon_settings["bg_icon"], icon_state = icon_settings[used_icon_key])
|
|
current_button.active_underlay_icon_state = icon_settings[used_icon_key]
|
|
|
|
/**
|
|
* Applies our button icon and icon state to the button
|
|
*
|
|
* current_button - what button are we editing?
|
|
* force - whether an update is forced regardless of existing status
|
|
*/
|
|
/datum/action/proc/apply_button_icon(atom/movable/screen/movable/action_button/current_button, force = FALSE)
|
|
if(!button_icon || !button_icon_state || (current_button.icon_state == button_icon_state && !force))
|
|
return
|
|
|
|
current_button.icon = button_icon
|
|
current_button.icon_state = button_icon_state
|
|
|
|
/**
|
|
* Applies any overlays to our button
|
|
*
|
|
* current_button - what button are we editing?
|
|
* force - whether an update is forced regardless of existing status
|
|
*/
|
|
/datum/action/proc/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE)
|
|
|
|
SEND_SIGNAL(src, COMSIG_ACTION_OVERLAY_APPLY, current_button, force)
|
|
|
|
if(!overlay_icon || !overlay_icon_state || (current_button.active_overlay_icon_state == overlay_icon_state && !force))
|
|
return
|
|
|
|
current_button.cut_overlay(current_button.button_overlay)
|
|
current_button.button_overlay = mutable_appearance(icon = overlay_icon, icon_state = overlay_icon_state)
|
|
current_button.add_overlay(current_button.button_overlay)
|
|
current_button.active_overlay_icon_state = overlay_icon_state
|
|
|
|
/**
|
|
* Any other miscellaneous "status" updates within the action button is handled here,
|
|
* such as redding out when unavailable or modifying maptext.
|
|
*
|
|
* current_button - what button are we editing?
|
|
* force - whether an update is forced regardless of existing status
|
|
*/
|
|
/datum/action/proc/update_button_status(atom/movable/screen/movable/action_button/current_button, force = FALSE)
|
|
current_button.update_keybind_maptext(full_key)
|
|
if(IsAvailable())
|
|
current_button.color = rgb(255,255,255,255)
|
|
else
|
|
current_button.color = transparent_when_unavailable ? rgb(128,0,0,128) : rgb(128,0,0)
|
|
|
|
/// Gives our action to the passed viewer.
|
|
/// Puts our action in their actions list and shows them the button.
|
|
/datum/action/proc/GiveAction(mob/viewer)
|
|
var/datum/hud/our_hud = viewer.hud_used
|
|
if(viewers[our_hud]) // Already have a copy of us? go away
|
|
return
|
|
|
|
LAZYOR(viewer.actions, src) // Move this in
|
|
ShowTo(viewer)
|
|
|
|
/// Adds our action button to the screen of the passed viewer.
|
|
/datum/action/proc/ShowTo(mob/viewer)
|
|
var/datum/hud/our_hud = viewer.hud_used
|
|
if(!our_hud || viewers[our_hud]) // There's no point in this if you have no hud in the first place
|
|
return
|
|
|
|
var/atom/movable/screen/movable/action_button/button = create_button()
|
|
SetId(button, viewer)
|
|
|
|
button.our_hud = our_hud
|
|
viewers[our_hud] = button
|
|
if(viewer.client)
|
|
viewer.client.screen += button
|
|
|
|
button.load_position(viewer)
|
|
viewer.update_action_buttons()
|
|
|
|
/// Removes our action from the passed viewer.
|
|
/datum/action/proc/HideFrom(mob/viewer)
|
|
var/datum/hud/our_hud = viewer.hud_used
|
|
var/atom/movable/screen/movable/action_button/button = viewers[our_hud]
|
|
LAZYREMOVE(viewer.actions, src)
|
|
if(button)
|
|
qdel(button)
|
|
|
|
/// Creates an action button movable for the passed mob, and returns it.
|
|
/datum/action/proc/create_button()
|
|
var/atom/movable/screen/movable/action_button/button = new()
|
|
button.linked_action = src
|
|
button.allow_observer_click = allow_observer_click
|
|
build_button_icon(button, ALL, TRUE)
|
|
return button
|
|
|
|
/datum/action/proc/SetId(atom/movable/screen/movable/action_button/our_button, mob/owner)
|
|
//button id generation
|
|
var/bitfield = 0
|
|
for(var/datum/action/action in owner.actions)
|
|
if(action == src) // This could be us, which is dumb
|
|
continue
|
|
var/atom/movable/screen/movable/action_button/button = action.viewers[owner.hud_used]
|
|
if(action.name == name && button.id)
|
|
bitfield |= button.id
|
|
|
|
bitfield = ~bitfield // Flip our possible ids, so we can check if we've found a unique one
|
|
for(var/i in 0 to 23) // We get 24 possible bitflags in dm
|
|
var/bitflag = 1 << i // Shift us over one
|
|
if(bitfield & bitflag)
|
|
our_button.id = bitflag
|
|
return
|
|
|
|
/// Updates our buttons if our target's icon was updated
|
|
/datum/action/proc/on_target_icon_update(datum/source, updates, updated)
|
|
SIGNAL_HANDLER
|
|
|
|
var/update_flag = NONE
|
|
var/forced = FALSE
|
|
if(updates & UPDATE_ICON_STATE)
|
|
update_flag |= UPDATE_BUTTON_ICON
|
|
forced = TRUE
|
|
if(updates & UPDATE_OVERLAYS)
|
|
update_flag |= UPDATE_BUTTON_OVERLAY
|
|
forced = TRUE
|
|
if(updates & (UPDATE_NAME|UPDATE_DESC))
|
|
update_flag |= UPDATE_BUTTON_NAME
|
|
// Status is not relevant, and background is not relevant. Neither will change
|
|
|
|
// Force the update if an icon state or overlay change was done
|
|
build_all_button_icons(update_flag, forced)
|
|
|
|
/// A general use signal proc that reacts to an event and updates JUST our button's status
|
|
/datum/action/proc/update_status_on_signal(datum/source, new_stat, old_stat)
|
|
SIGNAL_HANDLER
|
|
|
|
build_all_button_icons(UPDATE_BUTTON_STATUS)
|
|
|
|
/// Signal proc for COMSIG_MIND_TRANSFERRED - for minds, transfers our action to our new mob on mind transfer
|
|
/datum/action/proc/on_target_mind_swapped(datum/mind/source, mob/old_current)
|
|
SIGNAL_HANDLER
|
|
|
|
// Grant() calls Remove() from the existing owner so we're covered on that
|
|
Grant(source.current)
|
|
|
|
/// Checks if our action is actively selected. Used for selecting icons primarily.
|
|
/datum/action/proc/is_action_active(atom/movable/screen/movable/action_button/current_button)
|
|
return FALSE
|
|
|
|
/datum/action/proc/begin_creating_bind(atom/movable/screen/movable/action_button/current_button, mob/user)
|
|
if(!current_button || user != owner)
|
|
return
|
|
if(!isnull(full_key))
|
|
full_key = null
|
|
update_button_status(current_button)
|
|
return
|
|
full_key = tgui_input_keycombo(user, "Please bind a key for this action.")
|
|
update_button_status(current_button)
|
|
|
|
/datum/action/proc/keydown(mob/source, key, client/client, full_key)
|
|
SIGNAL_HANDLER
|
|
if(isnull(full_key) || full_key != src.full_key)
|
|
return
|
|
if(istype(source))
|
|
if(source.next_click > world.time)
|
|
return
|
|
else
|
|
source.next_click = world.time + CLICK_CD_ACTIVATE_ABILITY
|
|
INVOKE_ASYNC(src, PROC_REF(Trigger))
|