From ccfdaed1604f019e047cfc481ca8c456d5ae7060 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 6 Sep 2024 19:26:13 -0700 Subject: [PATCH 01/42] wip --- .../signals/atom/mob/living/signals_human.dm | 3 + .../dcs/signals/atom/mob/signals_mob.dm | 2 + code/__DEFINES/equipment.dm | 4 + code/__DEFINES/human_ai.dm | 11 + code/__DEFINES/subsystems.dm | 3 +- code/__DEFINES/xeno_ai.dm | 2 +- code/__HELPERS/unsorted.dm | 31 +- code/_compile_options.dm | 2 +- code/_globalvars/global_lists.dm | 6 +- code/controllers/subsystem/human_ai.dm | 72 ++++ code/controllers/subsystem/pathfinding.dm | 51 ++- code/game/objects/items.dm | 6 + .../items/explosives/grenades/marines.dm | 2 +- .../items/reagent_containers/autoinjectors.dm | 36 ++ .../items/reagent_containers/hypospray.dm | 2 +- code/game/objects/items/stacks/medical.dm | 85 +++++ code/game/objects/items/storage/firstaid.dm | 17 + code/modules/admin/admin_verbs.dm | 4 + code/modules/mob/inventory.dm | 2 + .../human/ai/action_datums/action_datums.dm | 18 + .../human/ai/action_datums/approach_target.dm | 25 ++ .../human/ai/action_datums/item_pickup.dm | 65 ++++ .../ai/action_datums/orders/order_action.dm | 5 + .../action_datums/orders/patrol_waypoints.dm | 76 ++++ .../human/ai/action_datums/take_cover.dm | 32 ++ .../ai/action_datums/take_inside_cover.dm | 27 ++ .../human/ai/action_datums/throw_back_nade.dm | 32 ++ .../carbon/human/ai/ai_management_menu.dm | 112 ++++++ .../living/carbon/human/ai/brain/ai_brain.dm | 351 ++++++++++++++++++ .../carbon/human/ai/brain/ai_brain_cover.dm | 22 ++ .../carbon/human/ai/brain/ai_brain_guns.dm | 84 +++++ .../carbon/human/ai/brain/ai_brain_health.dm | 224 +++++++++++ .../carbon/human/ai/brain/ai_brain_items.dm | 293 +++++++++++++++ .../human/ai/brain/ai_brain_pathfinding.dm | 94 +++++ .../carbon/human/ai/brain/ai_brain_squad.dm | 60 +++ .../human/ai/brain/ai_brain_targeting.dm | 289 ++++++++++++++ .../carbon/human/ai/firearm_appraisal.dm | 37 ++ .../mob/living/carbon/human/ai/human_ai.dm | 36 ++ code/modules/mob/living/carbon/human/human.dm | 2 +- .../mob/living/carbon/human/inventory.dm | 22 +- .../mob/living/carbon/xenomorph/ai/xeno_ai.dm | 10 +- code/modules/projectiles/ammunition.dm | 7 + code/modules/projectiles/gun.dm | 5 + colonialmarines.dme | 21 ++ .../tgui/interfaces/HumanAIManager.tsx | 206 ++++++++++ 45 files changed, 2450 insertions(+), 46 deletions(-) create mode 100644 code/__DEFINES/human_ai.dm create mode 100644 code/controllers/subsystem/human_ai.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm create mode 100644 code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm create mode 100644 code/modules/mob/living/carbon/human/ai/ai_management_menu.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm create mode 100644 code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm create mode 100644 code/modules/mob/living/carbon/human/ai/human_ai.dm create mode 100644 tgui/packages/tgui/interfaces/HumanAIManager.tsx 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 1d7a727232..3c76617367 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 0f2457d4b1..85edf10243 100644 --- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm +++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm @@ -131,3 +131,5 @@ /// 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" diff --git a/code/__DEFINES/equipment.dm b/code/__DEFINES/equipment.dm index 77638ed568..22901c15a4 100644 --- a/code/__DEFINES/equipment.dm +++ b/code/__DEFINES/equipment.dm @@ -82,6 +82,10 @@ #define ANIMATED_SURGICAL_TOOL (1<<12) /// Has heat source but isn't 'on fire' and thus can be stored #define IGNITING_ITEM (1<<13) +/// This item is classified as a healing item for the sake of human AI +#define HEALING_ITEM (1<<14) +/// This item is classified as ammunition for the sake of human AI +#define AMMUNITION_ITEM (1<<15) //========================================================================================== diff --git a/code/__DEFINES/human_ai.dm b/code/__DEFINES/human_ai.dm new file mode 100644 index 0000000000..a857498edf --- /dev/null +++ b/code/__DEFINES/human_ai.dm @@ -0,0 +1,11 @@ +#define HUMAN_AI_HEALTHITEMS "health" +#define HUMAN_AI_AMMUNITION "ammo" + +/// 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) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 14399cc231..513b06d231 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -162,6 +162,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 @@ -171,7 +172,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 9095f56ad9..1e5d858012 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 diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 60fa224ef8..40374e5c4c 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -51,8 +51,8 @@ #define format_frequency(f) "[round(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 @@ -2141,3 +2141,30 @@ 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) 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/_globalvars/global_lists.dm b/code/_globalvars/global_lists.dm index c2cfb8263f..d726de0aef 100644 --- a/code/_globalvars/global_lists.dm +++ b/code/_globalvars/global_lists.dm @@ -110,9 +110,9 @@ GLOBAL_REFERENCE_LIST_INDEXED(xeno_datum_list, /datum/caste_datum, caste_type) //Chem Stuff var/global/list/chemical_reactions_filtered_list //List of all /datum/chemical_reaction datums filtered by reaction components. Used during chemical reactions -var/global/list/chemical_reactions_list //List of all /datum/chemical_reaction datums indexed by reaction id. Used to search for the result instead of the components. -var/global/list/chemical_reagents_list //List of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff -var/global/list/chemical_properties_list //List of all /datum/chem_property datums indexed by property name +var/global/list/datum/chemical_reaction/chemical_reactions_list //List of all /datum/chemical_reaction datums indexed by reaction id. Used to search for the result instead of the components. +var/global/list/datum/reagent/chemical_reagents_list //List of all /datum/reagent datums indexed by reagent id. Used by chemistry stuff +var/global/list/datum/chem_property/chemical_properties_list //List of all /datum/chem_property datums indexed by property name //List of all id's from classed /datum/reagent datums indexed by class or tier. Used by chemistry generator and chem spawners. var/global/list/list/chemical_gen_classes_list = list("C" = list(),"C1" = list(),"C2" = list(),"C3" = list(),"C4" = list(),"C5" = list(),"C6" = list(),"T1" = list(),"T2" = list(),"T3" = list(),"T4" = list(),"tau" = list()) //properties generated in chemicals, helps to make sure the same property doesn't show up 10 times diff --git a/code/controllers/subsystem/human_ai.dm b/code/controllers/subsystem/human_ai.dm new file mode 100644 index 0000000000..ae1f680bd2 --- /dev/null +++ b/code/controllers/subsystem/human_ai.dm @@ -0,0 +1,72 @@ + +SUBSYSTEM_DEF(human_ai) + name = "Human AI" + priority = SS_PRIORITY_HUMAN_AI + flags = SS_NO_INIT + 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() + +/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 = "Debug.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 05301a4191..3de6b343db 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,8 +29,6 @@ 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 @@ -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 @@ -61,12 +59,11 @@ SUBSYSTEM_DEF(xeno_pathfinding) 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) + for(var/atom/A as anything in L) + distance_between += A.xeno_ai_obstacle(current_run.agent, direction, target) if(distance_between < distances[neighbor]) distances[neighbor] = distance_between @@ -126,7 +123,7 @@ 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) @@ -137,23 +134,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 +159,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 +172,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 +195,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 +205,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 f6eaff2713..d5e72b6c51 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -1087,3 +1087,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/explosives/grenades/marines.dm b/code/game/objects/items/explosives/grenades/marines.dm index 36ba614041..8e2915ab7c 100644 --- a/code/game/objects/items/explosives/grenades/marines.dm +++ b/code/game/objects/items/explosives/grenades/marines.dm @@ -12,7 +12,7 @@ name = "\improper M40 HEDP grenade" desc = "High-Explosive Dual-Purpose. A small, but deceptively strong blast grenade that has been phasing out the M15 HE grenades alongside the M40 HEFA. Capable of being loaded in the M92 Launcher, or thrown by hand." icon_state = "grenade" - det_time = 40 + det_time = 100 // zonenote item_state = "grenade_hedp" dangerous = TRUE underslug_launchable = TRUE diff --git a/code/game/objects/items/reagent_containers/autoinjectors.dm b/code/game/objects/items/reagent_containers/autoinjectors.dm index 0666ee6abc..2d48acf9f2 100644 --- a/code/game/objects/items/reagent_containers/autoinjectors.dm +++ b/code/game/objects/items/reagent_containers/autoinjectors.dm @@ -73,6 +73,20 @@ ..() update_icon() +/obj/item/reagent_container/hypospray/autoinjector/ai_can_use(mob/living/carbon/human/user) + if(!uses_left) + return FALSE + + if((user.reagents.get_reagent_amount(chemname) + amount_per_transfer_from_this) > (chemical_reagents_list[chemname].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 +131,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" @@ -293,6 +312,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 fcea8997f0..da6fd8575b 100644 --- a/code/game/objects/items/reagent_containers/hypospray.dm +++ b/code/game/objects/items/reagent_containers/hypospray.dm @@ -13,7 +13,7 @@ possible_transfer_amounts = list(3, 5, 10, 15, 30) flags_atom = FPRINT|OPENCONTAINER flags_equip_slot = SLOT_WAIST - flags_item = NOBLUDGEON + flags_item = NOBLUDGEON|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 f96903cfb6..81ab0d523f 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_item = parent_type::flags_item | 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/ointment name = "ointment" desc = "Used to treat burns, infected wounds, and relieve itching in unusual places." @@ -190,6 +201,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" @@ -199,6 +246,7 @@ heal_brute = 15 stack_id = "mending herbs" alien = TRUE + /obj/item/stack/medical/advanced/ointment/predator name = "soothing herbs" singular_name = "soothing herb" @@ -208,6 +256,7 @@ heal_burn = 15 stack_id = "soothing herbs" alien = TRUE + /obj/item/stack/medical/advanced/ointment name = "burn kit" singular_name = "burn kit" @@ -257,6 +306,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" @@ -314,3 +387,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 49f790410c..5d2b13d42a 100644 --- a/code/game/objects/items/storage/firstaid.dm +++ b/code/game/objects/items/storage/firstaid.dm @@ -346,6 +346,7 @@ ) storage_flags = STORAGE_FLAGS_BOX|STORAGE_CLICK_GATHER|STORAGE_QUICK_GATHER storage_slots = null + flags_item = parent_type::flags_item | HEALING_ITEM use_sound = "pillbottle" max_storage_space = 16 var/skilllock = SKILL_MEDICAL_MEDIC @@ -511,6 +512,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/verb/set_maptext() set category = "Object" set name = "Set short label (on-sprite)" diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 6e47391fc1..e9e0b70468 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -71,6 +71,10 @@ var/list/admin_verbs_default = list( /client/proc/cmd_mod_say, /* alternate way of typing asay, no different than cmd_admin_say */ /client/proc/staffwho, /client/proc/cmd_admin_tacmaps_panel, + /datum/admins/proc/toggle_ai, + /datum/admins/proc/toggle_human_ai, + /datum/admins/proc/create_human_ai_patrol, + /datum/admins/proc/open_human_ai_management_panel, ) var/list/admin_verbs_admin = list( diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index d71a908d62..69aa5e9b53 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..1f0cdc492b --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm @@ -0,0 +1,25 @@ +/datum/ongoing_action/approach_target + name = "Approach Target" + var/atom/movable/target + var/acceptable_distance + +/datum/ongoing_action/approach_target/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + target = arguments[2] + acceptable_distance = arguments[3] + +/datum/ongoing_action/approach_target/Destroy(force, ...) + target = null + return ..() + +/datum/ongoing_action/approach_target/trigger_action() + if(QDELETED(target) || !isturf(target.loc)) + 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/item_pickup.dm b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm new file mode 100644 index 0000000000..de113f8231 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm @@ -0,0 +1,65 @@ +/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 && 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.store_item(to_pickup, storage_spot) + 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.store_item(to_pickup, storage_spot) + 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..912e69440f --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm @@ -0,0 +1,5 @@ +/datum/ongoing_action/order + order = TRUE + +/datum/ongoing_action/order/New(list/arguments) + 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..e292edb7cf --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm @@ -0,0 +1,76 @@ +/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) + if(waiting || QDELETED(current_waypoint_target) || !isturf(current_waypoint_target)) + 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 + 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 = "Debug.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") + waypoint_list += get_turf(usr) + 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/take_cover.dm b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm new file mode 100644 index 0000000000..652e165232 --- /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)) + 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/ai_management_menu.dm b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm new file mode 100644 index 0000000000..31d89049fe --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm @@ -0,0 +1,112 @@ +/datum/human_ai_management_menu + +/datum/human_ai_management_menu/New() + +/datum/human_ai_management_menu/proc/ui_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["on_cooldown"] = !COOLDOWN_FINISHED(src, panel_emote_cooldown) + + return data + +/datum/human_ai_management_menu/ui_static_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() + var/list/order_list = list() + for(var/datum/human_ai_brain/brain as anything in squad.ai_in_squad) + name_list += brain.tied_human?.real_name + for(var/datum/ongoing_action/order/order as anything in squad.assigned_orders) + order_list += order.name + data["squads"] += list(list( + "id" = squad.id, + "members" = english_list(name_list), + "orders" = english_list(order_list), + "ref" = REF(squad), + )) + + 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/human_ai_brain/brain = 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.add_order(locate(params["order"])) + update_static_data(usr, ui) + return TRUE + +/datum/admins/proc/open_human_ai_management_panel() + set name = "Human AI Management Panel" + set category = "Debug.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/datum/human_ai_management_menu/ui = new(usr) + ui.ui_interact(usr) 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..5c3c3ac17b --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm @@ -0,0 +1,351 @@ +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 + + /// 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/list/detection_turfs = list() + + var/in_combat = FALSE + var/combat_decay_time = 30 SECONDS + +/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)) + 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 + GLOB.human_ai_brains -= src + return ..() + +/datum/human_ai_brain/process(delta_time) + if(tied_human.is_mob_incapacitated()) + end_gun_fire() + return + + 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) + bullet_detect(things_around) + + if(!currently_busy && healing_start_check()) + currently_busy = TRUE + start_healing() + + if(!currently_busy && should_reload_primary()) + currently_busy = TRUE + reload_primary() + + if(!currently_busy && !current_target && primary_weapon) + current_target = get_target(view_distance) + RegisterSignal(current_target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_delete)) + + if(!currently_busy && primary_weapon && current_target && !currently_firing && COOLDOWN_FINISHED(src, fire_overload_cooldown) && primary_weapon.has_ammunition()) + currently_busy = TRUE + if(get_dist(tied_human, current_target) > gun_data.maximum_range) + if(!has_ongoing_action(/datum/ongoing_action/approach_target) && !in_cover) + ADD_ONGOING_ACTION(src, /datum/ongoing_action/approach_target, current_target, gun_data.maximum_range) + attack_target() + +/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(/datum/ongoing_action/throw_back_nade)) + 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 = getline2(tied_human, location, include_from_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, /datum/ongoing_action/throw_back_nade, 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/add_ongoing_order(datum/ongoing_action/ref) + if(!ref.order) + stack_trace("Action of [ref.type] was attempted to be added as an order.") + ongoing_orders += ref + +/datum/human_ai_brain/proc/has_ongoing_order(path) + if(!ispath(path)) + return FALSE + + for(var/datum/ongoing_action/action as anything in ongoing_orders) + if(istype(action, path)) + return TRUE + + return FALSE + +/datum/human_ai_brain/proc/bullet_detect(list/things_nearby) + var/obj/projectile/bullet = locate(/obj/projectile) in things_nearby + if(!bullet) + return + + if(ismob(bullet.firer)) + var/mob/firer = bullet.firer + if(!in_cover && !faction_check(firer)) // If it's our own bullets, we don't need to be alarmed + locate_cover(bullet, bullet.dir) + +/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))) + 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) + for(var/obj/structure/barricade/cade in view_contents) + 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, /datum/ongoing_action/take_cover, shortest_cover_turf, cover_atom, FALSE) + return + 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)) + continue + 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, /datum/ongoing_action/take_cover, shortest_cover_turf, cover_atom, TRUE) + return + + + else + 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) + 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, /datum/ongoing_action/take_inside_cover, highest_cover_turf) + +/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, /datum/ongoing_action/take_inside_cover, highest_cover_turf) +#endif + +/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/faction_check(mob/target) + return target?.faction == tied_human.faction + +/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 && !faction_check(bullet.firer)) // If it's our own bullets, we don't need to be alarmed + enter_combat() + locate_cover(bullet, bullet.dir) + +/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 + + in_combat = TRUE + addtimer(CALLBACK(src, PROC_REF(exit_combat)), combat_decay_time, TIMER_UNIQUE | TIMER_OVERRIDE) + +/datum/human_ai_brain/proc/exit_combat() + in_combat = FALSE 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..c371219411 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm @@ -0,0 +1,22 @@ +/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) 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..192d22c1b0 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm @@ -0,0 +1,84 @@ +/datum/human_ai_brain + var/obj/item/weapon/gun/primary_weapon + /// 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() + return (primary_weapon?.current_mag?.current_rounds <= 0) 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..32f5381b57 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm @@ -0,0 +1,224 @@ +/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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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..e52d2b9b0c --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm @@ -0,0 +1,293 @@ +/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(), + ) + + 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, + ) + +/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) + equipped_items_original_loc[object_ref] = object_loc + RegisterSignal(object_ref, COMSIG_ITEM_DROPPED, PROC_REF(on_equipment_dropped)) + +/datum/human_ai_brain/proc/store_item(obj/item/object_ref, object_loc) + 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) + + +/// 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((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(isgun(equipment)) + appraise_inventory(FALSE, FALSE, FALSE, FALSE) + +/datum/human_ai_brain/proc/recalculate_containers() + container_refs = list( + "belt" = tied_human.belt, + "backpack" = tied_human.back, + "left_pocket" = tied_human.l_store, + "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) + 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/belt)) + return + + 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) + for(var/obj/item/inv_item as anything in tied_human.belt) + RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + if(inv_item.flags_item & HEALING_ITEM) + // only trauma kits and similar for now for ease of development + equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = "belt" + else if(inv_item.flags_item & AMMUNITION_ITEM) + equipment_map[HUMAN_AI_AMMUNITION][inv_item] = "belt" + + if(back) + if(!istype(tied_human.back, /obj/item/storage/backpack)) + return + + 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) + for(var/obj/item/inv_item as anything in tied_human.back) + RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + if(inv_item.flags_item & HEALING_ITEM) + // only trauma kits and similar for now for ease of development + equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = "backpack" + else if(inv_item.flags_item & AMMUNITION_ITEM) + equipment_map[HUMAN_AI_AMMUNITION][inv_item] = "backpack" + + if(pocket_l) + if(!istype(tied_human.l_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] != "left_pocket") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.l_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + for(var/obj/item/inv_item as anything in tied_human.l_store) + RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + if(inv_item.flags_item & HEALING_ITEM) + // only trauma kits and similar for now for ease of development + equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = "left_pocket" + else if(inv_item.flags_item & AMMUNITION_ITEM) + equipment_map[HUMAN_AI_AMMUNITION][inv_item] = "left_pocket" + + 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) + for(var/obj/item/inv_item as anything in tied_human.r_store) + RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + if(inv_item.flags_item & HEALING_ITEM) + // only trauma kits and similar for now for ease of development + equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = "right_pocket" + else if(inv_item.flags_item & AMMUNITION_ITEM) + equipment_map[HUMAN_AI_AMMUNITION][inv_item] = "right_pocket" + +/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, /datum/ongoing_action/item_pickup, 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 + +/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/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) + for(var/obj/item/thing in things_around) + if(!isturf(thing.loc)) + continue + + if(!primary_weapon && isgun(thing)) + ADD_ONGOING_ACTION(src, /datum/ongoing_action/item_pickup, thing) + break + + if(istype(thing, /obj/item/storage/belt) && !container_refs["belt"]) + ADD_ONGOING_ACTION(src, /datum/ongoing_action/item_pickup, thing) + break + + if(istype(thing, /obj/item/storage/backpack) && !container_refs["backpack"]) + ADD_ONGOING_ACTION(src, /datum/ongoing_action/item_pickup, thing) + break + + if(istype(thing, /obj/item/storage/pouch) && (!container_refs["left_pocket"] || !container_refs["right_pocket"])) + ADD_ONGOING_ACTION(src, /datum/ongoing_action/item_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, /datum/ongoing_action/item_pickup, thing) + break + + else 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, /datum/ongoing_action/item_pickup, thing) + break + 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..130a7f5483 --- /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 = 24 + 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 = ai_range) + 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.xeno_ai_obstacle(tied_human, get_dir(tied_human.loc, next_turf)) == INFINITY) + return FALSE + //INVOKE_ASYNC(A, TYPE_PROC_REF(/atom, xeno_ai_act), tied_human) + 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..ff2baac52f --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm @@ -0,0 +1,60 @@ +/datum/human_ai_squad + /// Numeric ID of the squad + var/id + /// The AI humans in the squad + var/list/ai_in_squad = list() + /// List of orders assigned to this squad + var/list/assigned_orders = list() + +/datum/human_ai_squad/New() + . = ..() + SShuman_ai.squads += src + SShuman_ai.highest_squad_id++ + id = SShuman_ai.highest_squad_id + +/datum/human_ai_squad/Destroy(force, ...) + SShuman_ai.squads -= src + 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 + + for(var/datum/ongoing_action/order as anything in assigned_orders) + adding.add_ongoing_order(order) + +/datum/human_ai_squad/proc/remove_from_squad(datum/human_ai_brain/removing) + for(var/datum/ongoing_action/order as anything in assigned_orders) + removing.ongoing_orders -= order + removing.squad_id = null + ai_in_squad -= removing + +/datum/human_ai_squad/proc/add_order(datum/ongoing_action/order) + for(var/datum/human_ai_brain/brain as anything in ai_in_squad) + brain.add_ongoing_order(order) + +/datum/human_ai_squad/proc/tgui_data() + var/list/order_name_list = list() + for(var/datum/ongoing_action/order/order as anything in assigned_orders) + order_name_list += order.name + return list( + "id" = id, + "orders" = order_name_list, + ) + +/datum/human_ai_brain + /// Numeric ID of the squad this AI is in, if any + var/squad_id + +/datum/human_ai_brain/proc/add_to_squad(new_id) + if(isnull(new_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) 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..babc5238e0 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm @@ -0,0 +1,289 @@ +#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 = TRUE + /// List of overwatched turfs + var/list/turf/open/overwatch_turfs = list() + +/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(target.faction == tied_human.faction) + 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 + + if(get_dist(tied_human, current_target) > gun_data.maximum_range) + currently_busy = FALSE + return + + primary_weapon.set_target(current_target) + ensure_primary_hand(primary_weapon) + if(primary_weapon.flags_item & TWOHANDED && !(primary_weapon.flags_item & WIELDED)) + primary_weapon.wield(tied_human) + sleep(max(primary_weapon.wield_delay, short_action_delay * action_delay_mult)) + if(istype(primary_weapon, /obj/item/weapon/gun/shotgun/pump) && !primary_weapon.in_chamber) + var/obj/item/weapon/gun/shotgun/pump/shotgun = primary_weapon + shotgun.pump_shotgun(tied_human) + shotgun.recent_pump = world.time + else if(istype(primary_weapon, /obj/item/weapon/gun/boltaction) && !primary_weapon.in_chamber) + var/obj/item/weapon/gun/boltaction/bolt = primary_weapon + bolt.unique_action(tied_human) + bolt.recent_cycle = world.time + bolt.unique_action(tied_human) + bolt.recent_cycle = world.time + if(!primary_weapon.in_chamber) + end_gun_fire() + return + + currently_firing = TRUE + enter_combat() + RegisterSignal(tied_human, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fire)) + primary_weapon.start_fire(object = current_target, bypass_checks = 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(get_dist(tied_human, current_target) > gun_data.maximum_range) + end_gun_fire() + return + + if(QDELETED(current_target)) + end_gun_fire() + return + + if(primary_weapon.current_mag?.current_rounds <= 0) + end_gun_fire() + return + + if(!(current_target in viewers(world.view, tied_human))) + end_gun_fire() + if(overwatch_allowed) + establish_overwatch() + 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) + +/datum/human_ai_brain/proc/end_gun_fire() + primary_weapon?.set_target(null) + 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() + +#undef EXTRA_CHECK_DISTANCE_MULTIPLIER 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..85caacba45 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm @@ -0,0 +1,37 @@ +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 = 0 + /// Maximum engagement range + var/maximum_range = 7 + /// 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() + +/datum/firearm_appraisal/rifle + burst_amount_max = 8 + gun_types = list( + /obj/item/weapon/gun/rifle, + ) + +/datum/firearm_appraisal/smg + burst_amount_max = 10 + maximum_range = 6 + gun_types = list( + /obj/item/weapon/gun/smg, + ) + +/datum/firearm_appraisal/shotgun + burst_amount_max = 2 + maximum_range = 3 + gun_types = list( + /obj/item/weapon/gun/shotgun, + ) 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..e9fa833154 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/human_ai.dm @@ -0,0 +1,36 @@ +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 + GLOB.ai_humans += src + ai_brain = new(src) + create_hud() + +/mob/living/carbon/human/Destroy(force) + 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 + +/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/human.dm b/code/modules/mob/living/carbon/human/human.dm index 837f193c4c..cdaed7986f 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 22deabce45..f529f32e59 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -106,6 +106,8 @@ if(!. || !I) return FALSE + var/slot + if(I == wear_suit) if(s_store && !(s_store.flags_equip_slot & SLOT_SUIT_STORE)) drop_inv_item_on_ground(s_store) @@ -117,6 +119,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) @@ -127,6 +130,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) @@ -144,43 +148,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) @@ -490,7 +505,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) @@ -502,7 +517,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/stripPanelUnequip(obj/item/interact_item, mob/target_mob, slot_to_process) if(HAS_TRAIT(target_mob, TRAIT_UNSTRIPPABLE) && !target_mob.is_mob_incapacitated()) //Can't strip the unstrippable! 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 cf951007be..5d046cf098 100644 --- a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm +++ b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm @@ -47,7 +47,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) @@ -179,12 +179,12 @@ 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(!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. @@ -207,7 +207,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 594ad6b69d..9ce52424ae 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_item = parent_type::flags_item | 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) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 6ab143b8d8..b0808244c7 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -2008,3 +2008,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/colonialmarines.dme b/colonialmarines.dme index 58cee33fcf..0d6878c23e 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -66,6 +66,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" @@ -250,6 +251,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" @@ -1900,8 +1902,27 @@ #include "code\modules\mob\living\carbon\human\logout.dm" #include "code\modules\mob\living\carbon\human\say.dm" #include "code\modules\mob\living\carbon\human\unarmed_attacks.dm" +#include "code\modules\mob\living\carbon\human\ai\ai_management_menu.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_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\update_icons.dm" #include "code\modules\mob\living\carbon\human\whisper.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..afd5570be8 --- /dev/null +++ b/tgui/packages/tgui/interfaces/HumanAIManager.tsx @@ -0,0 +1,206 @@ +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, LabeledList, NoticeBox, ProgressBar, Section, Dimmer, Stack, Tabs, Divider } from '../components'; +import { Window } from '../layouts'; +import { InterfaceLockNoticeBox } from './common/InterfaceLockNoticeBox'; +import { classes, BooleanLike } from 'common/react'; + +type Squad = { + id: number; + orders: string; + members: string; + ref: 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(context); + const [categoryIndex, setCategoryIndex] = useLocalState( + context, + 'category_index', + 'Farewell' + ); + 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(context); + return ( +
+
+
+
+ ); +}; + +const HumanAIReadout = (props) => { + const human: AIHuman = props.human; + const context: BackendContext = props.context; + const { data, act } = useBackend(context); + return ( +
+
+
+
+
+
+ Health: {human.health}%
+ Faction: {human.faction}
+ In Combat: {human.in_combat}
+ Squad #: {human.squad}
+ 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(context); + return ( +
+
+
+
+ ); +}; + +export const HumanAIManager = (props) => { + return ( + + + + + + ); +}; From c6b8a35164de6e07e303b23a369a086d75018fba Mon Sep 17 00:00:00 2001 From: John Doe Date: Sun, 8 Sep 2024 12:06:35 -0700 Subject: [PATCH 02/42] good enough for a technical test, probably. --- code/__DEFINES/human_ai.dm | 2 + .../items/explosives/grenades/marines.dm | 2 +- code/modules/admin/admin_verbs.dm | 3 +- code/modules/client/client_defines.dm | 3 + code/modules/client/client_procs.dm | 1 + .../ai/action_datums/orders/order_action.dm | 1 + .../action_datums/orders/patrol_waypoints.dm | 16 +- .../human/ai/action_datums/take_cover.dm | 2 +- .../living/carbon/human/ai/ai_equipment.dm | 25 ++ .../carbon/human/ai/ai_management_menu.dm | 51 +++- .../living/carbon/human/ai/brain/ai_brain.dm | 144 +++------- .../human/ai/brain/ai_brain_communication.dm | 2 + .../carbon/human/ai/brain/ai_brain_cover.dm | 162 ++++++++++++ .../human/ai/brain/ai_brain_factions.dm | 14 + .../carbon/human/ai/brain/ai_brain_items.dm | 19 +- .../human/ai/brain/ai_brain_pathfinding.dm | 4 +- .../carbon/human/ai/brain/ai_brain_squad.dm | 55 ++-- .../human/ai/brain/ai_brain_targeting.dm | 24 +- .../mob/living/carbon/human/ai/human_ai.dm | 6 + colonialmarines.dme | 3 + .../tgui/interfaces/HumanAIManager.tsx | 247 ++++++++++++++---- 21 files changed, 571 insertions(+), 215 deletions(-) create mode 100644 code/modules/mob/living/carbon/human/ai/ai_equipment.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm create mode 100644 code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm diff --git a/code/__DEFINES/human_ai.dm b/code/__DEFINES/human_ai.dm index a857498edf..076224e13f 100644 --- a/code/__DEFINES/human_ai.dm +++ b/code/__DEFINES/human_ai.dm @@ -9,3 +9,5 @@ #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/game/objects/items/explosives/grenades/marines.dm b/code/game/objects/items/explosives/grenades/marines.dm index 8e2915ab7c..36ba614041 100644 --- a/code/game/objects/items/explosives/grenades/marines.dm +++ b/code/game/objects/items/explosives/grenades/marines.dm @@ -12,7 +12,7 @@ name = "\improper M40 HEDP grenade" desc = "High-Explosive Dual-Purpose. A small, but deceptively strong blast grenade that has been phasing out the M15 HE grenades alongside the M40 HEFA. Capable of being loaded in the M92 Launcher, or thrown by hand." icon_state = "grenade" - det_time = 100 // zonenote + det_time = 40 item_state = "grenade_hedp" dangerous = TRUE underslug_launchable = TRUE diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index e9e0b70468..e91694ef3a 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -74,7 +74,8 @@ var/list/admin_verbs_default = list( /datum/admins/proc/toggle_ai, /datum/admins/proc/toggle_human_ai, /datum/admins/proc/create_human_ai_patrol, - /datum/admins/proc/open_human_ai_management_panel, + /client/proc/open_human_ai_management_panel, + /client/proc/create_human_ai, ) var/list/admin_verbs_admin = list( diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index f09023408f..4773252147 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -134,3 +134,6 @@ /// 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 diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 5dc0600287..36c2460dca 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -477,6 +477,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/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 index 912e69440f..87e080d807 100644 --- 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 @@ -1,5 +1,6 @@ /datum/ongoing_action/order order = TRUE + var/can_perform_in_combat = FALSE /datum/ongoing_action/order/New(list/arguments) 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 index e292edb7cf..c3343ffa4f 100644 --- 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 @@ -17,8 +17,8 @@ current_waypoint_target = null return ..() -/datum/ongoing_action/order/patrol_waypoints/trigger_action(datum) - if(waiting || QDELETED(current_waypoint_target) || !isturf(current_waypoint_target)) +/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) @@ -27,8 +27,9 @@ if(get_dist(current_waypoint_target, brain.tied_human) > 1) return ONGOING_ACTION_UNFINISHED - waiting = TRUE - addtimer(CALLBACK(src, PROC_REF(set_next_waypoint)), time_at_waypoint) + 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() @@ -63,7 +64,12 @@ 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") - waypoint_list += get_turf(usr) + 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 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 index 652e165232..85b88b4b25 100644 --- 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 @@ -28,5 +28,5 @@ 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)) + 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/ai_equipment.dm b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm new file mode 100644 index 0000000000..be27f6be54 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm @@ -0,0 +1,25 @@ +/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/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) 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 index 31d89049fe..587784c358 100644 --- a/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm +++ b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm @@ -50,16 +50,14 @@ data["squads"] = list() for(var/datum/human_ai_squad/squad as anything in SShuman_ai.squads) var/list/name_list = list() - var/list/order_list = list() for(var/datum/human_ai_brain/brain as anything in squad.ai_in_squad) name_list += brain.tied_human?.real_name - for(var/datum/ongoing_action/order/order as anything in squad.assigned_orders) - order_list += order.name data["squads"] += list(list( "id" = squad.id, "members" = english_list(name_list), - "orders" = english_list(order_list), + "order" = squad.assigned_order?.name, "ref" = REF(squad), + "squad_leader" = squad.squad_leader?.tied_human?.real_name, )) return data @@ -80,33 +78,64 @@ 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/human_ai_brain/brain = params["ai"] - brain.add_to_squad(params["squad"]) + 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.add_order(locate(params["order"])) + 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("refresh") update_static_data(usr, ui) return TRUE -/datum/admins/proc/open_human_ai_management_panel() +/client/proc/open_human_ai_management_panel() set name = "Human AI Management Panel" set category = "Debug.HumanAI" if(!check_rights(R_DEBUG)) return - var/datum/human_ai_management_menu/ui = new(usr) - ui.ui_interact(usr) + if(human_ai_menu) + human_ai_menu.ui_interact(mob) + return + + human_ai_menu = new /datum/human_ai_management_menu(src) + +/client/proc/create_human_ai() + set name = "Create Human AI" + set category = "Debug.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/mob/living/carbon/human/ai/ai_human = new() + cmd_admin_dress_human(ai_human) + 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 index 5c3c3ac17b..59c1cf703e 100644 --- a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm @@ -25,12 +25,14 @@ GLOBAL_LIST_EMPTY(human_ai_brains) var/list/ongoing_actions = list() /// List of semi-permanent "order" action datums. These do not expire - var/list/ongoing_orders = list() + //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 /datum/human_ai_brain/New(mob/living/carbon/human/tied_human) . = ..() @@ -51,6 +53,7 @@ GLOBAL_LIST_EMPTY(human_ai_brains) set_primary_weapon(null) current_target = null target_floor = null + ongoing_order = null GLOB.human_ai_brains -= src return ..() @@ -59,6 +62,13 @@ GLOBAL_LIST_EMPTY(human_ai_brains) 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) @@ -73,7 +83,7 @@ GLOBAL_LIST_EMPTY(human_ai_brains) // Might be wise to move these off tick and instead make them signal-based nade_throwback(things_around) item_search(things_around) - bullet_detect(things_around) + //bullet_detect(things_around) if(!currently_busy && healing_start_check()) currently_busy = TRUE @@ -184,20 +194,16 @@ GLOBAL_LIST_EMPTY(human_ai_brains) return FALSE -/datum/human_ai_brain/proc/add_ongoing_order(datum/ongoing_action/ref) +/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_orders += ref + ongoing_order = ref /datum/human_ai_brain/proc/has_ongoing_order(path) - if(!ispath(path)) - return FALSE - - for(var/datum/ongoing_action/action as anything in ongoing_orders) - if(istype(action, path)) - return TRUE - - return FALSE + return istype(ongoing_order, path) /datum/human_ai_brain/proc/bullet_detect(list/things_nearby) var/obj/projectile/bullet = locate(/obj/projectile) in things_nearby @@ -209,106 +215,6 @@ GLOBAL_LIST_EMPTY(human_ai_brains) if(!in_cover && !faction_check(firer)) // If it's our own bullets, we don't need to be alarmed locate_cover(bullet, bullet.dir) -/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))) - 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) - for(var/obj/structure/barricade/cade in view_contents) - 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, /datum/ongoing_action/take_cover, shortest_cover_turf, cover_atom, FALSE) - return - 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)) - continue - 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, /datum/ongoing_action/take_cover, shortest_cover_turf, cover_atom, TRUE) - return - - - else - 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) - 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, /datum/ongoing_action/take_inside_cover, highest_cover_turf) - -/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, /datum/ongoing_action/take_inside_cover, highest_cover_turf) -#endif - -/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/faction_check(mob/target) return target?.faction == tied_human.faction @@ -334,9 +240,10 @@ GLOBAL_LIST_EMPTY(human_ai_brains) if(istype(entering, /obj/projectile)) var/obj/projectile/bullet = entering if(ismob(bullet.firer)) - if(!in_cover && !faction_check(bullet.firer)) // If it's our own bullets, we don't need to be alarmed + if(!in_cover && !squad_covering && !faction_check(bullet.firer)) // If it's our own bullets, we don't need to be alarmed enter_combat() - locate_cover(bullet, bullet.dir) + 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() @@ -349,3 +256,12 @@ GLOBAL_LIST_EMPTY(human_ai_brains) /datum/human_ai_brain/proc/exit_combat() 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 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..b8d5fd7d9c --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm @@ -0,0 +1,2 @@ +/datum/human_ai_brain + var/list/in_combat_lines = list() 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 index c371219411..144648ee68 100644 --- 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 @@ -20,3 +20,165 @@ 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, /datum/ongoing_action/take_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, /datum/ongoing_action/take_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, /datum/ongoing_action/take_inside_cover, 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, /datum/ongoing_action/take_inside_cover, highest_cover_turf) + if(!from_squad) + squad_cover_processing(TRUE, view_contents - 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..f31827412a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm @@ -0,0 +1,14 @@ +GLOBAL_LIST_INIT(human_ai_factions, assemble_human_ai_factions()) + +/proc/assemble_human_ai_factions() + . = list() + for(var/datum/human_ai_faction/faction as anything in subtypesof(/datum/human_ai_faction)) + faction = new + .[faction.faction] = faction + +/datum/human_ai_faction + var/faction = FACTION_NEUTRAL + var/shoot_to_kill = TRUE + +/datum/human_ai_faction/proc/apply_faction_data(datum/human_ai_brain/brain) + return 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 index e52d2b9b0c..e7d0553282 100644 --- 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 @@ -45,7 +45,7 @@ storage_object.remove_from_storage(object_ref, tied_human) equipped_items_original_loc[object_ref] = object_loc - RegisterSignal(object_ref, COMSIG_ITEM_DROPPED, PROC_REF(on_equipment_dropped)) + RegisterSignal(object_ref, COMSIG_ITEM_DROPPED, PROC_REF(on_equipment_dropped), override = TRUE) /datum/human_ai_brain/proc/store_item(obj/item/object_ref, object_loc) if(object_ref.loc != tied_human) @@ -95,12 +95,15 @@ appraise_inventory(FALSE, FALSE, FALSE, FALSE) /datum/human_ai_brain/proc/recalculate_containers() - container_refs = list( - "belt" = tied_human.belt, - "backpack" = tied_human.back, - "left_pocket" = tied_human.l_store, - "right_pocket" = tied_human.r_store, - ) + 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) @@ -252,7 +255,7 @@ if(is_type_in_list(primary_weapon, appraisal.gun_types)) gun_data = appraisal break - + if(!gun_data) gun_data = default 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 index 130a7f5483..4d93454a64 100644 --- 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 @@ -4,7 +4,7 @@ var/turf/current_target_turf var/path_update_period = (0.5 SECONDS) var/no_path_found = FALSE - var/max_travel_distance = 24 + 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 @@ -30,7 +30,7 @@ tied_human.next_move_slowdown = 0 return TRUE -/datum/human_ai_brain/proc/move_to_next_turf(turf/T, max_range = ai_range) +/datum/human_ai_brain/proc/move_to_next_turf(turf/T, max_range = max_travel_distance) if(!T) return FALSE 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 index ff2baac52f..93fbb98f36 100644 --- 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 @@ -3,17 +3,18 @@ var/id /// The AI humans in the squad var/list/ai_in_squad = list() - /// List of orders assigned to this squad - var/list/assigned_orders = 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() . = ..() - SShuman_ai.squads += src - SShuman_ai.highest_squad_id++ id = SShuman_ai.highest_squad_id /datum/human_ai_squad/Destroy(force, ...) SShuman_ai.squads -= src + squad_leader = null return ..() /datum/human_ai_squad/proc/add_to_squad(datum/human_ai_brain/adding) @@ -23,38 +24,52 @@ adding.squad_id = id ai_in_squad += adding - for(var/datum/ongoing_action/order as anything in assigned_orders) - adding.add_ongoing_order(order) + adding.set_ongoing_order(assigned_order) /datum/human_ai_squad/proc/remove_from_squad(datum/human_ai_brain/removing) - for(var/datum/ongoing_action/order as anything in assigned_orders) - removing.ongoing_orders -= order + if(removing == squad_leader) + squad_leader = null + removing.ongoing_order = null removing.squad_id = null + removing.is_squad_leader = FALSE ai_in_squad -= removing -/datum/human_ai_squad/proc/add_order(datum/ongoing_action/order) +/datum/human_ai_squad/proc/set_order(datum/ongoing_action/order) + assigned_order = order for(var/datum/human_ai_brain/brain as anything in ai_in_squad) - brain.add_ongoing_order(order) + brain.set_ongoing_order(order) -/datum/human_ai_squad/proc/tgui_data() - var/list/order_name_list = list() - for(var/datum/ongoing_action/order/order as anything in assigned_orders) - order_name_list += order.name - return list( - "id" = id, - "orders" = order_name_list, - ) +/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 + new_leader.is_squad_leader = TRUE /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)) + 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] + 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 index babc5238e0..430c8d9281 100644 --- 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 @@ -159,7 +159,7 @@ bolt.recent_cycle = world.time bolt.unique_action(tied_human) bolt.recent_cycle = world.time - if(!primary_weapon.in_chamber) + if(!primary_weapon.in_chamber || !friendly_check()) end_gun_fire() return @@ -168,6 +168,23 @@ RegisterSignal(tied_human, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fire)) primary_weapon.start_fire(object = current_target, bypass_checks = TRUE) +/datum/human_ai_brain/proc/friendly_check() + var/list/turf_list = getline2(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 @@ -195,7 +212,7 @@ end_gun_fire() return - if(QDELETED(current_target)) + if(QDELETED(current_target) || !friendly_check()) end_gun_fire() return @@ -230,7 +247,8 @@ /datum/human_ai_brain/proc/end_gun_fire() primary_weapon?.set_target(null) - UnregisterSignal(current_target, COMSIG_PARENT_QDELETING) + if(current_target) + UnregisterSignal(current_target, COMSIG_PARENT_QDELETING) UnregisterSignal(tied_human, COMSIG_MOB_FIRED_GUN) current_target = null currently_busy = FALSE diff --git a/code/modules/mob/living/carbon/human/ai/human_ai.dm b/code/modules/mob/living/carbon/human/ai/human_ai.dm index e9fa833154..bf840f102a 100644 --- a/code/modules/mob/living/carbon/human/ai/human_ai.dm +++ b/code/modules/mob/living/carbon/human/ai/human_ai.dm @@ -12,9 +12,15 @@ GLOBAL_LIST_EMPTY(ai_humans) 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() + //INVOKE_ASYNC(src) /mob/living/carbon/human/Destroy(force) QDEL_NULL(ai_brain) diff --git a/colonialmarines.dme b/colonialmarines.dme index 0d6878c23e..61a0ed2b84 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -1902,9 +1902,12 @@ #include "code\modules\mob\living\carbon\human\logout.dm" #include "code\modules\mob\living\carbon\human\say.dm" #include "code\modules\mob\living\carbon\human\unarmed_attacks.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\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" diff --git a/tgui/packages/tgui/interfaces/HumanAIManager.tsx b/tgui/packages/tgui/interfaces/HumanAIManager.tsx index afd5570be8..1e7798006a 100644 --- a/tgui/packages/tgui/interfaces/HumanAIManager.tsx +++ b/tgui/packages/tgui/interfaces/HumanAIManager.tsx @@ -6,9 +6,11 @@ import { classes, BooleanLike } from 'common/react'; type Squad = { id: number; - orders: string; + order: string; members: string; ref: string; + primary_order: string; + squad_leader: string; }; type AIHuman = { @@ -37,10 +39,15 @@ type BackendContext = { const AIContext = (props, context) => { const { data, act } = useBackend(context); - const [categoryIndex, setCategoryIndex] = useLocalState( + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( context, - 'category_index', - 'Farewell' + 'squad_assignment_mode', + false + ); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + context, + 'order_assignment_mode', + false ); return ( @@ -51,7 +58,7 @@ const AIContext = (props, context) => { 'display': 'inline-block', 'padding-right': '6px', }}> -