diff --git a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm
index e2e2bb3a1d..fe088d8b20 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm
@@ -56,6 +56,9 @@
//from /mob/living/carbon/human/equip_to_slot()
#define COMSIG_HUMAN_EQUIPPED_ITEM "human_equipped_item"
+//from /mob/living/carbon/human/u_equip()
+#define COMSIG_HUMAN_UNEQUIPPED_ITEM "human_unequipped_item"
+
/// From /mob/proc/equip_to_slot_if_possible()
#define COMSIG_HUMAN_ATTEMPTING_EQUIP "human_attempting_equip"
#define COMPONENT_HUMAN_CANCEL_ATTEMPT_EQUIP (1<<0)
diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
index fa50103e08..8df1ede132 100644
--- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
+++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm
@@ -186,6 +186,8 @@
/// Cancels all running cloaking effects on target
#define COMSIG_MOB_EFFECT_CLOAK_CANCEL "mob_effect_cloak_cancel"
+#define COMSIG_MOB_DROP_ITEM "mob_drop_item"
+
#define COMSIG_MOB_END_TUTORIAL "mob_end_tutorial"
#define COMSIG_MOB_NESTED "mob_nested"
diff --git a/code/__DEFINES/equipment.dm b/code/__DEFINES/equipment.dm
index 5d5b81bf8c..79c72950d6 100644
--- a/code/__DEFINES/equipment.dm
+++ b/code/__DEFINES/equipment.dm
@@ -566,3 +566,15 @@ GLOBAL_LIST_INIT(uniform_categories, list(
#define PHONE_ON_BASE_UNIT_ICON_STATE "[initial(icon_state)]"
#define PHONE_OFF_BASE_UNIT_ICON_STATE "[initial(icon_state)]_ear"
#define PHONE_RINGING_ICON_STATE "[initial(icon_state)]_ring"
+
+// Human AI flags
+/// This item is classified as a healing item for the sake of human AI
+#define HEALING_ITEM (1<<0)
+/// This item is classified as ammunition for the sake of human AI
+#define AMMUNITION_ITEM (1<<1)
+/// This item is classified as a grenade for the sake of human AI
+#define GRENADE_ITEM (1<<2)
+/// This item is classified as a tool for the sake of human AI
+#define TOOL_ITEM (1<<3)
+/// This item is classified as a melee weapon for the sake of human AI
+#define MELEE_WEAPON_ITEM (1<<4)
diff --git a/code/__DEFINES/human_ai.dm b/code/__DEFINES/human_ai.dm
new file mode 100644
index 0000000000..408567d46c
--- /dev/null
+++ b/code/__DEFINES/human_ai.dm
@@ -0,0 +1,26 @@
+#define HUMAN_AI_HEALTHITEMS "health"
+#define HUMAN_AI_AMMUNITION "ammo"
+#define HUMAN_AI_GRENADES "grenades"
+#define HUMAN_AI_TOOLS "tools"
+
+#define AI_ACTION_APPROACH /datum/ongoing_action/approach_target
+#define AI_ACTION_APPROACH_CAREFUL /datum/ongoing_action/approach_target/carefully
+#define AI_ACTION_RETREAT /datum/ongoing_action/retreat_from_target
+#define AI_ACTION_PICKUP /datum/ongoing_action/item_pickup
+#define AI_ACTION_PICKUP_GUN /datum/ongoing_action/item_pickup/pickup_primary
+#define AI_ACTION_COVER /datum/ongoing_action/take_cover
+#define AI_ACTION_COVER_I /datum/ongoing_action/take_inside_cover
+#define AI_ACTION_THROWBACK /datum/ongoing_action/throw_back_nade
+#define AI_ACTION_NADE /datum/ongoing_action/throw_grenade
+#define AI_ACTION_MELEE_ATOM /datum/ongoing_action/melee_atom
+
+/// Action is completed, delete this and move onto the next ongoing action
+#define ONGOING_ACTION_COMPLETED "completed"
+/// Action isn't finished, move onto the next ongoing action
+#define ONGOING_ACTION_UNFINISHED "unfinished"
+/// Action isn't finished, block any further actions from the AI this tick
+#define ONGOING_ACTION_UNFINISHED_BLOCK "unfinished_block"
+
+#define ADD_ONGOING_ACTION(brain, path, arguments...) brain:_add_ongoing_action(path, ##arguments)
+
+#define HUMAN_AI_MAX_PATHFINDING_RANGE 45
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index 989c39cd7d..49c7a71a77 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -163,6 +163,7 @@
#define SS_PRIORITY_SOUND 250
#define SS_PRIORITY_TICKER 200
#define SS_PRIORITY_XENO_AI 185
+#define SS_PRIORITY_HUMAN_AI 182
#define SS_PRIORITY_NIGHTMARE 180
#define SS_PRIORITY_QUADTREE 160
#define SS_PRIORITY_CHAT 155
@@ -172,7 +173,7 @@
#define SS_PRIORITY_MOB 150
#define SS_PRIORITY_XENO 149
#define SS_PRIORITY_HUMAN 148
-#define SS_PRIORITY_XENO_PATHFINDING 130
+#define SS_PRIORITY_PATHFINDING 130
#define SS_PRIORITY_STAMINA 126
#define SS_PRIORITY_COMPONENT 125
#define SS_PRIORITY_NANOUI 120
diff --git a/code/__DEFINES/xeno_ai.dm b/code/__DEFINES/xeno_ai.dm
index d7cbe1bf22..72383fc969 100644
--- a/code/__DEFINES/xeno_ai.dm
+++ b/code/__DEFINES/xeno_ai.dm
@@ -1,4 +1,4 @@
-#define XENO_CALCULATING_PATH(X) (X in SSxeno_pathfinding.hash_path)
+#define CALCULATING_PATH(X) (X in SSpathfinding.hash_path)
#define DIRECTION_CHANGE_PENALTY 2
#define NO_WEED_PENALTY 2
@@ -91,7 +91,7 @@ PROBABILITY CALCULATIONS ARE HERE
/// Special blockers for pathfinding or obstacle handling
-#define XENO_AI_SPECIAL_BLOCKERS list(/obj/flamer_fire, /obj/vehicle/multitile, /turf/open/space, /turf/open/gm/river)
+#define AI_SPECIAL_BLOCKERS list(/obj/flamer_fire, /obj/vehicle/multitile, /turf/open/space, /turf/open/gm/river)
// Friend-or-foe universal check
#define IS_SAME_HIVENUMBER(A,B) (A.hivenumber == B.hivenumber)
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index b9a9f276c6..d97b448f66 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -46,8 +46,8 @@
#define format_frequency(f) "[floor((f) / 10)].[(f) % 10]"
#define reverse_direction(direction) ( \
- ( dir & (NORTH|SOUTH) ? ~dir & (NORTH|SOUTH) : 0 ) | \
- ( dir & (EAST|WEST) ? ~dir & (EAST|WEST) : 0 ) \
+ ( direction & (NORTH|SOUTH) ? ~direction & (NORTH|SOUTH) : 0 ) | \
+ ( direction & (EAST|WEST) ? ~direction & (EAST|WEST) : 0 ) \
)
// The sane, counter-clockwise angle to turn to get from /direction/ A to /direction/ B
@@ -1735,6 +1735,33 @@ GLOBAL_LIST_INIT(duplicate_forbidden_vars,list(
if(NORTHWEST)
return list(NORTHWEST, NORTH, WEST)
+/// Makes a given dir cardinal. If the dir is non-cardinal, it will return both cardinal directions that make up the direction. Else, it will be a single-entry list returned.
+/proc/make_dir_cardinal(direction)
+ switch(direction)
+ if(NORTH)
+ return list(NORTH)
+
+ if(EAST)
+ return list(EAST)
+
+ if(SOUTH)
+ return list(SOUTH)
+
+ if(WEST)
+ return list(WEST)
+
+ if(NORTHEAST)
+ return list(NORTH, EAST)
+
+ if(SOUTHEAST)
+ return list(EAST, SOUTH)
+
+ if(SOUTHWEST)
+ return list(SOUTH, WEST)
+
+ if(NORTHWEST)
+ return list(NORTH, WEST)
+
/// Returns TRUE if the target is somewhere that the game should not interact with if possible
/// In this case, admin Zs and tutorial areas
/proc/should_block_game_interaction(atom/target)
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 20aa208131..1c3f335a57 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -38,6 +38,6 @@
//#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between
-// #define TESTING
+#define TESTING
// #define REFERENCE_TRACKING
// #define GC_FAILURE_HARD_LOOKUP
diff --git a/code/controllers/subsystem/human_ai.dm b/code/controllers/subsystem/human_ai.dm
new file mode 100644
index 0000000000..9d582c231f
--- /dev/null
+++ b/code/controllers/subsystem/human_ai.dm
@@ -0,0 +1,79 @@
+
+SUBSYSTEM_DEF(human_ai)
+ name = "Human AI"
+ priority = SS_PRIORITY_HUMAN_AI
+ wait = 0.2 SECONDS
+ /// A list of mobs scheduled to process
+ var/list/mob/living/carbon/human/ai/current_run = list()
+
+ var/ai_kill = FALSE
+
+ /// List of current squads
+ var/list/datum/human_ai_squad/squads = list()
+
+ /// Dict of "id" : squad
+ var/list/squad_id_dict = list()
+
+ /// The current highest ID of any squad
+ var/highest_squad_id = 0
+
+ /// List of all existing orders
+ var/list/datum/ongoing_action/existing_orders = list()
+
+ var/list/human_ai_factions = list()
+
+/datum/controller/subsystem/human_ai/Initialize()
+ for(var/faction_path in subtypesof(/datum/human_ai_faction))
+ var/datum/human_ai_faction/faction_obj = new faction_path
+ human_ai_factions[faction_obj.faction] = faction_obj
+ return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/human_ai/stat_entry(msg)
+ msg = "P:[length(GLOB.human_ai_brains)]"
+ return ..()
+
+/datum/admins/proc/toggle_human_ai()
+ set name = "Toggle Human AI"
+ set category = "Game Master.HumanAI"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ SShuman_ai.ai_kill = !SShuman_ai.ai_kill
+ message_admins("[key_name_admin(usr)] [SShuman_ai.ai_kill? "killed" : "revived"] all human AI.")
+
+/datum/controller/subsystem/human_ai/fire(resumed = FALSE)
+ if(ai_kill)
+ return
+
+ if(!resumed)
+ src.current_run = GLOB.human_ai_brains.Copy()
+ // Cache for sanic speed (lists are references anyways)
+ var/list/current_run = src.current_run
+ while(length(current_run))
+ var/datum/human_ai_brain/brain = current_run[length(current_run)]
+ current_run.len--
+ if(!QDELETED(brain) && !brain.tied_human?.client)
+ brain.process(wait * 0.1)
+
+ if(MC_TICK_CHECK)
+ return
+
+/datum/controller/subsystem/human_ai/proc/create_new_squad()
+ highest_squad_id++
+ var/datum/human_ai_squad/new_squad = new
+ squads += new_squad
+ squad_id_dict["[highest_squad_id]"] = new_squad
+
+/datum/controller/subsystem/human_ai/proc/get_squad(squad_id)
+ RETURN_TYPE(/datum/human_ai_squad)
+
+ if(!squad_id || !(squad_id in squad_id_dict))
+ return null
+ return squad_id_dict[squad_id]
+
+/datum/controller/subsystem/human_ai/proc/create_new_order(datum/ongoing_action/path, ...)
+ if(!path::order)
+ stack_trace("Action of [path] was attempted to be created as an order.")
+ existing_orders += new path(args)
+
diff --git a/code/controllers/subsystem/pathfinding.dm b/code/controllers/subsystem/pathfinding.dm
index bf7d6ed1d9..6387d2a214 100644
--- a/code/controllers/subsystem/pathfinding.dm
+++ b/code/controllers/subsystem/pathfinding.dm
@@ -1,6 +1,6 @@
-SUBSYSTEM_DEF(xeno_pathfinding)
- name = "Xeno Pathfinding"
- priority = SS_PRIORITY_XENO_PATHFINDING
+SUBSYSTEM_DEF(pathfinding)
+ name = "Pathfinding"
+ priority = SS_PRIORITY_PATHFINDING
flags = SS_NO_INIT|SS_TICKER|SS_BACKGROUND
wait = 1
/// A list of mobs scheduled to process
@@ -11,11 +11,11 @@ SUBSYSTEM_DEF(xeno_pathfinding)
var/list/hash_path = list()
var/current_position = 1
-/datum/controller/subsystem/xeno_pathfinding/stat_entry(msg)
+/datum/controller/subsystem/pathfinding/stat_entry(msg)
msg = "P:[length(paths_to_calculate)]"
return ..()
-/datum/controller/subsystem/xeno_pathfinding/fire(resumed = FALSE)
+/datum/controller/subsystem/pathfinding/fire(resumed = FALSE)
if(!resumed)
current_processing = paths_to_calculate.Copy()
@@ -29,15 +29,13 @@ SUBSYSTEM_DEF(xeno_pathfinding)
var/turf/target = current_run.finish
- var/mob/living/carbon/xenomorph/X = current_run.travelling_xeno
-
var/list/visited_nodes = current_run.visited_nodes
var/list/distances = current_run.distances
var/list/f_distances = current_run.f_distances
var/list/prev = current_run.prev
while(length(visited_nodes))
- current_run.current_node = visited_nodes[visited_nodes.len]
+ current_run.current_node = visited_nodes[length(visited_nodes)]
visited_nodes.len--
if(current_run.current_node == target)
break
@@ -46,7 +44,7 @@ SUBSYSTEM_DEF(xeno_pathfinding)
var/turf/neighbor = get_step(current_run.current_node, direction)
var/distance_between = distances[current_run.current_node] * DISTANCE_PENALTY
if(isnull(distances[neighbor]))
- if(get_dist(neighbor, X) > current_run.path_range)
+ if(get_dist(neighbor, current_run.agent) > current_run.path_range)
continue
distances[neighbor] = INFINITY
f_distances[neighbor] = INFINITY
@@ -54,19 +52,22 @@ SUBSYSTEM_DEF(xeno_pathfinding)
if(direction != get_dir(prev[neighbor], neighbor))
distance_between += DIRECTION_CHANGE_PENALTY
- if(!neighbor.weeds)
+ if(isxeno(current_run.agent) && !neighbor.weeds)
distance_between += NO_WEED_PENALTY
for(var/i in neighbor)
var/atom/A = i
distance_between += A.object_weight
- var/list/L = LinkBlocked(X, current_run.current_node, neighbor, current_run.ignore, TRUE)
- L += check_special_blockers(X, neighbor)
+ var/list/L = LinkBlocked(current_run.agent, current_run.current_node, neighbor, current_run.ignore, TRUE)
+ L += check_special_blockers(current_run.agent, neighbor)
if(length(L))
- for(var/i in L)
- var/atom/A = i
- distance_between += A.xeno_ai_obstacle(X, direction, target)
+ if(isxeno(current_run.agent))
+ for(var/atom/A as anything in L)
+ distance_between += A.xeno_ai_obstacle(current_run.agent, direction, target)
+ else
+ for(var/atom/A as anything in L)
+ distance_between += A.human_ai_obstacle(current_run.agent, current_run.agent:ai_brain, direction, target) // zonenote unfuck me later
if(distance_between < distances[neighbor])
distances[neighbor] = distance_between
@@ -126,10 +127,10 @@ SUBSYSTEM_DEF(xeno_pathfinding)
current_run.to_return.Invoke(path)
QDEL_NULL(current_run)
-/datum/controller/subsystem/xeno_pathfinding/proc/check_special_blockers(mob/living/carbon/xenomorph/xeno, turf/checking_turf)
+/datum/controller/subsystem/pathfinding/proc/check_special_blockers(mob/agent, turf/checking_turf)
var/list/pass_back = list()
- for(var/spec_blocker in XENO_AI_SPECIAL_BLOCKERS)
+ for(var/spec_blocker in AI_SPECIAL_BLOCKERS)
pass_back += istype(checking_turf, spec_blocker) ? checking_turf : list()
for(var/atom/checked_atom as anything in checking_turf)
@@ -137,23 +138,23 @@ SUBSYSTEM_DEF(xeno_pathfinding)
return pass_back
-/datum/controller/subsystem/xeno_pathfinding/proc/stop_calculating_path(mob/living/carbon/xenomorph/X)
- var/datum/xeno_pathinfo/data = hash_path[X]
+/datum/controller/subsystem/pathfinding/proc/stop_calculating_path(mob/agent)
+ var/datum/xeno_pathinfo/data = hash_path[agent]
qdel(data)
-/datum/controller/subsystem/xeno_pathfinding/proc/calculate_path(atom/start, atom/finish, path_range, mob/living/carbon/xenomorph/travelling_xeno, datum/callback/CB, list/ignore)
+/datum/controller/subsystem/pathfinding/proc/calculate_path(atom/start, atom/finish, path_range, mob/agent, datum/callback/CB, list/ignore)
if(!get_turf(start) || !get_turf(finish))
return
- var/datum/xeno_pathinfo/data = hash_path[travelling_xeno]
- SSxeno_pathfinding.current_processing -= data
+ var/datum/xeno_pathinfo/data = hash_path[agent]
+ SSpathfinding.current_processing -= data
if(!data)
data = new()
- data.RegisterSignal(travelling_xeno, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/xeno_pathinfo, qdel_wrapper))
+ data.RegisterSignal(agent, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/xeno_pathinfo, qdel_wrapper))
- hash_path[travelling_xeno] = data
+ hash_path[agent] = data
paths_to_calculate += data
data.current_node = get_turf(start)
@@ -162,7 +163,7 @@ SUBSYSTEM_DEF(xeno_pathfinding)
var/turf/target = get_turf(finish)
data.finish = target
- data.travelling_xeno = travelling_xeno
+ data.agent = agent
data.to_return = CB
data.path_range = path_range
data.ignore = ignore
@@ -175,7 +176,7 @@ SUBSYSTEM_DEF(xeno_pathfinding)
/datum/xeno_pathinfo
var/turf/start
var/turf/finish
- var/mob/living/carbon/xenomorph/travelling_xeno
+ var/mob/agent
var/datum/callback/to_return
var/path_range
@@ -198,9 +199,9 @@ SUBSYSTEM_DEF(xeno_pathfinding)
prev = list()
/datum/xeno_pathinfo/Destroy(force)
- SSxeno_pathfinding.hash_path -= travelling_xeno
- SSxeno_pathfinding.paths_to_calculate -= src
- SSxeno_pathfinding.current_processing -= src
+ SSpathfinding.hash_path -= agent
+ SSpathfinding.paths_to_calculate -= src
+ SSpathfinding.current_processing -= src
#ifdef TESTING
addtimer(CALLBACK(src, PROC_REF(clear_colors), distances), 5 SECONDS)
@@ -208,7 +209,7 @@ SUBSYSTEM_DEF(xeno_pathfinding)
start = null
finish = null
- travelling_xeno = null
+ agent = null
to_return = null
visited_nodes = null
distances = null
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 7fd82aeb9c..ba91503e8e 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -65,6 +65,8 @@
flags_atom = FPRINT
/// flags for item stuff that isn't clothing/equipping specific.
var/flags_item = NO_FLAGS
+ /// flags for human AI to determine what this item does
+ var/flags_human_ai = NO_FLAGS
/// This is used to determine on which slots an item can fit.
var/flags_equip_slot = NO_FLAGS
@@ -1096,3 +1098,9 @@ cases. Override_icon_state should be a list.*/
///Called by /mob/living/carbon/swap_hand() when hands are swapped
/obj/item/proc/hands_swapped(mob/living/carbon/swapper_of_hands)
return
+
+/obj/item/proc/ai_use(mob/living/carbon/human/user)
+ return
+
+/obj/item/proc/ai_can_use(mob/living/carbon/human/user)
+ return FALSE
diff --git a/code/game/objects/items/devices/radio/encryptionkey.dm b/code/game/objects/items/devices/radio/encryptionkey.dm
index 160f4beaff..250a2c2911 100644
--- a/code/game/objects/items/devices/radio/encryptionkey.dm
+++ b/code/game/objects/items/devices/radio/encryptionkey.dm
@@ -35,8 +35,11 @@
return
var/obj/item/device/radio/headset/current_headset = loc
+ var/datum/radio_frequency/old_connections = current_headset.secure_radio_connections[old_name]
+ if(!old_connections)
+ return
- var/passed_freq = current_headset.secure_radio_connections[old_name].frequency
+ var/passed_freq = old_connections.frequency
current_headset.secure_radio_connections -= old_name
SSradio.remove_object(current_headset, passed_freq)
diff --git a/code/game/objects/items/explosives/grenades/grenade.dm b/code/game/objects/items/explosives/grenades/grenade.dm
index b2f95646a9..b60131dfb6 100644
--- a/code/game/objects/items/explosives/grenades/grenade.dm
+++ b/code/game/objects/items/explosives/grenades/grenade.dm
@@ -9,6 +9,7 @@
throw_range = 7
flags_atom = FPRINT|CONDUCT
flags_equip_slot = SLOT_WAIST
+ flags_human_ai = GRENADE_ITEM
hitsound = 'sound/weapons/smash.ogg'
allowed_sensors = list(/obj/item/device/assembly/timer)
max_container_volume = 60
@@ -151,3 +152,21 @@
walk(src, null, null)
..()
return
+
+/obj/item/explosive/grenade/ai_can_use(mob/living/carbon/human/user)
+ return TRUE
+
+/obj/item/explosive/grenade/ai_use(mob/living/carbon/human/ai/user, turf/target_turf)
+ attack_self(user)
+ user.toggle_throw_mode(THROW_MODE_NORMAL)
+ user.ai_brain.ensure_primary_hand(src)
+ sleep(det_time * 0.4)
+ if(QDELETED(src) || (loc != user))
+ return
+
+ user.ai_brain.say_grenade_thrown_line()
+ sleep(det_time * 0.4)
+ if(QDELETED(src) || (loc != user))
+ return
+
+ user.throw_item(user.ai_brain.target_floor)
diff --git a/code/game/objects/items/reagent_containers/autoinjectors.dm b/code/game/objects/items/reagent_containers/autoinjectors.dm
index 291f02d5f4..e1c11f95df 100644
--- a/code/game/objects/items/reagent_containers/autoinjectors.dm
+++ b/code/game/objects/items/reagent_containers/autoinjectors.dm
@@ -73,6 +73,22 @@
..()
update_icon()
+/obj/item/reagent_container/hypospray/autoinjector/ai_can_use(mob/living/carbon/human/user)
+ if(!uses_left)
+ return FALSE
+
+ var/datum/reagent/reagent_datum = GLOB.chemical_reagents_list[chemname]
+
+ if((user.reagents.get_reagent_amount(chemname) + amount_per_transfer_from_this) > reagent_datum.overdose)
+ return FALSE
+
+ if(skilllock != SKILL_MEDICAL_TRAINED && !skillcheck(user, SKILL_MEDICAL, skilllock))
+ return FALSE
+
+ return TRUE
+
+/obj/item/reagent_container/hypospray/autoinjector/ai_use(mob/living/carbon/human/user)
+ attack(user, user)
/obj/item/reagent_container/hypospray/autoinjector/tricord
name = "tricordrazine autoinjector"
@@ -117,6 +133,11 @@
display_maptext = TRUE
maptext_label = "D+"
+/obj/item/reagent_container/hypospray/autoinjector/dexalinp/ai_can_use(mob/living/carbon/human/user)
+ if(user.reagents.get_reagent_amount(chemname))
+ return FALSE
+ return ..()
+
/obj/item/reagent_container/hypospray/autoinjector/chloralhydrate
name = "anesthetic autoinjector"
chemname = "anesthetic"
@@ -311,6 +332,23 @@
desc = "An auto-injector loaded with a small amount of painkiller for marines to self-administer."
icon_state = "tramadol"
+/obj/item/reagent_container/hypospray/autoinjector/dylovene
+ name = "dylovene autoinjector"
+ chemname = "dylovene"
+ desc = "An autoinjector loaded with 3 uses of Dylovene, a general-use anti-toxin."
+ amount_per_transfer_from_this = LOWM_REAGENTS_OVERDOSE * INJECTOR_PERCENTAGE_OF_OD
+ volume = (LOWM_REAGENTS_OVERDOSE * INJECTOR_PERCENTAGE_OF_OD) * INJECTOR_USES
+ display_maptext = TRUE
+ maptext_label = "Dy"
+
+/obj/item/reagent_container/hypospray/autoinjector/dylovene/skillless
+ name = "dylovene EZ autoinjector"
+ desc = "An EZ autoinjector loaded with 3 uses of Dylovene, a general-use anti-toxin. Doesn't require any training to use."
+ icon_state = "emptyskill"
+ item_state = "emptyskill"
+ skilllock = SKILL_MEDICAL_DEFAULT
+
+
/obj/item/reagent_container/hypospray/autoinjector/empty
name = "autoinjector (C-T)"
desc = "A custom-made auto-injector, likely from research."
diff --git a/code/game/objects/items/reagent_containers/hypospray.dm b/code/game/objects/items/reagent_containers/hypospray.dm
index 94477520da..62afcb78bf 100644
--- a/code/game/objects/items/reagent_containers/hypospray.dm
+++ b/code/game/objects/items/reagent_containers/hypospray.dm
@@ -14,6 +14,7 @@
flags_atom = FPRINT|OPENCONTAINER
flags_equip_slot = SLOT_WAIST
flags_item = NOBLUDGEON
+ flags_human_ai = HEALING_ITEM
matter = list("plastic" = 1250, "glass" = 250)
transparent = TRUE
var/skilllock = SKILL_MEDICAL_TRAINED
diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm
index c4a496a123..c4da729b53 100644
--- a/code/game/objects/items/stacks/medical.dm
+++ b/code/game/objects/items/stacks/medical.dm
@@ -8,6 +8,7 @@
throw_speed = SPEED_VERY_FAST
throw_range = 20
attack_speed = 3
+ flags_human_ai = HEALING_ITEM
var/heal_brute = 0
var/heal_burn = 0
var/alien = FALSE
@@ -95,6 +96,16 @@
to_chat(user, SPAN_WARNING("There are no wounds on [possessive] [affecting.display_name]."))
return TRUE
+/obj/item/stack/medical/bruise_pack/ai_use(mob/living/carbon/human/user)
+ for(var/obj/limb/limb as anything in user.limbs)
+ if(QDELETED(src))
+ return
+
+ if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list)
+ user.zone_selected = limb.name
+ attack(user, user)
+ sleep(user.ai_brain.short_action_delay)
+
/obj/item/stack/medical/bruise_pack/two
amount = 2
@@ -193,6 +204,42 @@
to_chat(user, SPAN_WARNING("There are no wounds on [possessive] [affecting.display_name]."))
return TRUE
+/obj/item/stack/medical/advanced/bruise_pack/ai_can_use(mob/living/carbon/human/user)
+ for(var/obj/limb/limb as anything in user.limbs)
+ if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list)
+ return TRUE
+
+ for(var/datum/wound/wound in limb.wounds)
+ if(wound.internal || wound.damage_type == BURN)
+ continue
+
+ if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED)))
+ return TRUE
+ return FALSE
+
+/obj/item/stack/medical/advanced/bruise_pack/ai_use(mob/living/carbon/human/user)
+ for(var/obj/limb/limb as anything in user.limbs)
+ if(QDELETED(src))
+ return
+
+ if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list)
+ user.zone_selected = limb.name
+ attack(user, user)
+ sleep(user.ai_brain.short_action_delay)
+ continue
+
+ for(var/datum/wound/wound in limb.wounds)
+ if(wound.internal || wound.damage_type == BURN)
+ continue
+
+ if(QDELETED(src))
+ return
+
+ if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED)))
+ user.zone_selected = limb.name
+ attack(user, user)
+ sleep(user.ai_brain.short_action_delay)
+
/obj/item/stack/medical/advanced/bruise_pack/predator
name = "mending herbs"
singular_name = "mending herb"
@@ -202,6 +249,7 @@
heal_brute = 15
stack_id = "mending herbs"
alien = TRUE
+
/obj/item/stack/medical/advanced/ointment/predator
name = "soothing herbs"
singular_name = "soothing herb"
@@ -211,6 +259,7 @@
heal_burn = 15
stack_id = "soothing herbs"
alien = TRUE
+
/obj/item/stack/medical/advanced/ointment
name = "burn kit"
singular_name = "burn kit"
@@ -260,6 +309,30 @@
to_chat(user, SPAN_WARNING("There are no burns on [possessive] [affecting.display_name]."))
return TRUE
+/obj/item/stack/medical/advanced/ointment/ai_can_use(mob/living/carbon/human/user)
+ for(var/obj/limb/limb as anything in user.limbs)
+ for(var/datum/wound/wound in limb.wounds)
+ if(wound.internal || wound.damage_type == BRUTE)
+ continue
+
+ if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED)))
+ return TRUE
+ return FALSE
+
+/obj/item/stack/medical/advanced/ointment/ai_use(mob/living/carbon/human/user)
+ for(var/obj/limb/limb as anything in user.limbs)
+ for(var/datum/wound/wound in limb.wounds)
+ if(wound.internal || wound.damage_type == BRUTE)
+ continue
+
+ if(QDELETED(src))
+ return
+
+ if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED)))
+ user.zone_selected = limb.name
+ attack(user, user)
+ sleep(user.ai_brain.short_action_delay)
+
/obj/item/stack/medical/splint
name = "medical splints"
singular_name = "medical splint"
@@ -317,3 +390,15 @@
if(affecting.apply_splints(src, user, M, indestructible_splints)) // Referenced in external organ helpers.
use(1)
playsound(user, 'sound/handling/splint1.ogg', 25, 1, 2)
+
+
+/obj/item/stack/medical/splint/ai_use(mob/living/carbon/human/user)
+ for(var/obj/limb/limb as anything in user.limbs)
+ if(QDELETED(src))
+ return
+
+ if(limb.is_broken())
+ user.zone_selected = limb.name
+ attack(user, user)
+ sleep(user.ai_brain.short_action_delay)
+ continue
diff --git a/code/game/objects/items/storage/firstaid.dm b/code/game/objects/items/storage/firstaid.dm
index 6535497ad5..874740dd94 100644
--- a/code/game/objects/items/storage/firstaid.dm
+++ b/code/game/objects/items/storage/firstaid.dm
@@ -348,6 +348,7 @@
)
storage_flags = STORAGE_FLAGS_BOX|STORAGE_CLICK_GATHER|STORAGE_QUICK_GATHER
storage_slots = null
+ flags_human_ai = HEALING_ITEM
use_sound = "pillbottle"
max_storage_space = 16
var/skilllock = SKILL_MEDICAL_MEDIC
@@ -513,6 +514,22 @@
/obj/item/storage/pill_bottle/proc/error_idlock(mob/user)
to_chat(user, SPAN_WARNING("It must have some kind of ID lock..."))
+/obj/item/storage/pill_bottle/ai_can_use(mob/living/carbon/human/user)
+ if(!length(contents) || !COOLDOWN_FINISHED(user.ai_brain, pill_use_cooldown))
+ return FALSE
+
+ if(skilllock && !skillcheck(user, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC))
+ return FALSE
+
+ return TRUE
+
+/obj/item/storage/pill_bottle/ai_use(mob/living/carbon/human/user)
+ var/obj/item/pill = contents[1]
+ if(user.put_in_active_hand(pill))
+ remove_from_storage(pill, user)
+ pill.attack(user, user)
+ COOLDOWN_START(user.ai_brain, pill_use_cooldown, 10 SECONDS)
+
/obj/item/storage/pill_bottle/proc/choose_color(mob/user)
if(!user)
user = usr
diff --git a/code/game/objects/items/tools/cleaning_tools.dm b/code/game/objects/items/tools/cleaning_tools.dm
index 9fab254a71..c5343f808d 100644
--- a/code/game/objects/items/tools/cleaning_tools.dm
+++ b/code/game/objects/items/tools/cleaning_tools.dm
@@ -1,3 +1,6 @@
+/obj/item/tool
+ flags_human_ai = TOOL_ITEM
+
/obj/item/tool/mop
desc = "The world of janitalia wouldn't be complete without a mop."
name = "mop"
diff --git a/code/game/objects/items/tools/kitchen_tools.dm b/code/game/objects/items/tools/kitchen_tools.dm
index a29bf97cac..1c710d9649 100644
--- a/code/game/objects/items/tools/kitchen_tools.dm
+++ b/code/game/objects/items/tools/kitchen_tools.dm
@@ -134,6 +134,7 @@
matter = list("metal" = 12000)
attack_verb = list("slashed", "stabbed", "sliced", "torn", "ripped", "diced", "cut")
+ flags_human_ai = MELEE_WEAPON_ITEM | TOOL_ITEM
/*
* Plastic Pizza Cutter
diff --git a/code/game/objects/items/tools/maintenance_tools.dm b/code/game/objects/items/tools/maintenance_tools.dm
index 7264a8a2e5..1b8abdbe9a 100644
--- a/code/game/objects/items/tools/maintenance_tools.dm
+++ b/code/game/objects/items/tools/maintenance_tools.dm
@@ -479,6 +479,9 @@
pry_capable = IS_PRY_CAPABLE_CROWBAR
preferred_storage = list(/obj/item/clothing/accessory/storage/tool_webbing = WEAR_ACCESSORY)
+/obj/item/tool/crowbar/ai_can_use(mob/living/carbon/human/user)
+ return TRUE
+
/obj/item/tool/crowbar/red
icon = 'icons/obj/items/items.dmi'
icon_state = "red_crowbar"
diff --git a/code/game/objects/items/weapons/blades.dm b/code/game/objects/items/weapons/blades.dm
index b475de36a4..d425d5b20d 100644
--- a/code/game/objects/items/weapons/blades.dm
+++ b/code/game/objects/items/weapons/blades.dm
@@ -8,6 +8,7 @@
force = MELEE_FORCE_STRONG
throwforce = MELEE_FORCE_WEAK
sharp = IS_SHARP_ITEM_BIG
+ flags_human_ai = MELEE_WEAPON_ITEM
edge = 1
w_class = SIZE_LARGE
hitsound = 'sound/weapons/bladeslice.ogg'
diff --git a/code/game/objects/structures/barricade/folding.dm b/code/game/objects/structures/barricade/folding.dm
index 8fe00d04a7..fc90b76400 100644
--- a/code/game/objects/structures/barricade/folding.dm
+++ b/code/game/objects/structures/barricade/folding.dm
@@ -289,3 +289,15 @@
repair_materials = list("metal" = 0.3, "plasteel" = 0.45)
linkable = FALSE
+
+/obj/structure/barricade/plasteel/metal/wired/New()
+ can_wire = FALSE
+ is_wired = TRUE
+ climbable = FALSE
+ update_icon()
+ return ..()
+
+/obj/structure/barricade/plasteel/metal/wired/initialize_pass_flags(datum/pass_flags_container/PF)
+ ..()
+ flags_can_pass_front_temp &= ~PASS_OVER_THROW_MOB
+ flags_can_pass_behind_temp &= ~PASS_OVER_THROW_MOB
diff --git a/code/game/objects/structures/barricade/non_folding.dm b/code/game/objects/structures/barricade/non_folding.dm
index 575f1da738..1556e8cca4 100644
--- a/code/game/objects/structures/barricade/non_folding.dm
+++ b/code/game/objects/structures/barricade/non_folding.dm
@@ -294,3 +294,16 @@
barricade_type = "new_plasteel"
repair_materials = list("plasteel" = 0.45)
+/obj/structure/barricade/metal/plasteel/wired/New()
+ maxhealth += 50
+ update_health(-50)
+ can_wire = FALSE
+ is_wired = TRUE
+ climbable = FALSE
+ update_icon()
+ return ..()
+
+/obj/structure/barricade/metal/plasteel/wired/initialize_pass_flags(datum/pass_flags_container/PF)
+ ..()
+ flags_can_pass_front_temp &= ~PASS_OVER_THROW_MOB
+ flags_can_pass_behind_temp &= ~PASS_OVER_THROW_MOB
diff --git a/code/game/objects/structures/barricade/sandbags.dm b/code/game/objects/structures/barricade/sandbags.dm
index 0e2b77b4c1..c08cd6d77a 100644
--- a/code/game/objects/structures/barricade/sandbags.dm
+++ b/code/game/objects/structures/barricade/sandbags.dm
@@ -133,6 +133,10 @@
health += 50
build_stage++
+/obj/structure/barricade/sandbags/full
+
+/obj/structure/barricade/sandbags/full/New(loc, mob/user, direction, amount = 5)
+ . = ..()
/obj/structure/barricade/sandbags/wired/New()
health = BARRICADE_SANDBAG_TRESHOLD_5
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 2b3db70abe..95a83117e8 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -72,9 +72,17 @@ GLOBAL_LIST_INIT(admin_verbs_default, list(
/client/proc/cmd_admin_say, /*staff-only ooc chat*/
/client/proc/cmd_mod_say, /* alternate way of typing asay, no different than cmd_admin_say */
/client/proc/cmd_admin_tacmaps_panel,
+ /datum/admins/proc/toggle_ai,
+ /datum/admins/proc/toggle_human_ai,
+ /datum/admins/proc/create_human_ai_patrol,
+ /client/proc/open_human_ai_management_panel,
+ /client/proc/open_human_faction_management_panel,
+ /client/proc/create_human_ai,
/client/proc/other_records,
+ /client/proc/fortify_room,
))
+
GLOBAL_LIST_INIT(admin_verbs_admin, list(
/datum/admins/proc/togglejoin, /*toggles whether people can join the current game*/
/datum/admins/proc/announce, /*priority announce something to all clients.*/
diff --git a/code/modules/admin/verbs/select_equipment.dm b/code/modules/admin/verbs/select_equipment.dm
index edecb81d74..5441cba0e8 100644
--- a/code/modules/admin/verbs/select_equipment.dm
+++ b/code/modules/admin/verbs/select_equipment.dm
@@ -131,7 +131,7 @@
arm_equipment(M, dresscode, FALSE, count_participant)
if(!no_logs)
message_admins("[key_name_admin(usr)] changed the equipment of [key_name_admin(M)] to [dresscode].")
- return
+ return TRUE
/client/proc/cmd_admin_dress_all()
set category = "Debug"
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index f09023408f..d26378f332 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -134,3 +134,9 @@
/// Holds the game master datum for this client
var/datum/game_master/game_master_menu
+
+ /// Holds the human AI manager panel for this client
+ var/datum/human_ai_management_menu/human_ai_menu
+
+ /// Holds the human faction manager panel for this client
+ var/datum/human_faction_management_menu/human_faction_menu
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index 5bc91dc699..51592dc257 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -458,6 +458,7 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list(
QDEL_NULL(soundOutput)
QDEL_NULL(obj_window)
QDEL_NULL(game_master_menu)
+ QDEL_NULL(human_ai_menu)
if(prefs)
prefs.owner = null
QDEL_NULL(prefs.preview_dummy)
diff --git a/code/modules/gear_presets/_select_equipment.dm b/code/modules/gear_presets/_select_equipment.dm
index 2fc117421c..ee75bf9586 100644
--- a/code/modules/gear_presets/_select_equipment.dm
+++ b/code/modules/gear_presets/_select_equipment.dm
@@ -633,7 +633,7 @@ GLOBAL_LIST_INIT(rebel_rifles, list(
var/gunpath = pick(GLOB.rebel_smgs)
var/ammopath = GLOB.rebel_smgs[gunpath]
- spawn_weapon(gunpath, ammopath, M, ammo_amount)
+ spawn_weapon(gunpath, ammopath, M, FALSE, ammo_amount)
return 1
@@ -643,7 +643,7 @@ GLOBAL_LIST_INIT(rebel_rifles, list(
var/gunpath = pick(GLOB.rebel_shotguns)
var/ammopath = GLOB.rebel_shotguns[gunpath]
- spawn_weapon(gunpath, ammopath, M, ammo_amount)
+ spawn_weapon(gunpath, ammopath, M, FALSE, ammo_amount)
return 1
@@ -653,7 +653,7 @@ GLOBAL_LIST_INIT(rebel_rifles, list(
var/gunpath = pick(GLOB.rebel_rifles)
var/ammopath = GLOB.rebel_rifles[gunpath]
- spawn_weapon(gunpath, ammopath, M, ammo_amount)
+ spawn_weapon(gunpath, ammopath, M, FALSE, ammo_amount)
return 1
diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm
index 286645fc70..36233d930a 100644
--- a/code/modules/mob/inventory.dm
+++ b/code/modules/mob/inventory.dm
@@ -144,10 +144,12 @@
//drop the inventory item on a specific location
/mob/proc/drop_inv_item_to_loc(obj/item/I, atom/newloc, nomoveupdate, force)
+ SEND_SIGNAL(src, COMSIG_MOB_DROP_ITEM, I)
return u_equip(I, newloc, nomoveupdate, force)
//drop the inventory item on the ground
/mob/proc/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force)
+ SEND_SIGNAL(src, COMSIG_MOB_DROP_ITEM, I)
return u_equip(I, get_step(src, 0), nomoveupdate, force) // Drops on turf instead of loc
/mob/living/carbon/human/proc/pickup_recent()
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm
new file mode 100644
index 0000000000..876ecbba0a
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm
@@ -0,0 +1,18 @@
+/datum/ongoing_action
+ var/name
+ var/datum/human_ai_brain/brain
+ var/order = FALSE
+
+/datum/ongoing_action/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ src.brain = brain
+
+/datum/ongoing_action/Destroy(force, ...)
+ brain = null
+ return ..()
+
+/datum/ongoing_action/proc/trigger_action()
+ return
+
+/datum/ongoing_action/proc/tgui_data()
+ return list()
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm
new file mode 100644
index 0000000000..ff5e086cc2
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm
@@ -0,0 +1,32 @@
+/datum/ongoing_action/approach_target
+ name = "Approach Target"
+ var/atom/movable/target
+ var/acceptable_distance
+ var/do_target_dead
+
+/datum/ongoing_action/approach_target/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ target = arguments[2]
+ acceptable_distance = arguments[3]
+ do_target_dead = length(arguments) < 4 ? TRUE : arguments[4]
+
+/datum/ongoing_action/approach_target/Destroy(force, ...)
+ target = null
+ return ..()
+
+/datum/ongoing_action/approach_target/trigger_action()
+ if(QDELETED(target))
+ return ONGOING_ACTION_COMPLETED
+
+ if(ismob(target) && do_target_dead)
+ var/mob/M = target
+ if(M.is_dead())
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target, brain.tied_human) > acceptable_distance)
+ if(!brain.move_to_next_turf(get_turf(target)))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target, brain.tied_human) > acceptable_distance)
+ return ONGOING_ACTION_UNFINISHED
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/approach_target_carefully.dm b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target_carefully.dm
new file mode 100644
index 0000000000..d55eed4315
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target_carefully.dm
@@ -0,0 +1,7 @@
+/datum/ongoing_action/approach_target/carefully
+ name = "Approach Target Carefully"
+
+/datum/ongoing_action/approach_target/carefully/trigger_action()
+ if(brain.current_target)
+ return ONGOING_ACTION_COMPLETED
+ . = ..()
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm
new file mode 100644
index 0000000000..b187d3b543
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm
@@ -0,0 +1,84 @@
+/datum/ongoing_action/item_pickup
+ name = "Item Pickup"
+ var/obj/item/to_pickup
+
+/datum/ongoing_action/item_pickup/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ to_pickup = arguments[2]
+
+/datum/ongoing_action/item_pickup/Destroy(force, ...)
+ to_pickup = null
+ return ..()
+
+/datum/ongoing_action/item_pickup/trigger_action()
+ if(QDELETED(to_pickup) || !isturf(to_pickup.loc))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(to_pickup, brain.tied_human) > 1)
+ if(!brain.move_to_next_turf(get_turf(to_pickup)))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(to_pickup, brain.tied_human) > 1)
+ return ONGOING_ACTION_UNFINISHED
+
+ if(brain.primary_weapon)
+ brain.primary_weapon.unwield(brain.tied_human)
+
+ else if(isgun(to_pickup))
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ var/obj/item/weapon/gun/primary = to_pickup
+ // We do the three below lines to make it so that the AI can immediately pick up a gun and open fire. This ensures that we don't need to account for this possibility when firing.
+ primary.wield_time = world.time
+ primary.pull_time = world.time
+ primary.guaranteed_delay_time = world.time
+ return ONGOING_ACTION_COMPLETED
+
+ if(istype(to_pickup, /obj/item/storage/belt) && !brain.container_refs["belt"])
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.tied_human.equip_to_slot(to_pickup, WEAR_WAIST)
+ return ONGOING_ACTION_COMPLETED
+
+ if(istype(to_pickup, /obj/item/storage/backpack) && !brain.container_refs["backpack"])
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.tied_human.equip_to_slot(to_pickup, WEAR_BACK)
+ return ONGOING_ACTION_COMPLETED
+
+ if(istype(to_pickup, /obj/item/storage/pouch) && !brain.container_refs["left_pocket"])
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.tied_human.equip_to_slot(to_pickup, WEAR_L_STORE)
+ return ONGOING_ACTION_COMPLETED
+
+ if(istype(to_pickup, /obj/item/storage/pouch) && !brain.container_refs["right_pocket"])
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.tied_human.equip_to_slot(to_pickup, WEAR_R_STORE)
+ return ONGOING_ACTION_COMPLETED
+
+ var/storage_spot = brain.storage_has_room(to_pickup)
+ if(!storage_spot || !to_pickup.ai_can_use(brain.tied_human))
+ return ONGOING_ACTION_COMPLETED
+
+ if(is_type_in_list(to_pickup, brain.all_medical_items))
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.store_item(to_pickup, storage_spot, HUMAN_AI_HEALTHITEMS)
+ return ONGOING_ACTION_COMPLETED
+
+ if(brain.primary_weapon && istype(to_pickup, /obj/item/ammo_magazine))
+ var/obj/item/ammo_magazine/mag = to_pickup
+ if(istype(brain.primary_weapon, mag.gun_type))
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.store_item(to_pickup, storage_spot, HUMAN_AI_AMMUNITION)
+ return ONGOING_ACTION_COMPLETED
+
+ if(istype(to_pickup, /obj/item/explosive/grenade))
+ var/obj/item/explosive/grenade/nade = to_pickup
+ if(!nade.active)
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.store_item(to_pickup, storage_spot, HUMAN_AI_GRENADES)
+ return ONGOING_ACTION_COMPLETED
+
+ if(to_pickup.flags_human_ai & TOOL_ITEM)
+ brain.tied_human.put_in_hands(to_pickup, TRUE)
+ brain.store_item(to_pickup, storage_spot, HUMAN_AI_TOOLS)
+ return ONGOING_ACTION_COMPLETED
+
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/melee_atom.dm b/code/modules/mob/living/carbon/human/ai/action_datums/melee_atom.dm
new file mode 100644
index 0000000000..c8a82c6589
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/melee_atom.dm
@@ -0,0 +1,33 @@
+/datum/ongoing_action/melee_atom
+ name = "Melee Atom"
+ var/atom/target
+
+/datum/ongoing_action/melee_atom/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ target = arguments[2]
+
+/datum/ongoing_action/melee_atom/Destroy(force, ...)
+ target = null
+ return ..()
+
+/datum/ongoing_action/melee_atom/trigger_action()
+ if(QDELETED(target) || brain.in_combat || !brain.primary_weapon) // Lower priority than getting shot at
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target, brain.tied_human) > 1)
+ if(!brain.move_to_next_turf(get_turf(target)))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target, brain.tied_human) > 1)
+ return ONGOING_ACTION_UNFINISHED
+
+ //if(brain.primary_melee)
+ // brain.unholster_melee()
+
+ //X.do_click(src, "", list())
+
+ brain.unholster_primary() // this should eventually have support for melee weapons
+ //brain.ensure_primary_hand(brain.primary_weapon)
+ brain.tied_human.do_click(target, "", list())
+
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm b/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm
new file mode 100644
index 0000000000..1beefc0843
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm
@@ -0,0 +1,10 @@
+/datum/ongoing_action/order
+ order = TRUE
+ var/can_perform_in_combat = FALSE
+
+/datum/ongoing_action/order/New(list/arguments)
+ return
+
+/datum/ongoing_action/order/Destroy(force, ...)
+ SShuman_ai.existing_orders -= src
+ return ..()
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm b/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm
new file mode 100644
index 0000000000..6f77965e0a
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm
@@ -0,0 +1,82 @@
+/datum/ongoing_action/order/patrol_waypoints
+ name = "Patrol Waypoints"
+ var/list/waypoints = list()
+ var/time_at_waypoint = 10 SECONDS
+ var/current_waypoint_index = 1
+ var/turf/current_waypoint_target
+ var/waiting = FALSE
+ var/desc = ""
+
+/datum/ongoing_action/order/patrol_waypoints/New(list/arguments)
+ . = ..()
+ waypoints = arguments[2]
+ time_at_waypoint = (length(arguments) >= 3 ? arguments[3] : 10 SECONDS)
+ current_waypoint_target = waypoints[current_waypoint_index]
+
+/datum/ongoing_action/order/patrol_waypoints/Destroy(force, ...)
+ current_waypoint_target = null
+ return ..()
+
+/datum/ongoing_action/order/patrol_waypoints/trigger_action(datum/human_ai_brain/brain)
+ if(waiting || QDELETED(current_waypoint_target) || !isturf(current_waypoint_target)) // Since this one order applies to the entire squad, everyone stops approaching when one arrives
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(current_waypoint_target, brain.tied_human) > 1)
+ if(!brain.move_to_next_turf(current_waypoint_target))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(current_waypoint_target, brain.tied_human) > 1)
+ return ONGOING_ACTION_UNFINISHED
+ if(brain.is_squad_leader)
+ waiting = TRUE
+ addtimer(CALLBACK(src, PROC_REF(set_next_waypoint)), time_at_waypoint)
+ return ONGOING_ACTION_COMPLETED
+
+/datum/ongoing_action/order/patrol_waypoints/proc/set_next_waypoint()
+ if(current_waypoint_index >= length(waypoints))
+ current_waypoint_index = 1
+ else
+ current_waypoint_index++
+ current_waypoint_target = waypoints[current_waypoint_index]
+ waiting = FALSE
+
+/datum/ongoing_action/order/patrol_waypoints/tgui_data()
+ return list(
+ list(
+ "waypoint_amount",
+ "waiting",
+ "desc",
+ ),
+ list(
+ length(waypoints),
+ waiting,
+ desc,
+ )
+ )
+
+/datum/admins/proc/create_human_ai_patrol()
+ set name = "Create Human AI Patrol Waypoints"
+ set category = "Game Master.HumanAI"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/list/turf/waypoint_list = list()
+ while(TRUE)
+ if(tgui_input_list(usr, "Press Enter to save the turf you are on to the patrol datum. Press Cancel to finalize.", "Save Turf", list("Enter", "Cancel")) == "Enter")
+ var/turf/user_turf = get_turf(usr)
+ var/dist = length(waypoint_list) ? get_dist(waypoint_list[length(waypoint_list)], user_turf) : 0
+ if(length(waypoint_list) && (dist > HUMAN_AI_MAX_PATHFINDING_RANGE))
+ to_chat(usr, SPAN_WARNING("This waypoint is too far from the previous one. Maximum distance is [HUMAN_AI_MAX_PATHFINDING_RANGE] while this node's was [dist]."))
+ continue
+ waypoint_list += user_turf
+ continue
+ break
+
+ if(length(waypoint_list) <= 0)
+ return
+
+ var/datum/ongoing_action/order/patrol_waypoints/patrol = new(list(null, waypoint_list, 10 SECONDS))
+ patrol.desc = tgui_input_text(usr, "Input a description of the patrol.", "Description")
+ SShuman_ai.existing_orders += patrol
+ to_chat(usr, SPAN_NOTICE("Patrol order has been created."))
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/pickup_primary.dm b/code/modules/mob/living/carbon/human/ai/action_datums/pickup_primary.dm
new file mode 100644
index 0000000000..634df82299
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/pickup_primary.dm
@@ -0,0 +1,8 @@
+/// In case they drop their primary but get a new one in the time it takes to retrieve their old one
+/datum/ongoing_action/item_pickup/pickup_primary
+ name = "Pickup Primary"
+
+/datum/ongoing_action/item_pickup/pickup_primary/trigger_action()
+ if(brain.primary_weapon)
+ return ONGOING_ACTION_COMPLETED
+ return ..()
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/retreat_from_target.dm b/code/modules/mob/living/carbon/human/ai/action_datums/retreat_from_target.dm
new file mode 100644
index 0000000000..8bcea49b0a
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/retreat_from_target.dm
@@ -0,0 +1,40 @@
+/datum/ongoing_action/retreat_from_target
+ name = "Retreat From Target"
+ var/atom/movable/target
+ var/acceptable_distance
+ var/do_target_dead
+
+/datum/ongoing_action/retreat_from_target/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ target = arguments[2]
+ acceptable_distance = arguments[3]
+ do_target_dead = length(arguments) < 4 ? TRUE : arguments[4]
+
+/datum/ongoing_action/retreat_from_target/Destroy(force, ...)
+ target = null
+ return ..()
+
+/datum/ongoing_action/retreat_from_target/trigger_action()
+ if(QDELETED(target))
+ return ONGOING_ACTION_COMPLETED
+
+ if(ismob(target) && do_target_dead)
+ var/mob/M = target
+ if(M.is_dead())
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target, brain.tied_human) < acceptable_distance)
+ var/relative_dir = Get_Compass_Dir(target, brain.tied_human)
+ var/moved = FALSE
+ for(var/D in list(relative_dir, turn(relative_dir, 90), turn(relative_dir, -90)))
+ if(brain.move_to_next_turf(get_step(get_turf(brain.tied_human), D)))
+ moved = TRUE
+ break
+
+ if(!moved)
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target, brain.tied_human) < acceptable_distance)
+ return ONGOING_ACTION_UNFINISHED
+
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm
new file mode 100644
index 0000000000..85b88b4b25
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm
@@ -0,0 +1,32 @@
+/datum/ongoing_action/take_cover
+ name = "Take Cover"
+ var/turf/target_turf
+ var/atom/cover_atom
+ var/hard_cover
+
+/datum/ongoing_action/take_cover/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ target_turf = arguments[2]
+ cover_atom = arguments[3]
+ hard_cover = arguments[4]
+
+/datum/ongoing_action/take_cover/Destroy(force, ...)
+ target_turf = null
+ cover_atom = null
+ return ..()
+
+/datum/ongoing_action/take_cover/trigger_action()
+ if(!isturf(target_turf))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target_turf, brain.tied_human) > 0)
+ if(!brain.move_to_next_turf(target_turf))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target_turf, brain.tied_human) > 0)
+ return ONGOING_ACTION_UNFINISHED
+
+ brain.in_cover = TRUE
+ brain.hard_cover = hard_cover
+ brain.RegisterSignal(cover_atom, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/human_ai_brain, on_cover_destroyed), TRUE)
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm b/code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm
new file mode 100644
index 0000000000..d190447c2a
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm
@@ -0,0 +1,27 @@
+/datum/ongoing_action/take_inside_cover
+ name = "Take Cover (Inside)"
+ var/turf/target_turf
+
+/datum/ongoing_action/take_inside_cover/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ target_turf = arguments[2]
+
+/datum/ongoing_action/take_inside_cover/Destroy(force, ...)
+ target_turf = null
+ return ..()
+
+/datum/ongoing_action/take_inside_cover/trigger_action()
+ if(!isturf(target_turf))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target_turf, brain.tied_human) > 0)
+ if(!brain.move_to_next_turf(target_turf))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(target_turf, brain.tied_human) > 0)
+ return ONGOING_ACTION_UNFINISHED
+
+ brain.in_cover = TRUE
+ brain.hard_cover = TRUE
+ brain.RegisterSignal(brain.tied_human, COMSIG_HUMAN_BULLET_ACT, TYPE_PROC_REF(/datum/human_ai_brain, on_shot_inside_cover))
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm b/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm
new file mode 100644
index 0000000000..3bf5805242
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm
@@ -0,0 +1,32 @@
+/datum/ongoing_action/throw_back_nade
+ name = "Throw Back Grenade"
+ var/obj/item/explosive/grenade/throwing
+ var/turf/target_turf
+
+/datum/ongoing_action/throw_back_nade/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ throwing = arguments[2]
+ target_turf = arguments[3]
+
+/datum/ongoing_action/throw_back_nade/Destroy(force, ...)
+ throwing = null
+ target_turf = null
+ return ..()
+
+/datum/ongoing_action/throw_back_nade/trigger_action()
+ if(QDELETED(throwing) || !isturf(throwing.loc))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(throwing, brain.tied_human) > 1)
+ if(!brain.move_to_next_turf(get_turf(throwing)))
+ return ONGOING_ACTION_COMPLETED
+
+ if(get_dist(throwing, brain.tied_human) > 1)
+ return ONGOING_ACTION_UNFINISHED
+ brain.end_gun_fire()
+ brain.clear_main_hand()
+ brain.tied_human.put_in_active_hand(throwing)
+ if(target_turf)
+ brain.tied_human.toggle_throw_mode(THROW_MODE_NORMAL)
+ brain.tied_human.throw_item(target_turf)
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm b/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm
new file mode 100644
index 0000000000..9cae1ad53d
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm
@@ -0,0 +1,31 @@
+/datum/ongoing_action/throw_grenade
+ name = "Throw Grenade"
+ var/obj/item/explosive/grenade/throwing
+ var/turf/target_turf
+ var/mid_throw = FALSE
+
+/datum/ongoing_action/throw_grenade/New(datum/human_ai_brain/brain, list/arguments)
+ . = ..()
+ throwing = arguments[2]
+ target_turf = arguments[3]
+
+/datum/ongoing_action/throw_grenade/Destroy(force, ...)
+ throwing = null
+ target_turf = null
+ return ..()
+
+/datum/ongoing_action/throw_grenade/trigger_action()
+ if(QDELETED(throwing) || !target_turf)
+ return ONGOING_ACTION_COMPLETED
+
+ if(mid_throw)
+ return ONGOING_ACTION_UNFINISHED_BLOCK
+
+ mid_throw = TRUE
+ brain.holster_primary()
+ brain.equip_item_from_equipment_map(HUMAN_AI_GRENADES, throwing)
+ sleep(brain.short_action_delay * brain.action_delay_mult)
+ if(QDELETED(throwing) || (throwing.loc != brain.tied_human))
+ return ONGOING_ACTION_COMPLETED
+ throwing.ai_use(brain.tied_human, target_turf)
+ return ONGOING_ACTION_COMPLETED
diff --git a/code/modules/mob/living/carbon/human/ai/ai_equipment.dm b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm
new file mode 100644
index 0000000000..eda03eda1a
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm
@@ -0,0 +1,74 @@
+/datum/equipment_preset/clf/soldier/ai
+ name = "CLF Soldier (AI)"
+
+/datum/equipment_preset/clf/soldier/ai/load_gear(mob/living/carbon/human/new_human)
+ var/obj/item/clothing/under/colonist/clf/jumpsuit = new()
+ var/obj/item/clothing/accessory/storage/webbing/W = new()
+ jumpsuit.attach_accessory(new_human, W)
+ new_human.equip_to_slot_or_del(jumpsuit, WEAR_BODY)
+ spawn_rebel_suit(new_human)
+ spawn_rebel_helmet(new_human)
+ spawn_rebel_shoes(new_human)
+ spawn_rebel_gloves(new_human)
+ new_human.equip_to_slot_or_del(new /obj/item/tool/crowbar, WEAR_IN_JACKET)
+ new_human.equip_to_slot_or_del(new /obj/item/device/flashlight(new_human), WEAR_L_STORE)
+ new_human.equip_to_slot_or_del(new /obj/item/storage/pouch/firstaid/ert(new_human), WEAR_R_STORE)
+
+ //new_human.equip_to_slot_or_del(new /obj/item/storage/belt/shotgun/full/random(new_human), WEAR_WAIST)
+ //new_human.equip_to_slot_or_del(new /obj/item/weapon/gun/shotgun/pump/dual_tube/cmb(new_human), WEAR_BACK)
+
+ //new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/general_belt(new_human), WEAR_WAIST)
+ //new_human.equip_to_slot_or_del(new /obj/item/ammo_magazine/flamer_tank(new_human), WEAR_IN_BELT)
+ //new_human.equip_to_slot_or_del(new /obj/item/weapon/gun/flamer(new_human), WEAR_BACK)
+
+ //new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/general_belt(new_human), WEAR_WAIST)
+ //new_human.equip_to_slot_or_del(new /obj/item/ammo_magazine/rocket/anti_tank(new_human), WEAR_IN_BELT)
+ //new_human.equip_to_slot_or_del(new /obj/item/weapon/gun/launcher/rocket/anti_tank(new_human), WEAR_R_HAND)
+
+ new_human.equip_to_slot_or_del(new /obj/item/storage/belt/marine(new_human), WEAR_WAIST)
+ if(prob(50))
+ spawn_rebel_smg(new_human)
+ else
+ spawn_rebel_rifle(new_human)
+
+ new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/CLF(new_human), WEAR_L_EAR)
+
+/datum/equipment_preset/clf/soldier/ai/load_preset(mob/living/carbon/human/new_human, randomise, count_participant, client/mob_client, show_job_gear)
+ . = ..()
+ new_human.ai_brain?.appraise_inventory()
+
+/datum/equipment_preset/clf/specialist/ai
+ name = "CLF Specialist (AI)"
+
+/datum/equipment_preset/clf/specialist/ai/load_gear(mob/living/carbon/human/new_human)
+
+ //jumpsuit and their webbing
+ var/obj/item/clothing/under/colonist/clf/CLF = new()
+ var/obj/item/clothing/accessory/storage/webbing/five_slots/W = new()
+ CLF.attach_accessory(new_human, W)
+ new_human.equip_to_slot_or_del(CLF, WEAR_BODY)
+ //clothing
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/suit/storage/militia(new_human), WEAR_JACKET)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/head/helmet/swat(new_human), WEAR_HEAD)
+ new_human.equip_to_slot_or_del(new /obj/item/attachable/bayonet/upp(new_human), WEAR_FACE)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/shoes/combat(new_human), WEAR_FEET)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/gloves/combat(new_human), WEAR_HANDS)
+ new_human.equip_to_slot_or_del(new /obj/item/storage/belt/gun/m4a3/vp70(new_human), WEAR_WAIST)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/glasses/night(new_human), WEAR_EYES)
+ new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/CLF/cct(new_human), WEAR_L_EAR)
+ new_human.equip_to_slot_or_del(new /obj/item/clothing/ears/earmuffs(new_human), WEAR_R_EAR)
+ //standard backpack stuff
+ new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/lightpack(new_human), WEAR_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/device/flashlight(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/storage/firstaid/regular/response(new_human), WEAR_IN_BACK)
+ new_human.equip_to_slot_or_del(new /obj/item/tool/crowbar(new_human), WEAR_IN_BACK)
+
+ //storage items
+ new_human.equip_to_slot_or_del(new /obj/item/storage/pouch/explosive/C4(new_human), WEAR_L_STORE)
+ new_human.equip_to_slot_or_del(new /obj/item/storage/pouch/firstaid/ert(new_human), WEAR_R_STORE)
+
+ new_human.put_in_active_hand(new /obj/item/weapon/gun/launcher/rocket/anti_tank/disposable(new_human))
+
+/datum/equipment_preset/clf/specialist/ai/load_preset(mob/living/carbon/human/new_human, randomise, count_participant, client/mob_client, show_job_gear)
+ . = ..()
+ new_human.ai_brain?.appraise_inventory()
diff --git a/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm
new file mode 100644
index 0000000000..043620bb20
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm
@@ -0,0 +1,146 @@
+/datum/human_ai_management_menu
+
+/datum/human_ai_management_menu/New()
+
+/datum/human_ai_management_menu/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "HumanAIManager")
+ ui.open()
+
+/datum/human_ai_management_menu/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/human_ai_management_menu/ui_data(mob/user)
+ var/list/data = list()
+
+ data["orders"] = list()
+ for(var/datum/ongoing_action/order as anything in SShuman_ai.existing_orders)
+ data["orders"] += list(list(
+ "name" = order.name,
+ "type" = order.type,
+ "data" = order.tgui_data(),
+ "ref" = REF(order),
+ )
+ )
+
+ data["ai_humans"] = list()
+ for(var/datum/human_ai_brain/brain as anything in GLOB.human_ai_brains)
+ if(!brain.tied_human || brain.tied_human.stat == DEAD)
+ continue
+
+ data["ai_humans"] += list(list(
+ "name" = brain.tied_human.real_name,
+ "health" = FLOOR((brain.tied_human.health / brain.tied_human.maxHealth * 100), 1),
+ "loc" = list(brain.tied_human.x, brain.tied_human.y, brain.tied_human.z),
+ "faction" = brain.tied_human.faction,
+ "ref" = REF(brain.tied_human),
+ "brain_ref" = REF(brain),
+ "in_combat" = brain.in_combat,
+ "squad_id" = brain.squad_id,
+ ))
+
+ data["squads"] = list()
+ for(var/datum/human_ai_squad/squad as anything in SShuman_ai.squads)
+ var/list/name_list = list()
+ for(var/datum/human_ai_brain/brain as anything in squad.ai_in_squad)
+ name_list += brain.tied_human?.real_name
+ data["squads"] += list(list(
+ "id" = squad.id,
+ "members" = english_list(name_list),
+ "order" = squad.assigned_order?.name,
+ "ref" = REF(squad),
+ "squad_leader" = squad.squad_leader?.tied_human?.real_name,
+ ))
+
+ return data
+
+/datum/human_ai_management_menu/ui_static_data(mob/user)
+ var/list/data = list()
+
+ return data
+
+/datum/human_ai_management_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("view_variables")
+ if(!params["ref"])
+ return
+
+ var/datum/gotten_ref = locate(params["ref"])
+ if(!istype(gotten_ref))
+ return
+
+ ui.user.client?.debug_variables(gotten_ref)
+ return TRUE
+
+ if("create_squad")
+ SShuman_ai.create_new_squad()
+ //update_static_data(usr, ui)
+ return TRUE
+
+ if("assign_to_squad")
+ if(!params["squad"] || !params["ai"])
+ return
+
+ var/datum/brain = locate(params["ai"])
+ brain:add_to_squad(params["squad"])
+ //update_static_data(usr, ui)
+ return TRUE
+
+ if("assign_order")
+ if(!params["squad"] || !params["order"])
+ return
+
+ var/datum/human_ai_squad/squad = SShuman_ai.get_squad("[params["squad"]]")
+ squad.set_order(locate(params["order"]))
+ //update_static_data(usr, ui)
+ return TRUE
+
+ if("assign_sl")
+ if(!params["squad"] || !params["ai"])
+ return
+
+ var/datum/brain = locate(params["ai"])
+ var/datum/human_ai_squad/squad = SShuman_ai.get_squad("[params["squad"]]")
+ squad.set_squad_leader(brain)
+ //update_static_data(usr, ui)
+ return TRUE
+
+ if("delete_object") // This UI is fully GM-only so I'm not worried about someone abusing this
+ if(!params["ref"])
+ return
+
+ var/datum/ref_to_del = locate(params["ref"])
+ qdel(ref_to_del)
+ return TRUE
+
+/client/proc/open_human_ai_management_panel()
+ set name = "Human AI Management Panel"
+ set category = "Game Master.HumanAI"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ if(human_ai_menu)
+ human_ai_menu.tgui_interact(mob)
+ return
+
+ human_ai_menu = new /datum/human_ai_management_menu(src)
+ human_ai_menu.tgui_interact(mob)
+
+/client/proc/create_human_ai()
+ set name = "Create Human AI"
+ set category = "Game Master.HumanAI"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/mob/living/carbon/human/ai/ai_human = new()
+ if(!cmd_admin_dress_human(ai_human))
+ qdel(ai_human)
+ return
+ ai_human.forceMove(get_turf(mob))
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm
new file mode 100644
index 0000000000..be9f24d320
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm
@@ -0,0 +1,323 @@
+GLOBAL_LIST_EMPTY(human_ai_brains)
+
+/datum/human_ai_brain
+ var/mob/living/carbon/human/tied_human
+
+ var/micro_action_delay = 0.2 SECONDS
+ var/short_action_delay = 0.5 SECONDS
+ var/medium_action_delay = 2 SECONDS
+ var/long_action_delay = 5 SECONDS
+ var/action_delay_mult = 1
+
+ /// Midway through doing something, shouldn't start something else
+ var/currently_busy = FALSE
+
+ /// If TRUE, shoots until the target is dead. Else, stops when downed
+ var/shoot_to_kill = TRUE
+
+ /// Distance for view checks
+ var/view_distance = 7
+
+ /// How far the AI will chuck a nade if they can't find an enemy to throw it back at
+ var/nade_direction_throw = 4
+
+ /// How long we will ignore max fire distance after shot from afar
+ var/return_fire_duration = 5 SECONDS
+
+ /// List of current action datums
+ var/list/ongoing_actions = list()
+
+ /// List of semi-permanent "order" action datums. These do not expire
+ //var/list/ongoing_orders = list()
+ var/datum/ongoing_action/order/ongoing_order
+
+ var/list/detection_turfs = list()
+
+ var/in_combat = FALSE
+ var/combat_decay_time = 30 SECONDS
+ var/squad_covering = FALSE
+
+ var/list/friendly_factions = list()
+ var/list/neutral_factions = list()
+ var/previous_faction
+
+/datum/human_ai_brain/New(mob/living/carbon/human/tied_human)
+ . = ..()
+ src.tied_human = tied_human
+ RegisterSignal(tied_human, COMSIG_PARENT_QDELETING, PROC_REF(on_human_delete))
+ RegisterSignal(tied_human, COMSIG_HUMAN_EQUIPPED_ITEM, PROC_REF(on_item_equip))
+ RegisterSignal(tied_human, COMSIG_HUMAN_UNEQUIPPED_ITEM, PROC_REF(on_item_unequip))
+ RegisterSignal(tied_human, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_item_pickup))
+ RegisterSignal(tied_human, COMSIG_MOB_DROP_ITEM, PROC_REF(on_item_drop))
+ RegisterSignal(tied_human, COMSIG_MOVABLE_MOVED, PROC_REF(on_move))
+ RegisterSignal(tied_human, COMSIG_HUMAN_BULLET_ACT, PROC_REF(on_shot))
+ if(!length(all_medical_items))
+ all_medical_items = brute_heal_items + burn_heal_items + tox_heal_items + oxy_heal_items + bleed_heal_items + bonebreak_heal_items + painkiller_items
+ GLOB.human_ai_brains += src
+ setup_detection_radius()
+
+/datum/human_ai_brain/Destroy(force, ...)
+ tied_human = null
+ set_primary_weapon(null)
+ current_target = null
+ target_floor = null
+ ongoing_order = null
+ GLOB.human_ai_brains -= src
+ gun_data = null
+ //primary_melee = null
+ return ..()
+
+/datum/human_ai_brain/process(delta_time)
+ if(tied_human.is_mob_incapacitated())
+ end_gun_fire()
+ return
+
+ var/order_return
+ if(can_process_order())
+ order_return = ongoing_order.trigger_action(src)
+
+ if(!in_combat && !currently_busy && (order_return != ONGOING_ACTION_UNFINISHED))
+ approach_squad_leader()
+
+ for(var/datum/ongoing_action/action as anything in ongoing_actions)
+ var/retval = action.trigger_action()
+ switch(retval)
+ if(ONGOING_ACTION_UNFINISHED_BLOCK)
+ return
+ if(ONGOING_ACTION_COMPLETED)
+ ongoing_actions -= action
+ qdel(action)
+ currently_busy = FALSE
+
+ var/list/things_around = range(2, tied_human)
+ // Might be wise to move these off tick and instead make them signal-based
+ nade_throwback(things_around)
+ item_search(things_around)
+
+ if(!currently_busy && should_reload_primary())
+ currently_busy = TRUE
+ reload_primary()
+
+ if(primary_weapon && current_target)
+ if(!has_ongoing_action(AI_ACTION_APPROACH) && !has_ongoing_action(AI_ACTION_RETREAT))
+ var/target_futile = current_target.is_mob_incapacitated()
+ var/distance = get_dist(tied_human, current_target)
+ if(distance > gun_data.optimal_range || target_futile)
+ if(!in_cover)
+ var/walk_distance = target_futile ? gun_data.minimum_range : gun_data.optimal_range
+ ADD_ONGOING_ACTION(src, AI_ACTION_APPROACH, current_target, walk_distance)
+
+ else if(distance < gun_data.optimal_range)
+ var/walk_distance = in_cover ? gun_data.minimum_range : gun_data.optimal_range
+ ADD_ONGOING_ACTION(src, AI_ACTION_RETREAT, current_target, walk_distance)
+
+ if(!currently_busy && !currently_firing && COOLDOWN_FINISHED(src, fire_overload_cooldown))
+ currently_busy = TRUE
+ attack_target()
+
+ if(!currently_busy && !in_combat && healing_start_check())
+ currently_busy = TRUE
+ start_healing()
+
+ if(!currently_busy && !current_target && primary_weapon)
+ current_target = get_target(view_distance)
+ RegisterSignal(current_target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_delete))
+
+/datum/human_ai_brain/proc/on_human_delete(datum/source, force)
+ SIGNAL_HANDLER
+ tied_human = null
+
+/datum/human_ai_brain/proc/on_target_delete(datum/source, force)
+ SIGNAL_HANDLER
+ current_target = null
+
+/datum/human_ai_brain/proc/ensure_primary_hand(obj/item/held_item)
+ if(tied_human.get_inactive_hand() == held_item)
+ tied_human.swap_hand()
+
+/datum/human_ai_brain/proc/nade_throwback(list/things_around)
+ if(has_ongoing_action(AI_ACTION_THROWBACK))
+ return
+
+ var/turf/place_to_throw
+ var/obj/item/explosive/grenade/throw_nade
+ for(var/obj/item/explosive/grenade/nade in things_around)
+ if(!nade.active || nade.throwing)
+ continue
+
+ throw_nade = nade
+
+ var/list/possible_targets = list()
+ for(var/mob/living/carbon/target in range(view_distance, tied_human))
+ if(can_target(target))
+ possible_targets += target
+
+ if(length(possible_targets))
+ var/mob/living/carbon/chosen_target = pick(possible_targets)
+ var/list/turf_pathfind_list = AStar(get_turf(tied_human), get_turf(chosen_target), /turf/proc/AdjacentTurfs, /turf/proc/Distance, view_distance)
+ for(var/i = length(turf_pathfind_list); i >= 4; i--) // We cut it off at 4 because we want to avoid most of the nade blast
+ var/turf/target_turf = turf_pathfind_list[i]
+ if(target_turf in viewers(world.view, tied_human))
+ place_to_throw = target_turf
+ goto throw_back_nade
+ place_to_throw = turf_pathfind_list[4]
+ goto throw_back_nade
+
+ // We haven't found an enemy in range that we can throw to, so we'll just throw in a direction that doesn't have friendlies
+ var/list/directions = list(
+ locate(tied_human.x, tied_human.y + nade_direction_throw, tied_human.z),
+ locate(tied_human.x + nade_direction_throw, tied_human.y, tied_human.z),
+ locate(tied_human.x, tied_human.y - nade_direction_throw, tied_human.z),
+ locate(tied_human.x - nade_direction_throw, tied_human.y, tied_human.z),
+ )
+ location_loop:
+ for(var/turf/location as anything in directions)
+ if(location)
+ var/list/turf/path = get_line(tied_human, location, include_start_atom = FALSE)
+ for(var/turf/possible_blocker as anything in path)
+ if(possible_blocker.density)
+ continue location_loop
+
+ var/has_friendly = FALSE
+ for(var/mob/possible_friendly in range(3, location))
+ if(!can_target(possible_friendly))
+ has_friendly = TRUE
+ break
+
+ if(!has_friendly)
+ place_to_throw = location
+ goto throw_back_nade
+
+ // There's friendlies all around us, apparently. Just uh. Die ig.
+ break
+
+ if(!throw_nade)
+ return
+
+ throw_back_nade:
+ ADD_ONGOING_ACTION(src, AI_ACTION_THROWBACK, throw_nade, place_to_throw)
+
+/// Use ADD_ONGOING_ACTION() macro instead of calling this directly
+/datum/human_ai_brain/proc/_add_ongoing_action(datum/ongoing_action/path, ...)
+ if(path::order)
+ stack_trace("Order of [path] was attempted to be added as an action.")
+ ongoing_actions += new path(src, args)
+
+/datum/human_ai_brain/proc/has_ongoing_action(path)
+ if(!ispath(path))
+ return FALSE
+
+ for(var/datum/ongoing_action/action as anything in ongoing_actions)
+ if(istype(action, path))
+ return TRUE
+
+ return FALSE
+
+/datum/human_ai_brain/proc/set_ongoing_order(datum/ongoing_action/ref)
+ if(!ref)
+ return
+
+ if(!ref.order)
+ stack_trace("Action of [ref.type] was attempted to be added as an order.")
+ ongoing_order = ref
+
+/datum/human_ai_brain/proc/has_ongoing_order(path)
+ return istype(ongoing_order, path)
+
+/datum/human_ai_brain/proc/clear_ongoing_order()
+ ongoing_order = null
+
+/// Returns TRUE if the target is friendly/neutral to us
+/datum/human_ai_brain/proc/faction_check(mob/target)
+ if(!target)
+ return FALSE
+
+ if(target.faction == tied_human.faction)
+ return TRUE
+
+ if(target.faction in friendly_factions)
+ return TRUE
+
+ if(target.faction in neutral_factions)
+ return TRUE
+
+ return FALSE
+
+/datum/human_ai_brain/proc/setup_detection_radius()
+ if(length(detection_turfs))
+ clear_detection_radius()
+
+ for(var/turf/open/floor in range(1, tied_human))
+ RegisterSignal(floor, COMSIG_TURF_ENTERED, PROC_REF(on_detection_turf_enter))
+ detection_turfs += floor
+
+/datum/human_ai_brain/proc/clear_detection_radius()
+ for(var/turf/open/floor as anything in detection_turfs)
+ UnregisterSignal(floor, COMSIG_TURF_ENTERED)
+
+ detection_turfs.Cut()
+
+/datum/human_ai_brain/proc/on_detection_turf_enter(datum/source, atom/movable/entering)
+ SIGNAL_HANDLER
+ if(entering == tied_human)
+ return
+
+ if(istype(entering, /obj/projectile))
+ var/obj/projectile/bullet = entering
+ if(ismob(bullet.firer))
+ if(!in_cover && !squad_covering && !faction_check(bullet.firer)) // If it's our own bullets, we don't need to be alarmed
+ enter_combat()
+ if(!primary_weapon)
+ locate_cover(bullet, bullet.dir) // Zonenote: cover is fucked and needs work, so it's limited to only unarmed combatants
+
+/datum/human_ai_brain/proc/on_move(atom/movable/mover, atom/oldloc, direction)
+ setup_detection_radius()
+
+/datum/human_ai_brain/proc/enter_combat()
+ SIGNAL_HANDLER
+
+ if(!in_combat)
+ say_in_combat_line()
+ in_combat = TRUE
+ addtimer(CALLBACK(src, PROC_REF(exit_combat)), combat_decay_time, TIMER_UNIQUE | TIMER_OVERRIDE)
+
+/datum/human_ai_brain/proc/exit_combat()
+ if(in_combat)
+ say_exit_combat_line()
+ holster_primary()
+ in_combat = FALSE
+
+/datum/human_ai_brain/proc/can_process_order()
+ if(!ongoing_order)
+ return FALSE
+
+ if(!ongoing_order.can_perform_in_combat && in_combat)
+ return FALSE
+
+ return TRUE
+
+/datum/human_ai_brain/proc/on_shot(datum/source, damage_result, ammo_flags, obj/projectile/bullet)
+ SIGNAL_HANDLER
+
+ var/mob/firer = bullet.firer
+ if(firer?.faction in neutral_factions)
+ on_neutral_faction_betray(firer.faction)
+
+ if(primary_weapon?.ammo.max_range <= get_dist(tied_human, firer))
+ COOLDOWN_START(src, return_fire, return_fire_duration)
+
+ if(!faction_check(firer))
+ if(current_target != firer)
+ end_gun_fire()
+ current_target = firer
+
+/datum/human_ai_brain/proc/on_neutral_faction_betray(faction)
+ if(!tied_human.faction)
+ return
+
+ var/datum/human_ai_faction/our_faction = SShuman_ai.human_ai_factions[tied_human.faction]
+ if(!our_faction)
+ return
+
+ our_faction.remove_neutral_faction(faction)
+ our_faction.reapply_faction_data()
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm
new file mode 100644
index 0000000000..6d6c0e63a0
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm
@@ -0,0 +1,71 @@
+/datum/human_ai_brain
+ var/list/in_combat_lines = list(
+ "Taking fire!",
+ "Getting shot at!",
+ "Engaging hostiles!",
+ "Contact!",
+ "Contact contact!",
+ "We've got hostiles!",
+ "Take 'em down!",
+ "Hostile spotted, engaging!",
+ "Enemy hostiles here!",
+ "Being fired upon!",
+ "Blast 'em!",
+ )
+
+ var/list/exit_combat_lines = list(
+ "No more contacts.",
+ "Don't see 'em.",
+ "Going back to regular duties.",
+ "Nothin' left.",
+ "Can't find 'em.",
+ "No hostiles, returning to duties.",
+ )
+
+ var/list/squad_member_death_lines = list(
+ "Fuck! Man down!",
+ "We lost one!",
+ "Man down!",
+ "We're taking losses here!",
+ "Goddamn it.",
+ "Fuck!",
+ "Shit, our squad's down a man!",
+ "Squad integrity's failing!",
+ )
+
+ var/list/grenade_thrown_lines = list(
+ "Nade out!",
+ "Tossing a grenade!",
+ "Smoking 'em out!",
+ "Throwing a nade!",
+ "Grenade out!",
+ "Tossing a nade!",
+ "Pineapple out!",
+ "Fragging 'em!",
+ )
+
+ var/in_combat_line_chance = 40
+ var/exit_combat_line_chance = 40
+ var/squad_member_death_line_chance = 20
+ var/grenade_thrown_line_chance = 60
+
+
+/datum/human_ai_brain/proc/say_in_combat_line(chance = in_combat_line_chance)
+ if(!length(in_combat_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT))
+ return
+ tied_human.say(pick(in_combat_lines))
+
+/datum/human_ai_brain/proc/say_exit_combat_line(chance = exit_combat_line_chance)
+ if(!length(exit_combat_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT))
+ return
+ tied_human.say(pick(exit_combat_lines))
+
+/datum/human_ai_brain/proc/on_squad_member_death(mob/living/carbon/human/dead_member)
+ if(!length(squad_member_death_lines) || !prob(squad_member_death_line_chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT))
+ return
+ tied_human.say(pick(squad_member_death_lines))
+
+/datum/human_ai_brain/proc/say_grenade_thrown_line(chance = grenade_thrown_line_chance)
+ if(!length(grenade_thrown_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT))
+ return
+ tied_human.say(pick(grenade_thrown_lines))
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm
new file mode 100644
index 0000000000..9cd75406f6
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm
@@ -0,0 +1,184 @@
+/datum/human_ai_brain
+ /// If TRUE, AI is currently in some form of cover
+ var/in_cover = FALSE
+
+ /// If TRUE, the AI is in hard cover, meaning that it's solid and can't be seen through
+ var/hard_cover = FALSE
+
+ COOLDOWN_DECLARE(cover_search_cooldown)
+
+/datum/human_ai_brain/proc/on_cover_destroyed(atom/source, force)
+ UnregisterSignal(source, COMSIG_PARENT_QDELETING)
+ in_cover = FALSE
+ hard_cover = FALSE
+
+/datum/human_ai_brain/proc/on_shot_inside_cover(datum/source, damage_result, ammo_flags, obj/projectile/bullet)
+ if(faction_check(bullet.firer))
+ return // FF
+
+ // Cover isn't working. Charge!
+ in_cover = FALSE
+ hard_cover = FALSE
+ UnregisterSignal(tied_human, COMSIG_HUMAN_BULLET_ACT)
+
+
+/datum/human_ai_brain/proc/locate_cover(obj/projectile/bullet, projectile_dir)
+ if(!COOLDOWN_FINISHED(src, cover_search_cooldown))
+ return
+ COOLDOWN_START(src, cover_search_cooldown, 15 SECONDS)
+ var/list/turf_dict = list()
+ if(!recursive_turf_cover_scan(get_turf(tied_human), turf_dict, reverse_direction(projectile_dir)))
+ outside_cover_processing(bullet, projectile_dir, FALSE)
+ squad_cover_processing(FALSE, turf_dict, bullet, projectile_dir)
+ else
+ inside_cover_processing(turf_dict, FALSE)
+ squad_cover_processing(TRUE, turf_dict, bullet, projectile_dir)
+
+
+/datum/human_ai_brain/proc/squad_cover_processing(inside = TRUE, list/turf_dict, obj/projectile/bullet, projectile_dir)
+ if(!squad_id)
+ return
+
+ var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[squad_id]"]
+ if(!squad)
+ return
+
+ for(var/datum/human_ai_brain/brain as anything in squad.ai_in_squad)
+ if(brain == src)
+ continue
+
+ if(get_dist(tied_human, brain.tied_human) > view_distance)
+ continue
+
+ if(brain.tied_human.is_mob_incapacitated())
+ continue
+
+ brain.squad_covering = TRUE
+ COOLDOWN_START(brain, cover_search_cooldown, 15 SECONDS)
+
+ if(inside)
+ brain.inside_cover_processing(turf_dict, TRUE)
+ else
+ brain.outside_cover_processing(bullet, projectile_dir, TRUE)
+
+/datum/human_ai_brain/proc/recursive_turf_cover_scan(turf/scan_turf, list/turf_dict, cover_dir)
+ if(length(turf_dict) > 128)
+ return FALSE // abort if the room is too large
+ //if(istype(scan_turf, /turf/closed))
+ // return TRUE // abort if we're a wall
+ if(scan_turf in turf_dict)
+ return TRUE // abort if we've already been scanned
+ turf_dict[scan_turf] = 0
+ var/obj/structure/barricade/cade = locate() in scan_turf.contents
+ if(cade?.dir in get_related_directions(cover_dir))
+ turf_dict[scan_turf] += 5
+ var/obj/item/explosive/mine/mine = locate() in scan_turf.contents
+ if(mine)
+ if(!faction_check(mine.iff_signal))
+ turf_dict[scan_turf] -= 50
+ else
+ turf_dict[scan_turf] -= 2 // even if it's our mine, we don't really want to stand on it
+ for(var/obj/thing in scan_turf.contents)
+ if(thing.density && !istype(thing, /obj/structure/barricade))
+ turf_dict[scan_turf] -= 1000 // If it has something dense on it, don't bother
+ for(var/cardinal in GLOB.cardinals)
+ var/turf/nearby_turf = get_step(scan_turf, cardinal)
+ if(!nearby_turf)
+ continue
+ var/obj/structure/reagent_dispensers/fueltank/tank = locate() in nearby_turf.contents
+ if(tank)
+ turf_dict[scan_turf] -= 10 // ideally not near any highly explosive fuel tanks if we can help it
+ if(istype(nearby_turf, /turf/closed))
+ turf_dict[scan_turf] += 2 // Near a wall is a bit safer
+ continue
+ if(!recursive_turf_cover_scan(nearby_turf, turf_dict, cover_dir))
+ return FALSE
+ return TRUE
+
+
+/datum/human_ai_brain/proc/outside_cover_processing(obj/projectile/bullet, projectile_dir, from_squad = FALSE)
+ var/shortest_cover_dist = 50
+ var/turf/shortest_cover_turf
+ var/atom/cover_atom
+ var/list/cardinal_bullet_dirs = make_dir_cardinal(projectile_dir)
+ var/list/inverse_cardinal_bullet_dirs = list()
+ for(var/dir in cardinal_bullet_dirs)
+ inverse_cardinal_bullet_dirs += reverse_direction(dir)
+ var/list/view_contents = view(7, tied_human)
+ cade_loop:
+ for(var/obj/structure/barricade/cade in view_contents)
+ for(var/atom/thing in cade.loc)
+ if(thing.density && (cade != thing))
+ continue cade_loop
+ if(cade.dir in inverse_cardinal_bullet_dirs)
+ var/dist = get_dist(tied_human, cade)
+ if(dist < shortest_cover_dist)
+ shortest_cover_dist = dist
+ shortest_cover_turf = get_turf(cade)
+ cover_atom = cade
+ if(shortest_cover_turf)
+ ADD_ONGOING_ACTION(src, AI_ACTION_COVER, shortest_cover_turf, cover_atom, FALSE)
+ if(!from_squad)
+ squad_cover_processing(FALSE, view_contents - shortest_cover_turf)
+ else
+ squad_covering = FALSE
+ return
+ wall_loop:
+ for(var/turf/closed/wall in view_contents)
+ for(var/dir in cardinal_bullet_dirs)
+ var/turf/open/maybe_cover = get_step(wall, dir)
+ if(!istype(maybe_cover) || !(tied_human in viewers(world.view, maybe_cover)))
+ continue
+ for(var/atom/thing in maybe_cover)
+ if(thing.density)
+ continue wall_loop
+ if(bullet.firer in viewers(world.view, maybe_cover))
+ continue
+ var/dist = get_dist(tied_human, maybe_cover)
+ if(dist < shortest_cover_dist)
+ shortest_cover_dist = dist
+ shortest_cover_turf = maybe_cover
+ cover_atom = wall
+ if(shortest_cover_turf)
+ ADD_ONGOING_ACTION(src, AI_ACTION_COVER, shortest_cover_turf, cover_atom, TRUE)
+ if(!from_squad)
+ squad_cover_processing(FALSE, view_contents - shortest_cover_turf)
+
+ squad_covering = FALSE
+
+
+
+/datum/human_ai_brain/proc/inside_cover_processing(list/turf_dict, from_squad = FALSE)
+ var/highest_cover_value = turf_dict[turf_dict[1]]
+ var/turf/highest_cover_turf
+ for(var/turf/turf as anything in turf_dict)
+#ifdef TESTING
+ turf.maptext = "
[turf_dict[turf]]
"
+#endif
+ if(turf_dict[turf] > highest_cover_value)
+ highest_cover_value = turf_dict[turf]
+ highest_cover_turf = turf
+ if(!highest_cover_turf)
+ squad_covering = FALSE
+ return // damn
+#ifdef TESTING
+ to_chat(world, "highest_cover_value: [highest_cover_value], turf coords: [highest_cover_turf.x], [highest_cover_turf.y], [highest_cover_turf.z]")
+ addtimer(CALLBACK(src, PROC_REF(clear_cover_value_debug), turf_dict), 60 SECONDS)
+ ADD_ONGOING_ACTION(src, AI_ACTION_COVER_I, highest_cover_turf)
+ if(!from_squad)
+ squad_cover_processing(TRUE, turf_dict - highest_cover_turf)
+ else
+ squad_covering = FALSE
+
+/datum/human_ai_brain/proc/clear_cover_value_debug(list/turf_list)
+ for(var/turf/T as anything in turf_list)
+ T.maptext = null
+
+#else
+ ADD_ONGOING_ACTION(src, AI_ACTION_COVER_I, highest_cover_turf)
+ if(!from_squad)
+ squad_cover_processing(TRUE, turf_dict - highest_cover_turf)
+ else
+ squad_covering = FALSE
+#endif
+
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm
new file mode 100644
index 0000000000..ff7fc67efb
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm
@@ -0,0 +1,177 @@
+/datum/human_ai_faction
+ var/faction = FACTION_NEUTRAL
+ VAR_PROTECTED/shoot_to_kill = TRUE
+
+ VAR_PROTECTED/list/in_combat_lines = list()
+ VAR_PROTECTED/list/exit_combat_lines = list()
+ VAR_PROTECTED/list/squad_member_death_lines = list()
+ VAR_PROTECTED/list/grenade_thrown_lines = list()
+
+ VAR_PROTECTED/list/friendly_factions = list()
+ VAR_PROTECTED/list/neutral_factions = list()
+
+/datum/human_ai_faction/proc/apply_faction_data(datum/human_ai_brain/brain)
+ if(length(in_combat_lines))
+ brain.in_combat_lines = in_combat_lines
+
+ if(length(exit_combat_lines))
+ brain.exit_combat_lines = exit_combat_lines
+
+ if(length(squad_member_death_lines))
+ brain.squad_member_death_lines = squad_member_death_lines
+
+ if(length(grenade_thrown_lines))
+ brain.grenade_thrown_lines = grenade_thrown_lines
+
+ brain.shoot_to_kill = shoot_to_kill
+ brain.friendly_factions = friendly_factions
+ brain.neutral_factions = neutral_factions
+
+/datum/human_ai_faction/proc/reapply_faction_data()
+ for(var/datum/human_ai_brain/brain in GLOB.human_ai_brains)
+ if(brain.tied_human?.faction == faction)
+ apply_faction_data(brain)
+
+/datum/human_ai_faction/proc/add_friendly_faction(faction)
+ if(faction in friendly_factions)
+ return
+ friendly_factions += faction
+ if(faction in neutral_factions)
+ neutral_factions -= faction
+ reapply_faction_data()
+
+/datum/human_ai_faction/proc/add_neutral_faction(faction)
+ if(faction in neutral_factions)
+ return
+ neutral_factions += faction
+ if(faction in friendly_factions)
+ friendly_factions -= faction
+ reapply_faction_data()
+
+/datum/human_ai_faction/proc/remove_friendly_faction(faction)
+ if(!(faction in friendly_factions))
+ return
+ friendly_factions -= faction
+ reapply_faction_data()
+
+/datum/human_ai_faction/proc/remove_neutral_faction(faction)
+ if(!(faction in neutral_factions))
+ return
+ neutral_factions -= faction
+ reapply_faction_data()
+
+/datum/human_ai_faction/proc/get_friendly_factions()
+ return friendly_factions
+
+/datum/human_ai_faction/proc/get_neutral_factions()
+ return neutral_factions
+
+/datum/human_ai_faction/proc/set_shoot_to_kill(new_kill = TRUE)
+ shoot_to_kill = new_kill
+ reapply_faction_data()
+
+/datum/human_ai_faction/proc/get_shoot_to_kill()
+ return shoot_to_kill
+
+/datum/human_ai_faction/clf
+ faction = FACTION_CLF
+ friendly_factions = list(
+ FACTION_COLONIST,
+ )
+
+
+/datum/human_ai_faction/uscm
+ faction = FACTION_MARINE
+ friendly_factions = list(
+ FACTION_COLONIST,
+ FACTION_TWE,
+ FACTION_WY,
+ )
+ neutral_factions = list(
+ FACTION_FREELANCER,
+ FACTION_CONTRACTOR,
+ FACTION_UPP,
+ FACTION_MERCENARY,
+ )
+
+/datum/human_ai_faction/upp
+ faction = FACTION_UPP
+ friendly_factions = list(
+ FACTION_COLONIST,
+ )
+ neutral_factions = list(
+ FACTION_FREELANCER,
+ FACTION_CONTRACTOR,
+ FACTION_MARINE,
+ FACTION_MERCENARY,
+ FACTION_TWE,
+ )
+ in_combat_lines = list( // zonenote: tweak these. They're entirely the stereotype of "communist russkie" when we can do better than that. also languages
+ "For the UPP!",
+ "Die, you animal!",
+ "Capitalist dog!",
+ "Shoot them!",
+ "For glorious Union!",
+ "Attacking!",
+ "We will bury them!",
+ "Uraaaa!!",
+ "URAAA!!",
+ "To your last breath!",
+ "You're worth nothing!",
+ "This is the end, for you!",
+ "Die!",
+ "*warcry",
+ )
+ exit_combat_lines = list(
+ "I need a break...",
+ "Phew, that was tough work.",
+ "I think we can stop shooting now?",
+ "One step closer to victory!",
+ "Finally, break time.",
+ )
+ squad_member_death_lines = list(
+ "Man down!",
+ "Comrade!!",
+ "Get together!",
+ "Damn!",
+ "Taking hits!",
+ )
+
+
+/datum/human_ai_faction/wy
+ faction = FACTION_WY
+ friendly_factions = list(
+ FACTION_COLONIST,
+ FACTION_TWE,
+ FACTION_MARINE,
+ )
+ neutral_factions = list(
+ FACTION_FREELANCER,
+ FACTION_CONTRACTOR,
+ FACTION_MERCENARY,
+ )
+
+/datum/human_ai_faction/wy_deathsquad
+ faction = FACTION_WY_DEATHSQUAD
+ friendly_factions = list(
+ FACTION_WY,
+ )
+ in_combat_lines = list(
+ "Visual confirmed, engaging.",
+ "Engaging hostile.",
+ "Eliminating hostile.",
+ "Engaging.",
+ "Contact.",
+ "Viscon, proceeding."
+ )
+ exit_combat_lines = list(
+ "Hostilities ceased.",
+ "Ceasing engagement."
+ )
+ squad_member_death_lines = list(
+ "Allied unit disabled.",
+ "Friendly unit decomissioned.",
+ "Allied unit decomissioned.",
+ "Friendly unit disabled."
+ )
+ grenade_thrown_lines = list() // Wouldn't need to call this out
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm
new file mode 100644
index 0000000000..6cdfd59759
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm
@@ -0,0 +1,94 @@
+/datum/human_ai_brain
+ var/obj/item/weapon/gun/primary_weapon
+ //var/obj/item/weapon/primary_melee
+ /// Currently firing a gun
+ var/currently_firing = FALSE
+ /// Appraisal datum
+ var/datum/firearm_appraisal/gun_data
+ /// How many rounds fired in this burst
+ var/rounds_burst_fired = 0
+ /// If we've tried to reload (and failed) with our current inventory
+ var/tried_reload = FALSE
+ /// Cooldown for if we've fired too many rounds in a burst (for recoil)
+ COOLDOWN_DECLARE(fire_overload_cooldown)
+
+/datum/human_ai_brain/proc/unholster_primary()
+ if(tied_human.l_hand == primary_weapon || tied_human.r_hand == primary_weapon)
+ return
+
+ if(tied_human.get_active_hand())
+ tied_human.drop_held_item(tied_human.get_active_hand())
+
+ tied_human.u_equip(primary_weapon)
+ tied_human.put_in_active_hand(primary_weapon)
+ sleep(max(primary_weapon.wield_delay, short_action_delay * action_delay_mult))
+ primary_weapon.wield(tied_human)
+
+/datum/human_ai_brain/proc/holster_primary()
+ if(tied_human.s_store || (tied_human.l_hand != primary_weapon && tied_human.r_hand != primary_weapon))
+ return
+
+ if(currently_firing)
+ end_gun_fire()
+ tied_human.equip_to_slot(primary_weapon, WEAR_J_STORE)
+
+/datum/human_ai_brain/proc/reload_primary()
+ set waitfor = FALSE
+
+ if(!primary_weapon || tried_reload)
+ currently_busy = FALSE
+ return
+
+ currently_busy = TRUE
+
+ var/obj/item/ammo_magazine/mag = primary_ammo_search()
+ if(!mag)
+ tried_reload = TRUE
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] tried to reload without ammo.")
+#endif
+ currently_busy = FALSE
+ return //soz
+ ensure_primary_hand(primary_weapon)
+ primary_weapon.unwield(tied_human)
+ sleep(short_action_delay * action_delay_mult)
+ if(!(primary_weapon.flags_gun_features & GUN_INTERNAL_MAG))
+ primary_weapon.unload(tied_human, FALSE, TRUE, FALSE)
+ sleep(short_action_delay * action_delay_mult)
+ tied_human.swap_hand()
+ equip_item_from_equipment_map(HUMAN_AI_AMMUNITION, mag)
+ if(istype(mag, /obj/item/ammo_magazine/handful))
+ for(var/i in 1 to mag.current_rounds)
+ primary_weapon.attackby(mag, tied_human)
+ sleep(micro_action_delay * action_delay_mult)
+ if(!QDELETED(mag) && (mag.current_rounds > 0))
+ var/storage_slot = storage_has_room(mag)
+ if(storage_slot)
+ store_item(mag, storage_slot)
+ else
+ tied_human.drop_held_item(mag)
+ else
+ primary_weapon.attackby(mag, tied_human)
+ tied_human.swap_hand()
+ sleep(short_action_delay * action_delay_mult)
+ primary_weapon.wield(tied_human)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] reloaded [primary_weapon].")
+#endif
+ currently_busy = FALSE
+
+/datum/human_ai_brain/proc/primary_ammo_search()
+ for(var/obj/item/ammo_magazine/mag as anything in equipment_map[HUMAN_AI_AMMUNITION])
+ if(istype(primary_weapon, mag.gun_type) && mag.ai_can_use(tied_human))
+ return mag
+
+/datum/human_ai_brain/proc/should_reload_primary()
+ if(!primary_weapon || gun_data?.disposable)
+ return FALSE
+
+ if(!primary_weapon.current_mag)
+ return TRUE
+
+ if(primary_weapon.current_mag.current_rounds > 0 || primary_weapon?.in_chamber)
+ return FALSE
+ return TRUE
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm
new file mode 100644
index 0000000000..e8c1c3e4f7
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm
@@ -0,0 +1,238 @@
+/datum/human_ai_brain
+ var/static/list/brute_heal_items = list(
+ /obj/item/stack/medical/advanced/bruise_pack,
+ /obj/item/reagent_container/hypospray/autoinjector/bicaridine,
+ /obj/item/reagent_container/hypospray/autoinjector/tricord,
+ /obj/item/storage/pill_bottle/bicaridine,
+ /obj/item/storage/pill_bottle/merabica,
+ /obj/item/storage/pill_bottle/tricord,
+ )
+
+ var/static/list/burn_heal_items = list(
+ /obj/item/stack/medical/advanced/ointment,
+ /obj/item/reagent_container/hypospray/autoinjector/kelotane,
+ /obj/item/reagent_container/hypospray/autoinjector/tricord,
+ /obj/item/storage/pill_bottle/kelotane,
+ /obj/item/storage/pill_bottle/keloderm,
+ /obj/item/storage/pill_bottle/tricord,
+ )
+
+ var/static/list/tox_heal_items = list(
+ /obj/item/reagent_container/hypospray/autoinjector/dylovene,
+ /obj/item/reagent_container/hypospray/autoinjector/tricord,
+ /obj/item/storage/pill_bottle/antitox,
+ /obj/item/storage/pill_bottle/tricord,
+ )
+
+ var/static/list/oxy_heal_items = list(
+ /obj/item/reagent_container/hypospray/autoinjector/dexalinp,
+ /obj/item/reagent_container/hypospray/autoinjector/tricord,
+ /obj/item/storage/pill_bottle/dexalin,
+ /obj/item/storage/pill_bottle/dexalinplus,
+ /obj/item/storage/pill_bottle/tricord,
+ )
+
+ var/static/list/bleed_heal_items = list(
+ /obj/item/stack/medical/advanced/bruise_pack,
+ /obj/item/stack/medical/bruise_pack,
+ )
+
+ var/static/list/bonebreak_heal_items = list(
+ /obj/item/stack/medical/splint,
+ )
+
+ var/static/list/painkiller_items = list(
+ /obj/item/reagent_container/hypospray/autoinjector/tramadol,
+ /obj/item/reagent_container/hypospray/autoinjector/oxycodone,
+ /obj/item/storage/pill_bottle/tramadol,
+ )
+
+ /// Populated in New()
+ var/static/list/all_medical_items = list()
+
+ /// At what percentage of max HP to start searching for medical treatment
+ var/healing_start_threshold = 0.7
+ /// Requires this much damage of one type to consider it a problem
+ var/damage_problem_threshold = 5
+ /// Pain percentage (out of 100) for the AI to consider using painkillers
+ var/pain_percentage_threshold = 1
+
+ /// Cooldown on using pills to avoid OD. This isn't the best solution as it prevents the AI from using more than 1 pill of any kind every 10s, but it'll work for now
+ COOLDOWN_DECLARE(pill_use_cooldown)
+
+/datum/human_ai_brain/proc/healing_start_check()
+ return ((tied_human.health / tied_human.maxHealth) <= healing_start_threshold) || tied_human.is_bleeding() || tied_human.has_broken_limbs()
+
+/datum/human_ai_brain/proc/start_healing()
+ set waitfor = FALSE
+
+ currently_busy = TRUE
+ // Prioritize brute, then bleed, then broken bones, then burn, then pain, then tox, then oxy.
+ if(tied_human.getBruteLoss() > damage_problem_threshold)
+ var/obj/item/brute_heal
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, brute_heal_items) && heal_item.ai_can_use(tied_human))
+ brute_heal = heal_item
+ break
+
+ if(!brute_heal)
+ goto bleed
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, brute_heal)
+ sleep(short_action_delay)
+ brute_heal.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(brute_heal))
+ store_item(brute_heal)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] healed brute damage using [brute_heal].")
+#endif
+
+ bleed:
+ if(tied_human.is_bleeding())
+ var/obj/item/bleed_heal
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, bleed_heal_items) && heal_item.ai_can_use(tied_human))
+ bleed_heal = heal_item
+ break
+
+ if(!bleed_heal)
+ goto bone
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, bleed_heal)
+ sleep(short_action_delay)
+ bleed_heal.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(bleed_heal))
+ store_item(bleed_heal)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] fixed bleeding using [bleed_heal].")
+#endif
+
+ // Doesn't support bone-healing chems
+ bone:
+ if(tied_human.has_broken_limbs())
+ var/obj/item/bone_heal
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, bonebreak_heal_items) && heal_item.ai_can_use(tied_human))
+ bone_heal = heal_item
+ break
+
+ if(!bone_heal)
+ goto fire
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, bone_heal)
+ sleep(short_action_delay)
+ bone_heal.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(bone_heal))
+ store_item(bone_heal)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] splinted a fracture using [bone_heal].")
+#endif
+
+ fire:
+ if(tied_human.getFireLoss() > damage_problem_threshold)
+ var/obj/item/burn_heal
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, burn_heal_items) && heal_item.ai_can_use(tied_human))
+ burn_heal = heal_item
+ break
+
+ if(!burn_heal)
+ goto pain
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, burn_heal)
+ sleep(short_action_delay)
+ burn_heal.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(burn_heal))
+ store_item(burn_heal)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] healed burn damage using [burn_heal].")
+#endif
+
+ pain:
+ // This has the issue of the AI taking multiple painkillers if high on pain, despite them not stacking. Not worth fixing atm
+ if(tied_human.pain.get_pain_percentage() > pain_percentage_threshold)
+ var/obj/item/painkiller
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, painkiller_items) && heal_item.ai_can_use(tied_human))
+ painkiller = heal_item
+ break
+
+ if(!painkiller)
+ goto tox
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, painkiller)
+ sleep(short_action_delay)
+ painkiller.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(painkiller))
+ store_item(painkiller)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] healed pain using [painkiller].")
+#endif
+
+ tox:
+ if(tied_human.getToxLoss() > damage_problem_threshold)
+ var/obj/item/tox_heal
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, tox_heal_items) && heal_item.ai_can_use(tied_human))
+ tox_heal = heal_item
+ break
+
+ if(!tox_heal)
+ goto oxy
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, tox_heal)
+ sleep(short_action_delay)
+ tox_heal.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(tox_heal))
+ store_item(tox_heal)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] healed tox damage using [tox_heal].")
+#endif
+
+ oxy:
+ if(tied_human.getOxyLoss() > damage_problem_threshold)
+ var/obj/item/oxy_heal
+ for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS])
+ if(is_type_in_list(heal_item, oxy_heal_items) && heal_item.ai_can_use(tied_human))
+ oxy_heal = heal_item
+
+ if(!oxy_heal)
+ return
+
+ end_gun_fire()
+ currently_busy = TRUE
+ holster_primary()
+ equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, oxy_heal)
+ sleep(short_action_delay)
+ oxy_heal.ai_use(tied_human)
+ sleep(short_action_delay)
+ if(!QDELETED(oxy_heal))
+ store_item(oxy_heal)
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] healed oxygen damage using [oxy_heal].")
+#endif
+ currently_busy = FALSE
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm
new file mode 100644
index 0000000000..51e7cff0ce
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm
@@ -0,0 +1,353 @@
+/datum/human_ai_brain
+ var/list/equipped_items_original_loc = list()
+
+ /// list("object_type" = list(object_ref = "slot")
+ var/list/equipment_map = list(
+ HUMAN_AI_HEALTHITEMS = list(),
+ HUMAN_AI_AMMUNITION = list(),
+ HUMAN_AI_GRENADES = list(),
+ HUMAN_AI_TOOLS = list(),
+ )
+
+ var/list/container_refs = list(
+ "belt" = null,
+ "backpack" = null,
+ "left_pocket" = null,
+ "right_pocket" = null,
+ )
+
+ var/static/list/important_storage_slots = list(
+ WEAR_BACK,
+ WEAR_WAIST,
+ WEAR_L_STORE,
+ WEAR_R_STORE,
+ )
+
+ var/static/important_storage_slots_bitflag = SLOT_BACK | SLOT_WAIST | SLOT_STORE
+
+/datum/human_ai_brain/proc/get_object_from_loc(object_loc)
+ RETURN_TYPE(/obj/item/storage)
+
+ var/obj/item/storage/storage_object
+ switch(object_loc)
+ if("belt")
+ storage_object = tied_human.belt
+ if("backpack")
+ storage_object = tied_human.back
+ if("left_pocket")
+ storage_object = tied_human.l_store
+ if("right_pocket")
+ storage_object = tied_human.r_store
+ return storage_object
+
+/datum/human_ai_brain/proc/equip_item_from_equipment_map(object_type, obj/item/object_ref)
+ if(!object_type || !object_ref)
+ return
+
+ var/object_loc = equipment_map[object_type][object_ref]
+ var/obj/item/storage/storage_object = get_object_from_loc(object_loc)
+
+ storage_object.remove_from_storage(object_ref, tied_human)
+ tied_human.put_in_active_hand(object_ref)
+ equipped_items_original_loc[object_ref] = object_loc
+ RegisterSignal(object_ref, COMSIG_ITEM_DROPPED, PROC_REF(on_equipment_dropped), override = TRUE)
+
+/datum/human_ai_brain/proc/get_item_from_equipment_map_path(object_path, object_type)
+ return (locate(object_path) in equipment_map[object_type])
+
+/datum/human_ai_brain/proc/store_item(obj/item/object_ref, object_loc, slot_type)
+ if(object_ref.loc != tied_human)
+ return
+
+ if(object_ref in equipped_items_original_loc)
+ var/obj/item/storage/storage_object = get_object_from_loc(equipped_items_original_loc[object_ref])
+ equipped_items_original_loc -= object_ref
+ storage_object.attempt_item_insertion(object_ref, FALSE, tied_human)
+ else if(object_loc) // we assume that we've already checked if something will fit or not
+ var/obj/item/storage/storage_item = container_refs[object_loc]
+ storage_item.attempt_item_insertion(object_ref, FALSE, tied_human)
+ if(slot_type)
+ equipment_map[slot_type][object_ref] = object_loc
+
+/// Whenever an item is deleted, purge it from anywhere it may be stored in here
+/datum/human_ai_brain/proc/on_item_delete(obj/item/source, force)
+ SIGNAL_HANDLER
+ for(var/name in container_refs)
+ if(source == container_refs[name])
+ container_refs[name] = null
+ return
+
+ for(var/id in equipment_map)
+ for(var/obj/item/item_ref as anything in equipment_map[id])
+ if(source == item_ref)
+ equipment_map[id] -= item_ref
+ return
+
+/datum/human_ai_brain/proc/on_item_equip(datum/source, obj/item/equipment, slot)
+ SIGNAL_HANDLER
+
+ if((slot in important_storage_slots) && istype(equipment, /obj/item/storage))
+ recalculate_containers()
+ appraise_inventory(slot == WEAR_WAIST, slot == WEAR_BACK, slot == WEAR_L_STORE, slot == WEAR_R_STORE)
+
+ if(!primary_weapon && isgun(equipment))
+ set_primary_weapon(equipment)
+
+/datum/human_ai_brain/proc/on_item_unequip(datum/source, obj/item/equipment, slot)
+ SIGNAL_HANDLER
+
+ if((important_storage_slots_bitflag & slot) && istype(equipment, /obj/item/storage))
+ recalculate_containers()
+ appraise_inventory(slot == SLOT_WAIST, slot == SLOT_BACK, slot == SLOT_STORE, slot == SLOT_STORE)
+
+ if(isgun(equipment))
+ appraise_inventory(FALSE, FALSE, FALSE, FALSE)
+
+/datum/human_ai_brain/proc/recalculate_containers()
+ container_refs = list()
+ if(isstorage(tied_human.belt))
+ container_refs["belt"] = tied_human.belt
+ if(isstorage(tied_human.back))
+ container_refs["backpack"] = tied_human.back
+ if(isstorage(tied_human.l_store))
+ container_refs["left_pocket"] = tied_human.l_store
+ if(isstorage(tied_human.r_store))
+ container_refs["right_pocket"] = tied_human.r_store
+
+/// Currently doesn't support recursive storage
+/datum/human_ai_brain/proc/appraise_inventory(belt = TRUE, back = TRUE, pocket_l = TRUE, pocket_r = TRUE)
+ if(previous_faction != tied_human.faction)
+ previous_faction = tied_human.faction
+ var/datum/human_ai_faction/our_faction = SShuman_ai.human_ai_factions[tied_human.faction]
+ our_faction?.apply_faction_data(src)
+
+ /*if(tied_human.shoes && !primary_melee) // snowflake bootknife check
+ var/obj/item/weapon/knife = locate() in tied_human.shoes
+ if(knife)
+ set_primary_melee(knife)*/
+
+ tried_reload = FALSE // We don't really need to do this in a smart way
+ if(belt)
+ if(!istype(tied_human.belt, /obj/item/storage)) // belts can be backpacks, don't ask
+ goto back_statement
+
+ for(var/id in equipment_map)
+ for(var/obj/item/item as anything in equipment_map[id])
+ if(equipment_map[id][item] != "belt")
+ continue
+
+ equipment_map[id] -= item
+
+ RegisterSignal(tied_human.belt, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE)
+ item_slot_appraisal_loop(tied_human.belt, "belt")
+
+ back_statement:
+ if(back)
+ if(!istype(tied_human.back, /obj/item/storage/backpack))
+ goto l_pocket_statement
+
+ for(var/id in equipment_map)
+ for(var/obj/item/item as anything in equipment_map[id])
+ if(equipment_map[id][item] != "backpack")
+ continue
+
+ equipment_map[id] -= item
+
+ RegisterSignal(tied_human.back, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE)
+ item_slot_appraisal_loop(tied_human.back, "backpack")
+
+ l_pocket_statement:
+ if(pocket_l)
+ if(!istype(tied_human.l_store, /obj/item/storage/pouch))
+ goto r_pocket_statement
+
+ for(var/id in equipment_map)
+ for(var/obj/item/item as anything in equipment_map[id])
+ if(equipment_map[id][item] != "left_pocket")
+ continue
+
+ equipment_map[id] -= item
+
+ RegisterSignal(tied_human.l_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE)
+ item_slot_appraisal_loop(tied_human.l_store, "left_pocket")
+
+ r_pocket_statement:
+ if(pocket_r)
+ if(!istype(tied_human.r_store, /obj/item/storage/pouch))
+ return
+
+ for(var/id in equipment_map)
+ for(var/obj/item/item as anything in equipment_map[id])
+ if(equipment_map[id][item] != "right_pocket")
+ continue
+
+ equipment_map[id] -= item
+
+ RegisterSignal(tied_human.r_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE)
+ item_slot_appraisal_loop(tied_human.r_store, "right_pocket")
+
+/datum/human_ai_brain/proc/item_slot_appraisal_loop(obj/item/container_to_loop, slot_to_assign)
+ for(var/obj/item/inv_item as anything in container_to_loop)
+ RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE)
+ if(inv_item.flags_human_ai & HEALING_ITEM)
+ equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = slot_to_assign
+ else if(inv_item.flags_human_ai & AMMUNITION_ITEM)
+ equipment_map[HUMAN_AI_AMMUNITION][inv_item] = slot_to_assign
+ else if(inv_item.flags_human_ai & GRENADE_ITEM)
+ equipment_map[HUMAN_AI_GRENADES][inv_item] = slot_to_assign
+ else if(inv_item.flags_human_ai & TOOL_ITEM)
+ equipment_map[HUMAN_AI_TOOLS][inv_item] = slot_to_assign
+ //else if((inv_item.flags_human_ai & MELEE_WEAPON_ITEM) && !primary_melee)
+ // set_primary_melee(inv_item)
+
+/datum/human_ai_brain/proc/clear_main_hand()
+ var/obj/item/active_hand = tied_human.get_active_hand()
+ if(!active_hand)
+ return
+
+ if(primary_weapon == active_hand)
+ holster_primary()
+ return
+
+ var/storage_id = storage_has_room(active_hand)
+ if(!storage_id)
+ tied_human.drop_held_item(active_hand)
+ return
+
+ store_item(active_hand, storage_id)
+
+/datum/human_ai_brain/proc/storage_has_room(obj/item/inserting)
+ for(var/container_id in container_refs)
+ var/obj/item/storage/container = container_refs[container_id]
+ if(container?.can_be_inserted(inserting, tied_human, TRUE))
+ return container_id
+
+/datum/human_ai_brain/proc/on_equipment_dropped(obj/item/source, mob/dropper)
+ SIGNAL_HANDLER
+
+ if(isturf(source.loc))
+ equipped_items_original_loc -= source
+ UnregisterSignal(source, COMSIG_ITEM_DROPPED)
+
+/datum/human_ai_brain/proc/on_item_pickup(datum/source, obj/item/picked_up)
+ SIGNAL_HANDLER
+
+ if(!primary_weapon && isgun(picked_up))
+ set_primary_weapon(picked_up)
+
+/datum/human_ai_brain/proc/on_item_drop(datum/source, obj/item/dropped)
+ SIGNAL_HANDLER
+
+ if(dropped == primary_weapon)
+ set_primary_weapon(null)
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP_GUN, dropped)
+
+ for(var/slot in container_refs)
+ if(container_refs[slot] == dropped)
+ appraise_inventory(slot == "belt", slot == "backpack", slot == "left_pocket", slot == "right_pocket")
+ break
+
+ for(var/id in equipment_map)
+ for(var/obj/item/item_ref as anything in equipment_map[id])
+ if(item_ref == dropped)
+ equipment_map[id] -= item_ref
+ return
+
+/datum/human_ai_brain/proc/set_primary_weapon(obj/item/weapon/gun/new_gun)
+ if(primary_weapon)
+ UnregisterSignal(primary_weapon, COMSIG_PARENT_QDELETING)
+ primary_weapon = new_gun
+ appraise_primary()
+ if(primary_weapon)
+ RegisterSignal(primary_weapon, COMSIG_PARENT_QDELETING, PROC_REF(on_primary_delete))
+
+/datum/human_ai_brain/proc/on_primary_delete(datum/source, force)
+ SIGNAL_HANDLER
+
+ set_primary_weapon(null)
+
+/*datum/human_ai_brain/proc/set_primary_melee(obj/item/weapon/new_melee)
+ if(primary_melee)
+ UnregisterSignal(primary_melee, COMSIG_PARENT_QDELETING)
+ primary_melee = new_melee
+ appraise_primary()
+ if(primary_melee)
+ RegisterSignal(primary_melee, COMSIG_PARENT_QDELETING, PROC_REF(on_primary_melee_delete))
+
+/datum/human_ai_brain/proc/on_primary_melee_delete(datum/source, force)
+ SIGNAL_HANDLER
+
+ set_primary_melee(null)*/
+
+/datum/human_ai_brain/proc/appraise_primary()
+ if(!primary_weapon)
+ return
+ var/static/datum/firearm_appraisal/default = new()
+ for(var/datum/firearm_appraisal/appraisal as anything in GLOB.firearm_appraisals)
+ if(is_type_in_list(primary_weapon, appraisal.gun_types))
+ gun_data = appraisal
+ break
+
+ if(!gun_data)
+ gun_data = default
+
+/datum/human_ai_brain/proc/item_search(list/things_around)
+ search_loop:
+ for(var/obj/item/thing in things_around)
+ if(!isturf(thing.loc))
+ continue
+
+ if(!primary_weapon && isgun(thing))
+ var/obj/item/weapon/gun/thing_gun = thing
+ for(var/datum/firearm_appraisal/appraisal as anything in GLOB.firearm_appraisals)
+ if(is_type_in_list(thing_gun, appraisal.gun_types))
+ if(appraisal.disposable && thing_gun.current_mag?.current_rounds <= 0)
+ continue search_loop
+ break
+
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ if(istype(thing, /obj/item/storage/belt) && !container_refs["belt"])
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ if(istype(thing, /obj/item/storage/backpack) && !container_refs["backpack"])
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ if(istype(thing, /obj/item/storage/pouch) && (!container_refs["left_pocket"] || !container_refs["right_pocket"]))
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ var/storage_spot = storage_has_room(thing)
+ if(!storage_spot || !thing.ai_can_use(tied_human))
+ continue
+
+ if(is_type_in_list(thing, all_medical_items))
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ if(primary_weapon && istype(thing, /obj/item/ammo_magazine))
+ var/obj/item/ammo_magazine/mag = thing
+ if(istype(primary_weapon, mag.gun_type))
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ if(istype(thing, /obj/item/explosive/grenade))
+ var/obj/item/explosive/grenade/nade = thing
+ if(nade.active)
+ continue
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+ if(thing.flags_human_ai & TOOL_ITEM) // zonenote: they can pick up 1 billion crowbars
+ ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing)
+ break
+
+/datum/human_ai_brain/proc/get_tool_from_equipment_map(tool_trait)
+ RETURN_TYPE(/obj/item)
+ for(var/obj/item/maybe_tool as anything in equipment_map[HUMAN_AI_TOOLS])
+ if(!HAS_TRAIT(maybe_tool, tool_trait))
+ continue
+ return maybe_tool
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm
new file mode 100644
index 0000000000..1f7e185ca9
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm
@@ -0,0 +1,94 @@
+/datum/human_ai_brain
+ var/ai_move_delay = 0
+ var/list/current_path
+ var/turf/current_target_turf
+ var/path_update_period = (0.5 SECONDS)
+ var/no_path_found = FALSE
+ var/max_travel_distance = HUMAN_AI_MAX_PATHFINDING_RANGE
+ var/next_path_generation = 0
+ /// Amount of times no path found has occured
+ var/no_path_found_amount = 0
+ var/ai_timeout_time = 0
+ var/ai_timeout_period = 2 SECONDS
+
+ /// The time interval between calculating new paths if we cannot find a path
+ var/no_path_found_period = (2.5 SECONDS)
+
+ /// Cooldown declaration for delaying finding a new path if no path was found
+ COOLDOWN_DECLARE(no_path_found_cooldown)
+
+/datum/human_ai_brain/proc/can_move_and_apply_move_delay()
+ // Unable to move, try next time.
+ if(ai_move_delay > world.time || !(tied_human.mobility_flags & MOBILITY_MOVE) || tied_human.is_mob_incapacitated(TRUE) || (tied_human.body_position != STANDING_UP && !tied_human.can_crawl) || tied_human.anchored)
+ return FALSE
+
+ ai_move_delay = world.time + tied_human.move_delay
+ if(tied_human.recalculate_move_delay)
+ ai_move_delay = world.time + tied_human.movement_delay()
+ if(tied_human.next_move_slowdown)
+ ai_move_delay += tied_human.next_move_slowdown
+ tied_human.next_move_slowdown = 0
+ return TRUE
+
+/datum/human_ai_brain/proc/move_to_next_turf(turf/T, max_range = max_travel_distance)
+ if(!T)
+ return FALSE
+
+ if(no_path_found)
+ if(no_path_found_amount > 0)
+ COOLDOWN_START(src, no_path_found_cooldown, no_path_found_period)
+ no_path_found = FALSE
+ no_path_found_amount++
+ return FALSE
+
+ no_path_found_amount = 0
+
+ if((!current_path || (next_path_generation < world.time && current_target_turf != T)) && COOLDOWN_FINISHED(src, no_path_found_cooldown))
+ if(!CALCULATING_PATH(tied_human) || current_target_turf != T)
+ SSpathfinding.calculate_path(tied_human, T, max_range, tied_human, CALLBACK(src, PROC_REF(set_path)), list(tied_human, current_target))
+ current_target_turf = T
+ next_path_generation = world.time + path_update_period
+
+ if(CALCULATING_PATH(tied_human))
+ return TRUE
+
+ // No possible path to target.
+ if(!current_path && get_dist(T, tied_human) > 0)
+ return FALSE
+
+ // We've reached our destination
+ if(!length(current_path) || get_dist(T, tied_human) <= 0)
+ current_path = null
+ return TRUE
+
+ var/turf/next_turf = current_path[length(current_path)]
+ // We've somehow deviated from our current path. Generate next path whenever possible.
+ if(get_dist(next_turf, tied_human) > 1)
+ current_path = null
+ return TRUE
+
+ // Unable to move, try next time.
+ if(!can_move_and_apply_move_delay())
+ return TRUE
+
+ var/list/L = LinkBlocked(tied_human, tied_human.loc, next_turf, list(tied_human, current_target), TRUE)
+ L += SSpathfinding.check_special_blockers(tied_human, next_turf)
+ for(var/a in L)
+ var/atom/A = a
+ if(A.human_ai_obstacle(tied_human, src, get_dir(tied_human.loc, next_turf)) == INFINITY)
+ return FALSE
+ INVOKE_ASYNC(A, TYPE_PROC_REF(/atom, human_ai_act), tied_human, src)
+ var/successful_move = tied_human.Move(next_turf, get_dir(tied_human, next_turf))
+ if(successful_move)
+ ai_timeout_time = world.time
+ current_path.len--
+
+ if(ai_timeout_time < world.time - ai_timeout_period)
+ return FALSE
+
+ return TRUE
+
+/datum/human_ai_brain/proc/set_path(list/path)
+ current_path = path
+ if(!path)
+ no_path_found = TRUE
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm
new file mode 100644
index 0000000000..002acaf2cd
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm
@@ -0,0 +1,110 @@
+/datum/human_ai_squad
+ /// Numeric ID of the squad
+ var/id
+ /// The AI humans in the squad
+ var/list/ai_in_squad = list()
+ /// Primary order assigned to this squad
+ var/datum/ongoing_action/order/assigned_order
+ /// Ref to the squad leader brain
+ var/datum/human_ai_brain/squad_leader
+
+/datum/human_ai_squad/New()
+ . = ..()
+ id = SShuman_ai.highest_squad_id
+
+/datum/human_ai_squad/Destroy(force, ...)
+ for(var/datum/human_ai_brain/brain as anything in ai_in_squad)
+ remove_from_squad(brain)
+ SShuman_ai.squads -= src
+ squad_leader = null
+ return ..()
+
+/datum/human_ai_squad/proc/add_to_squad(datum/human_ai_brain/adding)
+ if(adding.squad_id && (adding.squad_id in SShuman_ai.squad_id_dict))
+ var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict[adding.squad_id]
+ squad.remove_from_squad(adding)
+ adding.squad_id = id
+ ai_in_squad += adding
+
+ adding.set_ongoing_order(assigned_order)
+ RegisterSignal(adding.tied_human, COMSIG_MOB_DEATH, PROC_REF(on_squad_member_death))
+ RegisterSignal(adding, COMSIG_PARENT_QDELETING, PROC_REF(on_squad_member_delete))
+
+/datum/human_ai_squad/proc/remove_from_squad(datum/human_ai_brain/removing)
+ if(removing == squad_leader)
+ set_squad_leader(null)
+ removing.ongoing_order = null
+ removing.squad_id = null
+ removing.is_squad_leader = FALSE
+ ai_in_squad -= removing
+ UnregisterSignal(removing?.tied_human, COMSIG_MOB_DEATH)
+ UnregisterSignal(removing, COMSIG_PARENT_QDELETING)
+
+/datum/human_ai_squad/proc/set_order(datum/ongoing_action/order)
+ assigned_order = order
+ RegisterSignal(order, COMSIG_PARENT_QDELETING, PROC_REF(on_order_delete))
+ for(var/datum/human_ai_brain/brain as anything in ai_in_squad)
+ brain.set_ongoing_order(order)
+
+/datum/human_ai_squad/proc/set_squad_leader(datum/human_ai_brain/new_leader)
+ if(squad_leader)
+ squad_leader.is_squad_leader = FALSE
+ squad_leader = new_leader
+ if(squad_leader)
+ new_leader.is_squad_leader = TRUE
+
+/datum/human_ai_squad/proc/on_squad_member_death(mob/living/carbon/human/dead_mob)
+ SIGNAL_HANDLER
+
+ if(istype(dead_mob, /mob/living/carbon/human/ai))
+ var/mob/living/carbon/human/ai/dead_squddie = dead_mob
+ if(squad_leader == dead_squddie.ai_brain)
+ set_squad_leader(null)
+
+ for(var/datum/human_ai_brain/squaddie as anything in ai_in_squad)
+ if(squaddie.tied_human.is_mob_incapacitated())
+ continue
+
+ squaddie.on_squad_member_death(dead_mob)
+
+/datum/human_ai_squad/proc/on_squad_member_delete(datum/human_ai_brain/deleting)
+ SIGNAL_HANDLER
+
+ remove_from_squad(deleting)
+
+/datum/human_ai_squad/proc/on_order_delete(datum/source, force)
+ SIGNAL_HANDLER
+
+ for(var/datum/human_ai_brain/squaddie as anything in ai_in_squad)
+ squaddie.clear_ongoing_order()
+
+ assigned_order = null
+
+/datum/human_ai_brain
+ /// Numeric ID of the squad this AI is in, if any
+ var/squad_id
+ var/is_squad_leader = FALSE
+
+/datum/human_ai_brain/proc/add_to_squad(new_id)
+ if(isnull(new_id) || (new_id == squad_id))
+ return
+
+ if(!("[new_id]" in SShuman_ai.squad_id_dict))
+ return
+
+ var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[new_id]"]
+ squad.add_to_squad(src)
+
+/datum/human_ai_brain/proc/approach_squad_leader()
+ if(is_squad_leader || !("[squad_id]" in SShuman_ai.squad_id_dict))
+ return FALSE
+
+ var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[squad_id]"]
+ if(!squad.squad_leader?.tied_human)
+ return FALSE
+
+ if(get_dist(tied_human, squad.squad_leader.tied_human) > 2)
+ if(!move_to_next_turf(squad.squad_leader.tied_human))
+ return FALSE
+ return FALSE
+ return TRUE
diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm
new file mode 100644
index 0000000000..8894e61600
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm
@@ -0,0 +1,322 @@
+#define EXTRA_CHECK_DISTANCE_MULTIPLIER 0.20
+
+/datum/human_ai_brain
+ /// At how far out the AI can see cloaked enemies
+ var/cloak_visible_range = 3
+ /// Maximum range to consider a target
+ var/ai_range = 7
+ /// Ref to the currently focused (and shooting at) target
+ var/mob/living/current_target
+ /// Ref to the last turf that the AI shot at
+ var/turf/open/target_floor
+ /// If TRUE, the AI is allowed to establish overwatches
+ var/overwatch_allowed = FALSE
+ /// If TRUE, the AI will throw grenades at enemies who enter cover
+ var/grenading_allowed = TRUE
+ /// List of overwatched turfs
+ var/list/turf/open/overwatch_turfs = list()
+
+ COOLDOWN_DECLARE(return_fire)
+
+/datum/human_ai_brain/proc/get_target(range)
+ var/list/viable_targets = list()
+ var/atom/movable/closest_target
+ var/smallest_distance = INFINITY
+
+ for(var/mob/living/carbon/potential_target as anything in GLOB.alive_mob_list)
+ if(!istype(potential_target))
+ continue
+
+ if(tied_human.z != potential_target.z)
+ continue
+
+ if(!can_target(potential_target))
+ continue
+
+ if(!(potential_target in viewers(tied_human))) // for now, only consider targets in view
+ continue
+
+ var/distance = get_dist(tied_human, potential_target)
+
+ if(distance > ai_range)
+ continue
+
+ viable_targets += potential_target
+
+ if(smallest_distance <= distance)
+ continue
+
+ closest_target = potential_target
+ smallest_distance = distance
+
+ for(var/obj/vehicle/multitile/potential_vehicle_target as anything in GLOB.all_multi_vehicles)
+ if(tied_human.z != potential_vehicle_target.z)
+ continue
+
+ var/distance = get_dist(tied_human, potential_vehicle_target)
+
+ if(distance > ai_range)
+ continue
+
+ if(potential_vehicle_target.health <= 0)
+ continue
+
+ if(potential_vehicle_target.vehicle_faction == tied_human.faction)
+ continue
+
+ /*var/skip_vehicle = FALSE
+ var/list/interior_living_mobs = potential_vehicle_target.interior.get_passengers()
+ for(var/mob/living/carbon/human/human_mob in interior_living_mobs)
+ if(!can_target(human_mob))
+ continue
+
+ skip_vehicle = TRUE
+ break
+
+ if(skip_vehicle)
+ continue*/
+
+ viable_targets += potential_vehicle_target
+
+ if(smallest_distance <= distance)
+ continue
+
+ closest_target = potential_vehicle_target
+ smallest_distance = distance
+
+ for(var/obj/structure/machinery/defenses/potential_defense_target as anything in GLOB.all_active_defenses)
+ if(tied_human.z != potential_defense_target.z)
+ continue
+
+ var/distance = get_dist(tied_human, potential_defense_target)
+
+ if(distance > ai_range)
+ continue
+
+ viable_targets += potential_defense_target
+
+ if(smallest_distance <= distance)
+ continue
+
+ closest_target = potential_defense_target
+ smallest_distance = distance
+
+ var/extra_check_distance = round(smallest_distance * EXTRA_CHECK_DISTANCE_MULTIPLIER)
+
+ if(extra_check_distance < 1)
+ return closest_target
+
+ var/list/extra_checked = orange(extra_check_distance, closest_target)
+
+ var/list/final_targets = extra_checked & viable_targets
+
+ return length(final_targets) ? pick(final_targets) : closest_target
+
+
+/datum/human_ai_brain/proc/can_target(mob/living/carbon/target)
+ if(!istype(target))
+ return FALSE
+
+ if(target.stat == DEAD)
+ return FALSE
+
+ if(!shoot_to_kill && target.stat == UNCONSCIOUS)
+ return FALSE
+
+ if(faction_check(target))
+ return FALSE
+
+ if(HAS_TRAIT(target, TRAIT_CLOAKED) && get_dist(tied_human, target) > cloak_visible_range)
+ return FALSE
+
+ return TRUE
+
+/datum/human_ai_brain/proc/attack_target()
+ set waitfor = FALSE
+
+ if(!current_target)
+ currently_busy = FALSE
+ return
+
+ if(!(primary_weapon in tied_human.get_hands()))
+ unholster_primary()
+ primary_weapon.guaranteed_delay_time = world.time
+ primary_weapon.wield_time = world.time
+ primary_weapon.pull_time = world.time
+
+ tied_human.face_atom(current_target)
+
+ if((get_dist(tied_human, current_target) > gun_data.maximum_range) && COOLDOWN_FINISHED(src, return_fire))
+ currently_busy = FALSE
+ return
+
+ primary_weapon.set_target(current_target)
+ gun_data.before_fire(primary_weapon, tied_human, src)
+ if((!primary_weapon?.current_mag?.current_rounds && !primary_weapon?.in_chamber) || !friendly_check())
+ end_gun_fire()
+ return
+
+ currently_firing = TRUE
+ enter_combat()
+ RegisterSignal(tied_human, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fire), TRUE)
+ primary_weapon.start_fire(object = current_target, bypass_checks = TRUE)
+
+/datum/human_ai_brain/proc/friendly_check()
+ var/list/turf_list = get_line(get_turf(tied_human), get_turf(current_target))
+ for(var/turf/tile in turf_list)
+ if(istype(tile, /turf/closed))
+ return TRUE
+
+ for(var/mob/living/carbon/human/possible_friendly in tile)
+ if(tied_human == possible_friendly)
+ continue
+
+ if(possible_friendly.body_position == LYING_DOWN)
+ continue
+
+ if(faction_check(possible_friendly))
+ return FALSE
+ return TRUE
+
+/datum/human_ai_brain/proc/on_gun_fire(datum/source, obj/item/weapon/gun/fired)
+ SIGNAL_HANDLER
+
+ //var/atom/target = primary_weapon.get_target()
+ if(QDELETED(current_target))
+ end_gun_fire()
+ return
+
+ if(ismob(current_target))
+ //var/mob/targeted_mob = target
+ if(current_target.stat == DEAD)
+ end_gun_fire()
+ return
+
+ else if(current_target.stat == UNCONSCIOUS && !shoot_to_kill)
+ end_gun_fire()
+ return
+
+ if(rounds_burst_fired > gun_data.burst_amount_max)
+ COOLDOWN_START(src, fire_overload_cooldown, max(short_action_delay, short_action_delay * action_delay_mult))
+ end_gun_fire()
+ return
+
+ if(QDELETED(current_target) || !friendly_check())
+ end_gun_fire()
+ return
+
+ if(primary_weapon.current_mag?.current_rounds <= 0 && !primary_weapon.in_chamber) // bullet removal comes after comsig is triggered
+ end_gun_fire()
+ if(gun_data?.disposable)
+ var/obj/item/gun = primary_weapon
+ set_primary_weapon(null)
+ tied_human.drop_held_item(gun)
+ return
+
+ if(!(current_target in viewers(view_distance, tied_human)))
+ if(COOLDOWN_FINISHED(src, return_fire))
+ end_gun_fire()
+ if(grenading_allowed && length(equipment_map[HUMAN_AI_GRENADES]))
+ throw_grenade_cover()
+ return
+ if(overwatch_allowed)
+ establish_overwatch()
+ return
+
+ if(get_dist(tied_human, current_target) > gun_data.maximum_range)
+ if(COOLDOWN_FINISHED(src, return_fire))
+ end_gun_fire()
+ if(!in_cover) // Doing this independently from viewers() check so we don't chase enemy if it's hid just around the corner. Might move it back
+ ADD_ONGOING_ACTION(src, AI_ACTION_APPROACH_CAREFUL, target_floor, 0)
+ return
+
+ if(istype(primary_weapon, /obj/item/weapon/gun/shotgun/pump))
+ var/obj/item/weapon/gun/shotgun/pump/shotgun = primary_weapon
+ addtimer(CALLBACK(shotgun, TYPE_PROC_REF(/obj/item/weapon/gun/shotgun/pump, pump_shotgun), tied_human), shotgun.pump_delay)
+ addtimer(CALLBACK(shotgun, TYPE_PROC_REF(/obj/item/weapon/gun/shotgun/pump, start_fire), null, current_target, null, null, null, TRUE), max(shotgun.pump_delay, shotgun.get_fire_delay()) + 1) // max with fire delay
+
+ else if(istype(primary_weapon, /obj/item/weapon/gun/boltaction))
+ var/obj/item/weapon/gun/boltaction/bolt = primary_weapon
+ addtimer(CALLBACK(bolt, TYPE_PROC_REF(/obj/item/weapon/gun/boltaction, unique_action), tied_human), 1)
+ addtimer(CALLBACK(bolt, TYPE_PROC_REF(/obj/item/weapon/gun/boltaction, unique_action), tied_human), bolt.bolt_delay + 1)
+ addtimer(CALLBACK(bolt, TYPE_PROC_REF(/obj/item/weapon/gun/boltaction, start_fire), null, current_target, null, null, null, TRUE), (bolt.bolt_delay * 2) + 1)
+
+ if(primary_weapon.gun_firemode == GUN_FIREMODE_AUTOMATIC)
+ rounds_burst_fired++
+
+ else if(primary_weapon.gun_firemode == GUN_FIREMODE_SEMIAUTO)
+ addtimer(CALLBACK(primary_weapon, TYPE_PROC_REF(/obj/item/weapon/gun, start_fire), null, current_target, null, null, null, TRUE), primary_weapon.get_fire_delay())
+
+ target_floor = get_turf(current_target)
+ tied_human.face_atom(target_floor)
+
+/datum/human_ai_brain/proc/end_gun_fire()
+ primary_weapon?.set_target(null)
+ if(current_target)
+ UnregisterSignal(current_target, COMSIG_PARENT_QDELETING)
+ UnregisterSignal(tied_human, COMSIG_MOB_FIRED_GUN)
+ current_target = null
+ currently_busy = FALSE
+ currently_firing = FALSE
+ rounds_burst_fired = 0
+
+/// If a target moves behind cover, the AI will overwatch the tiles around where the target was last scene.
+/// If they step on a visible overwatched tile, the AI immediately opens fire. The AI discards all overwatches when moving.
+/datum/human_ai_brain/proc/establish_overwatch()
+ if(!target_floor)
+ return
+
+#ifdef TESTING
+ to_chat(world, "[tied_human.name] has established new overwatch at [target_floor.x], [target_floor.y], [target_floor.z].")
+#endif
+ if(length(overwatch_turfs)) // for now, only 1 overwatch at a time
+ clear_overwatch()
+
+ for(var/turf/open/floor in range(2, target_floor)) // everything within 2 tiles of the center gets marked
+ RegisterSignal(floor, COMSIG_TURF_ENTERED, PROC_REF(on_overwatch_turf_enter))
+ overwatch_turfs += floor
+#ifdef TESTING
+ floor.color = "#aca43a"
+#endif
+
+/datum/human_ai_brain/proc/clear_overwatch()
+ for(var/turf/open/floor as anything in overwatch_turfs)
+ UnregisterSignal(floor, COMSIG_TURF_ENTERED)
+#ifdef TESTING
+ floor.color = null
+#endif
+
+#ifdef TESTING
+ if(length(overwatch_turfs))
+ to_chat(world, "[tied_human.name] has cleared existing overwatch.")
+#endif
+ overwatch_turfs.Cut()
+
+/datum/human_ai_brain/proc/on_overwatch_turf_enter(datum/source, atom/movable/entering)
+ SIGNAL_HANDLER
+
+ if(currently_firing || currently_busy)
+ clear_overwatch()
+ return
+
+ if(!can_target(entering))
+ return
+
+ if(!(entering in viewers(world.view, tied_human)))
+ return
+
+ clear_overwatch()
+ current_target = entering
+ attack_target()
+
+/datum/human_ai_brain/proc/throw_grenade_cover()
+ if(!target_floor || has_ongoing_action(AI_ACTION_NADE))
+ return
+
+ var/obj/item/explosive/grenade/nade = locate() in equipment_map[HUMAN_AI_GRENADES]
+ if(!nade)
+ return
+
+ ADD_ONGOING_ACTION(src, AI_ACTION_NADE, nade, target_floor)
+
+#undef EXTRA_CHECK_DISTANCE_MULTIPLIER
diff --git a/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm b/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm
new file mode 100644
index 0000000000..3fa21daca7
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm
@@ -0,0 +1,145 @@
+/datum/human_faction_management_menu
+
+/datum/human_faction_management_menu/New()
+
+/datum/human_faction_management_menu/tgui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "HumanFactionManager")
+ ui.open()
+
+/datum/human_faction_management_menu/ui_state(mob/user)
+ return GLOB.admin_state
+
+/datum/human_faction_management_menu/ui_data(mob/user)
+ var/list/data = list()
+
+ data["datumless_factions"] = FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH
+
+ data["factions"] = list()
+ for(var/faction_name in SShuman_ai.human_ai_factions)
+ var/datum/human_ai_faction/ai_faction = SShuman_ai.human_ai_factions[faction_name]
+ data["factions"] += list(list(
+ "name" = ai_faction.faction,
+ "shoot_to_kill" = ai_faction.get_shoot_to_kill(),
+ "friendly_factions" = english_list(ai_faction.get_friendly_factions()),
+ "neutral_factions" = english_list(ai_faction.get_neutral_factions()),
+ "ref" = REF(ai_faction),
+ ))
+ data["datumless_factions"] -= ai_faction.faction
+
+ return data
+
+/datum/human_faction_management_menu/ui_static_data(mob/user)
+ var/list/data = list()
+
+ data["all_factions"] = FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH
+
+ return data
+
+/datum/human_faction_management_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ switch(action)
+ if("create_faction")
+ if(!params["faction"])
+ return
+
+ var/gotten_faction = params["faction"]
+ if(gotten_faction in SShuman_ai.human_ai_factions)
+ return
+
+ var/datum/human_ai_faction/new_faction = new()
+ new_faction.faction = gotten_faction
+ SShuman_ai.human_ai_factions[new_faction.faction] = new_faction
+ return TRUE
+
+ if("set_shoot_to_kill")
+ if(!params["new_value"] || !params["faction_name"])
+ return
+
+ var/gotten_faction = params["faction_name"]
+ if(!(gotten_faction in SShuman_ai.human_ai_factions))
+ return
+
+ var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction]
+ faction_obj.set_shoot_to_kill(text2num(params["new_value"]))
+ return TRUE
+
+ if("remove_neutral_faction")
+ if(!params["faction"])
+ return
+
+ var/gotten_faction = params["faction"]
+ if(!(gotten_faction in SShuman_ai.human_ai_factions))
+ return
+
+ var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction]
+ var/gotten_input = tgui_input_list(ui.user, "Remove which faction being neutral to [gotten_faction]?", "Remove Neutral Faction", faction_obj.get_neutral_factions())
+ if(!gotten_input)
+ return
+
+ faction_obj.remove_neutral_faction(gotten_input)
+ return TRUE
+
+ if("remove_friendly_faction")
+ if(!params["faction"])
+ return
+
+ var/gotten_faction = params["faction"]
+ if(!(gotten_faction in SShuman_ai.human_ai_factions))
+ return
+
+ var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction]
+ var/gotten_input = tgui_input_list(ui.user, "Remove which faction being friendly to [gotten_faction]?", "Remove Friendly Faction", faction_obj.get_friendly_factions())
+ if(!gotten_input)
+ return
+
+ faction_obj.remove_friendly_faction(gotten_input)
+ return TRUE
+
+ if("add_neutral_faction")
+ if(!params["faction"])
+ return
+
+ var/gotten_faction = params["faction"]
+ if(!(gotten_faction in SShuman_ai.human_ai_factions))
+ return
+
+ var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction]
+ var/gotten_input = tgui_input_list(ui.user, "Set which faction being neutral to [gotten_faction]?", "Add Neutral Faction", (FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH) - faction_obj.get_neutral_factions() - faction_obj.faction)
+ if(!gotten_input)
+ return
+
+ faction_obj.add_neutral_faction(gotten_input)
+
+ if("add_friendly_faction")
+ if(!params["faction"])
+ return
+
+ var/gotten_faction = params["faction"]
+ if(!(gotten_faction in SShuman_ai.human_ai_factions))
+ return
+
+ var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction]
+ var/gotten_input = tgui_input_list(ui.user, "Set which faction being friendly to [gotten_faction]?", "Add Friendly Faction", (FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH) - faction_obj.get_friendly_factions() - faction_obj.faction)
+ if(!gotten_input)
+ return
+
+ faction_obj.add_friendly_faction(gotten_input)
+
+/client/proc/open_human_faction_management_panel()
+ set name = "Human Faction Management Panel"
+ set category = "Game Master.HumanAI"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ if(human_faction_menu)
+ human_faction_menu.tgui_interact(mob)
+ return
+
+ human_faction_menu = new /datum/human_faction_management_menu(src)
+ human_faction_menu.tgui_interact(mob)
diff --git a/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm
new file mode 100644
index 0000000000..9810cb5512
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm
@@ -0,0 +1,106 @@
+GLOBAL_LIST_INIT_TYPED(firearm_appraisals, /datum/firearm_appraisal, build_firearm_appraisal_list())
+
+/proc/build_firearm_appraisal_list()
+ . = list()
+ for(var/type in subtypesof(/datum/firearm_appraisal))
+ . += new type
+
+
+/datum/firearm_appraisal
+ /// Minimum engagement range with weapon type
+ var/minimum_range = 2
+ /// Optimal engagement range, try to stay at this distance
+ var/optimal_range = 6
+ /// Maximum engagement range, stop firing at this distance
+ var/maximum_range = 9
+ /// How many rounds to fire in 1 burst at most
+ var/burst_amount_max = 8
+ /// List of types that set the human AI to this appraisal type
+ var/list/gun_types = list()
+ /// If TRUE, this gun is disposable and isn't worth trying to reload
+ var/disposable = FALSE
+
+/// List of things we do before our next fire based on weapon type
+/datum/firearm_appraisal/proc/before_fire(obj/item/weapon/gun/firearm, mob/living/carbon/user, datum/human_ai_brain/AI)
+ SHOULD_CALL_PARENT(TRUE) // Every weapon can be twohanded
+
+ AI.ensure_primary_hand(firearm)
+ if((firearm.flags_item & TWOHANDED) && !(firearm.flags_item & WIELDED))
+ firearm.wield(user)
+ sleep(max(firearm.wield_delay, AI.short_action_delay * AI.action_delay_mult))
+
+ if(firearm.flags_gun_features & GUN_TRIGGER_SAFETY)
+ firearm.flags_gun_features ^= GUN_TRIGGER_SAFETY
+ firearm.gun_safety_handle(user)
+
+/datum/firearm_appraisal/rifle
+ burst_amount_max = 8
+ gun_types = list(
+ /obj/item/weapon/gun/rifle,
+ )
+
+/datum/firearm_appraisal/smartgun
+ burst_amount_max = 18
+ gun_types = list(
+ /obj/item/weapon/gun/smartgun,
+ )
+
+/datum/firearm_appraisal/smg
+ burst_amount_max = 10
+ minimum_range = 1
+ optimal_range = 5
+ gun_types = list(
+ /obj/item/weapon/gun/smg,
+ )
+
+/datum/firearm_appraisal/shotgun
+ burst_amount_max = 2
+ minimum_range = 1
+ optimal_range = 1 // point-blank our beloved
+ maximum_range = 3
+ gun_types = list(
+ /obj/item/weapon/gun/shotgun,
+ )
+
+/datum/firearm_appraisal/shotgun/before_fire(obj/item/weapon/gun/shotgun/firearm, mob/living/carbon/user, datum/human_ai_brain/AI)
+ . = ..()
+ if(firearm.in_chamber)
+ return
+ firearm.unique_action(user)
+
+/datum/firearm_appraisal/boltaction
+ gun_types = list(
+ /obj/item/weapon/gun/boltaction,
+ )
+
+/datum/firearm_appraisal/boltaction/before_fire(obj/item/weapon/gun/boltaction/firearm, mob/living/carbon/user, datum/human_ai_brain/AI)
+ . = ..()
+ if(firearm.in_chamber)
+ return
+ firearm.unique_action(user)
+ firearm.recent_cycle = world.time
+ firearm.unique_action(user)
+ firearm.recent_cycle = world.time
+
+/datum/firearm_appraisal/flamer
+ burst_amount_max = 1
+ minimum_range = 5 // To not try and walk into our flames in tight spaces
+ optimal_range = 5
+ maximum_range = 5
+ gun_types = list(
+ /obj/item/weapon/gun/flamer,
+ )
+
+/datum/firearm_appraisal/rpg
+ minimum_range = 5
+ optimal_range = 6
+ gun_types = list(
+ /obj/item/weapon/gun/launcher/rocket/anti_tank/disposable,
+ )
+ disposable = TRUE
+
+/datum/firearm_appraisal/rpg/multi_use
+ gun_types = list(
+ /obj/item/weapon/gun/launcher/rocket,
+ )
+ disposable = FALSE
diff --git a/code/modules/mob/living/carbon/human/ai/fortify_room.dm b/code/modules/mob/living/carbon/human/ai/fortify_room.dm
new file mode 100644
index 0000000000..130c076e17
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/fortify_room.dm
@@ -0,0 +1,67 @@
+/client/proc/fortify_room()
+ set name = "Fortify Room"
+ set category = "Game Master.HumanAI"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ var/list/turf_list = list()
+ var/retval
+
+ switch(tgui_input_list(mob, "How fortified should this be?", "Fortification Level", list("Wood", "Sandbag", "Sandbag (Wired)", "Metal", "Metal (Wired)", "Plasteel", "Plasteel (Wired)")))
+ if("Wood")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/wooden, null)
+ if("Sandbag")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/sandbags/full, null)
+ if("Sandbag (Wired)")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/sandbags/wired, null)
+ if("Metal")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal, /obj/structure/barricade/plasteel/metal)
+ if("Metal (Wired)")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/wired, /obj/structure/barricade/plasteel/metal/wired)
+ if("Plasteel")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/plasteel, /obj/structure/barricade/plasteel)
+ if("Plasteel (Wired)")
+ retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/plasteel/wired, /obj/structure/barricade/plasteel/wired)
+
+ if(retval)
+ to_chat(src, SPAN_NOTICE("Room fortified. Tiles scanned: [length(turf_list)]."))
+ else
+ to_chat(src, SPAN_NOTICE("Room too large to fully fortify. Capped at [length(turf_list)]."))
+
+/proc/recursive_turf_room_fortify(turf/scan_turf, list/turf_list, cade_type, folding_cade_type)
+ if(length(turf_list) > 195) // We're choosing 195 because 200 is the BYOND recursion limit so we're just playing it safe
+ return FALSE // abort if the room is too large
+ if(istype(scan_turf, /turf/closed))
+ return TRUE // abort if we're a wall
+ if(scan_turf in turf_list)
+ return TRUE // abort if we've already been scanned
+ if((locate(/obj/structure/machinery/door) in scan_turf) || (locate(/obj/structure/window_frame) in scan_turf) || (locate(/obj/structure/window) in scan_turf))
+ return TRUE // abort if there's a door or window here
+ turf_list += scan_turf
+ for(var/cardinal in GLOB.cardinals)
+ var/turf/nearby_turf = get_step(scan_turf, cardinal)
+ if(!nearby_turf)
+ continue
+
+ if((locate(/obj/structure/window_frame) in nearby_turf) || (locate(/obj/structure/window) in nearby_turf))
+ for(var/obj/structure/barricade/existing_cade in scan_turf)
+ if(existing_cade.dir == cardinal)
+ goto next_recurse
+
+ var/obj/structure/barricade/cade = new cade_type(scan_turf)
+ cade.setDir(cardinal)
+
+ if(folding_cade_type && (locate(/obj/structure/machinery/door) in nearby_turf))
+ for(var/obj/structure/barricade/existing_cade in scan_turf)
+ if(existing_cade.dir == cardinal)
+ goto next_recurse
+
+ var/obj/structure/barricade/plasteel/cade = new folding_cade_type(scan_turf)
+ cade.setDir(cardinal)
+ cade.open(cade) // this closes it
+
+ next_recurse:
+ if(!recursive_turf_room_fortify(nearby_turf, turf_list, cade_type, folding_cade_type))
+ return FALSE
+ return TRUE
diff --git a/code/modules/mob/living/carbon/human/ai/human_ai.dm b/code/modules/mob/living/carbon/human/ai/human_ai.dm
new file mode 100644
index 0000000000..4a86607fcc
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/human_ai.dm
@@ -0,0 +1,52 @@
+GLOBAL_LIST_EMPTY(non_ai_humans)
+GLOBAL_LIST_EMPTY(ai_humans)
+
+/mob/living/carbon/human
+ var/has_ai = FALSE
+ var/datum/human_ai_brain/ai_brain
+
+/mob/living/carbon/human/Initialize(mapload, new_species, ai)
+ . = ..()
+ if(has_ai)
+ ai = TRUE
+ if(!ai)
+ GLOB.non_ai_humans += src
+ else
+ real_name = random_name(gender, "Human")
+ name = real_name
+ //var/datum/preferences/prefs = new
+ //prefs.randomize_appearance(src)
+ //qdel(prefs)
+ GLOB.ai_humans += src
+ ai_brain = new(src)
+ create_hud()
+ a_intent = INTENT_DISARM
+ //INVOKE_ASYNC(src)
+
+/mob/living/carbon/human/Destroy(force)
+ if(ai_brain)
+ GLOB.ai_humans -= src
+ else
+ GLOB.non_ai_humans -= src
+ QDEL_NULL(ai_brain)
+ return ..()
+
+/mob/living/carbon/human/proc/process_ai()
+ SHOULD_CALL_PARENT(TRUE)
+ if(client)
+ return FALSE
+
+/mob/living/carbon/human/ai
+ has_ai = TRUE
+ mob_flags = AI_CONTROLLED
+
+/mob/living/carbon/human/ai/set_species(new_species, default_color)
+ . = ..()
+ mob_flags |= AI_CONTROLLED
+
+/obj/item/storage/backpack/satchel/ai
+ name = "satchel"
+
+/obj/item/storage/backpack/satchel/ai/fill_preset_inventory()
+ new /obj/item/reagent_container/hypospray/autoinjector/bicaridine(src)
+ new /obj/item/ammo_magazine/rifle(src)
diff --git a/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm b/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm
new file mode 100644
index 0000000000..e9600c343a
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm
@@ -0,0 +1,263 @@
+/atom/proc/human_ai_obstacle(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain, direction, turf/target)
+ if(get_turf(src) == target)
+ return 0
+ return INFINITY
+
+/atom/proc/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain)
+ ai_human.do_click(src, "", list())
+ return TRUE
+
+
+/////////////////////////////
+// OBJECTS //
+/////////////////////////////
+/obj/structure/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ if(!density)
+ return 0
+
+ if(!climbable)
+ return
+
+ return OBJECT_PENALTY
+
+/obj/structure/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain)
+ if(climbable && !human_ai.action_busy)
+ do_climb(human_ai)
+
+ return ..()
+
+/obj/structure/barricade/plasteel/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain)
+ if(!closed) // this means it's closed
+ ai_human.do_click(src, "", list())
+
+ return TRUE
+
+/////////////////////////////
+// MINERAL DOOR //
+/////////////////////////////
+/obj/structure/mineral_door/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ if(!brain.primary_weapon)
+ return INFINITY
+
+ return DOOR_PENALTY
+
+/obj/structure/mineral_door/resin/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain)
+ ADD_ONGOING_ACTION(brain, AI_ACTION_MELEE_ATOM, src)
+ return ..()
+
+
+/////////////////////////////
+// PLATFORMS //
+/////////////////////////////
+/obj/structure/platform/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return DOOR_PENALTY
+
+
+/////////////////////////////
+// PODDDOORS //
+/////////////////////////////
+/obj/structure/machinery/door/poddoor/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ if(!(stat & NOPOWER))
+ return INFINITY
+
+ if(density && !operating && !unacidable && brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR))
+ return DOOR_PENALTY
+
+ return INFINITY
+
+/obj/structure/machinery/door/airlock/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain)
+ if(locked || welded || isElectrified())
+ return
+
+ . = ..()
+
+ if(!(stat & NOPOWER) || !brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR))
+ return
+
+ brain.holster_primary()
+ var/obj/item/crowbar = brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR)
+ brain.equip_item_from_equipment_map(HUMAN_AI_TOOLS, crowbar)
+ attackby(crowbar, ai_human)
+ brain.store_item(crowbar, brain.storage_has_room(crowbar), HUMAN_AI_TOOLS)
+
+/////////////////////////////
+// AIRLOCK //
+/////////////////////////////
+/obj/structure/machinery/door/airlock/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ if(locked || welded || isElectrified())
+ return LOCKED_DOOR_PENALTY
+
+ return DOOR_PENALTY
+
+/obj/structure/machinery/door/airlock/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain)
+ if(locked || welded || isElectrified())
+ return
+
+ . = ..()
+
+ if(!(stat & NOPOWER))
+ return
+
+ brain.holster_primary()
+ var/obj/item/crowbar = brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR)
+ brain.equip_item_from_equipment_map(HUMAN_AI_TOOLS, crowbar)
+ attackby(crowbar, ai_human)
+ brain.store_item(crowbar, brain.storage_has_room(crowbar), HUMAN_AI_TOOLS)
+
+/////////////////////////////
+// HUMANS //
+/////////////////////////////
+/mob/living/carbon/human/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ if(status_flags & GODMODE)
+ return ..()
+
+ return HUMAN_PENALTY
+
+/*/mob/living/carbon/human/xeno_ai_act(mob/living/carbon/xenomorph/X)
+ if(status_flags & GODMODE)
+ return
+
+ . = ..()
+*/
+
+
+/////////////////////////////
+// XENOS //
+/////////////////////////////
+/mob/living/carbon/xenomorph/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return XENO_PENALTY
+
+/////////////////////////////
+// VEHICLES //
+/////////////////////////////
+/obj/vehicle/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return VEHICLE_PENALTY
+
+
+/////////////////////////////
+// SENTRY //
+/////////////////////////////
+/obj/structure/machinery/defenses/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return SENTRY_PENALTY
+
+
+/////////////////////////////
+// WINDOW FRAME //
+/////////////////////////////
+/*obj/structure/window_frame/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ if(buildstacktype && brain.get_tool_from_equipment_map(TRAIT_TOOL_WRENCH))
+ return ..()
+ return WINDOW_FRAME_PENALTY*/
+
+
+/////////////////////////////
+// BARRICADES //
+/////////////////////////////
+/obj/structure/barricade/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return BARRICADE_PENALTY
+
+/obj/structure/barricade/plasteel/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain)
+ . = ..()
+ if(!closed)
+ close(src)
+
+/obj/structure/barricade/handrail/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return DOOR_PENALTY
+
+
+/////////////////////////////
+// FIRE //
+/////////////////////////////
+/obj/flamer_fire/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/braineno, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return FIRE_PENALTY
+
+
+/////////////////////////////
+// WALLS //
+/////////////////////////////
+/turf/closed/wall/resin/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/braineno, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return WALL_PENALTY
+
+
+/////////////////////////////
+// FLOOR //
+/////////////////////////////
+/*
+ Sometimes open turfs are passed back as obstacles due to platforms and such,
+ generally it's fast so very slight penalty mainly for handling subtypes properly
+*/
+/turf/open/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return OPEN_TURF_PENALTY
+
+/turf/open/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain)
+ return FALSE
+
+/turf/open/space/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(!.)
+ return
+
+ return INFINITY
+
+
+/////////////////////////////
+// RIVER //
+/////////////////////////////
+/turf/open/gm/river/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ . = ..()
+ if(. && !covered)
+ . += base_river_slowdown
+
+/turf/open/gm/river/desert/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target)
+ if(toxic && !covered)
+ return FIRE_PENALTY
+
+ return ..()
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 5eadbd57c7..bba361b18e 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1,4 +1,4 @@
-/mob/living/carbon/human/Initialize(mapload, new_species = null)
+/mob/living/carbon/human/Initialize(mapload, new_species = null, ai = FALSE)
blood_type = pick(7;"O-", 38;"O+", 6;"A-", 34;"A+", 2;"B-", 9;"B+", 1;"AB-", 3;"AB+")
GLOB.human_mob_list += src
GLOB.alive_human_list += src
diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm
index dbc30b964a..b01a72f4fb 100644
--- a/code/modules/mob/living/carbon/human/inventory.dm
+++ b/code/modules/mob/living/carbon/human/inventory.dm
@@ -102,6 +102,12 @@
. = ..()
/mob/living/carbon/human/u_equip(obj/item/I, atom/newloc, nomoveupdate, force)
+ var/slot
+ if(I)
+ if(I == back)
+ slot = SLOT_BACK
+ else if(I == wear_mask)
+ slot = SLOT_FACE
. = ..()
if(!. || !I)
return FALSE
@@ -119,6 +125,7 @@
if(I.flags_inv_hide & HIDEJUMPSUIT)
update_inv_w_uniform()
update_inv_wear_suit()
+ slot = SLOT_OCLOTHING
else if(I == w_uniform)
if(r_store)
drop_inv_item_on_ground(r_store)
@@ -129,6 +136,7 @@
w_uniform = null
update_suit_sensors()
update_inv_w_uniform()
+ slot = SLOT_ICLOTHING
else if(I == head)
var/updatename = 0
if(head.flags_inv_hide & HIDEFACE)
@@ -146,43 +154,54 @@
update_inv_glasses()
update_tint()
update_inv_head()
+ slot = SLOT_HEAD
else if (I == gloves)
gloves = null
update_inv_gloves()
+ slot = SLOT_HANDS
else if (I == glasses)
glasses = null
update_tint()
update_glass_vision(I)
update_inv_glasses()
+ slot = SLOT_EYES
else if (I == wear_l_ear)
wear_l_ear = null
update_inv_ears()
+ slot = SLOT_EAR
else if (I == wear_r_ear)
wear_r_ear = null
update_inv_ears()
+ slot = SLOT_EAR
else if (I == shoes)
shoes = null
update_inv_shoes()
+ slot = SLOT_FEET
else if (I == belt)
belt = null
update_inv_belt()
+ slot = SLOT_WAIST
else if (I == wear_id)
wear_id = null
sec_hud_set_ID()
hud_set_squad()
update_inv_wear_id()
name = get_visible_name()
+ slot = SLOT_ID
else if (I == r_store)
r_store = null
update_inv_pockets()
+ slot = SLOT_STORE
else if (I == l_store)
l_store = null
update_inv_pockets()
+ slot = SLOT_STORE
else if (I == s_store)
s_store = null
update_inv_s_store()
+ slot = SLOT_SUIT_STORE
-
+ SEND_SIGNAL(src, COMSIG_HUMAN_UNEQUIPPED_ITEM, I, slot)
/mob/living/carbon/human/wear_mask_update(obj/item/I, equipping)
@@ -493,7 +512,7 @@
return ..()
/mob/living/carbon/human/proc/get_strip_delay(mob/living/carbon/human/user, mob/living/carbon/human/target)
- /// Default delay
+ /*/// Default delay
var/target_delay = HUMAN_STRIP_DELAY
/// Multiplier for how quickly the user can strip things.
var/user_speed = user.get_skill_duration_multiplier(SKILL_CQC)
@@ -506,7 +525,8 @@
target_delay += (target_skills * 2)
/// Final result is overall delay * speed multiplier
- return target_delay * user_speed
+ return target_delay * user_speed*/
+ return 0 // zonenote
/mob/living/carbon/human/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force)
remember_dropped_object(I)
diff --git a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm
index e87de58155..b4bd9c49ad 100644
--- a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm
+++ b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm
@@ -43,7 +43,7 @@
if(distance > max_travel_distance)
return
- SSxeno_pathfinding.calculate_path(src, P.firer, distance, src, CALLBACK(src, PROC_REF(set_path)), list(src, P.firer))
+ SSpathfinding.calculate_path(src, P.firer, distance, src, CALLBACK(src, PROC_REF(set_path)), list(src, P.firer))
/mob/living/carbon/xenomorph/proc/register_ai_action(datum/action/xeno_action/XA)
if(XA.owner != src)
@@ -165,12 +165,12 @@
return FALSE
if((!current_path || (next_path_generation < world.time && current_target_turf != T)) && COOLDOWN_FINISHED(src, no_path_found_cooldown))
- if(!XENO_CALCULATING_PATH(src) || current_target_turf != T)
- SSxeno_pathfinding.calculate_path(src, T, max_range, src, CALLBACK(src, PROC_REF(set_path)), list(src, current_target))
+ if(!CALCULATING_PATH(src) || current_target_turf != T)
+ SSpathfinding.calculate_path(src, T, max_range, src, CALLBACK(src, PROC_REF(set_path)), list(src, current_target))
current_target_turf = T
next_path_generation = world.time + path_update_period
- if(XENO_CALCULATING_PATH(src))
+ if(CALCULATING_PATH(src))
return TRUE
// No possible path to target.
@@ -193,7 +193,7 @@
var/turf/next_turf = current_path[current_path.len]
var/list/L = LinkBlocked(src, loc, next_turf, list(src, current_target), TRUE)
- L += SSxeno_pathfinding.check_special_blockers(src, next_turf)
+ L += SSpathfinding.check_special_blockers(src, next_turf)
for(var/a in L)
var/atom/A = a
if(A.xeno_ai_obstacle(src, get_dir(loc, next_turf)) == INFINITY)
diff --git a/code/modules/projectiles/ammunition.dm b/code/modules/projectiles/ammunition.dm
index e032d3ebbe..213dfeeee3 100644
--- a/code/modules/projectiles/ammunition.dm
+++ b/code/modules/projectiles/ammunition.dm
@@ -12,6 +12,7 @@ They're all essentially identical when it comes to getting the job done.
var/bonus_overlay = null //Sprite pointer in ammo.dmi to an overlay to add to the gun, for extended mags, box mags, and so on
flags_atom = FPRINT|CONDUCT
flags_equip_slot = SLOT_WAIST
+ flags_human_ai = AMMUNITION_ITEM
matter = list("metal" = 1000)
//Low.
throwforce = 2
@@ -191,6 +192,12 @@ They're all essentially identical when it comes to getting the job done.
default_ammo = source.default_ammo
gun_type = source.gun_type
+/obj/item/ammo_magazine/ai_can_use(mob/living/carbon/human/user)
+ if(current_rounds <= 0)
+ return FALSE
+
+ return TRUE
+
//~Art interjecting here for explosion when using flamer procs.
/obj/item/ammo_magazine/flamer_fire_act(damage, datum/cause_data/flame_cause_data)
if(current_rounds < 1)
@@ -246,6 +253,7 @@ bullets/shells. ~N
max_rounds = 5 // For shotguns, though this will be determined by the handful type when generated.
flags_atom = FPRINT|CONDUCT
flags_magazine = AMMUNITION_HANDFUL
+ flags_human_ai = NONE
attack_speed = 3 // should make reloading less painful
/obj/item/ammo_magazine/handful/Initialize(mapload, spawn_empty)
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index 50e735b918..232c932f47 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -2020,3 +2020,8 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed
/// Getter for gun_user
/obj/item/weapon/gun/proc/get_gun_user()
return gun_user
+
+/// Getter for target
+/obj/item/weapon/gun/proc/get_target()
+ RETURN_TYPE(/atom)
+ return target
diff --git a/code/modules/projectiles/gun_attachables.dm b/code/modules/projectiles/gun_attachables.dm
index 2737bc5245..2fca594a83 100644
--- a/code/modules/projectiles/gun_attachables.dm
+++ b/code/modules/projectiles/gun_attachables.dm
@@ -290,6 +290,7 @@ Defined in conflicts.dm of the #defines folder.
flags_equip_slot = SLOT_FACE
flags_armor_protection = SLOT_FACE
flags_item = CAN_DIG_SHRAPNEL
+ flags_human_ai = MELEE_WEAPON_ITEM
attach_icon = "bayonet_a"
melee_mod = 20
diff --git a/code/modules/projectiles/gun_helpers.dm b/code/modules/projectiles/gun_helpers.dm
index 46f7f68b6f..e3d6655a0b 100644
--- a/code/modules/projectiles/gun_helpers.dm
+++ b/code/modules/projectiles/gun_helpers.dm
@@ -157,30 +157,29 @@ DEFINES in setup.dm, referenced here.
if(CONFIG_GET(flag/remove_gun_restrictions))
return TRUE //Not if the config removed it.
- if(user.mind)
- switch(user.job)
- if(
- "PMC",
- "WY Agent",
- "Corporate Liaison",
- "Event",
- "UPP Armsmaster", //this rank is for the Fun - Ivan preset, it allows him to use the PMC guns randomly generated from his backpack
- ) return TRUE
- switch(user.faction)
- if(
- FACTION_WY_DEATHSQUAD,
- FACTION_PMC,
- FACTION_MERCENARY,
- FACTION_FREELANCER,
- ) return TRUE
-
- for(var/faction in user.faction_group)
- if(faction in FACTION_LIST_WY)
- return TRUE
-
- if(user.faction in FACTION_LIST_WY)
+ switch(user.job)
+ if(
+ "PMC",
+ "WY Agent",
+ "Corporate Liaison",
+ "Event",
+ "UPP Armsmaster", //this rank is for the Fun - Ivan preset, it allows him to use the PMC guns randomly generated from his backpack
+ ) return TRUE
+ switch(user.faction)
+ if(
+ FACTION_WY_DEATHSQUAD,
+ FACTION_PMC,
+ FACTION_MERCENARY,
+ FACTION_FREELANCER,
+ ) return TRUE
+
+ for(var/faction in user.faction_group)
+ if(faction in FACTION_LIST_WY)
return TRUE
+ if(user.faction in FACTION_LIST_WY)
+ return TRUE
+
to_chat(user, SPAN_WARNING("[src] flashes a warning sign indicating unauthorized use!"))
// Checks whether there is anything to put your harness
diff --git a/code/modules/projectiles/guns/flamer/flamer.dm b/code/modules/projectiles/guns/flamer/flamer.dm
index 73ed0a37b5..98c98ad229 100644
--- a/code/modules/projectiles/guns/flamer/flamer.dm
+++ b/code/modules/projectiles/guns/flamer/flamer.dm
@@ -149,6 +149,8 @@
unleash_foam(target, user)
else
unleash_flame(target, user)
+ current_mag.current_rounds = current_mag.get_ammo_percent()
+ SEND_SIGNAL(user, COMSIG_MOB_FIRED_GUN, src)
return AUTOFIRE_CONTINUE
return NONE
diff --git a/code/modules/projectiles/magazines/lever_action.dm b/code/modules/projectiles/magazines/lever_action.dm
index ac1d57dbd4..a32a49d926 100644
--- a/code/modules/projectiles/magazines/lever_action.dm
+++ b/code/modules/projectiles/magazines/lever_action.dm
@@ -92,6 +92,7 @@ Handfuls of lever_action rounds. For spawning directly on mobs in roundstart, ER
gun_type = /obj/item/weapon/gun/lever_action
handful_state = "lever_action_bullet"
transfer_handful_amount = 9
+ flags_human_ai = AMMUNITION_ITEM
/obj/item/ammo_magazine/handful/lever_action/training
name = "handful of blanks (45-70)"
diff --git a/code/modules/projectiles/magazines/shotguns.dm b/code/modules/projectiles/magazines/shotguns.dm
index 9fe9f0f440..f47a472bd3 100644
--- a/code/modules/projectiles/magazines/shotguns.dm
+++ b/code/modules/projectiles/magazines/shotguns.dm
@@ -189,6 +189,7 @@ GLOBAL_LIST_INIT(shotgun_handfuls_12g, list(
gun_type = /obj/item/weapon/gun/shotgun
handful_state = "slug_shell"
transfer_handful_amount = 5
+ flags_human_ai = AMMUNITION_ITEM
/obj/item/ammo_magazine/handful/shotgun/slug
@@ -321,6 +322,7 @@ GLOBAL_LIST_INIT(shotgun_handfuls_12g, list(
max_rounds = 8
current_rounds = 8
gun_type = /obj/item/weapon/gun/shotgun/double/cane
+ flags_human_ai = AMMUNITION_ITEM
/obj/item/ammo_magazine/handful/revolver/marksman
name = "handful of marksman revolver bullets (.44)"
diff --git a/colonialmarines.dme b/colonialmarines.dme
index 2acf1dec00..2142ee9258 100644
--- a/colonialmarines.dme
+++ b/colonialmarines.dme
@@ -67,6 +67,7 @@
#include "code\__DEFINES\html.dm"
#include "code\__DEFINES\hud.dm"
#include "code\__DEFINES\human.dm"
+#include "code\__DEFINES\human_ai.dm"
#include "code\__DEFINES\job.dm"
#include "code\__DEFINES\keybinding.dm"
#include "code\__DEFINES\language.dm"
@@ -271,6 +272,7 @@
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\hijack.dm"
#include "code\controllers\subsystem\human.dm"
+#include "code\controllers\subsystem\human_ai.dm"
#include "code\controllers\subsystem\inactivity.dm"
#include "code\controllers\subsystem\influxdriver.dm"
#include "code\controllers\subsystem\influxmcstats.dm"
@@ -1976,6 +1978,36 @@
#include "code\modules\mob\living\carbon\human\unarmed_attacks.dm"
#include "code\modules\mob\living\carbon\human\update_icons.dm"
#include "code\modules\mob\living\carbon\human\whisper.dm"
+#include "code\modules\mob\living\carbon\human\ai\ai_equipment.dm"
+#include "code\modules\mob\living\carbon\human\ai\ai_management_menu.dm"
+#include "code\modules\mob\living\carbon\human\ai\faction_management_panel.dm"
+#include "code\modules\mob\living\carbon\human\ai\fortify_room.dm"
+#include "code\modules\mob\living\carbon\human\ai\human_ai_interaction.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\melee_atom.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\pickup_primary.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\throw_grenade.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\approach_target_carefully.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\retreat_from_target.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\orders\order_action.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\orders\patrol_waypoints.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_communication.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_factions.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_squad.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\take_inside_cover.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_cover.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\take_cover.dm"
+#include "code\modules\mob\living\carbon\human\ai\firearm_appraisal.dm"
+#include "code\modules\mob\living\carbon\human\ai\human_ai.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\action_datums.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\approach_target.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\item_pickup.dm"
+#include "code\modules\mob\living\carbon\human\ai\action_datums\throw_back_nade.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_guns.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_health.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_items.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_pathfinding.dm"
+#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_targeting.dm"
#include "code\modules\mob\living\carbon\human\life\handle_breath.dm"
#include "code\modules\mob\living\carbon\human\life\handle_chemicals_in_body.dm"
#include "code\modules\mob\living\carbon\human\life\handle_disabilities.dm"
diff --git a/tgui/packages/tgui/interfaces/HumanAIManager.tsx b/tgui/packages/tgui/interfaces/HumanAIManager.tsx
new file mode 100644
index 0000000000..f1dc85214f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/HumanAIManager.tsx
@@ -0,0 +1,383 @@
+import { useBackend, useLocalState } from '../backend';
+import { Button, Section, Stack, Divider } from '../components';
+import { Window } from '../layouts';
+import { BooleanLike } from 'common/react';
+
+type Squad = {
+ id: number;
+ order: string;
+ members: string;
+ ref: string;
+ primary_order: string;
+ squad_leader: string;
+};
+
+type AIHuman = {
+ name: string;
+ health: number;
+ loc: number[];
+ faction: string;
+ ref: string;
+ brain_ref: string;
+ in_combat: BooleanLike;
+ squad_id: number;
+};
+
+type Order = {
+ name: string;
+ type: string;
+ data: any[];
+ ref: string;
+};
+
+type BackendContext = {
+ orders: Order[];
+ ai_humans: AIHuman[];
+ squads: Squad[];
+};
+
+const AIContext = (props, context) => {
+ const { data, act } = useBackend();
+ const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState(
+ 'squad_assignment_mode',
+ false,
+ );
+ const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState(
+ 'order_assignment_mode',
+ false,
+ );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {data.orders.map((order) => (
+
+ ))}
+
+
+
+ {data.ai_humans.map((human) => (
+
+ ))}
+
+
+
+ {data.squads.map((squad) => (
+
+ ))}
+
+
+ );
+};
+
+const CreatedOrder = (props) => {
+ const order: Order = props.order;
+ const context: BackendContext = props.context;
+ const { data, act } = useBackend();
+ const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState(
+ 'order_assignment_mode',
+ false,
+ );
+ const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1);
+ return (
+
+
+ {orderAssignmentMode ? (
+ <>
+
+
+ );
+};
+
+const HumanAIReadout = (props) => {
+ const human: AIHuman = props.human;
+ const context: BackendContext = props.context;
+ const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState(
+ 'squad_assignment_mode',
+ false,
+ );
+ const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1);
+ const { data, act } = useBackend();
+ const gottenSquad: Squad = data.squads[selectedSquad];
+ return (
+
+
+
+ {squadAssignmentMode ? (
+ <>
+
+ act('assign_to_squad', {
+ ai: human.brain_ref,
+ squad: selectedSquad,
+ })
+ }
+ color="green"
+ disabled={
+ selectedSquad === -1 || human.squad_id == selectedSquad
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+
+ act('assign_sl', {
+ ai: human.brain_ref,
+ squad: selectedSquad,
+ })
+ }
+ color={
+ gottenSquad !== undefined &&
+ gottenSquad.squad_leader == human.name
+ ? 'green'
+ : 'red'
+ }
+ disabled={selectedSquad === human.squad_id ? false : true}
+ style={{
+ float: 'left',
+ }}
+ />
+ >
+ ) : (
+ <>
+
+ act('view_variables', {
+ ref: human.ref,
+ })
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+
+ act('view_variables', {
+ ref: human.brain_ref,
+ })
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+ {
+ act('delete_object', {
+ ref: human.brain_ref,
+ });
+ act('delete_object', {
+ ref: human.ref,
+ });
+ }}
+ >
+ Del
+
+ >
+ )}
+
+
+
+ Health: {human.health}%
+ Faction: {human.faction}
+ In Combat: {human.in_combat}
+ Squad #: {human.squad_id}
+ Loc: {human.loc[0]}, {human.loc[1]}, {human.loc[2]}
+
+
+ );
+};
+
+const SquadReadout = (props) => {
+ const squad: Squad = props.squad;
+ const context: BackendContext = props.context;
+ const { data, act } = useBackend();
+ const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState(
+ 'squad_assignment_mode',
+ false,
+ );
+ const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1);
+ const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState(
+ 'order_assignment_mode',
+ false,
+ );
+ return (
+
+
+ {squadAssignmentMode || orderAssignmentMode ? (
+ setSelectedSquad(squad.id)}
+ color={squad.id === selectedSquad ? 'green' : 'red'}
+ />
+ ) : (
+
+ act('view_variables', {
+ ref: squad.ref,
+ })
+ }
+ />
+ )}
+
+ act('delete_object', {
+ ref: squad.ref,
+ })
+ }
+ >
+ Del
+
+
+
+ Members: {squad.members}
+ Order: {squad.order}
+ Squad Leader: {squad.squad_leader}
+
+
+ );
+};
+
+export const HumanAIManager = (props) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/HumanFactionManager.tsx b/tgui/packages/tgui/interfaces/HumanFactionManager.tsx
new file mode 100644
index 0000000000..07fbf02c8e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/HumanFactionManager.tsx
@@ -0,0 +1,161 @@
+import { useBackend, useLocalState } from '../backend';
+import { Button, Dropdown, Section, Stack, Divider } from '../components';
+import { Window } from '../layouts';
+import { BooleanLike } from 'common/react';
+
+type Faction = {
+ name: string;
+ shoot_to_kill: BooleanLike;
+ friendly_factions: string;
+ neutral_factions: string;
+ ref: string;
+};
+
+type BackendContext = {
+ factions: Faction[];
+ all_factions: string[];
+ datumless_factions: string[];
+};
+
+const FactionContext = (props, context) => {
+ const { data, act } = useBackend();
+ const [selectedToCreateFaction, setSelectedToCreateFaction] = useLocalState(
+ 'selected_to_create_faction',
+ '',
+ );
+ return (
+
+
+
+ setSelectedToCreateFaction(value)}
+ />
+
+
+ {
+ act('create_faction', {
+ faction: selectedToCreateFaction,
+ });
+ setSelectedToCreateFaction('');
+ }}
+ />
+
+
+
+
+ {data.factions.map((faction) => (
+
+ ))}
+
+
+ );
+};
+
+const ExistingFaction = (props) => {
+ const context: BackendContext = props.context;
+ const { data, act } = useBackend();
+ const faction: Faction = props.faction;
+ return (
+
+
+ Shoot To Kill: {faction.shoot_to_kill}
+
+ act('set_shoot_to_kill', {
+ faction_name: faction.name,
+ new_value: value,
+ })
+ }
+ />
+ Friendly Factions: {faction.friendly_factions}
+ <>
+
+ act('add_friendly_faction', {
+ faction: faction.name,
+ })
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+
+ act('remove_friendly_faction', {
+ faction: faction.name,
+ })
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+ >
+
+
+ Neutral Factions: {faction.neutral_factions}
+ <>
+
+ act('add_neutral_faction', {
+ faction: faction.name,
+ })
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+
+ act('remove_neutral_faction', {
+ faction: faction.name,
+ })
+ }
+ style={{
+ float: 'left',
+ }}
+ />
+ >
+
+
+ );
+};
+
+export const HumanFactionManager = (props) => {
+ return (
+
+
+
+
+
+ );
+};