mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 05:00:55 +01:00
Replaced our NPC AI with Behavior Trees. (#96628)
This PR replaces our current NPC AI with a [behavior tree system](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)). Behavior trees are a common way of creating AI in which you place nodes in a tree structure to define what actions an AI should take. AI controllers defined a list of /datum/ai_planning_subtree types in behavior_nodes. Each subtree was a self-contained unit that could call queue_behavior() to fire off /datum/ai_behavior actions. The controller iterated subtrees in order, each one deciding independently whether to queue something and deciding whether the next subtree would run. This has a few issues: 1. There's no real structure; you are just defining a list of things to try in order. 2. There was a loooot of subtrees that were basically the same as another but with some slight modification 3. It was hard to understand. Controllers now define a single json file describing a tree of nodes. The tree is composed of structural composites: Sequence - do A, then B, then C (and so on) Selector - try A, if it fails try B, then C (and so on) Parallel - run A and B simultaneously, with configurable failure/success policies and or looping behavior Subplan - loop a child continiously Along that we also have "Decorators". These are nodes that basically check a condition (E.g.; do we have a combat target). These decorators can be used to gate behavior and are re-useable across behavior trees. They also have a concept known as "Observers". Which lets them cancel lower priority behavior in case their condition changes (Which we check whenever a signal fires that fits that specific decorator). This makes the AI much more responsive to change in environment. For behaviors, we still use the ai_behavior datums. These are the actual behaviors such as "Move to X", "Attack X". The only major change is that these can no longer sleep() since they now run in the ai_controller. Lastly, we now also have subtrees, except now they are essentially pieces of behavior tree that can be re-used, or even overriden at runtime or as a variable. Allowing for making modular AI made out of several smaller trees. You can set variables on these nodes directly via the extension (see below), which should reduce the need to make subtypes of behaviors by a lot. All of these vars are saved on the JSON and will be applied at runtime. If you are using subtrees, you can also assign "bindings" to these variables, which will allow instances of the subtree to override those variables. Since a tree structure with variables becomes hard to parse in a JSON, I've made a VSCode extension to edit these JSONs: https://marketplace.visualstudio.com/items?itemName=BehaviorTreeG.behaviortreeg https://github.com/CabinetOnFire/BehaviorTreeG <img width="1795" height="1268" alt="image" src="https://github.com/user-attachments/assets/56aa2f0b-3cf9-449f-bca4-8281fca82db6" /> This extension allows you to edit the behavior tree JSONs, and browse through all the behaviors/decorators/subtrees we have If you'd like more info on how to build these AI check out the learn_ai.md. I will also make a tutorial to go over more depth on what the system offers because I kind of suck at doing technical write-ups. Targetting has been changed to. I've made a new acquire_targets behavior that takes a target_source (what am I targetting) and targetting_strategy (what does the candidate need to fulfill to be considered a target). This allows us to make composites targetting combinations to reduce the amount of specific find_and_set esque behaviors we had before. Not everything is ported to this system but that would be a longer term goal. I've added a new build_bt script that converts all the behavior tree JSONs into compiled versions. Why is this needed? Because I wanted to keep using defines in behavior trees, so we need a way to convert this into literal values before we send it to DM. This script runs on compile and should also run in CI (If I didn't fuck that up!). This saves to a new build/ folder. I've ported every single AI in the game to this system (except raptors, Kobsa is working on those so should be in soon!), so I do expect some bugs to come out of this. But I also fixed some issues that have probably been in the game for a long time such as: - Fixed penguins being unable to fish - Fixed bileworms not being able to devour people - Fixes goldgrubs not grubbing gold (they could not mine!) - Lizards actually eat food they find Either way, I'd reccomend a long TM on this. 1. (Hopefully) a better development experience for making AI 2. Less copy-paste for behaviors, we should be able to re-use more pieces to make behavior 3. Behavior trees is a more common pattern in making AI, so it should be easier to find resources to find out how to do things. 🆑 CabinetOnFire, Iamgoofball, SmartKar, Ben10omintrix refactor: Replaces our AI system with behavior trees, porting all datum/ai to it /🆑 I will add this PR with more details down the line. I think I got the big picture but its a big PR, so sorry if I missed something important. --------- Co-authored-by: Iamgoofball <iamgoofball@gmail.com> Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
This commit is contained in:
committed by
The Sharkenning
co-authored by
Iamgoofball
SmArtKar
Ghom
Ben10Omintrix
SyncIt21
parent
ad0d6a3e7d
commit
df7832aa43
@@ -1,5 +1,6 @@
|
||||
/datum/ai_controller/basic_controller
|
||||
movement_delay = 0.4 SECONDS
|
||||
behavior_tree_json = ABSTRACT_AI_CLASS
|
||||
|
||||
/datum/ai_controller/basic_controller/TryPossessPawn(atom/new_pawn)
|
||||
if(!isliving(new_pawn))
|
||||
@@ -62,9 +63,9 @@
|
||||
/datum/ai_controller/proc/on_tamed(datum/source, mob/living/new_friend)
|
||||
SIGNAL_HANDLER
|
||||
forgive_target(new_friend)
|
||||
clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
|
||||
clear_blackboard_key(BB_CURRENT_TARGET)
|
||||
clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST) //we have just been tamed by a new party, clean slate for everyone!
|
||||
CancelActions()
|
||||
cancel_current_plan()
|
||||
RegisterSignal(new_friend, COMSIG_LIVING_MADE_NEW_FRIEND, PROC_REF(on_master_tame))
|
||||
|
||||
/datum/ai_controller/proc/on_untamed(datum/source, mob/living/old_friend)
|
||||
@@ -77,7 +78,7 @@
|
||||
|
||||
/datum/ai_controller/proc/forgive_target(atom/target)
|
||||
var/static/list/keys_to_check = list(
|
||||
BB_BASIC_MOB_CURRENT_TARGET,
|
||||
BB_CURRENT_TARGET,
|
||||
BB_CURRENT_PET_TARGET,
|
||||
)
|
||||
for(var/key in keys_to_check)
|
||||
|
||||
@@ -1,144 +1,121 @@
|
||||
/// Amount of time to wait before executing attack if not specified
|
||||
#define DEFAULT_ATTACK_DELAY (0.4 SECONDS)
|
||||
|
||||
/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_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
///do we finish this action after hitting once?
|
||||
var/terminate_after_action = FALSE
|
||||
///do we have any alternate movement behavior?
|
||||
var/movement_behavior
|
||||
/// Perform a melee attack on the target specified.
|
||||
/datum/bt_node/ai_behavior/basic_melee_attack
|
||||
var/target_key
|
||||
var/targeting_strategy = BB_TARGETING_STRATEGY
|
||||
var/hiding_location_key
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
|
||||
/datum/bt_node/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller)
|
||||
. = ..()
|
||||
if(!controller.blackboard[targeting_strategy_key])
|
||||
|
||||
|
||||
if(!ispath(targeting_strategy))
|
||||
targeting_strategy = controller.blackboard[targeting_strategy]
|
||||
|
||||
if(!targeting_strategy)
|
||||
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, movement_behavior)
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
|
||||
/datum/bt_node/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if (isnull(target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
if (!can_attack(controller, target))
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
|
||||
if (isliving(controller.pawn))
|
||||
var/mob/living/pawn = controller.pawn
|
||||
if (world.time < pawn.next_move)
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
|
||||
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
|
||||
if(!targeting_strategy.can_attack(controller.pawn, target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/hiding_target = targeting_strategy.find_hidden_mobs(controller.pawn, 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, combat_mode = TRUE)
|
||||
if(terminate_after_action)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/proc/can_attack(datum/ai_controller/controller, atom/target)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if (!target.IsReachableBy(controller.pawn))
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
|
||||
return FALSE
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/can_attack_time = controller.blackboard[BB_BASIC_MOB_MELEE_COOLDOWN_TIMER]
|
||||
if (isnull(can_attack_time))
|
||||
var/blackboard_delay = controller.blackboard[BB_BASIC_MOB_MELEE_DELAY]
|
||||
var/attack_delay = isnull(blackboard_delay) ? DEFAULT_ATTACK_DELAY : blackboard_delay
|
||||
controller.set_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER, world.time + attack_delay)
|
||||
return FALSE
|
||||
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if (can_attack_time > world.time)
|
||||
return FALSE
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
return TRUE
|
||||
if (isliving(controller.pawn))
|
||||
var/mob/living/pawn = controller.pawn
|
||||
if (world.time < pawn.next_move)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_MELEE_COOLDOWN_TIMER)
|
||||
if(movement_behavior)
|
||||
controller.change_ai_movement_type(initial(controller.ai_movement))
|
||||
if(!succeeded)
|
||||
var/datum/targeting_strategy/strategy = GET_TARGETING_STRATEGY(targeting_strategy)
|
||||
if(!strategy.is_valid_target(controller.pawn, target, controller = controller))
|
||||
controller.clear_blackboard_key(target_key)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/interact_once
|
||||
terminate_after_action = TRUE
|
||||
var/hiding_target = strategy.find_hidden_mobs(controller.pawn, target) //If this is valid, theyre hidden in something!
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
|
||||
controller.set_blackboard_key(hiding_location_key, hiding_target)
|
||||
|
||||
var/atom/final_target = hiding_target || target
|
||||
INVOKE_ASYNC(controller, TYPE_PROC_REF(/datum/ai_controller, ai_interact), final_target, TRUE)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/// Single-hit variant: terminates after one successful attack and always clears the target key.
|
||||
/datum/bt_node/ai_behavior/basic_melee_attack/interact_once
|
||||
|
||||
/datum/bt_node/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
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?
|
||||
//Basic ranged attack behavior
|
||||
/datum/bt_node/ai_behavior/basic_ranged_attack
|
||||
var/target_key
|
||||
var/targeting_strategy = BB_TARGETING_STRATEGY
|
||||
var/hiding_location_key
|
||||
time_between_perform = 0.6 SECONDS
|
||||
/// Max range at which we can fire. Make sure your movement actually gets you this close please
|
||||
var/max_range = 9
|
||||
/// Avoid shooting through friendlies.
|
||||
var/avoid_friendly_fire = FALSE
|
||||
|
||||
/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
|
||||
/datum/bt_node/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller)
|
||||
. = ..()
|
||||
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)
|
||||
return TRUE
|
||||
|
||||
/datum/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
|
||||
/datum/bt_node/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
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))
|
||||
if(!ispath(targeting_strategy))
|
||||
targeting_strategy = controller.blackboard[targeting_strategy]
|
||||
|
||||
var/datum/targeting_strategy/strategy = GET_TARGETING_STRATEGY(targeting_strategy)
|
||||
|
||||
var/atom/hiding_target = strategy.find_hidden_mobs(basic_mob, target)
|
||||
var/atom/final_target = hiding_target ? hiding_target : target
|
||||
controller.set_blackboard_key(hiding_location_key, hiding_target)
|
||||
|
||||
if(!can_see(basic_mob, final_target, max_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))
|
||||
if(avoid_friendly_fire && check_friendly_in_path(basic_mob, target, strategy))
|
||||
adjust_position(basic_mob, target)
|
||||
return AI_BEHAVIOR_DELAY
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
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
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/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)
|
||||
/datum/bt_node/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))
|
||||
if(!targeting_strategy.is_valid_target(source, potential_friend))
|
||||
return TRUE
|
||||
|
||||
return FALSE
|
||||
|
||||
/datum/ai_behavior/basic_ranged_attack/proc/adjust_position(mob/living/living_pawn, atom/target)
|
||||
/datum/bt_node/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))
|
||||
@@ -146,13 +123,12 @@
|
||||
if(target_turf.is_blocked_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)
|
||||
/datum/bt_node/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)
|
||||
@@ -161,17 +137,14 @@
|
||||
var/direction_to_turf = get_dir(current_turf, next_turf)
|
||||
if(!ISDIAGONALDIR(direction_to_turf))
|
||||
continue
|
||||
|
||||
for(var/cardinal_direction in GLOB.cardinals)
|
||||
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
|
||||
/datum/bt_node/ai_behavior/basic_ranged_attack/avoid_friendly_fire
|
||||
avoid_friendly_fire = TRUE
|
||||
|
||||
#undef DEFAULT_ATTACK_DELAY
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
///behavior to befriend any targets
|
||||
/datum/ai_behavior/befriend_target
|
||||
/datum/bt_node/ai_behavior/befriend_target
|
||||
var/target_key
|
||||
var/befriend_message
|
||||
var/long_range_friendship = FALSE
|
||||
var/forget_target = TRUE
|
||||
|
||||
/datum/ai_behavior/befriend_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, befriend_message)
|
||||
/datum/bt_node/ai_behavior/befriend_target/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/mob/living/living_target = controller.blackboard[target_key]
|
||||
if(QDELETED(living_target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
if(!long_range_friendship && get_dist(living_pawn, living_target) > 1)
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
living_pawn.befriend(living_target)
|
||||
var/befriend_text = controller.blackboard[befriend_message]
|
||||
if(befriend_text)
|
||||
to_chat(living_target, span_nicegreen("[living_pawn] [befriend_text]"))
|
||||
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/befriend_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
/datum/bt_node/ai_behavior/befriend_target/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
if(forget_target)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/// Emotes a reinforcement call and alerts nearby faction members, adding the current target to their retaliate lists.
|
||||
/// Returns FAILURE when there is no valid target or the target is a friend.
|
||||
/datum/bt_node/ai_behavior/call_reinforcements
|
||||
/// How far to look for reinforcements
|
||||
var/reinforcements_range = 15
|
||||
///Target to call reinforcements on
|
||||
var/target_key = BB_CURRENT_TARGET
|
||||
|
||||
/datum/bt_node/ai_behavior/call_reinforcements/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/target = controller.blackboard[target_key]
|
||||
if(!istype(target, /mob))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
var/mob/pawn_mob = controller.pawn
|
||||
var/list/friends = controller.blackboard[BB_FRIENDS_LIST]
|
||||
if(friends && (target in friends))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/say_text = controller.blackboard[BB_REINFORCEMENTS_SAY]
|
||||
if(!isnull(say_text))
|
||||
pawn_mob.say(say_text, forced = "AI Controller")
|
||||
else
|
||||
var/emote_text = controller.blackboard[BB_REINFORCEMENTS_EMOTE]
|
||||
if(!isnull(emote_text))
|
||||
pawn_mob.manual_emote(emote_text)
|
||||
|
||||
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
|
||||
other_mob.ai_controller.set_blackboard_key_assoc_lazylist(BB_BASIC_MOB_RETALIATE_LIST, target, world.time)
|
||||
other_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_REINFORCEMENT_TARGET, pawn_mob)
|
||||
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/// Mining/swarm variant: boosts priority rather than forcing retaliation, shorter range and faster cooldown.
|
||||
/datum/bt_node/ai_behavior/call_reinforcements/mining
|
||||
reinforcements_range = 7
|
||||
|
||||
/datum/bt_node/ai_behavior/call_reinforcements/mining/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/target = controller.blackboard[target_key]
|
||||
if(!istype(target, /mob))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
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))
|
||||
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)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -1,14 +0,0 @@
|
||||
/// Clears a blackboard key (or keys), simply if you want to do this after an action without making a subtype
|
||||
/datum/ai_behavior/clear_key
|
||||
|
||||
/datum/ai_behavior/clear_key/perform(seconds_per_tick, datum/ai_controller/controller, list/to_clear)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/clear_key/finish_action(datum/ai_controller/controller, succeeded, list/to_clear)
|
||||
. = ..()
|
||||
if (!to_clear)
|
||||
return
|
||||
if (!islist(to_clear))
|
||||
to_clear = list(to_clear)
|
||||
for (var/key in to_clear)
|
||||
controller.clear_blackboard_key(key)
|
||||
@@ -1,35 +0,0 @@
|
||||
/datum/ai_behavior/find_and_set/valid_tree
|
||||
|
||||
/datum/ai_behavior/find_and_set/valid_tree/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
|
||||
var/list/valid_trees = list()
|
||||
for (var/obj/structure/flora/tree/tree_target in oview(search_range, controller.pawn))
|
||||
if(istype(tree_target, /obj/structure/flora/tree/dead)) //no died trees
|
||||
continue
|
||||
valid_trees += tree_target
|
||||
|
||||
if(valid_trees.len)
|
||||
return pick(valid_trees)
|
||||
|
||||
/datum/ai_behavior/climb_tree
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
|
||||
|
||||
/datum/ai_behavior/climb_tree/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/climb_tree/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/obj/structure/flora/target_tree = controller.blackboard[target_key]
|
||||
var/mob/living/basic/living_pawn = controller.pawn
|
||||
if(QDELETED(living_pawn)) // pawn can be null at this point
|
||||
return
|
||||
SEND_SIGNAL(living_pawn, COMSIG_LIVING_CLIMB_TREE, target_tree)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/climb_tree/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
if(succeeded)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/consider_venting",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/is_in_vent",
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/exit_vent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_BOTH",
|
||||
"key": "BB_ENTRY_VENT_TARGET"
|
||||
},
|
||||
"child": {
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/cooldown",
|
||||
"vars": {
|
||||
"cooldown_key": "BB_VENTING_COOLDOWN",
|
||||
"cooldown_duration": "BB_VENTCRAWL_COOLDOWN"
|
||||
},
|
||||
"child": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_ENTRY_VENT_TARGET"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/enter_vent"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/datum/ai_behavior/emote_on_target
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
|
||||
|
||||
|
||||
/datum/ai_behavior/emote_on_target/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/hunt_target = controller.blackboard[target_key]
|
||||
if (isnull(hunt_target))
|
||||
return FALSE
|
||||
set_movement_target(controller, hunt_target)
|
||||
|
||||
|
||||
/datum/ai_behavior/emote_on_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, list/emote_list)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(!length(emote_list) || isnull(target))
|
||||
return AI_BEHAVIOR_FAILED | AI_BEHAVIOR_DELAY
|
||||
run_emote(controller.pawn, target, emote_list)
|
||||
return AI_BEHAVIOR_SUCCEEDED | AI_BEHAVIOR_DELAY
|
||||
|
||||
|
||||
/datum/ai_behavior/emote_on_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
if(succeeded)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
|
||||
/datum/ai_behavior/emote_on_target/proc/run_emote(mob/living/living_pawn, atom/target, list/emote_list)
|
||||
living_pawn.manual_emote("[pick(emote_list)] [target]")
|
||||
@@ -0,0 +1,37 @@
|
||||
/// Finds the best adjacent turf to flee to away from a threat and stores it in a blackboard key.
|
||||
/// Tries get_step_away first, then falls back to shuffled directions if blocked.
|
||||
/// Returns INSTANT SUCCESS if a step was found, INSTANT FAILURE if completely cornered.
|
||||
/datum/bt_node/ai_behavior/find_flee_location
|
||||
var/target_key
|
||||
var/hiding_location_key
|
||||
var/destination_key
|
||||
|
||||
/datum/bt_node/ai_behavior/find_flee_location/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/run_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE
|
||||
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_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/turf/flee_turf = get_flee_step(controller, target)
|
||||
if(!flee_turf)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
controller.set_blackboard_key(destination_key, flee_turf)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/bt_node/ai_behavior/find_flee_location/proc/get_flee_step(datum/ai_controller/controller, atom/target)
|
||||
var/mob/living/pawn = controller.pawn
|
||||
var/turf/pawn_turf = get_turf(pawn)
|
||||
var/datum/can_pass_info/pass_info = new(pawn, controller.get_access())
|
||||
var/turf/next_step = get_step_away(pawn, target)
|
||||
if(!isnull(next_step) && next_step != pawn_turf && !next_step.density && !pawn_turf.LinkBlockedWithAccess(next_step, pass_info))
|
||||
return next_step
|
||||
var/list/all_dirs = GLOB.alldirs.Copy()
|
||||
all_dirs -= get_dir(pawn, next_step)
|
||||
all_dirs -= get_dir(pawn, target)
|
||||
shuffle_inplace(all_dirs)
|
||||
for(var/dir in all_dirs)
|
||||
next_step = get_step(pawn, dir)
|
||||
if(!isnull(next_step) && !next_step.density && !pawn_turf.LinkBlockedWithAccess(next_step, pass_info))
|
||||
return next_step
|
||||
return null
|
||||
@@ -1,35 +1,51 @@
|
||||
/datum/ai_behavior/find_mom
|
||||
///range to look for the mom
|
||||
/// Looks around for a nearby adult of one of BB_FIND_MOM_TYPES (skipping BB_IGNORE_MOM_TYPES) and stores it.
|
||||
/datum/bt_node/ai_behavior/find_mom
|
||||
time_between_perform = 2 SECONDS
|
||||
/// How far to look for our parent.
|
||||
var/look_range = 7
|
||||
/// Blackboard key holding the list of typepaths we accept as parents.
|
||||
var/mom_types_key = BB_FIND_MOM_TYPES
|
||||
/// Blackboard key holding typepaths to skip even if they match (e.g. other babies).
|
||||
var/ignore_types_key = BB_IGNORE_MOM_TYPES
|
||||
/// Blackboard key to store the found parent in.
|
||||
var/found_mom_key = BB_FOUND_MOM
|
||||
|
||||
/datum/ai_behavior/find_mom/perform(seconds_per_tick, datum/ai_controller/controller, mom_key, ignore_mom_key, found_mom)
|
||||
/datum/bt_node/ai_behavior/find_mom/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living_pawn = controller.pawn
|
||||
var/list/all_moms = list()
|
||||
var/list/mom_types = controller.blackboard[mom_key]
|
||||
var/list/ignore_types = controller.blackboard[ignore_mom_key]
|
||||
|
||||
var/list/mom_types = controller.blackboard[mom_types_key]
|
||||
if(!length(mom_types))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/list/ignore_types = controller.blackboard[ignore_types_key]
|
||||
var/list/all_moms = list()
|
||||
for(var/mob/mother in oview(look_range, living_pawn))
|
||||
if (is_possible_mom(mother, mom_types, ignore_types))
|
||||
if(is_possible_mom(mother, mom_types, ignore_types))
|
||||
all_moms += mother
|
||||
|
||||
if(length(all_moms))
|
||||
controller.set_blackboard_key(found_mom, pick(all_moms))
|
||||
controller.set_blackboard_key(found_mom_key, pick(all_moms))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
/datum/ai_behavior/find_mom/proc/is_possible_mom(mob/mother, list/mom_types, list/ignore_types)
|
||||
/datum/bt_node/ai_behavior/find_mom/proc/is_possible_mom(mob/mother, list/mom_types, list/ignore_types)
|
||||
if(!is_type_in_list(mother, mom_types))
|
||||
return FALSE
|
||||
if(is_type_in_list(mother, ignore_types)) // so the not permanent baby and the permanent baby subtype dont followed each other
|
||||
if(is_type_in_list(mother, ignore_types))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/ai_behavior/find_mom/raptor/is_possible_mom(mob/mother, list/mom_types, list/ignore_types)
|
||||
. = ..()
|
||||
if (!. || !istype(mother, /mob/living/basic/raptor))
|
||||
return FALSE
|
||||
var/mob/living/basic/raptor/raptor = mother
|
||||
return raptor.growth_stage == RAPTOR_ADULT
|
||||
/// A baby emotes at its parent: crying if the parent is dead, dancing happily otherwise.
|
||||
/datum/bt_node/ai_behavior/look_to_parent
|
||||
/// Blackboard key holding the parent to react to.
|
||||
var/parent_key = BB_FOUND_MOM
|
||||
|
||||
/datum/bt_node/ai_behavior/look_to_parent/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/parent = controller.blackboard[parent_key]
|
||||
if(QDELETED(parent))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
var/mob/living/baby = controller.pawn
|
||||
if(parent.stat == DEAD)
|
||||
baby.manual_emote("cries for their parent!")
|
||||
else
|
||||
baby.manual_emote("dances around their parent!")
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
///behavior for general interactions with any targets
|
||||
/datum/ai_behavior/interact_with_target
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
|
||||
///should we be clearing the target after the fact?
|
||||
var/clear_target = TRUE
|
||||
///should our combat mode be off during interaction?
|
||||
var/combat_mode = TRUE
|
||||
|
||||
/datum/ai_behavior/interact_with_target/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/interact_with_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target) || !pre_interact(controller, target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
controller.ai_interact(target, combat_mode)
|
||||
return AI_BEHAVIOR_SUCCEEDED | AI_BEHAVIOR_DELAY
|
||||
|
||||
/datum/ai_behavior/interact_with_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
if(clear_target || !succeeded)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
/datum/ai_behavior/interact_with_target/proc/pre_interact(datum/ai_controller/controller, target)
|
||||
return TRUE
|
||||
@@ -1,13 +1,6 @@
|
||||
/// 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
|
||||
/// Pick nearest instead of any, we should probably move this into a datum of some kind in the future?
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/nearest
|
||||
|
||||
/datum/ai_behavior/find_potential_targets/nearest/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_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)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* Simple behaviour for picking up an item we are already in range of.
|
||||
* The blackboard storage key isn't very safe because it doesn't make sense to register signals in here.
|
||||
* Use the AI held item component to manage this.
|
||||
*/
|
||||
/datum/ai_behavior/pick_up_item
|
||||
|
||||
/datum/ai_behavior/pick_up_item/setup(datum/ai_controller/controller, target_key, storage_key)
|
||||
. = ..()
|
||||
var/obj/item/target = controller.blackboard[target_key]
|
||||
return isitem(target) && isturf(target.loc) && !target.anchored
|
||||
|
||||
/datum/ai_behavior/pick_up_item/perform(seconds_per_tick, datum/ai_controller/controller, target_key, storage_key)
|
||||
var/obj/item/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target) || !isturf(target.loc)) // Someone picked it up or it got deleted
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
if(!controller.pawn.Adjacent(target)) // It teleported
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
pickup_item(controller, target, storage_key)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/pick_up_item/finish_action(datum/ai_controller/controller, success, target_key, storage_key)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
/datum/ai_behavior/pick_up_item/proc/pickup_item(datum/ai_controller/controller, obj/item/target, storage_key)
|
||||
var/atom/pawn = controller.pawn
|
||||
drop_existing_item(controller, storage_key)
|
||||
pawn.visible_message(span_notice("[pawn] picks up [target]."))
|
||||
target.forceMove(pawn)
|
||||
controller.set_blackboard_key(storage_key, target)
|
||||
return TRUE
|
||||
|
||||
/datum/ai_behavior/pick_up_item/proc/drop_existing_item(datum/ai_controller/controller, storage_key)
|
||||
var/obj/item/carried_item = controller.blackboard[storage_key]
|
||||
if(!carried_item)
|
||||
return
|
||||
controller.clear_blackboard_key(storage_key)
|
||||
var/atom/pawn = controller.pawn
|
||||
if(carried_item.loc != pawn)
|
||||
return
|
||||
pawn.visible_message(span_notice("[pawn] drops [carried_item]."))
|
||||
carried_item.forceMove(get_turf(pawn))
|
||||
return TRUE
|
||||
@@ -0,0 +1,25 @@
|
||||
/// Plays dead until a per-tick probability check (default 10%) triggers revival.
|
||||
/datum/bt_node/ai_behavior/play_dead
|
||||
var/probability = 10
|
||||
|
||||
/datum/bt_node/ai_behavior/play_dead/setup(datum/ai_controller/controller)
|
||||
var/mob/living/basic/pawn = controller.pawn
|
||||
if(!istype(pawn) || pawn.stat)
|
||||
return FALSE
|
||||
INVOKE_ASYNC(pawn, TYPE_PROC_REF(/mob, emote), "deathgasp", intentional = FALSE)
|
||||
ADD_TRAIT(pawn, TRAIT_FAKEDEATH, BASIC_MOB_DEATH_TRAIT)
|
||||
pawn.look_dead()
|
||||
|
||||
/datum/bt_node/ai_behavior/play_dead/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
if(SPT_PROB(probability, seconds_per_tick))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
/datum/bt_node/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
var/mob/living/basic/pawn = controller.pawn
|
||||
if(QDELETED(pawn) || pawn.stat)
|
||||
return
|
||||
pawn.visible_message(span_notice("[pawn] miraculously springs back to life!"))
|
||||
REMOVE_TRAIT(pawn, TRAIT_FAKEDEATH, BASIC_MOB_DEATH_TRAIT)
|
||||
pawn.look_alive()
|
||||
@@ -1,22 +0,0 @@
|
||||
/datum/ai_behavior/pull_target
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
|
||||
|
||||
/datum/ai_behavior/pull_target/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/pull_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/atom/movable/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target) || target.anchored || target.pulledby)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
var/mob/living/our_mob = controller.pawn
|
||||
our_mob.start_pulling(target)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/pull_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
if(!succeeded)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
@@ -1,76 +0,0 @@
|
||||
/// 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
|
||||
var/list/airlocks = SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/door/airlock)
|
||||
for(var/i in 1 to run_distance)
|
||||
var/turf/test_destination = get_ranged_target_turf_direct(source, target, range = i, offset = angle)
|
||||
if(test_destination.is_blocked_turf(source_atom = source, ignore_atoms = airlocks))
|
||||
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 ..()
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/datum/ai_behavior/set_travel_destination
|
||||
|
||||
/datum/ai_behavior/set_travel_destination/perform(seconds_per_tick, datum/ai_controller/controller, target_key, location_key)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
|
||||
if(QDELETED(target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
controller.set_blackboard_key(location_key, target)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* # Step towards turf
|
||||
* Moves a short distance towards a location repeatedly until you arrive at the destination.
|
||||
* You'd use this over travel_towards if you're travelling a long distance over a long time, because the AI controller has a maximum range.
|
||||
*/
|
||||
/datum/ai_behavior/step_towards_turf
|
||||
required_distance = 0
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
/// How far ahead do we plot movement per action? Further means longer until we return to the decision tree, fewer means jerkier movement
|
||||
/// This can still result in long moves because this is "a tile x tiles away" not "only move x tiles", you might path around some walls
|
||||
var/step_distance = 3
|
||||
|
||||
/datum/ai_behavior/step_towards_turf/setup(datum/ai_controller/controller, turf_key)
|
||||
var/turf/target_turf = controller.blackboard[turf_key]
|
||||
if (QDELETED(target_turf) || target_turf.is_blocked_turf(exclude_mobs = TRUE))
|
||||
target_turf = find_destination_turf(args)
|
||||
if (!target_turf)
|
||||
return FALSE
|
||||
controller.set_blackboard_key(turf_key, target_turf)
|
||||
|
||||
if (target_turf.z != controller.pawn.z)
|
||||
return FALSE
|
||||
|
||||
var/turf/destination = plot_movement(controller, target_turf)
|
||||
if (!destination)
|
||||
return FALSE
|
||||
set_movement_target(controller, destination)
|
||||
return ..()
|
||||
|
||||
/**
|
||||
* Get a turf to aim towards if we don't already have one, the default behaviour is actually to not do this but we want to extend it
|
||||
* Gets passed all of the arguments from `setup`
|
||||
*/
|
||||
/datum/ai_behavior/step_towards_turf/proc/find_destination_turf()
|
||||
return null
|
||||
|
||||
/**
|
||||
* Figure out where we're going to move to, which isn't all the way to the destination in one go
|
||||
*/
|
||||
/datum/ai_behavior/step_towards_turf/proc/plot_movement(datum/ai_controller/controller, turf/target_turf)
|
||||
var/distance_to_destination = get_dist(controller.pawn, target_turf)
|
||||
if (distance_to_destination <= step_distance)
|
||||
return target_turf
|
||||
|
||||
var/direction_to_destination = get_dir(controller.pawn, target_turf)
|
||||
return get_ranged_target_turf(controller.pawn, direction_to_destination, step_distance)
|
||||
|
||||
// We actually only wanted the movement so if we've arrived we're done
|
||||
/datum/ai_behavior/step_towards_turf/perform(seconds_per_tick, datum/ai_controller/controller, area_key, turf_key)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/**
|
||||
* # Step towards turf in area
|
||||
* Moves a short distance towards a location in an area
|
||||
* Unlike step_towards_turf it will reacquire a new turf from the area if it loses its target
|
||||
*/
|
||||
/datum/ai_behavior/step_towards_turf/in_area
|
||||
|
||||
/datum/ai_behavior/step_towards_turf/in_area/setup(datum/ai_controller/controller, turf_key, area_key)
|
||||
var/area/target_area = controller.blackboard[area_key]
|
||||
if (!target_area)
|
||||
return FALSE
|
||||
|
||||
return ..()
|
||||
|
||||
// Return the first valid turf in the area to replace a lost target
|
||||
/datum/ai_behavior/step_towards_turf/in_area/find_destination_turf(datum/ai_controller/controller, turf_key, area_key)
|
||||
var/area/target_area = controller.blackboard[area_key]
|
||||
var/list/target_area_turfs = get_area_turfs(target_area.type)
|
||||
for (var/turf/potential_target as anything in target_area_turfs)
|
||||
if (potential_target.is_blocked_turf(exclude_mobs = TRUE))
|
||||
continue
|
||||
return potential_target
|
||||
return null
|
||||
@@ -1,27 +1,29 @@
|
||||
/// Makes a mob simply stop and stare at a movable... yea...
|
||||
/datum/ai_behavior/stop_and_stare
|
||||
behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM
|
||||
/// Faces a nearby scary atom and holds still for a while.
|
||||
/datum/bt_node/ai_behavior/stop_and_stare
|
||||
/// Blackboard key holding the atom we're staring at.
|
||||
var/target_key = BB_STATIONARY_CAUSE
|
||||
/// Blackboard key holding how long (in deciseconds) to stay frozen for.
|
||||
var/stare_duration_key = BB_STATIONARY_SECONDS
|
||||
|
||||
/datum/ai_behavior/stop_and_stare/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
/datum/bt_node/ai_behavior/stop_and_stare/setup(datum/ai_controller/controller)
|
||||
var/atom/movable/target = controller.blackboard[target_key]
|
||||
return ismovable(target) && isturf(target.loc) && ismob(controller.pawn)
|
||||
|
||||
/datum/ai_behavior/stop_and_stare/get_cooldown(datum/ai_controller/cooldown_for)
|
||||
return cooldown_for.blackboard[BB_STATIONARY_COOLDOWN]
|
||||
/datum/bt_node/ai_behavior/stop_and_stare/get_cooldown(datum/ai_controller/cooldown_for)
|
||||
return cooldown_for.blackboard[stare_duration_key] || ..()
|
||||
|
||||
/datum/ai_behavior/stop_and_stare/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
/datum/bt_node/ai_behavior/stop_and_stare/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/atom/movable/target = controller.blackboard[target_key]
|
||||
if(!ismovable(target) || !isturf(target.loc)) // just to make sure that nothing funky happened between setup and perform
|
||||
return AI_BEHAVIOR_DELAY
|
||||
if(!ismovable(target) || !isturf(target.loc))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/mob/pawn_mob = controller.pawn
|
||||
var/turf/pawn_turf = get_turf(pawn_mob)
|
||||
|
||||
pawn_mob.face_atom(target)
|
||||
pawn_mob.balloon_alert_to_viewers("stops and stares...")
|
||||
set_movement_target(controller, pawn_turf, /datum/ai_movement/complete_stop)
|
||||
// Returning a long cooldown keeps this leaf RUNNING (and thus the mob standing still) for the stare.
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
if(controller.blackboard[BB_STATIONARY_MOVE_TO_TARGET])
|
||||
addtimer(CALLBACK(src, PROC_REF(set_movement_target), controller, target, initial(controller.ai_movement)), (controller.blackboard[BB_STATIONARY_SECONDS] + 1 SECONDS))
|
||||
return AI_BEHAVIOR_DELAY
|
||||
/datum/bt_node/ai_behavior/stop_and_stare/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
// Forget the cause so we can be spooked fresh next time it wanders into view.
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
/**
|
||||
* # Targeted Mob Ability
|
||||
* Attempts to use a mob's cooldown ability on a target
|
||||
*/
|
||||
/datum/ai_behavior/targeted_mob_ability
|
||||
/// Tries to use a specified ability on the current target
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability
|
||||
var/ability_key = BB_GENERIC_ACTION
|
||||
var/target_key
|
||||
/// Maximum distance at which the ability can fire (inclusive cuz this is tg :) )
|
||||
var/maximum_distance = 0
|
||||
///Does this require adjacency?
|
||||
var/require_adjacency = FALSE
|
||||
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/async_flags = handle_async()
|
||||
if(async_flags)
|
||||
return async_flags
|
||||
|
||||
/datum/ai_behavior/targeted_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key)
|
||||
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
|
||||
var/mob/living/target = controller.blackboard[target_key]
|
||||
if(QDELETED(ability) || QDELETED(target))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
var/mob/pawn = controller.pawn
|
||||
pawn.face_atom(target)
|
||||
if(maximum_distance && get_dist(controller.pawn, target) > maximum_distance)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(require_adjacency && !controller.pawn.Adjacent(target))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(!ability.IsAvailable())
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/mob/pawn_mob = controller.pawn
|
||||
pawn_mob.face_atom(target)
|
||||
return start_async()
|
||||
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/perform_async(datum/ai_controller/controller)
|
||||
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
var/result = ability.Trigger(target = target)
|
||||
if(result)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(!async_still_valid())
|
||||
return
|
||||
finish_async(result ? AI_BEHAVIOR_SUCCEEDED : AI_BEHAVIOR_FAILED)
|
||||
|
||||
/**
|
||||
* # Try Mob Ability and plan execute
|
||||
* Attempts to use a mob's cooldown ability on a target and then move the target into a special target blackboard datum
|
||||
* Doesn't need another subtype to clear BB_BASIC_MOB_EXECUTION_TARGET because it will be the target key for the normal action
|
||||
*/
|
||||
/datum/ai_behavior/targeted_mob_ability/and_plan_execute
|
||||
/// Variant for abilities that require adjacency (distance ≤ 1).
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/melee
|
||||
require_adjacency = TRUE
|
||||
|
||||
/datum/ai_behavior/targeted_mob_ability/and_plan_execute/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key)
|
||||
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/and_plan_execute
|
||||
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/and_plan_execute/finish_action(datum/ai_controller/controller, succeeded)
|
||||
controller.set_blackboard_key(BB_BASIC_MOB_EXECUTION_TARGET, controller.blackboard[target_key])
|
||||
return ..()
|
||||
|
||||
/**
|
||||
* # Try Mob Ability and clear target
|
||||
* Attempts to use a mob's cooldown ability on a target and releases the target when the action completes
|
||||
*/
|
||||
/datum/ai_behavior/targeted_mob_ability/and_clear_target
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/and_clear_target
|
||||
|
||||
/datum/ai_behavior/targeted_mob_ability/and_clear_target/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key)
|
||||
/datum/bt_node/ai_behavior/targeted_mob_ability/and_clear_target/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
/**
|
||||
* Attempts to move into the provided range and then use a mob's cooldown ability on a target
|
||||
*/
|
||||
/datum/ai_behavior/targeted_mob_ability/min_range
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
|
||||
required_distance = 6
|
||||
|
||||
/datum/ai_behavior/targeted_mob_ability/min_range/setup(datum/ai_controller/controller, ability_key, target_key)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/targeted_mob_ability/min_range/short
|
||||
required_distance = 3
|
||||
|
||||
@@ -5,101 +5,74 @@ GLOBAL_ALIST_EMPTY(hostile_machines_by_z)
|
||||
/// 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/vehicle/sealed/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
|
||||
|
||||
///Used to find combat targets; Allow finding things hidden in things such as lockers too.
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets
|
||||
target_source = /datum/target_source/hearers
|
||||
targeting_strategy = BB_TARGETING_STRATEGY
|
||||
vision_range = 9
|
||||
target_loss_distance = 16
|
||||
/// Blackboard key for aggro range, uses vision range if not specified
|
||||
var/aggro_range_key = BB_AGGRO_RANGE
|
||||
/// Range in which we can acquire a new target
|
||||
var/aggro_grab_range_key = BB_AGGRO_GRAB_RANGE
|
||||
/// Blackboard key for the target priority strategy
|
||||
/// Blackboard key holding the hiding-location atom (e.g. closet the target ducked into)
|
||||
var/hiding_location_key
|
||||
/// Blackboard key holding the /datum/target_priority_strategy typepath for selection
|
||||
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)])
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/get_cooldown(datum/ai_controller/controller)
|
||||
if(controller.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])
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/can_search(datum/ai_controller/controller)
|
||||
return !(controller.blackboard[BB_FIND_TARGETS_FIELD(type)])
|
||||
|
||||
if(!targeting_strategy)
|
||||
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
|
||||
|
||||
var/atom/current_target = controller.blackboard[target_key]
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/should_keep_target(datum/ai_controller/controller, datum/targeting_strategy/strategy, atom/current_target)
|
||||
if(!current_target)
|
||||
return FALSE
|
||||
if(!strategy.is_valid_target(controller.pawn, current_target, vision_range))
|
||||
return FALSE
|
||||
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
|
||||
if(!priority_strategy)
|
||||
return TRUE
|
||||
return controller.blackboard[BB_BASIC_MOB_TARGET_REFRESH_COOLDOWN] > world.time
|
||||
|
||||
var/aggro_range = vision_range
|
||||
if(isnull(current_target) && !isnull(controller.blackboard[aggro_grab_range_key]))
|
||||
aggro_range = controller.blackboard[aggro_grab_range_key]
|
||||
else if(!isnull(controller.blackboard[aggro_range_key]))
|
||||
aggro_range = controller.blackboard[aggro_range_key]
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/on_no_candidates(datum/ai_controller/controller, atom/current_target, datum/targeting_strategy/strategy, range)
|
||||
if(current_target && strategy.can_keep_target(controller.pawn, current_target, target_loss_distance))
|
||||
return list(current_target)
|
||||
if(!current_target)
|
||||
failed_to_find_anyone(controller, target_key, targeting_strategy, hiding_location_key)
|
||||
return list()
|
||||
|
||||
controller.clear_blackboard_key(target_key)
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/on_no_valid_candidates(datum/ai_controller/controller, atom/current_target)
|
||||
if(!current_target)
|
||||
failed_to_find_anyone(controller, target_key, targeting_strategy, hiding_location_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
|
||||
|
||||
var/turf/mob_turf = get_turf(living_mob)
|
||||
if(mob_turf?.z)
|
||||
for (var/atom/hostile_machine as anything in GLOB.hostile_machines_by_z[mob_turf.z])
|
||||
if (can_see(living_mob, hostile_machine, aggro_range))
|
||||
potential_targets += hostile_machine
|
||||
|
||||
if(!potential_targets.len)
|
||||
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))
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/filter_candidates(datum/ai_controller/controller, list/candidates, datum/targeting_strategy/strategy, atom/current_target)
|
||||
var/mob/living/pawn = controller.pawn
|
||||
var/datum/target_priority_strategy/priority_strategy = GET_TARGET_PRIORITY_STRATEGY(controller.blackboard[priority_strategy_key])
|
||||
var/current_priority = priority_strategy ? priority_strategy.get_target_priority(controller, current_target) : 0
|
||||
var/list/filtered = list()
|
||||
for(var/atom/candidate as anything in candidates)
|
||||
if(!strategy.is_valid_target(pawn, candidate, vision_range, controller))
|
||||
continue
|
||||
if (priority_strategy && priority_strategy.get_target_priority(controller, pot_target) < current_priority)
|
||||
if(priority_strategy && priority_strategy.get_target_priority(controller, candidate) < current_priority)
|
||||
continue
|
||||
filtered_targets += pot_target
|
||||
filtered += candidate
|
||||
return filtered
|
||||
|
||||
if(!filtered_targets.len)
|
||||
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)
|
||||
|
||||
EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [target] as a target for blackboard key [target_key]! Behavior: [src]", get_turf(target), "Target: [target]")
|
||||
EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(target))
|
||||
|
||||
controller.set_blackboard_key(target_key, target)
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/on_target_found(datum/ai_controller/controller, atom/target, datum/targeting_strategy/strategy)
|
||||
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)
|
||||
|
||||
if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially.
|
||||
var/atom/potential_hiding_location = strategy.find_hidden_mobs(controller.pawn, target)
|
||||
if(potential_hiding_location)
|
||||
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 = vision_range
|
||||
if(!isnull(controller.blackboard[aggro_grab_range_key]))
|
||||
aggro_range = controller.blackboard[aggro_grab_range_key]
|
||||
else if(!isnull(controller.blackboard[aggro_range_key]))
|
||||
aggro_range = controller.blackboard[aggro_range_key]
|
||||
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy, hiding_location_key)
|
||||
// TEMP DISABLED nuke this if performance improves
|
||||
/*
|
||||
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
|
||||
@@ -112,13 +85,16 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
|
||||
src,
|
||||
controller,
|
||||
target_key,
|
||||
targeting_strategy_key,
|
||||
targeting_strategy,
|
||||
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)
|
||||
*/
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
/datum/ai_behavior/find_potential_targets/proc/new_turf_found(turf/found, datum/ai_controller/controller, datum/targeting_strategy/strategy)
|
||||
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_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 in found)
|
||||
@@ -126,7 +102,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
|
||||
continue
|
||||
if(!is_type_in_typecache(maybe_target, GLOB.target_interested_atoms))
|
||||
continue
|
||||
if(!strategy.can_attack(pawn, maybe_target))
|
||||
if(!strategy.is_valid_target(pawn, maybe_target))
|
||||
continue
|
||||
valid_found = TRUE
|
||||
break
|
||||
@@ -136,18 +112,18 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
|
||||
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)
|
||||
modify_cooldown(world.time)
|
||||
|
||||
/datum/ai_behavior/find_potential_targets/proc/atom_allowed(atom/movable/checking, datum/targeting_strategy/strategy, mob/pawn)
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_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))
|
||||
if(!strategy.is_valid_target(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)
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_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 in found)
|
||||
@@ -156,7 +132,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
|
||||
// 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))
|
||||
if(!strategy.is_valid_target(pawn, maybe_target))
|
||||
continue
|
||||
accepted_targets += maybe_target
|
||||
|
||||
@@ -165,36 +141,46 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob, /obj/machinery/
|
||||
EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_TARGETING, "[controller.pawn] has selected [target] as a target for blackboard key [target_key]! Behavior: [src]", get_turf(target), "Target: [target]")
|
||||
EVLOG_LINES(controller, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(controller.pawn), get_turf(target))
|
||||
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)
|
||||
on_target_found(controller, target, strategy)
|
||||
|
||||
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)
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
if (succeeded)
|
||||
var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)]
|
||||
qdel(field) // autoclears so it's fine
|
||||
controller.CancelActions() // On retarget cancel any further queued actions so that they will setup again with new target
|
||||
controller.modify_cooldown(src, get_cooldown(controller))
|
||||
modify_cooldown(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)
|
||||
/// Picks the final target, preferring higher-priority candidates when a priority strategy is set.
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/pick_final_target(datum/ai_controller/controller, list/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)
|
||||
if(!priority_strategy)
|
||||
return filtered_targets[1]
|
||||
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
|
||||
/// Picks targets based on which one has the lowest health.
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/most_wounded
|
||||
|
||||
/datum/ai_behavior/find_potential_targets/prioritize_trait/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
|
||||
var/priority_targets = list()
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/most_wounded/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
|
||||
var/list/living_targets = list()
|
||||
for(var/mob/living/living_target in filtered_targets)
|
||||
living_targets += living_target
|
||||
if(living_targets.len)
|
||||
sortTim(living_targets, GLOBAL_PROC_REF(cmp_mob_health))
|
||||
return living_targets[living_targets.len]
|
||||
return ..()
|
||||
|
||||
/// Prioritizes targets carrying the trait named by our trait_key blackboard key over the rest.
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/prioritize_trait
|
||||
/// Blackboard key holding the trait that marks a target as high-priority.
|
||||
var/trait_key = BB_TARGET_PRIORITY_TRAIT
|
||||
|
||||
/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/prioritize_trait/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
|
||||
var/list/priority_targets = list()
|
||||
var/priority_trait = controller.blackboard[trait_key]
|
||||
for(var/atom/target as anything in filtered_targets)
|
||||
if(HAS_TRAIT(target, controller.blackboard[BB_TARGET_PRIORITY_TRAIT]))
|
||||
if(HAS_TRAIT(target, priority_trait))
|
||||
priority_targets += target
|
||||
if(length(priority_targets))
|
||||
return ..(controller, priority_targets)
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
///A tipped-over mob looks to a nearby person for help, then resigns itself to its fate.
|
||||
/datum/bt_node/ai_behavior/tipped_reaction
|
||||
/// Blackboard key holding the mob that tipped us over.
|
||||
var/tipper_key = BB_BASIC_MOB_TIPPER
|
||||
/// Blackboard key holding whether we are still reacting to being tipped.
|
||||
var/reacting_key = BB_BASIC_MOB_TIP_REACTING
|
||||
|
||||
///type of tipped reaction that is akin to puppy dog eyes
|
||||
/datum/ai_behavior/tipped_reaction
|
||||
|
||||
/datum/ai_behavior/tipped_reaction/perform(seconds_per_tick, datum/ai_controller/controller, tipper_key, reacting_key)
|
||||
/datum/bt_node/ai_behavior/tipped_reaction/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/carbon/tipper = controller.blackboard[tipper_key]
|
||||
|
||||
// visible part of the visible message
|
||||
var/seen_message = ""
|
||||
// self part of the visible message
|
||||
var/self_message = ""
|
||||
// the mob we're looking to for aid
|
||||
var/mob/living/carbon/savior
|
||||
// look for someone in a radius around us for help. If our original tipper is in range, prioritize them
|
||||
for(var/mob/living/carbon/potential_aid in oview(3, get_turf(controller.pawn)))
|
||||
@@ -18,6 +16,8 @@
|
||||
break
|
||||
savior = potential_aid
|
||||
|
||||
var/seen_message
|
||||
var/self_message
|
||||
if(prob(75) && savior)
|
||||
var/text = pick("imploringly", "pleadingly", "with a resigned expression")
|
||||
seen_message = "[controller.pawn] looks at [savior] [text]."
|
||||
@@ -28,7 +28,7 @@
|
||||
controller.pawn.visible_message(span_notice("[seen_message]"), span_notice("[self_message]"))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/tipped_reaction/finish_action(datum/ai_controller/controller, succeeded, tipper_key, reacting_key)
|
||||
/datum/bt_node/ai_behavior/tipped_reaction/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
//I'VE SAID MY PEACE...
|
||||
controller.set_blackboard_key(reacting_key, FALSE)
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* # Travel Towards
|
||||
* Moves towards the atom in the passed blackboard key.
|
||||
* Planning continues during this action so it can be interrupted by higher priority actions.
|
||||
*/
|
||||
/datum/ai_behavior/travel_towards
|
||||
required_distance = 0
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
/// If true we will get rid of our target on completion
|
||||
var/clear_target = FALSE
|
||||
///should we use a different movement type?
|
||||
var/new_movement_type
|
||||
|
||||
/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target, new_movement_type)
|
||||
|
||||
/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
if (clear_target)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
if(new_movement_type)
|
||||
controller.change_ai_movement_type(initial(controller.ai_movement))
|
||||
|
||||
/datum/ai_behavior/travel_towards/stop_on_arrival
|
||||
clear_target = TRUE
|
||||
|
||||
/datum/ai_behavior/travel_towards/adjacent
|
||||
required_distance = 1
|
||||
|
||||
/**
|
||||
* # Travel Towards Atom
|
||||
* Travel towards an atom you pass directly from the controller rather than a blackboard key.
|
||||
* You might need to do this to avoid repeating some checks in both a controller and an action.
|
||||
*/
|
||||
/datum/ai_behavior/travel_towards_atom
|
||||
required_distance = 0
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
|
||||
|
||||
/datum/ai_behavior/travel_towards_atom/setup(datum/ai_controller/controller, atom/target_atom)
|
||||
. = ..()
|
||||
if(isnull(target_atom))
|
||||
return FALSE
|
||||
set_movement_target(controller, target_atom)
|
||||
|
||||
/datum/ai_behavior/travel_towards_atom/perform(seconds_per_tick, datum/ai_controller/controller, atom/target_atom)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -1,11 +0,0 @@
|
||||
/datum/ai_behavior/unbuckle_mob
|
||||
|
||||
/datum/ai_behavior/unbuckle_mob/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/atom/movable/buckled_to = living_pawn.buckled
|
||||
|
||||
if(isnull(buckled_to))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
buckled_to.unbuckle_mob(living_pawn)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -0,0 +1,39 @@
|
||||
/// Triggers a mob ability stored in a blackboard key. Returns INSTANT SUCCESS if triggered, INSTANT FAILURE if unavailable or trigger fails.
|
||||
/datum/bt_node/ai_behavior/use_mob_ability
|
||||
var/ability_key = BB_GENERIC_ACTION
|
||||
|
||||
/datum/bt_node/ai_behavior/use_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/async_flags = handle_async()
|
||||
if(async_flags)
|
||||
return async_flags
|
||||
|
||||
var/datum/action/using_action = get_valid_ability(controller)
|
||||
if(QDELETED(using_action))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
return start_async()
|
||||
|
||||
/// Returns the action to trigger, or null if it isn't available. Override to prep the action before it fires.
|
||||
/datum/bt_node/ai_behavior/use_mob_ability/proc/get_valid_ability(datum/ai_controller/controller)
|
||||
var/datum/action/using_action = controller.blackboard[ability_key]
|
||||
if(QDELETED(using_action) || !using_action.IsAvailable())
|
||||
return null
|
||||
return using_action
|
||||
|
||||
/datum/bt_node/ai_behavior/use_mob_ability/perform_async(datum/ai_controller/controller)
|
||||
var/datum/action/using_action = controller.blackboard[ability_key]
|
||||
var/result = using_action.Trigger()
|
||||
if(!async_still_valid())
|
||||
return
|
||||
finish_async(result ? AI_BEHAVIOR_SUCCEEDED : AI_BEHAVIOR_FAILED)
|
||||
|
||||
/// Triggers a shapeshift ability, picking a random shape if none has been selected yet (AI can't use context wheels).
|
||||
/datum/bt_node/ai_behavior/use_mob_ability/shapeshift
|
||||
ability_key = BB_SHAPESHIFT_ACTION
|
||||
|
||||
/datum/bt_node/ai_behavior/use_mob_ability/shapeshift/get_valid_ability(datum/ai_controller/controller)
|
||||
var/datum/action/cooldown/spell/shapeshift/using_action = ..()
|
||||
if(QDELETED(using_action))
|
||||
return null
|
||||
if(isnull(using_action.shapeshift_type))
|
||||
using_action.shapeshift_type = pick(using_action.possible_shapes)
|
||||
return using_action
|
||||
@@ -1,143 +1,190 @@
|
||||
/// We hop into the vents through a vent outlet, and then crawl around a bit. Jolly good times.
|
||||
/// This also assumes that we are on the turf that the vent outlet is on. If it isn't, shit.
|
||||
///uhm...sus?
|
||||
/datum/bt_node/subtree/consider_venting
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_ai_behaviors/consider_venting.bt.json"
|
||||
|
||||
/// Warning: this was really snowflake code lifted from an obscure feature that likely has not been touched for over five years years.
|
||||
/// Something that isn't implemented is the ability to actually crawl through vents ourselves because I think that's just a waste of time for the same effect (instead of psuedo-teleportation, do REAL forceMoving)
|
||||
/// If you are seriously considering using this component, it would be a great idea to extend this proc to be more versatile/less overpowered - the mobs that currently implement this benefit the most
|
||||
/// since they are weak as shit with only five health. Up to you though, don't take what's written here as gospel.
|
||||
/datum/ai_behavior/crawl_through_vents
|
||||
action_cooldown = 10 SECONDS
|
||||
|
||||
/datum/ai_behavior/crawl_through_vents/get_cooldown(datum/ai_controller/cooldown_for)
|
||||
return cooldown_for.blackboard[BB_VENTCRAWL_COOLDOWN] || initial(action_cooldown)
|
||||
/// Enters a vent stored in entry_vent_key. Sets BB_EXIT_VENT_TARGET and BB_VENT_ENTRY_TIME on success.
|
||||
/datum/bt_node/ai_behavior/enter_vent
|
||||
var/entry_vent_key = BB_ENTRY_VENT_TARGET
|
||||
/// TRUE while the async crawl-in is running. perform() holds at DELAY until it resolves.
|
||||
var/is_starting_crawl = FALSE
|
||||
/// Set by the async action when the crawl finished but we did not end up in the vent.
|
||||
var/failed_ventcrawl = FALSE
|
||||
|
||||
/datum/ai_behavior/crawl_through_vents/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/target = controller.blackboard[target_key] || controller.blackboard[BB_ENTRY_VENT_TARGET]
|
||||
return istype(target) && isliving(controller.pawn) // only mobs can vent crawl in the current framework
|
||||
|
||||
/datum/ai_behavior/crawl_through_vents/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/entry_vent = controller.blackboard[target_key] || controller.blackboard[BB_ENTRY_VENT_TARGET]
|
||||
/datum/bt_node/ai_behavior/enter_vent/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/cached_pawn = controller.pawn
|
||||
if(HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING) || !controller.blackboard[BB_CURRENTLY_TARGETING_VENT] || !is_vent_valid(entry_vent))
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
if(!cached_pawn.can_enter_vent(entry_vent, provide_feedback = FALSE)) // we're an AI we scoff at feedback
|
||||
// "never enter a hole you can't get out of"
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
// We kicked off the crawl on a previous tick; report its result once it resolves. Flags reset in finish_action.
|
||||
if(failed_ventcrawl)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(is_starting_crawl)
|
||||
if(!HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING))
|
||||
return AI_BEHAVIOR_DELAY // still climbing in
|
||||
controller.set_blackboard_key(BB_VENT_ENTRY_TIME, world.time)
|
||||
if(prob(50))
|
||||
cached_pawn.visible_message(
|
||||
span_warning("[cached_pawn] scrambles into the ventilation ducts!"),
|
||||
span_hear("You hear something scampering through the ventilation ducts."),
|
||||
)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
var/vent_we_exit_out_of = calculate_exit_vent(controller, target_key)
|
||||
if(isnull(vent_we_exit_out_of)) // don't get into the vents if we can't get out of them, that's SILLY.
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
if(HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
controller.set_blackboard_key(BB_CURRENTLY_TARGETING_VENT, FALSE) // must be done here because we have a do_after sleep in handle_ventcrawl unfortunately and double dipping could lead to erroneous suicide pill calls.
|
||||
cached_pawn.handle_ventcrawl(entry_vent)
|
||||
if(!HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING)) //something failed and we ARE NOT IN THE VENT even though the earlier check said we were good to go! odd.
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/entry_vent = controller.blackboard[entry_vent_key]
|
||||
if(!is_vent_valid(entry_vent) || !isliving(cached_pawn))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
controller.set_blackboard_key(BB_EXIT_VENT_TARGET, vent_we_exit_out_of)
|
||||
if(!cached_pawn.can_enter_vent(entry_vent, provide_feedback = FALSE))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
if(prob(50))
|
||||
cached_pawn.visible_message(
|
||||
span_warning("[src] scrambles into the ventilation ducts!"),
|
||||
span_hear("You hear something scampering through the ventilation ducts."),
|
||||
)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/exit_vent = calculate_exit_vent(controller)
|
||||
if(isnull(exit_vent))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/lower_vent_time_limit = controller.blackboard[BB_LOWER_VENT_TIME_LIMIT] // the least amount of time we spend in the vents
|
||||
var/upper_vent_time_limit = controller.blackboard[BB_UPPER_VENT_TIME_LIMIT] // the most amount of time we spend in the vents
|
||||
|
||||
addtimer(CALLBACK(src, PROC_REF(exit_the_vents), controller), rand(lower_vent_time_limit, upper_vent_time_limit))
|
||||
controller.set_blackboard_key(BB_GIVE_UP_ON_VENT_PATHING_TIMER_ID, addtimer(CALLBACK(src, PROC_REF(delayed_suicide_pill), controller, target_key), controller.blackboard[BB_TIME_TO_GIVE_UP_ON_VENT_PATHING], TIMER_STOPPABLE))
|
||||
controller.set_blackboard_key(BB_EXIT_VENT_TARGET, exit_vent)
|
||||
is_starting_crawl = TRUE
|
||||
INVOKE_ASYNC(src, PROC_REF(perform_ventcrawl_action), controller, entry_vent)
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
/// Figure out an exit vent that we should head towards. If we don't have one, default to the entry vent. If they're all kaput, we die.
|
||||
/datum/ai_behavior/crawl_through_vents/proc/calculate_exit_vent(datum/ai_controller/controller, target_key)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/returnable_vent
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/vent_we_entered_through = controller.blackboard[target_key] || controller.blackboard[BB_ENTRY_VENT_TARGET]
|
||||
/// Runs the sleeping ventcrawl off the tick. Flags failure if we didn't end up in the vent, so perform() never has to sleep.
|
||||
/datum/bt_node/ai_behavior/enter_vent/proc/perform_ventcrawl_action(datum/ai_controller/controller, obj/machinery/atmospherics/components/unary/vent_pump/entry_vent)
|
||||
var/mob/living/cached_pawn = controller.pawn
|
||||
cached_pawn.handle_ventcrawl(entry_vent)
|
||||
if(!HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING)) //something failed and we ARE NOT IN THE VENT even though the earlier check said we were good to go! odd.
|
||||
failed_ventcrawl = TRUE
|
||||
|
||||
var/datum/pipeline/entry_vent_parent = vent_we_entered_through.parents[1]
|
||||
var/list/potential_exits = list()
|
||||
/datum/bt_node/ai_behavior/enter_vent/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
is_starting_crawl = FALSE
|
||||
failed_ventcrawl = FALSE
|
||||
if(!succeeded)
|
||||
controller.clear_blackboard_key(entry_vent_key)
|
||||
|
||||
for(var/obj/machinery/atmospherics/components/unary/vent_pump/vent in entry_vent_parent.other_atmos_machines)
|
||||
if(is_vent_valid(vent))
|
||||
potential_exits.Add(vent)
|
||||
/// Returns TRUE if the vent exists and isn't welded shut.
|
||||
/datum/bt_node/ai_behavior/enter_vent/proc/is_vent_valid(obj/machinery/atmospherics/components/unary/vent_pump/vent)
|
||||
return !QDELETED(vent) && !vent.welded
|
||||
|
||||
if(length(potential_exits))
|
||||
returnable_vent = pick(potential_exits)
|
||||
return returnable_vent
|
||||
|
||||
// if we're here, we're in "what the flarp" mode... okay maybe we can default to the vent we entered in.
|
||||
returnable_vent = vent_we_entered_through
|
||||
if(is_vent_valid(vent_we_entered_through))
|
||||
// AH WHAT THE FUCK. okay, maybe we're not inside the vents yet? let's return null and we can pick up on that based on the wider context of the proc that invokes it.
|
||||
/// Picks a random valid vent on the same pipeline as the entry vent. Falls back to the entry vent itself; returns null if nothing is usable.
|
||||
/datum/bt_node/ai_behavior/enter_vent/proc/calculate_exit_vent(datum/ai_controller/controller)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/entry_vent = controller.blackboard[entry_vent_key]
|
||||
if(QDELETED(entry_vent))
|
||||
return null
|
||||
|
||||
return returnable_vent // we return null in case something yonked between then and now so it's all good man
|
||||
var/datum/pipeline/parent_pipe = entry_vent.parents[1]
|
||||
var/list/candidates = list()
|
||||
for(var/obj/machinery/atmospherics/components/unary/vent_pump/vent in parent_pipe.other_atmos_machines)
|
||||
if(is_vent_valid(vent))
|
||||
candidates += vent
|
||||
|
||||
/// We've had enough horsing around in the vents, it's time to get out.
|
||||
/datum/ai_behavior/crawl_through_vents/proc/exit_the_vents(datum/ai_controller/controller, target_key)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/emergency_vent // vent we will scramble to search for in case plan A is a bust (exit vent)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/exit_vent = controller.blackboard[BB_EXIT_VENT_TARGET]
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
if(length(candidates))
|
||||
return pick(candidates)
|
||||
|
||||
if(!HAS_TRAIT(living_pawn, TRAIT_MOVE_VENTCRAWLING) && isturf(get_turf(living_pawn))) // we're out of the vents, so no need to do an exit
|
||||
// assume that we got yeeted out somehow and return this so we can halt the suicide pill timer.
|
||||
finish_action(controller, TRUE, target_key)
|
||||
return
|
||||
if(is_vent_valid(entry_vent))
|
||||
return null // in the pipeline already; let the caller handle it
|
||||
|
||||
living_pawn.forceMove(exit_vent)
|
||||
if(!living_pawn.can_enter_vent(exit_vent, provide_feedback = FALSE))
|
||||
// oh shit, something happened while we were waiting on that timer. let's figure out a different way to get out of here.
|
||||
emergency_vent = calculate_exit_vent(controller)
|
||||
if(isnull(emergency_vent))
|
||||
// it's joever. we cooked too hard.
|
||||
suicide_pill(controller)
|
||||
return
|
||||
return entry_vent
|
||||
|
||||
controller.set_blackboard_key(BB_EXIT_VENT_TARGET, emergency_vent) // assign and go again
|
||||
addtimer(CALLBACK(src, PROC_REF(exit_the_vents), controller), (rand(controller.blackboard[BB_LOWER_VENT_TIME_LIMIT], controller.blackboard[BB_UPPER_VENT_TIME_LIMIT]) / 2)) // we're in danger mode, so scurry out at half the time it would normally take.
|
||||
return
|
||||
|
||||
living_pawn.handle_ventcrawl(exit_vent)
|
||||
if(HAS_TRAIT(living_pawn, TRAIT_MOVE_VENTCRAWLING)) // how'd we fail? what the fuck
|
||||
stack_trace("We failed to exit the vents, even though we should have been fine? This is very weird.")
|
||||
suicide_pill(controller)
|
||||
return
|
||||
/// Waits inside a vent for a randomised duration, then exits. Handles the give-up timeout.
|
||||
/datum/bt_node/ai_behavior/exit_vent
|
||||
time_between_perform = 1 SECONDS
|
||||
var/target_exit_time = 0
|
||||
/// TRUE while the async crawl-out is running. perform() holds at DELAY until it resolves.
|
||||
var/is_exiting_crawl = FALSE
|
||||
/// Set by the async action when the crawl finished but we are somehow still in the vent.
|
||||
var/failed_ventcrawl = FALSE
|
||||
|
||||
finish_action(controller, TRUE, target_key)
|
||||
return
|
||||
|
||||
/// Incredibly stripped down version of the overarching `can_enter_vent` proc on `/mob, just meant for rapid rechecking of a vent. Will be TRUE if not blocked, FALSE otherwise.
|
||||
/datum/ai_behavior/crawl_through_vents/proc/is_vent_valid(obj/machinery/atmospherics/components/unary/vent_pump/checkable)
|
||||
return !QDELETED(checkable) && !checkable.welded
|
||||
|
||||
/// Wraps a delayed defeat, so we gotta handle the return value properly ya feel?
|
||||
/datum/ai_behavior/crawl_through_vents/proc/delayed_suicide_pill(datum/ai_controller/controller, target_key)
|
||||
if(suicide_pill(controller) & AI_BEHAVIOR_FAILED)
|
||||
finish_action(controller, FALSE, target_key)
|
||||
|
||||
/// Aw fuck, we may have been bested somehow. Regardless of what we do, we can't exit through a vent! Let's end our misery and prevent useless endless calculations.
|
||||
/datum/ai_behavior/crawl_through_vents/proc/suicide_pill(datum/ai_controller/controller)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
|
||||
if(istype(living_pawn))
|
||||
if(isnull(living_pawn.client)) // only call death if we don't have a client because maybe their natural intelligence can pick up where our AI calculations have failed
|
||||
living_pawn.death(TRUE) // call gibbed as true because we are never coming back it is so fucking joever
|
||||
|
||||
return AI_BEHAVIOR_FAILED
|
||||
|
||||
if(QDELETED(living_pawn)) // we got deleted by some other means, just presume the action is a wash and get outta here
|
||||
return NONE
|
||||
|
||||
qdel(living_pawn) // failover, we really should've been caught in the istype() but lets just bow out of existing at this point
|
||||
return NONE
|
||||
|
||||
/datum/ai_behavior/crawl_through_vents/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
/datum/bt_node/ai_behavior/exit_vent/setup(datum/ai_controller/controller)
|
||||
. = ..()
|
||||
var/lower = controller.blackboard[BB_LOWER_VENT_TIME_LIMIT]
|
||||
var/upper = controller.blackboard[BB_UPPER_VENT_TIME_LIMIT]
|
||||
var/entry_time = controller.blackboard[BB_VENT_ENTRY_TIME] || world.time
|
||||
target_exit_time = entry_time + rand(lower, upper)
|
||||
return TRUE
|
||||
|
||||
deltimer(controller.blackboard[BB_GIVE_UP_ON_VENT_PATHING_TIMER_ID])
|
||||
controller.clear_blackboard_key(target_key)
|
||||
controller.clear_blackboard_key(BB_ENTRY_VENT_TARGET)
|
||||
/datum/bt_node/ai_behavior/exit_vent/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/cached_pawn = controller.pawn
|
||||
|
||||
// We kicked off the crawl-out on a previous tick; report its result once it resolves. Flags reset in finish_action.
|
||||
if(failed_ventcrawl)
|
||||
return suicide_pill(cached_pawn)
|
||||
if(is_exiting_crawl)
|
||||
if(HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING))
|
||||
return AI_BEHAVIOR_DELAY // still climbing out
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
if(world.time < target_exit_time)
|
||||
var/give_up = controller.blackboard[BB_TIME_TO_GIVE_UP_ON_VENT_PATHING]
|
||||
var/entry_time = controller.blackboard[BB_VENT_ENTRY_TIME]
|
||||
if(give_up && entry_time && world.time > entry_time + give_up)
|
||||
return suicide_pill(cached_pawn)
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/exit_vent = controller.blackboard[BB_EXIT_VENT_TARGET]
|
||||
if(!is_vent_valid(exit_vent))
|
||||
exit_vent = calculate_exit_vent(controller)
|
||||
if(isnull(exit_vent))
|
||||
return suicide_pill(cached_pawn)
|
||||
controller.set_blackboard_key(BB_EXIT_VENT_TARGET, exit_vent)
|
||||
|
||||
cached_pawn.forceMove(exit_vent)
|
||||
if(!cached_pawn.can_enter_vent(exit_vent, provide_feedback = FALSE))
|
||||
// vent became unusable while we waited; try an emergency exit next tick
|
||||
var/emergency = calculate_exit_vent(controller)
|
||||
if(isnull(emergency))
|
||||
return suicide_pill(cached_pawn)
|
||||
controller.set_blackboard_key(BB_EXIT_VENT_TARGET, emergency)
|
||||
target_exit_time = world.time // retry immediately next tick
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
is_exiting_crawl = TRUE
|
||||
INVOKE_ASYNC(src, PROC_REF(perform_ventcrawl_action), controller, exit_vent)
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
/// Runs the sleeping ventcrawl off the tick. Flags failure if we're somehow still in the vent, so perform() never has to sleep.
|
||||
/datum/bt_node/ai_behavior/exit_vent/proc/perform_ventcrawl_action(datum/ai_controller/controller, obj/machinery/atmospherics/components/unary/vent_pump/exit_vent)
|
||||
var/mob/living/cached_pawn = controller.pawn
|
||||
cached_pawn.handle_ventcrawl(exit_vent)
|
||||
if(HAS_TRAIT(cached_pawn, TRAIT_MOVE_VENTCRAWLING))
|
||||
stack_trace("[cached_pawn] [type]: exited vent but still has TRAIT_MOVE_VENTCRAWLING")
|
||||
failed_ventcrawl = TRUE
|
||||
|
||||
/datum/bt_node/ai_behavior/exit_vent/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
is_exiting_crawl = FALSE
|
||||
failed_ventcrawl = FALSE
|
||||
controller.clear_blackboard_key(BB_VENT_ENTRY_TIME)
|
||||
controller.clear_blackboard_key(BB_EXIT_VENT_TARGET)
|
||||
controller.set_blackboard_key(BB_CURRENTLY_TARGETING_VENT, FALSE) // just in case
|
||||
controller.clear_blackboard_key(BB_ENTRY_VENT_TARGET)
|
||||
|
||||
/// Kills the pawn if it has no client, then returns INSTANT FAILED.
|
||||
/datum/bt_node/ai_behavior/exit_vent/proc/suicide_pill(mob/living/pawn)
|
||||
if(istype(pawn) && isnull(pawn.client))
|
||||
pawn.death(TRUE)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
/// Returns TRUE if the vent exists and isn't welded shut.
|
||||
/datum/bt_node/ai_behavior/exit_vent/proc/is_vent_valid(obj/machinery/atmospherics/components/unary/vent_pump/vent)
|
||||
return !QDELETED(vent) && !vent.welded
|
||||
|
||||
/// Picks a random valid vent on the same pipeline as BB_ENTRY_VENT_TARGET. Returns null if nothing is usable.
|
||||
/datum/bt_node/ai_behavior/exit_vent/proc/calculate_exit_vent(datum/ai_controller/controller)
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/entry_vent = controller.blackboard[BB_ENTRY_VENT_TARGET]
|
||||
if(QDELETED(entry_vent))
|
||||
return null
|
||||
|
||||
var/datum/pipeline/parent_pipe = entry_vent.parents[1]
|
||||
var/list/candidates = list()
|
||||
for(var/obj/machinery/atmospherics/components/unary/vent_pump/vent in parent_pipe.other_atmos_machines)
|
||||
if(is_vent_valid(vent))
|
||||
candidates += vent
|
||||
|
||||
if(length(candidates))
|
||||
return pick(candidates)
|
||||
|
||||
if(is_vent_valid(entry_vent))
|
||||
return null
|
||||
|
||||
return entry_vent
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/// Picks targets based on which one has the lowest health
|
||||
/datum/ai_behavior/find_potential_targets/most_wounded
|
||||
|
||||
/datum/ai_behavior/find_potential_targets/most_wounded/pick_final_target(datum/ai_controller/controller, list/filtered_targets)
|
||||
var/list/living_targets = list()
|
||||
for(var/mob/living/living_target in filtered_targets)
|
||||
living_targets += filtered_targets
|
||||
if(living_targets.len)
|
||||
sortTim(living_targets, GLOBAL_PROC_REF(cmp_mob_health))
|
||||
return pop(living_targets)
|
||||
return ..()
|
||||
@@ -1,15 +1,23 @@
|
||||
/datum/ai_behavior/write_on_paper
|
||||
/// Scrawls a random line from the writing list onto the carried paper, then drops it. Clears the carry key on finish.
|
||||
/datum/bt_node/ai_behavior/write_on_paper
|
||||
/// Blackboard key holding the paper to write on (also the virtual carry slot).
|
||||
var/paper_key
|
||||
/// Blackboard key holding the list of phrases to choose from.
|
||||
var/writing_list_key
|
||||
|
||||
/datum/ai_behavior/write_on_paper/perform(seconds_per_tick, datum/ai_controller/controller, found_paper, list_of_writings)
|
||||
/datum/bt_node/ai_behavior/write_on_paper/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/wizard = controller.pawn
|
||||
var/list/writing_list = controller.blackboard[list_of_writings]
|
||||
var/obj/item/paper/target = controller.blackboard[found_paper]
|
||||
var/obj/item/paper/target = controller.blackboard[paper_key]
|
||||
if(QDELETED(target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
var/list/writing_list = controller.blackboard[writing_list_key]
|
||||
if(length(writing_list))
|
||||
target.add_raw_text(pick(writing_list))
|
||||
target.update_appearance()
|
||||
wizard.dropItemToGround(target)
|
||||
if(target.loc == wizard)
|
||||
target.forceMove(get_turf(wizard))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/write_on_paper/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
/datum/bt_node/ai_behavior/write_on_paper/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
controller.clear_blackboard_key(paper_key)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/// Attack something which is already adjacent to us, without ending planning
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic
|
||||
melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/opportunistic
|
||||
end_planning = FALSE
|
||||
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
|
||||
if(QDELETED(target) || !controller.pawn.Adjacent(target))
|
||||
return
|
||||
if (isliving(controller.pawn))
|
||||
var/mob/living/pawn = controller.pawn
|
||||
if (LAZYLEN(pawn.do_afters))
|
||||
return
|
||||
controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
|
||||
|
||||
/// Attack something which is already adjacent to us without moving
|
||||
/datum/ai_behavior/basic_melee_attack/opportunistic
|
||||
action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove
|
||||
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/opportunistic/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
|
||||
if (!controller.blackboard_key_exists(targeting_strategy_key))
|
||||
CRASH("No target datum was supplied in the blackboard for [controller.pawn]")
|
||||
return controller.blackboard_key_exists(target_key)
|
||||
|
||||
/datum/ai_behavior/basic_melee_attack/opportunistic/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
|
||||
var/atom/movable/atom_pawn = controller.pawn
|
||||
var/atom/atom_target = controller.blackboard[target_key]
|
||||
if (QDELETED(atom_target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
if(!atom_target.IsReachableBy(atom_pawn))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
. = ..()
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -1,85 +0,0 @@
|
||||
/// If there's something between us and our target then we need to queue a behaviour to make it not be there
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path
|
||||
/// Blackboard key containing current target
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
/// The action to execute, extend to add a different cooldown or something
|
||||
var/attack_behaviour = /datum/ai_behavior/attack_obstructions
|
||||
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return
|
||||
|
||||
var/turf/next_step = get_step_towards(controller.pawn, target)
|
||||
if (!next_step.is_blocked_turf(exclude_mobs = TRUE, source_atom = controller.pawn))
|
||||
return
|
||||
|
||||
controller.queue_behavior(attack_behaviour, target_key)
|
||||
// Don't cancel future planning, maybe we can move now
|
||||
|
||||
/// Something is in our way, get it outta here
|
||||
/datum/ai_behavior/attack_obstructions
|
||||
action_cooldown = 2 SECONDS
|
||||
/// If we should attack walls, be prepared for complaints about breaches
|
||||
var/can_attack_turfs = FALSE
|
||||
/// For if you want your mob to be able to attack dense objects
|
||||
var/can_attack_dense_objects = FALSE
|
||||
|
||||
/datum/ai_behavior/attack_obstructions/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/mob/living/basic/basic_mob = controller.pawn
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
|
||||
if (QDELETED(target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/turf/next_step = get_step_towards(basic_mob, target)
|
||||
var/dir_to_next_step = get_dir(basic_mob, next_step)
|
||||
// If moving diagonally we need to punch both ways, or more accurately the one we are blocked in
|
||||
var/list/dirs_to_move = list()
|
||||
if (ISDIAGONALDIR(dir_to_next_step))
|
||||
for(var/direction in GLOB.cardinals)
|
||||
if(direction & dir_to_next_step)
|
||||
dirs_to_move += direction
|
||||
else
|
||||
dirs_to_move += dir_to_next_step
|
||||
|
||||
for (var/direction in dirs_to_move)
|
||||
if (attack_in_direction(controller, basic_mob, direction))
|
||||
return AI_BEHAVIOR_DELAY
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/attack_obstructions/proc/attack_in_direction(datum/ai_controller/controller, mob/living/basic/basic_mob, direction)
|
||||
var/turf/next_step = get_step(basic_mob, direction)
|
||||
if (!next_step.is_blocked_turf(exclude_mobs = TRUE, source_atom = controller.pawn))
|
||||
return FALSE
|
||||
|
||||
for (var/obj/object as anything in next_step.contents)
|
||||
if (!can_smash_object(basic_mob, object))
|
||||
continue
|
||||
basic_mob.melee_attack(object)
|
||||
return TRUE
|
||||
|
||||
if (can_attack_turfs)
|
||||
basic_mob.melee_attack(next_step)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/ai_behavior/attack_obstructions/proc/can_smash_object(mob/living/basic/basic_mob, obj/object)
|
||||
if (!object.density && !can_attack_dense_objects)
|
||||
return FALSE
|
||||
if (object.IsObscured())
|
||||
return FALSE
|
||||
if (basic_mob.see_invisible < object.invisibility)
|
||||
return FALSE
|
||||
var/list/whitelist = basic_mob.ai_controller.blackboard[BB_OBSTACLE_TARGETING_WHITELIST]
|
||||
if(whitelist && !is_type_in_typecache(object, whitelist))
|
||||
return FALSE
|
||||
|
||||
return TRUE // It's in our way, let's get it out of our way
|
||||
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path/low_priority_target
|
||||
target_key = BB_LOW_PRIORITY_HUNTING_TARGET
|
||||
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path/pet_target
|
||||
target_key = BB_CURRENT_PET_TARGET
|
||||
@@ -1,68 +0,0 @@
|
||||
/// 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)
|
||||
var/say_key = BB_REINFORCEMENTS_SAY
|
||||
/// Blackboard key containing an emote to perform when calling reinforcements
|
||||
var/emote_key = BB_REINFORCEMENTS_EMOTE
|
||||
/// Reinforcement-calling behavior to use
|
||||
var/call_type = /datum/ai_behavior/call_reinforcements
|
||||
|
||||
/datum/ai_planning_subtree/call_reinforcements/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
if (!decide_to_call(controller) || controller.blackboard[BB_BASIC_MOB_REINFORCEMENTS_COOLDOWN] > world.time)
|
||||
return
|
||||
|
||||
var/call_say = controller.blackboard[BB_REINFORCEMENTS_SAY]
|
||||
var/call_emote = controller.blackboard[BB_REINFORCEMENTS_EMOTE]
|
||||
|
||||
if(!isnull(call_say))
|
||||
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)
|
||||
|
||||
controller.queue_behavior(call_type)
|
||||
|
||||
/// Decides when to call reinforcements, can be overridden for alternate behavior
|
||||
/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))
|
||||
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 + cooldown)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/// 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
|
||||
@@ -1,65 +1,59 @@
|
||||
/// Add or remove people to our retaliation shitlist just on an arbitrary whim
|
||||
/datum/ai_planning_subtree/capricious_retaliate
|
||||
/// Blackboard key which tells us how to select valid targets
|
||||
var/targeting_strategy_key = BB_TARGETING_STRATEGY
|
||||
/// Whether we should skip checking faction for our decision
|
||||
var/ignore_faction = TRUE
|
||||
///Random chance to add things to our retaliate list
|
||||
/datum/bt_node/ai_behavior/capricious_retaliate
|
||||
var/targeting_strategy = BB_TARGETING_STRATEGY
|
||||
var/ignore_faction
|
||||
time_between_perform = 1 SECONDS
|
||||
|
||||
/datum/ai_planning_subtree/capricious_retaliate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
controller.queue_behavior(/datum/ai_behavior/capricious_retaliate, targeting_strategy_key, ignore_faction)
|
||||
|
||||
/// Add or remove people to our retaliation shitlist just on an arbitrary whim
|
||||
/datum/ai_behavior/capricious_retaliate
|
||||
action_cooldown = 1 SECONDS
|
||||
|
||||
/datum/ai_behavior/capricious_retaliate/perform(seconds_per_tick, datum/ai_controller/controller, targeting_strategy_key, ignore_faction)
|
||||
/datum/bt_node/ai_behavior/capricious_retaliate/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/atom/pawn = controller.pawn
|
||||
if (controller.blackboard_key_exists(BB_BASIC_MOB_RETALIATE_LIST))
|
||||
|
||||
if(controller.blackboard_key_exists(BB_BASIC_MOB_RETALIATE_LIST))
|
||||
var/deaggro_chance = controller.blackboard[BB_RANDOM_DEAGGRO_CHANCE] || 10
|
||||
if (!SPT_PROB(deaggro_chance, seconds_per_tick))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
pawn.visible_message(span_notice("[pawn] calms down.")) // We can blackboard key this if anyone else actually wants to customise it
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST)
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
|
||||
controller.CancelActions() // Otherwise they will try and get one last kick in
|
||||
return AI_BEHAVIOR_DELAY
|
||||
if(prob(deaggro_chance)) //Chance to chill the fuck out. This prob() should be matched with the frequency of calling.
|
||||
pawn.visible_message(span_notice("[pawn] calms down."))
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST)
|
||||
controller.clear_blackboard_key(BB_CURRENT_TARGET)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED // De-aggroed
|
||||
|
||||
var/aggro_chance = controller.blackboard[BB_RANDOM_AGGRO_CHANCE] || 0.5
|
||||
if (!SPT_PROB(aggro_chance, seconds_per_tick))
|
||||
if(!prob(aggro_chance)) //Check if we should get pissed at someone REEE
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/aggro_range = controller.blackboard[BB_AGGRO_RANGE] || 9
|
||||
var/list/potential_targets = hearers(aggro_range, get_turf(pawn)) - pawn
|
||||
if (!length(potential_targets))
|
||||
if(!length(potential_targets))
|
||||
failed_targeting(pawn)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/datum/targeting_strategy/target_helper = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
|
||||
if(!ispath(targeting_strategy))
|
||||
targeting_strategy = controller.blackboard[targeting_strategy]
|
||||
|
||||
var/datum/targeting_strategy/target_helper = GET_TARGETING_STRATEGY(targeting_strategy)
|
||||
|
||||
if(ignore_faction)
|
||||
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
|
||||
|
||||
var/mob/living/final_target = null
|
||||
if (ignore_faction)
|
||||
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
|
||||
while (isnull(final_target) && length(potential_targets))
|
||||
while(isnull(final_target) && length(potential_targets))
|
||||
var/mob/living/test_target = pick_n_take(potential_targets)
|
||||
if (target_helper.can_attack(pawn, test_target, vision_range = aggro_range))
|
||||
if(target_helper.is_valid_target(pawn, test_target, vision_range = aggro_range))
|
||||
final_target = test_target
|
||||
|
||||
if (isnull(final_target))
|
||||
if(isnull(final_target))
|
||||
failed_targeting(pawn)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
// Add to shitlist set_blackboard_key_assoc_lazylist calls post_blackboard_key_set, waking the combat branch
|
||||
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
|
||||
|
||||
/// Called if we try but fail to target something
|
||||
/datum/ai_behavior/capricious_retaliate/proc/failed_targeting(atom/pawn)
|
||||
pawn.visible_message(span_notice("[pawn] grumbles.")) // We're pissed off but with no outlet to vent our frustration upon
|
||||
/datum/bt_node/ai_behavior/capricious_retaliate/proc/failed_targeting(atom/pawn)
|
||||
pawn.visible_message(span_notice("[pawn] grumbles."))
|
||||
|
||||
/datum/ai_behavior/capricious_retaliate/finish_action(datum/ai_controller/controller, succeeded, ignore_faction)
|
||||
/datum/bt_node/ai_behavior/capricious_retaliate/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
if (succeeded || !ignore_faction)
|
||||
if(succeeded || !ignore_faction)
|
||||
return
|
||||
var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE
|
||||
controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/datum/ai_planning_subtree/climb_trees
|
||||
operational_datums = list(/datum/component/tree_climber)
|
||||
///chance to climb a tree
|
||||
var/climb_chance = 35
|
||||
|
||||
/datum/ai_planning_subtree/climb_trees/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
|
||||
if(!SPT_PROB(climb_chance, seconds_per_tick))
|
||||
return
|
||||
|
||||
if(controller.blackboard_key_exists(BB_CLIMBED_TREE))
|
||||
controller.queue_behavior(/datum/ai_behavior/climb_tree, BB_CLIMBED_TREE)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_tree, BB_CLIMBED_TREE, /obj/structure/flora/tree)
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
///simple behavior to make mobs randomly drag things around
|
||||
/datum/ai_planning_subtree/steal_items/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
if(living_pawn.pulling)
|
||||
if(prob(controller.blackboard[BB_GUILTY_CONSCIOUS_CHANCE]))
|
||||
controller.queue_behavior(/datum/ai_behavior/stop_dragging)
|
||||
return
|
||||
if(!prob(controller.blackboard[BB_STEAL_CHANCE]))
|
||||
return
|
||||
if(!controller.blackboard_key_exists(BB_ITEM_TO_STEAL))
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/find_stealable, BB_ITEM_TO_STEAL, /obj/item)
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/drag_target, BB_ITEM_TO_STEAL)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/ai_behavior/find_and_set/find_stealable
|
||||
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
action_cooldown = 2 MINUTES
|
||||
|
||||
/datum/ai_behavior/find_and_set/find_stealable/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
|
||||
var/list/possible_items = shuffle_inplace(oview(search_range, controller.pawn))
|
||||
for(var/obj/item/possible_item in possible_items)
|
||||
if(possible_item.pulledby || possible_item.anchored)
|
||||
continue
|
||||
if(can_see(living_pawn, possible_item))
|
||||
return possible_item
|
||||
|
||||
|
||||
/datum/ai_behavior/stop_dragging
|
||||
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
|
||||
/datum/ai_behavior/stop_dragging/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
living_pawn.stop_pulling()
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/drag_target
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
|
||||
|
||||
/datum/ai_behavior/drag_target/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/drag_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/atom/movable/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target) || target.anchored || target.pulledby)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
var/mob/living/our_mob = controller.pawn
|
||||
our_mob.start_pulling(target)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/drag_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
@@ -1,44 +1,33 @@
|
||||
// Performs the enrage behavior when health is below given threshold, and calm down behavior if above that value afterwards
|
||||
/datum/ai_planning_subtree/enrage
|
||||
/// Halves the basic mob's melee attack cooldown while its health is at or below a threshold, and restores it once recovered.
|
||||
/datum/bt_node/ai_behavior/enrage
|
||||
/// Fraction of max health at or below which the mob becomes enraged.
|
||||
var/health_threshold = 0.5
|
||||
var/enrage_behavior = /datum/ai_behavior/enrage
|
||||
|
||||
/datum/ai_planning_subtree/enrage/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
/datum/bt_node/ai_behavior/enrage/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
if(!isbasicmob(controller.pawn))
|
||||
return
|
||||
return AI_BEHAVIOR_FAILED
|
||||
|
||||
var/mob/living/basic/basic_pawn = controller.pawn
|
||||
var/low_health = (basic_pawn.health / basic_pawn.maxHealth) <= health_threshold
|
||||
|
||||
var/is_enraged = controller.blackboard_key_exists(BB_BASIC_MOB_ENRAGE)
|
||||
|
||||
if(low_health && !is_enraged)
|
||||
controller.queue_behavior(enrage_behavior, FALSE)
|
||||
else if(!low_health && is_enraged)
|
||||
controller.queue_behavior(enrage_behavior, TRUE)
|
||||
var/current_cooldown = basic_pawn.melee_attack_cooldown
|
||||
controller.set_blackboard_key(BB_BASIC_MOB_ENRAGE, TRUE)
|
||||
controller.set_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN, current_cooldown)
|
||||
basic_pawn.melee_attack_cooldown = current_cooldown / 2
|
||||
|
||||
if(controller.blackboard_key_exists(BB_CURRENT_TARGET))
|
||||
basic_pawn.visible_message(span_danger("\The [basic_pawn] gets an enraged look at [controller.blackboard[BB_CURRENT_TARGET]]!"))
|
||||
else
|
||||
basic_pawn.visible_message(span_danger("\The [basic_pawn] gets an enraged look!"))
|
||||
return AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/// Cuts down basic mob's melee attack cooldown in half
|
||||
/datum/ai_behavior/enrage
|
||||
|
||||
/datum/ai_behavior/enrage/perform(seconds_per_tick, datum/ai_controller/controller, calm_down)
|
||||
var/mob/living/basic/basic_pawn = controller.pawn
|
||||
if(calm_down)
|
||||
var/previous_delay = controller.blackboard[BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN]
|
||||
if(!low_health && is_enraged)
|
||||
// Technically something else could have modified the cooldown before/after but that requires further consideration so don't use this behavior in these scenarios
|
||||
basic_pawn.melee_attack_cooldown = previous_delay
|
||||
basic_pawn.melee_attack_cooldown = controller.blackboard[BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN]
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN)
|
||||
controller.clear_blackboard_key(BB_BASIC_MOB_ENRAGE)
|
||||
return AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
var/current_cooldown = basic_pawn.melee_attack_cooldown
|
||||
var/new_attack_cooldown = current_cooldown / 2
|
||||
|
||||
controller.set_blackboard_key(BB_BASIC_MOB_ENRAGE, TRUE)
|
||||
controller.set_blackboard_key(BB_BASIC_MOB_PREVIOUS_MELEE_COOLDOWN, current_cooldown)
|
||||
basic_pawn.melee_attack_cooldown = new_attack_cooldown
|
||||
|
||||
if(controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
|
||||
var/current_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
|
||||
controller.pawn.visible_message(span_danger("\The [controller.pawn] gets an enraged look at [current_target]!"))
|
||||
else
|
||||
controller.pawn.visible_message(span_danger("\The [controller.pawn] gets an enraged look!"))
|
||||
return AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_FAILED
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/escape_captivity",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_buckled_to_obj",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/buckle_target_dangerous",
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/break_out_of_object/from_bb",
|
||||
"vars": {
|
||||
"target_key": "BB_BASIC_MOB_ESCAPE_TARGET"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_contained_in_obj",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/container_attackable",
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/break_out_of_object/from_bb",
|
||||
"vars": {
|
||||
"target_key": "BB_BASIC_MOB_ESCAPE_TARGET"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_grabbed_by_enemy",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_is_restrained",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,70 +1,7 @@
|
||||
/// Generically try to escape from being trapped
|
||||
/datum/ai_planning_subtree/escape_captivity
|
||||
/// Targeting strategy for use deciding if we can attack a mob grabbing us
|
||||
var/targeting_strategy_key = BB_TARGETING_STRATEGY
|
||||
/// If true we will never attack objects
|
||||
var/pacifist = FALSE
|
||||
///Tries to escape activity, has observers to cancel if needed
|
||||
/datum/bt_node/subtree/escape_captivity
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/escape_captivity.bt.json"
|
||||
|
||||
/datum/ai_planning_subtree/escape_captivity/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
|
||||
if (isobj(living_pawn.buckled))
|
||||
// we can just stand up we don't need to freak out
|
||||
if (pacifist || !HAS_TRAIT(living_pawn.buckled, TRAIT_DANGEROUS_BUCKLE))
|
||||
controller.queue_behavior(/datum/ai_behavior/resist)
|
||||
// otherwise beat the shit out of we we gotta get out NOW
|
||||
else
|
||||
controller.queue_behavior(/datum/ai_behavior/break_out_of_object, living_pawn.buckled)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if (!isturf(living_pawn.loc) && !ismob(living_pawn.loc) && !istype(living_pawn.loc, /obj/item/mob_holder))
|
||||
var/atom/contained_in = living_pawn.loc
|
||||
var/attack_effective = FALSE
|
||||
if (!pacifist)
|
||||
if (isbasicmob(living_pawn)) // Currently this literally only works for basic mobs because it's hard to check for anyone else but it's ok because only they use this subtree
|
||||
var/mob/living/basic/basic_pawn = living_pawn
|
||||
attack_effective = basic_pawn.obj_damage > contained_in.damage_deflection
|
||||
if (attack_effective)
|
||||
controller.queue_behavior(/datum/ai_behavior/break_out_of_object, contained_in)
|
||||
else
|
||||
controller.queue_behavior(/datum/ai_behavior/resist)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
var/mob/puller = living_pawn.pulledby
|
||||
if (puller && puller.grab_state > GRAB_PASSIVE)
|
||||
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
|
||||
var/friends_list = controller.blackboard[BB_FRIENDS_LIST] || list()
|
||||
// Only resist grabs from mobs that aren't in our faction
|
||||
if (targeting_strategy?.can_attack(living_pawn, puller) && !(puller in friends_list))
|
||||
controller.queue_behavior(/datum/ai_behavior/resist)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if (HAS_TRAIT(living_pawn, TRAIT_RESTRAINED))
|
||||
controller.queue_behavior(/datum/ai_behavior/resist)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/// Keep attacking an object while it is our loc or while we are buckled to it
|
||||
/datum/ai_behavior/break_out_of_object
|
||||
action_cooldown = 0.2 SECONDS
|
||||
|
||||
/datum/ai_behavior/break_out_of_object/setup(datum/ai_controller/controller, atom/target)
|
||||
if (!should_attack_target(controller, target))
|
||||
return FALSE
|
||||
return TRUE
|
||||
|
||||
/datum/ai_behavior/break_out_of_object/perform(seconds_per_tick, datum/ai_controller/controller, atom/target_atom)
|
||||
if (!should_attack_target(controller, target_atom))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
controller.ai_interact(target = target_atom, combat_mode = TRUE)
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
/datum/ai_behavior/break_out_of_object/proc/should_attack_target(datum/ai_controller/controller, atom/target)
|
||||
if (QDELETED(target))
|
||||
return FALSE
|
||||
var/mob/living/pawn = controller.pawn
|
||||
if (!target.IsReachableBy(pawn))
|
||||
return FALSE
|
||||
return pawn.loc == target || pawn.buckled == target
|
||||
|
||||
/datum/ai_planning_subtree/escape_captivity/pacifist
|
||||
pacifist = TRUE
|
||||
/// Pacifist variant: never attacks objects, only resists.
|
||||
/datum/bt_node/subtree/escape_captivity/pacifist
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/escape_captivity_pacifist.bt.json"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/escape_captivity/pacifist",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_buckled_to_obj",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_contained_in_obj",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_grabbed_by_enemy",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/pawn_is_restrained",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/resist"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#define HIGH_HAPPINESS_THRESHOLD 0.7
|
||||
#define MODERATE_HAPPINESS_THRESHOLD 0.5
|
||||
|
||||
/datum/ai_planning_subtree/express_happiness
|
||||
operational_datums = list(/datum/component/happiness)
|
||||
///the key storing our happiness value
|
||||
var/happiness_key = BB_BASIC_HAPPINESS
|
||||
///list of emotions we relay when happy
|
||||
var/static/list/happy_emotions = list(
|
||||
"celebrates happily!",
|
||||
"dances around in excitement!",
|
||||
)
|
||||
///our moderate emotions
|
||||
var/static/list/moderate_emotions = list(
|
||||
"looks satisfied.",
|
||||
"trots around.",
|
||||
)
|
||||
///emotions we display when we are sad
|
||||
var/static/list/depressed_emotions = list(
|
||||
"looks depressed...",
|
||||
"turns its back and sulks...",
|
||||
"looks towards the floor in dissapointment...",
|
||||
)
|
||||
|
||||
/datum/ai_planning_subtree/express_happiness/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(!SPT_PROB(5, seconds_per_tick))
|
||||
return
|
||||
var/happiness_value = controller.blackboard[happiness_key]
|
||||
if(isnull(happiness_value))
|
||||
return
|
||||
var/list/final_list
|
||||
switch(happiness_value)
|
||||
if(HIGH_HAPPINESS_THRESHOLD to INFINITY)
|
||||
final_list = controller.blackboard[BB_HAPPY_EMOTIONS] || happy_emotions
|
||||
if(MODERATE_HAPPINESS_THRESHOLD to HIGH_HAPPINESS_THRESHOLD)
|
||||
final_list = controller.blackboard[BB_MODERATE_EMOTIONS] || moderate_emotions
|
||||
else
|
||||
final_list = controller.blackboard[BB_SAD_EMOTIONS] || depressed_emotions
|
||||
if(!length(final_list))
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(final_list))
|
||||
|
||||
#undef HIGH_HAPPINESS_THRESHOLD
|
||||
#undef MODERATE_HAPPINESS_THRESHOLD
|
||||
@@ -1,42 +0,0 @@
|
||||
/// similar to finding a target but looks for food types in the // the what?
|
||||
/datum/ai_planning_subtree/find_food
|
||||
///behavior we use to find the food
|
||||
var/datum/ai_behavior/finding_behavior = /datum/ai_behavior/find_and_set/in_list
|
||||
///key of foods list
|
||||
var/food_list_key = BB_BASIC_FOODS
|
||||
///key where we store our food
|
||||
var/found_food_key = BB_TARGET_FOOD
|
||||
///key holding any emotes we play after eating food
|
||||
var/emotes_blackboard_list = BB_EAT_EMOTES
|
||||
///key where we store our search range
|
||||
var/search_range = BB_SEARCH_RANGE
|
||||
|
||||
/datum/ai_planning_subtree/find_food/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/list/foods_list = controller.blackboard[food_list_key]
|
||||
if(!length(foods_list))
|
||||
CRASH("the types of food has not been supplied in the [food_list_key] key!")
|
||||
if(controller.blackboard[BB_NEXT_FOOD_EAT] > world.time)
|
||||
return
|
||||
if(!controller.blackboard_key_exists(found_food_key))
|
||||
controller.queue_behavior(finding_behavior, found_food_key, foods_list, controller.blackboard[BB_SEARCH_RANGE])
|
||||
return
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/interact_with_target/eat_food, found_food_key, emotes_blackboard_list)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/ai_behavior/interact_with_target/eat_food
|
||||
///default list of actions we take after eating
|
||||
var/list/food_actions = list(
|
||||
"eats up happily!",
|
||||
"chomps with glee!",
|
||||
)
|
||||
|
||||
/datum/ai_behavior/interact_with_target/eat_food/perform(seconds_per_tick, datum/ai_controller/controller, target_key, emotes_blackboard_list)
|
||||
. = ..()
|
||||
if(. & AI_BEHAVIOR_FAILED)
|
||||
return
|
||||
var/list/emotes_to_pick = controller.blackboard[emotes_blackboard_list] || food_actions
|
||||
if(!length(emotes_to_pick))
|
||||
return
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
living_pawn.manual_emote(pick(emotes_to_pick))
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/find_paper_and_write",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_SELF",
|
||||
"key": "BB_SIMPLE_CARRY_ITEM"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/write_on_paper",
|
||||
"vars": {
|
||||
"paper_key": "BB_SIMPLE_CARRY_ITEM",
|
||||
"writing_list_key": "BB_WRITING_LIST"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
|
||||
"key": "BB_FOUND_PAPER"
|
||||
},
|
||||
"child": {
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/attack_obstructions",
|
||||
"vars": {
|
||||
"time_between_perform": "0.4 SECONDS",
|
||||
"target_key": "BB_FOUND_PAPER",
|
||||
"can_attack_turfs": true,
|
||||
"can_attack_dense_objects": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_FOUND_PAPER",
|
||||
"required_dist": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/pick_up_item_virtual",
|
||||
"vars": {
|
||||
"target_key": "BB_FOUND_PAPER",
|
||||
"storage_key": "BB_SIMPLE_CARRY_ITEM"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,22 +1,3 @@
|
||||
/datum/ai_planning_subtree/find_paper_and_write
|
||||
|
||||
/datum/ai_planning_subtree/find_paper_and_write/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/living/basic/wizard = controller.pawn
|
||||
|
||||
if(controller.blackboard_key_exists(BB_SIMPLE_CARRY_ITEM))
|
||||
controller.queue_behavior(/datum/ai_behavior/write_on_paper, BB_SIMPLE_CARRY_ITEM, BB_WRITING_LIST)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
var/obj/item/paper/target = controller.blackboard[BB_FOUND_PAPER]
|
||||
|
||||
if(QDELETED(target))
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/empty_paper, BB_FOUND_PAPER, /obj/item/paper)
|
||||
return
|
||||
|
||||
if(get_turf(wizard) != get_turf(target))
|
||||
controller.queue_behavior(/datum/ai_behavior/travel_towards, BB_FOUND_PAPER)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if(!(target in wizard.contents))
|
||||
controller.queue_behavior(/datum/ai_behavior/pick_up_item, BB_FOUND_PAPER, BB_SIMPLE_CARRY_ITEM)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
/// Idle behaviour: hunt down a nearby blank paper, fetch it, scrawl a threat on it and drop it.
|
||||
/datum/bt_node/subtree/find_paper_and_write
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/find_paper_and_write.bt.json"
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/datum/ai_planning_subtree/look_for_adult
|
||||
///how far we must be from the mom
|
||||
var/minimum_distance = 1
|
||||
|
||||
/datum/ai_planning_subtree/look_for_adult/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/target = controller.blackboard[BB_FOUND_MOM]
|
||||
var/mob/baby = controller.pawn
|
||||
|
||||
if(QDELETED(target))
|
||||
find_mom(controller)
|
||||
return
|
||||
|
||||
if(get_dist(target, baby) > minimum_distance)
|
||||
controller.queue_behavior(/datum/ai_behavior/travel_towards/stop_on_arrival, BB_FOUND_MOM)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if(!SPT_PROB(15, seconds_per_tick))
|
||||
return
|
||||
|
||||
if(target.stat == DEAD)
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_emote, "cries for their parent!")
|
||||
else
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_emote, "dances around their parent!")
|
||||
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/ai_planning_subtree/look_for_adult/proc/find_mom(datum/ai_controller/controller)
|
||||
controller.queue_behavior(/datum/ai_behavior/find_mom, BB_FIND_MOM_TYPES, BB_IGNORE_MOM_TYPES, BB_FOUND_MOM)
|
||||
|
||||
/datum/ai_planning_subtree/look_for_adult/raptor/find_mom(datum/ai_controller/controller)
|
||||
controller.queue_behavior(/datum/ai_behavior/find_mom/raptor, BB_FIND_MOM_TYPES, BB_FOUND_MOM)
|
||||
@@ -1,6 +0,0 @@
|
||||
/// Find something with a specific trait to run from
|
||||
/datum/ai_planning_subtree/find_target_prioritize_traits
|
||||
|
||||
/datum/ai_planning_subtree/find_target_prioritize_traits/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
controller.queue_behavior(/datum/ai_behavior/find_potential_targets/prioritize_trait, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION, BB_TARGET_PRIORITY_TRAIT)
|
||||
@@ -1,44 +0,0 @@
|
||||
#define FISHING_COOLDOWN 45 SECONDS
|
||||
|
||||
///subtree for fishing and eating food!
|
||||
/datum/ai_planning_subtree/fish
|
||||
///behavior we use to find fishable objects
|
||||
var/datum/ai_behavior/find_fishable_behavior = /datum/ai_behavior/find_and_set/in_list
|
||||
///behavior we use to fish!
|
||||
var/datum/ai_behavior/fishing_behavior = /datum/ai_behavior/interact_with_target/fishing
|
||||
///blackboard key storing things we can fish from
|
||||
var/fishable_list_key = BB_FISHABLE_LIST
|
||||
///key where we store found fishable items
|
||||
var/fishing_target_key = BB_FISHING_TARGET
|
||||
///key where we store our fishing cooldown
|
||||
var/fishing_cooldown_key = BB_FISHING_COOLDOWN
|
||||
///our fishing range
|
||||
var/fishing_range = 5
|
||||
|
||||
/datum/ai_planning_subtree/fish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(controller.blackboard[BB_ONLY_FISH_WHILE_HUNGRY] && controller.blackboard[BB_NEXT_FOOD_EAT] > world.time)
|
||||
return
|
||||
if(controller.blackboard[BB_FISHING_TIMER] > world.time)
|
||||
return
|
||||
if(!controller.blackboard_key_exists(fishing_target_key))
|
||||
controller.queue_behavior(find_fishable_behavior, fishing_target_key, controller.blackboard[fishable_list_key], fishing_range)
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/interact_with_target/fishing, fishing_target_key, fishing_cooldown_key)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
///less expensive fishing behavior!
|
||||
/datum/ai_planning_subtree/fish/fish_from_turfs
|
||||
find_fishable_behavior = /datum/ai_behavior/find_and_set/in_list/closest_turf
|
||||
|
||||
/datum/ai_behavior/interact_with_target/fishing
|
||||
clear_target = FALSE
|
||||
combat_mode = FALSE
|
||||
|
||||
/datum/ai_behavior/interact_with_target/fishing/finish_action(datum/ai_controller/controller, succeeded, fishing_target_key, fishing_cooldown_key)
|
||||
. = ..()
|
||||
if(!succeeded)
|
||||
return
|
||||
var/cooldown = controller.blackboard[fishing_cooldown_key] || FISHING_COOLDOWN
|
||||
controller.set_blackboard_key(BB_FISHING_TIMER, world.time + cooldown)
|
||||
|
||||
#undef FISHING_COOLDOWN
|
||||
@@ -1,39 +0,0 @@
|
||||
/// 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/SelectBehaviors(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
|
||||
|
||||
/// Try to escape from your current target, without performing any other actions.
|
||||
/// Reads from some fleeing-specific targeting keys rather than the current mob target.
|
||||
/datum/ai_planning_subtree/flee_target/from_flee_key
|
||||
target_key = BB_BASIC_MOB_FLEE_TARGET
|
||||
hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION
|
||||
|
||||
/// A subtype that forces the mob to flee from targets with the scary fisherman trait anyway.
|
||||
/datum/ai_planning_subtree/flee_target/from_fisherman
|
||||
|
||||
/datum/ai_planning_subtree/flee_target/from_fisherman/should_flee(datum/ai_controller/controller, atom/flee_from)
|
||||
if (!QDELETED(flee_from) && HAS_TRAIT(flee_from, TRAIT_SCARY_FISHERMAN))
|
||||
return TRUE
|
||||
return ..()
|
||||
@@ -0,0 +1,2 @@
|
||||
/datum/bt_node/subtree/generic_hunger
|
||||
behavior_tree_json = "code/datums/ai/generic_hunger.bt.json"
|
||||
@@ -0,0 +1,2 @@
|
||||
/datum/bt_node/subtree/generic_play_instrument
|
||||
behavior_tree_json = "code/datums/ai/generic_play_instrument.bt.json"
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/go_for_swim",
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"repeat_secondary_delay": "1 SECONDS",
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_BOTH",
|
||||
"key": "BB_SWIM_ALTERNATE_TURF"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_SWIM_ALTERNATE_TURF",
|
||||
"required_dist": 0,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/set_bb_cooldown",
|
||||
"vars": {
|
||||
"cooldown_key": "BB_KEY_SWIMMER_COOLDOWN",
|
||||
"cooldown_duration": "30 SECONDS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/wait",
|
||||
"vars": {
|
||||
"duration": "30 SECONDS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
|
||||
"vars": {
|
||||
"target_key": "BB_SWIM_ALTERNATE_TURF",
|
||||
"targeting_strategy": "/datum/targeting_strategy/walkable_turf",
|
||||
"target_source": "/datum/target_source/oview_land_turfs"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_SWIM_ALTERNATE_TURF",
|
||||
"required_dist": 0,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/swim_splash"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,59 +1,15 @@
|
||||
#define DEFAULT_TIME_SWIMMER 30 SECONDS
|
||||
/// Wander between water and land, splashing about now and then.
|
||||
/datum/bt_node/subtree/go_for_swim
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/go_for_swim.bt.json"
|
||||
|
||||
///subtree to go and swim!
|
||||
/datum/ai_planning_subtree/go_for_swim
|
||||
|
||||
/datum/ai_planning_subtree/go_for_swim/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(controller.blackboard_key_exists(BB_SWIM_ALTERNATE_TURF))
|
||||
controller.queue_behavior(/datum/ai_behavior/travel_towards/swimming, BB_SWIM_ALTERNATE_TURF)
|
||||
|
||||
if(isnull(controller.blackboard[BB_KEY_SWIM_TIME]))
|
||||
controller.set_blackboard_key(BB_KEY_SWIM_TIME, DEFAULT_TIME_SWIMMER)
|
||||
/// Splashes about while standing in water.
|
||||
/datum/bt_node/ai_behavior/swim_splash
|
||||
|
||||
/datum/bt_node/ai_behavior/swim_splash/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/turf/our_turf = get_turf(living_pawn)
|
||||
|
||||
// we have been taken out of water!
|
||||
controller.set_blackboard_key(BB_CURRENTLY_SWIMMING, iswaterturf(our_turf))
|
||||
|
||||
if(controller.blackboard[BB_KEY_SWIM_TIME] < world.time)
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/swim_alternate, BB_SWIM_ALTERNATE_TURF, /turf/open)
|
||||
return
|
||||
|
||||
// have some fun in the water
|
||||
if(controller.blackboard[BB_CURRENTLY_SWIMMING] && SPT_PROB(5, seconds_per_tick))
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_emote, "splashes water all around!")
|
||||
|
||||
|
||||
///find land if its time to get out of water, otherwise find water
|
||||
/datum/ai_behavior/find_and_set/swim_alternate
|
||||
|
||||
/datum/ai_behavior/find_and_set/swim_alternate/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
if(QDELETED(living_pawn))
|
||||
return null
|
||||
var/look_for_land = controller.blackboard[BB_CURRENTLY_SWIMMING]
|
||||
var/list/possible_turfs = list()
|
||||
for(var/turf/possible_turf in oview(search_range, living_pawn))
|
||||
if(isclosedturf(possible_turf) || is_space_or_openspace(possible_turf))
|
||||
continue
|
||||
if(possible_turf.is_blocked_turf())
|
||||
continue
|
||||
if(look_for_land == iswaterturf(possible_turf))
|
||||
continue
|
||||
possible_turfs += possible_turf
|
||||
|
||||
if(!length(possible_turfs))
|
||||
return null
|
||||
|
||||
return(pick(possible_turfs))
|
||||
|
||||
/datum/ai_behavior/travel_towards/swimming
|
||||
clear_target = TRUE
|
||||
|
||||
/datum/ai_behavior/travel_towards/swimming/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
var/time_to_add = controller.blackboard[BB_KEY_SWIMMER_COOLDOWN] ? controller.blackboard[BB_KEY_SWIMMER_COOLDOWN] : DEFAULT_TIME_SWIMMER
|
||||
controller.set_blackboard_key(BB_KEY_SWIM_TIME, world.time + time_to_add )
|
||||
|
||||
#undef DEFAULT_TIME_SWIMMER
|
||||
if(!istype(living_pawn) || !iswaterturf(get_turf(living_pawn)))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(!SPT_PROB(5, seconds_per_tick))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
living_pawn.manual_emote("splashes water all around!")
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
/// Step away if too close, or towards if too far
|
||||
/datum/ai_planning_subtree/maintain_distance
|
||||
/// Blackboard key holding atom we want to stay away from
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
/// How far do we look for our target?
|
||||
var/view_distance = 10
|
||||
/// the run away behavior we will use
|
||||
var/run_away_behavior = /datum/ai_behavior/step_away
|
||||
|
||||
/datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
if(LAZYLEN(living_pawn.do_afters))
|
||||
return
|
||||
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if (!isliving(target) || !can_see(controller.pawn, target, view_distance))
|
||||
return // Don't run away from cucumbers, they're not snakes
|
||||
var/range = get_dist(controller.pawn, target)
|
||||
|
||||
var/minimum_distance = controller.blackboard[BB_RANGED_SKIRMISH_MIN_DISTANCE] || 4
|
||||
var/maximum_distance = controller.blackboard[BB_RANGED_SKIRMISH_MAX_DISTANCE] || 6
|
||||
|
||||
if (range < minimum_distance)
|
||||
controller.queue_behavior(run_away_behavior, target_key, minimum_distance)
|
||||
return
|
||||
if (range > maximum_distance)
|
||||
controller.queue_behavior(/datum/ai_behavior/pursue_to_range, target_key, maximum_distance)
|
||||
return
|
||||
|
||||
/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance
|
||||
run_away_behavior = /datum/ai_behavior/cover_minimum_distance
|
||||
|
||||
/// Take one step away
|
||||
/datum/ai_behavior/step_away
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
required_distance = 0
|
||||
action_cooldown = 0.2 SECONDS
|
||||
|
||||
/datum/ai_behavior/step_away/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/atom/current_target = controller.blackboard[target_key]
|
||||
if (QDELETED(current_target))
|
||||
return FALSE
|
||||
|
||||
var/mob/living/our_pawn = controller.pawn
|
||||
our_pawn.face_atom(current_target)
|
||||
|
||||
var/turf/next_step = get_step_away(controller.pawn, current_target)
|
||||
if (!isnull(next_step) && !next_step.is_blocked_turf(exclude_mobs = TRUE))
|
||||
set_movement_target(controller, target = next_step, new_movement = /datum/ai_movement/basic_avoidance/backstep)
|
||||
return TRUE
|
||||
|
||||
var/list/all_dirs = GLOB.alldirs.Copy()
|
||||
all_dirs -= get_dir(controller.pawn, next_step)
|
||||
all_dirs -= get_dir(controller.pawn, current_target)
|
||||
shuffle_inplace(all_dirs)
|
||||
|
||||
for (var/dir in all_dirs)
|
||||
next_step = get_step(controller.pawn, dir)
|
||||
if (!isnull(next_step) && !next_step.is_blocked_turf(exclude_mobs = TRUE))
|
||||
set_movement_target(controller, target = next_step, new_movement = /datum/ai_movement/basic_avoidance/backstep)
|
||||
return TRUE
|
||||
return FALSE
|
||||
|
||||
/datum/ai_behavior/step_away/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/step_away/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
controller.change_ai_movement_type(initial(controller.ai_movement))
|
||||
|
||||
/// Pursue a target until we are within a provided range
|
||||
/datum/ai_behavior/pursue_to_range
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_MOVE_AND_PERFORM
|
||||
|
||||
/datum/ai_behavior/pursue_to_range/setup(datum/ai_controller/controller, target_key, range)
|
||||
. = ..()
|
||||
var/atom/current_target = controller.blackboard[target_key]
|
||||
if (QDELETED(current_target))
|
||||
return FALSE
|
||||
if (get_dist(controller.pawn, current_target) <= range)
|
||||
return FALSE
|
||||
set_movement_target(controller, current_target)
|
||||
|
||||
/datum/ai_behavior/pursue_to_range/perform(seconds_per_tick, datum/ai_controller/controller, target_key, range)
|
||||
var/atom/current_target = controller.blackboard[target_key]
|
||||
if (!QDELETED(current_target) && get_dist(controller.pawn, current_target) > range)
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
///instead of taking a single step, we cover the entire distance
|
||||
/datum/ai_behavior/cover_minimum_distance
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
required_distance = 0
|
||||
action_cooldown = 0.2 SECONDS
|
||||
|
||||
/datum/ai_behavior/cover_minimum_distance/setup(datum/ai_controller/controller, target_key, minimum_distance)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
var/required_distance = minimum_distance - get_dist(controller.pawn, target) //the distance we need to move
|
||||
var/distance = 0
|
||||
var/turf/chosen_turf
|
||||
for(var/turf/open/potential_turf in oview(required_distance, controller.pawn))
|
||||
var/new_distance_from_target = get_dist(potential_turf, target)
|
||||
if(potential_turf.is_blocked_turf())
|
||||
continue
|
||||
if(new_distance_from_target > distance)
|
||||
chosen_turf = potential_turf
|
||||
distance = new_distance_from_target
|
||||
if(isnull(chosen_turf))
|
||||
return FALSE
|
||||
set_movement_target(controller, target = chosen_turf)
|
||||
|
||||
/datum/ai_behavior/cover_minimum_distance/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -1,68 +0,0 @@
|
||||
//behavior to find mineable mineral walls
|
||||
|
||||
/datum/ai_planning_subtree/mine_walls
|
||||
var/find_wall_behavior = /datum/ai_behavior/find_mineral_wall
|
||||
|
||||
/datum/ai_planning_subtree/mine_walls/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(controller.blackboard_key_exists(BB_TARGET_MINERAL_WALL))
|
||||
controller.queue_behavior(/datum/ai_behavior/mine_wall, BB_TARGET_MINERAL_WALL)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
controller.queue_behavior(find_wall_behavior, BB_TARGET_MINERAL_WALL)
|
||||
|
||||
/datum/ai_behavior/mine_wall
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
action_cooldown = 15 SECONDS
|
||||
|
||||
/datum/ai_behavior/mine_wall/setup(datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/turf/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
set_movement_target(controller, target)
|
||||
|
||||
/datum/ai_behavior/mine_wall/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
. = ..()
|
||||
var/mob/living/basic/living_pawn = controller.pawn
|
||||
var/turf/closed/mineral/target = controller.blackboard[target_key]
|
||||
var/is_gibtonite_turf = istype(target, /turf/closed/mineral/gibtonite)
|
||||
if(!controller.ai_interact(target = target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
if(is_gibtonite_turf)
|
||||
living_pawn.manual_emote("sighs...") //accept whats about to happen to us
|
||||
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/mine_wall/finish_action(datum/ai_controller/controller, success, target_key)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
|
||||
/datum/ai_behavior/find_mineral_wall
|
||||
|
||||
/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key)
|
||||
var/mob/living_pawn = controller.pawn
|
||||
|
||||
for(var/turf/closed/mineral/potential_wall in oview(9, living_pawn))
|
||||
if(!check_if_mineable(controller, potential_wall)) //check if its surrounded by walls
|
||||
continue
|
||||
controller.set_blackboard_key(found_wall_key, potential_wall) //closest wall first!
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall)
|
||||
var/mob/living/source = controller.pawn
|
||||
var/direction_to_turf = get_dir(target_wall, source)
|
||||
if(!ISDIAGONALDIR(direction_to_turf))
|
||||
return TRUE
|
||||
var/list/directions_to_check = list()
|
||||
for(var/direction_check in GLOB.cardinals)
|
||||
if(direction_check & direction_to_turf)
|
||||
directions_to_check += direction_check
|
||||
|
||||
for(var/direction in directions_to_check)
|
||||
var/turf/test_turf = get_step(target_wall, direction)
|
||||
if(isnull(test_turf))
|
||||
continue
|
||||
if(!test_turf.is_blocked_turf(ignore_atoms = list(source)))
|
||||
return TRUE
|
||||
return FALSE
|
||||
@@ -1,69 +1,64 @@
|
||||
/// Try to line up with a cardinal direction of your target
|
||||
/datum/ai_planning_subtree/move_to_cardinal
|
||||
/// Behaviour to execute to line ourselves up
|
||||
var/move_behaviour = /datum/ai_behavior/move_to_cardinal
|
||||
/// Blackboard key in which to store selected target
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
|
||||
/datum/ai_planning_subtree/move_to_cardinal/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
if(!controller.blackboard_key_exists(target_key))
|
||||
return
|
||||
controller.queue_behavior(move_behaviour, target_key)
|
||||
|
||||
/// Try to line up with a cardinal direction of your target
|
||||
/datum/ai_behavior/move_to_cardinal
|
||||
required_distance = 0
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
/// How close to our target is too close?
|
||||
/// Moves to line up with the target along a cardinal direction, so a directional ability can fire down the lane.
|
||||
/// Reports SUCCESS once lined up and within range, FAILURE when the target is gone, too far, or pathing gives up.
|
||||
/datum/bt_node/ai_behavior/move_to_cardinal
|
||||
time_between_perform = 0
|
||||
/// Blackboard key holding the atom to line up with.
|
||||
var/target_key = BB_CURRENT_TARGET
|
||||
/// How close to our target is too close.
|
||||
var/minimum_distance = 1
|
||||
/// How far away is too far?
|
||||
/// How far away is too far.
|
||||
var/maximum_distance = 9
|
||||
/// The cardinal tile of our target we are currently moving toward.
|
||||
var/atom/destination
|
||||
/// Set by on_movement_failed() when the movement system gives up pathing.
|
||||
var/movement_failed = FALSE
|
||||
|
||||
/datum/ai_behavior/move_to_cardinal/setup(datum/ai_controller/controller, target_key)
|
||||
/datum/bt_node/ai_behavior/move_to_cardinal/setup(datum/ai_controller/controller)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return FALSE
|
||||
target_nearest_cardinal(controller, target)
|
||||
RegisterSignal(controller.pawn, COMSIG_MOB_AI_MOVEMENT_FAILED, PROC_REF(on_movement_failed))
|
||||
move_towards_nearest_cardinal(controller, target)
|
||||
return TRUE
|
||||
|
||||
/// Set our movement target to the closest cardinal space to our target
|
||||
/datum/ai_behavior/move_to_cardinal/proc/target_nearest_cardinal(datum/ai_controller/controller, atom/target)
|
||||
/datum/bt_node/ai_behavior/move_to_cardinal/proc/on_movement_failed(atom/source)
|
||||
SIGNAL_HANDLER
|
||||
movement_failed = TRUE
|
||||
|
||||
/// Begin moving toward the closest unblocked cardinal tile of our target.
|
||||
/datum/bt_node/ai_behavior/move_to_cardinal/proc/move_towards_nearest_cardinal(datum/ai_controller/controller, atom/target)
|
||||
var/atom/move_target
|
||||
var/closest = INFINITY
|
||||
|
||||
for (var/dir in GLOB.cardinals)
|
||||
for(var/dir in GLOB.cardinals)
|
||||
var/turf/cardinal_turf = get_ranged_target_turf(target, dir, minimum_distance)
|
||||
if (cardinal_turf.is_blocked_turf())
|
||||
if(cardinal_turf.is_blocked_turf())
|
||||
continue
|
||||
var/distance_to = get_dist(controller.pawn, cardinal_turf)
|
||||
if (distance_to >= closest)
|
||||
if(distance_to >= closest)
|
||||
continue
|
||||
closest = distance_to
|
||||
move_target = cardinal_turf
|
||||
|
||||
if (isnull(move_target))
|
||||
if(isnull(move_target))
|
||||
move_target = target
|
||||
if (controller.current_movement_target == move_target)
|
||||
return
|
||||
set_movement_target(controller, move_target)
|
||||
controller.ai_movement.start_moving_towards(controller, move_target, 0)
|
||||
destination = move_target
|
||||
|
||||
/datum/ai_behavior/move_to_cardinal/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if (QDELETED(target))
|
||||
/datum/bt_node/ai_behavior/move_to_cardinal/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
if(movement_failed)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if(QDELETED(target))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if (!(get_dir(controller.pawn, target) in GLOB.cardinals))
|
||||
target_nearest_cardinal(controller, target)
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
var/distance_to_target = get_dist(controller.pawn, target)
|
||||
if (distance_to_target < minimum_distance)
|
||||
target_nearest_cardinal(controller, target)
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
if (distance_to_target > maximum_distance)
|
||||
if(distance_to_target > maximum_distance)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(!(get_dir(controller.pawn, target) in GLOB.cardinals) || distance_to_target < minimum_distance)
|
||||
move_towards_nearest_cardinal(controller, target)
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/move_to_cardinal/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
if (!succeeded)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
/datum/bt_node/ai_behavior/move_to_cardinal/finish_action(datum/ai_controller/controller, succeeded)
|
||||
UnregisterSignal(controller.pawn, COMSIG_MOB_AI_MOVEMENT_FAILED)
|
||||
movement_failed = FALSE
|
||||
controller.ai_movement.stop_moving_towards(controller)
|
||||
return ..()
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/// Opportunistically searches for and hides/scurries through vents.
|
||||
/datum/ai_planning_subtree/opportunistic_ventcrawler
|
||||
|
||||
/datum/ai_planning_subtree/opportunistic_ventcrawler/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(HAS_TRAIT(controller.pawn, TRAIT_MOVE_VENTCRAWLING))
|
||||
return SUBTREE_RETURN_FINISH_PLANNING // hold on let me cook
|
||||
|
||||
var/obj/machinery/atmospherics/components/unary/vent_pump/target = controller.blackboard[BB_ENTRY_VENT_TARGET]
|
||||
|
||||
if(QDELETED(target))
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_ENTRY_VENT_TARGET, /obj/machinery/atmospherics/components/unary/vent_pump) // keep looking otherwise they KILL US AND WE DIE
|
||||
return
|
||||
|
||||
if(get_turf(controller.pawn) != get_turf(target))
|
||||
controller.queue_behavior(/datum/ai_behavior/travel_towards, BB_ENTRY_VENT_TARGET)
|
||||
return
|
||||
|
||||
controller.set_blackboard_key(BB_CURRENTLY_TARGETING_VENT, TRUE)
|
||||
controller.queue_behavior(/datum/ai_behavior/crawl_through_vents, BB_ENTRY_VENT_TARGET)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING // we are going into this vent... no distractions
|
||||
@@ -1,21 +0,0 @@
|
||||
/datum/ai_planning_subtree/find_and_hunt_target/play_with_owner
|
||||
target_key = BB_OWNER_TARGET
|
||||
hunting_behavior = /datum/ai_behavior/hunt_target/play_with_owner
|
||||
finding_behavior = /datum/ai_behavior/find_hunt_target/find_owner
|
||||
hunt_targets = list(/mob/living)
|
||||
hunt_chance = 80
|
||||
hunt_range = 9
|
||||
|
||||
/datum/ai_behavior/find_hunt_target/find_owner
|
||||
action_cooldown = 1 MINUTES
|
||||
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
|
||||
|
||||
/datum/ai_behavior/find_hunt_target/find_owner/valid_dinner(mob/living/source, atom/friend, radius, datum/ai_controller/controller, seconds_per_tick)
|
||||
return (friend != source) && (source.has_ally(friend)) && can_see(source, friend, radius)
|
||||
|
||||
/datum/ai_behavior/hunt_target/play_with_owner
|
||||
|
||||
/datum/ai_behavior/hunt_target/play_with_owner/target_caught(mob/living/hunter, atom/hunted)
|
||||
var/list/interactions_list = hunter.ai_controller.blackboard[BB_INTERACTIONS_WITH_OWNER]
|
||||
var/interaction_message = length(interactions_list) ? pick(interactions_list) : "Plays with"
|
||||
hunter.manual_emote("[interaction_message] [hunted]!")
|
||||
@@ -1,23 +0,0 @@
|
||||
|
||||
///Subtree that checks if we are on the target atom's tile, and sets it as a travel target if not
|
||||
///The target is taken from the blackboard. This one always requires a specific implementation.
|
||||
/datum/ai_planning_subtree/prepare_travel_to_destination
|
||||
var/target_key
|
||||
var/travel_destination_key = BB_TRAVEL_DESTINATION
|
||||
|
||||
/datum/ai_planning_subtree/prepare_travel_to_destination/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
|
||||
//Target is deleted, or we are already standing on it
|
||||
if(QDELETED(target) || (isturf(target) && controller.pawn.loc == target) || (target.loc == controller.pawn.loc))
|
||||
return
|
||||
|
||||
//Already set with this value, return
|
||||
if(controller.blackboard[target_key] == controller.blackboard[travel_destination_key])
|
||||
return
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/set_travel_destination, target_key, travel_destination_key)
|
||||
return //continue planning regardless of success
|
||||
|
||||
/datum/ai_planning_subtree/prepare_travel_to_destination/trader
|
||||
target_key = BB_SHOP_SPOT
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/random_speech_loop",
|
||||
"bindings": {
|
||||
"bqdqne64": {
|
||||
"label": "speech_behavior",
|
||||
"default": "/datum/bt_node/ai_behavior/random_speech_blackboard"
|
||||
}
|
||||
},
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"loop_delay": "1 SECONDS",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "$bqdqne64"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/datum/bt_node/subtree/random_speech_loop
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/random_speech_loop.bt.json"
|
||||
@@ -1,50 +0,0 @@
|
||||
/// Fire a ranged attack without interrupting movement.
|
||||
/datum/ai_planning_subtree/ranged_skirmish
|
||||
operational_datums = list(/datum/component/ranged_attacks)
|
||||
/// Blackboard key holding target atom
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
/// What AI behaviour do we actually run?
|
||||
var/attack_behavior = /datum/ai_behavior/ranged_skirmish
|
||||
/// If target is further away than this we don't fire
|
||||
var/max_range = 9
|
||||
/// If target is closer than this we don't fire
|
||||
var/min_range = 2
|
||||
|
||||
/datum/ai_planning_subtree/ranged_skirmish/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
if(!controller.blackboard_key_exists(target_key))
|
||||
return
|
||||
controller.queue_behavior(attack_behavior, target_key, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION, max_range, min_range)
|
||||
|
||||
/// How often will we try to perform our ranged attack?
|
||||
/datum/ai_behavior/ranged_skirmish
|
||||
action_cooldown = 0.5 SECONDS
|
||||
|
||||
/datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key]
|
||||
return !QDELETED(target)
|
||||
|
||||
/datum/ai_behavior/ranged_skirmish/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range)
|
||||
var/atom/target = controller.blackboard[target_key]
|
||||
if (QDELETED(target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
|
||||
if(!targeting_strategy.can_attack(controller.pawn, target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/hiding_target = targeting_strategy.find_hidden_mobs(controller.pawn, target)
|
||||
controller.set_blackboard_key(hiding_location_key, hiding_target)
|
||||
|
||||
target = hiding_target || target
|
||||
|
||||
var/distance = get_dist(controller.pawn, target)
|
||||
if (distance > max_range || distance < min_range)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
controller.ai_interact(target = target, combat_mode = TRUE)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_planning_subtree/ranged_skirmish/no_minimum
|
||||
min_range = 0
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/run_away_from_target",
|
||||
"bindings": {
|
||||
"byk9gqj4": {
|
||||
"label": "target_key",
|
||||
"default": "BB_CURRENT_TARGET"
|
||||
}
|
||||
},
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_BASIC_MOB_STOP_FLEEING",
|
||||
"invert": true,
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/find_flee_location",
|
||||
"vars": {
|
||||
"target_key": "$byk9gqj4",
|
||||
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION",
|
||||
"destination_key": "BB_FLEE_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_FLEE_LOCATION",
|
||||
"required_dist": 0,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/// Flee from BB_CURRENT_TARGET, gated by BB_BASIC_MOB_STOP_FLEEING.
|
||||
/// Computes a flee waypoint each loop via find_flee_location then moves to it.
|
||||
/datum/bt_node/subtree/run_away_from_target
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/run_away_from_target.bt.json"
|
||||
|
||||
/// Flee variant that fires ranged attacks at the current target while moving.
|
||||
/datum/bt_node/subtree/run_away_from_target/run_and_shoot
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/run_away_from_target_run_and_shoot.bt.json"
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/run_away_from_target/run_and_shoot",
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_BASIC_MOB_STOP_FLEEING",
|
||||
"invert": true,
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/find_flee_location",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET",
|
||||
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION",
|
||||
"destination_key": "BB_FLEE_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_ranged_attack",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET",
|
||||
"targeting_strategy": "BB_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_FLEE_LOCATION",
|
||||
"required_dist": 0,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/// Intermittently run an emote
|
||||
/datum/ai_planning_subtree/run_emote
|
||||
var/emote_key = BB_EMOTE_KEY
|
||||
var/emote_chance_key = BB_EMOTE_CHANCE
|
||||
|
||||
/datum/ai_planning_subtree/run_emote/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/emote_chance = controller.blackboard[emote_chance_key] || 0
|
||||
if (!SPT_PROB(emote_chance, seconds_per_tick))
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/run_emote, emote_key)
|
||||
|
||||
/// Emote from a blackboard key
|
||||
/datum/ai_behavior/run_emote
|
||||
|
||||
/datum/ai_behavior/run_emote/perform(seconds_per_tick, datum/ai_controller/controller, emote_key)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
if (!isliving(living_pawn))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/list/emote_list = controller.blackboard[emote_key]
|
||||
var/emote
|
||||
if (islist(emote_list))
|
||||
emote = length(emote_list) ? pick(emote_list) : null
|
||||
else
|
||||
emote = emote_list
|
||||
|
||||
if(isnull(emote))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
living_pawn.emote(emote)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -1,41 +0,0 @@
|
||||
/// Shapeshift when we have no target, until someone has been nearby for long enough
|
||||
/datum/ai_planning_subtree/shapechange_ambush
|
||||
operational_datums = list(/datum/component/ai_target_timer)
|
||||
/// Key where we keep our ability
|
||||
var/ability_key = BB_SHAPESHIFT_ACTION
|
||||
/// Key where we keep our target
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
/// How long to lull our target into a false sense of security
|
||||
var/minimum_target_time = 8 SECONDS
|
||||
|
||||
/datum/ai_planning_subtree/shapechange_ambush/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/mob/living/living_pawn = controller.pawn
|
||||
var/is_shifted = ismob(living_pawn.loc)
|
||||
var/has_target = controller.blackboard_key_exists(target_key)
|
||||
var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
|
||||
|
||||
if (!is_shifted)
|
||||
if (has_target)
|
||||
return // We're busy
|
||||
|
||||
if (using_action?.IsAvailable())
|
||||
controller.queue_behavior(/datum/ai_behavior/use_mob_ability/shapeshift, BB_SHAPESHIFT_ACTION) // Shift
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if (!has_target || !using_action?.IsAvailable())
|
||||
return SUBTREE_RETURN_FINISH_PLANNING // Lie in wait
|
||||
var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
|
||||
if (time_on_target < minimum_target_time)
|
||||
return // Wait a bit longer
|
||||
controller.queue_behavior(/datum/ai_behavior/use_mob_ability/shapeshift, BB_SHAPESHIFT_ACTION) // Surprise!
|
||||
|
||||
/// Selects a random shapeshift ability before shifting
|
||||
/datum/ai_behavior/use_mob_ability/shapeshift
|
||||
|
||||
/datum/ai_behavior/use_mob_ability/shapeshift/setup(datum/ai_controller/controller, ability_key)
|
||||
var/datum/action/cooldown/spell/shapeshift/using_action = controller.blackboard[ability_key]
|
||||
if (!using_action?.IsAvailable())
|
||||
return FALSE
|
||||
if (isnull(using_action.shapeshift_type)) // If we don't have a shape then pick one, AI can't use context wheels
|
||||
using_action.shapeshift_type = pick(using_action.possible_shapes)
|
||||
return ..()
|
||||
@@ -1,33 +0,0 @@
|
||||
/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/SelectBehaviors(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.
|
||||
|
||||
/datum/ai_planning_subtree/basic_ranged_attack_subtree
|
||||
operational_datums = list(/datum/component/ranged_attacks)
|
||||
var/datum/ai_behavior/basic_ranged_attack/ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack
|
||||
|
||||
/datum/ai_planning_subtree/basic_ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
|
||||
return
|
||||
controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions.
|
||||
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree/no_fisherman
|
||||
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree/no_fisherman/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/atom/movable/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
|
||||
if(QDELETED(target))
|
||||
return ..()
|
||||
if(!HAS_TRAIT(target, TRAIT_SCARY_FISHERMAN))
|
||||
return ..()
|
||||
@@ -1,25 +0,0 @@
|
||||
/// 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/SelectBehaviors(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/SelectBehaviors(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
|
||||
@@ -1,26 +0,0 @@
|
||||
/datum/ai_planning_subtree/simple_find_target
|
||||
/// Variable to store target in
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
/// Targeting strategy key to use
|
||||
var/strategy_key = BB_TARGETING_STRATEGY
|
||||
/// Behavior to use to find targets
|
||||
var/target_behavior = /datum/ai_behavior/find_potential_targets
|
||||
|
||||
/datum/ai_planning_subtree/simple_find_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
controller.queue_behavior(target_behavior, target_key, strategy_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
|
||||
|
||||
// Prevents finding a target if a human is nearby
|
||||
/datum/ai_planning_subtree/simple_find_target/not_while_observed
|
||||
|
||||
/datum/ai_planning_subtree/simple_find_target/not_while_observed/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
for(var/mob/living/carbon/human/watcher in hearers(7, controller.pawn))
|
||||
if(watcher.stat != DEAD)
|
||||
return
|
||||
return ..()
|
||||
|
||||
/datum/ai_planning_subtree/simple_find_target/to_flee
|
||||
target_key = BB_BASIC_MOB_FLEE_TARGET
|
||||
|
||||
/datum/ai_planning_subtree/simple_find_target/hunt
|
||||
strategy_key = BB_HUNT_TARGETING_STRATEGY
|
||||
@@ -1,6 +0,0 @@
|
||||
/// Selects the most wounded potential target that we can see
|
||||
/datum/ai_planning_subtree/simple_find_wounded_target
|
||||
|
||||
/datum/ai_planning_subtree/simple_find_wounded_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
controller.queue_behavior(/datum/ai_behavior/find_potential_targets/most_wounded, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/skittish_brawler_combat",
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"repeat_secondary_delay": "BASIC_MOB_FIND_TARGET_RATE",
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_TARGET",
|
||||
"observer_abort": "BT_ABORT_SELF"
|
||||
},
|
||||
"child": {
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/is_at_distance",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET",
|
||||
"maximum_distance": "DEFAULT_BASIC_FLEE_DISTANCE"
|
||||
},
|
||||
"child": {
|
||||
"type": "subtree",
|
||||
"subtype": "/datum/bt_node/subtree/run_away_from_target"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_melee_attack",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET",
|
||||
"targeting_strategy": "BB_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "subtree",
|
||||
"subtype": "/datum/bt_node/subtree/random_walk"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/acquire_target/target_from_retaliate_list/nearest",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET",
|
||||
"targeting_strategy": "BB_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/// Cowardly brawler combat: keep our distance from attackers, fleeing if they get close but
|
||||
/// turning to bite if they hang back just out of reach.
|
||||
/datum/bt_node/subtree/skittish_brawler_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/skittish_brawler_combat.bt.json"
|
||||
@@ -1,35 +0,0 @@
|
||||
/// Disables AI after a certain amount of time spent with no target, you will have to enable the AI again somewhere else
|
||||
/datum/ai_planning_subtree/sleep_with_no_target
|
||||
/// Behaviour to execute when sleeping
|
||||
var/sleep_behaviour = /datum/ai_behavior/sleep_after_targetless_time
|
||||
/// Target key to interrogate
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
|
||||
/datum/ai_planning_subtree/sleep_with_no_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
controller.queue_behavior(sleep_behaviour, BB_BASIC_MOB_CURRENT_TARGET)
|
||||
|
||||
/// Disables AI after a certain amount of time spent with no target, you will have to enable the AI again somewhere else
|
||||
/datum/ai_behavior/sleep_after_targetless_time
|
||||
/// Turn off AI if we spend this many seconds without a target
|
||||
var/time_to_wait = 10 SECONDS
|
||||
|
||||
/datum/ai_behavior/sleep_after_targetless_time/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
|
||||
return (controller.blackboard_key_exists(target_key)) ? ( AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED) : ( AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED)
|
||||
|
||||
/datum/ai_behavior/sleep_after_targetless_time/finish_action(datum/ai_controller/controller, succeeded, target_key)
|
||||
. = ..()
|
||||
if (!succeeded)
|
||||
controller.clear_blackboard_key(BB_TARGETLESS_TIME)
|
||||
return
|
||||
|
||||
if (isnull(controller.blackboard[BB_TARGETLESS_TIME]))
|
||||
controller.set_blackboard_key(BB_TARGETLESS_TIME, world.time + time_to_wait)
|
||||
|
||||
if (controller.blackboard[BB_TARGETLESS_TIME] < world.time)
|
||||
enter_sleep(controller)
|
||||
controller.clear_blackboard_key(BB_TARGETLESS_TIME)
|
||||
|
||||
/// Disables AI, override to do additional things or something else
|
||||
/datum/ai_behavior/sleep_after_targetless_time/proc/enter_sleep(datum/ai_controller/controller)
|
||||
controller.set_ai_status(AI_STATUS_OFF)
|
||||
@@ -1,242 +0,0 @@
|
||||
/datum/ai_planning_subtree/random_speech
|
||||
//The chance of an emote occurring each second
|
||||
var/speech_chance = 0
|
||||
///Hearable emotes
|
||||
var/list/emote_hear
|
||||
///Unlike speak_emote, the list of things in this variable only show by themselves with no spoken text. IE: Ian barks, Ian yaps
|
||||
var/list/emote_see
|
||||
///Possible lines of speech the AI can have
|
||||
var/list/speak
|
||||
///The sound effects associated with this speech, if any
|
||||
var/list/sound
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/New()
|
||||
. = ..()
|
||||
if(LAZYLEN(speak))
|
||||
speak = string_list(speak)
|
||||
if(LAZYLEN(sound))
|
||||
sound = string_list(sound)
|
||||
if(LAZYLEN(emote_hear))
|
||||
emote_hear = string_list(emote_hear)
|
||||
if(LAZYLEN(emote_see))
|
||||
emote_see = string_list(emote_see)
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(!SPT_PROB(speech_chance, seconds_per_tick))
|
||||
return
|
||||
speak(controller)
|
||||
|
||||
/// Actually perform an action
|
||||
/datum/ai_planning_subtree/random_speech/proc/speak(datum/ai_controller/controller)
|
||||
var/audible_emotes_length = emote_hear?.len
|
||||
var/non_audible_emotes_length = emote_see?.len
|
||||
var/speak_lines_length = speak?.len
|
||||
|
||||
var/total_choices_length = audible_emotes_length + non_audible_emotes_length + speak_lines_length
|
||||
|
||||
if (total_choices_length == 0)
|
||||
return
|
||||
|
||||
var/random_number_in_range = rand(1, total_choices_length)
|
||||
var/sound_to_play = length(sound) > 0 ? pick(sound) : null
|
||||
|
||||
if(random_number_in_range <= audible_emotes_length)
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_hear), sound_to_play)
|
||||
else if(random_number_in_range <= (audible_emotes_length + non_audible_emotes_length))
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_emote, pick(emote_see))
|
||||
else
|
||||
controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play)
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/insect
|
||||
speech_chance = 5
|
||||
sound = list('sound/mobs/non-humanoids/insect/chitter.ogg')
|
||||
emote_hear = list("chitters.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/mothroach
|
||||
speech_chance = 15
|
||||
emote_hear = list("flutters.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/mouse
|
||||
speech_chance = 1
|
||||
speak = list("Squeak!", "SQUEAK!", "Squeak?")
|
||||
sound = list('sound/mobs/non-humanoids/mouse/mousesqueek.ogg')
|
||||
emote_hear = list("squeaks.")
|
||||
emote_see = list("runs in a circle.", "shakes.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/frog
|
||||
speech_chance = 3
|
||||
emote_see = list("jumps in a circle.", "shakes.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/lizard // all of these have to be three words long or i'm killing you. you're dead.
|
||||
speech_chance = 3
|
||||
emote_hear = list("stamps around some.", "hisses a bit.")
|
||||
emote_see = list("blehs the tongue.", "tilts the head.", "does a spin.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/sheep
|
||||
speech_chance = 5
|
||||
speak = list("baaa","baaaAAAAAH!","baaah")
|
||||
sound = list('sound/mobs/non-humanoids/sheep/sheep1.ogg', 'sound/mobs/non-humanoids/sheep/sheep2.ogg', 'sound/mobs/non-humanoids/sheep/sheep3.ogg')
|
||||
emote_hear = list("bleats.")
|
||||
emote_see = list("shakes her head.", "stares into the distance.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/rabbit
|
||||
speech_chance = 10
|
||||
speak = list("Mrrp.", "CHIRP!", "Mrrp?") // rabbits make some weird noises dude i don't know what to tell you
|
||||
emote_hear = list("hops.")
|
||||
emote_see = list("hops around.", "bounces up and down.")
|
||||
|
||||
/// For the easter subvariant of rabbits, these ones actually speak catchphrases.
|
||||
/datum/ai_planning_subtree/random_speech/rabbit/easter
|
||||
speak = list(
|
||||
"Hop into Easter!",
|
||||
"Come get your eggs!",
|
||||
"Prizes for everyone!",
|
||||
)
|
||||
|
||||
/// These ones have a space mask on, so their catchphrases are muffled.
|
||||
/datum/ai_planning_subtree/random_speech/rabbit/easter/space
|
||||
speak = list(
|
||||
"Hmph mmph mmmph!",
|
||||
"Mmphe mmphe mmphe!",
|
||||
"Hmm mmm mmm!",
|
||||
)
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/chicken
|
||||
speech_chance = 15 // really talkative ladies
|
||||
speak = list("Cluck!", "BWAAAAARK BWAK BWAK BWAK!", "Bwaak bwak.")
|
||||
sound = list('sound/mobs/non-humanoids/chicken/clucks.ogg', 'sound/mobs/non-humanoids/chicken/bagawk.ogg')
|
||||
emote_hear = list("clucks.", "croons.")
|
||||
emote_see = list("pecks at the ground.","flaps her wings viciously.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/chick
|
||||
speech_chance = 4
|
||||
speak = list("Cherp.", "Cherp?", "Chirrup.", "Cheep!")
|
||||
sound = list('sound/mobs/non-humanoids/chicken/chick_peep.ogg')
|
||||
emote_hear = list("cheeps.")
|
||||
emote_see = list("pecks at the ground.","flaps her tiny wings.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/cow
|
||||
speech_chance = 1
|
||||
speak = list("moo?","moo","MOOOOOO")
|
||||
sound = list('sound/mobs/non-humanoids/cow/cow.ogg')
|
||||
emote_hear = list("brays.")
|
||||
emote_see = list("shakes her head.")
|
||||
|
||||
///unlike normal cows, wisdom cows speak of wisdom and won't shut the fuck up
|
||||
/datum/ai_planning_subtree/random_speech/cow/wisdom
|
||||
speech_chance = 15
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/cow/wisdom/New()
|
||||
. = ..()
|
||||
speak = GLOB.wisdoms //Done here so it's setup properly
|
||||
sound = list()
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/deer
|
||||
speech_chance = 1
|
||||
speak = list("Weeeeeeee?", "Weeee", "WEOOOOOOOOOO")
|
||||
emote_hear = list("brays.")
|
||||
emote_see = list("shakes her head.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/dog
|
||||
speech_chance = 1
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/dog/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(!isdog(controller.pawn))
|
||||
return
|
||||
|
||||
// Stay in sync with dog fashion.
|
||||
var/mob/living/basic/pet/dog/dog_pawn = controller.pawn
|
||||
dog_pawn.update_dog_speech(src)
|
||||
|
||||
return ..()
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/faithless
|
||||
speech_chance = 1
|
||||
emote_see = list("wails.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/garden_gnome
|
||||
speech_chance = 5
|
||||
speak = list("Gnot a gnelf!", "Gnot a gnoblin!", "Howdy chum!")
|
||||
emote_hear = list("snores.", "burps.")
|
||||
emote_see = list("blinks.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/tree
|
||||
speech_chance = 3
|
||||
emote_see = list("photosynthesizes angrily.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/pig
|
||||
speech_chance = 3
|
||||
speak = list("oink?","oink","snurf")
|
||||
sound = list('sound/mobs/non-humanoids/pig/pig1.ogg', 'sound/mobs/non-humanoids/pig/pig2.ogg')
|
||||
emote_hear = list("snorts.")
|
||||
emote_see = list("sniffs around.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/pony
|
||||
speech_chance = 3
|
||||
sound = list('sound/mobs/non-humanoids/pony/whinny01.ogg', 'sound/mobs/non-humanoids/pony/whinny02.ogg', 'sound/mobs/non-humanoids/pony/whinny03.ogg')
|
||||
emote_hear = list("whinnies!")
|
||||
emote_see = list("horses around.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/pony/tamed
|
||||
speech_chance = 3
|
||||
sound = list('sound/mobs/non-humanoids/pony/snort.ogg')
|
||||
emote_hear = list("snorts.")
|
||||
emote_see = list("snorts.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/killer_tomato
|
||||
speech_chance = 3
|
||||
emote_hear = list("gnashes.", "growls lowly.", "snarls.")
|
||||
emote_see = list("salivates.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/ant
|
||||
speech_chance = 1
|
||||
speak = list("BZZZZT!", "CHTCHTCHT!", "Bzzz", "ChtChtCht")
|
||||
sound = list('sound/mobs/non-humanoids/insect/chitter.ogg')
|
||||
emote_hear = list("buzzes.", "clacks.")
|
||||
emote_see = list("shakes their head.", "twitches their antennae.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/fox
|
||||
speech_chance = 1
|
||||
speak = list("Ack-Ack", "Ack-Ack-Ack-Ackawoooo", "Geckers", "Awoo", "Tchoff")
|
||||
emote_hear = list("howls.", "barks.", "screams.")
|
||||
emote_see = list("shakes their head.", "shivers.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/crab
|
||||
speech_chance = 1
|
||||
sound = list('sound/mobs/non-humanoids/crab/claw_click.ogg')
|
||||
emote_hear = list("clicks.")
|
||||
emote_see = list("clacks.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/penguin
|
||||
speech_chance = 5
|
||||
speak = list("Gah Gah!", "NOOT NOOT!", "NOOT!", "Noot", "noot", "Prah!", "Grah!")
|
||||
emote_hear = list("squawks", "gakkers")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/bear
|
||||
speech_chance = 5
|
||||
emote_hear = list("rawrs.","grumbles.","grawls.", "stomps!")
|
||||
emote_see = list("stares ferociously.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/cats
|
||||
speech_chance = 10
|
||||
sound = list(SFX_CAT_MEOW)
|
||||
emote_hear = list("meows.")
|
||||
emote_see = list("meows.")
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/blackboard //literal tower of babel, subtree form
|
||||
speech_chance = 1
|
||||
|
||||
/datum/ai_planning_subtree/random_speech/blackboard/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/list/speech_lines = controller.blackboard[BB_BASIC_MOB_SPEAK_LINES]
|
||||
if(isnull(speech_lines))
|
||||
return ..()
|
||||
|
||||
// Note to future developers: this behaviour a singleton so this probably doesn't work as you would expect
|
||||
// The whole speech tree really needs to be refactored because this isn't how we use AI data these days
|
||||
speak = speech_lines[BB_EMOTE_SAY] || list()
|
||||
emote_see = speech_lines[BB_EMOTE_SEE] || list()
|
||||
emote_hear = speech_lines[BB_EMOTE_HEAR] || list()
|
||||
sound = speech_lines[BB_EMOTE_SOUND] || list()
|
||||
speech_chance = speech_lines[BB_SPEAK_CHANCE] ? speech_lines[BB_SPEAK_CHANCE] : initial(speech_chance)
|
||||
|
||||
return ..()
|
||||
@@ -1,12 +0,0 @@
|
||||
/// Locate a thing (practically any atom) to stop and stare at.
|
||||
/datum/ai_planning_subtree/stare_at_thing
|
||||
|
||||
/datum/ai_planning_subtree/stare_at_thing/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/atom/target = controller.blackboard[BB_STATIONARY_CAUSE]
|
||||
|
||||
if(isnull(target)) // No target? Time to locate one using the list we set in this mob's blackboard.
|
||||
var/list/potential_scares = controller.blackboard[BB_STATIONARY_TARGETS]
|
||||
controller.queue_behavior(/datum/ai_behavior/find_and_set/in_list, BB_STATIONARY_CAUSE, potential_scares)
|
||||
return
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/stop_and_stare, BB_STATIONARY_CAUSE)
|
||||
@@ -1,94 +0,0 @@
|
||||
/// 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
|
||||
/// Behavior to use to select our target
|
||||
var/target_behavior = /datum/ai_behavior/target_from_retaliate_list
|
||||
|
||||
/datum/ai_planning_subtree/target_retaliate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
controller.queue_behavior(target_behavior, 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]
|
||||
|
||||
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)
|
||||
|
||||
if (!QDELETED(existing_target) && 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
|
||||
// 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))
|
||||
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)
|
||||
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)
|
||||
. = ..()
|
||||
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)
|
||||
@@ -1,34 +0,0 @@
|
||||
/// Attempts to use a mob ability on a target
|
||||
/datum/ai_planning_subtree/targeted_mob_ability
|
||||
/// Blackboard key for the ability
|
||||
var/ability_key = BB_TARGETED_ACTION
|
||||
/// Blackboard key for where the target ref is stored
|
||||
var/target_key = BB_BASIC_MOB_CURRENT_TARGET
|
||||
/// Behaviour to perform using ability
|
||||
var/use_ability_behaviour = /datum/ai_behavior/targeted_mob_ability
|
||||
/// If true we terminate planning after trying to use the ability.
|
||||
var/finish_planning = TRUE
|
||||
|
||||
/datum/ai_planning_subtree/targeted_mob_ability/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if (!ability_key)
|
||||
CRASH("You forgot to tell this mob where to find its ability")
|
||||
|
||||
if (!controller.blackboard_key_exists(target_key))
|
||||
return
|
||||
|
||||
var/datum/action/cooldown/using_action = controller.blackboard[ability_key]
|
||||
if (!using_action?.IsAvailable())
|
||||
return
|
||||
if (!additional_ability_checks(controller, using_action))
|
||||
return
|
||||
|
||||
controller.queue_behavior(use_ability_behaviour, ability_key, target_key)
|
||||
if (finish_planning)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/// Any additional checks before we queue the behaviour
|
||||
/datum/ai_planning_subtree/targeted_mob_ability/proc/additional_ability_checks(datum/ai_controller/controller, datum/action/cooldown/using_action)
|
||||
return TRUE
|
||||
|
||||
/datum/ai_planning_subtree/targeted_mob_ability/continue_planning
|
||||
finish_planning = FALSE
|
||||
@@ -1,55 +0,0 @@
|
||||
///behavior to activate ability to escape from target
|
||||
/datum/ai_planning_subtree/teleport_away_from_target
|
||||
///minimum distance away from the target before we execute behavior
|
||||
var/minimum_distance = 2
|
||||
///the ability we will execute
|
||||
var/ability_key
|
||||
|
||||
/datum/ai_planning_subtree/teleport_away_from_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
|
||||
return
|
||||
var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
|
||||
var/distance_from_target = get_dist(target, controller.pawn)
|
||||
if(distance_from_target >= minimum_distance)
|
||||
controller.clear_blackboard_key(BB_ESCAPE_DESTINATION)
|
||||
return
|
||||
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
|
||||
if(!ability?.IsAvailable())
|
||||
return
|
||||
var/turf/location_turf = controller.blackboard[BB_ESCAPE_DESTINATION]
|
||||
|
||||
if(isnull(location_turf))
|
||||
controller.queue_behavior(/datum/ai_behavior/find_furthest_turf_from_target, BB_BASIC_MOB_CURRENT_TARGET, BB_ESCAPE_DESTINATION, minimum_distance)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
if(get_dist(location_turf, target) < minimum_distance || !can_see(controller.pawn, location_turf)) //target moved close too close or we moved too far since finding the target turf
|
||||
controller.clear_blackboard_key(BB_ESCAPE_DESTINATION)
|
||||
return
|
||||
|
||||
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target, ability_key, BB_ESCAPE_DESTINATION)
|
||||
|
||||
///find furtherst turf target so we may teleport to it
|
||||
/datum/ai_behavior/find_furthest_turf_from_target
|
||||
|
||||
/datum/ai_behavior/find_furthest_turf_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, set_key, range)
|
||||
var/mob/living/living_target = controller.blackboard[target_key]
|
||||
if(QDELETED(living_target))
|
||||
return AI_BEHAVIOR_INSTANT
|
||||
|
||||
var/distance = 0
|
||||
var/turf/chosen_turf
|
||||
for(var/turf/open/potential_destination in oview(range, living_target))
|
||||
if(potential_destination.is_blocked_turf())
|
||||
continue
|
||||
var/new_distance_to_target = get_dist(potential_destination, living_target)
|
||||
if(new_distance_to_target > distance)
|
||||
chosen_turf = potential_destination
|
||||
distance = new_distance_to_target
|
||||
if(distance == range)
|
||||
break //we have already found the max distance
|
||||
|
||||
if(isnull(chosen_turf))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
|
||||
controller.set_blackboard_key(set_key, chosen_turf)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/tip_reaction",
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_true",
|
||||
"vars": {
|
||||
"key": "BB_BASIC_MOB_TIP_REACTING",
|
||||
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/tipped_reaction"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
///used by cows
|
||||
/datum/ai_planning_subtree/tip_reaction
|
||||
|
||||
/datum/ai_planning_subtree/tip_reaction/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
var/tip_reacting = controller.blackboard[BB_BASIC_MOB_TIP_REACTING]
|
||||
if(!tip_reacting)
|
||||
return
|
||||
controller.queue_behavior(/datum/ai_behavior/tipped_reaction, BB_BASIC_MOB_TIPPER, BB_BASIC_MOB_TIP_REACTING)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING //no point in trying, boy. you're TIPPED.
|
||||
@@ -1,21 +0,0 @@
|
||||
/// Simply walk to a location
|
||||
/datum/ai_planning_subtree/travel_to_point
|
||||
/// Blackboard key where we travel a place we walk to
|
||||
var/location_key = BB_TRAVEL_DESTINATION
|
||||
/// What do we do in order to travel
|
||||
var/travel_behaviour = /datum/ai_behavior/travel_towards
|
||||
|
||||
/datum/ai_planning_subtree/travel_to_point/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
. = ..()
|
||||
var/atom/target = controller.blackboard[location_key]
|
||||
if (QDELETED(target))
|
||||
return
|
||||
controller.queue_behavior(travel_behaviour, location_key)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
|
||||
/datum/ai_planning_subtree/travel_to_point/and_clear_target
|
||||
travel_behaviour = /datum/ai_behavior/travel_towards/stop_on_arrival
|
||||
|
||||
/datum/ai_planning_subtree/travel_to_point/and_clear_target/reinforce
|
||||
location_key = BB_BASIC_MOB_REINFORCEMENT_TARGET
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Simple behaviours which simply try to use an ability whenever it is available.
|
||||
* For something which wants a target try `targeted_mob_ability`.
|
||||
*/
|
||||
/datum/ai_planning_subtree/use_mob_ability
|
||||
/// Blackboard key for the ability
|
||||
var/ability_key = BB_GENERIC_ACTION
|
||||
/// Behaviour to perform using ability
|
||||
var/use_ability_behaviour = /datum/ai_behavior/use_mob_ability
|
||||
/// If true we terminate planning after trying to use the ability.
|
||||
var/finish_planning = FALSE
|
||||
|
||||
/datum/ai_planning_subtree/use_mob_ability/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
if (!ability_key)
|
||||
CRASH("You forgot to tell this mob where to find its ability")
|
||||
|
||||
var/datum/action/using_action = controller.blackboard[ability_key]
|
||||
if (!using_action?.IsAvailable())
|
||||
return
|
||||
|
||||
controller.queue_behavior(use_ability_behaviour, ability_key)
|
||||
if (finish_planning)
|
||||
return SUBTREE_RETURN_FINISH_PLANNING
|
||||
|
||||
/datum/ai_behavior/use_mob_ability
|
||||
|
||||
/datum/ai_behavior/use_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key)
|
||||
var/datum/action/using_action = controller.blackboard[ability_key]
|
||||
if (QDELETED(using_action))
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
if(using_action.Trigger())
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
@@ -5,138 +5,121 @@
|
||||
)
|
||||
|
||||
ai_movement = /datum/ai_movement/basic_avoidance
|
||||
idle_behavior = /datum/idle_behavior/idle_random_walk
|
||||
behavior_tree_json = ABSTRACT_AI_CLASS
|
||||
|
||||
|
||||
/datum/bt_node/subtree/simple_hostile_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_hostile_combat.bt.json"
|
||||
|
||||
|
||||
/// The most basic AI tree which just finds a guy and then runs at them to click them
|
||||
/datum/ai_controller/basic_controller/simple/simple_hostile
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_hostile.bt.json"
|
||||
|
||||
|
||||
/datum/bt_node/subtree/simple_ranged_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ranged_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_ranged_retaliate_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ranged_retaliate_combat.bt.json"
|
||||
|
||||
|
||||
/datum/bt_node/subtree/simple_skirmisher_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_skirmisher_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_ability_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_ability_retaliate_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_retaliate_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_ability_melee_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_melee_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_ability_ranged_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_ranged_combat.bt.json"
|
||||
|
||||
|
||||
/datum/bt_node/subtree/simple_retaliate_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_retaliate_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_capricious_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_capricious_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_fearful_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_fearful_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_skittish_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_skittish_combat.bt.json"
|
||||
|
||||
/datum/bt_node/subtree/simple_hostile_obstacles_combat
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_hostile_obstacles_combat.bt.json"
|
||||
|
||||
|
||||
|
||||
/// Find a target, walk at target, attack intervening obstacles
|
||||
/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_hostile_obstacles.bt.json"
|
||||
|
||||
/// Find a target, walk at target, attack intervening obstacles
|
||||
/// Find a target, maintain distance, shoot them
|
||||
/datum/ai_controller/basic_controller/simple/simple_ranged
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/maintain_distance,
|
||||
/datum/ai_planning_subtree/ranged_skirmish,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ranged.bt.json"
|
||||
|
||||
/datum/ai_controller/basic_controller/simple/simple_ranged_retaliate
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/target_retaliate,
|
||||
/datum/ai_planning_subtree/maintain_distance,
|
||||
/datum/ai_planning_subtree/ranged_skirmish,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ranged_retaliate.bt.json"
|
||||
|
||||
/// Find a target, walk towards it AND shoot it
|
||||
/datum/ai_controller/basic_controller/simple/simple_skirmisher
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/ranged_skirmish,
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_skirmisher.bt.json"
|
||||
|
||||
/// Use an ability on target on cooldown
|
||||
/datum/ai_controller/basic_controller/simple/simple_ability
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/maintain_distance,
|
||||
/datum/ai_planning_subtree/targeted_mob_ability,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability.bt.json"
|
||||
|
||||
/datum/ai_controller/basic_controller/simple/simple_ability_retaliate
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/target_retaliate,
|
||||
/datum/ai_planning_subtree/maintain_distance,
|
||||
/datum/ai_planning_subtree/targeted_mob_ability,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_retaliate.bt.json"
|
||||
|
||||
/// Use an ability on target on cooldown, then try to punch them
|
||||
/datum/ai_controller/basic_controller/simple/simple_ability_melee
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/targeted_mob_ability,
|
||||
/datum/ai_planning_subtree/attack_obstacle_in_path,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_melee.bt.json"
|
||||
|
||||
/// Use an ability on target on cooldown, then try to shoot them
|
||||
/datum/ai_controller/basic_controller/simple/simple_ability_ranged
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/simple_find_target,
|
||||
/datum/ai_planning_subtree/maintain_distance,
|
||||
/datum/ai_planning_subtree/targeted_mob_ability,
|
||||
/datum/ai_planning_subtree/ranged_skirmish,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_ability_ranged.bt.json"
|
||||
|
||||
/// Fight back if attacked
|
||||
/datum/ai_controller/basic_controller/simple/simple_retaliate
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_retaliate.bt.json"
|
||||
ai_traits = DEFAULT_AI_FLAGS | STOP_MOVING_WHEN_PULLED
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/target_retaliate,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
|
||||
/// Get pissed at random people for no reason
|
||||
/datum/ai_controller/basic_controller/simple/simple_capricious
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_capricious.bt.json"
|
||||
ai_traits = DEFAULT_AI_FLAGS | STOP_MOVING_WHEN_PULLED
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/capricious_retaliate,
|
||||
/datum/ai_planning_subtree/target_retaliate,
|
||||
/datum/ai_planning_subtree/basic_melee_attack_subtree,
|
||||
)
|
||||
|
||||
/// Runs away from anyone it sees
|
||||
/datum/ai_controller/basic_controller/simple/simple_fearful
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_fearful.bt.json"
|
||||
ai_traits = PASSIVE_AI_FLAGS
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/simple_find_nearest_target_to_flee,
|
||||
/datum/ai_planning_subtree/flee_target,
|
||||
)
|
||||
|
||||
/// Runs away when attacked
|
||||
/datum/ai_controller/basic_controller/simple/simple_skittish
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_skittish.bt.json"
|
||||
ai_traits = PASSIVE_AI_FLAGS
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
|
||||
/datum/ai_planning_subtree/flee_target,
|
||||
)
|
||||
|
||||
/// Does what it is told and protects da boss
|
||||
/// TODO: port pet command system to BT so pet_planning functions correctly
|
||||
/datum/ai_controller/basic_controller/simple/simple_goon
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_goon.bt.json"
|
||||
blackboard = list(
|
||||
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
|
||||
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
|
||||
)
|
||||
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/escape_captivity,
|
||||
/datum/ai_planning_subtree/pet_planning,
|
||||
)
|
||||
|
||||
/// Literally does nothing except random speedh
|
||||
/// Literally does nothing except random speech
|
||||
/datum/ai_controller/basic_controller/talk
|
||||
idle_behavior = null
|
||||
planning_subtrees = list(
|
||||
/datum/ai_planning_subtree/random_speech/blackboard,
|
||||
)
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/talk.bt.json"
|
||||
|
||||
|
||||
/datum/bt_node/subtree/simple_hostile_combat_with_retaliate
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/simple_hostile_combat_with_retaliate.bt.json"
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
/**
|
||||
* Traverse to a target with the intention of picking it up.
|
||||
* If we can't do that, add it to a list of ignored items.
|
||||
*/
|
||||
/datum/ai_behavior/fetch_seek
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT|AI_BEHAVIOR_REQUIRE_REACH
|
||||
|
||||
/datum/ai_behavior/fetch_seek/setup(datum/ai_controller/controller, target_key, delivery_key)
|
||||
. = ..()
|
||||
var/obj/item/fetch_thing = controller.blackboard[target_key]
|
||||
// It stopped existing
|
||||
if (QDELETED(fetch_thing))
|
||||
return FALSE
|
||||
set_movement_target(controller, fetch_thing)
|
||||
|
||||
/datum/ai_behavior/fetch_seek/perform(seconds_per_tick, datum/ai_controller/controller, target_key, delivery_key)
|
||||
var/obj/item/fetch_thing = controller.blackboard[target_key]
|
||||
|
||||
// It stopped existing
|
||||
if (QDELETED(fetch_thing))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
// We can't pick this up
|
||||
if (fetch_thing.anchored)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/fetch_seek/finish_action(datum/ai_controller/controller, success, target_key, delivery_key)
|
||||
. = ..()
|
||||
if (success)
|
||||
return
|
||||
// Blacklist item if we failed
|
||||
var/obj/item/target = controller.blackboard[target_key]
|
||||
if (target)
|
||||
controller.set_blackboard_key_assoc_lazylist(BB_FETCH_IGNORE_LIST, target, TRUE)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
controller.clear_blackboard_key(delivery_key)
|
||||
|
||||
/**
|
||||
* The second half of fetching, deliver the item to a target.
|
||||
*/
|
||||
/datum/ai_behavior/deliver_fetched_item
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT|AI_BEHAVIOR_REQUIRE_REACH
|
||||
|
||||
/datum/ai_behavior/deliver_fetched_item/setup(datum/ai_controller/controller, delivery_key, storage_key)
|
||||
. = ..()
|
||||
var/mob/living/return_target = controller.blackboard[delivery_key]
|
||||
if(QDELETED(return_target)) // Guess it's mine now
|
||||
return FALSE
|
||||
set_movement_target(controller, return_target)
|
||||
|
||||
/datum/ai_behavior/deliver_fetched_item/perform(seconds_per_tick, datum/ai_controller/controller, delivery_key, storage_key)
|
||||
var/mob/living/return_target = controller.blackboard[delivery_key]
|
||||
if(QDELETED(return_target))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
if(!deliver_item(controller, return_target, storage_key))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/ai_behavior/deliver_fetched_item/finish_action(datum/ai_controller/controller, success, delivery_key)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(delivery_key)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
|
||||
/// Actually deliver the fetched item to the target, if we still have it
|
||||
/// Returns TRUE if we succeeded, FALSE if we failed
|
||||
/datum/ai_behavior/deliver_fetched_item/proc/deliver_item(datum/ai_controller/controller, return_target, storage_key)
|
||||
var/mob/pawn = controller.pawn
|
||||
var/obj/item/carried_item = controller.blackboard[storage_key]
|
||||
if(QDELETED(carried_item) || carried_item.loc != pawn)
|
||||
pawn.visible_message(span_notice("[pawn] looks around as if [pawn.p_they()] [pawn.p_have()] lost something."))
|
||||
return FALSE
|
||||
|
||||
pawn.visible_message(span_notice("[pawn] delivers [carried_item] to [return_target]."))
|
||||
carried_item.forceMove(get_turf(return_target))
|
||||
controller.clear_blackboard_key(storage_key)
|
||||
return TRUE
|
||||
|
||||
/**
|
||||
* The alternate second half of fetching, attack the item if we can eat it.
|
||||
* Or make pleading eyes at someone who has picked it up.
|
||||
*
|
||||
* Unfortunately this doesn't work because food can't currently be eaten by mobs.
|
||||
*/
|
||||
/datum/ai_behavior/eat_fetched_snack
|
||||
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
|
||||
action_cooldown = 0.8 SECONDS
|
||||
|
||||
/datum/ai_behavior/eat_fetched_snack/setup(datum/ai_controller/controller, target_key, delivery_key)
|
||||
. = ..()
|
||||
var/obj/item/snack = controller.blackboard[target_key]
|
||||
if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc)))
|
||||
return FALSE // This isn't food at all!
|
||||
set_movement_target(controller, snack)
|
||||
|
||||
/datum/ai_behavior/eat_fetched_snack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, delivery_key)
|
||||
var/obj/item/snack = controller.blackboard[target_key]
|
||||
var/is_living_loc = isliving(snack.loc)
|
||||
if(QDELETED(snack) || (!isturf(snack.loc) && !is_living_loc))
|
||||
// Where did it go?
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
|
||||
var/mob/living/basic/basic_pawn = controller.pawn
|
||||
if(is_living_loc)
|
||||
if(SPT_PROB(10, seconds_per_tick))
|
||||
basic_pawn.manual_emote("Stares at [snack.loc]'s [snack.name] intently.")
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
if(!basic_pawn.Adjacent(snack))
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
controller.ai_interact(target = snack)
|
||||
|
||||
if(QDELETED(snack)) // we ate it!
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
return AI_BEHAVIOR_DELAY
|
||||
|
||||
/datum/ai_behavior/eat_fetched_snack/finish_action(datum/ai_controller/controller, succeeded, target_key, delivery_key)
|
||||
. = ..()
|
||||
controller.clear_blackboard_key(target_key)
|
||||
controller.clear_blackboard_key(delivery_key)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
|
||||
/**
|
||||
* Clear our failed fetch list every so often
|
||||
*/
|
||||
/datum/ai_behavior/forget_failed_fetches
|
||||
/// How long to wait between resetting the list
|
||||
var/cooldown_duration = AI_FETCH_IGNORE_DURATION
|
||||
/// Time until we should forget things we failed to pick up
|
||||
COOLDOWN_DECLARE(reset_ignore_cooldown)
|
||||
|
||||
/datum/ai_behavior/forget_failed_fetches/setup(datum/ai_controller/controller, ...)
|
||||
. = ..()
|
||||
if (!COOLDOWN_FINISHED(src, reset_ignore_cooldown))
|
||||
return FALSE
|
||||
if (!length(controller.blackboard[BB_FETCH_IGNORE_LIST]))
|
||||
return
|
||||
|
||||
/datum/ai_behavior/forget_failed_fetches/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
COOLDOWN_START(src, reset_ignore_cooldown, cooldown_duration)
|
||||
controller.clear_blackboard_key(BB_FETCH_IGNORE_LIST)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/attack",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_melee_attack",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"targeting_strategy": "BB_PET_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_PET_ATTACK_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/attack/dog",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_melee_attack/dog",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"targeting_strategy": "BB_PET_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_PET_ATTACK_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/attack/minebot",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_ranged_attack/minebot",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"targeting_strategy": "BB_PET_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_PET_ATTACK_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/attack/ranged/glockroach",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_ranged_attack/glockroach",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"targeting_strategy": "BB_PET_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_PET_ATTACK_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/maintain_distance",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/beehive",
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_HOME",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/enter_exit_hive"
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/breed",
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/key_in_typelist",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"typelist_key": "BB_BABIES_PARTNER_TYPES"
|
||||
},
|
||||
"child": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/keys_different_gender",
|
||||
"vars": {
|
||||
"invert": true,
|
||||
"key_a": "BB_MY_PAWN",
|
||||
"key_b": "BB_CURRENT_PET_TARGET"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/perform_emote",
|
||||
"vars": {
|
||||
"emote": "Seems confused"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/ai_interact",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"combat_mode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Pet-command-specific BT behaviors and override subtrees.
|
||||
// Generic leaf behaviors (wait, play_dead, pick_up_item_virtual, pass_item_virtual, ai_interact) live in basic_ai_behaviors/.
|
||||
|
||||
/// Validates a protect_owner target; clears command + target if invalid.
|
||||
/datum/bt_node/ai_behavior/protect_owner_check
|
||||
|
||||
/datum/bt_node/ai_behavior/protect_owner_check/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/mob/living/victim = controller.blackboard[BB_CURRENT_PET_TARGET]
|
||||
if(QDELETED(victim))
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
controller.clear_blackboard_key(BB_CURRENT_PET_TARGET)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
var/datum/targeting_strategy/targeter = GET_TARGETING_STRATEGY(controller.blackboard[BB_PET_TARGETING_STRATEGY])
|
||||
if(!targeter?.is_valid_target(controller.pawn, victim))
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
controller.clear_blackboard_key(BB_CURRENT_PET_TARGET)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
var/minimum_stat = controller.blackboard[BB_TARGET_MINIMUM_STAT]
|
||||
if((!isnull(minimum_stat) && victim.stat > minimum_stat) || victim == controller.pawn)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
controller.clear_blackboard_key(BB_CURRENT_PET_TARGET)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/// Validates a fetch item at target_key; adds to ignore list and clears keys on failure.
|
||||
/datum/bt_node/ai_behavior/fetch_seek
|
||||
var/target_key
|
||||
|
||||
/datum/bt_node/ai_behavior/fetch_seek/setup(datum/ai_controller/controller)
|
||||
var/obj/item/target = controller.blackboard[target_key]
|
||||
return !QDELETED(target)
|
||||
|
||||
/datum/bt_node/ai_behavior/fetch_seek/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
var/obj/item/fetch_thing = controller.blackboard[target_key]
|
||||
if(QDELETED(fetch_thing))
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
if(fetch_thing.anchored)
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
|
||||
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/datum/bt_node/ai_behavior/fetch_seek/finish_action(datum/ai_controller/controller, succeeded)
|
||||
. = ..()
|
||||
if(succeeded)
|
||||
return
|
||||
var/obj/item/target = controller.blackboard[target_key]
|
||||
if(target)
|
||||
controller.set_blackboard_key_assoc_lazylist(BB_FETCH_IGNORE_LIST, target, TRUE)
|
||||
controller.clear_blackboard_key(target_key)
|
||||
controller.clear_blackboard_key(BB_FETCH_DELIVER_TO)
|
||||
|
||||
/// Clears the fetch ignore list at most once per AI_FETCH_IGNORE_DURATION. Always succeeds.
|
||||
/datum/bt_node/ai_behavior/forget_failed_fetches
|
||||
COOLDOWN_DECLARE(clear_cooldown)
|
||||
|
||||
/datum/bt_node/ai_behavior/forget_failed_fetches/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
if(COOLDOWN_FINISHED(src, clear_cooldown) && LAZYLEN(controller.blackboard[BB_FETCH_IGNORE_LIST]))
|
||||
COOLDOWN_START(src, clear_cooldown, AI_FETCH_IGNORE_DURATION)
|
||||
controller.clear_blackboard_key(BB_FETCH_IGNORE_LIST)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
/// Clears BB_ACTIVE_PET_COMMAND and removes the SUBPLAN_ID_PET_COMMAND override.
|
||||
/datum/bt_node/ai_behavior/clear_pet_command
|
||||
|
||||
/datum/bt_node/ai_behavior/clear_pet_command/perform(seconds_per_tick, datum/ai_controller/controller)
|
||||
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
|
||||
controller.set_behavior_tree_override(SUBPLAN_ID_PET_COMMAND, null)
|
||||
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
|
||||
|
||||
|
||||
/// Waits forever; blocks normal AI while stay/idle is active.
|
||||
/datum/bt_node/subtree/pet_command/stay
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_stay.bt.json"
|
||||
|
||||
/// Loops move_to_target toward BB_CURRENT_PET_TARGET until the key is cleared.
|
||||
/datum/bt_node/subtree/pet_command/follow
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_follow.bt.json"
|
||||
|
||||
/// Plays dead (10%/tick to get up). Clears command on revival.
|
||||
/datum/bt_node/subtree/pet_command/play_dead
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_play_dead.bt.json"
|
||||
|
||||
/// Attacks BB_CURRENT_PET_TARGET in a looping melee combat parallel.
|
||||
/datum/bt_node/subtree/pet_command/attack
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_attack.bt.json"
|
||||
|
||||
/// Protect owner: loops a validity check then melee attack. Clears command if target invalid.
|
||||
/datum/bt_node/subtree/pet_command/protect_owner
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_protect_owner.bt.json"
|
||||
|
||||
/// Travels to BB_CURRENT_PET_TARGET, clears command on arrival.
|
||||
/datum/bt_node/subtree/pet_command/move_to
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_move_to.bt.json"
|
||||
|
||||
/// Moves to BB_CURRENT_PET_TARGET and fishes there on a loop.
|
||||
/datum/bt_node/subtree/pet_command/fish
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_fish.bt.json"
|
||||
|
||||
/// Moves to BB_CURRENT_PET_TARGET and breeds once. Clears command on completion.
|
||||
/datum/bt_node/subtree/pet_command/breed
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_breed.bt.json"
|
||||
|
||||
/// Moves to BB_CURRENT_PET_TARGET and fires BB_TARGETED_ACTION on it once.
|
||||
/datum/bt_node/subtree/pet_command/targeted_ability
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_targeted_ability.bt.json"
|
||||
|
||||
/// Fires the ability stored in BB_PET_ACTIVE_ABILITY once (untargeted).
|
||||
/datum/bt_node/subtree/pet_command/untargeted_ability
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_untargeted_ability.bt.json"
|
||||
|
||||
/// Fetch: seek > pick up > deliver. Falls back to clear_pet_command if nothing to do.
|
||||
/datum/bt_node/subtree/pet_command/fetch
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_fetch.bt.json"
|
||||
|
||||
/// Attacks BB_CURRENT_PET_TARGET using the dog's melee behavior (paws if BB_DOG_HARASS_HARM is false, bites otherwise).
|
||||
/datum/bt_node/subtree/pet_command/attack/dog
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_attack_dog.bt.json"
|
||||
|
||||
/// Attacks BB_CURRENT_PET_TARGET with glockroach ranged attack (1s cooldown).
|
||||
/datum/bt_node/subtree/pet_command/attack/ranged/glockroach
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_attack_ranged_glockroach.bt.json"
|
||||
|
||||
/// Attacks BB_CURRENT_PET_TARGET with minebot ranged attack (avoids friendly fire).
|
||||
/datum/bt_node/subtree/pet_command/attack/minebot
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_attack_minebot.bt.json"
|
||||
|
||||
/// Protect owner: loops validity check then glockroach ranged attack. Clears command if target invalid.
|
||||
/datum/bt_node/subtree/pet_command/protect_owner/ranged/glockroach
|
||||
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_protect_owner_ranged_glockroach.bt.json"
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/fetch",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/forget_failed_fetches"
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/fetch_seek",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/pick_up_item_virtual",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"storage_key": "BB_SIMPLE_CARRY_ITEM"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_SIMPLE_CARRY_ITEM",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_FETCH_DELIVER_TO",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_FETCH_DELIVER_TO",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/pass_item_virtual",
|
||||
"vars": {
|
||||
"delivery_key": "BB_FETCH_DELIVER_TO",
|
||||
"storage_key": "BB_SIMPLE_CARRY_ITEM"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"observer_abort": "BT_ABORT_BOTH",
|
||||
"invert": true,
|
||||
"key": "BB_CURRENT_PET_TARGET"
|
||||
},
|
||||
"child": {
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/wait"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/fish",
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_FAIL_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/ai_interact",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"combat_mode": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/follow",
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_FAIL_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/mine_walls",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/mine_wall",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/find_mineral_wall",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/move_to",
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 0,
|
||||
"finish_on_arrival": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* # Pet Planning
|
||||
* Perform behaviour based on what pet commands you have received. This is delegated to the pet command datum.
|
||||
* When a command is set, we blackboard a key to our currently active command.
|
||||
* The blackboard also has a weak reference to every command datum available to us.
|
||||
* We use the key to figure out which datum to run, then ask it to figure out how to execute its action.
|
||||
*/
|
||||
/datum/ai_planning_subtree/pet_planning
|
||||
|
||||
/datum/ai_planning_subtree/pet_planning/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
|
||||
var/datum/pet_command/command = controller.blackboard[BB_ACTIVE_PET_COMMAND]
|
||||
if (!command)
|
||||
return // Do something else
|
||||
return command.execute_action(controller)
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/play_dead",
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/play_dead"
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/protect_owner",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/protect_owner_check"
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_melee_attack",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"targeting_strategy": "BB_PET_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_PET_ATTACK_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"required_dist": 1,
|
||||
"finish_on_arrival": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/protect_owner/ranged/glockroach",
|
||||
"type": "selector",
|
||||
"children": [
|
||||
{
|
||||
"type": "decorator",
|
||||
"decorator": "/datum/bt_node/decorator/bb_key_set",
|
||||
"vars": {
|
||||
"key": "BB_CURRENT_PET_TARGET",
|
||||
"observer_abort": "BT_ABORT_BOTH"
|
||||
},
|
||||
"child": {
|
||||
"type": "parallel",
|
||||
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
|
||||
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
|
||||
"repeat_secondary": true,
|
||||
"finish_on_primary": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "subplan",
|
||||
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
|
||||
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
|
||||
"children": [
|
||||
{
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/protect_owner_check"
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/basic_ranged_attack/glockroach",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_PET_TARGET",
|
||||
"targeting_strategy": "BB_PET_TARGETING_STRATEGY",
|
||||
"hiding_location_key": "BB_PET_ATTACK_HIDING_LOCATION"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/maintain_distance",
|
||||
"vars": {
|
||||
"target_key": "BB_CURRENT_TARGET"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/scatter",
|
||||
"type": "sequence",
|
||||
"children": [
|
||||
{
|
||||
"type": "subtree",
|
||||
"subtype": "/datum/bt_node/subtree/run_away_from_target",
|
||||
"bindings": {
|
||||
"byk9gqj4": "BB_CURRENT_PET_TARGET"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/clear_pet_command"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dm_type": "/datum/bt_node/subtree/pet_command/stay",
|
||||
"type": "leaf",
|
||||
"behavior": "/datum/bt_node/ai_behavior/wait",
|
||||
"vars": {
|
||||
"duration": 0
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user