diff --git a/code/__DEFINES/ai/ai_blackboard.dm b/code/__DEFINES/ai/ai_blackboard.dm index 161f9973b36..b5169934a85 100644 --- a/code/__DEFINES/ai/ai_blackboard.dm +++ b/code/__DEFINES/ai/ai_blackboard.dm @@ -77,6 +77,13 @@ #define BB_TARGET_WOUNDED_ONLY "BB_target_wounded_only" /// What typepath the holding object targeting strategy should look for #define BB_TARGET_HELD_ITEM "BB_target_held_item" +/// How likely is this mob to move when idle per tick? +#define BB_BASIC_MOB_IDLE_WALK_CHANCE "BB_basic_idle_walk_chance" + +/// Minimum range to keep target within +#define BB_RANGED_SKIRMISH_MIN_DISTANCE "BB_ranged_skirmish_min_distance" +/// Maximum range to keep target within +#define BB_RANGED_SKIRMISH_MAX_DISTANCE "BB_ranged_skirmish_max_distance" /// Blackboard key storing how long your targeting strategy has held a particular target #define BB_BASIC_MOB_HAS_TARGET_TIME "BB_basic_mob_has_target_time" diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index 7fa8f49cb79..04addf1786d 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -134,6 +134,7 @@ #define VV_HK_GIVE_DIRECT_CONTROL "give_direct_control" #define VV_HK_OFFER_GHOSTS "offer_ghosts" #define VV_HK_VIEW_PLANES "view_planes" +#define VV_HK_GIVE_AI "give_ai" // /mob/living #define VV_HK_GIVE_SPEECH_IMPEDIMENT "impede_speech" diff --git a/code/datums/ai/basic_mobs/admin_ai_templates.dm b/code/datums/ai/basic_mobs/admin_ai_templates.dm new file mode 100644 index 00000000000..a7f23dd67d8 --- /dev/null +++ b/code/datums/ai/basic_mobs/admin_ai_templates.dm @@ -0,0 +1,453 @@ +/// 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 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, + /datum/pet_command/protect_owner, + ) + var/datum/component/obeys_commands/command_component = target.AddComponent(/datum/component/obeys_commands, pet_commands) + + if (isnull(da_boss)) + return + + target.befriend(da_boss) + // Fuck it we're in admin territory we can do code crimes here + var/datum/pet_command/follow/follow_command = command_component.available_commands["Follow"] + follow_command?.set_command_active(target, 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) diff --git a/code/datums/ai/basic_mobs/base_basic_controller.dm b/code/datums/ai/basic_mobs/base_basic_controller.dm index d222cc1ef6d..7eb1279bb66 100644 --- a/code/datums/ai/basic_mobs/base_basic_controller.dm +++ b/code/datums/ai/basic_mobs/base_basic_controller.dm @@ -2,7 +2,7 @@ movement_delay = 0.4 SECONDS /datum/ai_controller/basic_controller/TryPossessPawn(atom/new_pawn) - if(!isbasicmob(new_pawn)) + if(!isliving(new_pawn)) return AI_CONTROLLER_INCOMPATIBLE var/mob/living/basic/basic_mob = new_pawn @@ -42,7 +42,7 @@ if(ai_traits & PAUSE_DURING_DO_AFTER && LAZYLEN(living_pawn.do_afters)) return AI_UNABLE_TO_RUN | AI_PREVENT_CANCEL_ACTIONS //dont erase targets post a do_after -/datum/ai_controller/basic_controller/proc/update_speed(mob/living/basic/basic_mob) +/datum/ai_controller/basic_controller/proc/update_speed(mob/living/basic_mob) SIGNAL_HANDLER movement_delay = basic_mob.cached_multiplicative_slowdown diff --git a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm index 549abbc75ec..8b8b0d0bf6f 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/maintain_distance.dm @@ -2,10 +2,6 @@ /datum/ai_planning_subtree/maintain_distance /// Blackboard key holding atom we want to stay away from var/target_key = BB_BASIC_MOB_CURRENT_TARGET - /// How close will we allow our target to get? - var/minimum_distance = 4 - /// How far away will we allow our target to get? - var/maximum_distance = 6 /// How far do we look for our target? var/view_distance = 10 /// the run away behavior we will use @@ -13,10 +9,19 @@ /datum/ai_planning_subtree/maintain_distance/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) . = ..() + + var/mob/living/living_pawn = controller.pawn + if(LAZYLEN(living_pawn.do_afters)) + return + var/atom/target = controller.blackboard[target_key] if (!isliving(target) || !can_see(controller.pawn, target, view_distance)) return // Don't run away from cucumbers, they're not snakes var/range = get_dist(controller.pawn, target) + + var/minimum_distance = controller.blackboard[BB_RANGED_SKIRMISH_MIN_DISTANCE] || 4 + var/maximum_distance = controller.blackboard[BB_RANGED_SKIRMISH_MAX_DISTANCE] || 6 + if (range < minimum_distance) controller.queue_behavior(run_away_behavior, target_key, minimum_distance) return diff --git a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm index efe5dc911cd..3004199a70f 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/ranged_skirmish.dm @@ -18,7 +18,7 @@ /// How often will we try to perform our ranged attack? /datum/ai_behavior/ranged_skirmish - action_cooldown = 1 SECONDS + action_cooldown = 0.5 SECONDS /datum/ai_behavior/ranged_skirmish/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key, max_range, min_range) . = ..() diff --git a/code/datums/ai/basic_mobs/generic_controllers.dm b/code/datums/ai/basic_mobs/generic_controllers.dm index dae1b944dd3..d38cfc70763 100644 --- a/code/datums/ai/basic_mobs/generic_controllers.dm +++ b/code/datums/ai/basic_mobs/generic_controllers.dm @@ -1,26 +1,124 @@ -/// The most basic AI tree which just finds a guy and then runs at them to click them -/datum/ai_controller/basic_controller/simple_hostile +/// Basetype with normal parameters +/datum/ai_controller/basic_controller/simple blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, ) ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk + +/// The most basic AI tree which just finds a guy and then runs at them to click them +/datum/ai_controller/basic_controller/simple/simple_hostile planning_subtrees = list( /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/basic_melee_attack_subtree, ) /// Find a target, walk at target, attack intervening obstacles -/datum/ai_controller/basic_controller/simple_hostile_obstacles - blackboard = list( - BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, - ) - - ai_movement = /datum/ai_movement/basic_avoidance - idle_behavior = /datum/idle_behavior/idle_random_walk +/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, ) + +/// Find a target, walk at target, attack intervening obstacles +/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, + ) + +/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, + ) + +/// 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, + ) + +/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/simple_retaliate + ai_traits = 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 +/datum/ai_controller/basic_controller/simple/simple_capricious + ai_traits = 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 = 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 = 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, + ) + +/// Does what it is told and protects da boss +/datum/ai_controller/basic_controller/simple/simple_goon + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends, + BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends, + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/pet_planning, + ) diff --git a/code/datums/ai/idle_behaviors/idle_random_walk.dm b/code/datums/ai/idle_behaviors/idle_random_walk.dm index c27caa50072..bedcb12e5c7 100644 --- a/code/datums/ai/idle_behaviors/idle_random_walk.dm +++ b/code/datums/ai/idle_behaviors/idle_random_walk.dm @@ -8,7 +8,8 @@ if(LAZYLEN(living_pawn.do_afters)) return FALSE - if(SPT_PROB(walk_chance, seconds_per_tick) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) + var/actual_chance = controller.blackboard[BB_BASIC_MOB_IDLE_WALK_CHANCE] || walk_chance + if(SPT_PROB(actual_chance, seconds_per_tick) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) var/move_dir = pick(GLOB.alldirs) var/turf/destination_turf = get_step(living_pawn, move_dir) if(!destination_turf?.can_cross_safely(living_pawn)) diff --git a/code/datums/elements/ai_retaliate.dm b/code/datums/elements/ai_retaliate.dm index 4cca4089633..03465d3c246 100644 --- a/code/datums/elements/ai_retaliate.dm +++ b/code/datums/elements/ai_retaliate.dm @@ -22,4 +22,6 @@ /datum/element/ai_retaliate/proc/on_attacked(mob/victim, atom/attacker) SIGNAL_HANDLER + if (victim == attacker) + return victim.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 891f8a46167..7ac5fa72372 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -664,3 +664,18 @@ ADMIN_VERB(create_mob_worm, R_FUN, "Create Mob Worm", "Attach a linked list of m QDEL_NULL(segment.ai_controller) segment.AddComponent(/datum/component/mob_chain, front = previous) previous = segment + +ADMIN_VERB(give_ai_controller, R_FUN, "Give AI Controller", ADMIN_VERB_NO_DESCRIPTION, ADMIN_CATEGORY_HIDDEN, mob/living/my_guy) + var/static/list/controllers = subtypesof(/datum/admin_ai_template) + var/static/list/controllers_by_name = list() + if (!length(controllers_by_name)) + for (var/datum/admin_ai_template/template as anything in controllers) + controllers_by_name["[initial(template.name)]"] = template + + var/chosen = tgui_input_list(user, "Which template should we apply?", "Select Template", controllers_by_name) + if (isnull(chosen)) + return + + var/chosen_type = controllers_by_name[chosen] + var/datum/admin_ai_template/using_template = new chosen_type + using_template.apply(my_guy, user) diff --git a/code/modules/mob/living/basic/heretic/fire_shark.dm b/code/modules/mob/living/basic/heretic/fire_shark.dm index e7375f4b33f..41c48972673 100644 --- a/code/modules/mob/living/basic/heretic/fire_shark.dm +++ b/code/modules/mob/living/basic/heretic/fire_shark.dm @@ -21,7 +21,7 @@ mob_size = MOB_SIZE_TINY speak_emote = list("screams") basic_mob_flags = DEL_ON_DEATH - ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles initial_language_holder = /datum/language_holder/carp/hear_common /mob/living/basic/heretic_summon/fire_shark/Initialize(mapload) diff --git a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm index 8a900b0308a..ccccc795224 100644 --- a/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm +++ b/code/modules/mob/living/basic/icemoon/ice_demon/ice_demon_ai.dm @@ -1,6 +1,7 @@ /datum/ai_controller/basic_controller/ice_demon blackboard = list( BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_RANGED_SKIRMISH_MAX_DISTANCE = 7, BB_LIST_SCARY_ITEMS = list( /obj/item/weldingtool, /obj/item/flashlight/flare, @@ -13,16 +14,12 @@ /datum/ai_planning_subtree/simple_find_target, /datum/ai_planning_subtree/flee_target/ice_demon, /datum/ai_planning_subtree/ranged_skirmish/ice_demon, - /datum/ai_planning_subtree/maintain_distance/cover_minimum_distance/ice_demon, + /datum/ai_planning_subtree/maintain_distance/cover_minimum_distance, /datum/ai_planning_subtree/teleport_away_from_target, /datum/ai_planning_subtree/find_and_hunt_target/teleport_destination, /datum/ai_planning_subtree/targeted_mob_ability/summon_afterimages, ) - -/datum/ai_planning_subtree/maintain_distance/cover_minimum_distance/ice_demon - maximum_distance = 7 - /datum/ai_planning_subtree/teleport_away_from_target ability_key = BB_DEMON_TELEPORT_ABILITY diff --git a/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm index 286e7539624..957eadf5a3b 100644 --- a/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm +++ b/code/modules/mob/living/basic/lavaland/hivelord/hivelord.dm @@ -98,7 +98,7 @@ attack_vis_effect = ATTACK_EFFECT_BITE obj_damage = 0 density = FALSE - ai_controller = /datum/ai_controller/basic_controller/simple_hostile + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile /mob/living/basic/hivelord_brood/Initialize(mapload) . = ..() diff --git a/code/modules/mob/living/basic/pets/cat/feral.dm b/code/modules/mob/living/basic/pets/cat/feral.dm index e8a7d7b754f..1a5bf1ca9b1 100644 --- a/code/modules/mob/living/basic/pets/cat/feral.dm +++ b/code/modules/mob/living/basic/pets/cat/feral.dm @@ -5,5 +5,5 @@ maxHealth = 30 melee_damage_lower = 7 melee_damage_upper = 15 - ai_controller = /datum/ai_controller/basic_controller/simple_hostile + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile faction = list(FACTION_CAT, ROLE_SYNDICATE) diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm index 474c2cf77d0..165c5b85f35 100644 --- a/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm +++ b/code/modules/mob/living/basic/space_fauna/netherworld/blankbody.dm @@ -26,7 +26,7 @@ lighting_cutoff_green = 15 lighting_cutoff_blue = 40 - ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles /mob/living/basic/blankbody/Initialize(mapload) . = ..() diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm index 292f85e4764..d9973142c12 100644 --- a/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm +++ b/code/modules/mob/living/basic/space_fauna/netherworld/creature.dm @@ -27,7 +27,7 @@ lighting_cutoff_green = 25 lighting_cutoff_blue = 15 - ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles var/health_scaling = TRUE /mob/living/basic/creature/Initialize(mapload) diff --git a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm index 3f7adc22722..76e3dcc2d62 100644 --- a/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm +++ b/code/modules/mob/living/basic/space_fauna/netherworld/migo.dm @@ -28,7 +28,7 @@ lighting_cutoff_green = 15 lighting_cutoff_blue = 50 - ai_controller = /datum/ai_controller/basic_controller/simple_hostile_obstacles + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile_obstacles var/static/list/migo_sounds /// Odds migo will dodge var/dodge_prob = 10 diff --git a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm index f2e33eaacd9..0d97a10dffe 100644 --- a/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm +++ b/code/modules/mob/living/basic/space_fauna/paper_wizard/paper_wizard.dm @@ -105,7 +105,7 @@ faction = list(FACTION_STICKMAN) melee_damage_lower = 1 melee_damage_upper = 5 - ai_controller = /datum/ai_controller/basic_controller/simple_hostile + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile /mob/living/basic/paper_wizard/copy/Initialize(mapload) . = ..() diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index e4e10cd9452..95987946354 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1338,6 +1338,7 @@ . = ..() VV_DROPDOWN_OPTION("", "---------") VV_DROPDOWN_OPTION(VV_HK_GIB, "Gib") + VV_DROPDOWN_OPTION(VV_HK_GIVE_AI, "Give AI Controller") VV_DROPDOWN_OPTION(VV_HK_REMOVE_SPELL, "Remove Spell") VV_DROPDOWN_OPTION(VV_HK_GIVE_SPELL, "Give Spell") VV_DROPDOWN_OPTION(VV_HK_REMOVE_SPELL, "Remove Spell") @@ -1380,6 +1381,9 @@ return usr.client.cmd_admin_godmode(src) + if(href_list[VV_HK_GIVE_AI]) + return SSadmin_verbs.dynamic_invoke_verb(usr, /datum/admin_verb/give_ai_controller, src) + if(href_list[VV_HK_GIVE_MOB_ACTION]) return SSadmin_verbs.dynamic_invoke_verb(usr, /datum/admin_verb/give_mob_action, src) diff --git a/tgstation.dme b/tgstation.dme index a4bcec66222..37ef5543013 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -907,6 +907,7 @@ #include "code\datums\ai\bane\bane_behaviors.dm" #include "code\datums\ai\bane\bane_controller.dm" #include "code\datums\ai\bane\bane_subtrees.dm" +#include "code\datums\ai\basic_mobs\admin_ai_templates.dm" #include "code\datums\ai\basic_mobs\base_basic_controller.dm" #include "code\datums\ai\basic_mobs\generic_controllers.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm"