Replaced our NPC AI with Behavior Trees. (#96628)

This PR replaces our current NPC AI with a [behavior tree
system](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)).
Behavior trees are a common way of creating AI in which you place nodes
in a tree structure to define what actions an AI should take.

AI controllers defined a list of /datum/ai_planning_subtree types in
behavior_nodes. Each subtree was a self-contained unit that could call
queue_behavior() to fire off /datum/ai_behavior actions. The controller
iterated subtrees in order, each one deciding independently whether to
queue something and deciding whether the next subtree would run.

This has a few issues:
1. There's no real structure; you are just defining a list of things to
try in order.
2. There was a loooot of subtrees that were basically the same as
another but with some slight modification
3. It was hard to understand.

Controllers now define a single json file describing a tree of nodes.
The tree is composed of structural composites:

Sequence - do A, then B, then C (and so on)
Selector - try A, if it fails try B, then C (and so on)
Parallel - run A and B simultaneously, with configurable failure/success
policies and or looping behavior
Subplan - loop a child continiously

Along that we also have "Decorators". These are nodes that basically
check a condition (E.g.; do we have a combat target). These decorators
can be used to gate behavior and are re-useable across behavior trees.
They also have a concept known as "Observers". Which lets them cancel
lower priority behavior in case their condition changes (Which we check
whenever a signal fires that fits that specific decorator). This makes
the AI much more responsive to change in environment.

For behaviors, we still use the ai_behavior datums. These are the actual
behaviors such as "Move to X", "Attack X". The only major change is that
these can no longer sleep() since they now run in the ai_controller.

Lastly, we now also have subtrees, except now they are essentially
pieces of behavior tree that can be re-used, or even overriden at
runtime or as a variable. Allowing for making modular AI made out of
several smaller trees.

You can set variables on these nodes directly via the extension (see
below), which should reduce the need to make subtypes of behaviors by a
lot. All of these vars are saved on the JSON and will be applied at
runtime.

If you are using subtrees, you can also assign "bindings" to these
variables, which will allow instances of the subtree to override those
variables.

Since a tree structure with variables becomes hard to parse in a JSON,
I've made a VSCode extension to edit these JSONs:

https://marketplace.visualstudio.com/items?itemName=BehaviorTreeG.behaviortreeg
https://github.com/CabinetOnFire/BehaviorTreeG

<img width="1795" height="1268" alt="image"
src="https://github.com/user-attachments/assets/56aa2f0b-3cf9-449f-bca4-8281fca82db6"
/>

This extension allows you to edit the behavior tree JSONs, and browse
through all the behaviors/decorators/subtrees we have

If you'd like more info on how to build these AI check out the
learn_ai.md. I will also make a tutorial to go over more depth on what
the system offers because I kind of suck at doing technical write-ups.

Targetting has been changed to. I've made a new acquire_targets behavior
that takes a target_source (what am I targetting) and
targetting_strategy (what does the candidate need to fulfill to be
considered a target). This allows us to make composites targetting
combinations to reduce the amount of specific find_and_set esque
behaviors we had before. Not everything is ported to this system but
that would be a longer term goal.

I've added a new build_bt script that converts all the behavior tree
JSONs into compiled versions. Why is this needed? Because I wanted to
keep using defines in behavior trees, so we need a way to convert this
into literal values before we send it to DM. This script runs on compile
and should also run in CI (If I didn't fuck that up!). This saves to a
new build/ folder.

I've ported every single AI in the game to this system (except raptors,
Kobsa is working on those so should be in soon!), so I do expect some
bugs to come out of this. But I also fixed some issues that have
probably been in the game for a long time such as:
- Fixed penguins being unable to fish
- Fixed bileworms not being able to devour people
- Fixes goldgrubs not grubbing gold (they could not mine!)
- Lizards actually eat food they find

Either way, I'd reccomend a long TM on this.

1. (Hopefully) a better development experience for making AI
2. Less copy-paste for behaviors, we should be able to re-use more
pieces to make behavior
3. Behavior trees is a more common pattern in making AI, so it should be
easier to find resources to find out how to do things.

🆑 CabinetOnFire, Iamgoofball, SmartKar, Ben10omintrix
refactor: Replaces our AI system with behavior trees, porting all
datum/ai to it
/🆑

I will add this PR with more details down the line. I think I got the
big picture but its a big PR, so sorry if I missed something important.

---------

Co-authored-by: Iamgoofball <iamgoofball@gmail.com>
Co-authored-by: SmArtKar <44720187+SmArtKar@users.noreply.github.com>
Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com>
Co-authored-by: Ben10Omintrix <138636438+Ben10Omintrix@users.noreply.github.com>
Co-authored-by: SyncIt21 <110812394+SyncIt21@users.noreply.github.com>
This commit is contained in:
CabinetOnFire
2026-08-15 10:33:57 -06:00
committed by The Sharkenning
co-authored by Iamgoofball SmArtKar Ghom Ben10Omintrix SyncIt21
parent ad0d6a3e7d
commit df7832aa43
1272 changed files with 37929 additions and 13503 deletions
@@ -67,23 +67,25 @@
/mob/living/basic/alien/get_butt_sprite()
return icon('icons/mob/butts.dmi', BUTT_SPRITE_XENOMORPH)
///Places alien weeds on the turf the mob is currently standing on.
///Places alien weeds on the turf the mob is currently standing on. Returns TRUE if weeds were placed.
/mob/living/basic/alien/proc/place_weeds()
if(!isturf(loc) || isspaceturf(loc))
return
return FALSE
if(locate(/obj/structure/alien/weeds/node) in get_turf(src))
return
return FALSE
visible_message(span_alertalien("[src] plants some alien weeds!"))
new /obj/structure/alien/weeds/node(loc)
return TRUE
///Lays an egg on the turf the mob is currently standing on.
///Lays an egg on the turf the mob is currently standing on. Returns TRUE if an egg was laid.
/mob/living/basic/alien/proc/lay_alien_egg()
if(!isturf(loc) || isspaceturf(loc))
return
return FALSE
if(locate(/obj/structure/alien/egg) in get_turf(src))
return
return FALSE
visible_message(span_alertalien("[src] lays an egg!"))
new /obj/structure/alien/egg(loc)
return TRUE
/mob/living/basic/alien/get_bloodtype()
return get_blood_type(BLOOD_TYPE_XENO)
@@ -0,0 +1,9 @@
{
"dm_type": "/datum/ai_controller/basic_controller/alien",
"type": "subtree",
"subtype": "/datum/bt_node/subtree/basic_alien",
"bindings": {
"buewuqr6": "/datum/bt_node/subtree/melee_alien_combat",
"bkzbx73d": "/datum/bt_node/subtree/lay_alien_egg"
}
}
+35 -54
View File
@@ -1,76 +1,57 @@
/datum/ai_controller/basic_controller/alien
behavior_tree_json = "code/modules/mob/living/basic/alien/alien.bt.json"
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
BB_RANGED_SKIRMISH_MIN_DISTANCE = 2,
BB_RANGED_SKIRMISH_MAX_DISTANCE = 3,
)
movement_delay = 0.8 SECONDS
/datum/ai_controller/basic_controller/alien/sentinel
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_ranged_attack_subtree/alien,
)
behavior_tree_json = "code/modules/mob/living/basic/alien/sentinel.bt.json"
/datum/ai_controller/basic_controller/alien/drone
idle_behavior = /datum/idle_behavior/idle_random_walk/plant_weeds
behavior_tree_json = "code/modules/mob/living/basic/alien/drone.bt.json"
/datum/ai_controller/basic_controller/alien/queen
idle_behavior = /datum/idle_behavior/idle_random_walk/plant_weeds/queen
behavior_tree_json = "code/modules/mob/living/basic/alien/queen.bt.json"
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_ranged_attack_subtree/alien,
)
/**
* Alien projectile
* Try to avoid friendly fire, and has a 3 second delay.
*/
/datum/ai_planning_subtree/basic_ranged_attack_subtree/alien
ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/alien
/datum/bt_node/subtree/basic_alien
behavior_tree_json = "code/modules/mob/living/basic/alien/basic_alien.bt.json"
/datum/ai_behavior/basic_ranged_attack/alien
action_cooldown = 3 SECONDS
required_distance = 3
avoid_friendly_fire = TRUE
/datum/idle_behavior/idle_random_walk/plant_weeds
var/plant_cooldown = 30
var/plants_off = 0
/datum/bt_node/subtree/ranged_alien_combat
behavior_tree_json = "code/modules/mob/living/basic/alien/ranged_alien_combat.bt.json"
/datum/idle_behavior/idle_random_walk/plant_weeds/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
. = ..()
if(!.)
return .
plant_cooldown--
/datum/bt_node/subtree/melee_alien_combat
behavior_tree_json = "code/modules/mob/living/basic/alien/melee_alien_combat.bt.json"
/datum/bt_node/subtree/plant_alien_weeds
behavior_tree_json = "code/modules/mob/living/basic/alien/plant_alien_weeds.bt.json"
/datum/bt_node/subtree/lay_alien_egg
behavior_tree_json = "code/modules/mob/living/basic/alien/lay_alien_egg.bt.json"
/// Plants alien weeds on the pawn's current turf. Fails if the pawn can't plant or weeds couldn't be placed.
/datum/bt_node/ai_behavior/plant_alien_weeds
/datum/bt_node/ai_behavior/plant_alien_weeds/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/basic/alien/alien_pawn = controller.pawn
if(alien_pawn.can_plant_weeds && !plants_off && plant_cooldown <= 0)
plant_cooldown = initial(plant_cooldown)
alien_pawn.place_weeds()
if(!alien_pawn.can_plant_weeds || !alien_pawn.place_weeds())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
/datum/idle_behavior/idle_random_walk/plant_weeds/queen
var/eggs_off = 0
var/egg_cooldown = 30
/// Lays an alien egg on the pawn's current turf. Fails if the pawn can't lay eggs or an egg couldn't be placed.
/datum/bt_node/ai_behavior/lay_alien_egg
/datum/idle_behavior/idle_random_walk/plant_weeds/queen/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller)
. = ..()
if(!.)
return .
egg_cooldown--
/datum/bt_node/ai_behavior/lay_alien_egg/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/basic/alien/alien_pawn = controller.pawn
if(alien_pawn.can_lay_eggs && !eggs_off && egg_cooldown <= 0)
egg_cooldown = initial(egg_cooldown)
alien_pawn.lay_alien_egg()
if(!alien_pawn.can_lay_eggs || !alien_pawn.lay_alien_egg())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
@@ -0,0 +1,60 @@
{
"dm_type": "/datum/bt_node/subtree/basic_alien",
"bindings": {
"buewuqr6": {
"label": "Combat Subtree",
"default": "/datum/bt_node/subtree"
},
"bkzbx73d": {
"label": "Idle Behavior",
"default": "/datum/bt_node/subtree/random_walk"
}
},
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "subtree",
"subtype": "$buewuqr6"
}
},
{
"type": "subtree",
"subtype": "$bkzbx73d"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
@@ -0,0 +1,9 @@
{
"dm_type": "/datum/ai_controller/basic_controller/alien/drone",
"type": "subtree",
"subtype": "/datum/bt_node/subtree/basic_alien",
"bindings": {
"buewuqr6": "/datum/bt_node/subtree/melee_alien_combat",
"bkzbx73d": "/datum/bt_node/ai_behavior/plant_alien_weeds"
}
}
@@ -0,0 +1,14 @@
{
"dm_type": "/datum/bt_node/subtree/lay_alien_egg",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/cooldown",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"cooldown_key": "BB_ALIEN_PLANT_COOLDOWN",
"cooldown_duration": "30 SECONDS"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/lay_alien_egg"
}
}
@@ -0,0 +1,47 @@
{
"dm_type": "/datum/bt_node/subtree/melee_alien_combat",
"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": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/attack_obstructions",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
},
{
"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
}
}
]
}
@@ -0,0 +1,23 @@
{
"dm_type": "/datum/bt_node/subtree/plant_alien_weeds",
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/cooldown",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"cooldown_key": "BB_ALIEN_PLANT_COOLDOWN",
"cooldown_duration": "30 SECONDS"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/plant_alien_weeds"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/idle_random_walk"
}
]
}
@@ -0,0 +1,9 @@
{
"dm_type": "/datum/ai_controller/basic_controller/alien/queen",
"type": "subtree",
"subtype": "/datum/bt_node/subtree/basic_alien",
"bindings": {
"buewuqr6": "/datum/bt_node/subtree/ranged_alien_combat",
"bkzbx73d": "/datum/bt_node/ai_behavior/lay_alien_egg"
}
}
@@ -0,0 +1,48 @@
{
"dm_type": "/datum/bt_node/subtree/ranged_alien_combat",
"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": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/attack_obstructions",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/basic_ranged_attack",
"vars": {
"time_between_perform": "3 SECONDS",
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION",
"avoid_friendly_fire": true
}
}
]
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/maintain_distance",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"approach_movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
}
@@ -0,0 +1,8 @@
{
"dm_type": "/datum/ai_controller/basic_controller/alien/sentinel",
"type": "subtree",
"subtype": "/datum/bt_node/subtree/basic_alien",
"bindings": {
"buewuqr6": "/datum/bt_node/subtree/ranged_alien_combat"
}
}
@@ -3,55 +3,34 @@
* Only notable quirk is that it uses JPS movement, simple avoidance would fail to realise it can path through blobs
*/
/datum/ai_controller/basic_controller/blobbernaut
behavior_tree_json = "code/modules/mob/living/basic/blob_minions/blobbernaut.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/jps
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/**
* Move to a point designated by the overmind, otherwise just slap people nearby
*/
/datum/ai_controller/basic_controller/blob_zombie
behavior_tree_json = "code/modules/mob/living/basic/blob_minions/blob_zombie.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/jps
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/travel_to_point/and_clear_target,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/**
* As blob zombie but will prioritise attacking corpses to zombify them
*/
/datum/ai_controller/basic_controller/blob_spore
behavior_tree_json = "code/modules/mob/living/basic/blob_minions/blob_spore.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/jps
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/find_and_hunt_target/corpses/human,
/datum/ai_planning_subtree/travel_to_point/and_clear_target,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
@@ -0,0 +1,69 @@
{
"dm_type": "/datum/ai_controller/basic_controller/blob_spore",
"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": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/move_to_and_hunt",
"bindings": {
"bvtz06kb": "BB_CURRENT_HUNTING_TARGET",
"b3y599q4": "BB_CURRENT_HUNTING_TARGET",
"brrasnah": "BB_CURRENT_HUNTING_TARGET"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_TRAVEL_DESTINATION"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_TRAVEL_DESTINATION",
"required_dist": 0
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/clear_key",
"vars": {
"key": "BB_TRAVEL_DESTINATION"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/simple_hostile_obstacles_combat"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_CURRENT_HUNTING_TARGET",
"targeting_strategy": "/datum/targeting_strategy/dead_mob",
"target_source": "/datum/target_source/oview_single_type/human_mob"
}
}
]
}
@@ -0,0 +1,42 @@
{
"dm_type": "/datum/ai_controller/basic_controller/blob_zombie",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_TRAVEL_DESTINATION"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_TRAVEL_DESTINATION",
"required_dist": 0
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/clear_key",
"vars": {
"key": "BB_TRAVEL_DESTINATION"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/simple_hostile_obstacles_combat"
}
]
}
@@ -0,0 +1,14 @@
{
"dm_type": "/datum/ai_controller/basic_controller/blobbernaut",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/simple_hostile_obstacles_combat"
}
]
}
@@ -68,11 +68,10 @@ Difficulty: Medium
grant_actions_by_list(get_innate_actions())
ai_controller.set_blackboard_key(BB_BDM_RANGED_ATTACK_COOLDOWN, ranged_attack_cooldown_duration)
RegisterSignals(ai_controller, list(AI_CONTROLLER_BEHAVIOR_QUEUED(/datum/ai_behavior/basic_melee_attack), AI_CONTROLLER_BEHAVIOR_QUEUED(/datum/ai_behavior/targeted_mob_ability)), PROC_REF(handle_saw_transformation))
RegisterSignal(src, COMSIG_LIVING_DROP_LOOT, PROC_REF(death_effect))
AddComponent(/datum/component/boss_music, 'sound/music/boss/bdm_boss.ogg', COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET))
AddComponent(/datum/component/boss_music, 'sound/music/boss/bdm_boss.ogg', COMSIG_AI_BLACKBOARD_KEY_SET(BB_CURRENT_TARGET))
/// Block deletion of their saw under normal circumstances. It is fused to their hands as far as we're concerned.
/mob/living/basic/boss/blood_drunk_miner/proc/on_saw_deleted(datum/source, force)
@@ -99,12 +98,6 @@ Difficulty: Medium
)
return innate_abilities
/// Invokes the transform weapon ability when signaled by the AI controller.
/mob/living/basic/boss/blood_drunk_miner/proc/handle_saw_transformation()
SIGNAL_HANDLER
INVOKE_ASYNC(ai_controller.blackboard[BB_BDM_TRANSFORM_WEAPON_ABILITY], TYPE_PROC_REF(/datum/action, Trigger), src, NONE)
/mob/living/basic/boss/blood_drunk_miner/proc/transform_saw()
miner_saw.attack_self(src)
var/saw_open = HAS_TRAIT(miner_saw, TRAIT_TRANSFORM_ACTIVE)
@@ -5,6 +5,7 @@
/// - If in melee range, use melee attacks (depending on saw state)
/// - After attacks, transform saw state from open to closed.
/datum/ai_controller/blood_drunk_miner
behavior_tree_json = "code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_miner.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/no_gutted_mobs,
BB_TARGET_MINIMUM_STAT = DEAD,
@@ -14,53 +15,10 @@
movement_delay = 0.25 SECONDS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/shoot_pka,
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/dash_attack,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/// Parent type that contains key logic important for subsequent abilities
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk
/// Range where we determine what distance we're at. If higher, we consider ourselves out of PKA range and will dash attack instead. Inclusive when it comes to choosing to shoot PKA.
var/pka_range = 3
/// Check our blackboard to see if we are able to use a ranged ability in the first place
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/additional_ability_checks(datum/ai_controller/controller, datum/action/cooldown/using_action)
. = ..()
if(controller.blackboard[BB_BDM_RANGED_ATTACK_COOLDOWN] > world.time)
return FALSE
controller.override_blackboard_key(BB_BDM_RANGED_ATTACK_COOLDOWN, world.time + controller.blackboard[BB_BDM_RANGED_ATTACK_COOLDOWN_DURATION])
/// The BDM will preferentially shoot its PKA within range over other abilities
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/shoot_pka
ability_key = BB_BDM_KINETIC_ACCELERATOR_ABILITY
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/shoot_pka/additional_ability_checks(datum/ai_controller/controller, datum/action/cooldown/using_action)
. = ..()
// do not shoot the PKA if we are not in the right range
var/mob/living/pawn = controller.pawn
var/mob/living/victim = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(get_dist(pawn, victim) >= pka_range)
return FALSE
return TRUE
/// The BDM will dash attack if not in PKA range
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/dash_attack
ability_key = BB_BDM_DASH_ATTACK_ABILITY
/datum/ai_planning_subtree/targeted_mob_ability/blood_drunk/dash_attack/additional_ability_checks(datum/ai_controller/controller, datum/action/cooldown/using_action)
. = ..()
// only dash attack if we are out of PKA range
var/mob/living/pawn = controller.pawn
var/mob/living/victim = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(get_dist(pawn, victim) < pka_range)
return FALSE
return TRUE
/datum/ai_controller/blood_drunk_miner/doom
movement_delay = 0.5 SECONDS
/datum/bt_node/subtree/blood_drunk_miner_combat
behavior_tree_json = "code/modules/mob/living/basic/boss/blood_drunk_miner/blood_drunk_miner_combat.bt.json"
@@ -0,0 +1,45 @@
{
"dm_type": "/datum/ai_controller/blood_drunk_miner",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": "subtree",
"subtype": "/datum/bt_node/subtree/blood_drunk_miner_combat"
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/wait",
"vars": {
"duration": 0
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
@@ -0,0 +1,132 @@
{
"dm_type": "/datum/bt_node/subtree/blood_drunk_miner_combat",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": true,
"finish_on_primary": true,
"children": [
{
"type": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/attack_obstructions",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/mob_stat_at_least",
"vars": {
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_BDM_KINETIC_ACCELERATOR_ABILITY",
"target_key": "BB_CURRENT_TARGET",
"maximum_distance": 3
}
},
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_BDM_TRANSFORM_WEAPON_ABILITY",
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/succeed"
}
]
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/is_at_distance",
"vars": {
"invert": false,
"target_key": "BB_CURRENT_TARGET",
"min_distance": 3
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_BDM_DASH_ATTACK_ABILITY",
"target_key": "BB_CURRENT_TARGET"
}
}
},
{
"type": "sequence",
"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": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_BDM_TRANSFORM_WEAPON_ABILITY",
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/succeed"
}
]
}
]
}
]
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"required_dist": 1,
"finish_on_arrival": false
}
}
]
}
}
+2 -1
View File
@@ -81,7 +81,8 @@
return
if(should_devour(target))
devour(target)
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
return BASIC_MOB_END_ATTACK_CHAIN_COOLDOWN
return
/// Determines if this mob is worth devouring
/mob/living/basic/boss/proc/should_devour(mob/living/victim)
@@ -69,8 +69,8 @@
if(!maploaded)
return
spawn_loc = loc
RegisterSignal(src, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(target_gained))
RegisterSignal(src, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(target_lost))
RegisterSignal(src, COMSIG_AI_BLACKBOARD_KEY_SET(BB_CURRENT_TARGET), PROC_REF(target_gained))
RegisterSignal(src, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_CURRENT_TARGET), PROC_REF(target_lost))
SSqueuelinks.add_to_queue(src, RUIN_QUEUE, 0)
return INITIALIZE_HINT_LATELOAD
@@ -179,9 +179,9 @@
/// Immediately set out blackboard target key (if empty) to whoever attacks us; this is primarily because it has a lowered aggro range and a high sight range
/mob/living/basic/boss/thing/proc/immediate_aggro(datum/source, mob/attacker, flags)
SIGNAL_HANDLER
if(isnull(ai_controller) || stat || !istype(attacker) || ai_controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
if(isnull(ai_controller) || stat || !istype(attacker) || ai_controller.blackboard_key_exists(BB_CURRENT_TARGET))
return
ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, attacker)
ai_controller?.set_blackboard_key(BB_CURRENT_TARGET, attacker)
/mob/living/basic/boss/thing/vv_edit_var(vname, vval)
. = ..()
@@ -303,7 +303,7 @@
/// queue id
var/queue_id = RUIN_QUEUE
/// blackboard key for target
var/target_bb_key = BB_BASIC_MOB_CURRENT_TARGET
var/target_bb_key = BB_CURRENT_TARGET
/obj/structure/aggro_gate/Initialize(mapload)
. = ..()
@@ -1,72 +1,17 @@
/datum/ai_controller/basic_controller/thing_boss
behavior_tree_json = "code/modules/mob/living/basic/boss/thing/thing_boss.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/no_gutted_mobs,
BB_TARGET_MINIMUM_STAT = DEAD, // Will attack dead ungutted mobs
BB_THETHING_ATTACKMODE = TRUE, //Whether we are using our melee abilities right now
BB_THETHING_MELEEMODE = TRUE, //Whether we are using our melee abilities right now
BB_THETHING_NOAOE = TRUE, // Restricts us to only melee abilities
BB_THETHING_LASTAOE = null, // Last AOE ability key executed
BB_AGGRO_RANGE = 16,
BB_AGGRO_GRAB_RANGE = 6,
)
ai_movement = /datum/ai_movement/basic_avoidance // dont need anything better because the arena is a square lol
idle_behavior = null
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/thing_boss_aoe,
/datum/ai_planning_subtree/thing_boss_melee,
)
/datum/ai_planning_subtree/thing_boss_aoe/SelectBehaviors(datum/ai_controller/monkey/controller, seconds_per_tick)
var/mob/living/pawn = controller.pawn
if(HAS_TRAIT_FROM(pawn, TRAIT_IMMOBILIZED, MEGAFAUNA_TRAIT) || (controller.blackboard[BB_THETHING_ATTACKMODE] || controller.blackboard[BB_THETHING_NOAOE]))
return
// our target
var/mob/living/shaft_miner = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(shaft_miner) || shaft_miner.stat == DEAD) //Dont use abilities on off z level targets, or dead shaft miners. We want to melee those.
return
/datum/bt_node/subtree/thing_aoe
behavior_tree_json = "code/modules/mob/living/basic/boss/thing/thing_aoe.bt.json"
controller.set_blackboard_key(BB_THETHING_ATTACKMODE, TRUE) // putting this here so we go to melee mode if we cant do any aoe
var/static/list/aoe_attacks = list(BB_THETHING_DECIMATE, BB_THETHING_BIGTENDRILS, BB_THETHING_CARDTENDRILS, BB_THETHING_ACIDSPIT)
var/list/possible_attacks = aoe_attacks.Copy() - controller.blackboard[BB_THETHING_LASTAOE]
for(var/bb_action_key in possible_attacks)
var/datum/action/action = controller.blackboard[bb_action_key]
if(!action?.IsAvailable())
possible_attacks -= bb_action_key
if(!length(possible_attacks))
return
var/current_aoe_key = pick(possible_attacks)
controller.set_blackboard_key(BB_THETHING_LASTAOE, current_aoe_key)
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability, current_aoe_key, BB_BASIC_MOB_CURRENT_TARGET)
if(prob(60) && shaft_miner.body_position != LYING_DOWN) //potential follow-up
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability, BB_THETHING_CHARGE, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_planning_subtree/thing_boss_melee/SelectBehaviors(datum/ai_controller/monkey/controller, seconds_per_tick)
var/mob/living/pawn = controller.pawn
if(HAS_TRAIT_FROM(pawn, TRAIT_IMMOBILIZED, MEGAFAUNA_TRAIT))
return
// our target
var/mob/living/shaft_miner = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(shaft_miner))
return
var/target_dist = get_dist(pawn, shaft_miner)
var/datum/action/shriek = controller.blackboard[BB_THETHING_SHRIEK]
var/datum/action/charge = controller.blackboard[BB_THETHING_CHARGE]
if(isnull(shriek) || isnull(charge))
return // pray this never occurs
controller.set_blackboard_key(BB_THETHING_ATTACKMODE, FALSE)
if(shriek.IsAvailable() && target_dist <= 2 && shaft_miner.stat != DEAD)
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/min_range/short, BB_THETHING_SHRIEK, BB_BASIC_MOB_CURRENT_TARGET)
return
else if(charge.IsAvailable() && target_dist >= 5) // While we cant hit prone targets, this helps to close the distance.
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability, BB_THETHING_CHARGE, BB_BASIC_MOB_CURRENT_TARGET)
return
controller.queue_behavior(/datum/ai_behavior/basic_melee_attack, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION)
/datum/bt_node/subtree/thing_melee
behavior_tree_json = "code/modules/mob/living/basic/boss/thing/thing_melee.bt.json"
@@ -0,0 +1,72 @@
{
"dm_type": "/datum/bt_node/subtree/thing_aoe",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_true",
"vars": {
"invert": true,
"key": "BB_THETHING_NOAOE"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_true",
"vars": {
"invert": true,
"key": "BB_THETHING_MELEEMODE"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/pawn_has_trait_from",
"vars": {
"observer_abort": "BT_ABORT_SELF",
"invert": true,
"trait": "TRAIT_IMMOBILIZED",
"source": "MEGAFAUNA_TRAIT"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_THETHING_MELEEMODE",
"value": true
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/pick_random_ability",
"vars": {
"ability_keys": "list(BB_THETHING_DECIMATE, BB_THETHING_BIGTENDRILS, BB_THETHING_CARDTENDRILS, BB_THETHING_ACIDSPIT)",
"last_used_key": "BB_THETHING_LASTAOE",
"result_key": "BB_GENERIC_ACTION"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_GENERIC_ACTION",
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/random_chance",
"vars": {
"chance": 0.6
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_THETHING_CHARGE",
"target_key": "BB_CURRENT_TARGET"
}
}
}
]
}
}
}
}
@@ -0,0 +1,70 @@
{
"dm_type": "/datum/ai_controller/basic_controller/thing_boss",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/thing_aoe"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/thing_melee"
}
]
}
]
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/wait",
"vars": {
"duration": 0
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION",
"vision_range": 6
}
}
]
}
]
}
@@ -0,0 +1,89 @@
{
"dm_type": "/datum/bt_node/subtree/thing_melee",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/true_for_time",
"vars": {
"observer_abort": "BT_ABORT_SELF",
"duration": "5 SECONDS"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_THETHING_MELEEMODE",
"value": false
}
},
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_THETHING_SHRIEK",
"target_key": "BB_CURRENT_TARGET",
"maximum_distance": 2
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/is_at_distance",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"min_distance": 3
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability",
"vars": {
"ability_key": "BB_THETHING_CHARGE",
"target_key": "BB_CURRENT_TARGET"
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/pawn_has_trait_from",
"vars": {
"observer_abort": "BT_ABORT_SELF",
"invert": true,
"trait": "TRAIT_IMMOBILIZED",
"source": "MEGAFAUNA_TRAIT"
},
"child": {
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": true,
"finish_on_primary": true,
"children": [
{
"type": "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
}
}
]
}
}
]
}
]
}
}
@@ -0,0 +1,22 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
}
+14 -269
View File
@@ -1,6 +1,5 @@
#define BOT_NO_BEACON_PATH_PENALTY 30 SECONDS
/datum/ai_controller/basic_controller/bot
behavior_tree_json = "code/modules/mob/living/basic/bots/bot.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_SALUTE_MESSAGES = list(
@@ -11,24 +10,16 @@
)
ai_movement = /datum/ai_movement/jps/bot
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/find_patrol_beacon,
)
max_target_distance = AI_BOT_PATH_LENGTH
can_idle = FALSE
///minimum distance we need to be from our target in path calculations
var/minimum_distance = 0
///keys to be reset when the bot is reseted
ai_traits = DEFAULT_AI_FLAGS | RUN_WHILE_UNWATCHED
///keys to be reset when the bot is reset
var/list/reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
)
/datum/targeting_strategy/basic/bot/can_attack(mob/living/living_mob, atom/the_target, vision_range)
/datum/targeting_strategy/basic/bot/is_valid_target(mob/living/living_mob, atom/the_target, vision_range, datum/ai_controller/controller = null)
var/datum/ai_controller/basic_controller/bot/my_controller = living_mob.ai_controller
if(isnull(my_controller))
return FALSE
@@ -39,9 +30,8 @@
return FALSE
if(get_turf(living_mob) == get_turf(living_target))
return ..()
var/list/path = get_path_to(living_mob, living_target, mintargetdist = my_controller.minimum_distance, max_distance = 10, access = my_controller.get_access())
if(!length(path) || QDELETED(living_mob))
my_controller?.add_to_blacklist(living_target)
if(!my_controller.can_reach_target(living_target, distance = 10))
my_controller.add_to_blacklist(living_target)
return FALSE
return ..()
@@ -56,7 +46,7 @@
/datum/ai_controller/basic_controller/bot/proc/on_movement_start(mob/living/basic/bot/source, atom/target)
SIGNAL_HANDLER
if(current_movement_target == blackboard[BB_BEACON_TARGET])
if(target == blackboard[BB_BEACON_TARGET])
source.update_bot_mode(new_mode = BOT_PATROL)
return
@@ -69,6 +59,10 @@
set_blackboard_key_assoc_lazylist(BB_TEMPORARY_IGNORE_LIST, target, TRUE)
addtimer(CALLBACK(src, PROC_REF(remove_from_blacklist), target), final_duration)
/// Unreachable targets get added to the temporary ignore list so we stop pathing to them. Subtypes override to skip blacklisting in stationary mode.
/datum/ai_controller/basic_controller/bot/note_unreachable_target(atom/target)
add_to_blacklist(target)
/datum/ai_controller/basic_controller/bot/proc/remove_from_blacklist(atom/target)
if(QDELETED(target))
return
@@ -100,15 +94,15 @@
/datum/ai_controller/basic_controller/bot/proc/reset_bot()
SIGNAL_HANDLER
CancelActions()
cancel_current_plan()
if(!length(reset_keys))
return
for(var/key in reset_keys)
clear_blackboard_key(key)
///set the target if we can reach them
/datum/ai_controller/basic_controller/bot/proc/set_if_can_reach(key, target, duration, distance = 10, bypass_add_to_blacklist = FALSE)
if(can_reach_target(target, distance))
/datum/ai_controller/basic_controller/bot/proc/set_if_can_reach(key, target, duration, distance = 10, bypass_add_to_blacklist = FALSE, minimum_distance = 0)
if(can_reach_target(target, distance, minimum_distance))
EVLOG_MAPTEXT(src, EVLOG_CATEGORY_AI_TARGETING, "[pawn] has selected [target] as a target for blackboard key [key]!", get_turf(target), "Target: [target]")
EVLOG_LINES(src, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(pawn), get_turf(target))
set_blackboard_key(key, target)
@@ -120,252 +114,3 @@
EVLOG_LINES(src, EVLOG_CATEGORY_AI_TARGETING, "Line to target", get_turf(pawn), get_turf(target))
add_to_blacklist(target, final_duration)
return FALSE
/datum/ai_controller/basic_controller/bot/proc/can_reach_target(target, distance = 10)
if(!isdatum(target)) //we dont need to check if its not a datum!
return TRUE
if(get_turf(pawn) == get_turf(target))
return TRUE
var/list/path = get_path_to(pawn, target, simulated_only = !HAS_TRAIT(pawn, TRAIT_SPACEWALK), mintargetdist = minimum_distance, max_distance = distance, access = get_access())
return (!!length(path))
/datum/ai_planning_subtree/find_patrol_beacon
///travel towards beacon behavior
var/travel_behavior = /datum/ai_behavior/travel_towards/beacon
/datum/ai_planning_subtree/find_patrol_beacon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/bot/bot_pawn = controller.pawn
if(controller.blackboard[BB_BOT_BEACON_COOLDOWN] > world.time)
return
if(!(bot_pawn.bot_mode_flags & BOT_MODE_AUTOPATROL) || bot_pawn.mode == BOT_SUMMON)
return
if(controller.blackboard_key_exists(BB_BEACON_TARGET))
controller.queue_behavior(travel_behavior, BB_BEACON_TARGET)
return
if(controller.blackboard_key_exists(BB_PREVIOUS_BEACON_TARGET))
controller.queue_behavior(/datum/ai_behavior/find_next_beacon_target, BB_BEACON_TARGET)
return
controller.queue_behavior(/datum/ai_behavior/find_first_beacon_target, BB_BEACON_TARGET)
/datum/ai_behavior/find_first_beacon_target
/datum/ai_behavior/find_first_beacon_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/closest_distance = INFINITY
var/mob/living/basic/bot/bot_pawn = controller.pawn
var/atom/final_target
var/atom/previous_target = controller.blackboard[BB_PREVIOUS_BEACON_TARGET]
for(var/obj/machinery/navbeacon/beacon as anything in GLOB.navbeacons["[bot_pawn.z]"])
var/dist = get_dist(bot_pawn, beacon)
if(beacon == previous_target || dist <= 1)
continue
if(dist > closest_distance)
continue
closest_distance = dist
final_target = beacon
if(isnull(final_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_BEACON_TARGET, final_target)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_next_beacon_target
action_cooldown = 5 SECONDS
/datum/ai_behavior/find_next_beacon_target/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key)
var/mob/living/basic/bot/bot_pawn = controller.pawn
var/atom/final_target
var/obj/machinery/navbeacon/prev_beacon = controller.blackboard[BB_PREVIOUS_BEACON_TARGET]
if(QDELETED(prev_beacon))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
for(var/obj/machinery/navbeacon/beacon as anything in GLOB.navbeacons["[bot_pawn.z]"])
if(beacon.location == prev_beacon.codes[NAVBEACON_PATROL_NEXT])
final_target = beacon
break
if(isnull(final_target))
controller.clear_blackboard_key(BB_PREVIOUS_BEACON_TARGET) //failed to find the next beacon, search for a first beacon again
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_PREVIOUS_BEACON_TARGET, final_target)
controller.clear_blackboard_key(BB_BEACON_TARGET)
if(LAZYACCESS(controller.blackboard[BB_TEMPORARY_IGNORE_LIST], final_target) || get_dist(bot_pawn, final_target) > controller.max_target_distance)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(controller.set_if_can_reach(key = BB_BEACON_TARGET, target = final_target, duration = 3 MINUTES, distance = controller.max_target_distance))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
controller.set_blackboard_key(BB_BOT_BEACON_COOLDOWN, world.time + BOT_NO_BEACON_PATH_PENALTY)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/travel_towards/beacon
clear_target = TRUE
new_movement_type = /datum/ai_movement/jps/bot/travel_to_beacon
/datum/ai_behavior/travel_towards/beacon/setup(datum/ai_controller/controller, target_key)
var/atom/target_beacon = controller.blackboard[target_key]
if(LAZYACCESS(controller.blackboard[BB_TEMPORARY_IGNORE_LIST], target_beacon))
return FALSE
return ..()
/datum/ai_behavior/travel_towards/beacon/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, target_key)
var/atom/target = controller.blackboard[target_key]
if(!succeeded)
controller.set_blackboard_key(BB_BOT_BEACON_COOLDOWN, world.time + BOT_NO_BEACON_PATH_PENALTY)
controller.add_to_blacklist(target, 3 MINUTES)
controller.set_blackboard_key(BB_PREVIOUS_BEACON_TARGET, target)
return ..()
/datum/ai_planning_subtree/respond_to_summon
/datum/ai_planning_subtree/respond_to_summon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(!controller.blackboard_key_exists(BB_BOT_SUMMON_TARGET))
return
controller.queue_behavior(/datum/ai_behavior/travel_towards/bot_summon, BB_BOT_SUMMON_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/travel_towards/bot_summon
clear_target = TRUE
new_movement_type = /datum/ai_movement/jps/bot/travel_to_beacon
/datum/ai_behavior/travel_towards/bot_summon/finish_action(datum/ai_controller/controller, succeeded, target_key)
var/mob/living/basic/bot/bot_pawn = controller.pawn
if(QDELETED(bot_pawn)) // pawn can be null at this point
return ..()
bot_pawn.calling_ai_ref = null
bot_pawn.update_bot_mode(new_mode = BOT_IDLE)
return ..()
/datum/ai_planning_subtree/salute_authority
/datum/ai_planning_subtree/salute_authority/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/bot/bot_pawn = controller.pawn
//we are criminals, dont salute the dirty pigs
if(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED)
return
if(controller.blackboard_key_exists(BB_SALUTE_TARGET))
controller.queue_behavior(/datum/ai_behavior/salute_authority, BB_SALUTE_TARGET, BB_SALUTE_MESSAGES)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set/valid_authority, BB_SALUTE_TARGET)
/datum/ai_behavior/find_and_set/valid_authority
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
action_cooldown = BOT_COMMISSIONED_SALUTE_DELAY
/datum/ai_behavior/find_and_set/valid_authority/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
for(var/mob/living/nearby_mob in oview(search_range, controller.pawn))
if(!HAS_TRAIT(nearby_mob, TRAIT_COMMISSIONED))
continue
return nearby_mob
return null
/datum/ai_behavior/salute_authority
/datum/ai_behavior/salute_authority/perform(seconds_per_tick, datum/ai_controller/controller, target_key, salute_keys)
if(!controller.blackboard_key_exists(target_key))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/list/salute_list = controller.blackboard[salute_keys]
if(!length(salute_list))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/basic/bot/bot_pawn = controller.pawn
//special interaction if we are wearing a fedora
var/obj/item/our_hat = (locate(/obj/item/clothing/head) in bot_pawn)
if(our_hat)
salute_list += "tips [our_hat] at "
bot_pawn.manual_emote(pick(salute_list) + " [controller.blackboard[target_key]]!")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/salute_authority/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/bot_search
action_cooldown = 2 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/bot_search/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key, looking_for, radius = 5, pathing_distance = 10, bypass_add_blacklist = FALSE, turf_search = FALSE)
if(!istype(controller))
stack_trace("attempted to give [controller.pawn] the bot search behavior!")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/living_pawn = controller.pawn
var/list/ignore_list = controller.blackboard[BB_TEMPORARY_IGNORE_LIST]
var/list/objects_to_search = turf_search ? RANGE_TURFS(radius, controller.pawn) : oview(radius, controller.pawn) //use range turfs instead of oview when we can for performance
for(var/atom/potential_target as anything in objects_to_search)
if(QDELETED(living_pawn))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(!is_type_in_typecache(potential_target, looking_for))
continue
if(LAZYACCESS(ignore_list, potential_target))
continue
if(!valid_target(controller, potential_target))
continue
if(!can_see(controller.pawn, potential_target, radius))
continue
if(controller.set_if_can_reach(key = target_key, target = potential_target, distance = pathing_distance, bypass_add_to_blacklist = bypass_add_blacklist))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/bot_search/proc/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
return TRUE
///behavior to make our bot talk
/datum/ai_behavior/bot_speech
action_cooldown = 5 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/bot_speech/perform(seconds_per_tick, datum/ai_controller/controller, list/list_to_pick_from, announce_key)
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[announce_key]
if(isnull(announcement) || !length(list_to_pick_from))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
announcement.announce(pick(list_to_pick_from))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
///behavior to interact with atoms
/datum/ai_behavior/bot_interact
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
///should we remove the target afterwards?
var/clear_target = TRUE
/datum/ai_behavior/bot_interact/setup(datum/ai_controller/controller, target_key)
. = ..()
var/turf/target = controller.blackboard[target_key]
if(isnull(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/bot_interact/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/basic/living_pawn = controller.pawn
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
living_pawn.UnarmedAttack(target, proximity_flag = TRUE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/bot_interact/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(clear_target)
controller.clear_blackboard_key(target_key)
if(!succeeded && !isnull(target))
controller.add_to_blacklist(target)
/datum/ai_behavior/bot_interact/keep_target
clear_target = FALSE
#undef BOT_NO_BEACON_PATH_PENALTY
@@ -32,7 +32,7 @@
set_hud_image_state(DIAG_BOT_HUD, "")
///proc that handles drawing and transforming the bot's path onto diagnostic huds
/mob/living/basic/bot/proc/generate_bot_path(datum/move_loop/has_target/jps/source)
/mob/living/basic/bot/proc/generate_bot_path(datum/move_loop/has_target/jps/source, list/path)
SIGNAL_HANDLER
UnregisterSignal(src, COMSIG_MOVELOOP_JPS_FINISHED_PATHING)
@@ -40,8 +40,11 @@
if(isnull(ai_controller))
return
if(!length(path))
return
var/atom/move_target = ai_controller.current_movement_target
var/atom/move_target = path[path.len]
if(move_target != ai_controller.blackboard[BB_BEACON_TARGET])
return
@@ -0,0 +1,28 @@
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_PET_TARGET"
},
"child": {
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_PET_TARGET",
"required_dist": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/execute_clean",
"vars": {
"target_key": "BB_CURRENT_PET_TARGET"
}
}
]
}
}
@@ -0,0 +1,251 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/cleanbot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/override_id_set",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"override_id": "SUBPLAN_ID_PET_COMMAND"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree",
"override_id": "SUBPLAN_ID_PET_COMMAND"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_is_emagged",
"vars": {
"invert": true
},
"child": {
"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_BOTH"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"required_dist": 1,
"finish_on_arrival": true
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/execute_clean",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
}
]
}
},
{
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_FRIENDLY_JANITOR"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_FRIENDLY_JANITOR",
"required_dist": 1,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/befriend_target",
"vars": {
"target_key": "BB_FRIENDLY_JANITOR",
"befriend_message": "BB_FRIENDLY_MESSAGE"
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_FRIENDLY_JANITOR",
"invert": true
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_FRIENDLY_JANITOR",
"target_source": "/datum/target_source/oview_single_type/human_mob",
"targeting_strategy": "/datum/targeting_strategy/conscious_human/cleanbot_whisperer",
"vision_range": 5,
"time_between_perform": "30 SECONDS"
}
}
}
]
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"observer_abort": "BT_ABORT_NONE",
"cooldown_key": "BB_POST_CLEAN_COOLDOWN"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"time_between_perform": "3 SECONDS",
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "/datum/targeting_strategy/pickup_item",
"target_source": "/datum/target_source/cleanbot_cleanables",
"vision_range": 5,
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"must_be_reachable": true,
"reach_distance": 15
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_is_emagged",
"child": {
"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/cooldown",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"cooldown_key": "BB_ACID_SPRAY_COOLDOWN",
"cooldown_duration": 30
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_ACID_SPRAY_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_ACID_SPRAY_TARGET",
"required_dist": 0,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/execute_clean",
"vars": {
"target_key": "BB_ACID_SPRAY_TARGET"
}
}
]
}
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/use_mob_ability",
"vars": {
"ability_key": "BB_CLEANBOT_FOAM"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"cooldown_key": "BB_POST_CLEAN_COOLDOWN"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_ACID_SPRAY_TARGET",
"target_source": "/datum/target_source/oview_single_type/human_mob",
"targeting_strategy": "/datum/targeting_strategy/conscious_human",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"vision_range": 5,
"time_between_perform": "30 SECONDS"
}
}
]
}
}
]
}
@@ -2,6 +2,7 @@
#define POST_CLEAN_COOLDOWN 5 SECONDS
/datum/ai_controller/basic_controller/bot/cleanbot
behavior_tree_json = "code/modules/mob/living/basic/bots/cleanbot/cleanbot.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/allow_items,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
@@ -13,20 +14,9 @@
),
BB_FRIENDLY_MESSAGE = "empathetically acknowledges your hardwork and tough circumstances",
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/pet_planning/cleanbot,
/datum/ai_planning_subtree/cleaning_subtree,
/datum/ai_planning_subtree/befriend_janitors,
/datum/ai_planning_subtree/acid_spray,
/datum/ai_planning_subtree/use_mob_ability/foam_area,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/find_patrol_beacon/cleanbot,
)
reset_keys = list(
BB_ACTIVE_PET_COMMAND,
BB_CLEAN_TARGET,
BB_CURRENT_TARGET,
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
@@ -39,99 +29,40 @@
BB_HUNTABLE_TRASH = CLEANBOT_CLEAN_TRASH,
)
/datum/ai_planning_subtree/pet_planning/cleanbot/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/bot_pawn = controller.pawn
//we are DONE listening to orders
if(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED)
return
return ..()
/// Gathers nearby cleanable atoms: decals plus whatever types the cleanbot's currently enabled janitor mode flags allow.
/datum/target_source/cleanbot_cleanables
/datum/ai_planning_subtree/cleaning_subtree
/datum/ai_planning_subtree/cleaning_subtree/SelectBehaviors(datum/ai_controller/basic_controller/bot/cleanbot/controller, seconds_per_tick)
if(controller.blackboard_key_exists(BB_CLEAN_TARGET))
controller.queue_behavior(/datum/ai_behavior/execute_clean, BB_CLEAN_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/target_source/cleanbot_cleanables/collect_candidates(mob/living/pawn, datum/ai_controller/basic_controller/bot/cleanbot/controller, range)
var/list/final_hunt_list = list()
final_hunt_list += controller.blackboard[BB_CLEANABLE_DECALS]
var/list/flag_list = controller.clean_flags
var/mob/living/basic/bot/cleanbot/bot_pawn = controller.pawn
for(var/list_key in flag_list)
if(!(bot_pawn.janitor_mode_flags & flag_list[list_key]))
var/mob/living/basic/bot/cleanbot/bot_pawn = pawn
for(var/list_key in controller.clean_flags)
if(!(bot_pawn.janitor_mode_flags & controller.clean_flags[list_key]))
continue
final_hunt_list += controller.blackboard[list_key]
if(!length(final_hunt_list))
return list()
var/list/type_filter = typecacheof(final_hunt_list)
return typecache_filter_list(oview(range, pawn), type_filter)
controller.queue_behavior(/datum/ai_behavior/find_and_set/in_list/clean_targets, BB_CLEAN_TARGET, final_hunt_list)
///clean that shit bro fr fr 67
/datum/bt_node/ai_behavior/execute_clean
var/target_key
/datum/ai_behavior/find_and_set/in_list/clean_targets
action_cooldown = 3 SECONDS
/datum/ai_behavior/find_and_set/in_list/clean_targets/search_tactic(datum/ai_controller/basic_controller/bot/controller, locate_paths, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
var/list/found = typecache_filter_list(oview(search_range, controller.pawn), locate_paths)
var/list/ignore_list = controller.blackboard[BB_TEMPORARY_IGNORE_LIST]
for(var/atom/found_item in found)
if(QDELETED(controller.pawn))
break
if(LAZYACCESS(ignore_list, found_item))
continue
if(get_turf(found_item) == get_turf(controller.pawn))
return found_item
var/list/path = get_path_to(controller.pawn, found_item, max_distance = BOT_CLEAN_PATH_LIMIT, access = controller.get_access())
if(!length(path))
controller.add_to_blacklist(found_item)
continue
return found_item
/datum/ai_planning_subtree/acid_spray
/datum/ai_planning_subtree/acid_spray/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/cleanbot/bot_pawn = controller.pawn
if(!(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED))
return
if(controller.blackboard_key_exists(BB_ACID_SPRAY_TARGET))
controller.queue_behavior(/datum/ai_behavior/execute_clean, BB_ACID_SPRAY_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set/spray_target, BB_ACID_SPRAY_TARGET, /mob/living/carbon/human, 5)
/datum/ai_behavior/find_and_set/spray_target
action_cooldown = 30 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/find_and_set/spray_target/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
var/list/ignore_list = controller.blackboard[BB_TEMPORARY_IGNORE_LIST]
for(var/mob/living/carbon/human/human_target in oview(search_range, controller.pawn))
if(LAZYACCESS(ignore_list, human_target))
continue
if(human_target.stat != CONSCIOUS || isnull(human_target.mind))
continue
return human_target
return null
/datum/ai_behavior/execute_clean
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/execute_clean/setup(datum/ai_controller/controller, target_key)
. = ..()
var/turf/target = controller.blackboard[target_key]
if(isnull(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/execute_clean/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
/datum/bt_node/ai_behavior/execute_clean/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/basic/living_pawn = controller.pawn
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
EVLOG_TEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[living_pawn] execute_clean: target deleted")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(get_dist(living_pawn, target) > 1)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[living_pawn] cleaning [target]", get_turf(target), "Cleaning")
living_pawn.UnarmedAttack(target, proximity_flag = TRUE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/execute_clean/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
/datum/bt_node/ai_behavior/execute_clean/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded)
. = ..()
controller.set_blackboard_key(BB_POST_CLEAN_COOLDOWN, POST_CLEAN_COOLDOWN + world.time)
var/atom/target = controller.blackboard[target_key]
@@ -147,56 +78,25 @@
var/list/speech_list = controller.blackboard[BB_CLEANBOT_EMAGGED_PHRASES]
if(length(speech_list))
var/mob/living/living_pawn = controller.pawn
if(!QDELETED(living_pawn)) // pawn can be null at this point
living_pawn.say(pick(speech_list), forced = "ai controller")
if(!QDELETED(living_pawn))
INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/atom/movable, say), pick(speech_list), forced = "ai controller")
controller.clear_blackboard_key(target_key)
/datum/ai_planning_subtree/use_mob_ability/foam_area
ability_key = BB_CLEANBOT_FOAM
finish_planning = FALSE
/datum/ai_planning_subtree/use_mob_ability/foam_area/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/bot_pawn = controller.pawn
if(!(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED))
return
return ..()
/// Valid if the target is a conscious human janitor-whisperer the cleanbot hasn't already befriended.
/datum/targeting_strategy/conscious_human/cleanbot_whisperer/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
if(!HAS_TRAIT(target, TRAIT_CLEANBOT_WHISPERER))
return FALSE
return !living_mob.has_ally(REF(target))
/datum/ai_planning_subtree/befriend_janitors
/datum/ai_planning_subtree/befriend_janitors/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/bot_pawn = controller.pawn
//we are now evil. dont befriend the janitors
if(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED)
return
if(controller.blackboard_key_exists(BB_FRIENDLY_JANITOR))
controller.queue_behavior(/datum/ai_behavior/befriend_target, BB_FRIENDLY_JANITOR, BB_FRIENDLY_MESSAGE)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set/friendly_janitor, BB_FRIENDLY_JANITOR, /mob/living/carbon/human, 5)
/datum/ai_behavior/find_and_set/friendly_janitor
action_cooldown = 30 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/find_and_set/friendly_janitor/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
var/mob/living/living_pawn = controller.pawn
for(var/mob/living/carbon/human/human_target in oview(search_range, living_pawn))
if(human_target.stat != CONSCIOUS || isnull(human_target.mind))
continue
if(!HAS_TRAIT(human_target, TRAIT_CLEANBOT_WHISPERER))
continue
if(living_pawn.has_ally(REF(human_target)))
continue
return human_target
return null
/datum/ai_planning_subtree/find_patrol_beacon/cleanbot
/datum/ai_planning_subtree/find_patrol_beacon/cleanbot/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
if(controller.blackboard[BB_POST_CLEAN_COOLDOWN] >= world.time)
return
return ..()
/datum/bt_node/subtree/clean_pet_target
behavior_tree_json = "code/modules/mob/living/basic/bots/cleanbot/clean_pet_target.bt.json"
///Tells the cleanbot to go clean a target
/datum/pet_command/clean
command_name = "Clean"
command_desc = "Command a cleanbot to clean the mess."
@@ -215,11 +115,11 @@
return ..()
/datum/pet_command/clean/execute_action(datum/ai_controller/basic_controller/bot/controller)
if(controller.blackboard_key_exists(BB_CURRENT_PET_TARGET))
controller.queue_behavior(/datum/ai_behavior/execute_clean, BB_CURRENT_PET_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
var/atom/target = controller.blackboard[BB_CURRENT_PET_TARGET]
if(QDELETED(target))
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
return
controller.set_behavior_tree_override(SUBPLAN_ID_PET_COMMAND, /datum/bt_node/subtree/clean_pet_target)
#undef BOT_CLEAN_PATH_LIMIT
#undef POST_CLEAN_COOLDOWN
@@ -0,0 +1,73 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/dedbot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"required_dist": 1,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability/melee",
"vars": {
"ability_key": "BB_DEDBOT_SLASH",
"target_key": "BB_CURRENT_TARGET"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
+1 -12
View File
@@ -49,31 +49,20 @@
grant_actions_by_list(innate_actions)
/datum/ai_controller/basic_controller/bot/dedbot
behavior_tree_json = "code/modules/mob/living/basic/bots/dedbot.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = DEAD,
BB_AGGRO_RANGE = 2,
)
ai_movement = /datum/ai_movement/jps/bot
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability/exenterate,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/find_patrol_beacon,
)
max_target_distance = AI_BOT_PATH_LENGTH
///keys to be reset when the bot is reseted
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
)
/datum/ai_planning_subtree/targeted_mob_ability/exenterate
ability_key = BB_DEDBOT_SLASH
finish_planning = FALSE
/datum/action/cooldown/mob_cooldown/exenterate
name = "Exenterate"
desc = "Disembowel every living thing in range with your blades."
@@ -0,0 +1,132 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/ed209",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": false,
"repeat_secondary_delay": "BASIC_MOB_FIND_TARGET_RATE",
"finish_on_primary": false,
"children": [
{
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/is_target_stunned",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"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/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": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
}
},
{
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": true,
"finish_on_primary": true,
"children": [
{
"type": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/basic_ranged_attack",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION",
"max_range": 9
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/maintain_distance",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"approach_movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
@@ -2,18 +2,13 @@
#define SPECIAL_LINES "special_lines"
/datum/ai_controller/basic_controller/bot/ed209
behavior_tree_json = "code/modules/mob/living/basic/bots/ed209/ed209.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/secbot,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_ALWAYS_IGNORE_FACTION = TRUE,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/ranged_skirmish,
/datum/ai_planning_subtree/arrest_target/ed209,
/datum/ai_planning_subtree/find_patrol_beacon,
BB_RANGED_SKIRMISH_MIN_DISTANCE = 2,
BB_RANGED_SKIRMISH_MAX_DISTANCE = 3
)
reset_keys = list(
BB_BEACON_TARGET,
@@ -26,7 +21,7 @@
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_target_set))
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_CURRENT_TARGET), PROC_REF(on_target_set))
/datum/ai_controller/basic_controller/bot/ed209/proc/on_target_set()
@@ -48,18 +43,5 @@
var/list/final_list = my_bot.sheriffized ? lines_to_pick[SPECIAL_LINES] : lines_to_pick[DEFAULT_LINES]
INVOKE_ASYNC(announcement, TYPE_PROC_REF(/datum/action/cooldown/bot_announcement, announce), pick(final_list))
/datum/ai_planning_subtree/arrest_target/ed209
arrest_behavior = /datum/ai_behavior/basic_melee_attack/interact_once/bot/ed209
/datum/ai_behavior/basic_melee_attack/interact_once/bot/ed209
action_cooldown = 0.5 SECONDS
/datum/ai_behavior/basic_melee_attack/interact_once/bot/ed209/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
. = ..()
if(!(. & AI_BEHAVIOR_DELAY))
return AI_BEHAVIOR_DELAY //this kinda sucks but we have to do this cause we need to shoot while moving to stun
#undef DEFAULT_LINES
#undef SPECIAL_LINES
@@ -2,13 +2,10 @@
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_RANGED_SKIRMISH_MIN_DISTANCE = 2,
BB_RANGED_SKIRMISH_MAX_DISTANCE = 3
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_ranged_attack_subtree,
/datum/ai_planning_subtree/find_patrol_beacon,
)
behavior_tree_json = "code/modules/mob/living/basic/bots/ed209/ed209_syndicate.bt.json"
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
@@ -0,0 +1,21 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/ed209/syndicate",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/simple_ranged_combat",
"bindings": {
"bsjjwub2": "/datum/bt_node/subtree/bot_patrol"
}
}
]
}
@@ -0,0 +1,119 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/firebot",
"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": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/announce_fire_detected"
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"required_dist": 1,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_interact/extinguish",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"target_source": "/datum/target_source/firebot_targets",
"targeting_strategy": "/datum/targeting_strategy/extinguishable_person",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"must_be_reachable": true,
"vision_range": 5
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"target_source": "/datum/target_source/range_turfs/firebot_hotspots",
"targeting_strategy": "/datum/targeting_strategy/burning_hotspot",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"must_be_reachable": true,
"vision_range": 5
}
}
]
}
]
}
]
},
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/handle_firebot_speech"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
}
]
}
]
}
@@ -5,17 +5,9 @@
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/allow_turfs,
BB_UNREACHABLE_LIST_COOLDOWN = 3 MINUTES,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/extinguishing_people,
/datum/ai_planning_subtree/extinguishing_turfs,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/firebot_speech,
/datum/ai_planning_subtree/find_patrol_beacon,
)
behavior_tree_json = "code/modules/mob/living/basic/bots/firebot/firebot.bt.json"
reset_keys = list(
BB_FIREBOT_EXTINGUISH_TARGET,
BB_CURRENT_TARGET,
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
@@ -27,106 +19,106 @@
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_FIREBOT_EXTINGUISH_TARGET), PROC_REF(on_target_found))
///say a silly line whenever we find someone on fire
/datum/ai_controller/basic_controller/bot/firebot/proc/on_target_found()
SIGNAL_HANDLER
if(!COOLDOWN_FINISHED(src, announcement_cooldown))
return
var/datum/action/cooldown/bot_announcement/announcement = blackboard[BB_ANNOUNCE_ABILITY]
/datum/bt_node/ai_behavior/announce_fire_detected
/datum/bt_node/ai_behavior/announce_fire_detected/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/firebot/controller)
if(!COOLDOWN_FINISHED(controller, announcement_cooldown))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
if(isnull(announcement))
return
var/list/lines = blackboard[BB_FIREBOT_FIRE_DETECTED_LINES]
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
var/list/lines = controller.blackboard[BB_FIREBOT_FIRE_DETECTED_LINES]
if(!length(lines))
return
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
INVOKE_ASYNC(announcement, TYPE_PROC_REF(/datum/action/cooldown/bot_announcement, announce), pick(lines))
COOLDOWN_START(src, announcement_cooldown, ANNOUNCEMENT_TIMER)
COOLDOWN_START(controller, announcement_cooldown, ANNOUNCEMENT_TIMER)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
///subtree for extinguishing people
/datum/ai_planning_subtree/extinguishing_people
/datum/ai_planning_subtree/extinguishing_people/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
if(controller.blackboard_key_exists(BB_FIREBOT_EXTINGUISH_TARGET))
controller.queue_behavior(/datum/ai_behavior/basic_melee_attack/interact_once/extinguish, BB_FIREBOT_EXTINGUISH_TARGET, BB_TARGETING_STRATEGY)
return SUBTREE_RETURN_FINISH_PLANNING
var/mob/living/basic/bot/firebot/living_bot = controller.pawn
var/range = living_bot.firebot_mode_flags & FIREBOT_STATIONARY_MODE ? 1 : 5
if(living_bot.firebot_mode_flags & FIREBOT_EXTINGUISH_PEOPLE)
controller.queue_behavior(/datum/ai_behavior/bot_search/people_on_fire, BB_FIREBOT_EXTINGUISH_TARGET, controller.blackboard[BB_FIREBOT_CAN_EXTINGUISH], range)
///behavior for finding people on fire
/datum/ai_behavior/bot_search/people_on_fire
/datum/ai_behavior/bot_search/people_on_fire/valid_target(datum/ai_controller/basic_controller/bot/controller, mob/living/my_target)
var/mob/living/basic/bot/living_bot = controller.pawn
return (my_target.on_fire || (living_bot.bot_access_flags & BOT_COVER_EMAGGED))
///subtree for finding turfs to extinguish
/datum/ai_planning_subtree/extinguishing_turfs
/datum/ai_planning_subtree/extinguishing_turfs/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
if(controller.blackboard_key_exists(BB_FIREBOT_EXTINGUISH_TARGET))
/// Firebot skips blacklisting unreachable targets while stationary, matching the old set_if_can_reach bypass.
/datum/ai_controller/basic_controller/bot/firebot/note_unreachable_target(atom/target)
var/mob/living/basic/bot/firebot/bot_pawn = pawn
if(bot_pawn.firebot_mode_flags & FIREBOT_STATIONARY_MODE)
return
return ..()
var/mob/living/basic/bot/firebot/living_bot = controller.pawn
var/should_bypass_blacklist = living_bot.firebot_mode_flags & FIREBOT_STATIONARY_MODE
/// Gathers nearby living mobs to extinguish; empty unless people-extinguishing is on, range clamped to adjacent tiles when stationary.
/datum/target_source/firebot_targets
if(living_bot.firebot_mode_flags & FIREBOT_EXTINGUISH_FLAMES)
controller.queue_behavior(/datum/ai_behavior/search_burning_turfs, BB_FIREBOT_EXTINGUISH_TARGET, should_bypass_blacklist)
/datum/target_source/firebot_targets/collect_candidates(mob/living/pawn, datum/ai_controller/controller, range)
var/mob/living/basic/bot/firebot/bot_pawn = pawn
if(!(bot_pawn.firebot_mode_flags & FIREBOT_EXTINGUISH_PEOPLE))
return list()
if(bot_pawn.firebot_mode_flags & FIREBOT_STATIONARY_MODE)
range = 1
var/list/candidates = list()
for(var/mob/living/candidate in oview(range, pawn))
candidates += candidate
return candidates
///behavior to find burning turfs
/datum/ai_behavior/search_burning_turfs
action_cooldown = 2 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/// Valid if the mob is on fire (or anyone, while emagged) and is a type this firebot is allowed to extinguish.
/datum/targeting_strategy/extinguishable_person/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
var/mob/living/target_mob = target
if(!isliving(target_mob))
return FALSE
var/mob/living/basic/bot/firebot/bot_pawn = living_mob
if(!target_mob.on_fire && !(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED))
return FALSE
return is_type_in_list(target_mob, controller.blackboard[BB_FIREBOT_CAN_EXTINGUISH])
/datum/ai_behavior/search_burning_turfs/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key, bypass_add_blacklist = FALSE)
var/mob/living/living_pawn = controller.pawn
var/list/ignore_list = controller.blackboard[BB_TEMPORARY_IGNORE_LIST]
for(var/turf/possible_turf as anything in RANGE_TURFS(5, living_pawn))
if(QDELETED(living_pawn))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(!isopenturf(possible_turf))
continue
var/turf/open/open_turf = possible_turf
if(!open_turf.active_hotspot)
continue
if(LAZYACCESS(ignore_list, possible_turf))
continue
if(controller.set_if_can_reach(key = target_key, target = possible_turf, bypass_add_to_blacklist = bypass_add_blacklist))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/// Gathers turfs in range; empty unless flame-extinguishing is enabled.
/datum/target_source/range_turfs/firebot_hotspots
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/target_source/range_turfs/firebot_hotspots/collect_candidates(mob/living/pawn, datum/ai_controller/controller, range)
var/mob/living/basic/bot/firebot/bot_pawn = pawn
if(!(bot_pawn.firebot_mode_flags & FIREBOT_EXTINGUISH_FLAMES))
return list()
return ..(pawn, controller, range)
///behavior to extinguish mobs or turfs
/datum/ai_behavior/basic_melee_attack/interact_once/extinguish
/// Valid if the turf is an open turf with an active fire.
/datum/targeting_strategy/burning_hotspot/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
if(!isopenturf(target))
return FALSE
var/turf/open/open_turf = target
return !!open_turf.active_hotspot
/datum/ai_behavior/basic_melee_attack/interact_once/extinguish/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
var/atom/target = controller.blackboard[BB_FIREBOT_EXTINGUISH_TARGET]
/datum/bt_node/ai_behavior/bot_interact/extinguish
/datum/bt_node/ai_behavior/bot_interact/extinguish/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded)
. = ..()
// if we couldn't reach OR we emagged a living target, blacklist them
var/atom/target = controller.blackboard[target_key]
var/mob/living/basic/bot/living_bot = controller.pawn
//if we couldnt path, or we successfully burnt someone, ignore them for a bit!
if(!succeeded || (isliving(target) && (living_bot.bot_access_flags & BOT_COVER_EMAGGED)))
controller.add_to_blacklist(target)
return ..()
///subtree to make us say funny idle lines
/datum/ai_planning_subtree/firebot_speech
///chance we spout lines
/datum/bt_node/ai_behavior/handle_firebot_speech
time_between_perform = 20 SECONDS
var/speech_prob = 3
/datum/ai_planning_subtree/firebot_speech/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
if(controller.blackboard[BB_FIREBOT_EXTINGUISH_TARGET] || !SPT_PROB(speech_prob, seconds_per_tick))
return
/datum/bt_node/ai_behavior/handle_firebot_speech/perform(seconds_per_tick, datum/ai_controller/controller)
if(!SPT_PROB(speech_prob, seconds_per_tick))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
var/mob/living/basic/bot/living_bot = controller.pawn
var/list/idle_lines = (living_bot.bot_access_flags & BOT_COVER_EMAGGED) ? controller.blackboard[BB_FIREBOT_EMAGGED_LINES] : controller.blackboard[BB_FIREBOT_IDLE_LINES]
controller.queue_behavior(/datum/ai_behavior/bot_speech, idle_lines, BB_ANNOUNCE_ABILITY)
if(!length(idle_lines))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
announcement?.announce(pick(idle_lines))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
#undef ANNOUNCEMENT_TIMER
@@ -0,0 +1,191 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/honkbot",
"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": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"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/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": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/honkbot_slip"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CLOWN_FRIEND"
},
"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/play_with_clown",
"vars": {
"target_key": "BB_CLOWN_FRIEND"
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CLOWN_FRIEND",
"required_dist": 1,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
},
{
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_SLIPPERY_TARGET",
"target_source": "/datum/target_source/honkbot_slippery",
"targeting_strategy": "/datum/targeting_strategy/can_see",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"vision_range": 5,
"time_between_perform": "5 SECONDS"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_SLIP_TARGET",
"target_source": "/datum/target_source/oview_single_type/human_mob",
"targeting_strategy": "/datum/targeting_strategy/can_see/slip_victim",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"must_be_reachable": true,
"vision_range": 5
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_CLOWN_FRIEND",
"target_source": "/datum/target_source/oview_single_type/living_mob",
"targeting_strategy": "/datum/targeting_strategy/clown_friend",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"must_be_reachable": true,
"vision_range": 5,
"time_between_perform": "5 SECONDS"
}
}
]
}
]
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
}
]
}
@@ -55,7 +55,7 @@
return honkbot_sounds
/mob/living/basic/bot/secbot/honkbot/proc/pre_slip()
return (prob(70) && ai_controller?.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET))
return (prob(70) && ai_controller?.blackboard_key_exists(BB_CURRENT_TARGET))
/mob/living/basic/bot/secbot/honkbot/proc/post_slip()
INVOKE_ASYNC(src, TYPE_PROC_REF(/mob/living/basic/bot, speak), HONKBOT_VOICED_HONK_SAD)
@@ -4,16 +4,7 @@
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_ALWAYS_IGNORE_FACTION = TRUE,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/use_mob_ability/random_honk,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/arrest_target,
/datum/ai_planning_subtree/slip_victims,
/datum/ai_planning_subtree/play_with_clowns,
/datum/ai_planning_subtree/find_patrol_beacon,
)
behavior_tree_json = "code/modules/mob/living/basic/bots/honkbots/honkbot.bt.json"
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
@@ -24,15 +15,10 @@
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_SLIP_TARGET), PROC_REF(on_clear_target))
// on_clear_target intentionally removed it caused grab-then-immediately-release bug
// Cleanup is handled by on_stop_pulling instead
RegisterSignal(new_pawn, COMSIG_ATOM_NO_LONGER_PULLING, PROC_REF(on_stop_pulling))
/datum/ai_controller/basic_controller/bot/honkbot/proc/on_clear_target(datum/source)
SIGNAL_HANDLER
var/mob/living/living_pawn = pawn
living_pawn.stop_pulling()
/datum/ai_controller/basic_controller/bot/honkbot/proc/on_stop_pulling(datum/source)
SIGNAL_HANDLER
@@ -43,128 +29,107 @@
add_to_blacklist(slip_target)
clear_blackboard_key(BB_SLIP_TARGET)
/datum/ai_planning_subtree/play_with_clowns/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/clown_target = controller.blackboard[BB_CLOWN_FRIEND]
if(QDELETED(clown_target))
var/list/my_list = controller.blackboard[BB_CLOWNS_LIST]
controller.queue_behavior(/datum/ai_behavior/bot_search/clown_friends, BB_CLOWN_FRIEND, my_list)
return
controller.queue_behavior(/datum/ai_behavior/play_with_clown, BB_CLOWN_FRIEND)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/bot_search/clown_friends
/datum/ai_behavior/bot_search/clown_friends/valid_target(datum/ai_controller/basic_controller/bot/controller, mob/living/my_target)
if(HAS_TRAIT(my_target, TRAIT_PERCEIVED_AS_CLOWN))
return TRUE
if(!istype(my_target, /mob/living/silicon/robot))
return FALSE
var/mob/living/silicon/robot/robot_target = my_target
return istype(robot_target.model, /obj/item/robot_model/clown)
/datum/bt_node/subtree/honkbot_slip
behavior_tree_json = "code/modules/mob/living/basic/bots/honkbots/honkbot_slip.bt.json"
/datum/ai_behavior/play_with_clown
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/play_with_clown/setup(datum/ai_controller/controller, target_key)
/datum/bt_node/ai_behavior/use_mob_ability/random_honk
/datum/bt_node/ai_behavior/use_mob_ability/random_honk/perform(seconds_per_tick, datum/ai_controller/controller)
if(!SPT_PROB(5, seconds_per_tick))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return ..()
/// Valid if the target is a visible human who isn't buckled and has gravity someone a slip can actually knock over.
/datum/targeting_strategy/can_see/slip_victim/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
if(!.)
return FALSE
set_movement_target(controller, target)
var/mob/living/carbon/human/candidate = target
if(!istype(candidate))
return FALSE
return !candidate.buckled && candidate.has_gravity()
/datum/ai_behavior/play_with_clown/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
// Positions the pulled victim onto the slippery item by stepping away, then releases.
/datum/bt_node/ai_behavior/release_and_slip
var/victim_key
/datum/bt_node/ai_behavior/release_and_slip/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/victim = controller.blackboard[victim_key]
var/mob/living/our_mob = controller.pawn
if(QDELETED(victim) || our_mob.pulling != victim)
EVLOG_TEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[our_mob] release_and_slip: not pulling victim")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[our_mob] releasing [victim]!", get_turf(our_mob), "HONK!")
var/list/possible_dirs = GLOB.alldirs.Copy()
possible_dirs -= get_dir(our_mob, victim)
for(var/direction in possible_dirs)
var/turf/possible_turf = get_step(our_mob, direction)
if(possible_turf.is_blocked_turf(source_atom = our_mob))
possible_dirs -= direction
if(length(possible_dirs))
step(our_mob, pick(possible_dirs))
our_mob.stop_pulling()
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/// Gathers nearby atoms matching the slippery-item typepaths stored in BB_SLIPPERY_ITEMS.
/datum/target_source/honkbot_slippery
/datum/target_source/honkbot_slippery/collect_candidates(mob/living/pawn, datum/ai_controller/controller, range)
var/list/slippery_items = controller.blackboard[BB_SLIPPERY_ITEMS]
if(!length(slippery_items))
return list()
return typecache_filter_list(oview(range, pawn), typecacheof(slippery_items))
/// Valid only if the target is within line of sight (not just within range).
/datum/targeting_strategy/can_see/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
return can_see(living_mob, target, vision_range)
/// Valid if the target is a conscious clown (by trait, or a borg running the clown model).
/datum/targeting_strategy/clown_friend/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
var/mob/living/target_mob = target
if(!isliving(target_mob) || target_mob.stat != CONSCIOUS)
return FALSE
if(HAS_TRAIT(target_mob, TRAIT_PERCEIVED_AS_CLOWN))
return TRUE
if(istype(target_mob, /mob/living/silicon/robot))
var/mob/living/silicon/robot/robot_target = target_mob
return istype(robot_target.model, /obj/item/robot_model/clown)
return FALSE
/datum/bt_node/ai_behavior/play_with_clown
var/target_key
/datum/bt_node/ai_behavior/play_with_clown/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/living_target = controller.blackboard[target_key]
if(QDELETED(living_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(get_dist(controller.pawn, living_target) > 1)
return AI_BEHAVIOR_INSTANT
var/mob/living/living_pawn = controller.pawn
var/datum/action/honk_ability = controller.blackboard[BB_HONK_ABILITY]
honk_ability?.Trigger()
living_pawn.manual_emote("celebrates with [living_target]!")
living_pawn.emote("flip")
living_pawn.emote("beep")
INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), "flip")
INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), "beep")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/play_with_clown/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
/datum/bt_node/ai_behavior/play_with_clown/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded)
. = ..()
var/mob/living/living_target = controller.blackboard[target_key]
if(QDELETED(living_target))
return
controller.add_to_blacklist(living_target)
if(!isnull(living_target))
controller.add_to_blacklist(living_target)
controller.clear_blackboard_key(target_key)
/datum/ai_planning_subtree/slip_victims/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/living_pawn = controller.pawn
if(!living_pawn.has_gravity())
return
var/atom/slippery_item = controller.blackboard[BB_SLIPPERY_TARGET]
if(QDELETED(slippery_item) || !can_see(controller.pawn, slippery_item, 5))
controller.clear_blackboard_key(BB_SLIP_TARGET)
controller.clear_blackboard_key(BB_SLIPPERY_TARGET)
controller.queue_behavior(/datum/ai_behavior/bot_search, BB_SLIPPERY_TARGET, controller.blackboard[BB_SLIPPERY_ITEMS])
return
var/mob/living/living_target = controller.blackboard[BB_SLIP_TARGET]
if(QDELETED(living_target))
var/static/list/to_slip = typecacheof(list(/mob/living/carbon/human))
controller.queue_behavior(/datum/ai_behavior/bot_search/slip_target, BB_SLIP_TARGET, to_slip)
return
if(living_pawn.pulling == living_target)
controller.queue_behavior(/datum/ai_behavior/drag_to_slip, BB_SLIP_TARGET, BB_SLIPPERY_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/drag_target, BB_SLIP_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/bot_search/slip_target
/datum/ai_behavior/bot_search/slip_target/valid_target(datum/ai_controller/basic_controller/bot/controller, mob/living/my_target)
return (!my_target.buckled && my_target.has_gravity())
/datum/ai_behavior/drag_to_slip
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
required_distance = 0
/datum/ai_behavior/drag_to_slip/setup(datum/ai_controller/controller, slip_target, slippery_target)
. = ..()
var/atom/target = controller.blackboard[slippery_target]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/drag_to_slip/perform(seconds_per_tick, datum/ai_controller/controller, slip_target, slippery_target)
var/mob/living/our_pawn = controller.pawn
var/atom/living_target = controller.blackboard[slip_target]
if(QDELETED(living_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/list/possible_dirs = GLOB.alldirs.Copy()
possible_dirs -= get_dir(our_pawn, living_target)
for(var/direction in possible_dirs)
var/turf/possible_turf = get_step(our_pawn, direction)
if(possible_turf.is_blocked_turf(source_atom = our_pawn))
possible_dirs -= direction
step(our_pawn, pick(possible_dirs))
our_pawn.stop_pulling()
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/drag_to_slip/finish_action(datum/ai_controller/basic_controller/bot/controller, success, slip_target, slippery_target)
. = ..()
if(success)
var/mob/living/living_pawn = controller.pawn
living_pawn.emote("flip")
var/atom/slipped_victim = controller.blackboard[slip_target]
if(!isnull(slipped_victim))
controller.add_to_blacklist(slipped_victim)
controller.clear_blackboard_key(slip_target)
controller.clear_blackboard_key(slippery_target)
/datum/ai_planning_subtree/use_mob_ability/random_honk
ability_key = BB_HONK_ABILITY
/datum/ai_planning_subtree/use_mob_ability/random_honk/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(!SPT_PROB(5, seconds_per_tick))
return
return ..()
@@ -0,0 +1,77 @@
{
"dm_type": "/datum/bt_node/subtree/honkbot_slip",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/can_see_target",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_SLIPPERY_TARGET",
"range": 5
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/can_see_target",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_SLIP_TARGET",
"range": 5
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/pawn_has_gravity",
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_SLIP_TARGET",
"required_dist": 1,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/grab_target",
"vars": {
"target_key": "BB_SLIP_TARGET"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/is_grabbing_target",
"vars": {
"observer_abort": "BT_ABORT_SELF",
"key": "BB_SLIP_TARGET"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_SLIPPERY_TARGET",
"required_dist": 0,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/release_and_slip",
"vars": {
"victim_key": "BB_SLIP_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/perform_emote",
"vars": {
"emote": "flip"
}
}
]
}
}
}
}
@@ -0,0 +1,109 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/hygienebot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_WASH_TARGET"
},
"child": {
"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": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": true,
"finish_on_primary": true,
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/wash_target",
"vars": {
"target_key": "BB_WASH_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_WASH_TARGET",
"required_dist": 0,
"finish_on_arrival": false,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/cooldown",
"vars": {
"observer_abort": "BT_ABORT_NONE",
"cooldown_key": "BB_TRASH_TALK_COOLDOWN",
"cooldown_duration": "4 SECONDS"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/commence_trashtalk",
"vars": {
"target_key": "BB_WASH_TARGET"
}
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target/hygiene_wash",
"vars": {
"target_key": "BB_WASH_TARGET",
"target_source": "/datum/target_source/oview_single_type/human_mob",
"targeting_strategy": "/datum/targeting_strategy/conscious_human/washable_human",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"vision_range": 5,
"time_between_perform": "5 SECONDS"
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
}
]
}
]
}
@@ -2,6 +2,7 @@
#define BOT_ANGER_THRESHOLD 5
/datum/ai_controller/basic_controller/bot/hygienebot
behavior_tree_json = "code/modules/mob/living/basic/bots/hygienebot/hygienebot.bt.json"
blackboard = list(
BB_SALUTE_MESSAGES = list(
"salutes",
@@ -9,14 +10,6 @@
),
BB_WASH_FRUSTRATION = 0,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/handle_trash_talk,
/datum/ai_planning_subtree/wash_people,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/find_patrol_beacon,
)
reset_keys = list(
BB_WASH_TARGET,
BB_BEACON_TARGET,
@@ -24,122 +17,68 @@
BB_BOT_SUMMON_TARGET,
)
/datum/ai_planning_subtree/handle_trash_talk
/datum/ai_planning_subtree/handle_trash_talk/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
if(!controller.blackboard_key_exists(BB_WASH_TARGET))
return
controller.queue_behavior(/datum/ai_behavior/commence_trashtalk, BB_WASH_TARGET)
/datum/ai_behavior/commence_trashtalk
action_cooldown = 4 SECONDS
/datum/bt_node/ai_behavior/commence_trashtalk
var/target_key
/datum/ai_behavior/commence_trashtalk/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
/datum/bt_node/ai_behavior/commence_trashtalk/perform(seconds_per_tick, datum/ai_controller/controller)
if(!controller.blackboard_key_exists(target_key))
return AI_BEHAVIOR_FAILED | AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/frustration_count = controller.blackboard[BB_WASH_FRUSTRATION]
controller.set_blackboard_key(BB_WASH_FRUSTRATION, min(frustration_count + 1, BOT_FRUSTRATION_LIMIT))
if(controller.blackboard[BB_WASH_FRUSTRATION] < BOT_ANGER_THRESHOLD)
return AI_BEHAVIOR_FAILED | AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
announcement?.announce(pick(controller.blackboard[BB_WASH_THREATS]))
return AI_BEHAVIOR_SUCCEEDED | AI_BEHAVIOR_DELAY
/datum/ai_planning_subtree/wash_people
/datum/ai_planning_subtree/wash_people/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/bot_pawn = controller.pawn
var/atom/wash_target = controller.blackboard[BB_WASH_TARGET]
if(QDELETED(wash_target))
controller.queue_behavior(/datum/ai_behavior/find_valid_wash_targets, BB_WASH_TARGET, bot_pawn.bot_access_flags)
return
if(get_dist(bot_pawn, wash_target) < 9)
controller.queue_behavior(/datum/ai_behavior/wash_target, BB_WASH_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
controller.clear_blackboard_key(BB_WASH_TARGET) //delete if too far
/datum/ai_behavior/find_valid_wash_targets
action_cooldown = 5 SECONDS
/datum/ai_behavior/find_valid_wash_targets/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key, our_access_flags)
. = ..()
var/list/ignore_list = controller.blackboard[BB_TEMPORARY_IGNORE_LIST]
var/atom/found_target
for(var/mob/living/carbon/human/wash_potential in oview(5, controller.pawn))
if(found_target)
break
if(isnull(wash_potential.mind) || wash_potential.stat != CONSCIOUS)
continue
if(LAZYACCESS(ignore_list, wash_potential))
continue
// BUBBER EDIT ADDITION BEGIN - Dirty quirk
if (HAS_TRAIT(wash_potential, TRAIT_DIRTY))
found_target = wash_potential
break
// BUGGER EDIT ADDITION END
if(our_access_flags & BOT_COVER_EMAGGED)
controller.add_to_blacklist(wash_potential)
found_target = wash_potential
break
for(var/atom/clothing in wash_potential.get_equipped_items(INCLUDE_HELD|INCLUDE_PROSTHETICS))
if(GET_ATOM_BLOOD_DNA_LENGTH(clothing))
found_target = wash_potential
break
if(isnull(found_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(target_key, found_target)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/find_valid_wash_targets/finish_action(datum/ai_controller/controller, succeeded, target_key)
/// Valid if the target is a conscious human with bloodied clothing (or anyone, while emagged).
/datum/targeting_strategy/conscious_human/washable_human/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!succeeded)
return
if(!.)
return FALSE
var/mob/living/basic/bot/bot_pawn = living_mob
if(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED)
return TRUE
var/mob/living/carbon/human/human_target = target
for(var/atom/clothing in human_target.get_equipped_items(INCLUDE_HELD|INCLUDE_PROSTHETICS))
if(GET_ATOM_BLOOD_DNA_LENGTH(clothing))
return TRUE
return FALSE
/// Finds someone to wash and announces it; while emagged the target is blacklisted so the bot washes each person only once.
/datum/bt_node/ai_behavior/acquire_target/update_interaction_target/hygiene_wash
/datum/bt_node/ai_behavior/acquire_target/update_interaction_target/hygiene_wash/on_target_found(datum/ai_controller/basic_controller/bot/controller, atom/target, datum/targeting_strategy/strategy)
var/mob/living/basic/bot/bot_pawn = controller.pawn
if(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED)
controller.add_to_blacklist(target)
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
announcement.announce(pick(controller.blackboard[BB_WASH_FOUND]))
/datum/ai_behavior/wash_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
required_distance = 0
/datum/ai_behavior/wash_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/bt_node/ai_behavior/wash_target
var/target_key
/datum/ai_behavior/wash_target/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key)
. = ..()
/datum/bt_node/ai_behavior/wash_target/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/mob/living/carbon/human/unclean_target = controller.blackboard[target_key]
var/mob/living/basic/living_pawn = controller.pawn
if(QDELETED(unclean_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(get_dist(living_pawn, unclean_target) > 0)
return AI_BEHAVIOR_INSTANT
living_pawn.melee_attack(unclean_target)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
if(living_pawn.loc == get_turf(unclean_target))
living_pawn.melee_attack(unclean_target)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/wash_target/finish_action(datum/ai_controller/controller, succeeded, target_key)
/datum/bt_node/ai_behavior/wash_target/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.clear_blackboard_key(target_key)
var/wash_frustration = controller.blackboard[BB_WASH_FRUSTRATION]
controller.clear_blackboard_key(BB_WASH_FRUSTRATION)
controller.set_blackboard_key(BB_WASH_FRUSTRATION, 0)
if(!succeeded || wash_frustration <= BOT_ANGER_THRESHOLD)
return
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
@@ -0,0 +1,109 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/medbot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"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": false,
"children": [
{
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": false,
"repeat_secondary_delay": "BASIC_MOB_FIND_TARGET_RATE",
"finish_on_primary": false,
"children": [
{
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_medical_flag",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"flag": "MEDBOT_DECLARE_CRIT"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree/medbot_find_and_announce_crit"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_medical_flag",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"invert": true,
"flag": "MEDBOT_TIPPED_MODE"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree/medbot_treat_patient"
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_PATIENT_IN_CRIT",
"target_source": "/datum/target_source/oview_single_type/human_mob",
"targeting_strategy": "/datum/targeting_strategy/crit_patient",
"vision_range": 7
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target/medbot_patient",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"target_source": "/datum/target_source/oview_single_type/human_mob/medbot_patient",
"targeting_strategy": "/datum/targeting_strategy/treatable_patient",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"must_be_reachable": true,
"reach_distance": 20,
"vision_range": 7
}
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_medical_flag",
"vars": {
"flag": "MEDBOT_SPEAK_MODE"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/handle_medbot_speech",
"vars": {
"announce_key": "BB_ANNOUNCE_ABILITY"
}
}
}
]
}
]
}
@@ -1,17 +1,18 @@
#define BOT_PATIENT_PATH_LIMIT 20
/// Find and treat a patient used by both the speak-mode parallel and the silent fallback branch.
/datum/bt_node/subtree/medbot_treat_patient
behavior_tree_json = "code/modules/mob/living/basic/bots/medbot/medbot_treat_patient.bt.json"
/// Find a patient in hard-crit and announce them on radio.
/datum/bt_node/subtree/medbot_find_and_announce_crit
behavior_tree_json = "code/modules/mob/living/basic/bots/medbot/medbot_find_and_announce_crit.bt.json"
/datum/ai_controller/basic_controller/bot/medbot
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/handle_medbot_speech,
/datum/ai_planning_subtree/find_and_hunt_target/patients_in_crit,
/datum/ai_planning_subtree/treat_wounded_target,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/find_patrol_beacon/medbot,
)
behavior_tree_json = "code/modules/mob/living/basic/bots/medbot/medbot.bt.json"
ai_movement = /datum/ai_movement/jps/bot/medbot
reset_keys = list(
BB_PATIENT_TARGET,
BB_CURRENT_TARGET,
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
@@ -21,7 +22,7 @@
maximum_length = BOT_PATIENT_PATH_LIMIT
max_pathing_attempts = 20
// only AI isnt allowed to move when this flag is set, sentient players can
// only AI isn't allowed to move when this flag is set, sentient players can
/datum/ai_movement/jps/bot/medbot/allowed_to_move(datum/move_loop/source)
var/datum/ai_controller/controller = source.extra_info
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
@@ -33,176 +34,137 @@
maximum_length = AI_BOT_PATH_LENGTH
/datum/ai_planning_subtree/treat_wounded_target
/datum/ai_planning_subtree/treat_wounded_target/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
if(bot_pawn.medical_mode_flags & MEDBOT_TIPPED_MODE)
controller.clear_blackboard_key(BB_PATIENT_TARGET)
/// Medbot's note_unreachable_target skips blacklisting while stationary, matching the old set_if_can_reach bypass.
/datum/ai_controller/basic_controller/bot/medbot/note_unreachable_target(atom/target)
var/mob/living/basic/bot/medbot/bot_pawn = pawn
if(bot_pawn.medical_mode_flags & MEDBOT_STATIONARY_MODE)
return
var/is_stationary = bot_pawn.medical_mode_flags & MEDBOT_STATIONARY_MODE
if(controller.blackboard_key_exists(BB_PATIENT_TARGET))
controller.queue_behavior(/datum/ai_behavior/tend_to_patient, BB_PATIENT_TARGET, bot_pawn.heal_threshold, bot_pawn.damage_type_healer, bot_pawn.bot_access_flags, is_stationary)
return SUBTREE_RETURN_FINISH_PLANNING
return ..()
controller.queue_behavior(/datum/ai_behavior/find_suitable_patient, BB_PATIENT_TARGET, bot_pawn.heal_threshold, bot_pawn.damage_type_healer, bot_pawn.medical_mode_flags, bot_pawn.bot_access_flags)
/// Gathers nearby humans as patients; range is clamped to adjacent tiles when the medbot is in stationary mode. I should probably just make this a blackboard thing but I cannot be arsed right now.
/datum/target_source/oview_single_type/human_mob/medbot_patient
/datum/ai_behavior/find_suitable_patient
var/search_range = 7
action_cooldown = 2 SECONDS
/datum/target_source/oview_single_type/human_mob/medbot_patient/collect_candidates(mob/living/pawn, datum/ai_controller/controller, range)
var/mob/living/basic/bot/medbot/bot_pawn = pawn
if(bot_pawn.medical_mode_flags & MEDBOT_STATIONARY_MODE)
range = 1
return ..(pawn, controller, range)
/datum/ai_behavior/find_suitable_patient/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key, threshold, heal_type, mode_flags, access_flags)
search_range = (mode_flags & MEDBOT_STATIONARY_MODE) ? 1 : initial(search_range)
var/list/ignore_keys = controller.blackboard[BB_TEMPORARY_IGNORE_LIST]
for(var/mob/living/carbon/human/treatable_target in oview(search_range, controller.pawn))
// BUBBER EDIT ADDITION START - Don't hunt down synthetics
if(treatable_target.mob_biotypes & MOB_ROBOTIC)
continue
// BUBBER EDIT ADDITION END
if(LAZYACCESS(ignore_keys, treatable_target) || treatable_target.stat == DEAD)
continue
if((access_flags & BOT_COVER_EMAGGED) && treatable_target.stat == CONSCIOUS)
controller.set_if_can_reach(key = BB_PATIENT_TARGET, target = treatable_target, distance = BOT_PATIENT_PATH_LIMIT, bypass_add_to_blacklist = (search_range == 1))
break
if((heal_type == HEAL_ALL_DAMAGE))
if(treatable_target.get_total_damage() > threshold)
controller.set_if_can_reach(key = BB_PATIENT_TARGET, target = treatable_target, distance = BOT_PATIENT_PATH_LIMIT, bypass_add_to_blacklist = (search_range == 1))
break
continue
if(treatable_target.get_current_damage_of_type(damagetype = heal_type) > threshold)
controller.set_if_can_reach(key = BB_PATIENT_TARGET, target = treatable_target, distance = BOT_PATIENT_PATH_LIMIT, bypass_add_to_blacklist = (search_range == 1))
break
if(controller.blackboard_key_exists(BB_PATIENT_TARGET))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
else
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/datum/ai_behavior/find_suitable_patient/finish_action(datum/ai_controller/controller, succeeded, target_key)
/// Valid if the patient needs the damage type this medbot heals (or is a conscious target while emagged).
/datum/targeting_strategy/treatable_patient/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!succeeded || QDELETED(controller.pawn) ||get_dist(controller.pawn, controller.blackboard[target_key]) <= 1)
if(!.)
return FALSE
var/mob/living/carbon/human/patient = target
if(!istype(patient) || patient.stat == DEAD)
return FALSE
var/mob/living/basic/bot/medbot/bot_pawn = living_mob
if((bot_pawn.bot_access_flags & BOT_COVER_EMAGGED) && patient.stat == CONSCIOUS)
return TRUE
if(bot_pawn.damage_type_healer == HEAL_ALL_DAMAGE)
return patient.get_total_damage() > bot_pawn.heal_threshold
return patient.get_current_damage_of_type(damagetype = bot_pawn.damage_type_healer) > bot_pawn.heal_threshold
/// Finds a patient to treat, announcing that the bot is on its way when the patient isn't already adjacent.
/datum/bt_node/ai_behavior/acquire_target/update_interaction_target/medbot_patient
/datum/bt_node/ai_behavior/acquire_target/update_interaction_target/medbot_patient/on_target_found(datum/ai_controller/controller, atom/target, datum/targeting_strategy/strategy)
if(QDELETED(controller.pawn) || get_dist(controller.pawn, target) <= 1)
return
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
announcement?.announce(pick(controller.blackboard[BB_WAIT_SPEECH]))
/datum/ai_behavior/tend_to_patient
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/tend_to_patient/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/tend_to_patient/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key, threshold, damage_type_healer, access_flags, is_stationary)
/datum/bt_node/ai_behavior/tend_to_patient
var/target_key
/datum/bt_node/ai_behavior/tend_to_patient/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/mob/living/carbon/human/patient = controller.blackboard[target_key]
if(QDELETED(patient) || patient.stat == DEAD)
EVLOG_TEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[controller.pawn] tend_to_patient: patient gone (deleted=[QDELETED(patient)], stat=[patient?.stat])")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(check_if_healed(patient, threshold, damage_type_healer, access_flags))
if(get_dist(controller.pawn, patient) > 1)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED //We technically failed, but we want to try again so succeed.
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
if(check_if_healed(patient, bot_pawn.heal_threshold, bot_pawn.damage_type_healer, bot_pawn.bot_access_flags))
EVLOG_TEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[bot_pawn] tend_to_patient: [patient] is fully healed")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
var/mob/living/basic/bot/bot_pawn = controller.pawn
if(patient.stat >= HARD_CRIT && prob(5))
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
announcement?.announce(pick(controller.blackboard[BB_NEAR_DEATH_SPEECH]))
EVLOG_MAPTEXT(controller, EVLOG_CATEGORY_AI_BEHAVIORS, "[bot_pawn] healing [patient] (dmg=[patient.get_total_damage()])", get_turf(patient), "Heal")
bot_pawn.melee_attack(patient)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
// only clear the target if they get healed
/datum/ai_behavior/tend_to_patient/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, target_key, threshold, damage_type_healer, access_flags, is_stationary)
/datum/bt_node/ai_behavior/tend_to_patient/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded)
. = ..()
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
var/atom/target = controller.blackboard[target_key]
var/is_stationary = bot_pawn.medical_mode_flags & MEDBOT_STATIONARY_MODE
if(!succeeded)
if(!isnull(target) && !is_stationary)
controller.add_to_blacklist(target)
controller.clear_blackboard_key(target_key)
return
if(QDELETED(target) || !check_if_healed(target, threshold, damage_type_healer, access_flags))
if(QDELETED(target) || !check_if_healed(target, bot_pawn.heal_threshold, bot_pawn.damage_type_healer, bot_pawn.bot_access_flags))
return
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
announcement?.announce(pick(controller.blackboard[BB_AFTERHEAL_SPEECH]))
controller.clear_blackboard_key(target_key)
/datum/ai_behavior/tend_to_patient/proc/check_if_healed(mob/living/carbon/human/patient, threshold, damage_type_healer, access_flags)
/datum/bt_node/ai_behavior/tend_to_patient/proc/check_if_healed(mob/living/carbon/human/patient, threshold, damage_type_healer, access_flags)
if(access_flags & BOT_COVER_EMAGGED)
return (patient.stat > CONSCIOUS)
var/patient_damage = (damage_type_healer == HEAL_ALL_DAMAGE) ? patient.get_total_damage() : patient.get_current_damage_of_type(damagetype = damage_type_healer)
return (patient_damage <= threshold)
/datum/ai_planning_subtree/handle_medbot_speech
var/speech_chance = 5
/datum/ai_planning_subtree/handle_medbot_speech/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
/datum/bt_node/ai_behavior/handle_medbot_speech
var/announce_key
time_between_perform = 20 SECONDS
/datum/bt_node/ai_behavior/handle_medbot_speech/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
//we cant speak!
if(!(bot_pawn.medical_mode_flags & MEDBOT_SPEAK_MODE))
return
var/currently_tipped = bot_pawn.medical_mode_flags & MEDBOT_TIPPED_MODE
speech_chance = ((bot_pawn.bot_access_flags & BOT_COVER_EMAGGED) || currently_tipped) ? 15 : initial(speech_chance)
var/speech_chance = ((bot_pawn.bot_access_flags & BOT_COVER_EMAGGED) || currently_tipped) ? 15 : 5
if(!SPT_PROB(speech_chance, seconds_per_tick))
return
controller.queue_behavior(/datum/ai_behavior/handle_medbot_speech, BB_ANNOUNCE_ABILITY, bot_pawn.mode, bot_pawn.bot_access_flags, currently_tipped)
/datum/ai_behavior/handle_medbot_speech
action_cooldown = 20 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/handle_medbot_speech/perform(seconds_per_tick, datum/ai_controller/controller, announce_key, mode, cover_flags, currently_tipped)
. = ..()
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[announce_key]
var/list/speech_to_pick_from
if(currently_tipped)
speech_to_pick_from = controller.blackboard[BB_WORRIED_ANNOUNCEMENTS]
else if(cover_flags & BOT_COVER_EMAGGED)
else if(bot_pawn.bot_access_flags & BOT_COVER_EMAGGED)
speech_to_pick_from = controller.blackboard[BB_EMAGGED_SPEECH]
else if(mode == BOT_IDLE)
else if(bot_pawn.mode == BOT_IDLE)
speech_to_pick_from = controller.blackboard[BB_IDLE_SPEECH]
var/mob/living/living_pawn = controller.pawn
if(locate(/obj/item/clothing/head/costume/chicken) in living_pawn)
speech_to_pick_from += MEDIBOT_VOICED_CHICKEN
if(!length(speech_to_pick_from))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
announcement.announce(pick(speech_to_pick_from))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_planning_subtree/find_and_hunt_target/patients_in_crit
target_key = BB_PATIENT_IN_CRIT
hunting_behavior = /datum/ai_behavior/announce_patient
finding_behavior = /datum/ai_behavior/find_hunt_target/patient_in_crit
hunt_targets = list(/mob/living/carbon/human)
finish_planning = FALSE
/datum/ai_planning_subtree/find_and_hunt_target/patients_in_crit/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
if(!(bot_pawn.medical_mode_flags & MEDBOT_DECLARE_CRIT))
return
return ..()
/datum/ai_behavior/find_hunt_target/patient_in_crit
/datum/ai_behavior/find_hunt_target/patient_in_crit/valid_dinner(mob/living/source, mob/living/carbon/human/patient, radius)
if(patient.stat < UNCONSCIOUS || isnull(patient.mind))
/// Valid if the patient is at least unconscious, has a mind, and is visible used to announce medical emergencies.
/datum/targeting_strategy/crit_patient/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
return can_see(source, patient, radius)
var/mob/living/carbon/human/patient = target
if(!istype(patient) || patient.stat < UNCONSCIOUS || isnull(patient.mind))
return FALSE
return can_see(living_mob, patient, vision_range)
/datum/ai_behavior/announce_patient
action_cooldown = 3 MINUTES
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/bt_node/ai_behavior/announce_patient
var/target_key
time_between_perform = 3 MINUTES
/datum/ai_behavior/announce_patient/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller, target_key)
/datum/bt_node/ai_behavior/announce_patient/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/mob/living/living_target = controller.blackboard[target_key]
if(QDELETED(living_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
@@ -213,22 +175,8 @@
announcement.announce(text_to_announce, controller.blackboard[BB_RADIO_CHANNEL])
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/announce_patient/finish_action(datum/ai_controller/controller, succeeded, target_key)
/datum/bt_node/ai_behavior/announce_patient/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.clear_blackboard_key(target_key)
/datum/ai_planning_subtree/find_patrol_beacon/medbot
///travel towards beacon behavior
travel_behavior = /datum/ai_behavior/travel_towards/beacon/medbot
/datum/ai_planning_subtree/find_patrol_beacon/medbot/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/bot/medbot/bot_pawn = controller.pawn
if(bot_pawn.medical_mode_flags & MEDBOT_STATIONARY_MODE)
return
return ..()
/datum/ai_behavior/travel_towards/beacon/medbot
new_movement_type = /datum/ai_movement/jps/bot/medbot/travel_to_beacon
#undef BOT_PATIENT_PATH_LIMIT
@@ -0,0 +1,16 @@
{
"dm_type": "/datum/bt_node/subtree/medbot_find_and_announce_crit",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_PATIENT_IN_CRIT"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/announce_patient",
"vars": {
"target_key": "BB_PATIENT_IN_CRIT"
}
}
}
@@ -0,0 +1,28 @@
{
"dm_type": "/datum/bt_node/subtree/medbot_treat_patient",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/tend_to_patient",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
}
]
}
}
@@ -0,0 +1,103 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/mulebot",
"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": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_MULEBOT_TRAVEL_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_MULEBOT_TRAVEL_TARGET",
"required_dist": 0,
"finish_on_arrival": true
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/handle_delivery",
"vars": {
"target_key": "BB_MULEBOT_TRAVEL_TARGET"
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_wire_cut",
"vars": {
"wire": "WIRE_BEACON",
"invert": true
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_MULEBOT_TRAVEL_TARGET",
"invert": true
},
"child": {
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_mode",
"vars": {
"mode": "BOT_DELIVER"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/find_delivery_beacon",
"vars": {
"target_key": "BB_MULEBOT_TRAVEL_TARGET",
"tag_key": "BB_MULEBOT_DESTINATION_BEACON"
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_mode",
"vars": {
"mode": "BOT_GO_HOME"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/find_delivery_beacon",
"vars": {
"target_key": "BB_MULEBOT_TRAVEL_TARGET",
"tag_key": "BB_MULEBOT_HOME_BEACON"
}
}
}
]
}
}
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
}
]
}
@@ -165,7 +165,7 @@
. = ..()
if(mode != BOT_BLOCKED)
return
var/obj/machinery/navbeacon/beacon = ai_controller.current_movement_target
var/obj/machinery/navbeacon/beacon = ai_controller.blackboard[BB_CURRENT_MOVEMENT_TARGET]
if(!istype(beacon))
return
var/intended_mode = beacon.location == ai_controller.blackboard[BB_MULEBOT_HOME_BEACON] ? BOT_GO_HOME : BOT_DELIVER
@@ -1,4 +1,5 @@
/datum/ai_controller/basic_controller/bot/mulebot
behavior_tree_json = "code/modules/mob/living/basic/bots/mulebot/mulebot.bt.json"
blackboard = list(
BB_SALUTE_MESSAGES = list(
"blinks its light in appreciation towards",
@@ -6,12 +7,6 @@
)
ai_movement = /datum/ai_movement/jps/bot/mulebot
max_target_distance = AI_MULEBOT_PATH_LENGTH
planning_subtrees = list(
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/attempt_delivery,
/datum/ai_planning_subtree/find_delivery_beacon,
)
reset_keys = list(
BB_BOT_SUMMON_TARGET,
BB_MULEBOT_DESTINATION_BEACON,
@@ -40,72 +35,15 @@
)
RegisterSignals(my_bot, content_signals, PROC_REF(update_able_to_run))
/datum/ai_planning_subtree/find_delivery_beacon
///what behavior do we use to seek beacons
var/find_beacon_behaviour = /datum/ai_behavior/find_delivery_beacon
/// Loads or unloads cargo at the delivery beacon held in target_key, then heads home or idles.
/datum/bt_node/ai_behavior/handle_delivery
var/target_key
time_between_perform = 1 SECONDS
/datum/ai_planning_subtree/find_delivery_beacon/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn
if(bot_pawn.wires.is_cut(WIRE_BEACON))
return
if(!controller.blackboard_key_exists(BB_MULEBOT_TRAVEL_TARGET))
controller.queue_behavior(find_beacon_behaviour, BB_MULEBOT_TRAVEL_TARGET)
/datum/ai_behavior/find_delivery_beacon
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/find_delivery_beacon/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn
var/atom/delivery_beacon
var/beacon_tag = null
switch(bot_pawn.mode)
if(BOT_DELIVER)
beacon_tag = controller.blackboard[BB_MULEBOT_DESTINATION_BEACON]
if(BOT_GO_HOME)
beacon_tag = controller.blackboard[BB_MULEBOT_HOME_BEACON]
else
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
for(var/obj/machinery/navbeacon/beacon as anything in GLOB.deliverybeacons)
if(beacon.location == beacon_tag)
delivery_beacon = beacon
break
if(isnull(delivery_beacon))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_MULEBOT_TRAVEL_TARGET, delivery_beacon)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/travel_towards/delivery_beacon
new_movement_type = /datum/ai_movement/jps/bot/mulebot
/datum/ai_planning_subtree/attempt_delivery
///behavior we use to unload crates
var/delivery_behaviour = /datum/ai_behavior/handle_delivery
/datum/ai_planning_subtree/attempt_delivery/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(!controller.blackboard_key_exists(BB_MULEBOT_TRAVEL_TARGET))
return
controller.queue_behavior(delivery_behaviour, BB_MULEBOT_TRAVEL_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/handle_delivery
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/handle_delivery/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/handle_delivery/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
/datum/bt_node/ai_behavior/handle_delivery/perform(seconds_per_tick, datum/ai_controller/controller)
var/obj/machinery/navbeacon/beacon = controller.blackboard[target_key]
if(QDELETED(beacon))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/basic/bot/mulebot/bot_pawn = controller.pawn
var/load_direction = beacon.codes[NAVBEACON_DELIVERY_DIRECTION] // this will be the load/unload dir
@@ -0,0 +1,32 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/repairbot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"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": "subtree",
"subtype": "/datum/bt_node/subtree/repairbot_repair_target"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/repairbot_find_target"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_salute_authority"
}
]
}
]
}
@@ -1,52 +1,104 @@
#define REPAIRBOT_SPEECH_TIMER 30 SECONDS
/// Emagged repairbot behavior: mug robots then deconstruct structures.
/datum/bt_node/subtree/repairbot_emagged
behavior_tree_json = "code/modules/mob/living/basic/bots/repairbot/repairbot_emagged.bt.json"
/datum/bt_node/subtree/repairbot_repair_target
behavior_tree_json = "code/modules/mob/living/basic/bots/repairbot/repairbot_repair_target.bt.json"
/datum/bt_node/subtree/repairbot_find_target
behavior_tree_json = "code/modules/mob/living/basic/bots/repairbot/repairbot_find_target.bt.json"
/datum/ai_controller/basic_controller/bot/repairbot
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/repairbot_speech,
/datum/ai_planning_subtree/mug_robot,
/datum/ai_planning_subtree/refill_materials,
/datum/ai_planning_subtree/repairbot_deconstruction,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/replace_floors/breaches,
/datum/ai_planning_subtree/wall_girder,
/datum/ai_planning_subtree/build_girder,
/datum/ai_planning_subtree/replace_window,
/datum/ai_planning_subtree/replace_floors,
/datum/ai_planning_subtree/fix_window,
/datum/ai_planning_subtree/salute_authority,
/datum/ai_planning_subtree/find_patrol_beacon,
)
behavior_tree_json = "code/modules/mob/living/basic/bots/repairbot/repairbot.bt.json"
reset_keys = list(
BB_TILELESS_FLOOR,
BB_GIRDER_TARGET,
BB_GIRDER_TO_WALL_TARGET,
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_WELDER_TARGET,
BB_WINDOW_FRAMETARGET,
BB_CURRENT_TARGET,
)
minimum_distance = 1
///subtree to refill our stacks
/datum/ai_planning_subtree/refill_materials
/datum/ai_planning_subtree/refill_materials/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/static/list/refillable_items = typecacheof(list(
/obj/item/stack/sheet/iron,
/obj/item/stack/sheet/glass,
/obj/item/stack/tile,
/datum/bt_node/ai_behavior/repairbot_speech
/datum/bt_node/ai_behavior/repairbot_speech/setup(datum/ai_controller/controller)
if(controller.blackboard[BB_REPAIRBOT_SPEECH_COOLDOWN] > world.time)
return FALSE
var/static/list/keys_to_look = list(
BB_CURRENT_TARGET,
BB_DECONSTRUCT_TARGET,
)
for(var/key in keys_to_look)
if(controller.blackboard_key_exists(key))
return ..()
return FALSE
/datum/bt_node/ai_behavior/repairbot_speech/perform(seconds_per_tick, datum/ai_controller/controller)
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
// determine speech type: emagged -> emagged speech, otherwise normal
var/list/speech_to_pick_from
if(controller.blackboard_key_exists(BB_DECONSTRUCT_TARGET))
speech_to_pick_from = controller.blackboard[BB_REPAIRBOT_EMAGGED_SPEECH]
else
speech_to_pick_from = controller.blackboard[BB_REPAIRBOT_NORMAL_SPEECH]
if(!length(speech_to_pick_from))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
announcement.announce(pick(speech_to_pick_from))
controller.set_blackboard_key(BB_REPAIRBOT_SPEECH_COOLDOWN, world.time + REPAIRBOT_SPEECH_TIMER)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/bt_node/ai_behavior/bot_interact/tip_robot
/datum/bt_node/ai_behavior/bot_interact/tip_robot/setup(datum/ai_controller/controller)
var/mob/living/target = controller.blackboard[target_key]
var/mob/living/pawn = controller.pawn
if(QDELETED(target) || pawn.pulling != target)
return FALSE
return ..()
/datum/bt_node/ai_behavior/bot_interact/tip_robot/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
if(succeeded)
var/mob/living/living_pawn = controller.pawn
living_pawn.stop_pulling()
/datum/bt_node/ai_behavior/bot_search/valid_robot
time_between_perform = 10 SECONDS
/datum/bt_node/ai_behavior/bot_search/valid_robot/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
if(!istype(my_target, /mob/living/silicon/robot))
return FALSE
return (!HAS_TRAIT(my_target, TRAIT_MOB_TIPPED)) && can_see(controller.pawn, my_target)
/datum/bt_node/ai_behavior/bot_search/deconstructable
time_between_perform = 5 SECONDS
/datum/bt_node/ai_behavior/bot_search/deconstructable/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
return (!(my_target.resistance_flags & INDESTRUCTIBLE) && !isgroundlessturf(my_target))
/datum/bt_node/ai_behavior/bot_search/deconstructable/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/static/list/things_to_deconstruct = typecacheof(list(
/obj/structure/window,
/turf/open/floor,
/turf/closed/wall,
))
if(!controller.blackboard_key_exists(BB_REFILLABLE_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_search/refillable_target, BB_REFILLABLE_TARGET, refillable_items)
return
controller.queue_behavior(/datum/ai_behavior/bot_interact, BB_REFILLABLE_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
looking_for = things_to_deconstruct
return ..()
/datum/ai_behavior/bot_search/refillable_target
action_cooldown = 10 SECONDS
/datum/ai_behavior/bot_search/refillable_target/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
/datum/bt_node/ai_behavior/bot_search/refillable_target
time_between_perform = 10 SECONDS
/datum/bt_node/ai_behavior/bot_search/refillable_target/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
var/static/list/desired_types = list(
/obj/item/stack/sheet/iron,
/obj/item/stack/sheet/glass,
@@ -62,139 +114,45 @@
return TRUE
return FALSE
/datum/ai_planning_subtree/mug_robot
/datum/ai_planning_subtree/mug_robot/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/living_bot = controller.pawn
if(!(living_bot.bot_access_flags & BOT_COVER_EMAGGED))
return
var/static/list/robot_targets = typecacheof(
/mob/living/silicon/robot,
)
if(!controller.blackboard_key_exists(BB_ROBOT_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_search/valid_robot, BB_ROBOT_TARGET, robot_targets)
return
if(!living_bot.pulling)
controller.queue_behavior(/datum/ai_behavior/drag_target, BB_ROBOT_TARGET)
else
controller.queue_behavior(/datum/ai_behavior/bot_interact/tip_robot, BB_ROBOT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/bot_search/valid_robot
action_cooldown = 10 SECONDS
/datum/ai_behavior/bot_search/valid_robot/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
return (!HAS_TRAIT(my_target, TRAIT_MOB_TIPPED)) && can_see(controller.pawn, my_target)
/datum/ai_behavior/bot_interact/tip_robot
/datum/ai_behavior/bot_interact/tip_robot/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
if(succeeded)
var/mob/living/living_pawn = controller.pawn
living_pawn.stop_pulling()
///subtree to deconstruct things when we're emagged
/datum/ai_planning_subtree/repairbot_deconstruction
/datum/ai_planning_subtree/repairbot_deconstruction/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/basic/bot/living_bot = controller.pawn
if(!(living_bot.bot_access_flags & BOT_COVER_EMAGGED))
return
var/static/list/things_to_deconstruct = typecacheof(list(
/obj/structure/window,
/turf/open/floor,
/turf/closed/wall,
/datum/bt_node/ai_behavior/bot_search/refillable_target/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/static/list/refillable_items = typecacheof(list(
/obj/item/stack/sheet/iron,
/obj/item/stack/sheet/glass,
/obj/item/stack/tile,
))
if(!controller.blackboard_key_exists(BB_DECONSTRUCT_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_search/deconstructable, BB_DECONSTRUCT_TARGET, things_to_deconstruct)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/bot_interact, BB_DECONSTRUCT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
looking_for = refillable_items
return ..()
/datum/ai_behavior/bot_search/deconstructable
action_cooldown = 5 SECONDS
/datum/ai_behavior/bot_search/deconstructable/valid_target(datum/ai_controller/basic_controller/bot/controller, atom/my_target)
return (!(my_target.resistance_flags & INDESTRUCTIBLE) && !isgroundlessturf(my_target))
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf
turf_search = TRUE
time_between_perform = 5 SECONDS
///subtree to control bot speech
/datum/ai_planning_subtree/repairbot_speech
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/proc/get_turf_type_filter()
return typecacheof(list(/turf/open/floor/plating))
/datum/ai_planning_subtree/repairbot_speech/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
if(controller.blackboard[BB_REPAIRBOT_SPEECH_COOLDOWN] > world.time)
return
var/static/list/keys_to_look = list(
BB_WELDER_TARGET,
BB_WINDOW_FRAMETARGET,
BB_TILELESS_FLOOR,
BB_BREACHED_FLOOR,
BB_GIRDER_TO_WALL_TARGET,
BB_GIRDER_TARGET,
BB_DECONSTRUCT_TARGET,
)
for(var/key in keys_to_look)
if(controller.blackboard_key_exists(key))
controller.queue_behavior(/datum/ai_behavior/repairbot_speech, key)
return
/datum/ai_behavior/repairbot_speech
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/repairbot_speech/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/datum/action/cooldown/bot_announcement/announcement = controller.blackboard[BB_ANNOUNCE_ABILITY]
var/list/speech_to_pick_from = (target_key == BB_DECONSTRUCT_TARGET) ? controller.blackboard[BB_REPAIRBOT_EMAGGED_SPEECH] : controller.blackboard[BB_REPAIRBOT_NORMAL_SPEECH]
if(!length(speech_to_pick_from))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
announcement.announce(pick(speech_to_pick_from))
controller.set_blackboard_key(BB_REPAIRBOT_SPEECH_COOLDOWN, world.time + REPAIRBOT_SPEECH_TIMER)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
///subtree to replace iron platings
/datum/ai_planning_subtree/replace_floors
///flag we check before executing
var/required_flag = REPAIRBOT_REPLACE_TILES
///key of our floor target
var/floor_key = BB_TILELESS_FLOOR
///type of tile we need to replace floors
var/needed_tile_type = /obj/item/stack/tile
///type of floors we can replace
var/list/type_of_turf = list(/turf/open/floor/plating)
///our searching behavior
var/search_behavior = /datum/ai_behavior/bot_search/valid_plateless_turf
/datum/ai_planning_subtree/replace_floors/New()
. = ..()
type_of_turf = typecacheof(type_of_turf)
/datum/ai_planning_subtree/replace_floors/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/setup(datum/ai_controller/controller)
var/mob/living/basic/bot/repairbot/bot_pawn = controller.pawn
if(!(bot_pawn.repairbot_flags & required_flag))
return
if(!locate(needed_tile_type) in bot_pawn)
return
if(controller.blackboard_key_exists(floor_key))
controller.queue_behavior(/datum/ai_behavior/bot_interact, floor_key)
return SUBTREE_RETURN_FINISH_PLANNING
if(!(bot_pawn.repairbot_flags & REPAIRBOT_REPLACE_TILES))
return FALSE
if(!locate(/obj/item/stack/tile) in bot_pawn)
return FALSE
return TRUE
controller.queue_behavior(search_behavior, floor_key, type_of_turf, 5, 10, FALSE, TRUE)
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
looking_for = get_turf_type_filter()
return ..()
/datum/ai_behavior/bot_search/valid_plateless_turf
action_cooldown = 5 SECONDS
/datum/ai_behavior/bot_search/valid_plateless_turf/valid_target(datum/ai_controller/basic_controller/bot/controller, turf/open/my_target)
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/valid_target(datum/ai_controller/basic_controller/bot/controller, turf/open/my_target)
var/static/list/blacklist_objects = typecacheof(list(
/obj/structure/window,
/obj/structure/grille,
))
for(var/atom/possible_blacklisted in my_target.contents)
if(is_type_in_typecache(possible_blacklisted, blacklist_objects))
return FALSE
if(istype(my_target, /turf/open/floor/plating) && !can_see(controller.pawn, my_target, 5))
return FALSE
var/static/list/blacklist_areas = typecacheof(list(
/area/space,
/area/station/maintenance,
@@ -202,129 +160,133 @@
var/turf_area = get_area(my_target)
return !(is_type_in_typecache(turf_area, blacklist_areas))
///subtree to fix hull breaches
/datum/ai_planning_subtree/replace_floors/breaches
floor_key = BB_BREACHED_FLOOR
needed_tile_type = /obj/item/stack/tile/iron
type_of_turf = list(/turf/open/space)
required_flag = REPAIRBOT_FIX_BREACHES
search_behavior = /datum/ai_behavior/bot_search/valid_plateless_turf/breached
/// Breach variant: searches /turf/open/space instead, requires REPAIRBOT_FIX_BREACHES flag.
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/breached
///exists as to not conflict with the base turf searching behavior cause of how the queue system works...
/datum/ai_behavior/bot_search/valid_plateless_turf/breached
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/breached/setup(datum/ai_controller/controller)
var/mob/living/basic/bot/repairbot/bot_pawn = controller.pawn
if(!(bot_pawn.repairbot_flags & REPAIRBOT_FIX_BREACHES))
return FALSE
if(!locate(/obj/item/stack/tile/iron) in bot_pawn)
return FALSE
return TRUE
///subtree to build girders
/datum/ai_planning_subtree/build_girder
/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/breached/get_turf_type_filter()
return typecacheof(list(/turf/open/space))
/datum/ai_planning_subtree/build_girder/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
/datum/bt_node/ai_behavior/bot_search/valid_girder
time_between_perform = 5 SECONDS
/datum/bt_node/ai_behavior/bot_search/valid_girder/setup(datum/ai_controller/controller)
var/mob/living/basic/bot/repairbot/bot_pawn = controller.pawn
if(!(bot_pawn.repairbot_flags & REPAIRBOT_FIX_GIRDERS))
return FALSE
var/obj/item/stack/sheet/iron/my_iron = locate() in bot_pawn
if(isnull(my_iron) || my_iron.amount < 2)
return FALSE
return TRUE
/datum/bt_node/ai_behavior/bot_search/valid_girder/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/static/list/searchable_girder = typecacheof(list(/obj/structure/girder))
looking_for = searchable_girder
return ..()
/datum/bt_node/ai_behavior/bot_search/valid_girder/valid_target(datum/ai_controller/basic_controller/bot/controller, obj/my_target)
if(!istype(my_target, /obj/structure/girder))
return FALSE
return isfloorturf(my_target.loc)
/datum/bt_node/ai_behavior/targeted_mob_ability/build_girder
maximum_distance = 1
/datum/bt_node/ai_behavior/targeted_mob_ability/build_girder/setup(datum/ai_controller/controller)
var/mob/living/basic/bot/repairbot/bot_pawn = controller.pawn
if(!(bot_pawn.repairbot_flags & REPAIRBOT_BUILD_GIRDERS))
return
return FALSE
var/obj/item/stack/rods/my_rods = locate() in bot_pawn
if(isnull(my_rods) || my_rods.amount < 2)
return
var/datum/action/cooldown/ability = controller.blackboard[BB_GIRDER_BUILD_ABILITY]
return FALSE
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
if(!ability?.IsAvailable())
return
if(controller.blackboard_key_exists(BB_GIRDER_TARGET))
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/build_girder, BB_GIRDER_BUILD_ABILITY, BB_GIRDER_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
return FALSE
return ..()
/datum/bt_node/ai_behavior/targeted_mob_ability/build_girder/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.clear_blackboard_key(target_key)
/// Search for open turfs adjacent to space (valid girder build locations).
/datum/bt_node/ai_behavior/bot_search/valid_wall_target
turf_search = TRUE
time_between_perform = 5 SECONDS
/datum/bt_node/ai_behavior/bot_search/valid_wall_target/setup(datum/ai_controller/controller)
var/mob/living/basic/bot/repairbot/bot_pawn = controller.pawn
if(!(bot_pawn.repairbot_flags & REPAIRBOT_BUILD_GIRDERS))
return FALSE
var/obj/item/stack/rods/my_rods = locate() in bot_pawn
if(isnull(my_rods) || my_rods.amount < 2)
return FALSE
return TRUE
/datum/bt_node/ai_behavior/bot_search/valid_wall_target/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/static/list/searchable_turfs = typecacheof(list(/turf/open))
controller.queue_behavior(/datum/ai_behavior/bot_search/valid_wall_target, BB_GIRDER_TARGET, searchable_turfs, 5, 10, FALSE, TRUE)
looking_for = searchable_turfs
return ..()
/datum/ai_behavior/bot_search/valid_wall_target
action_cooldown = 5 SECONDS
/datum/ai_behavior/bot_search/valid_wall_target/valid_target(datum/ai_controller/basic_controller/bot/controller, turf/my_target)
/datum/bt_node/ai_behavior/bot_search/valid_wall_target/valid_target(datum/ai_controller/basic_controller/bot/controller, turf/my_target)
if(!istype(my_target, /turf/open))
return FALSE
if(istype(get_area(my_target), /area/space) || isgroundlessturf(my_target) || my_target.is_blocked_turf())
return FALSE
var/static/list/blacklist_objects = list(
var/static/list/blacklist_objects = typecacheof(list(
/obj/machinery/door,
/obj/structure/grille,
)
))
for(var/atom/contents in my_target)
if(is_type_in_typecache(contents, blacklist_objects))
return FALSE
var/turf/adjacent_turfs = get_adjacent_open_turfs(my_target)
for(var/turf/possible_spaced_turf as anything in adjacent_turfs)
if(isspaceturf(possible_spaced_turf) && istype(get_area(possible_spaced_turf), /area/space))
return TRUE
return FALSE
/datum/ai_behavior/targeted_mob_ability/build_girder
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/bt_node/ai_behavior/bot_search/valid_grille_target
time_between_perform = 5 SECONDS
/datum/ai_behavior/targeted_mob_ability/build_girder/setup(datum/ai_controller/controller, ability_key, target_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
/datum/bt_node/ai_behavior/bot_search/valid_grille_target/setup(datum/ai_controller/controller)
var/mob/living/basic/bot/repairbot/bot_pawn = controller.pawn
if(!(bot_pawn.repairbot_flags & REPAIRBOT_REPLACE_WINDOWS))
return FALSE
set_movement_target(controller, target)
if(!locate(/obj/item/stack/sheet/glass) in bot_pawn)
return FALSE
return TRUE
/datum/ai_behavior/targeted_mob_ability/build_girder/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
///subtree to place glass on windows
/datum/ai_planning_subtree/replace_window
/datum/ai_planning_subtree/replace_window/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/bot/repairbot/living_pawn = controller.pawn
if(!(living_pawn.repairbot_flags & REPAIRBOT_REPLACE_WINDOWS))
return
if(!locate(/obj/item/stack/sheet/glass) in living_pawn)
return
if(controller.blackboard_key_exists(BB_WINDOW_FRAMETARGET))
controller.queue_behavior(/datum/ai_behavior/bot_interact, BB_WINDOW_FRAMETARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/bt_node/ai_behavior/bot_search/valid_grille_target/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/static/list/searchable_grilles = typecacheof(list(/obj/structure/grille))
controller.queue_behavior(/datum/ai_behavior/bot_search/valid_grille_target, BB_WINDOW_FRAMETARGET, searchable_grilles)
looking_for = searchable_grilles
return ..()
/datum/ai_behavior/bot_search/valid_grille_target/valid_target(datum/ai_controller/basic_controller/bot/controller, obj/structure/my_target)
/datum/bt_node/ai_behavior/bot_search/valid_grille_target/valid_target(datum/ai_controller/basic_controller/bot/controller, obj/structure/my_target)
if(!istype(my_target, /obj/structure/grille))
return FALSE
if(locate(/obj/structure/window) in get_turf(my_target))
return FALSE
return (!istype(get_area(my_target), /area/space))
/datum/bt_node/ai_behavior/bot_search/valid_window_fix
time_between_perform = 5 SECONDS
///subtree to place iron on girders
/datum/ai_planning_subtree/wall_girder
/datum/ai_planning_subtree/wall_girder/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/bot/repairbot/living_pawn = controller.pawn
if(!(living_pawn.repairbot_flags & REPAIRBOT_FIX_GIRDERS))
return
var/obj/item/stack/sheet/iron/my_iron = locate() in living_pawn
if(isnull(my_iron) || my_iron.amount < 2)
return
if(controller.blackboard_key_exists(BB_GIRDER_TO_WALL_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_interact, BB_GIRDER_TO_WALL_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
var/static/list/searchable_girder = typecacheof(list(/obj/structure/girder))
controller.queue_behavior(/datum/ai_behavior/bot_search/valid_girder, BB_GIRDER_TO_WALL_TARGET, searchable_girder)
/datum/ai_behavior/bot_search/valid_girder
action_cooldown = 5 SECONDS
/datum/ai_behavior/bot_search/valid_girder/valid_target(datum/ai_controller/basic_controller/bot/controller, obj/my_target)
return isfloorturf(my_target.loc)
///subtree to repair machines with welders
/datum/ai_planning_subtree/fix_window
/datum/ai_planning_subtree/fix_window/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard_key_exists(BB_WELDER_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_interact, BB_WELDER_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/bt_node/ai_behavior/bot_search/valid_window_fix/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/static/list/searchable_objects = typecacheof(list(/obj/structure/window))
controller.queue_behavior(/datum/ai_behavior/bot_search/valid_window_fix, BB_WELDER_TARGET, searchable_objects)
looking_for = searchable_objects
return ..()
/datum/ai_behavior/bot_search/valid_window_fix
action_cooldown = 5 SECONDS
/datum/ai_behavior/bot_search/valid_window_fix/valid_target(datum/ai_controller/basic_controller/bot/controller, obj/my_target)
/datum/bt_node/ai_behavior/bot_search/valid_window_fix/valid_target(datum/ai_controller/basic_controller/bot/controller, obj/my_target)
if(!istype(my_target, /obj/structure/window))
return FALSE
return (my_target.get_integrity() < my_target.max_integrity || !my_target.anchored)
#undef REPAIRBOT_SPEECH_TIMER
@@ -0,0 +1,71 @@
{
"dm_type": "/datum/bt_node/subtree/repairbot_emagged",
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_ROBOT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_ROBOT_TARGET",
"required_dist": 0,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_interact/tip_robot",
"vars": {
"target_key": "BB_ROBOT_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/grab_target",
"vars": {
"target_key": "BB_ROBOT_TARGET"
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_DECONSTRUCT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_DECONSTRUCT_TARGET",
"required_dist": 0,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_interact",
"vars": {
"target_key": "BB_DECONSTRUCT_TARGET"
}
}
]
}
}
]
}
@@ -0,0 +1,143 @@
{
"dm_type": "/datum/bt_node/subtree/repairbot_find_target",
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_is_emagged",
"vars": {
"invert": false
},
"child": {
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_robot",
"vars": {
"target_key": "BB_ROBOT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/deconstructable",
"vars": {
"target_key": "BB_DECONSTRUCT_TARGET",
"minimum_distance": 1
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_NONE",
"invert": true,
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "selector",
"children": [
{
"type": "sequence",
"children": [
{
"type": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf/breached",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/refillable_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_grille_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_plateless_turf",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_window_fix",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_girder",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_REPAIRBOT_INTERACTION_TYPE",
"value": "REPAIRBOT_INTERACTION_INTERACT"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/cancel_current_plan"
}
]
},
{
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_search/valid_wall_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"minimum_distance": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_REPAIRBOT_INTERACTION_TYPE",
"value": "REPAIRBOT_INTERACTION_BUILD_GIRDERS"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/cancel_current_plan"
}
]
}
]
}
}
]
}
@@ -0,0 +1,88 @@
{
"dm_type": "/datum/bt_node/subtree/repairbot_repair_target",
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_is_emagged",
"vars": {
"observer_abort": "BT_ABORT_BOTH"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree/repairbot_emagged"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bot_is_emagged",
"vars": {
"observer_abort": "BT_ABORT_SELF",
"invert": true
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"required_dist": 1,
"finish_on_arrival": true,
"movement_type": "/datum/ai_movement/jps"
}
},
{
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_equals",
"vars": {
"key": "BB_REPAIRBOT_INTERACTION_TYPE",
"value": "REPAIRBOT_INTERACTION_INTERACT"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/bot_interact",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_equals",
"vars": {
"key": "BB_REPAIRBOT_INTERACTION_TYPE",
"value": "REPAIRBOT_INTERACTION_BUILD_GIRDERS"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability/build_girder",
"vars": {
"ability_key": "BB_GIRDER_BUILD_ABILITY",
"target_key": "BB_CURRENT_TARGET"
}
}
}
]
}
]
}
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
}
@@ -0,0 +1,85 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/secbot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"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/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": true,
"movement_type": "/datum/ai_movement/basic_avoidance"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
@@ -198,7 +198,7 @@
/mob/living/basic/bot/secbot/proc/on_entered(datum/source, atom/movable/to_be_tripped)
SIGNAL_HANDLER
var/mob/living/possible_target = ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
var/mob/living/possible_target = ai_controller.blackboard[BB_CURRENT_TARGET]
if(!has_gravity() || !ismob(to_be_tripped) || !possible_target)
return
var/mob/living/carbon/tripped_mob = to_be_tripped
@@ -1,23 +1,17 @@
/datum/ai_controller/basic_controller/bot/secbot
behavior_tree_json = "code/modules/mob/living/basic/bots/secbot/secbot.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/secbot,
BB_UNREACHABLE_LIST_COOLDOWN = 1 MINUTES,
BB_ALWAYS_IGNORE_FACTION = TRUE,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/arrest_target,
/datum/ai_planning_subtree/find_patrol_beacon,
)
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
BB_BOT_SUMMON_TARGET,
)
/datum/targeting_strategy/basic/secbot/can_attack(mob/living/living_mob, atom/the_target, vision_range)
/datum/targeting_strategy/basic/secbot/is_valid_target(mob/living/living_mob, atom/the_target, vision_range, datum/ai_controller/controller = null)
var/datum/ai_controller/basic_controller/bot/my_controller = living_mob.ai_controller
if(isnull(my_controller))
return FALSE
@@ -37,13 +31,12 @@
my_controller.set_blackboard_key(BB_CURRENT_CRIMINAL_ASSESSMENT, assessed_threat)
return (assessed_threat > THREAT_ASSESS_DANGEROUS)
/datum/ai_controller/basic_controller/bot/secbot/TryPossessPawn(atom/new_pawn)
. = ..()
if(. & AI_CONTROLLER_INCOMPATIBLE)
return
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_target_set))
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_clear_target))
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_CURRENT_TARGET), PROC_REF(on_target_set))
RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(BB_CURRENT_TARGET), PROC_REF(on_clear_target))
/datum/ai_controller/basic_controller/bot/secbot/proc/on_target_set()
SIGNAL_HANDLER
@@ -61,31 +54,3 @@
/datum/ai_controller/basic_controller/bot/secbot/proc/on_clear_target()
SIGNAL_HANDLER
clear_blackboard_key(BB_CURRENT_CRIMINAL_ASSESSMENT)
/datum/ai_planning_subtree/arrest_target
///what behavior do we use when arresting?
var/datum/ai_behavior/arrest_behavior = /datum/ai_behavior/basic_melee_attack/interact_once/bot
/datum/ai_planning_subtree/arrest_target/SelectBehaviors(datum/ai_controller/basic_controller/bot/controller, seconds_per_tick)
var/mob/living/carbon/my_target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
if(QDELETED(my_target) || !istype(my_target) || my_target.handcuffed)
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
var/mob/living/basic/bot/secbot/my_bot = controller.pawn
var/bot_flags = my_bot.security_mode_flags
if(my_target.IsParalyzed() && !(bot_flags & SECBOT_HANDCUFF_TARGET))
controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
return
controller.queue_behavior(arrest_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/basic_melee_attack/interact_once/bot
movement_behavior = /datum/ai_movement/basic_avoidance
/datum/ai_behavior/basic_melee_attack/interact_once/bot/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key)
var/mob/living/carbon/human/human_target = controller.blackboard[target_key]
if(!isnull(human_target) && human_target.handcuffed)
controller.remove_from_blackboard_lazylist_key(BB_BASIC_MOB_RETALIATE_LIST, human_target)
return ..()
@@ -57,7 +57,7 @@
/mob/living/basic/bot/secbot/grievous/on_entered(datum/source, atom/movable/movable_target)
. = ..()
if(!ismob(movable_target) || !ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] == movable_target)
if(!ismob(movable_target) || !ai_controller.blackboard[BB_CURRENT_TARGET] == movable_target)
return
visible_message(span_warning("[src] flails his swords and cuts [movable_target]!"))
playsound(src, 'sound/mobs/non-humanoids/beepsky/beepskyspinsabre.ogg' , 100, TRUE, -1)
@@ -1,16 +1,8 @@
/datum/ai_controller/basic_controller/bot/secbot/super_beepsky
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_patrol_beacon,
)
/datum/ai_controller/basic_controller/bot/secbot/super_beepsky/on_target_set()
. = ..()
var/mob/living/basic/bot/secbot/grievous/super_beeps = pawn
if(!super_beeps.sword_active)
INVOKE_ASYNC(super_beeps.weapon, TYPE_PROC_REF(/obj/item, attack_self), super_beeps)
super_beeps.visible_message("<b>[super_beeps]</b> points at [blackboard[BB_BASIC_MOB_CURRENT_TARGET]]!")
super_beeps.visible_message("<b>[super_beeps]</b> points at [blackboard[BB_CURRENT_TARGET]]!")
@@ -0,0 +1,74 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bot/vibebot",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity/pacifist"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_respond_to_summon"
},
{
"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_VIBEBOT_PARTY_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_VIBEBOT_PARTY_TARGET",
"required_dist": 1,
"finish_on_arrival": true
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/vibebot_party",
"vars": {
"ability_key": "BB_VIBEBOT_PARTY_ABILITY",
"target_key": "BB_VIBEBOT_PARTY_TARGET"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/bot_patrol"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_VIBEBOT_PARTY_TARGET",
"target_source": "/datum/target_source/oview_single_type/human_mob",
"targeting_strategy": "/datum/targeting_strategy/conscious_human/party_friend",
"ignore_list_key": "BB_TEMPORARY_IGNORE_LIST",
"vision_range": 5,
"time_between_perform": "5 SECONDS"
}
}
]
}
]
}
@@ -1,16 +1,11 @@
/datum/ai_controller/basic_controller/bot/vibebot
behavior_tree_json = "code/modules/mob/living/basic/bots/vibebot/vibebot.bt.json"
blackboard = list(
BB_UNREACHABLE_LIST_COOLDOWN = 2 MINUTES,
BB_VIBEBOT_HAPPY_SONG = VIBEBOT_CHEER_SONG,
BB_VIBEBOT_GRIM_SONG = VIBEBOT_GRIM_MUSIC,
BB_VIBEBOT_BIRTHDAY_SONG = VIBEBOT_HAPPY_BIRTHDAY,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity/pacifist,
/datum/ai_planning_subtree/respond_to_summon,
/datum/ai_planning_subtree/find_party_friends,
/datum/ai_planning_subtree/find_patrol_beacon,
)
reset_keys = list(
BB_BEACON_TARGET,
BB_PREVIOUS_BEACON_TARGET,
@@ -45,43 +40,36 @@
song.start_playing(pawn)
addtimer(CALLBACK(song, TYPE_PROC_REF(/datum/song, stop_playing)), 10 SECONDS) //in 10 seconds, stop playing music
///subtree we use to find party friends in general
/datum/ai_planning_subtree/find_party_friends
/datum/ai_planning_subtree/find_party_friends/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/static/list/type_to_search = typecacheof(list(/mob/living/carbon/human))
if(!controller.blackboard_key_exists(BB_VIBEBOT_PARTY_TARGET))
controller.queue_behavior(/datum/ai_behavior/bot_search/party_friends, BB_VIBEBOT_PARTY_TARGET, type_to_search)
return
/// Valid if the target is a conscious human who's in a bad mood or having a birthday someone who could use cheering up.
/datum/targeting_strategy/conscious_human/party_friend/is_valid_target(mob/living/living_mob, atom/target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
var/mob/living/carbon/human/human_target = target
return human_target.mob_mood?.mood_level < MOOD_LEVEL_NEUTRAL || HAS_TRAIT(human_target, TRAIT_BIRTHDAY_BOY)
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target/vibebot_party, BB_VIBEBOT_PARTY_ABILITY, BB_VIBEBOT_PARTY_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/bt_node/ai_behavior/vibebot_party
var/ability_key
var/target_key
///behavior we use to party with people
/datum/ai_behavior/targeted_mob_ability/and_clear_target/vibebot_party
behavior_flags = AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/bt_node/ai_behavior/vibebot_party/perform(seconds_per_tick, datum/ai_controller/basic_controller/bot/controller)
var/mob/living/living_target = controller.blackboard[target_key]
if(QDELETED(living_target))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(get_dist(controller.pawn, living_target) > 1)
return AI_BEHAVIOR_INSTANT
var/datum/action/cooldown/ability = controller.blackboard[ability_key]
if(ability)
INVOKE_ASYNC(ability, TYPE_PROC_REF(/datum/action, Trigger), living_target)
var/mob/living/living_pawn = controller.pawn
living_pawn.manual_emote("celebrates with [living_target]!")
INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), "flip")
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/targeted_mob_ability/and_clear_target/vibebot_party/setup(datum/ai_controller/controller, ability_key, target_key)
/datum/bt_node/ai_behavior/vibebot_party/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/targeted_mob_ability/and_clear_target/vibebot_party/finish_action(datum/ai_controller/basic_controller/bot/controller, succeeded, ability_key, target_key)
var/atom/target = controller.blackboard[target_key]
controller.add_to_blacklist(target)
if(succeeded)
var/mob/living/living_pawn = controller.pawn
living_pawn.manual_emote("celebrates with [target]!")
living_pawn.emote("flip")
return ..()
///behavior that searches for party friends
/datum/ai_behavior/bot_search/party_friends
action_cooldown = 5 SECONDS
/datum/ai_behavior/bot_search/party_friends/valid_target(datum/ai_controller/basic_controller/bot/controller, mob/living/carbon/human/my_target)
if(my_target.stat != CONSCIOUS || isnull(my_target.mind))
return FALSE
return (my_target.mob_mood.mood_level < MOOD_LEVEL_NEUTRAL || HAS_TRAIT(my_target, TRAIT_BIRTHDAY_BOY))
if(!isnull(target))
controller.add_to_blacklist(target)
controller.clear_blackboard_key(target_key)
@@ -0,0 +1,28 @@
{
"dm_type": "/datum/ai_controller/basic_controller/clown",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": "subtree",
"subtype": "/datum/bt_node/subtree/simple_retaliate_combat"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
]
}
@@ -1,17 +1,11 @@
/datum/ai_controller/basic_controller/clown
behavior_tree_json = "code/modules/mob/living/basic/clown/clown.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_BASIC_MOB_SPEAK_LINES = null,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/blackboard,
)
/datum/ai_controller/basic_controller/clown/murder
blackboard = list(
@@ -0,0 +1,83 @@
{
"dm_type": "/datum/ai_controller/basic_controller/artificer",
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"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/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
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/run_away_from_target"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/most_wounded",
"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/acquire_target/target_from_retaliate_list",
"vars": {
"target_key": "BB_BASIC_MOB_FLEE_TARGET",
"targeting_strategy": "BB_FLEE_TARGETING_STRATEGY",
"hiding_location_key": "BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION"
}
}
]
}
@@ -5,6 +5,7 @@
* If there is no one to heal, they will run away from any non-allied mobs.
*/
/datum/ai_controller/basic_controller/artificer
behavior_tree_json = "code/modules/mob/living/basic/cult/constructs/artificer.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/same_faction/construct,
BB_FLEE_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
@@ -12,14 +13,6 @@
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_wounded_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/target_retaliate/to_flee,
/datum/ai_planning_subtree/flee_target/from_flee_key,
)
/**
* Juggernauts
@@ -27,19 +20,13 @@
* Juggernauts slowly walk toward non-allied mobs and pummel them to death.
*/
/datum/ai_controller/basic_controller/juggernaut
behavior_tree_json = "code/modules/mob/living/basic/cult/constructs/juggernaut.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/**
* Proteons
@@ -47,6 +34,7 @@
* Proteons perform cowardly hit-and-run attacks, fleeing melee when struck but returning to fight again.
*/
/datum/ai_controller/basic_controller/proteon
behavior_tree_json = "code/modules/mob/living/basic/cult/constructs/proteon.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
@@ -54,15 +42,6 @@
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/target_retaliate/to_flee,
/datum/ai_planning_subtree/flee_target/from_flee_key,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/**
* Wraiths
@@ -70,25 +49,17 @@
* Wraiths seek out the most injured non-allied mob to beat to death.
*/
/datum/ai_controller/basic_controller/wraith
behavior_tree_json = "code/modules/mob/living/basic/cult/constructs/wraith.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = HARD_CRIT,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/simple_find_wounded_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/// Targeting strategy that will only allow mobs that constructs can heal.
/datum/targeting_strategy/basic/same_faction/construct
target_wounded_key = BB_TARGET_WOUNDED_ONLY
/datum/targeting_strategy/basic/same_faction/construct/can_attack(mob/living/living_mob, atom/the_target, vision_range, check_faction = TRUE)
/datum/targeting_strategy/basic/same_faction/construct/is_valid_target(mob/living/living_mob, atom/the_target, vision_range, datum/ai_controller/controller = null)
if(isconstruct(the_target) || istype(the_target, /mob/living/basic/shade))
return ..()
return FALSE
@@ -0,0 +1,5 @@
{
"dm_type": "/datum/ai_controller/basic_controller/juggernaut",
"type": "subtree",
"subtype": "/datum/bt_node/subtree/simple_hostile_obstacles_combat"
}
@@ -0,0 +1,14 @@
{
"dm_type": "/datum/ai_controller/basic_controller/proteon",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/skittish_brawler_combat"
}
]
}
@@ -0,0 +1,83 @@
{
"dm_type": "/datum/ai_controller/basic_controller/wraith",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": "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": "selector",
"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/attack_obstructions",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
}
]
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"required_dist": 1
}
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets/most_wounded",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
@@ -0,0 +1,137 @@
{
"dm_type": "/datum/ai_controller/basic_controller/vatbeast",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": "subtree",
"subtype": "/datum/bt_node/subtree/move_to_and_eat"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"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": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/attack_obstructions",
"vars": {
"target_key": "BB_CURRENT_TARGET"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_at_least",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"key": "BB_BASIC_MOB_HAS_TARGET_TIME",
"minimum": "10 SECONDS"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/targeted_mob_ability/melee",
"vars": {
"ability_key": "BB_GENERIC_ACTION",
"target_key": "BB_CURRENT_TARGET"
}
}
},
{
"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": "selector",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_TARGET_FOOD",
"targeting_strategy": "/datum/targeting_strategy/pickup_item",
"target_source": "/datum/target_source/oview_typed/from_bb_key/basic_foods"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/target_from_retaliate_list",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION",
"check_faction": true
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
}
]
}
]
}
]
}
@@ -47,7 +47,7 @@
var/datum/action/cooldown/tentacle_slap/slapper = new (src)
slapper.Grant(src)
ai_controller.set_blackboard_key(BB_TARGETED_ACTION, slapper)
ai_controller.set_blackboard_key(BB_GENERIC_ACTION, slapper)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(enjoyed_food))
/mob/living/basic/vatbeast/tamed(mob/living/tamer, obj/item/food)
@@ -64,34 +64,12 @@
/// Attack people and slap them
/datum/ai_controller/basic_controller/vatbeast
behavior_tree_json = "code/modules/mob/living/basic/cytology/vatbeast.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/targeted_mob_ability/vatbeast_slap,
/datum/ai_planning_subtree/basic_melee_attack_subtree
)
/// Only do this if we are adjacent to target and have been mad at the same guy for at least 10 seconds
/// That slap REALLY hurts
/datum/ai_planning_subtree/targeted_mob_ability/vatbeast_slap
operational_datums = list(/datum/component/ai_target_timer)
/datum/ai_planning_subtree/targeted_mob_ability/vatbeast_slap/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[target_key]
if (!isliving(target) || !controller.pawn.Adjacent(target))
return
var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
if (time_on_target < 10 SECONDS)
return
return ..()
/// Ability that allows the owner to slap other mobs a short distance away.
/// For vatbeats, this ability is shared with the rider.
@@ -55,7 +55,7 @@
/datum/pet_command/free,
/datum/pet_command/beehive/enter,
/datum/pet_command/beehive/exit,
/datum/pet_command/follow/bee,
/datum/pet_command/follow,
/datum/pet_command/attack/swirl,
/datum/pet_command/scatter,
)
@@ -0,0 +1,86 @@
{
"dm_type": "/datum/ai_controller/basic_controller/bee",
"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": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree",
"override_id": "SUBPLAN_ID_PET_COMMAND"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/find_hive"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/transition_hive_status"
},
{
"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": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/pollinate_target"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/simple_hostile_combat"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/enter_exit_hive"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/random_chance",
"vars": {
"chance": 0.85
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_TARGET_HYDRO",
"target_source": "/datum/target_source/oview_single_type/hydroponics",
"targeting_strategy": "/datum/targeting_strategy/pollinatable_hydro",
"vision_range": 10,
"time_between_perform": "10 SECONDS"
}
}
}
]
}
]
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"invert": true,
"key": "BB_TARGET_HOME"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/find_hive"
}
}
]
}
@@ -1,87 +1,9 @@
/// if we have a hive, this will be our aggro distance
#define AGGRO_DISTANCE_FROM_HIVE 2
/datum/ai_behavior/hunt_target/pollinate
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/pollinate/target_caught(mob/living/hunter, obj/machinery/hydroponics/hydro_target)
var/datum/callback/callback = CALLBACK(hunter, TYPE_PROC_REF(/mob/living/basic/bee, pollinate), hydro_target)
callback.Invoke()
/datum/ai_behavior/find_hunt_target/pollinate
action_cooldown = 10 SECONDS
/datum/ai_behavior/find_hunt_target/pollinate/valid_dinner(mob/living/source, obj/machinery/hydroponics/dinner, radius)
if(!dinner.can_bee_pollinate())
return FALSE
return can_see(source, dinner, radius)
/datum/ai_behavior/enter_exit_hive
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
action_cooldown = 10 SECONDS
/datum/ai_behavior/enter_exit_hive/setup(datum/ai_controller/controller, target_key, attack_key)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/enter_exit_hive/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
var/obj/structure/beebox/current_home = controller.blackboard[target_key]
var/atom/attack_target = controller.blackboard[attack_key]
if(attack_target) // forget about who we attacking when we go home
controller.clear_blackboard_key(attack_key)
controller.ai_interact(target = current_home, combat_mode = FALSE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/inhabit_hive
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/inhabit_hive/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/inhabit_hive/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/obj/structure/beebox/potential_home = controller.blackboard[target_key]
var/mob/living/bee_pawn = controller.pawn
if(!potential_home.habitable(bee_pawn)) //the house become full before we get to it
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.ai_interact(target = potential_home, combat_mode = FALSE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/inhabit_hive/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
if(!succeeded)
controller.clear_blackboard_key(target_key) //failed to make it our home so find another
/datum/ai_behavior/find_and_set/bee_hive
action_cooldown = 10 SECONDS
/datum/ai_behavior/find_and_set/bee_hive/search_tactic(datum/ai_controller/controller, locate_path, search_range = SEARCH_TACTIC_DEFAULT_RANGE)
var/list/valid_hives = list()
var/mob/living/bee_pawn = controller.pawn
if(istype(bee_pawn.loc, /obj/structure/beebox))
return bee_pawn.loc //for premade homes
for(var/obj/structure/beebox/potential_home in oview(search_range, bee_pawn))
if(!potential_home.habitable(bee_pawn))
continue
valid_hives += potential_home
if(valid_hives.len)
return pick(valid_hives)
/datum/targeting_strategy/basic/bee
/datum/targeting_strategy/basic/bee/can_attack(mob/living/owner, atom/target, vision_range)
/datum/targeting_strategy/basic/bee/is_valid_target(mob/living/owner, atom/target, vision_range, datum/ai_controller/controller = null)
if(!isliving(target))
return FALSE
. = ..()
@@ -104,14 +26,6 @@
return !(mob_target.bee_friendly())
///pet commands
/datum/pet_command/follow/bee
///the behavior we use to follow
follow_behavior = /datum/ai_behavior/pet_follow_friend/bee
/datum/ai_behavior/pet_follow_friend/bee
required_distance = 0
///swirl around the owner in menacing fashion
/datum/pet_command/attack/swirl
command_name = "Swirl"
@@ -138,49 +52,7 @@
/datum/pet_command/attack/swirl/execute_action(datum/ai_controller/controller)
if(controller.blackboard_key_exists(BB_CURRENT_PET_TARGET))
return ..()
controller.queue_behavior(/datum/ai_behavior/swirl_around_target, BB_SWARM_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/swirl_around_target
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
required_distance = 0
///chance to swirl
var/swirl_chance = 60
/datum/ai_behavior/swirl_around_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/swirl_around_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
var/atom/target = controller.blackboard[target_key]
var/mob/living/living_pawn = controller.pawn
if(QDELETED(target))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
if(get_dist(target, living_pawn) > 1)
set_movement_target(controller, target)
return AI_BEHAVIOR_DELAY
if(!SPT_PROB(swirl_chance, seconds_per_tick))
return AI_BEHAVIOR_DELAY
var/list/possible_turfs = list()
for(var/turf/possible_turf in oview(2, target))
if(possible_turf.is_blocked_turf(source_atom = living_pawn))
continue
possible_turfs += possible_turf
if(!length(possible_turfs))
return AI_BEHAVIOR_DELAY
if(isnull(controller.movement_target_source) || controller.movement_target_source == type)
set_movement_target(controller, pick(possible_turfs))
return AI_BEHAVIOR_DELAY
controller.set_behavior_tree_override(SUBPLAN_ID_PET_COMMAND, /datum/bt_node/subtree/pet_command/swirl)
/datum/pet_command/beehive
@@ -205,9 +77,7 @@
return
/datum/pet_command/beehive/execute_action(datum/ai_controller/controller)
controller.queue_behavior(/datum/ai_behavior/enter_exit_hive, BB_CURRENT_HOME)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
return SUBTREE_RETURN_FINISH_PLANNING
controller.set_behavior_tree_override(SUBPLAN_ID_PET_COMMAND, /datum/bt_node/subtree/pet_command/beehive)
/datum/pet_command/beehive/enter
command_name = "Enter beehive"
@@ -237,11 +107,7 @@
set_command_target(parent, commander)
/datum/pet_command/scatter/execute_action(datum/ai_controller/controller)
controller.queue_behavior(/datum/ai_behavior/run_away_from_target/scatter, BB_CURRENT_PET_TARGET)
controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
return SUBTREE_RETURN_FINISH_PLANNING
controller.set_behavior_tree_override(SUBPLAN_ID_PET_COMMAND, /datum/bt_node/subtree/pet_command/scatter)
/datum/ai_behavior/run_away_from_target/scatter
run_distance = 4
#undef AGGRO_DISTANCE_FROM_HIVE
@@ -6,22 +6,7 @@
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/find_valid_home,
/datum/ai_planning_subtree/enter_exit_home,
/datum/ai_planning_subtree/find_and_hunt_target/pollinate,
/datum/ai_planning_subtree/simple_find_target/bee,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
/datum/ai_planning_subtree/simple_find_target/bee/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/atom/hydro_target = controller.blackboard[BB_TARGET_HYDRO]
if(hydro_target)
return SUBTREE_RETURN_FINISH_PLANNING
return ..()
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/bee/bee.bt.json"
/datum/ai_controller/basic_controller/queen_bee
blackboard = list(
@@ -30,70 +15,14 @@
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/find_valid_home,
/datum/ai_planning_subtree/enter_exit_home/queen,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/bee/queen_bee.bt.json"
/datum/ai_planning_subtree/find_valid_home
/datum/bt_node/subtree/pollinate_target
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/bee/pollinate_target.bt.json"
/datum/ai_planning_subtree/find_valid_home/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/work_bee = controller.pawn
/datum/bt_node/subtree/find_hive
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/bee/find_hive.bt.json"
var/obj/structure/beebox/current_home = controller.blackboard[BB_CURRENT_HOME]
if(QDELETED(current_home))
controller.queue_behavior(/datum/ai_behavior/find_and_set/bee_hive, BB_CURRENT_HOME, /obj/structure/beebox)
return
if(work_bee in current_home.bees)
return
controller.queue_behavior(/datum/ai_behavior/inhabit_hive, BB_CURRENT_HOME)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_planning_subtree/enter_exit_home
///chance we go back home
var/flyback_chance = 15
///chance we exit the home
var/exit_chance = 35
/datum/ai_planning_subtree/enter_exit_home/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/obj/structure/beebox/current_home = controller.blackboard[BB_CURRENT_HOME]
if(QDELETED(current_home))
return
var/mob/living/bee_pawn = controller.pawn
var/action_prob = (bee_pawn.loc == current_home) ? exit_chance : flyback_chance
if(!SPT_PROB(action_prob, seconds_per_tick))
return
controller.queue_behavior(/datum/ai_behavior/enter_exit_hive, BB_CURRENT_HOME, BB_BASIC_MOB_CURRENT_TARGET)
return SUBTREE_RETURN_FINISH_PLANNING
//the queen spend more time in the hive
/datum/ai_planning_subtree/enter_exit_home/queen
flyback_chance = 85
exit_chance = 5
/datum/ai_planning_subtree/find_and_hunt_target/pollinate
target_key = BB_TARGET_HYDRO
hunting_behavior = /datum/ai_behavior/hunt_target/pollinate
finding_behavior = /datum/ai_behavior/find_hunt_target/pollinate
hunt_targets = list(/obj/machinery/hydroponics)
hunt_range = 10
hunt_chance = 85
/datum/ai_planning_subtree/find_and_hunt_target/pollinate/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/atom/atom_pawn = controller.pawn
if(!isturf(atom_pawn.loc))
return
return ..()
/datum/bt_node/subtree/transition_hive_status
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/bee/transition_hive_status.bt.json"
@@ -0,0 +1,132 @@
/**
* Searches for a valid beebox home. Skipped if the bee is already inside its home.
* Sets BB_CURRENT_HOME when found.
*/
/datum/bt_node/ai_behavior/find_hive
/datum/bt_node/ai_behavior/find_hive/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/basic/bee/bee_pawn = controller.pawn
var/obj/structure/beebox/current_home = controller.blackboard[BB_CURRENT_HOME]
if(!QDELETED(current_home))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED // already have a home; let the inhabit sequence handle it
if(istype(bee_pawn.loc, /obj/structure/beebox))
controller.set_blackboard_key(BB_CURRENT_HOME, bee_pawn.loc)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
for(var/obj/structure/beebox/potential_home in oview(10, bee_pawn))
if(!potential_home.habitable(bee_pawn))
continue
controller.set_blackboard_key(BB_TARGET_HOME, potential_home)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
/**
* Moves to BB_CURRENT_HOME and inhabits it (ai_interact). Clears BB_CURRENT_HOME on failure.
* Must be in range (adjacent) to work. Returns FAILURE if home is gone or full.
*/
/datum/bt_node/ai_behavior/inhabit_hive
time_between_perform = 10 SECONDS
/datum/bt_node/ai_behavior/inhabit_hive/perform(seconds_per_tick, datum/ai_controller/controller)
var/mob/living/basic/bee/bee_pawn = controller.pawn
var/obj/structure/beebox/home = controller.blackboard[BB_CURRENT_HOME]
if(QDELETED(home))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(!home.habitable(bee_pawn))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
if(!bee_pawn.Adjacent(home))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
INVOKE_ASYNC(controller, TYPE_PROC_REF(/datum/ai_controller, ai_interact), home, FALSE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/bt_node/ai_behavior/inhabit_hive/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
if(!succeeded)
controller.clear_blackboard_key(BB_CURRENT_HOME)
///Chance to leave or enter hive
/datum/bt_node/ai_behavior/enter_exit_hive
time_between_perform = 1 SECONDS
var/flyback_chance = 15
var/exit_chance = 35
/datum/bt_node/ai_behavior/enter_exit_hive/perform(seconds_per_tick, datum/ai_controller/controller)
var/obj/structure/beebox/home = controller.blackboard[BB_CURRENT_HOME]
if(QDELETED(home))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/mob/living/bee_pawn = controller.pawn
var/prob_to_use = (bee_pawn.loc == home) ? exit_chance : flyback_chance
if(!SPT_PROB(prob_to_use, seconds_per_tick))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_WANTS_TO_TRANSITION_HIVE, TRUE)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/// Queen variant: strongly prefers staying in hive.
/datum/bt_node/ai_behavior/enter_exit_hive/queen
flyback_chance = 85
exit_chance = 5
/// Pollinates the hydro tray at BB_TARGET_HYDRO. Must be adjacent.
/datum/bt_node/ai_behavior/pollinate_hydro
time_between_perform = 5 SECONDS
/datum/bt_node/ai_behavior/pollinate_hydro/perform(seconds_per_tick, datum/ai_controller/controller)
var/obj/machinery/hydroponics/tray = controller.blackboard[BB_TARGET_HYDRO]
var/mob/living/basic/bee/bee_pawn = controller.pawn
if(QDELETED(tray) || !bee_pawn.Adjacent(tray))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
bee_pawn.pollinate(tray)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/bt_node/ai_behavior/pollinate_hydro/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_TARGET_HYDRO)
/// Picks a random turf near BB_SWARM_TARGET and stores it in BB_SWIRL_TURF for move_to_target.
/// Succeeds when a turf is found; fails otherwise so the parent sequence fails and retries.
/datum/bt_node/ai_behavior/swirl_around_target
var/swirl_chance = 60
/datum/bt_node/ai_behavior/swirl_around_target/perform(seconds_per_tick, datum/ai_controller/controller)
var/atom/target = controller.blackboard[BB_SWARM_TARGET]
var/mob/living/bee_pawn = controller.pawn
if(QDELETED(target))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(!SPT_PROB(swirl_chance, seconds_per_tick))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/list/possible_turfs = list()
for(var/turf/possible_turf in oview(2, target))
if(!possible_turf.is_blocked_turf(source_atom = bee_pawn))
possible_turfs += possible_turf
if(!length(possible_turfs))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_SWIRL_TURF, pick(possible_turfs))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED
/// Scatter command: runs away from BB_CURRENT_PET_TARGET then clears the command.
/datum/bt_node/subtree/pet_command/scatter
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_scatter.bt.json"
/// Swirl command: swarm around BB_SWARM_TARGET continuously (no auto-clear).
/datum/bt_node/subtree/pet_command/swirl
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_swirl.bt.json"
/// Beehive command: move to hive and enter/exit it.
/datum/bt_node/subtree/pet_command/beehive
behavior_tree_json = "code/datums/ai/basic_mobs/pet_commands/pet_command_beehive.bt.json"
@@ -0,0 +1,43 @@
{
"dm_type": "/datum/bt_node/subtree/find_hive",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"invert": false,
"key": "BB_TARGET_HOME"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_CURRENT_HOME",
"invert": true
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_HOME",
"required_dist": 1,
"finish_on_arrival": false
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/inhabit_hive"
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/clear_key",
"vars": {
"key": "BB_TARGET_HOME"
}
}
]
}
}
}
@@ -0,0 +1,27 @@
{
"dm_type": "/datum/bt_node/subtree/pollinate_target",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"key": "BB_TARGET_HYDRO"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_TARGET_HYDRO",
"required_dist": 1,
"finish_on_arrival": true
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/pollinate_hydro"
}
]
}
}
@@ -0,0 +1,58 @@
{
"dm_type": "/datum/ai_controller/basic_controller/queen_bee",
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_CHILD_ONE",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": false,
"repeat_secondary_delay": "BASIC_MOB_FIND_TARGET_RATE",
"finish_on_primary": false,
"children": [
{
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/find_hive"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/pollinate_target"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/transition_hive_status"
},
{
"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": "subtree",
"subtype": "/datum/bt_node/subtree/simple_hostile_combat"
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/enter_exit_hive/queen"
}
]
}
]
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"invert": true,
"key": "BB_TARGET_HOME"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/find_hive"
}
}
]
}
@@ -0,0 +1,45 @@
{
"dm_type": "/datum/bt_node/subtree/transition_hive_status",
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_equals",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"key": "BB_WANTS_TO_TRANSITION_HIVE",
"value": true
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_CURRENT_HOME",
"required_dist": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/ai_interact",
"vars": {
"target_key": "BB_CURRENT_HOME"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/clear_key",
"vars": {
"key": "BB_CURRENT_TARGET"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_WANTS_TO_TRANSITION_HIVE",
"value": false
}
}
]
}
}
@@ -0,0 +1,87 @@
{
"dm_type": "/datum/ai_controller/basic_controller/chick",
"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": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_FOUND_MOM",
"observer_abort": "BT_ABORT_SELF"
},
"child": {
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/is_at_distance",
"vars": {
"target_key": "BB_FOUND_MOM",
"min_distance": 2
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_FOUND_MOM",
"required_dist": 1,
"finish_on_arrival": true
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/random_chance",
"vars": {
"chance": 0.15
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/look_to_parent",
"vars": {
"parent_key": "BB_FOUND_MOM"
}
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
}
]
}
]
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_FOUND_MOM",
"invert": true
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/find_mom"
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
@@ -81,15 +81,18 @@
grow_as = null
/datum/ai_controller/basic_controller/chick
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/chicken/chick.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_FIND_MOM_TYPES = list(/mob/living/basic/chicken),
BB_BASIC_MOB_SPEAK_LINES = list(
BB_EMOTE_SAY = list("Cherp.", "Cherp?", "Chirrup.", "Cheep!"),
BB_EMOTE_HEAR = list("cheeps."),
BB_EMOTE_SEE = list("pecks at the ground.","flaps her tiny wings."),
BB_EMOTE_SOUND = list('sound/mobs/non-humanoids/chicken/chick_peep.ogg'),
BB_SPEAK_CHANCE = 4,
),
)
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/look_for_adult,
)
@@ -0,0 +1,5 @@
{
"dm_type": "/datum/ai_controller/basic_controller/chicken",
"type": "subtree",
"subtype": "/datum/bt_node/subtree/skittish_and_speak"
}
@@ -94,17 +94,18 @@ GLOBAL_VAR_INIT(chicken_count, 0)
)
/datum/ai_controller/basic_controller/chicken
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/chicken/chicken.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_BASIC_MOB_SPEAK_LINES = list(
BB_EMOTE_SAY = list("Cluck!", "BWAAAAARK BWAK BWAK BWAK!", "Bwaak bwak."),
BB_EMOTE_HEAR = list("clucks.", "croons."),
BB_EMOTE_SEE = list("pecks at the ground.", "flaps her wings viciously."),
BB_EMOTE_SOUND = list('sound/mobs/non-humanoids/chicken/clucks.ogg', 'sound/mobs/non-humanoids/chicken/bagawk.ogg'),
BB_SPEAK_CHANCE = 15,
),
)
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/flee_target,
/datum/ai_planning_subtree/random_speech/chicken,
)
@@ -90,7 +90,7 @@
* tipper - the mob who tipped us
*/
/mob/living/basic/cow/proc/after_cow_tipped(mob/living/carbon/tipper)
addtimer(CALLBACK(src, PROC_REF(set_tip_react_blackboard), tipper), rand(10 SECONDS, 20 SECONDS))
addtimer(CALLBACK(src, PROC_REF(set_tip_react_blackboard), tipper), rand(10 SECONDS, 20 SECONDS))
/*
* We've been waiting long enough, we're going to tell our AI to begin pleading.
@@ -0,0 +1,48 @@
{
"dm_type": "/datum/ai_controller/basic_controller/cow",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/tip_reaction"
},
{
"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": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/move_to_and_eat"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
}
]
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/find_food"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
]
}
@@ -1,15 +1,21 @@
/datum/ai_controller/basic_controller/cow
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/cow/cow.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_BASIC_MOB_TIP_REACTING = FALSE,
BB_BASIC_MOB_TIPPER = null,
BB_BASIC_MOB_SPEAK_LINES = list(
BB_EMOTE_SAY = list("moo?", "moo", "MOOOOOO"),
BB_EMOTE_HEAR = list("brays."),
BB_EMOTE_SEE = list("shakes her head."),
BB_EMOTE_SOUND = list('sound/mobs/non-humanoids/cow/cow.ogg'),
BB_SPEAK_CHANCE = 1,
),
)
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/tip_reaction,
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/random_speech/cow,
)
/// While we're tipped over, plead with nearby people instead of doing anything else.
/datum/bt_node/subtree/tip_reaction
behavior_tree_json = "code/datums/ai/basic_mobs/basic_subtrees/tip_reaction.bt.json"
@@ -0,0 +1,96 @@
{
"dm_type": "/datum/ai_controller/basic_controller/cow/moonicorn",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/tip_reaction"
},
{
"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": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_CURRENT_TARGET"
},
"child": {
"type": "parallel",
"failure_policy": "BT_PARALLEL_FAILURE_ANY",
"success_policy": "BT_PARALLEL_SUCCESS_CHILD_ONE",
"repeat_secondary": true,
"finish_on_primary": true,
"children": [
{
"type": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/basic_melee_attack",
"vars": {
"target_key": "BB_CURRENT_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/move_to_and_eat"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_combat_targets",
"vars": {
"target_key": "BB_CURRENT_TARGET",
"targeting_strategy": "BB_TARGETING_STRATEGY",
"hiding_location_key": "BB_CURRENT_TARGET_HIDING_LOCATION"
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/find_food"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
]
}
@@ -40,27 +40,24 @@
APPLY_FACTION_AND_ALLIES_FROM(src, tamer)
/datum/ai_controller/basic_controller/cow/moonicorn
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/cow/cow_moonicorn.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/allow_items/moonicorn,
BB_BASIC_MOB_TIP_REACTING = FALSE,
BB_BASIC_MOB_TIPPER = null,
)
planning_subtrees = list(
/datum/ai_planning_subtree/escape_captivity,
/datum/ai_planning_subtree/tip_reaction,
/datum/ai_planning_subtree/random_speech/cow,
//finds someone to kill
/datum/ai_planning_subtree/simple_find_target,
//...or something to eat, possibly. both types of target handled by melee attack subtree
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
BB_BASIC_MOB_SPEAK_LINES = list(
BB_EMOTE_SAY = list("moo?", "moo", "MOOOOOO"),
BB_EMOTE_HEAR = list("brays."),
BB_EMOTE_SEE = list("shakes her head."),
BB_EMOTE_SOUND = list('sound/mobs/non-humanoids/cow/cow.ogg'),
BB_SPEAK_CHANCE = 1,
),
)
///moonicorns will not attack people holding something that could tame them.
/datum/targeting_strategy/basic/allow_items/moonicorn
/datum/targeting_strategy/basic/allow_items/moonicorn/can_attack(mob/living/living_mob, atom/the_target, vision_range)
/datum/targeting_strategy/basic/allow_items/moonicorn/is_valid_target(mob/living/living_mob, atom/the_target, vision_range, datum/ai_controller/controller = null)
. = ..()
if(!.)
return FALSE
@@ -0,0 +1,28 @@
{
"dm_type": "/datum/ai_controller/basic_controller/cow/wisdom",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/tip_reaction"
},
{
"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": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
]
}
@@ -12,6 +12,10 @@
/mob/living/basic/cow/wisdom/Initialize(mapload, granted_wisdom, granted_experience = 500, milked_reagent = null)
src.milked_reagent = milked_reagent
. = ..()
ai_controller.set_blackboard_key(BB_BASIC_MOB_SPEAK_LINES, list(
BB_EMOTE_SAY = GLOB.wisdoms,
BB_SPEAK_CHANCE = 15,
))
src.granted_wisdom = granted_wisdom
if(!granted_wisdom)
src.granted_wisdom = pick(GLOB.skill_types)
@@ -28,15 +32,12 @@
return ..()
/datum/ai_controller/basic_controller/cow/wisdom
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/cow/cow_wisdom.bt.json"
//don't give a targeting strategy
blackboard = list(
BB_BASIC_MOB_TIP_REACTING = FALSE,
BB_BASIC_MOB_TIPPER = null,
)
planning_subtrees = list(
/datum/ai_planning_subtree/tip_reaction,
/datum/ai_planning_subtree/random_speech/cow/wisdom,
BB_BASIC_MOB_SPEAK_LINES = null,
)
///Give intense wisdom to the attacker if they're being friendly about it
@@ -0,0 +1,372 @@
{
"dm_type": "/datum/ai_controller/basic_controller/deer",
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_CURRENT_TARGET",
"observer_abort": "BT_ABORT_BOTH"
},
"child": {
"type": "subtree",
"subtype": "/datum/bt_node/subtree/run_away_from_target"
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"key": "BB_STATIONARY_CAUSE",
"observer_abort": "BT_ABORT_LOWER_PRIORITY"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/stop_and_stare"
}
},
{
"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": "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": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "selector",
"children": [
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"cooldown_key": "BB_DEER_NEXT_REST_TIMER"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_SELF",
"key": "BB_DEER_TREEHOME"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_DEER_TREEHOME",
"required_dist": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/deer_rest"
}
]
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_DEER_PLAYFRIEND"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_DEER_PLAYFRIEND",
"required_dist": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_cooldown",
"vars": {
"cooldown_key": "BB_DEER_PLAY_COOLDOWN",
"cooldown_duration": "10 SECONDS"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/deer_play"
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_LOWER_PRIORITY",
"key": "BB_DEER_TREE_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_DEER_TREE_TARGET",
"required_dist": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/hunt_target/deer_mark",
"vars": {
"target_key": "BB_DEER_TREE_TARGET",
"cooldown_key": "BB_DEER_MARK_COOLDOWN"
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_DEER_GRASS_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_DEER_GRASS_TARGET",
"required_dist": 0
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/hunt_target/deer_graze",
"vars": {
"target_key": "BB_DEER_GRASS_TARGET",
"cooldown_key": "BB_DEER_GRAZE_COOLDOWN"
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_DEER_WATER_TARGET"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/move_to_target",
"vars": {
"target_key": "BB_DEER_WATER_TARGET",
"required_dist": 1
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/hunt_target/deer_drink",
"vars": {
"target_key": "BB_DEER_WATER_TARGET",
"cooldown_key": "BB_DEER_DRINK_COOLDOWN"
}
}
]
}
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_walk"
}
]
}
]
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/random_speech_blackboard"
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"cooldown_key": "BB_DEER_PLAY_COOLDOWN"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/random_chance",
"vars": {
"chance": 0.03
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_DEER_WANTS_TO_PLAY",
"value": true
}
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_true",
"vars": {
"observer_abort": "BT_ABORT_BOTH",
"key": "BB_DEER_WANTS_TO_PLAY"
},
"child": {
"type": "sequence",
"children": [
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_DEER_PLAYFRIEND",
"target_source": "/datum/target_source/oview_single_type/deer_animals",
"targeting_strategy": "/datum/targeting_strategy/playable_deer"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/set_bb_key",
"vars": {
"target_key": "BB_DEER_WANTS_TO_PLAY",
"value": false
}
}
]
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"invert": true,
"key": "BB_DEER_TREEHOME"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"invert": true,
"key": "BB_DEER_TREE_TARGET"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"cooldown_key": "BB_DEER_MARK_COOLDOWN"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_DEER_TREE_TARGET",
"target_source": "/datum/target_source/oview_single_type/flora_tree",
"targeting_strategy": "/datum/targeting_strategy/anything"
}
}
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"invert": true,
"key": "BB_DEER_GRASS_TARGET"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"cooldown_key": "BB_DEER_GRAZE_COOLDOWN"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_DEER_GRASS_TARGET",
"target_source": "/datum/target_source/range_turfs/typecache_visible/deer_grass",
"targeting_strategy": "/datum/targeting_strategy/anything"
}
}
}
},
{
"type": "decorator",
"decorator": "/datum/bt_node/decorator/bb_key_set",
"vars": {
"invert": true,
"key": "BB_DEER_WATER_TARGET"
},
"child": {
"type": "decorator",
"decorator": "/datum/bt_node/decorator/key_off_cooldown",
"vars": {
"cooldown_key": "BB_DEER_DRINK_COOLDOWN"
},
"child": {
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_DEER_WATER_TARGET",
"target_source": "/datum/target_source/range_turfs/typecache_visible/deer_water",
"targeting_strategy": "/datum/targeting_strategy/anything"
}
}
}
}
]
},
{
"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"
}
},
{
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_STATIONARY_CAUSE",
"target_source": "/datum/target_source/oview_typed/from_bb_key/stationary_targets",
"targeting_strategy": "/datum/targeting_strategy/anything"
}
}
]
}
]
}
@@ -1,156 +1,95 @@
/datum/ai_controller/basic_controller/deer
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/deer/deer.bt.json"
blackboard = list(
BB_STATIONARY_MOVE_TO_TARGET = TRUE,
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_BASIC_MOB_SPEAK_LINES = list(
BB_EMOTE_SAY = list("Weeeeeeee?", "Weeee", "WEOOOOOOOOOO"),
BB_EMOTE_HEAR = list("brays."),
BB_EMOTE_SEE = list("shakes her head."),
BB_SPEAK_CHANCE = 1,
),
)
ai_traits = PASSIVE_AI_FLAGS
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/random_speech/deer,
/datum/ai_planning_subtree/stare_at_thing,
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/flee_target,
/datum/ai_planning_subtree/rest_at_home,
/datum/ai_planning_subtree/play_with_friends,
/datum/ai_planning_subtree/find_and_hunt_target/mark_territory,
/datum/ai_planning_subtree/find_and_hunt_target/graze,
/datum/ai_planning_subtree/find_and_hunt_target/drink_water,
)
///subtree to go around drinking water
/datum/ai_planning_subtree/find_and_hunt_target/drink_water
target_key = BB_DEER_WATER_TARGET
finding_behavior = /datum/ai_behavior/find_and_set/in_list/turf_types
hunting_behavior = /datum/ai_behavior/hunt_target/drink_water
hunt_targets = list(/turf/open/water)
hunt_range = 7
hunt_chance = 5
/datum/ai_behavior/hunt_target/drink_water
always_reset_target = TRUE
hunt_cooldown = 20 SECONDS
/datum/ai_behavior/hunt_target/drink_water/target_caught(mob/living/hunter, atom/hunted)
var/static/list/possible_emotes = list("drinks the water!", "dances in the water!", "splashes around happily!")
hunter.manual_emote(pick(possible_emotes))
///subtree to go around grazing
/datum/ai_planning_subtree/find_and_hunt_target/graze
target_key = BB_DEER_GRASS_TARGET
finding_behavior = /datum/ai_behavior/find_and_set/in_list/turf_types
hunting_behavior = /datum/ai_behavior/hunt_target/eat_grass
hunt_targets = list(/turf/open/floor/grass, /turf/open/misc/grass)
hunt_range = 7
hunt_chance = 45
/datum/ai_behavior/hunt_target/eat_grass
/// Munches grass with a happy little emote.
/datum/bt_node/ai_behavior/hunt_target/deer_graze
always_reset_target = TRUE
hunt_cooldown = 15 SECONDS
/datum/ai_behavior/hunt_target/eat_grass/target_caught(mob/living/hunter, atom/hunted)
/datum/bt_node/ai_behavior/hunt_target/deer_graze/target_caught(mob/living/hunter, atom/hunted)
var/static/list/possible_emotes = list("eats the grass!", "munches down the grass!", "chews on the grass!")
hunter.manual_emote(pick(possible_emotes))
///subtree to go around playing with other deers
/datum/ai_planning_subtree/play_with_friends
/// Splashes happily in the water.
/datum/bt_node/ai_behavior/hunt_target/deer_drink
always_reset_target = TRUE
hunt_cooldown = 20 SECONDS
/datum/bt_node/ai_behavior/hunt_target/deer_drink/target_caught(mob/living/hunter, atom/hunted)
var/static/list/possible_emotes = list("drinks the water!", "dances in the water!", "splashes around happily!")
hunter.manual_emote(pick(possible_emotes))
/datum/ai_planning_subtree/play_with_friends/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/static/list/emote_list = list("plays with", "dances with", "celebrates with")
var/static/list/friend_types = typecacheof(list(/mob/living/basic/deer))
if(controller.blackboard_key_exists(BB_DEER_PLAYFRIEND))
controller.queue_behavior(/datum/ai_behavior/emote_on_target, BB_DEER_PLAYFRIEND, emote_list)
if(SPT_PROB(3, seconds_per_tick))
controller.queue_behavior(/datum/ai_behavior/find_hunt_target/valid_deer, BB_DEER_PLAYFRIEND, friend_types)
return SUBTREE_RETURN_FINISH_PLANNING
/datum/ai_behavior/emote_on_target/deer_play
/datum/ai_behavior/emote_on_target/deer_play/run_emote(mob/living/living_pawn, atom/target, list/emote_list)
. = ..()
living_pawn.spin(spintime = 4, speed = 1)
/datum/ai_behavior/find_hunt_target/valid_deer/valid_dinner(mob/living/source, mob/living/deer, radius, datum/ai_controller/controller, seconds_per_tick)
if(deer.stat == DEAD)
return FALSE
if(!can_see(source, deer, radius))
return FALSE
deer.ai_controller?.set_blackboard_key(BB_DEER_PLAYFRIEND, source)
return can_see(source, deer, radius)
///subtree to mark trees as territories
/datum/ai_planning_subtree/find_and_hunt_target/mark_territory
target_key = BB_DEER_TREE_TARGET
finding_behavior = /datum/ai_behavior/find_hunt_target
hunting_behavior = /datum/ai_behavior/hunt_target/mark_territory
hunt_targets = list(/obj/structure/flora/tree)
hunt_range = 7
hunt_chance = 75
/datum/ai_behavior/hunt_target/mark_territory
/// Claims a tree as home territory.
/datum/bt_node/ai_behavior/hunt_target/deer_mark
always_reset_target = TRUE
hunt_cooldown = 15 SECONDS
/datum/ai_behavior/hunt_target/mark_territory/target_caught(mob/living/hunter, atom/hunted)
/datum/bt_node/ai_behavior/hunt_target/deer_mark/target_caught(mob/living/hunter, atom/hunted)
hunter.manual_emote("marks [hunted] with its hooves!")
hunter.ai_controller.set_blackboard_key(BB_DEER_TREEHOME, hunted)
hunter.ai_controller?.set_blackboard_key(BB_DEER_TREEHOME, hunted)
/datum/ai_planning_subtree/find_and_hunt_target/mark_territory/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard_key_exists(BB_DEER_TREEHOME)) //already found our home, abort!
return
return ..()
/datum/ai_planning_subtree/rest_at_home/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard[BB_DEER_RESTING] > world.time) //we're resting for now, nothing more to do
return SUBTREE_RETURN_FINISH_PLANNING
if(!controller.blackboard_key_exists(BB_DEER_TREEHOME) || controller.blackboard[BB_DEER_NEXT_REST_TIMER] > world.time)
return
controller.queue_behavior(/datum/ai_behavior/return_home, BB_DEER_TREEHOME)
/datum/ai_behavior/return_home
required_distance = 0
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
///minimum time till next rest
/// Rests by its home tree, then won't feel the need to rest again for a while.
/datum/bt_node/ai_behavior/deer_rest
/// How long we stand resting once we arrive (kept RUNNING via the cooldown).
var/rest_duration = 15 SECONDS
/// Minimum/maximum time before we want to rest again.
var/minimum_time = 2 MINUTES
///maximum time till next rest
var/maximum_time = 4 MINUTES
///ID for the rest timer
var/timerid
///Are we napping?
var/sleeping
/datum/ai_behavior/return_home/setup(datum/ai_controller/controller, target_key)
/datum/bt_node/ai_behavior/deer_rest/setup(datum/ai_controller/controller)
. = ..()
var/atom/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
var/list/possible_turfs = get_adjacent_open_turfs(target)
shuffle_inplace(possible_turfs)
for(var/turf/possible_turf as anything in possible_turfs)
if(!possible_turf.is_blocked_turf())
set_movement_target(controller, possible_turf)
return TRUE
return FALSE
timerid = addtimer(CALLBACK(src, PROC_REF(finish_action), controller, TRUE), rest_duration, TIMER_UNIQUE | TIMER_STOPPABLE)
/datum/ai_behavior/return_home/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
/datum/bt_node/ai_behavior/deer_rest/perform(seconds_per_tick, datum/ai_controller/controller)
if(sleeping)
return AI_BEHAVIOR_DELAY
var/mob/living/living_pawn = controller.pawn
var/static/list/possible_emotes = list("rests its legs...", "yawns and naps...", "curls up and rests...")
living_pawn.manual_emote(pick(possible_emotes))
controller.set_blackboard_key(BB_DEER_RESTING, world.time + 15 SECONDS)
sleeping = TRUE
return AI_BEHAVIOR_DELAY
/datum/bt_node/ai_behavior/deer_rest/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
deltimer(timerid)
timerid = null
sleeping = FALSE
controller.set_blackboard_key(BB_DEER_NEXT_REST_TIMER, world.time + rand(minimum_time, maximum_time))
/// Plays with another deer, spinning around them.
/datum/bt_node/ai_behavior/deer_play
/// Blackboard key holding the deer we're playing with.
var/friend_key = BB_DEER_PLAYFRIEND
/datum/bt_node/ai_behavior/deer_play/perform(seconds_per_tick, datum/ai_controller/controller)
var/atom/friend = controller.blackboard[friend_key]
if(QDELETED(friend))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/static/list/possible_emotes = list("plays with", "dances with", "celebrates with")
var/mob/living/living_pawn = controller.pawn
living_pawn.manual_emote("[pick(possible_emotes)] [friend]!")
living_pawn.spin(spintime = 4, speed = 1)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/bt_node/ai_behavior/deer_play/finish_action(datum/ai_controller/controller, succeeded)
. = ..()
controller.clear_blackboard_key(friend_key)
@@ -0,0 +1,26 @@
{
"dm_type": "/datum/ai_controller/basic_controller/goat",
"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": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/forage_and_retaliate"
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
@@ -1,23 +1,15 @@
/// Goats are normally content to sorta hang around and crunch any plant in sight, but they will go ape on someone who attacks them.
/datum/ai_controller/basic_controller/goat
behavior_tree_json = "code/modules/mob/living/basic/farm_animals/goat/goat.bt.json"
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_BASIC_MOB_SPEAK_LINES = list(
BB_EMOTE_SAY = list("EHEHEHEHEH", "eh?"),
BB_EMOTE_HEAR = list("brays."),
BB_EMOTE_SEE = list("shakes their head.", "stamps a foot.", "glares around."),
BB_SPEAK_CHANCE = 3,
),
)
ai_traits = DEFAULT_AI_FLAGS | STOP_MOVING_WHEN_PULLED
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/capricious_retaliate, // Capricious like Capra, get it?
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/random_speech/goat,
)
/datum/ai_planning_subtree/random_speech/goat
speech_chance = 3
emote_hear = list("brays.")
emote_see = list("shakes their head.", "stamps a foot.", "glares around.")
speak = list("EHEHEHEHEH", "eh?")
@@ -0,0 +1,11 @@
{
"dm_type": "/datum/bt_node/subtree/forage_for_goose_food",
"type": "leaf",
"behavior": "/datum/bt_node/ai_behavior/acquire_target/update_interaction_target",
"vars": {
"target_key": "BB_TARGET_FOOD",
"targeting_strategy": "/datum/targeting_strategy/goose_edible",
"target_source": "/datum/target_source/oview_items",
"vision_range": 1
}
}
@@ -0,0 +1,38 @@
{
"dm_type": "/datum/ai_controller/basic_controller/goose",
"type": "selector",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/escape_captivity"
},
{
"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": "subplan",
"success_policy": "BT_SUBPLAN_LOOP_ON_SUCCESS",
"failure_policy": "BT_SUBPLAN_LOOP_ON_FAILURE",
"children": [
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/forage_and_retaliate",
"bindings": {
"q8w3rtv1": "/datum/bt_node/subtree/forage_for_goose_food"
}
}
]
},
{
"type": "subtree",
"subtype": "/datum/bt_node/subtree/random_speech_loop"
}
]
}
]
}
@@ -30,8 +30,6 @@
gold_core_spawnable = HOSTILE_SPAWN
/// Do we actually destroy food we eat?
var/conserve_food = FALSE
/// Unfortunately, geese want to eat every item
var/static/list/item_typecache = typecacheof(/obj/item)
/mob/living/basic/goose/Initialize(mapload)
. = ..()
@@ -42,8 +40,6 @@
RegisterSignal(src, COMSIG_MOB_PRE_EAT, PROC_REF(on_tried_gobbling))
RegisterSignal(src, COMSIG_MOB_ATE, PROC_REF(on_gobbled))
ai_controller.set_blackboard_key(BB_BASIC_FOODS, item_typecache)
/mob/living/basic/goose/death(gibbed)
if (!gibbed && length(contents))
var/turf/drop_turf = drop_location()
@@ -157,8 +153,7 @@
remove_status_effect(/datum/status_effect/goose_choking) // We're going to cough it out
/mob/living/basic/goose/vomit/proc/stop_deadchat_plays()
var/initial_behaviour = initial(ai_controller?.idle_behavior)
ai_controller?.idle_behavior = SSidle_ai_behaviors.idle_behaviors[initial_behaviour]
ai_controller?.clear_blackboard_key(BB_DISABLE_IDLE)
/mob/living/basic/goose/vomit/deadchat_plays(mode = ANARCHY_MODE, cooldown = 12 SECONDS)
var/list/goose_inputs = list(
@@ -172,6 +167,6 @@
return
// Stop automated movement, retain the other behaviour so you can lead the horse to plastic and have it drink.
ai_controller?.idle_behavior = null
ai_controller?.set_blackboard_key(BB_DISABLE_IDLE, TRUE)
#undef GOOSE_SATIATED

Some files were not shown because too many files have changed in this diff Show More