Basic mobs targeting, attacks, and pig migration. (#28987)

* Basic mobs targeting, attacks, and pig migration.

* run updatepaths

* fix duplicate macro def

* Update code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm

Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
Signed-off-by: warriorstar-orion <orion@snowfrost.garden>

---------

Signed-off-by: warriorstar-orion <orion@snowfrost.garden>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
This commit is contained in:
warriorstar-orion
2025-04-15 10:45:56 -04:00
committed by GitHub
parent 5da3694877
commit efc8adb6dd
44 changed files with 1190 additions and 69 deletions
@@ -0,0 +1,147 @@
/datum/ai_behavior/basic_melee_attack
action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
///do we finish this action after hitting once?
var/terminate_after_action = FALSE
/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!controller.blackboard[targeting_strategy_key])
CRASH("No targeting strategy was supplied in the blackboard for [controller.pawn]")
//Hiding location is priority
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
if(isliving(controller.pawn))
var/mob/living/pawn = controller.pawn
if(world.time < pawn.next_move)
return AI_BEHAVIOR_INSTANT
var/mob/living/basic/basic_mob = controller.pawn
//targeting strategy will kill the action if not real anymore
var/atom/target = controller.blackboard[target_key]
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
if(!targeting_strategy.can_attack(basic_mob, target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
controller.set_blackboard_key(hiding_location_key, hiding_target)
var/atom/final_target = hiding_target || target
controller.ai_interact(target = final_target, intent = INTENT_HARM)
if(terminate_after_action)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY
/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!succeeded)
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/basic_melee_attack/interact_once
terminate_after_action = TRUE
/datum/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/basic_ranged_attack
action_cooldown = 0.6 SECONDS
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
required_distance = 3
/// range we will try chasing the target before giving up
var/chase_range = 9
///do we care about avoiding friendly fire?
var/avoid_friendly_fire = FALSE
/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(HAS_TRAIT(controller.pawn, TRAIT_HANDS_BLOCKED))
return FALSE
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
var/mob/living/basic/basic_mob = controller.pawn
//targeting strategy will kill the action if not real anymore
var/atom/target = controller.blackboard[target_key]
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
if(!targeting_strategy.can_attack(basic_mob, target, chase_range))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
var/atom/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something!
var/atom/final_target = hiding_target ? hiding_target : target
if(!can_see(basic_mob, final_target, required_distance))
return AI_BEHAVIOR_INSTANT
if(avoid_friendly_fire && check_friendly_in_path(basic_mob, target, targeting_strategy))
adjust_position(basic_mob, target)
return AI_BEHAVIOR_DELAY
controller.set_blackboard_key(hiding_location_key, hiding_target)
basic_mob.RangedAttack(final_target)
return AI_BEHAVIOR_DELAY //only start the cooldown when the shot is shot
/datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!succeeded)
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/basic_ranged_attack/proc/check_friendly_in_path(mob/living/source, atom/target, datum/targeting_strategy/targeting_strategy)
var/list/turfs_list = calculate_trajectory(source, target)
for(var/turf/possible_turf as anything in turfs_list)
for(var/mob/living/potential_friend in possible_turf)
if(!targeting_strategy.can_attack(source, potential_friend))
return TRUE
return FALSE
/datum/ai_behavior/basic_ranged_attack/proc/adjust_position(mob/living/living_pawn, atom/target)
var/turf/our_turf = get_turf(living_pawn)
var/list/possible_turfs = list()
for(var/direction in GLOB.alldirs)
var/turf/target_turf = get_step(our_turf, direction)
if(isnull(target_turf))
continue
if(is_blocked_turf(target_turf) || get_dist(target_turf, target) > get_dist(living_pawn, target))
continue
possible_turfs += target_turf
if(!length(possible_turfs))
return
var/turf/picked_turf = get_closest_atom(/turf, possible_turfs, target)
step(living_pawn, get_dir(living_pawn, picked_turf))
/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source , atom/target)
var/list/turf_list = get_line(source, target)
var/list_length = length(turf_list) - 1
for(var/i in 1 to list_length)
var/turf/current_turf = turf_list[i]
var/turf/next_turf = turf_list[i + 1]
var/direction_to_turf = get_dir(current_turf, next_turf)
if(!IS_DIR_DIAGONAL(direction_to_turf))
continue
for(var/cardinal_direction in GLOB.cardinal)
if(cardinal_direction & direction_to_turf)
turf_list += get_step(current_turf, cardinal_direction)
turf_list -= get_turf(source)
turf_list -= get_turf(target)
return turf_list
/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire
avoid_friendly_fire = TRUE
@@ -0,0 +1,13 @@
/// Picks targets based on which one is closest to you, choice between targets at equal distance is arbitrary
/datum/ai_behavior/find_potential_targets/nearest
/datum/ai_behavior/find_potential_targets/nearest/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
var/turf/our_position = get_turf(controller.pawn)
return get_closest_atom(/atom/, filtered_targets, our_position)
/// As above but targets have been filtered from the 'retaliate' blackboard list
/datum/ai_behavior/target_from_retaliate_list/nearest
/datum/ai_behavior/target_from_retaliate_list/nearest/pick_final_target(datum/ai_controller/controller, list/enemies_list)
var/turf/our_position = get_turf(controller.pawn)
return get_closest_atom(/atom/, enemies_list, our_position)
@@ -0,0 +1,75 @@
/// Move to a position further away from your current target
/datum/ai_behavior/run_away_from_target
required_distance = 0
action_cooldown = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// How far do we try to run? Further makes for smoother running, but potentially weirder pathfinding
var/run_distance = DEFAULT_BASIC_FLEE_DISTANCE
/// Clear target if we finish the action unsuccessfully
var/clear_failed_targets = TRUE
/datum/ai_behavior/run_away_from_target/setup(datum/ai_controller/controller, target_key, hiding_location_key)
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
run_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || initial(run_distance)
if(!plot_path_away_from(controller, target))
return FALSE
return ..()
/datum/ai_behavior/run_away_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return AI_BEHAVIOR_DELAY
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
if(QDELETED(target) || !can_see(controller.pawn, target, run_distance))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
if(get_dist(controller.pawn, controller.current_movement_target) > required_distance)
return AI_BEHAVIOR_DELAY // Still heading over
if(plot_path_away_from(controller, target))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/run_away_from_target/proc/plot_path_away_from(datum/ai_controller/controller, atom/target)
var/turf/target_destination = get_turf(controller.pawn)
var/static/list/offset_angles = list(45, 90, 135, 180, 225, 270)
for(var/angle in offset_angles)
var/turf/test_turf = get_furthest_turf(controller.pawn, angle, target)
if(isnull(test_turf))
continue
var/distance_from_target = get_dist(target, test_turf)
if(distance_from_target <= get_dist(target, target_destination))
continue
target_destination = test_turf
if(distance_from_target == run_distance) //we already got the max running distance
break
if(target_destination == get_turf(controller.pawn))
return FALSE
set_movement_target(controller, target_destination)
return TRUE
/datum/ai_behavior/run_away_from_target/proc/get_furthest_turf(atom/source, angle, atom/target)
var/turf/return_turf
for(var/i in 1 to run_distance)
var/turf/test_destination = get_ranged_target_turf_direct(source, target, range = i, offset = angle)
if(is_blocked_turf(test_destination, excluded_objs = GLOB.airlocks + src))
break
return_turf = test_destination
return return_turf
/datum/ai_behavior/run_away_from_target/finish_action(datum/ai_controller/controller, succeeded, target_key, hiding_location_key)
. = ..()
if(clear_failed_targets)
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/run_away_from_target/run_and_shoot
clear_failed_targets = FALSE
/datum/ai_behavior/run_away_from_target/run_and_shoot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key)
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_pawn = controller.pawn
living_pawn.RangedAttack(target)
return ..()
@@ -0,0 +1,158 @@
/// List of objects that AIs will treat as targets
GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom)
/// Static typecache list of things we are interested in
/// Consider this a union of the for loop and the hearers call from below
/// Must be kept up to date with the contents of hostile_machines
GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(
/mob,
/obj/machinery/porta_turret,
/obj/mecha,
)))
/datum/ai_behavior/find_potential_targets
action_cooldown = 2 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// How far can we see stuff?
var/vision_range = 9
/// Blackboard key for aggro range, uses vision range if not specified
var/aggro_range_key = BB_AGGRO_RANGE
/datum/ai_behavior/find_potential_targets/get_cooldown(datum/ai_controller/cooldown_for)
if(cooldown_for.blackboard[BB_FIND_TARGETS_FIELD(type)])
return 60 SECONDS
return ..()
/datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
var/mob/living/living_mob = controller.pawn
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
if(!targeting_strategy)
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))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
controller.clear_blackboard_key(target_key)
// If we're using a field rn, just don't do anything yeah?
if(controller.blackboard[BB_FIND_TARGETS_FIELD(type)])
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/list/potential_targets = hearers(aggro_range, get_turf(controller.pawn)) - living_mob //Remove self, so we don't suicide
for(var/atom/hostile_machine as anything in GLOB.hostile_machines)
if(can_see(living_mob, hostile_machine, aggro_range))
potential_targets += hostile_machine
if(!length(potential_targets))
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()
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
continue
if(!length(filtered_targets))
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)
var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, target)
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.set_blackboard_key(hiding_location_key, potential_hiding_location)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_potential_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
var/aggro_range = controller.blackboard[aggro_range_key] || vision_range
// takes the larger between our range() input and our implicit hearers() input (world.view)
aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2))
// Alright, here's the interesting bit
// We're gonna use this max range to hook into a proximity field so we can just await someone interesting to come along
// Rather then trying to check every few seconds
var/datum/proximity_monitor/advanced/ai_target_tracking/detection_field = new(
controller.pawn,
aggro_range,
TRUE,
src,
controller,
target_key,
targeting_strategy_key,
hiding_location_key,
)
// We're gonna store this field in our blackboard, so we can clear it away if we end up finishing successsfully
controller.set_blackboard_key(BB_FIND_TARGETS_FIELD(type), detection_field)
/datum/ai_behavior/find_potential_targets/proc/new_turf_found(turf/found, datum/ai_controller/controller, datum/targeting_strategy/strategy)
var/valid_found = FALSE
var/mob/pawn = controller.pawn
for(var/maybe_target as anything in found)
if(maybe_target == pawn)
continue
if(!is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
continue
if(!strategy.can_attack(pawn, maybe_target))
continue
valid_found = TRUE
break
if(!valid_found)
return
// If we found any one thing we "could" attack, then run the full search again so we can select from the best possible canidate
var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
qdel(field) // autoclears so it's fine
// Fire instantly, you should find something I hope
controller.modify_cooldown(src, world.time)
/datum/ai_behavior/find_potential_targets/proc/atom_allowed(atom/movable/checking, datum/targeting_strategy/strategy, mob/pawn)
if(checking == pawn)
return FALSE
if(!ismob(checking) && !is_type_in_typecache(checking, GLOB.target_interested_atoms))
return FALSE
if(!strategy.can_attack(pawn, checking))
return FALSE
return TRUE
/datum/ai_behavior/find_potential_targets/proc/new_atoms_found(list/atom/movable/found, datum/ai_controller/controller, target_key, datum/targeting_strategy/strategy, hiding_location_key)
var/mob/pawn = controller.pawn
var/list/accepted_targets = list()
for(var/maybe_target as anything in found)
if(maybe_target == pawn)
continue
// Need to better handle viewers here
if(!ismob(maybe_target) && !is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
continue
if(!strategy.can_attack(pawn, maybe_target))
continue
accepted_targets += maybe_target
// Alright, we found something acceptable, let's use it yeah?
var/atom/target = pick_final_target(controller, accepted_targets)
controller.set_blackboard_key(target_key, target)
var/atom/potential_hiding_location = strategy.find_hidden_mobs(pawn, target)
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.set_blackboard_key(hiding_location_key, potential_hiding_location)
finish_action(controller, succeeded = TRUE)
/datum/ai_behavior/find_potential_targets/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(succeeded)
var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
qdel(field) // autoclears so it's fine
controller.cancel_actions() // On retarget cancel any further queued actions so that they will setup again with new target
controller.modify_cooldown(src, get_cooldown(controller))
/// 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)
@@ -0,0 +1,25 @@
/// Try to escape from your current target, without performing any other actions.
/datum/ai_planning_subtree/flee_target
/// Behaviour to execute in order to flee
var/flee_behaviour = /datum/ai_behavior/run_away_from_target
/// Blackboard key in which to store selected target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Blackboard key in which to store selected target's hiding place
var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
/datum/ai_planning_subtree/flee_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
var/atom/flee_from = controller.blackboard[target_key]
if(!should_flee(controller, flee_from))
return
var/flee_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE
if(get_dist(controller.pawn, flee_from) >= flee_distance)
return
controller.queue_behavior(flee_behaviour, target_key, hiding_place_key)
return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here.
/datum/ai_planning_subtree/flee_target/proc/should_flee(datum/ai_controller/controller, atom/flee_from)
if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING] || QDELETED(flee_from))
return FALSE
return TRUE
@@ -53,11 +53,3 @@
if(length(speak_verbs))
speak_verb = pick(speak_verbs)
controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play, speak_verb)
/datum/ai_planning_subtree/random_speech/cow
speech_chance = 2
speak = list("Moo?", "Moo", "MOOOOOO")
speak_verbs = list("moos", "moos hauntingly")
sound = list('sound/creatures/cow.ogg')
emote_hear = list("brays.")
emote_see = list("shakes her head.")
@@ -0,0 +1,13 @@
/datum/ai_planning_subtree/basic_melee_attack_subtree
/// What do we do in order to attack someone?
var/datum/ai_behavior/basic_melee_attack/melee_attack_behavior = /datum/ai_behavior/basic_melee_attack
/// Is this the last thing we do? (if we set a movement target, this will usually be yes)
var/end_planning = TRUE
/datum/ai_planning_subtree/basic_melee_attack_subtree/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return
controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
if(end_planning)
return SUBTREE_RETURN_FINISH_PLANNING // we are going into battle...no distractions.
@@ -0,0 +1,25 @@
/// Find the nearest thing which we assume is hostile and set it as the flee target
/datum/ai_planning_subtree/simple_find_nearest_target_to_flee
/datum/ai_planning_subtree/simple_find_nearest_target_to_flee/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
controller.queue_behavior(/datum/ai_behavior/find_potential_targets/nearest, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
/// Find the nearest thing on our list of 'things which have done damage to me' and set it as the flee target
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee
///the targeting strategy we use
var/targeting_key = BB_TARGETING_STRATEGY
///what key should we set the target as
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
. = ..()
if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING])
return
controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list/nearest, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/from_flee_key
target_key = BB_BASIC_MOB_FLEE_TARGET
targeting_key = BB_FLEE_TARGETING_STRATEGY
@@ -0,0 +1,80 @@
/// Sets the BB target to a mob which you can see and who has recently attacked you
/datum/ai_planning_subtree/target_retaliate
operational_datums = list(/datum/element/ai_retaliate, /datum/component/ai_retaliate_advanced)
/// Blackboard key which tells us how to select valid targets
var/targeting_strategy_key = BB_TARGETING_STRATEGY
/// Blackboard key in which to store selected target
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
/// Blackboard key in which to store selected target's hiding place
var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION
/// do we check for faction?
var/check_faction = FALSE
/datum/ai_planning_subtree/target_retaliate/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction)
/datum/ai_planning_subtree/target_retaliate/check_faction
check_faction = TRUE
/// Places a mob which you can see and who has recently attacked you into some 'run away from this' AI keys
/// Can use a different targeting strategy than you use to select attack targets
/// Not required if fleeing is the only target behaviour or uses the same target datum
/datum/ai_planning_subtree/target_retaliate/to_flee
targeting_strategy_key = BB_FLEE_TARGETING_STRATEGY
target_key = BB_BASIC_MOB_FLEE_TARGET
hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION
/**
* Picks a target from a provided list of atoms who have been pissing you off
* You will probably need /datum/element/ai_retaliate to take advantage of this unless you're populating the blackboard yourself
*/
/datum/ai_behavior/target_from_retaliate_list
action_cooldown = 2 SECONDS
/// How far can we see stuff?
var/vision_range = 9
/datum/ai_behavior/target_from_retaliate_list/perform(seconds_per_tick, datum/ai_controller/controller, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction)
var/mob/living/living_mob = controller.pawn
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
if(!targeting_strategy)
. = AI_BEHAVIOR_DELAY
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
var/list/shitlist = controller.blackboard[shitlist_key]
var/atom/existing_target = controller.blackboard[target_key]
if(!check_faction)
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
if(!QDELETED(existing_target) && (locate(existing_target) in shitlist) && targeting_strategy.can_attack(living_mob, existing_target, vision_range))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
var/list/enemies_list = list()
for(var/mob/living/potential_target as anything in shitlist)
if(!targeting_strategy.can_attack(living_mob, potential_target, vision_range))
continue
enemies_list += potential_target
if(!length(enemies_list))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/atom/new_target = pick_final_target(controller, enemies_list)
controller.set_blackboard_key(target_key, new_target)
var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, new_target)
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
controller.set_blackboard_key(hiding_location_key, potential_hiding_location)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/// 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)
/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)
. = ..()
if(succeeded || check_faction)
return
var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction)