diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm index ee7807f146d..289f7586222 100644 --- a/code/__DEFINES/ai/ai_blackboard.dm +++ b/code/__DEFINES/ai/ai_blackboard.dm @@ -34,6 +34,9 @@ ///Blackboard key for a whitelist typecache of "things we can target while trying to move" #define BB_OBSTACLE_TARGETTING_WHITELIST "BB_targetting_whitelist" +/// Blackboard key storing how long your targetting datum has held a particular target +#define BB_BASIC_MOB_HAS_TARGET_TIME "BB_basic_mob_has_target_time" + ///Targetting keys for something to run away from, if you need to store this separately from current target #define BB_BASIC_MOB_FLEE_TARGET "BB_basic_flee_target" #define BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION "BB_basic_flee_target_hiding_location" diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index d287d97b2ec..c396771a033 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -545,6 +545,8 @@ multiple modular subtrees with behaviors * * key - A blackboard key */ /datum/ai_controller/proc/clear_blackboard_key(key) + if(isnull(blackboard[key])) + return CLEAR_AI_DATUM_TARGET(blackboard[key], key) blackboard[key] = null if(isnull(pawn)) diff --git a/code/datums/components/ai_has_target_timer.dm b/code/datums/components/ai_has_target_timer.dm new file mode 100644 index 00000000000..bcd748ce638 --- /dev/null +++ b/code/datums/components/ai_has_target_timer.dm @@ -0,0 +1,79 @@ +/// Increments a blackboard key while the attached mob is engaged with a particular target, does nothing else on its own +/datum/component/ai_target_timer + /// Blackboard key to store data inside + var/increment_key + /// Blackboard key to watch to indicate whether we are 'in combat' + var/target_key + /// Amount of time we have spent focused on one target + var/time_on_target = 0 + /// The last target we had + var/atom/last_target + /// Timer used to see if you + var/reset_clock_timer + +/datum/component/ai_target_timer/Initialize(increment_key = BB_BASIC_MOB_HAS_TARGET_TIME, target_key = BB_BASIC_MOB_CURRENT_TARGET) + . = ..() + if (!isliving(parent)) + return COMPONENT_INCOMPATIBLE + var/mob/living/mob_parent = parent + if (isnull(mob_parent.ai_controller)) + return COMPONENT_INCOMPATIBLE + src.increment_key = increment_key + src.target_key = target_key + +/datum/component/ai_target_timer/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(changed_target)) + RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key), PROC_REF(lost_target)) + ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + +/datum/component/ai_target_timer/UnregisterFromParent() + finalise_losing_target() + UnregisterSignal(parent, list(COMSIG_AI_BLACKBOARD_KEY_SET(target_key), COMSIG_AI_BLACKBOARD_KEY_CLEARED(target_key))) + REMOVE_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + return ..() + +/datum/component/ai_target_timer/Destroy(force, silent) + finalise_losing_target() + return ..() + +/// When we get a new target, reset the timer and start processing +/datum/component/ai_target_timer/proc/changed_target(mob/living/source) + SIGNAL_HANDLER + var/mob/living/living_parent = parent + var/atom/new_target = living_parent.ai_controller.blackboard[target_key] + deltimer(reset_clock_timer) + if (new_target == last_target) + return + time_on_target = 0 + store_current_time() + START_PROCESSING(SSdcs, src) + if (!isnull(last_target)) + UnregisterSignal(last_target, COMSIG_QDELETING) + RegisterSignal(new_target, COMSIG_QDELETING, PROC_REF(finalise_losing_target)) + last_target = new_target + +/// When we lose our target, start a short timer in case we reacquire it very quickly +/datum/component/ai_target_timer/proc/lost_target() + SIGNAL_HANDLER + reset_clock_timer = addtimer(CALLBACK(src, PROC_REF(finalise_losing_target)), 3 SECONDS, TIMER_STOPPABLE | TIMER_DELETE_ME) + +/// Called if we have had no target for long enough +/datum/component/ai_target_timer/proc/finalise_losing_target() + deltimer(reset_clock_timer) + STOP_PROCESSING(SSdcs, src) + if (!isnull(last_target)) + UnregisterSignal(last_target, COMSIG_QDELETING) + last_target = null + time_on_target = 0 + if (!QDELETED(parent)) + store_current_time() + +/// Store the current time on our timer in our blackboard key +/datum/component/ai_target_timer/proc/store_current_time() + var/mob/living/living_parent = parent + living_parent.ai_controller.set_blackboard_key(increment_key, time_on_target) + +/datum/component/ai_target_timer/process(seconds_per_tick) + time_on_target += seconds_per_tick SECONDS + store_current_time() diff --git a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm index 2771fe2bd76..063200b486b 100644 --- a/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm +++ b/code/game/objects/structures/crates_lockers/closets/cardboardbox.dm @@ -85,6 +85,7 @@ SET_PLANE_EXPLICIT(alert, ABOVE_LIGHTING_PLANE, src) var/atom/movable/flick_visual/exclamation = flick_overlay_view(alert, 1 SECONDS) exclamation.alpha = 0 + exclamation.pixel_x = -pixel_x animate(exclamation, pixel_z = 32, alpha = 255, time = 0.5 SECONDS, easing = ELASTIC_EASING) // We use this list to update plane values on parent z change, which is why we need the timer too // I'm sorry :( diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm index 1423e4036fa..95df44a8326 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath.dm @@ -53,6 +53,7 @@ AddElement(/datum/element/ai_retaliate) AddElement(/datum/element/footstep, FOOTSTEP_MOB_HEAVY) AddElement(/datum/element/basic_eating, heal_amt = 10, food_types = goliath_foods) + AddComponent(/datum/component/ai_target_timer) AddComponent(/datum/component/basic_mob_attack_telegraph) AddComponentFrom(INNATE_TRAIT, /datum/component/shovel_hands) if (tameable) diff --git a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm index 27de70c5653..83369de8862 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm @@ -1,3 +1,6 @@ +/// We won't use tentacles unless we have had the same target for this long +#define MIN_TIME_TO_TENTACLE 3 SECONDS + /datum/ai_controller/basic_controller/goliath blackboard = list( BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/allow_items/goliath, @@ -20,12 +23,16 @@ stat_attack = HARD_CRIT /datum/ai_planning_subtree/basic_melee_attack_subtree/goliath + operational_datums = list(/datum/component/ai_target_timer) melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/goliath /// Go for the tentacles if they're available /datum/ai_behavior/basic_melee_attack/goliath /datum/ai_behavior/basic_melee_attack/goliath/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key, health_ratio_key) + var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0 + if (time_on_target < MIN_TIME_TO_TENTACLE) + return ..() var/mob/living/target = controller.blackboard[target_key] // Interrupt attack chain to use tentacles, unless the target is already tentacled if (ismecha(target) || (isliving(target) && !target.has_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled))) @@ -37,11 +44,15 @@ /datum/ai_planning_subtree/targeted_mob_ability/goliath_tentacles ability_key = BB_GOLIATH_TENTACLES + operational_datums = list(/datum/component/ai_target_timer) /datum/ai_planning_subtree/targeted_mob_ability/goliath_tentacles/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) var/mob/living/target = controller.blackboard[target_key] if (!(isliving(target) || ismecha(target)) || (isliving(target) && target.has_status_effect(/datum/status_effect/incapacitating/stun/goliath_tentacled))) return // Target can be an item or already grabbed, we don't want to tentacle those + var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0 + if (time_on_target < MIN_TIME_TO_TENTACLE) + return // We need to spend some time acquiring our target first return ..() /// If we got nothing better to do, find a turf we can search for tasty roots and such @@ -115,3 +126,5 @@ /datum/ai_behavior/goliath_dig/finish_action(datum/ai_controller/controller, succeeded, target_key) . = ..() controller.clear_blackboard_key(target_key) + +#undef MIN_TIME_TO_TENTACLE diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher.dm index faaac2e29af..327c202a67d 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher.dm @@ -11,8 +11,8 @@ base_pixel_x = -12 speak_emote = list("chimes") speed = 3 - maxHealth = 200 - health = 200 + maxHealth = 160 + health = 160 attack_verb_continuous = "buffets" attack_verb_simple = "buffet" crusher_loot = /obj/item/crusher_trophy/watcher_wing @@ -44,6 +44,7 @@ AddElement(/datum/element/ai_retaliate) AddElement(/datum/element/simple_flying) AddElement(/datum/element/content_barfer) + AddComponent(/datum/component/ai_target_timer) AddComponent(/datum/component/basic_ranged_ready_overlay, overlay_state = eye_glow) AddComponent(\ /datum/component/ranged_attacks,\ @@ -84,8 +85,8 @@ icon_living = "watcher_magmawing" icon_dead = "watcher_magmawing_dead" eye_glow = "fire_glow" - maxHealth = 215 //Compensate for the lack of slowdown on projectiles with a bit of extra health - health = 215 + maxHealth = 175 //Compensate for the lack of slowdown on projectiles with a bit of extra health + health = 175 projectile_type = /obj/projectile/temp/watcher/magma_wing gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze/fire crusher_loot = /obj/item/crusher_trophy/blaster_tubes/magma_wing @@ -98,8 +99,8 @@ icon_state = "watcher_icewing" icon_living = "watcher_icewing" icon_dead = "watcher_icewing_dead" - maxHealth = 170 - health = 170 + maxHealth = 130 + health = 130 projectile_type = /obj/projectile/temp/watcher/ice_wing gaze_attack = /datum/action/cooldown/mob_cooldown/watcher_gaze/ice butcher_results = list(/obj/item/stack/ore/diamond = 5, /obj/item/stack/sheet/bone = 1) diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm index 3c8d23c4500..3634b14b3db 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm @@ -16,6 +16,7 @@ /datum/ai_planning_subtree/targeted_mob_ability/overwatch ability_key = BB_WATCHER_OVERWATCH + operational_datums = list(/datum/component/ai_target_timer) /datum/ai_planning_subtree/targeted_mob_ability/overwatch/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) var/mob/living/living_pawn = controller.pawn @@ -24,6 +25,9 @@ var/atom/target = controller.blackboard[target_key] if (QDELETED(target) || HAS_TRAIT(target, TRAIT_OVERWATCH_IMMUNE)) return // We should probably let miners move sometimes + var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0 + if (time_on_target < 5 SECONDS) + return // We need to spend some time acquiring our target first return ..() /datum/ai_planning_subtree/use_mob_ability/gaze diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm index 0f8d36f2e82..e4eb9562f53 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_gaze.dm @@ -84,7 +84,8 @@ /// Display an animated overlay over our head to indicate what's going on /datum/action/cooldown/mob_cooldown/watcher_gaze/proc/show_indicator_overlay(overlay_state) clear_current_overlay() - current_overlay = image(icon = 'icons/effects/eldritch.dmi', loc = owner, icon_state = overlay_state, pixel_x = -owner.pixel_x, pixel_y = 28) + current_overlay = image(icon = 'icons/effects/eldritch.dmi', loc = owner, icon_state = overlay_state, pixel_x = -owner.pixel_x, pixel_y = 28, layer = ABOVE_ALL_MOB_LAYER) + SET_PLANE_EXPLICIT(current_overlay, ABOVE_LIGHTING_PLANE, owner) for(var/client/add_to in GLOB.clients) add_to.images += current_overlay diff --git a/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm b/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm index 71fa67ed6fb..36ad2d61b4c 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_overwatch.dm @@ -96,7 +96,7 @@ . = ..() if (!.) return FALSE - ADD_TRAIT(owner, TRAIT_OVERWATCHED, TRAIT_STATUS_EFFECT(id)) + owner.add_traits(list(TRAIT_OVERWATCHED, TRAIT_OVERWATCH_IMMUNE), TRAIT_STATUS_EFFECT(id)) owner.do_alert_animation() owner.Immobilize(0.25 SECONDS) // Just long enough that they don't trigger it by mistake owner.playsound_local(owner, 'sound/machines/chime.ogg', 50, TRUE) @@ -109,7 +109,7 @@ /datum/status_effect/overwatch/on_remove() UnregisterSignal(owner, forbidden_actions + list(COMSIG_QDELETING, COMSIG_LIVING_DEATH)) QDEL_NULL(link) - REMOVE_TRAIT(owner, TRAIT_OVERWATCHED, TRAIT_STATUS_EFFECT(id)) + owner.remove_traits(list(TRAIT_OVERWATCHED, TRAIT_OVERWATCH_IMMUNE), TRAIT_STATUS_EFFECT(id)) if (!QDELETED(owner)) owner.apply_status_effect(/datum/status_effect/overwatch_immune) return ..() @@ -135,6 +135,7 @@ qdel(src) return overwatch_triggered = TRUE + watcher.do_alert_animation() INVOKE_ASYNC(watcher, TYPE_PROC_REF(/atom/, fire_projectile), projectile_type, owner, projectile_sound) /// Can't overwatch you if I don't exist diff --git a/tgstation.dme b/tgstation.dme index c45d93fcd92..b55244c624d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -983,6 +983,7 @@ #include "code\datums\components\action_item_overlay.dm" #include "code\datums\components\admin_popup.dm" #include "code\datums\components\aggro_emote.dm" +#include "code\datums\components\ai_has_target_timer.dm" #include "code\datums\components\ai_retaliate_advanced.dm" #include "code\datums\components\anti_magic.dm" #include "code\datums\components\appearance_on_aggro.dm"