Files
Bubberstation/code/datums/actions/cooldown_action.dm
+37 21b4095dfd [MDB IGNORE] [IDB IGNORE] Upstream Sync - 04/17/2026 (#5453)
Upstream 04/17/2026

fixes https://github.com/Bubberstation/Bubberstation/issues/5549

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: tgstation-ci[bot] <179393467+tgstation-ci[bot]@users.noreply.github.com>
Co-authored-by: ArcaneMusic <41715314+ArcaneMusic@users.noreply.github.com>
Co-authored-by: MrMelbert <51863163+MrMelbert@users.noreply.github.com>
Co-authored-by: Rhials <28870487+Rhials@users.noreply.github.com>
Co-authored-by: rageguy505 <54517726+rageguy505@users.noreply.github.com>
Co-authored-by: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com>
Co-authored-by: John Willard <53777086+JohnFulpWillard@users.noreply.github.com>
Co-authored-by: Aliceee2ch <160794176+Aliceee2ch@users.noreply.github.com>
Co-authored-by: Time-Green <7501474+Time-Green@users.noreply.github.com>
Co-authored-by: Tsar-Salat <62388554+Tsar-Salat@users.noreply.github.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Maxipat <108554989+Maxipat112@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: SimplyLogan <47579821+loganuk@users.noreply.github.com>
Co-authored-by: loganuk <fakeemail123@aol.com>
Co-authored-by: Leland Kemble <70413276+lelandkemble@users.noreply.github.com>
Co-authored-by: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com>
Co-authored-by: Roxy <75404941+TealSeer@users.noreply.github.com>
Co-authored-by: Lucy <lucy@absolucy.moe>
Co-authored-by: siliconOpossum <138069572+siliconOpossum@users.noreply.github.com>
Co-authored-by: Isratosh <Isratosh@hotmail.com>
Co-authored-by: TheRyeGuyWhoWillNowDie <70169560+TheRyeGuyWhoWillNowDie@users.noreply.github.com>
Co-authored-by: Neocloudy <88008002+Neocloudy@users.noreply.github.com>
Co-authored-by: Alexander V. <volas@ya.ru>
Co-authored-by: ElGitificador <168473461+ElGitificador@users.noreply.github.com>
Co-authored-by: Twaticus <46540570+Twaticus@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bloop <13398309+vinylspiders@users.noreply.github.com>
Co-authored-by: Cameron Lennox <killer65311@gmail.com>
Co-authored-by: Tim <timothymtorres@gmail.com>
Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: Layzu666 <121319428+Layzu666@users.noreply.github.com>
Co-authored-by: Arturlang <24881678+Arturlang@users.noreply.github.com>
Co-authored-by: _0Steven <42909981+00-Steven@users.noreply.github.com>
Co-authored-by: mrmanlikesbt <99309552+mrmanlikesbt@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: John F. Kennedy <54908920+MacaroniCritter@users.noreply.github.com>
Co-authored-by: Cursor <102828457+theselfish@users.noreply.github.com>
Co-authored-by: Josh <josh.adam.powell@gmail.com>
Co-authored-by: Josh Powell <josh.powell@softwire.com>
Co-authored-by: Yobrocharlie <Charliemiller5617@gmail.com>
Co-authored-by: Hardly3D <66234359+Hardly3D@users.noreply.github.com>
Co-authored-by: shayoki <96078776+shayoki@users.noreply.github.com>
Co-authored-by: LT3 <83487515+lessthnthree@users.noreply.github.com>
2026-05-16 00:56:00 +02:00

332 lines
12 KiB
Plaintext

#define COOLDOWN_NO_DISPLAY_TIME (180 SECONDS)
/// Preset for an action that has a cooldown.
/datum/action/cooldown
check_flags = NONE
transparent_when_unavailable = FALSE
/// The actual next time this ability can be used
var/next_use_time = 0
/// The default cooldown applied when StartCooldown() is called
var/cooldown_time = 0
/// The default melee cooldown applied after the ability ends. If set to null, copies cooldown_time.
var/melee_cooldown_time = 0
/// The actual next time the owner of this action can melee
var/next_melee_use_time = 0
/// Whether or not you want the cooldown for the ability to display in text form
var/text_cooldown = TRUE
/// Significant figures to round cooldown to
var/cooldown_rounding = 0.1
/// Shares cooldowns with other abiliies, bitflag
var/shared_cooldown = NONE
/// List of prerequisite actions that are used in this sequenced ability, you cannot put other sequenced abilities in this
var/list/sequence_actions
/// List of prerequisite actions that have been initialized
var/list/initialized_actions
// These are only used for click_to_activate actions
/// Setting for intercepting clicks before activating the ability
var/click_to_activate = FALSE
/// The cooldown added onto the user's next click.
var/click_cd_override = CLICK_CD_CLICK_ABILITY
/// If TRUE, we will unset after using our click intercept.
var/unset_after_click = TRUE
/// What icon to replace our mouse cursor with when active. Optional
var/ranged_mousepointer
/// The base icon_state of this action's background
var/base_background_icon_state
/// The icon state the background uses when active
var/active_background_icon_state
/// The base icon_state of the overlay we apply
var/base_overlay_icon_state
/// The active icon_state of the overlay we apply
var/active_overlay_icon_state
/// The base icon state of the spell's button icon, used for editing the icon "off"
var/base_icon_state
/// The active icon state of the spell's button icon, used for editing the icon "on"
var/active_icon_state
/datum/action/cooldown/New(Target, original = TRUE)
. = ..()
if(active_background_icon_state)
base_background_icon_state ||= background_icon_state
if(active_overlay_icon_state)
base_overlay_icon_state ||= overlay_icon_state
if(active_icon_state)
base_icon_state ||= button_icon_state
if(isnull(melee_cooldown_time))
melee_cooldown_time = cooldown_time
if(original)
create_sequence_actions()
/datum/action/cooldown/create_button(mob/viewer)
var/atom/movable/screen/movable/action_button/button = ..()
button.maptext = ""
button.maptext_x = 4
button.maptext_y = 2
button.maptext_width = 32
button.maptext_height = 16
return button
/datum/action/cooldown/update_button_status(atom/movable/screen/movable/action_button/button, force = FALSE)
. = ..()
var/time_left = max(next_use_time - world.time, 0)
if(!text_cooldown || !owner || time_left == 0 || time_left >= COOLDOWN_NO_DISPLAY_TIME)
button.maptext = ""
else
if (cooldown_rounding > 0)
button.maptext = MAPTEXT_TINY_UNICODE("[round(time_left/10, cooldown_rounding)]")
else
button.maptext = MAPTEXT_TINY_UNICODE("[round(time_left/10)]")
if(!IsAvailable() || !is_action_active(button))
return
// If we don't change the icon state, or don't apply a special overlay,
if(active_background_icon_state || active_icon_state || active_overlay_icon_state)
return
// ...we need to show it's active somehow. So, make it greeeen
button.color = COLOR_GREEN
/datum/action/cooldown/apply_button_background(atom/movable/screen/movable/action_button/current_button, force)
if(active_background_icon_state)
background_icon_state = is_action_active(current_button) ? active_background_icon_state : base_background_icon_state
return ..()
/datum/action/cooldown/apply_button_icon(atom/movable/screen/movable/action_button/current_button, force)
if(active_icon_state)
button_icon_state = is_action_active(current_button) ? active_icon_state : base_icon_state
return ..()
/datum/action/cooldown/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force)
if(active_overlay_icon_state)
overlay_icon_state = is_action_active(current_button) ? active_overlay_icon_state : base_overlay_icon_state
return ..()
/datum/action/cooldown/is_action_active(atom/movable/screen/movable/action_button/current_button)
return click_to_activate && current_button.our_hud?.mymob?.click_intercept == src
/datum/action/cooldown/Destroy()
QDEL_LIST(initialized_actions)
return ..()
/datum/action/cooldown/Grant(mob/granted_to)
. = ..()
if(!owner)
return
build_all_button_icons()
if(next_use_time > world.time)
START_PROCESSING(SSfastprocess, src)
RegisterSignal(granted_to, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, PROC_REF(handle_melee_attack))
for(var/datum/action/cooldown/ability as anything in initialized_actions)
ability.Grant(granted_to)
/datum/action/cooldown/Remove(mob/removed_from)
UnregisterSignal(removed_from, COMSIG_HOSTILE_PRE_ATTACKINGTARGET)
if(click_to_activate && removed_from.click_intercept == src)
unset_click_ability(removed_from, refund_cooldown = FALSE)
for(var/datum/action/cooldown/ability as anything in initialized_actions)
ability.Remove(removed_from)
return ..()
/datum/action/cooldown/IsAvailable(feedback = FALSE)
return ..() && (next_use_time <= world.time)
/// Initializes any sequence actions
/datum/action/cooldown/proc/create_sequence_actions()
if(!LAZYLEN(sequence_actions))
return
// remove existing actions if any
QDEL_LIST(initialized_actions)
initialized_actions = list()
for(var/type_path in sequence_actions)
var/datum/action/cooldown/ability = new type_path(target, original = FALSE)
// prevents clients from using the individual abilities in sequences (this stops it from being added to mob actions when granted as well)
ability.owner_has_control = FALSE
// [ability] = delay
initialized_actions[ability] = sequence_actions[type_path]
/// Starts a cooldown time to be shared with similar abilities
/// Will use default cooldown time if an override is not specified
/datum/action/cooldown/proc/StartCooldown(override_cooldown_time, override_melee_cooldown_time)
// "Shared cooldowns" covers actions which are not the same type,
// but have the same cooldown group and are on the same mob
if(shared_cooldown != NONE)
StartCooldownOthers(override_cooldown_time)
StartCooldownSelf(override_cooldown_time)
if(isnum(override_melee_cooldown_time))
next_melee_use_time = world.time + override_melee_cooldown_time
else
next_melee_use_time = world.time + melee_cooldown_time
/// Starts a cooldown time for this ability only
/// Will use default cooldown time if an override is not specified
/datum/action/cooldown/proc/StartCooldownSelf(override_cooldown_time)
if(isnum(override_cooldown_time))
next_use_time = world.time + override_cooldown_time
else
next_use_time = world.time + cooldown_time
// Don't start a cooldown if we have a cooldown time of 0 seconds
if(next_use_time == world.time)
return
build_all_button_icons(UPDATE_BUTTON_STATUS)
START_PROCESSING(SSfastprocess, src)
/// Starts a cooldown time for other abilities that share a cooldown with this. Has some niche usage with more complicated attack ai!
/// Will use default cooldown time if an override is not specified
/datum/action/cooldown/proc/StartCooldownOthers(override_cooldown_time)
if(!length(owner?.actions))
return // Possible if they have an action they don't control
for(var/datum/action/cooldown/shared_ability in owner.actions - src)
if(!(shared_cooldown & shared_ability.shared_cooldown))
continue
if(isnum(override_cooldown_time))
shared_ability.StartCooldownSelf(override_cooldown_time)
else
shared_ability.StartCooldownSelf(cooldown_time)
/// Resets the cooldown of this ability
/datum/action/cooldown/proc/ResetCooldown()
next_use_time = world.time
build_all_button_icons(UPDATE_BUTTON_STATUS)
/// Re-enables this cooldown action
/datum/action/cooldown/proc/enable()
action_disabled = FALSE
build_all_button_icons(UPDATE_BUTTON_STATUS)
/// Disables this cooldown action
/datum/action/cooldown/proc/disable()
action_disabled = TRUE
build_all_button_icons(UPDATE_BUTTON_STATUS)
/// Re-enables all cooldown actions
/datum/action/cooldown/proc/enable_cooldown_actions()
for(var/datum/action/cooldown/cd_action in owner.actions)
cd_action.enable()
/// Disables all cooldown actions
/datum/action/cooldown/proc/disable_cooldown_actions()
for(var/datum/action/cooldown/cd_action in owner.actions)
cd_action.disable()
/datum/action/cooldown/Trigger(mob/clicker, trigger_flags, atom/target)
. = ..()
if(!.)
return FALSE
if(!owner)
return FALSE
var/mob/user = clicker || owner
// If our cooldown action is a click_to_activate action:
// The actual action is activated on whatever the user clicks on -
// the target is what the action is being used on
// In trigger, we handle setting the click intercept
if(click_to_activate)
if(target)
// For automatic / mob handling
return InterceptClickOn(user, null, target)
var/datum/action/cooldown/already_set = user.click_intercept
if(already_set == src)
// if we clicked ourself and we're already set, unset and return
return unset_click_ability(user, refund_cooldown = TRUE)
else if(istype(already_set))
// if we have an active set already, unset it before we set our's
already_set.unset_click_ability(user, refund_cooldown = TRUE)
return set_click_ability(user)
// If our cooldown action is not a click_to_activate action:
// We can just continue on and use the action
// the target is the user of the action (often, the owner)
return PreActivate(user)
/// Intercepts client owner clicks to activate the ability
/datum/action/cooldown/proc/InterceptClickOn(mob/living/clicker, params, atom/target)
if(!IsAvailable(feedback = TRUE))
return FALSE
if(!target)
return FALSE
// The actual action begins here
if(!PreActivate(target))
return FALSE
// And if we reach here, the action was complete successfully
if(unset_after_click)
unset_click_ability(clicker, refund_cooldown = FALSE)
clicker.next_click = world.time + click_cd_override
return TRUE
/// For signal calling
/datum/action/cooldown/proc/PreActivate(atom/target)
if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src, target) & COMPONENT_BLOCK_ABILITY_START)
return
// Note, that PreActivate handles no cooldowns at all by default.
// Be sure to call StartCooldown() in Activate() where necessary.
. = Activate(target)
// There is a possibility our action (or owner) is qdeleted in Activate().
if(!QDELETED(src) && !QDELETED(owner))
SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_FINISHED, src)
/// To be implemented by subtypes (if not generic)
/datum/action/cooldown/proc/Activate(atom/target)
var/total_delay = 0
for(var/datum/action/cooldown/ability as anything in initialized_actions)
if(LAZYLEN(ability.initialized_actions) > 0)
ability.initialized_actions = list()
addtimer(CALLBACK(ability, PROC_REF(Activate), target), total_delay)
total_delay += initialized_actions[ability]
StartCooldown()
/// Cancels melee attacks if they are on cooldown.
/datum/action/cooldown/proc/handle_melee_attack(mob/source, mob/target)
SIGNAL_HANDLER
if(next_melee_use_time > world.time)
return COMPONENT_HOSTILE_NO_ATTACK
/datum/action/cooldown/process()
if(!owner || (next_use_time - world.time) <= 0)
build_all_button_icons(UPDATE_BUTTON_STATUS)
STOP_PROCESSING(SSfastprocess, src)
return
build_all_button_icons(UPDATE_BUTTON_STATUS)
/**
* Set our action as the click override on the passed mob.
*/
/datum/action/cooldown/proc/set_click_ability(mob/on_who)
SHOULD_CALL_PARENT(TRUE)
on_who.click_intercept = src
if(ranged_mousepointer)
on_who.client?.mouse_override_icon = ranged_mousepointer
on_who.update_mouse_pointer()
build_all_button_icons(UPDATE_BUTTON_STATUS)
return TRUE
/**
* Unset our action as the click override of the passed mob.
*
* if refund_cooldown is TRUE, we are being unset by the user clicking the action off
* if refund_cooldown is FALSE, we are being forcefully unset, likely by someone actually using the action
*/
/datum/action/cooldown/proc/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
SHOULD_CALL_PARENT(TRUE)
on_who.click_intercept = null
if(ranged_mousepointer)
on_who.client?.mouse_override_icon = initial(on_who.client?.mouse_override_icon)
on_who.update_mouse_pointer()
build_all_button_icons(UPDATE_BUTTON_STATUS)
return TRUE
#undef COOLDOWN_NO_DISPLAY_TIME