From b73b8b7d0a0c5bc78632d9ef44a96806a1541bcc Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Mon, 21 Aug 2023 20:32:45 +0200 Subject: [PATCH] [MIRROR] Mining mob tweaks [MDB IGNORE] (#23242) * Mining mob tweaks (#77763) ## About The Pull Request ~~I wanted to do this after #77700 (wow cool numbers) but nobody has merged it yet despite how simple it is so i'll just hope they don't conflict.~~ Thanks san I'm fucking about with mining mobs with the intention of making them more interesting but not necessarily towards making mining _harder_, but some of these changes unquestionably have done so. These changes are mostly in response to feedback about Watchers who are definitely significantly more threatening than previously, although some of this is user error. - Watchers are annoying when traversing lavaland because they use their ability on you instantly upon acquiring a target, if you are trying to escape other fauna this quickly becomes deadly. - A lot of players don't really realise what the overwatch ability is actually doing and so just complain about getting machine gunned. - If you _do_ react properly to the ability it still makes fighting them take a lot longer than it used to. - The "look away" icon is hard to see in the dark sometimes To ammeliorate these factors I have: - Reduced watcher health by ~20% - Display an alerted graphic over the head of the watcher every time you trigger the overwatch. - Multiple watchers now won't overwatch you at the same time (this made the "penalty" volley essentially become instant death) - The "look away" icon is rendered above the lighting plane so you can always see it - Added a new component which tracks how long a mob has had a specific target. - - Watchers will now only Overwatch you if they've seen you for at least 5 seconds (usually they'll try and shoot at you twice before this). - - Goliaths will only tentacle you if they've seen you for at least 3 seconds. If overwatch is still problematic after this I guess I can just nerf it to not track movement at all and only respond to attacks. ## Why It's Good For The Game I don't want to discourage miners from "actually mining" by having them get sniped just for walking around and the added time-to-kill on these guys could make clearing tendrils more tedious too. ## Changelog :cl: balance: Watchers have less health balance: You can't be overwatched by several watchers at a time balance: Watchers won't overwatch you instantly upon seeing you balance: Goliaths won't launch tentacles at you instantly upon seeing you /:cl: * Mining mob tweaks --------- Co-authored-by: Jacquerel --- code/__DEFINES/ai/ai_blackboard.dm | 3 + code/datums/ai/_ai_controller.dm | 2 + code/datums/components/ai_has_target_timer.dm | 79 +++++++++++++++++++ .../crates_lockers/closets/cardboardbox.dm | 1 + .../living/basic/lavaland/goliath/goliath.dm | 1 + .../basic/lavaland/goliath/goliath_ai.dm | 13 +++ .../living/basic/lavaland/watcher/watcher.dm | 13 +-- .../basic/lavaland/watcher/watcher_ai.dm | 4 + .../basic/lavaland/watcher/watcher_gaze.dm | 3 +- .../lavaland/watcher/watcher_overwatch.dm | 5 +- tgstation.dme | 1 + 11 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 code/datums/components/ai_has_target_timer.dm 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"