mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 13:10:02 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
. = ..()
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user