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,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