From ef41677463e041920b1a9dfa7ce9eb4ded7fbdd0 Mon Sep 17 00:00:00 2001 From: warriorstar-orion Date: Thu, 7 Aug 2025 11:55:52 -0400 Subject: [PATCH] Port mob cooldown actions and AI behavior. (#29924) * Port mob cooldown actions and AI behavior. * fix lint * fix missing override args * Update code/_onclick/click.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/_onclick/click.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/datums/actions/action.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * Update code/__DEFINES/ai/blackboard_defines.dm Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> * remove unused defines --------- Signed-off-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Co-authored-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> --- code/__DEFINES/ai/blackboard_defines.dm | 2 + code/__DEFINES/dcs/mob_signals.dm | 10 + code/__DEFINES/misc_defines.dm | 1 + code/_onclick/click.dm | 16 +- code/datums/actions/action.dm | 6 +- code/datums/actions/cooldown_action.dm | 364 ++++++++++++++++++ code/datums/actions/item_action.dm | 8 +- code/datums/actions/mobs/mobcooldown.dm | 8 + code/datums/actions/spell_action.dm | 4 +- .../targeted_mob_abilities.dm | 55 +++ .../basic_subtrees/targeted_mob_ability.dm | 34 ++ code/game/gamemodes/cult/blood_magic.dm | 2 +- code/game/gamemodes/cult/cult_actions.dm | 6 +- .../demons/slaughter_demon/slaughter.dm | 3 - .../miniantags/guardian/host_actions.dm | 2 +- .../items/bio_chips/bio_chip_stealth.dm | 2 +- code/modules/games/cards.dm | 2 +- .../mob/living/carbon/human/species/golem.dm | 2 +- .../simple_animal/slime/slime_powers.dm | 2 +- code/modules/mob/mob_vars.dm | 3 + code/modules/surgery/organs/vocal_cords.dm | 2 +- paradise.dme | 4 + 22 files changed, 516 insertions(+), 22 deletions(-) create mode 100644 code/datums/actions/cooldown_action.dm create mode 100644 code/datums/actions/mobs/mobcooldown.dm create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_abilities.dm create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm index 89c2e8d0417..cf9f76fd1dd 100644 --- a/code/__DEFINES/ai/blackboard_defines.dm +++ b/code/__DEFINES/ai/blackboard_defines.dm @@ -47,6 +47,8 @@ #define BB_BASIC_MOB_RETALIATE_LIST "BB_BASIC_MOB_SHITLIST" ///Blackboard key for a whitelist typecache of "things we can target while trying to move" #define BB_OBSTACLE_TARGETING_WHITELIST "BB_targeting_whitelist" +/// some behaviors that check current_target also set this on deep crit mobs +#define BB_BASIC_MOB_EXECUTION_TARGET "BB_basic_execution_target" //Hunting BB keys diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm index 4154e87b750..7b713d27487 100644 --- a/code/__DEFINES/dcs/mob_signals.dm +++ b/code/__DEFINES/dcs/mob_signals.dm @@ -263,3 +263,13 @@ ///from the ranged_attacks component for basic mobs: (mob/living/basic/firer, atom/target, modifiers) #define COMSIG_BASICMOB_POST_ATTACK_RANGED "basicmob_post_attack_ranged" + +/// From base of /datum/action/cooldown/proc/PreActivate(), sent to the action owner: (datum/action/cooldown/activated) +#define COMSIG_MOB_ABILITY_STARTED "mob_ability_base_started" + /// Return to block the ability from starting / activating + #define COMPONENT_BLOCK_ABILITY_START (1<<0) +/// From base of /datum/action/cooldown/proc/PreActivate(), sent to the action owner: (datum/action/cooldown/finished) +#define COMSIG_MOB_ABILITY_FINISHED "mob_ability_base_finished" + +/// From base of /datum/action/cooldown/proc/set_statpanel_format(): (list/stat_panel_data) +#define COMSIG_ACTION_SET_STATPANEL "ability_set_statpanel" diff --git a/code/__DEFINES/misc_defines.dm b/code/__DEFINES/misc_defines.dm index 8e31fbf5dde..b0141cab58b 100644 --- a/code/__DEFINES/misc_defines.dm +++ b/code/__DEFINES/misc_defines.dm @@ -494,6 +494,7 @@ /// Prepares a text to be used for maptext. Use this so it doesn't look hideous. #define MAPTEXT(text) {"[##text]"} #define MAPTEXT_CENTER(text) {"[##text]"} +#define MAPTEXT_SMALL(text) {"[##text]"} //Fullscreen overlay resolution in tiles. #define FULLSCREEN_OVERLAY_RESOLUTION_X 15 diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 3230540eabb..441a53667fb 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -51,8 +51,7 @@ if(QDELETED(A)) return - if(client?.click_intercept) - client.click_intercept.InterceptClickOn(src, params, A) + if(check_click_intercept(params,A)) return if(next_click > world.time) @@ -516,5 +515,18 @@ click_turf.Click(location, control, list2params(modifiers)) . = 1 +/mob/proc/check_click_intercept(params,A) + // Client level intercept + if(client?.click_intercept) + if(call(client.click_intercept, "InterceptClickOn")(src, params, A)) + return TRUE + + // Mob level intercept + if(click_interceptor) + if(call(click_interceptor, "InterceptClickOn")(src, params, A)) + return TRUE + + return FALSE + #undef MAX_SAFE_BYOND_ICON_SCALE_TILES #undef MAX_SAFE_BYOND_ICON_SCALE_PX diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index 09290724b73..1772a3b3b72 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -32,6 +32,8 @@ var/action_disabled = FALSE /// The appearance used as an overlay for when the action is unavailable var/mutable_appearance/unavailable_effect + /// If False, the owner of this action does not get a hud and cannot activate it on their own + var/owner_has_control = TRUE /datum/action/New(target) link_to(target) @@ -114,7 +116,7 @@ Trigger() return FALSE -/datum/action/proc/IsAvailable()// returns 1 if all checks pass +/datum/action/proc/IsAvailable(show_message = TRUE) // returns 1 if all checks pass if(!owner) return FALSE if((check_flags & AB_CHECK_HANDS_BLOCKED) && HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED)) @@ -194,6 +196,8 @@ /datum/action/proc/update_button_status(atom/movable/screen/movable/action_button/button, force = FALSE) button.overlays -= unavailable_effect button.maptext = "" + if(IsAvailable(show_message = FALSE)) + button.color = rgb(255, 255, 255, 255) if(should_draw_cooldown()) apply_unavailable_effect(button) diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm new file mode 100644 index 00000000000..715fe1fc096 --- /dev/null +++ b/code/datums/actions/cooldown_action.dm @@ -0,0 +1,364 @@ +#define COOLDOWN_NO_DISPLAY_TIME (180 SECONDS) + +/// Preset for an action that has a cooldown. +/datum/action/cooldown + transparent_when_unavailable = FALSE + + /// The actual next time this ability can be used + var/next_use_time = 0 + /// The stat panel this action shows up in the stat panel in. If null, will not show up. + var/panel + /// 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() + 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_SMALL("[round(time_left/10, cooldown_rounding)]") + else + button.maptext = MAPTEXT_SMALL("[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_interceptor == src + +/datum/action/cooldown/Destroy() + QDEL_LIST_CONTENTS(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_interceptor == 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(show_message = TRUE) + 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_CONTENTS(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(trigger_flags, atom/target) + . = ..() + if(!.) + return FALSE + if(!owner) + return FALSE + + var/mob/user = usr || 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_interceptor + 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(show_message = 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_interceptor = src + // TODO: Support custom mouse cursors for click abilities + 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_interceptor = null + // TODO: Support unsetting custom mouse cursors for click abilities + build_all_button_icons(UPDATE_BUTTON_STATUS) + return TRUE + +/// Formats the action to be returned to the stat panel. +/datum/action/cooldown/proc/set_statpanel_format() + if(!panel) + return null + + var/time_remaining = max(next_use_time - world.time, 0) + var/time_remaining_in_seconds = round(time_remaining / 10, 0.1) + var/cooldown_time_in_seconds = round(cooldown_time / 10, 0.1) + + var/list/stat_panel_data = list() + + // Pass on what panel we should be displayed in. + stat_panel_data[PANEL_DISPLAY_PANEL] = panel + // Also pass on the name of the spell, with some spacing + stat_panel_data[PANEL_DISPLAY_NAME] = " - [name]" + + // No cooldown time at all, just show the ability + if(cooldown_time_in_seconds <= 0) + stat_panel_data[PANEL_DISPLAY_STATUS] = "" + + // It's a toggle-active ability, show if it's active + else if(click_to_activate && owner.click_interceptor == src) + stat_panel_data[PANEL_DISPLAY_STATUS] = "ACTIVE" + + // It's on cooldown, show the cooldown + else if(time_remaining_in_seconds > 0) + stat_panel_data[PANEL_DISPLAY_STATUS] = "CD - [time_remaining_in_seconds]s / [cooldown_time_in_seconds]s" + + // It's not on cooldown, show that it is ready + else + stat_panel_data[PANEL_DISPLAY_STATUS] = "READY" + + SEND_SIGNAL(src, COMSIG_ACTION_SET_STATPANEL, stat_panel_data) + + return stat_panel_data + +#undef COOLDOWN_NO_DISPLAY_TIME diff --git a/code/datums/actions/item_action.dm b/code/datums/actions/item_action.dm index eec08e49a57..b9b67fc866f 100644 --- a/code/datums/actions/item_action.dm +++ b/code/datums/actions/item_action.dm @@ -115,7 +115,7 @@ desc = "Recall yourself, and anyone nearby, to an attuned hierophant beacon at any time.
If the beacon is still attached, will detach it." button_icon_state = "vortex_recall" -/datum/action/item_action/vortex_recall/IsAvailable() +/datum/action/item_action/vortex_recall/IsAvailable(show_message = TRUE) if(istype(target, /obj/item/hierophant_club)) var/obj/item/hierophant_club/H = target if(H.teleporting) @@ -221,7 +221,7 @@ /datum/action/item_action/jetpack_stabilization name = "Toggle Jetpack Stabilization" -/datum/action/item_action/jetpack_stabilization/IsAvailable() +/datum/action/item_action/jetpack_stabilization/IsAvailable(show_message = TRUE) var/obj/item/tank/jetpack/J = target if(!istype(J) || !J.on) return FALSE @@ -319,7 +319,7 @@ /datum/action/item_action/organ_action check_flags = AB_CHECK_CONSCIOUS -/datum/action/item_action/organ_action/IsAvailable() +/datum/action/item_action/organ_action/IsAvailable(show_message = TRUE) var/obj/item/organ/internal/I = target if(!I.owner) return FALSE @@ -360,7 +360,7 @@ /datum/action/item_action/accessory check_flags = AB_CHECK_RESTRAINED|AB_CHECK_STUNNED|AB_CHECK_LYING|AB_CHECK_CONSCIOUS -/datum/action/item_action/accessory/IsAvailable() +/datum/action/item_action/accessory/IsAvailable(show_message = TRUE) . = ..() if(!.) return FALSE diff --git a/code/datums/actions/mobs/mobcooldown.dm b/code/datums/actions/mobs/mobcooldown.dm new file mode 100644 index 00000000000..91721402791 --- /dev/null +++ b/code/datums/actions/mobs/mobcooldown.dm @@ -0,0 +1,8 @@ +/datum/action/cooldown/mob_cooldown + name = "Standard Mob Cooldown Ability" + button_icon_state = "sniper_zoom" + desc = "Click this ability to attack." + check_flags = AB_CHECK_CONSCIOUS + cooldown_time = 5 SECONDS + click_to_activate = TRUE + shared_cooldown = MOB_SHARED_COOLDOWN_1 diff --git a/code/datums/actions/spell_action.dm b/code/datums/actions/spell_action.dm index 739338a06a0..09be20622c8 100644 --- a/code/datums/actions/spell_action.dm +++ b/code/datums/actions/spell_action.dm @@ -38,13 +38,13 @@ spell.AltClick(usr) return TRUE -/datum/action/spell_action/IsAvailable() +/datum/action/spell_action/IsAvailable(show_message = TRUE) if(!target) return FALSE var/datum/spell/spell = target if(owner) - return spell.can_cast(owner, show_message = TRUE) + return spell.can_cast(owner, show_message) return FALSE /datum/action/spell_action/apply_unavailable_effect(atom/movable/screen/movable/action_button/button) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_abilities.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_abilities.dm new file mode 100644 index 00000000000..1e438ea0979 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeted_mob_abilities.dm @@ -0,0 +1,55 @@ +/** + * # Targeted Mob Ability + * Attempts to use a mob's cooldown ability on a target + */ +/datum/ai_behavior/targeted_mob_ability + +/datum/ai_behavior/targeted_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key) + var/datum/action/cooldown/ability = controller.blackboard[ability_key] + var/mob/living/target = controller.blackboard[target_key] + if(QDELETED(ability) || QDELETED(target)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + var/mob/pawn = controller.pawn + pawn.face_atom(target) + var/result = ability.Trigger(target = target) + if(result) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + +/** + * # Try Mob Ability and plan execute + * Attempts to use a mob's cooldown ability on a target and then move the target into a special target blackboard datum + * Doesn't need another subtype to clear BB_BASIC_MOB_EXECUTION_TARGET because it will be the target key for the normal action + */ +/datum/ai_behavior/targeted_mob_ability/and_plan_execute + +/datum/ai_behavior/targeted_mob_ability/and_plan_execute/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key) + controller.set_blackboard_key(BB_BASIC_MOB_EXECUTION_TARGET, controller.blackboard[target_key]) + return ..() + +/** + * # Try Mob Ability and clear target + * Attempts to use a mob's cooldown ability on a target and releases the target when the action completes + */ +/datum/ai_behavior/targeted_mob_ability/and_clear_target + +/datum/ai_behavior/targeted_mob_ability/and_clear_target/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + +/** + * Attempts to move into the provided range and then use a mob's cooldown ability on a target + */ +/datum/ai_behavior/targeted_mob_ability/min_range + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + required_distance = 6 + +/datum/ai_behavior/targeted_mob_ability/min_range/setup(datum/ai_controller/controller, ability_key, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/targeted_mob_ability/min_range/short + required_distance = 3 diff --git a/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm b/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm new file mode 100644 index 00000000000..99a61ae3bbd --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/targeted_mob_ability.dm @@ -0,0 +1,34 @@ +/// Attempts to use a mob ability on a target +/datum/ai_planning_subtree/targeted_mob_ability + /// Blackboard key for the ability + var/ability_key = BB_TARGETED_ACTION + /// Blackboard key for where the target ref is stored + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// Behaviour to perform using ability + var/use_ability_behaviour = /datum/ai_behavior/targeted_mob_ability + /// If true we terminate planning after trying to use the ability. + var/finish_planning = TRUE + +/datum/ai_planning_subtree/targeted_mob_ability/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!ability_key) + CRASH("You forgot to tell this mob where to find its ability") + + if(!controller.blackboard_key_exists(target_key)) + return + + var/datum/action/cooldown/using_action = controller.blackboard[ability_key] + if(!using_action?.IsAvailable()) + return + if(!additional_ability_checks(controller, using_action)) + return + + controller.queue_behavior(use_ability_behaviour, ability_key, target_key) + if(finish_planning) + return SUBTREE_RETURN_FINISH_PLANNING + +/// Any additional checks before we queue the behaviour +/datum/ai_planning_subtree/targeted_mob_ability/proc/additional_ability_checks(datum/ai_controller/controller, datum/action/cooldown/using_action) + return TRUE + +/datum/ai_planning_subtree/targeted_mob_ability/continue_planning + finish_planning = FALSE diff --git a/code/game/gamemodes/cult/blood_magic.dm b/code/game/gamemodes/cult/blood_magic.dm index 9e36133dc96..9f463ec006b 100644 --- a/code/game/gamemodes/cult/blood_magic.dm +++ b/code/game/gamemodes/cult/blood_magic.dm @@ -128,7 +128,7 @@ hand_magic = null ..() -/datum/action/innate/cult/blood_spell/IsAvailable() +/datum/action/innate/cult/blood_spell/IsAvailable(show_message = TRUE) if(!IS_CULTIST(owner) || owner.incapacitated() || !charges) return FALSE return ..() diff --git a/code/game/gamemodes/cult/cult_actions.dm b/code/game/gamemodes/cult/cult_actions.dm index fc3dc0aaae7..a43b78fba66 100644 --- a/code/game/gamemodes/cult/cult_actions.dm +++ b/code/game/gamemodes/cult/cult_actions.dm @@ -4,7 +4,7 @@ check_flags = AB_CHECK_RESTRAINED|AB_CHECK_STUNNED|AB_CHECK_CONSCIOUS buttontooltipstyle = "cult" -/datum/action/innate/cult/IsAvailable() +/datum/action/innate/cult/IsAvailable(show_message = TRUE) if(!IS_CULTIST(owner)) return FALSE return ..() @@ -66,7 +66,7 @@ name = "Spiritual Communion" desc = "Conveys a message from the spirit realm that all cultists can hear." -/datum/action/innate/cult/comm/spirit/IsAvailable() +/datum/action/innate/cult/comm/spirit/IsAvailable(show_message = TRUE) return TRUE /datum/action/innate/cult/comm/spirit/cultist_commune(mob/living/user, message) @@ -95,7 +95,7 @@ button_icon_state = GET_CULT_DATA(tome_icon, "tome") ..() -/datum/action/innate/cult/check_progress/IsAvailable() +/datum/action/innate/cult/check_progress/IsAvailable(show_message = TRUE) return IS_CULTIST(owner) || isobserver(owner) /datum/action/innate/cult/check_progress/Activate() diff --git a/code/game/gamemodes/miniantags/demons/slaughter_demon/slaughter.dm b/code/game/gamemodes/miniantags/demons/slaughter_demon/slaughter.dm index a07f50aa10b..75a417afd47 100644 --- a/code/game/gamemodes/miniantags/demons/slaughter_demon/slaughter.dm +++ b/code/game/gamemodes/miniantags/demons/slaughter_demon/slaughter.dm @@ -167,9 +167,6 @@ button_icon_state = "demon_comms" background_icon_state = "bg_demon" -/datum/action/innate/demon_whisper/IsAvailable() - return ..() - /datum/action/innate/demon_whisper/proc/choose_targets(mob/user = usr)//yes i am copying from telepathy..hush... var/list/validtargets = list() for(var/mob/living/M in view(user.client.maxview(), get_turf(user))) diff --git a/code/game/gamemodes/miniantags/guardian/host_actions.dm b/code/game/gamemodes/miniantags/guardian/host_actions.dm index 957c94ee55d..79db77e2a83 100644 --- a/code/game/gamemodes/miniantags/guardian/host_actions.dm +++ b/code/game/gamemodes/miniantags/guardian/host_actions.dm @@ -66,7 +66,7 @@ button_icon_state = "reset" var/cooldown_timer -/datum/action/guardian/reset_guardian/IsAvailable() +/datum/action/guardian/reset_guardian/IsAvailable(show_message = TRUE) if(cooldown_timer) return FALSE return TRUE diff --git a/code/game/objects/items/bio_chips/bio_chip_stealth.dm b/code/game/objects/items/bio_chips/bio_chip_stealth.dm index 76a747c0a1c..2e9fa41da33 100644 --- a/code/game/objects/items/bio_chips/bio_chip_stealth.dm +++ b/code/game/objects/items/bio_chips/bio_chip_stealth.dm @@ -68,7 +68,7 @@ on_cooldown = FALSE build_all_button_icons() -/datum/action/item_action/agent_box/IsAvailable() +/datum/action/item_action/agent_box/IsAvailable(show_message = TRUE) if(..() && !on_cooldown) return TRUE return FALSE diff --git a/code/modules/games/cards.dm b/code/modules/games/cards.dm index 9f6de17baac..62a26776c7b 100644 --- a/code/modules/games/cards.dm +++ b/code/modules/games/cards.dm @@ -888,7 +888,7 @@ name = "Remove a card - Remove a single card from the hand." button_icon_state = "remove_card" -/datum/action/item_action/remove_card/IsAvailable() +/datum/action/item_action/remove_card/IsAvailable(show_message = TRUE) var/obj/item/cardhand/C = target if(length(C.cards) <= 1) return FALSE diff --git a/code/modules/mob/living/carbon/human/species/golem.dm b/code/modules/mob/living/carbon/human/species/golem.dm index bf203601824..f81b9a11b32 100644 --- a/code/modules/mob/living/carbon/human/species/golem.dm +++ b/code/modules/mob/living/carbon/human/species/golem.dm @@ -510,7 +510,7 @@ var/last_teleport = 0 var/tele_range = 6 -/datum/action/innate/unstable_teleport/IsAvailable() +/datum/action/innate/unstable_teleport/IsAvailable(show_message = TRUE) if(..()) if(world.time > last_teleport + cooldown && !activated) return 1 diff --git a/code/modules/mob/living/simple_animal/slime/slime_powers.dm b/code/modules/mob/living/simple_animal/slime/slime_powers.dm index 6b394e20ccc..e167ac0b8b4 100644 --- a/code/modules/mob/living/simple_animal/slime/slime_powers.dm +++ b/code/modules/mob/living/simple_animal/slime/slime_powers.dm @@ -11,7 +11,7 @@ background_icon_state = "bg_alien" var/needs_growth = NO_GROWTH_NEEDED -/datum/action/innate/slime/IsAvailable() +/datum/action/innate/slime/IsAvailable(show_message = TRUE) if(..()) var/mob/living/simple_animal/slime/S = owner if(needs_growth == GROWTH_NEEDED) diff --git a/code/modules/mob/mob_vars.dm b/code/modules/mob/mob_vars.dm index e3cebe253a1..fd8575ce1cb 100644 --- a/code/modules/mob/mob_vars.dm +++ b/code/modules/mob/mob_vars.dm @@ -250,6 +250,9 @@ /// Does this mob speak OOC? /// Controls whether they can say some symbols. var/speaks_ooc = FALSE + /// Allows a datum to intercept all click calls this mob is the source of. + /// This is *not* necessarily an instance of [/datum/click_intercept]. + var/datum/click_interceptor /// For storing what do_after's something has, key = string, value = amount of interactions of that type happening. var/list/do_afters diff --git a/code/modules/surgery/organs/vocal_cords.dm b/code/modules/surgery/organs/vocal_cords.dm index 289beee4223..ca9fc1167da 100644 --- a/code/modules/surgery/organs/vocal_cords.dm +++ b/code/modules/surgery/organs/vocal_cords.dm @@ -103,7 +103,7 @@ GLOBAL_DATUM_INIT(multispin_words, /regex, regex("like a record baby")) ..() cords = target -/datum/action/item_action/organ_action/colossus/IsAvailable() +/datum/action/item_action/organ_action/colossus/IsAvailable(show_message = TRUE) if(world.time < cords.next_command) return FALSE if(!owner) diff --git a/paradise.dme b/paradise.dme index 91cec492681..a07f3462628 100644 --- a/paradise.dme +++ b/paradise.dme @@ -446,10 +446,12 @@ #include "code\datums\vision_override.dm" #include "code\datums\vv_core_topics.dm" #include "code\datums\actions\action.dm" +#include "code\datums\actions\cooldown_action.dm" #include "code\datums\actions\generic_action.dm" #include "code\datums\actions\innate_action.dm" #include "code\datums\actions\item_action.dm" #include "code\datums\actions\spell_action.dm" +#include "code\datums\actions\mobs\mobcooldown.dm" #include "code\datums\ai\ai_behavior.dm" #include "code\datums\ai\ai_controller.dm" #include "code\datums\ai\ai_planning_subtree.dm" @@ -462,6 +464,7 @@ #include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targeting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\stop_and_stare.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeted_mob_abilities.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\swirl_around.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" @@ -481,6 +484,7 @@ #include "code\datums\ai\basic_mobs\basic_subtrees\swirl_around_target.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\stare_at_thing.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\targeted_mob_ability.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\tip_reaction.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\ventcrawl_find_target.dm"