mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-08-23 13:10:02 +01:00
Replaced our NPC AI with Behavior Trees. (#96628)
This PR replaces our current NPC AI with a [behavior tree system](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)). Behavior trees are a common way of creating AI in which you place nodes in a tree structure to define what actions an AI should take. AI controllers defined a list of /datum/ai_planning_subtree types in behavior_nodes. Each subtree was a self-contained unit that could call queue_behavior() to fire off /datum/ai_behavior actions. The controller iterated subtrees in order, each one deciding independently whether to queue something and deciding whether the next subtree would run. This has a few issues: 1. There's no real structure; you are just defining a list of things to try in order. 2. There was a loooot of subtrees that were basically the same as another but with some slight modification 3. It was hard to understand. Controllers now define a single json file describing a tree of nodes. The tree is composed of structural composites: Sequence - do A, then B, then C (and so on) Selector - try A, if it fails try B, then C (and so on) Parallel - run A and B simultaneously, with configurable failure/success policies and or looping behavior Subplan - loop a child continiously Along that we also have "Decorators". These are nodes that basically check a condition (E.g.; do we have a combat target). These decorators can be used to gate behavior and are re-useable across behavior trees. They also have a concept known as "Observers". Which lets them cancel lower priority behavior in case their condition changes (Which we check whenever a signal fires that fits that specific decorator). This makes the AI much more responsive to change in environment. For behaviors, we still use the ai_behavior datums. These are the actual behaviors such as "Move to X", "Attack X". The only major change is that these can no longer sleep() since they now run in the ai_controller. Lastly, we now also have subtrees, except now they are essentially pieces of behavior tree that can be re-used, or even overriden at runtime or as a variable. Allowing for making modular AI made out of several smaller trees. You can set variables on these nodes directly via the extension (see below), which should reduce the need to make subtypes of behaviors by a lot. All of these vars are saved on the JSON and will be applied at runtime. If you are using subtrees, you can also assign "bindings" to these variables, which will allow instances of the subtree to override those variables. Since a tree structure with variables becomes hard to parse in a JSON, I've made a VSCode extension to edit these JSONs: https://marketplace.visualstudio.com/items?itemName=BehaviorTreeG.behaviortreeg https://github.com/CabinetOnFire/BehaviorTreeG <img width="1795" height="1268" alt="image" src="https://github.com/user-attachments/assets/56aa2f0b-3cf9-449f-bca4-8281fca82db6" /> This extension allows you to edit the behavior tree JSONs, and browse through all the behaviors/decorators/subtrees we have If you'd like more info on how to build these AI check out the learn_ai.md. I will also make a tutorial to go over more depth on what the system offers because I kind of suck at doing technical write-ups. Targetting has been changed to. I've made a new acquire_targets behavior that takes a target_source (what am I targetting) and targetting_strategy (what does the candidate need to fulfill to be considered a target). This allows us to make composites targetting combinations to reduce the amount of specific find_and_set esque behaviors we had before. Not everything is ported to this system but that would be a longer term goal. I've added a new build_bt script that converts all the behavior tree JSONs into compiled versions. Why is this needed? Because I wanted to keep using defines in behavior trees, so we need a way to convert this into literal values before we send it to DM. This script runs on compile and should also run in CI (If I didn't fuck that up!). This saves to a new build/ folder. I've ported every single AI in the game to this system (except raptors, Kobsa is working on those so should be in soon!), so I do expect some bugs to come out of this. But I also fixed some issues that have probably been in the game for a long time such as: - Fixed penguins being unable to fish - Fixed bileworms not being able to devour people - Fixes goldgrubs not grubbing gold (they could not mine!) - Lizards actually eat food they find Either way, I'd reccomend a long TM on this. 1. (Hopefully) a better development experience for making AI 2. Less copy-paste for behaviors, we should be able to re-use more pieces to make behavior 3. Behavior trees is a more common pattern in making AI, so it should be easier to find resources to find out how to do things. 🆑 CabinetOnFire, Iamgoofball, SmartKar, Ben10omintrix refactor: Replaces our AI system with behavior trees, porting all datum/ai to it /🆑 I will add this PR with more details down the line. I think I got the big picture but its a big PR, so sorry if I missed something important. --------- Co-authored-by: Iamgoofball <iamgoofball@gmail.com> Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com> Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
This commit is contained in:
committed by
The Sharkenning
co-authored by
Iamgoofball
SmArtKar
Ghom
Ben10Omintrix
SyncIt21
parent
ad0d6a3e7d
commit
df7832aa43
@@ -1,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
|
||||
Reference in New Issue
Block a user