mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-12 18:51:53 +00:00
## About The Pull Request Since #90505 added another entry to it the Regal Rat Riot ability, which turns maintenance creatures into versions loyal to the rat, has become sort of unmanageable (and to be honest it was a bit gross to start with). Instead of having a big if/else list (which was making the same range check multiple times...) that sets stats on a bunch of mobs, I delegated it to the mobs themselves and instead of changing some stats of the existing mobs we just turn them into a new mob which can be spawned or placed separately by mappers or admins if they want. Other stuff I changed: Riot (the ability which transforms mobs into minions) no longer spawns a mouse if it fails to find anything. Instead you have a chance to fish mice out of disposals bins while digging out trash and items. Domain is now a toggle which activates itself every 6 seconds rather than a button you manually click every 6 seconds. Riot makes a visual effect when used. Rare Pepe randomisation is done via a random spawner instead of the mob modifying a bunch of its own properties in Initialise. A bunch of mobs now automatically follow you after being tamed. I wrote this assuming I was going to add it to the rioted mobs but then didn't end up doing that because you might want them to immediately attack someone. My rule of thumb is that if I think you'd want the mob to attack someone the moment it is befriended I didn't add this and if you wouldn't I did. I changed some of the regal rat minion names, and some of them can now spawn from gold slime which couldn't before. ## Why It's Good For The Game This proc sucked and now it's nicer. As for the other changes; - A tamed mob immediately following you is nice feedback and saves you a click as it's likely to be your first action. Also removes some admin panel shitcode I added. - I changed Domain to a toggle because you generally want to use it on cooldown and someone suggested it on this PR and it sounded like a good idea. - I saw someone in Discord complaining that the previous flow of recruiting rats by hitting Riot with nothing around to summon one, waiting, hitting it again to convert one rat, and waiting again was tedious and annoying which I agree with. This method improves the quality of life by separating these two actions but _also_ as a side effect reduces a regal rat's ability to secretly stockpile 50 rats in a hidden maintenance room because most disposal bins are in slightly more visible areas, they'll actually need to go and make a mess somewhere someone can see them. ## Changelog 🆑 balance: Regal Rats can now grab mice out of disposal bins, and no longer spawn them with the Riot ability. balance: The Riot ability no longer needs to be used once for each slightly different kind of mob in your radius. balance: The Regal Rat Domain ability is now toggled on and off. balance: Several kinds of mob will immediately start following you once tamed. balance: Rats, hostile frogs, and evil snails can be created via gold slime reaction. /🆑
451 lines
17 KiB
Plaintext
451 lines
17 KiB
Plaintext
/// Used to set up a basic AI controller on a mob for admin ease of use
|
|
/datum/admin_ai_template
|
|
/// What do admins see when selecting this option?
|
|
var/name = ""
|
|
/// What AI controller do we apply?
|
|
var/controller_type
|
|
/// Should we be active even if the target has an active client?
|
|
var/override_client
|
|
/// Do we apply the hostile faction?
|
|
var/make_hostile
|
|
/// How likely is it that we move when not busy?
|
|
var/idle_chance
|
|
/// When do we stop targeting mobs?
|
|
var/minimum_stat
|
|
|
|
/// Actually perform the process
|
|
/datum/admin_ai_template/proc/apply(mob/living/target, client/user)
|
|
if (QDELETED(target) || !isliving(target))
|
|
to_chat(user, span_warning("Invalid target for AI controller."))
|
|
return
|
|
if (gather_information(target, user))
|
|
apply_controller(target, user)
|
|
|
|
/// Set up any stored variables before we actually apply the controller
|
|
/datum/admin_ai_template/proc/gather_information(mob/living/target, client/user)
|
|
override_client = tgui_alert(user, "Would you like this controller to be active even while the mob has a client controlling it?", "Override Client?", list("Yes", "No"))
|
|
if (isnull(override_client))
|
|
return FALSE
|
|
override_client = override_client == "Yes"
|
|
|
|
idle_chance = tgui_input_number(user, "How likely (% chance per second) should this mob be to move to another tile when it's not doing anything else?", "Walk Chance", max_value = 100, min_value = 0)
|
|
if (isnull(idle_chance))
|
|
return FALSE
|
|
|
|
if (isnull(make_hostile))
|
|
make_hostile = tgui_alert(user, "Do you want to override this mob's faction with the hostile faction?", "Override Faction?", list("Yes", "No"))
|
|
if (isnull(make_hostile))
|
|
return FALSE
|
|
make_hostile = make_hostile == "Yes"
|
|
|
|
if (isnull(minimum_stat))
|
|
var/static/list/stat_types = list(
|
|
"Conscious" = CONSCIOUS,
|
|
"Soft Crit" = SOFT_CRIT,
|
|
"Unconscious" = UNCONSCIOUS,
|
|
"Hard Crit" = HARD_CRIT,
|
|
"Dead (will probably get stuck punching a corpse forever)" = DEAD,
|
|
)
|
|
var/selected_stat = tgui_input_list(user, "Attack targets at the maximum health level of...?", "Persistence Level", stat_types, "Soft Crit")
|
|
if (isnull(selected_stat))
|
|
return FALSE
|
|
minimum_stat = stat_types[selected_stat]
|
|
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/proc/apply_controller(mob/living/target, client/user)
|
|
if (QDELETED(target))
|
|
to_chat(user, span_warning("Target stopped existing while you were answering prompts :("))
|
|
return
|
|
|
|
QDEL_NULL(target.ai_controller)
|
|
target.ai_controller = new controller_type(target)
|
|
|
|
if (make_hostile)
|
|
target.faction = list(FACTION_HOSTILE, REF(target))
|
|
|
|
var/datum/ai_controller/controller = target.ai_controller
|
|
controller.set_blackboard_key(BB_BASIC_MOB_IDLE_WALK_CHANCE, idle_chance)
|
|
controller.set_blackboard_key(BB_TARGET_MINIMUM_STAT, minimum_stat)
|
|
if (override_client)
|
|
controller.continue_processing_when_client = TRUE
|
|
controller.reset_ai_status()
|
|
|
|
/// Walks at a guy and attacks
|
|
/datum/admin_ai_template/hostile
|
|
name = "Hostile Melee"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles
|
|
|
|
/// Walks away from a guy and attacks
|
|
/datum/admin_ai_template/hostile_ranged
|
|
name = "Hostile Ranged"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_ranged
|
|
/// When should we retreat?
|
|
var/min_range
|
|
/// When should we advance?
|
|
var/max_range
|
|
/// What projectile do we fire?
|
|
var/projectile_type
|
|
/// What's the time between shots?
|
|
var/fire_cooldown
|
|
/// How many projectiles per shot?
|
|
var/burst_shots
|
|
/// What's the delay between projectiles in a burst?
|
|
var/burst_interval
|
|
/// What sound do we make?
|
|
var/projectile_sound
|
|
|
|
/datum/admin_ai_template/hostile_ranged/gather_information(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!.)
|
|
return FALSE
|
|
|
|
if (!setup_ranged_attacks(target, user))
|
|
return FALSE
|
|
|
|
return decide_min_max_range(target, user)
|
|
|
|
/// Give target a gun
|
|
/datum/admin_ai_template/hostile_ranged/proc/setup_ranged_attacks(mob/living/target, client/user)
|
|
if (target.GetComponent(/datum/component/ranged_attacks))
|
|
return TRUE
|
|
|
|
var/static/list/all_projectiles = subtypesof(/obj/projectile)
|
|
// These don't really browsable user-friendly names because there's a lot of duplicates, sorry admins
|
|
projectile_type = tgui_input_list(user, "What projectile should we fire?", "Select ammo", all_projectiles)
|
|
if (isnull(projectile_type))
|
|
return FALSE
|
|
|
|
fire_cooldown = tgui_input_number(user, "How many seconds between shots?", "Fire Rate", round_value = FALSE, max_value = 10, min_value = 0.2, default = 1)
|
|
if (isnull(fire_cooldown))
|
|
return FALSE
|
|
fire_cooldown = fire_cooldown SECONDS
|
|
|
|
burst_shots = tgui_input_number(user, "How many shots to fire per burst?", "Burst Count", max_value = 100, min_value = 1, default = 1)
|
|
if (isnull(burst_shots))
|
|
return FALSE
|
|
if (burst_shots > 1)
|
|
burst_interval = tgui_input_number(user, "How many seconds delay between burst shots?", "Burst Rate", round_value = FALSE, max_value = 2, min_value = 0.1, default = 0.2)
|
|
if (isnull(burst_interval))
|
|
return FALSE
|
|
burst_interval = burst_interval SECONDS
|
|
|
|
var/pick_sound = tgui_alert(user, "Select a firing sound effect?", "Select Sound", list("Yes", "No"))
|
|
if (isnull(pick_sound))
|
|
return FALSE
|
|
if (pick_sound == "Yes")
|
|
projectile_sound = input("", "Select fire sound",) as null|sound
|
|
|
|
return TRUE
|
|
|
|
/// Decide our movement details
|
|
/datum/admin_ai_template/hostile_ranged/proc/decide_min_max_range(mob/living/target, client/user)
|
|
min_range = tgui_input_number(user, "How far should this mob try to stay away from its target?", "Min Distance", max_value = 9, min_value = 0, default = 2)
|
|
if (isnull(min_range))
|
|
return FALSE
|
|
|
|
max_range = tgui_input_number(user, "How close should this mob try to stay to its target?", "Max Distance", max_value = 9, min_value = 1, default = 6)
|
|
if (isnull(max_range))
|
|
return FALSE
|
|
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/hostile_ranged/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
|
|
var/datum/ai_controller/controller = target.ai_controller
|
|
controller.set_blackboard_key(BB_RANGED_SKIRMISH_MIN_DISTANCE, min_range)
|
|
controller.set_blackboard_key(BB_RANGED_SKIRMISH_MAX_DISTANCE, max_range)
|
|
|
|
if (!projectile_type)
|
|
return
|
|
|
|
target.AddComponent(\
|
|
/datum/component/ranged_attacks,\
|
|
cooldown_time = fire_cooldown,\
|
|
projectile_type = projectile_type,\
|
|
projectile_sound = projectile_sound,\
|
|
burst_shots = burst_shots,\
|
|
burst_intervals = burst_interval,\
|
|
)
|
|
|
|
if (fire_cooldown <= 1 SECONDS)
|
|
target.AddComponent(/datum/component/ranged_mob_full_auto)
|
|
|
|
/// Walks at a guy while shooting and attacks
|
|
/datum/admin_ai_template/hostile_ranged/and_melee
|
|
name = "Hostile Ranged/Melee"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_skirmisher
|
|
|
|
/datum/admin_ai_template/hostile_ranged/and_melee/decide_min_max_range(mob/living/target, client/user)
|
|
return TRUE
|
|
|
|
/// Maintain distance from a guy and use an ability on cooldown
|
|
/datum/admin_ai_template/ability
|
|
name = "Hostile Ability User"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_ability
|
|
/// What is our ability?
|
|
var/ability_type
|
|
/// When should we retreat?
|
|
var/min_range
|
|
/// When should we advance?
|
|
var/max_range
|
|
|
|
/datum/admin_ai_template/ability/gather_information(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!.)
|
|
return FALSE
|
|
|
|
// We'll limit it to mob actions because they're mostly set up for random mobs already, and spells take some extra finagling for wizard clothing etc
|
|
var/static/list/all_mob_actions = sort_list(subtypesof(/datum/action/cooldown/mob_cooldown), GLOBAL_PROC_REF(cmp_typepaths_asc))
|
|
var/static/list/actions_by_name = list()
|
|
if (!length(actions_by_name))
|
|
for (var/datum/action/cooldown/mob_cooldown as anything in all_mob_actions)
|
|
actions_by_name["[initial(mob_cooldown.name)] ([mob_cooldown])"] = mob_cooldown
|
|
|
|
ability_type = tgui_input_list(user, "Which ability should it use?", "Select Ability", actions_by_name)
|
|
if (isnull(ability_type))
|
|
return FALSE
|
|
|
|
ability_type = actions_by_name[ability_type]
|
|
return decide_min_max_range(target, user)
|
|
|
|
/// Decide our movement details, some copy/paste here unfortunately
|
|
/datum/admin_ai_template/ability/proc/decide_min_max_range(mob/living/target, client/user)
|
|
min_range = tgui_input_number(user, "How far should this mob try to stay away from its target?", "Min Distance", max_value = 9, min_value = 0, default = 2)
|
|
if (isnull(min_range))
|
|
return FALSE
|
|
|
|
max_range = tgui_input_number(user, "How close should this mob try to stay to its target?", "Max Distance", max_value = 9, min_value = 1, default = 6)
|
|
if (isnull(max_range))
|
|
return FALSE
|
|
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/ability/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
|
|
var/datum/action/cooldown/ability = locate(ability_type) in target.actions
|
|
if (isnull(ability))
|
|
ability = new ability_type(target)
|
|
ability.Grant(target)
|
|
|
|
var/datum/ai_controller/controller = target.ai_controller
|
|
controller.set_blackboard_key(BB_TARGETED_ACTION, ability)
|
|
controller.set_blackboard_key(BB_RANGED_SKIRMISH_MIN_DISTANCE, min_range)
|
|
controller.set_blackboard_key(BB_RANGED_SKIRMISH_MAX_DISTANCE, max_range)
|
|
|
|
/// Walks at a guy and uses an ability on that guy
|
|
/datum/admin_ai_template/ability/melee
|
|
name = "Hostile Ability User (Melee Attacks)"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_ability_melee
|
|
|
|
/datum/admin_ai_template/ability/melee/decide_min_max_range(mob/living/target, client/user)
|
|
return TRUE
|
|
|
|
/// Stays away from a guy and uses an ability on that guy
|
|
/datum/admin_ai_template/hostile_ranged/ability
|
|
name = "Hostile Ability User (Ranged Attacks)"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_ability_ranged
|
|
/// What is our ability?
|
|
var/ability_type
|
|
|
|
/datum/admin_ai_template/hostile_ranged/ability/gather_information(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!.)
|
|
return FALSE
|
|
|
|
// Sadly gotta copy/paste this here too
|
|
var/static/list/all_mob_actions = sort_list(subtypesof(/datum/action/cooldown/mob_cooldown), GLOBAL_PROC_REF(cmp_typepaths_asc))
|
|
var/static/list/actions_by_name = list()
|
|
if (!length(actions_by_name))
|
|
for (var/datum/action/cooldown/mob_cooldown as anything in all_mob_actions)
|
|
actions_by_name["[initial(mob_cooldown.name)] ([mob_cooldown])"] = mob_cooldown
|
|
|
|
ability_type = tgui_input_list(user, "Which ability should it use?", "Select Ability", actions_by_name)
|
|
if (isnull(ability_type))
|
|
return FALSE
|
|
ability_type = actions_by_name[ability_type]
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/hostile_ranged/ability/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
|
|
var/datum/action/cooldown/ability = locate(ability_type) in target.actions
|
|
if (isnull(ability))
|
|
ability = new ability_type(target)
|
|
ability.Grant(target)
|
|
|
|
var/datum/ai_controller/controller = target.ai_controller
|
|
controller.set_blackboard_key(BB_TARGETED_ACTION, ability)
|
|
|
|
/// Chill unless you throw hands
|
|
/datum/admin_ai_template/retaliate
|
|
name = "Passive But Fights Back (Melee)"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_retaliate
|
|
make_hostile = FALSE
|
|
|
|
/datum/admin_ai_template/retaliate/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!HAS_TRAIT_FROM(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, /datum/element/ai_retaliate)) // Not really what this is for but it should work
|
|
target.AddElement(/datum/element/ai_retaliate)
|
|
|
|
/// Shoots anyone who attacks them
|
|
/datum/admin_ai_template/hostile_ranged/ability/retaliate
|
|
name = "Passive But Fights Back (Ranged Attacks)"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_ranged_retaliate
|
|
make_hostile = FALSE
|
|
|
|
/datum/admin_ai_template/hostile_ranged/ability/retaliate/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!HAS_TRAIT_FROM(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, /datum/element/ai_retaliate)) // Not really what this is for but it should work
|
|
target.AddElement(/datum/element/ai_retaliate)
|
|
|
|
/// Uses their signature move on anyone who attacks them
|
|
/datum/admin_ai_template/ability/retaliate
|
|
name = "Passive But Fights Back (Ability)"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_ability_retaliate
|
|
make_hostile = FALSE
|
|
|
|
/datum/admin_ai_template/ability/retaliate/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!HAS_TRAIT_FROM(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, /datum/element/ai_retaliate)) // Not really what this is for but it should work
|
|
target.AddElement(/datum/element/ai_retaliate)
|
|
|
|
/// Who knows what this guy will do, he's a loose cannon
|
|
/datum/admin_ai_template/grumpy
|
|
name = "Gets Mad Unpredictably"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_capricious
|
|
make_hostile = FALSE
|
|
/// Chance per second to get pissed off
|
|
var/flipout_chance
|
|
/// Chance per second to stop being pissed off
|
|
var/calm_down_chance
|
|
|
|
/datum/admin_ai_template/grumpy/gather_information(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!.)
|
|
return FALSE
|
|
|
|
flipout_chance = tgui_input_number(user, "What's the % chance per second we'll get mad for no reason?", "Tantrum Chance", round_value = FALSE, max_value = 100, min_value = 0, default = 0.5)
|
|
if (isnull(flipout_chance))
|
|
return FALSE
|
|
|
|
calm_down_chance = tgui_input_number(user, "What's the % chance per second we'll stop being mad?", "Zen Chance", round_value = FALSE, max_value = 100, min_value = 0, default = 10)
|
|
if (isnull(calm_down_chance))
|
|
return FALSE
|
|
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/grumpy/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
var/datum/ai_controller/controller = target.ai_controller
|
|
controller.set_blackboard_key(BB_RANDOM_AGGRO_CHANCE, flipout_chance)
|
|
controller.set_blackboard_key(BB_RANDOM_DEAGGRO_CHANCE, calm_down_chance)
|
|
|
|
if (!HAS_TRAIT_FROM(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, /datum/element/ai_retaliate)) // Not really what this is for but it should work
|
|
target.AddElement(/datum/element/ai_retaliate)
|
|
|
|
/// Coward
|
|
/datum/admin_ai_template/fearful
|
|
name = "Runs Away"
|
|
minimum_stat = CONSCIOUS
|
|
make_hostile = FALSE
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_fearful
|
|
|
|
/// Doesn't like violence
|
|
/datum/admin_ai_template/skittish
|
|
name = "Runs Away From Attackers"
|
|
minimum_stat = CONSCIOUS
|
|
make_hostile = FALSE
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_skittish
|
|
|
|
/datum/admin_ai_template/skittish/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!HAS_TRAIT_FROM(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, /datum/element/ai_retaliate)) // Not really what this is for but it should work
|
|
target.AddElement(/datum/element/ai_retaliate)
|
|
|
|
/// You gottit boss
|
|
/datum/admin_ai_template/goon
|
|
name = "Obeys Commands"
|
|
controller_type = /datum/ai_controller/basic_controller/simple/simple_goon
|
|
/// Who is really in charge here?
|
|
var/mob/living/da_boss
|
|
|
|
/datum/admin_ai_template/goon/gather_information(mob/living/target, client/user)
|
|
. = ..()
|
|
if (!.)
|
|
return FALSE
|
|
|
|
var/find_a_mob = tgui_alert(user, "Make this mob a minion of a mob in your tile? (If you don't do this you will need to use the befriend proc)", "Set Master?", list("Yes", "No"))
|
|
if (isnull(override_client))
|
|
return FALSE
|
|
find_a_mob = find_a_mob == "Yes"
|
|
if (!find_a_mob)
|
|
return TRUE
|
|
|
|
return grab_mob(target, user)
|
|
|
|
/// Find a mob to make the boss
|
|
/datum/admin_ai_template/goon/proc/grab_mob(mob/living/target, client/user)
|
|
var/list/mobs_in_my_tile = list()
|
|
for (var/mob/living/dude in (range(0, user.mob) - target))
|
|
mobs_in_my_tile[dude.real_name] = dude
|
|
|
|
if (length(mobs_in_my_tile))
|
|
var/picked = tgui_input_list(user, "Select new master.", "Set Master", mobs_in_my_tile + "Try Again", "Try Again")
|
|
if (isnull(picked))
|
|
return FALSE
|
|
if (picked == "Try Again")
|
|
return grab_mob(target, user)
|
|
|
|
da_boss = mobs_in_my_tile[picked]
|
|
return TRUE
|
|
|
|
var/find_a_mob = tgui_alert(user, "No applicable mobs found. Try again?", "Try Again?", list("Yes", "No"))
|
|
if (isnull(find_a_mob))
|
|
return FALSE
|
|
find_a_mob = find_a_mob == "Yes"
|
|
if (!find_a_mob)
|
|
return TRUE
|
|
return grab_mob(target, user)
|
|
|
|
/datum/admin_ai_template/goon/apply_controller(mob/living/target, client/user)
|
|
. = ..()
|
|
// There's not really much point making this customisable at the moment
|
|
var/static/list/pet_commands = list(
|
|
/datum/pet_command/idle,
|
|
/datum/pet_command/move,
|
|
/datum/pet_command/attack,
|
|
/datum/pet_command/follow/start_active,
|
|
/datum/pet_command/protect_owner,
|
|
)
|
|
target.AddComponent(/datum/component/obeys_commands, pet_commands)
|
|
|
|
if (isnull(da_boss))
|
|
return
|
|
|
|
target.befriend(da_boss)
|
|
|
|
/// Whatever it was doing before we fucked with it (mostly, can't do this with total confidence)
|
|
/datum/admin_ai_template/reset
|
|
name = "Reset"
|
|
|
|
/datum/admin_ai_template/reset/gather_information(mob/living/target, client/user)
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/reset/apply_controller(mob/living/target, client/user)
|
|
QDEL_NULL(target.ai_controller)
|
|
var/controller_type = initial(target.ai_controller)
|
|
target.ai_controller = new controller_type(src)
|
|
|
|
/// Like I'm doing nothing at all, nothing at all
|
|
/datum/admin_ai_template/clear
|
|
name = "None"
|
|
|
|
/datum/admin_ai_template/clear/gather_information(mob/living/target, client/user)
|
|
return TRUE
|
|
|
|
/datum/admin_ai_template/clear/apply_controller(mob/living/target, client/user)
|
|
QDEL_NULL(target.ai_controller)
|