From 0ae73635e2417202932f8f1c63942372b8e632bf Mon Sep 17 00:00:00 2001
From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Date: Fri, 22 Aug 2025 11:33:14 -0400
Subject: [PATCH] Adds some generic AI controllers (#30061)
* Generic controllers
* Linters
---
code/__DEFINES/ai/blackboard_defines.dm | 5 +
.../basic_subtrees/capricious_retaliate.dm | 65 +++++++++
.../ai/basic_mobs/generic_controllers.dm | 134 ++++++++++++++++++
.../hostile_controllers.dm | 56 --------
.../mob/living/basic/hostile/skeleton_mob.dm | 2 +-
.../living/basic/nether_mobs/nether_mobs.dm | 4 +-
paradise.dme | 3 +-
7 files changed, 209 insertions(+), 60 deletions(-)
create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
create mode 100644 code/datums/ai/basic_mobs/generic_controllers.dm
delete mode 100644 code/datums/ai/basic_mobs/generic_controllers/hostile_controllers.dm
diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm
index 27c543720d5..9ed9dc200e0 100644
--- a/code/__DEFINES/ai/blackboard_defines.dm
+++ b/code/__DEFINES/ai/blackboard_defines.dm
@@ -59,6 +59,11 @@
/// Key that holds the cooldown for our hunting subtree
#define BB_HUNTING_COOLDOWN(type) "BB_HUNTING_COOLDOWN_[type]"
+/// Chance to randomly acquire a new target
+#define BB_RANDOM_AGGRO_CHANCE "BB_random_aggro_chance"
+/// Chance to randomly drop all of our targets
+#define BB_RANDOM_DEAGGRO_CHANCE "BB_random_deaggro_chance"
+
// Food and eating
/// list of foods this mob likes
diff --git a/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
new file mode 100644
index 00000000000..79e1dd05747
--- /dev/null
+++ b/code/datums/ai/basic_mobs/basic_subtrees/capricious_retaliate.dm
@@ -0,0 +1,65 @@
+/// Add or remove people to our retaliation shitlist just on an arbitrary whim
+/datum/ai_planning_subtree/capricious_retaliate
+ /// Blackboard key which tells us how to select valid targets
+ var/targeting_strategy_key = BB_TARGETING_STRATEGY
+ /// Whether we should skip checking faction for our decision
+ var/ignore_faction = TRUE
+
+/datum/ai_planning_subtree/capricious_retaliate/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
+ . = ..()
+ controller.queue_behavior(/datum/ai_behavior/capricious_retaliate, targeting_strategy_key, ignore_faction)
+
+/// Add or remove people to our retaliation shitlist just on an arbitrary whim
+/datum/ai_behavior/capricious_retaliate
+ action_cooldown = 1 SECONDS
+
+/datum/ai_behavior/capricious_retaliate/perform(seconds_per_tick, datum/ai_controller/controller, targeting_strategy_key, ignore_faction)
+ var/atom/pawn = controller.pawn
+ if(controller.blackboard_key_exists(BB_BASIC_MOB_RETALIATE_LIST))
+ var/deaggro_chance = controller.blackboard[BB_RANDOM_DEAGGRO_CHANCE] || 10
+ if(!SPT_PROB(deaggro_chance, seconds_per_tick))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+ pawn.visible_message("[pawn] calms down.") // We can blackboard key this if anyone else actually wants to customise it
+ controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST)
+ controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
+ controller.cancel_actions() // Otherwise they will try and get one last kick in
+ return AI_BEHAVIOR_DELAY
+
+ var/aggro_chance = controller.blackboard[BB_RANDOM_AGGRO_CHANCE] || 0.5
+ if(!SPT_PROB(aggro_chance, seconds_per_tick))
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/aggro_range = controller.blackboard[BB_AGGRO_RANGE] || 10
+ var/list/potential_targets = hearers(aggro_range, get_turf(pawn)) - pawn
+ if(!length(potential_targets))
+ failed_targeting(pawn)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ var/datum/targeting_strategy/target_helper = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key])
+
+ var/mob/living/final_target = null
+ if(ignore_faction)
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE)
+ while(isnull(final_target) && length(potential_targets))
+ var/mob/living/test_target = pick_n_take(potential_targets)
+ if(target_helper.can_attack(pawn, test_target, vision_range = aggro_range))
+ final_target = test_target
+
+ if(isnull(final_target))
+ failed_targeting(pawn)
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
+
+ controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, final_target)
+ pawn.visible_message("[pawn] glares grumpily at [final_target]!")
+ return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
+
+/// Called if we try but fail to target something
+/datum/ai_behavior/capricious_retaliate/proc/failed_targeting(atom/pawn)
+ pawn.visible_message("[pawn] grumbles.") // We're pissed off but with no outlet to vent our frustration upon
+
+/datum/ai_behavior/capricious_retaliate/finish_action(datum/ai_controller/controller, succeeded, ignore_faction)
+ . = ..()
+ if(succeeded || !ignore_faction)
+ return
+ var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE
+ controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction)
diff --git a/code/datums/ai/basic_mobs/generic_controllers.dm b/code/datums/ai/basic_mobs/generic_controllers.dm
new file mode 100644
index 00000000000..d7766e1934a
--- /dev/null
+++ b/code/datums/ai/basic_mobs/generic_controllers.dm
@@ -0,0 +1,134 @@
+/// Find a target, walk at target, attack intervening obstacles
+/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Prowls around when not attacking people
+/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/prowler
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ )
+
+/// Will ventcrawl around until it finds a target to attack.
+/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/ventcrawler
+ blackboard = list (
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
+ BB_VENT_SEARCH_RANGE = 10
+ )
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/ventcrawl_find_target,
+ /datum/ai_planning_subtree/ventcrawl,
+ )
+
+/// Find a target, keep distance
+/datum/ai_controller/basic_controller/simple/simple_ranged
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ )
+
+/// Retaliate at range, keep distance
+/datum/ai_controller/basic_controller/simple/simple_ranged_retaliate
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ )
+
+/// Find a target, walk towards it AND shoot it
+/datum/ai_controller/basic_controller/simple/simple_skirmisher
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Wanders around until it finds something it can walk towards and shoot
+/datum/ai_controller/basic_controller/simple/simple_skirmisher/prowler
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/find_and_hunt_target/prowl,
+ /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
+ )
+
+/// Use an ability on target on cooldown
+/datum/ai_controller/basic_controller/simple/simple_ability
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ )
+
+/// Use an ability to retaliate on cooldown
+/datum/ai_controller/basic_controller/simple/simple_ability_retaliate
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ )
+
+/// Use an ability on target on cooldown, then try to punch them
+/datum/ai_controller/basic_controller/simple/simple_ability_melee
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ /datum/ai_planning_subtree/attack_obstacle_in_path,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Use an ability on target on cooldown, then try to shoot them
+/datum/ai_controller/basic_controller/simple/simple_ability_ranged
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_target,
+ /datum/ai_planning_subtree/maintain_distance,
+ /datum/ai_planning_subtree/targeted_mob_ability,
+ /datum/ai_planning_subtree/ranged_skirmish,
+ )
+
+/// Fight back if attacked
+/datum/ai_controller/basic_controller/simple/retaliate
+ ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Get pissed at random people for no reason. Most often seen with goats.
+/datum/ai_controller/basic_controller/simple/simple_capricious
+ ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/capricious_retaliate,
+ /datum/ai_planning_subtree/target_retaliate,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ )
+
+/// Runs away from anyone it sees
+/datum/ai_controller/basic_controller/simple/simple_fearful
+ ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/simple_find_nearest_target_to_flee,
+ /datum/ai_planning_subtree/flee_target,
+ )
+
+/// Runs away when attacked
+/datum/ai_controller/basic_controller/simple/simple_skittish
+ ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
+ /datum/ai_planning_subtree/flee_target,
+ )
diff --git a/code/datums/ai/basic_mobs/generic_controllers/hostile_controllers.dm b/code/datums/ai/basic_mobs/generic_controllers/hostile_controllers.dm
deleted file mode 100644
index 8d285a38c08..00000000000
--- a/code/datums/ai/basic_mobs/generic_controllers/hostile_controllers.dm
+++ /dev/null
@@ -1,56 +0,0 @@
-/// Find a target, walk at target, attack intervening obstacles
-/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
-
-/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/demonic_incursion
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- /datum/ai_planning_subtree/find_and_hunt_target/prowl,
- /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
- )
-
-/datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/ventcrawler
- blackboard = list (
- BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
- BB_VENT_SEARCH_RANGE = 10
- )
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- /datum/ai_planning_subtree/ventcrawl_find_target,
- /datum/ai_planning_subtree/ventcrawl,
- )
-
-/// Find a target, walk towards it AND shoot it
-/datum/ai_controller/basic_controller/simple/simple_skirmisher
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/ranged_skirmish,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
-
-/datum/ai_controller/basic_controller/simple/simple_skirmisher/demon_incursion
- planning_subtrees = list(
- /datum/ai_planning_subtree/simple_find_target,
- /datum/ai_planning_subtree/ranged_skirmish,
- /datum/ai_planning_subtree/attack_obstacle_in_path,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- /datum/ai_planning_subtree/find_and_hunt_target/prowl,
- /datum/ai_planning_subtree/attack_obstacle_in_path/prowl,
- )
-
-/// Fight back if attacked
-/datum/ai_controller/basic_controller/simple/retaliate
- ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED
- planning_subtrees = list(
- /datum/ai_planning_subtree/target_retaliate,
- /datum/ai_planning_subtree/basic_melee_attack_subtree,
- )
diff --git a/code/modules/mob/living/basic/hostile/skeleton_mob.dm b/code/modules/mob/living/basic/hostile/skeleton_mob.dm
index aaebfe7cc43..17aa73081ef 100644
--- a/code/modules/mob/living/basic/hostile/skeleton_mob.dm
+++ b/code/modules/mob/living/basic/hostile/skeleton_mob.dm
@@ -32,7 +32,7 @@
step_type = FOOTSTEP_MOB_SHOE
gold_core_spawnable = HOSTILE_SPAWN
loot = list(/obj/effect/decal/remains/human)
- ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/demonic_incursion
+ ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/prowler
/mob/living/basic/skeleton/Initialize(mapload)
. = ..()
diff --git a/code/modules/mob/living/basic/nether_mobs/nether_mobs.dm b/code/modules/mob/living/basic/nether_mobs/nether_mobs.dm
index 3c5b169bcdc..211fd482c92 100644
--- a/code/modules/mob/living/basic/nether_mobs/nether_mobs.dm
+++ b/code/modules/mob/living/basic/nether_mobs/nether_mobs.dm
@@ -12,7 +12,7 @@
melee_attack_cooldown_min = 1.5 SECONDS
melee_attack_cooldown_max = 2.5 SECONDS
environment_smash = ENVIRONMENT_SMASH_STRUCTURES
- ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/demonic_incursion
+ ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles/prowler
attack_verb_simple = "chomp"
attack_verb_continuous = "chomps"
attack_sound = 'sound/weapons/bladeslice.ogg'
@@ -32,7 +32,7 @@
if(prob(grappler_chance))
AddComponent(/datum/component/ranged_attacks, projectile_type = /obj/item/projectile/energy/demonic_grappler, projectile_sound = 'sound/weapons/wave.ogg')
name = "grappling " + name
- ai_controller = new /datum/ai_controller/basic_controller/simple/simple_skirmisher/demon_incursion(src)
+ ai_controller = new /datum/ai_controller/basic_controller/simple/simple_skirmisher/prowler(src)
update_appearance(UPDATE_NAME)
color = "#5494DA"
AddElement(/datum/element/ai_retaliate)
diff --git a/paradise.dme b/paradise.dme
index 378b356da8a..80e0179cd27 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -458,6 +458,7 @@
#include "code\datums\ai\ai_controller.dm"
#include "code\datums\ai\ai_planning_subtree.dm"
#include "code\datums\ai\basic_mobs\basic_controller.dm"
+#include "code\datums\ai\basic_mobs\generic_controllers.dm"
#include "code\datums\ai\basic_mobs\simple_controller.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm"
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\befriend_target.dm"
@@ -473,6 +474,7 @@
#include "code\datums\ai\basic_mobs\basic_ai_behaviors\travel_towards.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\attack_adjacent_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\attack_obstacle_in_path.dm"
+#include "code\datums\ai\basic_mobs\basic_subtrees\capricious_retaliate.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\flee_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\maintain_distance.dm"
@@ -491,7 +493,6 @@
#include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\ventcrawl_find_target.dm"
#include "code\datums\ai\basic_mobs\basic_subtrees\ventcrawl_subtree.dm"
-#include "code\datums\ai\basic_mobs\generic_controllers\hostile_controllers.dm"
#include "code\datums\ai\basic_mobs\hunting_behavior\find_and_hunt_target.dm"
#include "code\datums\ai\basic_mobs\hunting_behavior\find_hunt_target.dm"
#include "code\datums\ai\basic_mobs\hunting_behavior\hunt_target.dm"