From 119c1e9ccca456617c81803acfd0f54c34b4bb55 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Fri, 8 May 2026 21:40:01 +0200 Subject: [PATCH] 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 :cl: add: Mining mobs now use priority when choosing their target, prioritizing NODE drones over miners who haven't attacked them or their allies /:cl: --- code/__DEFINES/ai/ai.dm | 1 + code/__DEFINES/ai/ai_blackboard.dm | 6 +- .../subsystem/processing/ai_behaviors.dm | 21 +++-- .../basic_ai_behaviors/targeting.dm | 30 +++++-- .../basic_subtrees/call_reinforcements.dm | 39 ++++++--- .../basic_subtrees/capricious_retaliate.dm | 2 +- .../basic_subtrees/target_retaliate.dm | 18 ++-- .../_target_priority_strategy.dm | 13 +++ .../mining_strategies.dm | 84 +++++++++++++++++++ .../components/ai_retaliate_advanced.dm | 2 +- code/datums/elements/ai_retaliate.dm | 2 +- code/modules/mob/living/basic/clown/clown.dm | 2 +- .../basic/lavaland/bileworm/bileworm_ai.dm | 2 + .../basic/lavaland/brimdemon/brimdemon_ai.dm | 2 + .../basic/lavaland/goliath/goliath_ai.dm | 2 + .../living/basic/lavaland/legion/legion_ai.dm | 3 + .../basic/lavaland/legion/legion_brood.dm | 11 ++- .../lavaland/lobstrosity/lobstrosity_ai.dm | 2 + .../mob/living/basic/lavaland/mook/mook.dm | 2 +- .../basic/lavaland/watcher/watcher_ai.dm | 2 + .../mob/living/basic/space_fauna/carp/carp.dm | 2 +- .../living/basic/space_fauna/garden_gnome.dm | 2 +- .../mob/living/basic/trooper/nanotrasen.dm | 2 +- tgstation.dme | 2 + 24 files changed, 216 insertions(+), 38 deletions(-) create mode 100644 code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm create mode 100644 code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm index aafd3feb203..f8285b075e4 100644 --- a/code/__DEFINES/ai/ai.dm +++ b/code/__DEFINES/ai/ai.dm @@ -1,5 +1,6 @@ #define GET_AI_BEHAVIOR(behavior_type) SSai_behaviors.ai_behaviors[behavior_type] #define GET_TARGETING_STRATEGY(targeting_type) SSai_behaviors.targeting_strategies[targeting_type] +#define GET_TARGET_PRIORITY_STRATEGY(targeting_type) SSai_behaviors.target_priority_strategies[targeting_type] #define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type) //AI controller flags diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm index 3e9a65027af..660a4e3c82c 100644 --- a/code/__DEFINES/ai/ai_blackboard.dm +++ b/code/__DEFINES/ai/ai_blackboard.dm @@ -83,6 +83,7 @@ #define BB_BASIC_MOB_CURRENT_TARGET "BB_basic_current_target" #define BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION "BB_basic_current_target_hiding_location" #define BB_TARGETING_STRATEGY "targeting_strategy" +#define BB_TARGET_PRIORITY_STRATEGY "target_priority_strategy" #define BB_HUNT_TARGETING_STRATEGY "hunt_targeting_strategy" ///some behaviors that check current_target also set this on deep crit mobs #define BB_BASIC_MOB_EXECUTION_TARGET "BB_basic_execution_target" @@ -96,6 +97,7 @@ #define BB_TARGET_HELD_ITEM "BB_target_held_item" /// How likely is this mob to move when idle per tick? #define BB_BASIC_MOB_IDLE_WALK_CHANCE "BB_basic_idle_walk_chance" +#define BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN "BB_basic_mob_target_refresh_cooldown" /// Minimum range to keep target within #define BB_RANGED_SKIRMISH_MIN_DISTANCE "BB_ranged_skirmish_min_distance" @@ -140,7 +142,7 @@ /// How often can we get spooked by a target? #define BB_STATIONARY_COOLDOWN "BB_stationary_cooldown" -///List of mobs who have damaged us +/// Assoc list of mobs who have damaged us -> Moment at which they damaged us #define BB_BASIC_MOB_RETALIATE_LIST "BB_basic_mob_shitlist" /// Chance to randomly acquire a new target @@ -197,6 +199,8 @@ #define BB_BASIC_MOB_REINFORCEMENT_TARGET "BB_basic_mob_reinforcement_target" /// The next time at which this mob can call for reinforcements #define BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN "BB_basic_mob_reinforcements_cooldown" +/// Soft-reinforcements requests which will cause the mob to prioritize the attacker +#define BB_MINING_MOB_REINFORCEMENTS_REQUESTS "BB_mining_mob_reinforcements_requests" /// the direction we started when executing stare at things #define BB_STARTING_DIRECTION "BB_startdir" diff --git a/code/controllers/subsystem/processing/ai_behaviors.dm b/code/controllers/subsystem/processing/ai_behaviors.dm index 7304c5ed362..aa5ef4eb55c 100644 --- a/code/controllers/subsystem/processing/ai_behaviors.dm +++ b/code/controllers/subsystem/processing/ai_behaviors.dm @@ -8,24 +8,33 @@ PROCESSING_SUBSYSTEM_DEF(ai_behaviors) /datum/controller/subsystem/movement/ai_movement, ) wait = 1 - ///List of all ai_behavior singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors() + /// List of all ai_behavior singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_ai_behaviors() var/list/ai_behaviors - ///List of all targeting_strategy singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors() + /// List of all targeting_strategy singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_targeting_strats() var/list/targeting_strategies + /// List of all target_priority_strategy singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_target_priority_strats() + var/list/target_priority_strategies /datum/controller/subsystem/processing/ai_behaviors/Initialize() - SetupAIBehaviors() - SetupTargetingStrats() + setup_ai_behaviors() + setup_targeting_strats() + setup_target_priority_strats() return SS_INIT_SUCCESS -/datum/controller/subsystem/processing/ai_behaviors/proc/SetupAIBehaviors() +/datum/controller/subsystem/processing/ai_behaviors/proc/setup_ai_behaviors() ai_behaviors = list() for(var/behavior_type in subtypesof(/datum/ai_behavior)) var/datum/ai_behavior/ai_behavior = new behavior_type ai_behaviors[behavior_type] = ai_behavior -/datum/controller/subsystem/processing/ai_behaviors/proc/SetupTargetingStrats() +/datum/controller/subsystem/processing/ai_behaviors/proc/setup_targeting_strats() targeting_strategies = list() for(var/target_type in subtypesof(/datum/targeting_strategy)) var/datum/targeting_strategy/target_start = new target_type targeting_strategies[target_type] = target_start + +/datum/controller/subsystem/processing/ai_behaviors/proc/setup_target_priority_strats() + target_priority_strategies = list() + for(var/target_type in subtypesof(/datum/target_priority_strategy)) + var/datum/target_priority_strategy/target_start = new target_type + target_priority_strategies[target_type] = target_start diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm index 9305c398611..de9697902d2 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm @@ -12,6 +12,10 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/ var/vision_range = 9 /// Blackboard key for aggro range, uses vision range if not specified var/aggro_range_key = BB_AGGRO_RANGE + /// Blackboard key for the target priority strategy + var/priority_strategy_key = BB_TARGET_PRIORITY_STRATEGY + /// If we have a priority strategy set, how often do we refresh our target search? + var/priority_refresh_cooldown = 6 SECONDS /datum/ai_behavior/find_potential_targets/get_cooldown(datum/ai_controller/cooldown_for) if(cooldown_for.blackboard[BB_FIND_TARGETS_FIELD(type)]) @@ -26,7 +30,8 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/ CRASH("No target datum was supplied in the blackboard for [controller.pawn]") var/atom/current_target = controller.blackboard[target_key] - if (targeting_strategy.can_attack(living_mob, current_target, vision_range)) + var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[priority_strategy_key]) + if((!priority_strategy || controller.blackboard[BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN] > world.time) && current_target && targeting_strategy.can_attack(living_mob, current_target, vision_range)) return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED var/aggro_range = controller.blackboard[aggro_range_key] || vision_range @@ -46,22 +51,30 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/ potential_targets += hostile_machine if(!potential_targets.len) - failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key) + if(!current_target) + failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key) return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED var/list/filtered_targets = list() + var/current_priority = 0 + if(priority_strategy) + current_priority = priority_strategy.get_target_priority(controller, current_target) for(var/atom/pot_target in potential_targets) - if(targeting_strategy.can_attack(living_mob, pot_target))//Can we attack it? - filtered_targets += pot_target + if(!targeting_strategy.can_attack(living_mob, pot_target)) continue + if (priority_strategy && priority_strategy.get_target_priority(controller, pot_target) < current_priority) + continue + filtered_targets += pot_target if(!filtered_targets.len) - failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key) + if(!current_target) + failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key) return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED var/atom/target = pick_final_target(controller, filtered_targets) controller.set_blackboard_key(target_key, target) + controller.set_blackboard_key(BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN, world.time + priority_refresh_cooldown) var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, target) @@ -153,7 +166,10 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/ /// Returns the desired final target from the filtered list of targets /datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets) - return pick(filtered_targets) + var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[priority_strategy_key]) + if (!priority_strategy) + return pick(filtered_targets) + return priority_strategy.select_target(controller, filtered_targets) /// Targets with the trait specified by the BB_TARGET_PRIORITY_TRAIT blackboard key will be prioritized over the rest. /datum/ai_behavior/find_potential_targets/prioritize_trait @@ -164,7 +180,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/ if(HAS_TRAIT(target, controller.blackboard[BB_TARGET_PRIORITY_TRAIT])) priority_targets += target if(length(priority_targets)) - return pick(priority_targets) + return ..(controller, priority_targets) return ..() /datum/ai_behavior/find_potential_targets/bigger_range diff --git a/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm index f78697b2b81..46886680d52 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/call_reinforcements.dm @@ -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 diff --git a/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm index 52b19036b9a..211e01be1f2 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm @@ -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 diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm index 6893bad6f6a..f1c04e1d8a7 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm @@ -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) . = ..() diff --git a/code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm b/code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm new file mode 100644 index 00000000000..3cb58ba086c --- /dev/null +++ b/code/datums/ai/basic_mobs/target_priority_strategies/_target_priority_strategy.dm @@ -0,0 +1,13 @@ +/// Target priority datum, exists to allow mobs to have custom targeting priorities. Singleton. +/datum/target_priority_strategy + +/// Returns a number representing the priority of a target, higher -> more likely to attack +/datum/target_priority_strategy/proc/get_target_priority(datum/ai_controller/controller, atom/target) + return 1 + +/// Returns a single atom from the list of passed targets +/datum/target_priority_strategy/proc/select_target(datum/ai_controller/controller, list/atom/targets) + var/list/target_priorities = list() + for (var/atom/target as anything in targets) + target_priorities[target] = get_target_priority(controller, target) + return pick_weight(target_priorities) diff --git a/code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm b/code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm new file mode 100644 index 00000000000..e69ee59ff10 --- /dev/null +++ b/code/datums/ai/basic_mobs/target_priority_strategies/mining_strategies.dm @@ -0,0 +1,84 @@ +/// Ashies get lower priorities than humans +#define AGGRO_PRIORITY_ASHWALKER 1 +/// Everyone by default +#define AGGRO_PRIORITY_HUMAN 2 +/// Mobs who have attacked someone in our view recently +#define AGGRO_PRIORITY_MINER 8 +/// NODE drone priority +#define AGGRO_PRIORITY_NODE 10 +/// NODE drone priority for legion broods and brimdemons +#define AGGRO_PRIORITY_NODE_LOW_PRIO 3 +/// Priority for mobs we're retaliating against +#define AGGRO_PRIORITY_RETALIATE 15 + +/// Prioritizes NODE drones until attacked, then swaps back onto miners +/datum/target_priority_strategy/mining + /// For how long do we keep aggro on mobs over NODE drones? + var/retaliate_aggro_memory = 25 SECONDS + /// Priority for NODE drones + var/node_priority = AGGRO_PRIORITY_NODE + +/datum/target_priority_strategy/mining/get_target_priority(datum/ai_controller/controller, mob/living/target) + if (!isliving(target)) + return ..() + + // If a mob recently attacked us, it has higher priority than NODE drones, otherwise we care about NODE drones more + var/list/shitlist = controller.blackboard[BB_BASIC_MOB_RETALIATE_LIST] + if (shitlist?[target] && (shitlist[target] + retaliate_aggro_memory > world.time)) + // Does not decay until it passes the retaliate memory threshold to avoid constant target swapping + return AGGRO_PRIORITY_RETALIATE + + if (istype(target, /mob/living/basic/node_drone)) + return node_priority + + // Check if they've recently attacked any of our friends, if so - increase their priority by 1 for each attack in the past [retaliate_aggro_memory] seconds + var/list/allies_shitlist = controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS] + var/list/target_requests = allies_shitlist?[target] + if (!target_requests) + return target.has_faction(FACTION_ASHWALKER) ? AGGRO_PRIORITY_ASHWALKER : AGGRO_PRIORITY_HUMAN + + var/aggro_boost = 0 + var/total_requests = length(target_requests) + // Goes end-to-start because we do not care about requests older than [retaliate_aggro_memory] + for (var/i in 1 to total_requests) + var/request_time = target_requests[total_requests - i + 1] + if (request_time + retaliate_aggro_memory < world.time) + break + aggro_boost += 1 + + if (aggro_boost) + return AGGRO_PRIORITY_MINER + aggro_boost + return target.has_faction(FACTION_ASHWALKER) ? AGGRO_PRIORITY_ASHWALKER : AGGRO_PRIORITY_HUMAN + +/datum/target_priority_strategy/mining/select_target(datum/ai_controller/controller, list/atom/targets) + var/max_priority = 0 + var/min_distance = INFINITY + var/lucky_fella = null + + // Selects highest priority targets, then picks the closest + for (var/atom/target as anything in targets) + var/target_prio = get_target_priority(controller, target) + if (target_prio < max_priority) + continue + + if (target_prio > max_priority) + max_priority = target_prio + min_distance = INFINITY + + var/target_dist = get_dist(controller.pawn, target) + if (target_dist < min_distance) + lucky_fella = target + min_distance = target_dist + + return pick(lucky_fella) + +/datum/target_priority_strategy/mining/low_node_priority + // Higher than normal humans but will instantly pivot towards anyone who attacks or attacked mobs in our vicinity + node_priority = AGGRO_PRIORITY_NODE_LOW_PRIO + +#undef AGGRO_PRIORITY_ASHWALKER +#undef AGGRO_PRIORITY_HUMAN +#undef AGGRO_PRIORITY_MINER +#undef AGGRO_PRIORITY_NODE +#undef AGGRO_PRIORITY_NODE_LOW_PRIO +#undef AGGRO_PRIORITY_RETALIATE diff --git a/code/datums/components/ai_retaliate_advanced.dm b/code/datums/components/ai_retaliate_advanced.dm index d734fa92b3c..12e22c3d395 100644 --- a/code/datums/components/ai_retaliate_advanced.dm +++ b/code/datums/components/ai_retaliate_advanced.dm @@ -33,5 +33,5 @@ if (!victim.ai_controller) return - victim.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + victim.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time) post_retaliate_callback?.InvokeAsync(attacker) diff --git a/code/datums/elements/ai_retaliate.dm b/code/datums/elements/ai_retaliate.dm index 03465d3c246..4fcb5158143 100644 --- a/code/datums/elements/ai_retaliate.dm +++ b/code/datums/elements/ai_retaliate.dm @@ -24,4 +24,4 @@ if (victim == attacker) return - victim.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + victim.ai_controller?.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time) diff --git a/code/modules/mob/living/basic/clown/clown.dm b/code/modules/mob/living/basic/clown/clown.dm index 7808c45f72f..9c8ec1e8bb7 100644 --- a/code/modules/mob/living/basic/clown/clown.dm +++ b/code/modules/mob/living/basic/clown/clown.dm @@ -57,7 +57,7 @@ if (!istype(attacker)) return for (var/mob/living/basic/clown/harbringer in oview(src, 7)) - harbringer.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + harbringer.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time) /mob/living/basic/clown/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) if(!istype(target, /obj/item/food/grown/banana/bunch)) diff --git a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm index e18af7ed2ce..01c4a564673 100644 --- a/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm +++ b/code/modules/mob/living/basic/lavaland/bileworm/bileworm_ai.dm @@ -1,10 +1,12 @@ /datum/ai_controller/basic_controller/bileworm blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/bileworm, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining, ) planning_subtrees = list( /datum/ai_planning_subtree/escape_captivity, + /datum/ai_planning_subtree/call_reinforcements/mining, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/bileworm_attack, /datum/ai_planning_subtree/bileworm_execute, diff --git a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm index eb6f6f671ed..b60d7ab4a90 100644 --- a/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm +++ b/code/modules/mob/living/basic/lavaland/brimdemon/brimdemon_ai.dm @@ -4,6 +4,7 @@ /datum/ai_controller/basic_controller/brimdemon blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining/low_node_priority, BB_TARGET_MINIMUM_STAT = HARD_CRIT, ) @@ -11,6 +12,7 @@ idle_behavior = /datum/idle_behavior/idle_random_walk/no_target planning_subtrees = list( /datum/ai_planning_subtree/escape_captivity, + /datum/ai_planning_subtree/call_reinforcements/mining, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic, /datum/ai_planning_subtree/move_to_cardinal/brimdemon, 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 63842f05ef5..5fe6e9d891d 100644 --- a/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm +++ b/code/modules/mob/living/basic/lavaland/goliath/goliath_ai.dm @@ -4,6 +4,7 @@ /datum/ai_controller/basic_controller/goliath blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining, BB_TARGET_MINIMUM_STAT = HARD_CRIT, ) @@ -11,6 +12,7 @@ idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( /datum/ai_planning_subtree/escape_captivity, + /datum/ai_planning_subtree/call_reinforcements/mining, /datum/ai_planning_subtree/target_retaliate/check_faction, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/find_food, diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm index 96cde9e5ac5..ff81f32ffc0 100644 --- a/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm +++ b/code/modules/mob/living/basic/lavaland/legion/legion_ai.dm @@ -2,6 +2,7 @@ /datum/ai_controller/basic_controller/legion blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/legion, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining, BB_TARGET_MINIMUM_STAT = HARD_CRIT, BB_AGGRO_RANGE = 5, // Unobservant BB_BASIC_MOB_FLEE_DISTANCE = 6, @@ -11,6 +12,7 @@ idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( /datum/ai_planning_subtree/escape_captivity, + /datum/ai_planning_subtree/call_reinforcements/mining, /datum/ai_planning_subtree/random_speech/legion, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/targeted_mob_ability, @@ -21,6 +23,7 @@ /datum/ai_controller/basic_controller/legion_brood blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/legion, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining/low_node_priority, BB_TARGET_MINIMUM_STAT = HARD_CRIT, ) diff --git a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm index 1fc6a7d02be..dd2bbe18de2 100644 --- a/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm +++ b/code/modules/mob/living/basic/lavaland/legion/legion_brood.dm @@ -103,8 +103,17 @@ else add_ally(creator) created_by = WEAKREF(creator) - ai_controller?.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator) RegisterSignal(creator, COMSIG_QDELETING, PROC_REF(creator_destroyed)) + if (!ai_controller) + return + + ai_controller.set_blackboard_key(BB_LEGION_BROOD_CREATOR, creator) + if (!creator.ai_controller) + return + + // Inherit our creator's target and reinforcement requests + ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, creator.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + ai_controller.set_blackboard_key(BB_MINING_MOB_REINFORCEMENTS_REQUESTS, creator.ai_controller.blackboard[BB_MINING_MOB_REINFORCEMENTS_REQUESTS]) /// Reference handling /mob/living/basic/mining/legion_brood/proc/creator_destroyed() diff --git a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm index c634f1238d1..6906121b4b8 100644 --- a/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm +++ b/code/modules/mob/living/basic/lavaland/lobstrosity/lobstrosity_ai.dm @@ -8,6 +8,7 @@ /datum/ai_controller/basic_controller/lobstrosity blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining, BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends, BB_TARGET_MINIMUM_STAT = HARD_CRIT, BB_LOBSTROSITY_EXPLOIT_TRAITS = list(TRAIT_INCAPACITATED, TRAIT_FLOORED, TRAIT_IMMOBILIZED, TRAIT_KNOCKEDOUT), @@ -24,6 +25,7 @@ idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( /datum/ai_planning_subtree/escape_captivity, + /datum/ai_planning_subtree/call_reinforcements/mining, /datum/ai_planning_subtree/random_speech/insect, /datum/ai_planning_subtree/hoard_fingers, /datum/ai_planning_subtree/pet_planning, diff --git a/code/modules/mob/living/basic/lavaland/mook/mook.dm b/code/modules/mob/living/basic/lavaland/mook/mook.dm index 80d398f23d0..966c1bfba18 100644 --- a/code/modules/mob/living/basic/lavaland/mook/mook.dm +++ b/code/modules/mob/living/basic/lavaland/mook/mook.dm @@ -187,7 +187,7 @@ if(istype(intruder, /mob/living/basic/mining/mook)) return for(var/mob/living/basic/mining/mook/villager in oview(src, 9)) - villager.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, intruder) + villager.ai_controller?.set_blackboard_key_assoc(BB_BASIC_MOB_RETALIATE_LIST, intruder, world.time) /mob/living/basic/mining/mook/worker 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 d31f76eaee6..15df9f7eba1 100644 --- a/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm +++ b/code/modules/mob/living/basic/lavaland/watcher/watcher_ai.dm @@ -1,12 +1,14 @@ /datum/ai_controller/basic_controller/watcher blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_TARGET_PRIORITY_STRATEGY = /datum/target_priority_strategy/mining, ) ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( /datum/ai_planning_subtree/escape_captivity, + /datum/ai_planning_subtree/call_reinforcements/mining, /datum/ai_planning_subtree/target_retaliate/check_faction, /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/maintain_distance, diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm index 6f1ff9e864f..d114b6d42a1 100644 --- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm +++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm @@ -320,4 +320,4 @@ /// If someone slaps one of the school, scatter /mob/living/basic/carp/passive/proc/on_attacked(mob/living/attacker) for(var/mob/living/basic/carp/passive/schoolmate in oview(src, 9)) - schoolmate.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + schoolmate.ai_controller?.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time) diff --git a/code/modules/mob/living/basic/space_fauna/garden_gnome.dm b/code/modules/mob/living/basic/space_fauna/garden_gnome.dm index d2ab4e691b5..f6926b2ea51 100644 --- a/code/modules/mob/living/basic/space_fauna/garden_gnome.dm +++ b/code/modules/mob/living/basic/space_fauna/garden_gnome.dm @@ -121,7 +121,7 @@ if (!istype(attacker)) return for (var/mob/living/basic/garden_gnome/potential_gnome in oview(src, 7)) - potential_gnome.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + potential_gnome.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time) /datum/ai_controller/basic_controller/garden_gnome blackboard = list( diff --git a/code/modules/mob/living/basic/trooper/nanotrasen.dm b/code/modules/mob/living/basic/trooper/nanotrasen.dm index 80f0f348624..8798467969b 100644 --- a/code/modules/mob/living/basic/trooper/nanotrasen.dm +++ b/code/modules/mob/living/basic/trooper/nanotrasen.dm @@ -98,4 +98,4 @@ if (!istype(attacker)) return for (var/mob/living/basic/trooper/nanotrasen/potential_trooper in oview(src, 7)) - potential_trooper.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + potential_trooper.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker, world.time) diff --git a/tgstation.dme b/tgstation.dme index 36f5422e607..8604a7465cb 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -1034,6 +1034,8 @@ #include "code\datums\ai\basic_mobs\pet_commands\pet_follow_friend.dm" #include "code\datums\ai\basic_mobs\pet_commands\pet_use_targeted_ability.dm" #include "code\datums\ai\basic_mobs\pet_commands\play_dead.dm" +#include "code\datums\ai\basic_mobs\target_priority_strategies\_target_priority_strategy.dm" +#include "code\datums\ai\basic_mobs\target_priority_strategies\mining_strategies.dm" #include "code\datums\ai\basic_mobs\targeting_strategies\_targeting_strategy.dm" #include "code\datums\ai\basic_mobs\targeting_strategies\basic_targeting_strategy.dm" #include "code\datums\ai\basic_mobs\targeting_strategies\dont_target_friends.dm"