Adds a priority targeting framework, and priority targeting to mining mobs (#95720)

## About The Pull Request

Introduces a new targeting priority strategy system for basicmob AIs,
which allows controllers to decide which mob to prioritize over others.

Mining mobs will now focus on the NODE drone unless hit, and will pursue
the attacker for 25 seconds before dropping the aggro. They also get
increased aggro if you've attacked other mobs in their view recently,
and after a few hits will have enough aggro to swap to you from the NODE
drone. Ashwalkers get a reduction in aggro because they live there.
Legion broods and brimdemons will immediately target anyone who attacks
their allies rather than waiting for multiple hits. Broods also now
inherit their parent's targets and retaliation/reinforcements lists.


https://github.com/user-attachments/assets/6baaba8a-8b3c-4b2f-ae8b-842f0b1f2b6d

#### This is a bounty for ArcaneMusic

## Why It's Good For The Game

Makes vent defense mob behavior more predictable and easier for players
to manipulate, allowing them to draw aggro from the NODE drone should
make vents more engaging and less of an AI rng fest

## Changelog
🆑
add: Mining mobs now use priority when choosing their target,
prioritizing NODE drones over miners who haven't attacked them or their
allies
/🆑
This commit is contained in:
SmArtKar
2026-05-08 21:40:01 +02:00
committed by GitHub
parent 7579ccc8c2
commit 119c1e9ccc
24 changed files with 216 additions and 38 deletions
@@ -1,5 +1,3 @@
#define REINFORCEMENTS_COOLDOWN (30 SECONDS)
/// Calls all nearby mobs that share a faction to give backup in combat
/datum/ai_planning_subtree/call_reinforcements
/// Blackboard key containing something to say when calling reinforcements (takes precedence over emotes)
@@ -21,8 +19,6 @@
controller.queue_behavior(/datum/ai_behavior/perform_speech, call_say)
else if(!isnull(call_emote))
controller.queue_behavior(/datum/ai_behavior/perform_emote, call_emote)
else
controller.queue_behavior(/datum/ai_behavior/perform_emote, "cries for help!")
controller.queue_behavior(call_type)
@@ -30,20 +26,43 @@
/datum/ai_planning_subtree/call_reinforcements/proc/decide_to_call(datum/ai_controller/controller)
return controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && istype(controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET], /mob)
/datum/ai_planning_subtree/call_reinforcements/mining
call_type = /datum/ai_behavior/call_reinforcements/mining
/// Call out to all mobs in the specified range for help
/datum/ai_behavior/call_reinforcements
/// How frequently can we call for reinforcements?
var/cooldown = 30 SECONDS
/// Range to call reinforcements from
var/reinforcements_range = 15
/datum/ai_behavior/call_reinforcements/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/pawn_mob = controller.pawn
for(var/mob/other_mob in oview(reinforcements_range, pawn_mob))
if(pawn_mob.faction_check_atom(other_mob) && !isnull(other_mob.ai_controller))
// Add our current target to their retaliate list so that they'll attack our aggressor
other_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET])
other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
if(!pawn_mob.faction_check_atom(other_mob) || isnull(other_mob.ai_controller))
continue
// Add our current target to their retaliate list so that they'll attack our aggressor
other_mob.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET], world.time)
other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + REINFORCEMENTS_COOLDOWN)
controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + cooldown)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
#undef REINFORCEMENTS_COOLDOWN
/// Does not force retaliation, but increases targeting priority instead
/datum/ai_behavior/call_reinforcements/mining
cooldown = 1 SECONDS
reinforcements_range = 7
/datum/ai_behavior/call_reinforcements/mining/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/pawn_mob = controller.pawn
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
for(var/mob/other_mob in oview(reinforcements_range, pawn_mob))
if(!pawn_mob.faction_check_atom(other_mob) || isnull(other_mob.ai_controller))
continue
var/list/existing_requests = other_mob.ai_controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS]
if (!existing_requests || !existing_requests[target])
other_mob.ai_controller.set_blackboard_key_assoc_lazylist(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, target, list())
other_mob.ai_controller.add_blackboard_key_assoc(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, target, world.time)
controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN, world.time + cooldown)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
@@ -49,7 +49,7 @@
failed_targeting(pawn)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, final_target)
controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, final_target, world.time)
pawn.visible_message(span_warning("[pawn] glares grumpily at [final_target]!"))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
@@ -43,6 +43,12 @@
var/list/shitlist = controller.blackboard[shitlist_key]
var/atom/existing_target = controller.blackboard[target_key]
var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[BB_TARGET_PRIORITY_STRATEGY])
var/existing_priority = 0
// If we have an existing target and its priority is higher than our new target's, don't switch focus
if (priority_strategy && existing_target)
existing_priority = priority_strategy.get_target_priority(controller, existing_target)
if (!check_faction)
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
@@ -53,13 +59,12 @@
for(var/mob/living/potential_target as anything in shitlist)
if(!targeting_strategy.can_attack(living_mob, potential_target, vision_range))
continue
// Strict comparasion because priority strategies might not care about retaliation, so this makes existing targets not override potential retaliates
if (priority_strategy && priority_strategy.get_target_priority(controller, potential_target) < existing_priority)
continue
enemies_list += potential_target
if(!length(enemies_list))
if(existing_target)
controller.clear_blackboard_key(target_key)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/atom/new_target = pick_final_target(controller, enemies_list)
@@ -74,7 +79,10 @@
/// Returns the desired final target from the filtered list of enemies
/datum/ai_behavior/target_from_retaliate_list/proc/pick_final_target(datum/ai_controller/controller, list/enemies_list)
return pick(enemies_list)
var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[BB_TARGET_PRIORITY_STRATEGY])
if (!priority_strategy)
return pick(enemies_list)
return priority_strategy.select_target(controller, enemies_list)
/datum/ai_behavior/target_from_retaliate_list/finish_action(datum/ai_controller/controller, succeeded, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction)
. = ..()