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:
CabinetOnFire
2026-08-15 10:33:57 -06:00
committed by The Sharkenning
co-authored by Iamgoofball SmArtKar Ghom Ben10Omintrix SyncIt21
parent ad0d6a3e7d
commit df7832aa43
1272 changed files with 37929 additions and 13503 deletions
@@ -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
}
}
]
}
}
]
}
@@ -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