diff --git a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm index e2e2bb3a1d..fe088d8b20 100644 --- a/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm +++ b/code/__DEFINES/dcs/signals/atom/mob/living/signals_human.dm @@ -56,6 +56,9 @@ //from /mob/living/carbon/human/equip_to_slot() #define COMSIG_HUMAN_EQUIPPED_ITEM "human_equipped_item" +//from /mob/living/carbon/human/u_equip() +#define COMSIG_HUMAN_UNEQUIPPED_ITEM "human_unequipped_item" + /// From /mob/proc/equip_to_slot_if_possible() #define COMSIG_HUMAN_ATTEMPTING_EQUIP "human_attempting_equip" #define COMPONENT_HUMAN_CANCEL_ATTEMPT_EQUIP (1<<0) diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm index fa50103e08..8df1ede132 100644 --- a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm +++ b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm @@ -186,6 +186,8 @@ /// Cancels all running cloaking effects on target #define COMSIG_MOB_EFFECT_CLOAK_CANCEL "mob_effect_cloak_cancel" +#define COMSIG_MOB_DROP_ITEM "mob_drop_item" + #define COMSIG_MOB_END_TUTORIAL "mob_end_tutorial" #define COMSIG_MOB_NESTED "mob_nested" diff --git a/code/__DEFINES/equipment.dm b/code/__DEFINES/equipment.dm index 5d5b81bf8c..79c72950d6 100644 --- a/code/__DEFINES/equipment.dm +++ b/code/__DEFINES/equipment.dm @@ -566,3 +566,15 @@ GLOBAL_LIST_INIT(uniform_categories, list( #define PHONE_ON_BASE_UNIT_ICON_STATE "[initial(icon_state)]" #define PHONE_OFF_BASE_UNIT_ICON_STATE "[initial(icon_state)]_ear" #define PHONE_RINGING_ICON_STATE "[initial(icon_state)]_ring" + +// Human AI flags +/// This item is classified as a healing item for the sake of human AI +#define HEALING_ITEM (1<<0) +/// This item is classified as ammunition for the sake of human AI +#define AMMUNITION_ITEM (1<<1) +/// This item is classified as a grenade for the sake of human AI +#define GRENADE_ITEM (1<<2) +/// This item is classified as a tool for the sake of human AI +#define TOOL_ITEM (1<<3) +/// This item is classified as a melee weapon for the sake of human AI +#define MELEE_WEAPON_ITEM (1<<4) diff --git a/code/__DEFINES/human_ai.dm b/code/__DEFINES/human_ai.dm new file mode 100644 index 0000000000..408567d46c --- /dev/null +++ b/code/__DEFINES/human_ai.dm @@ -0,0 +1,26 @@ +#define HUMAN_AI_HEALTHITEMS "health" +#define HUMAN_AI_AMMUNITION "ammo" +#define HUMAN_AI_GRENADES "grenades" +#define HUMAN_AI_TOOLS "tools" + +#define AI_ACTION_APPROACH /datum/ongoing_action/approach_target +#define AI_ACTION_APPROACH_CAREFUL /datum/ongoing_action/approach_target/carefully +#define AI_ACTION_RETREAT /datum/ongoing_action/retreat_from_target +#define AI_ACTION_PICKUP /datum/ongoing_action/item_pickup +#define AI_ACTION_PICKUP_GUN /datum/ongoing_action/item_pickup/pickup_primary +#define AI_ACTION_COVER /datum/ongoing_action/take_cover +#define AI_ACTION_COVER_I /datum/ongoing_action/take_inside_cover +#define AI_ACTION_THROWBACK /datum/ongoing_action/throw_back_nade +#define AI_ACTION_NADE /datum/ongoing_action/throw_grenade +#define AI_ACTION_MELEE_ATOM /datum/ongoing_action/melee_atom + +/// Action is completed, delete this and move onto the next ongoing action +#define ONGOING_ACTION_COMPLETED "completed" +/// Action isn't finished, move onto the next ongoing action +#define ONGOING_ACTION_UNFINISHED "unfinished" +/// Action isn't finished, block any further actions from the AI this tick +#define ONGOING_ACTION_UNFINISHED_BLOCK "unfinished_block" + +#define ADD_ONGOING_ACTION(brain, path, arguments...) brain:_add_ongoing_action(path, ##arguments) + +#define HUMAN_AI_MAX_PATHFINDING_RANGE 45 diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 989c39cd7d..49c7a71a77 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -163,6 +163,7 @@ #define SS_PRIORITY_SOUND 250 #define SS_PRIORITY_TICKER 200 #define SS_PRIORITY_XENO_AI 185 +#define SS_PRIORITY_HUMAN_AI 182 #define SS_PRIORITY_NIGHTMARE 180 #define SS_PRIORITY_QUADTREE 160 #define SS_PRIORITY_CHAT 155 @@ -172,7 +173,7 @@ #define SS_PRIORITY_MOB 150 #define SS_PRIORITY_XENO 149 #define SS_PRIORITY_HUMAN 148 -#define SS_PRIORITY_XENO_PATHFINDING 130 +#define SS_PRIORITY_PATHFINDING 130 #define SS_PRIORITY_STAMINA 126 #define SS_PRIORITY_COMPONENT 125 #define SS_PRIORITY_NANOUI 120 diff --git a/code/__DEFINES/xeno_ai.dm b/code/__DEFINES/xeno_ai.dm index d7cbe1bf22..72383fc969 100644 --- a/code/__DEFINES/xeno_ai.dm +++ b/code/__DEFINES/xeno_ai.dm @@ -1,4 +1,4 @@ -#define XENO_CALCULATING_PATH(X) (X in SSxeno_pathfinding.hash_path) +#define CALCULATING_PATH(X) (X in SSpathfinding.hash_path) #define DIRECTION_CHANGE_PENALTY 2 #define NO_WEED_PENALTY 2 @@ -91,7 +91,7 @@ PROBABILITY CALCULATIONS ARE HERE /// Special blockers for pathfinding or obstacle handling -#define XENO_AI_SPECIAL_BLOCKERS list(/obj/flamer_fire, /obj/vehicle/multitile, /turf/open/space, /turf/open/gm/river) +#define AI_SPECIAL_BLOCKERS list(/obj/flamer_fire, /obj/vehicle/multitile, /turf/open/space, /turf/open/gm/river) // Friend-or-foe universal check #define IS_SAME_HIVENUMBER(A,B) (A.hivenumber == B.hivenumber) diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index b9a9f276c6..d97b448f66 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -46,8 +46,8 @@ #define format_frequency(f) "[floor((f) / 10)].[(f) % 10]" #define reverse_direction(direction) ( \ - ( dir & (NORTH|SOUTH) ? ~dir & (NORTH|SOUTH) : 0 ) | \ - ( dir & (EAST|WEST) ? ~dir & (EAST|WEST) : 0 ) \ + ( direction & (NORTH|SOUTH) ? ~direction & (NORTH|SOUTH) : 0 ) | \ + ( direction & (EAST|WEST) ? ~direction & (EAST|WEST) : 0 ) \ ) // The sane, counter-clockwise angle to turn to get from /direction/ A to /direction/ B @@ -1735,6 +1735,33 @@ GLOBAL_LIST_INIT(duplicate_forbidden_vars,list( if(NORTHWEST) return list(NORTHWEST, NORTH, WEST) +/// Makes a given dir cardinal. If the dir is non-cardinal, it will return both cardinal directions that make up the direction. Else, it will be a single-entry list returned. +/proc/make_dir_cardinal(direction) + switch(direction) + if(NORTH) + return list(NORTH) + + if(EAST) + return list(EAST) + + if(SOUTH) + return list(SOUTH) + + if(WEST) + return list(WEST) + + if(NORTHEAST) + return list(NORTH, EAST) + + if(SOUTHEAST) + return list(EAST, SOUTH) + + if(SOUTHWEST) + return list(SOUTH, WEST) + + if(NORTHWEST) + return list(NORTH, WEST) + /// Returns TRUE if the target is somewhere that the game should not interact with if possible /// In this case, admin Zs and tutorial areas /proc/should_block_game_interaction(atom/target) diff --git a/code/_compile_options.dm b/code/_compile_options.dm index 20aa208131..1c3f335a57 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -38,6 +38,6 @@ //#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between -// #define TESTING +#define TESTING // #define REFERENCE_TRACKING // #define GC_FAILURE_HARD_LOOKUP diff --git a/code/controllers/subsystem/human_ai.dm b/code/controllers/subsystem/human_ai.dm new file mode 100644 index 0000000000..9d582c231f --- /dev/null +++ b/code/controllers/subsystem/human_ai.dm @@ -0,0 +1,79 @@ + +SUBSYSTEM_DEF(human_ai) + name = "Human AI" + priority = SS_PRIORITY_HUMAN_AI + wait = 0.2 SECONDS + /// A list of mobs scheduled to process + var/list/mob/living/carbon/human/ai/current_run = list() + + var/ai_kill = FALSE + + /// List of current squads + var/list/datum/human_ai_squad/squads = list() + + /// Dict of "id" : squad + var/list/squad_id_dict = list() + + /// The current highest ID of any squad + var/highest_squad_id = 0 + + /// List of all existing orders + var/list/datum/ongoing_action/existing_orders = list() + + var/list/human_ai_factions = list() + +/datum/controller/subsystem/human_ai/Initialize() + for(var/faction_path in subtypesof(/datum/human_ai_faction)) + var/datum/human_ai_faction/faction_obj = new faction_path + human_ai_factions[faction_obj.faction] = faction_obj + return SS_INIT_SUCCESS + +/datum/controller/subsystem/human_ai/stat_entry(msg) + msg = "P:[length(GLOB.human_ai_brains)]" + return ..() + +/datum/admins/proc/toggle_human_ai() + set name = "Toggle Human AI" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + SShuman_ai.ai_kill = !SShuman_ai.ai_kill + message_admins("[key_name_admin(usr)] [SShuman_ai.ai_kill? "killed" : "revived"] all human AI.") + +/datum/controller/subsystem/human_ai/fire(resumed = FALSE) + if(ai_kill) + return + + if(!resumed) + src.current_run = GLOB.human_ai_brains.Copy() + // Cache for sanic speed (lists are references anyways) + var/list/current_run = src.current_run + while(length(current_run)) + var/datum/human_ai_brain/brain = current_run[length(current_run)] + current_run.len-- + if(!QDELETED(brain) && !brain.tied_human?.client) + brain.process(wait * 0.1) + + if(MC_TICK_CHECK) + return + +/datum/controller/subsystem/human_ai/proc/create_new_squad() + highest_squad_id++ + var/datum/human_ai_squad/new_squad = new + squads += new_squad + squad_id_dict["[highest_squad_id]"] = new_squad + +/datum/controller/subsystem/human_ai/proc/get_squad(squad_id) + RETURN_TYPE(/datum/human_ai_squad) + + if(!squad_id || !(squad_id in squad_id_dict)) + return null + return squad_id_dict[squad_id] + +/datum/controller/subsystem/human_ai/proc/create_new_order(datum/ongoing_action/path, ...) + if(!path::order) + stack_trace("Action of [path] was attempted to be created as an order.") + existing_orders += new path(args) + diff --git a/code/controllers/subsystem/pathfinding.dm b/code/controllers/subsystem/pathfinding.dm index bf7d6ed1d9..6387d2a214 100644 --- a/code/controllers/subsystem/pathfinding.dm +++ b/code/controllers/subsystem/pathfinding.dm @@ -1,6 +1,6 @@ -SUBSYSTEM_DEF(xeno_pathfinding) - name = "Xeno Pathfinding" - priority = SS_PRIORITY_XENO_PATHFINDING +SUBSYSTEM_DEF(pathfinding) + name = "Pathfinding" + priority = SS_PRIORITY_PATHFINDING flags = SS_NO_INIT|SS_TICKER|SS_BACKGROUND wait = 1 /// A list of mobs scheduled to process @@ -11,11 +11,11 @@ SUBSYSTEM_DEF(xeno_pathfinding) var/list/hash_path = list() var/current_position = 1 -/datum/controller/subsystem/xeno_pathfinding/stat_entry(msg) +/datum/controller/subsystem/pathfinding/stat_entry(msg) msg = "P:[length(paths_to_calculate)]" return ..() -/datum/controller/subsystem/xeno_pathfinding/fire(resumed = FALSE) +/datum/controller/subsystem/pathfinding/fire(resumed = FALSE) if(!resumed) current_processing = paths_to_calculate.Copy() @@ -29,15 +29,13 @@ SUBSYSTEM_DEF(xeno_pathfinding) var/turf/target = current_run.finish - var/mob/living/carbon/xenomorph/X = current_run.travelling_xeno - var/list/visited_nodes = current_run.visited_nodes var/list/distances = current_run.distances var/list/f_distances = current_run.f_distances var/list/prev = current_run.prev while(length(visited_nodes)) - current_run.current_node = visited_nodes[visited_nodes.len] + current_run.current_node = visited_nodes[length(visited_nodes)] visited_nodes.len-- if(current_run.current_node == target) break @@ -46,7 +44,7 @@ SUBSYSTEM_DEF(xeno_pathfinding) var/turf/neighbor = get_step(current_run.current_node, direction) var/distance_between = distances[current_run.current_node] * DISTANCE_PENALTY if(isnull(distances[neighbor])) - if(get_dist(neighbor, X) > current_run.path_range) + if(get_dist(neighbor, current_run.agent) > current_run.path_range) continue distances[neighbor] = INFINITY f_distances[neighbor] = INFINITY @@ -54,19 +52,22 @@ SUBSYSTEM_DEF(xeno_pathfinding) if(direction != get_dir(prev[neighbor], neighbor)) distance_between += DIRECTION_CHANGE_PENALTY - if(!neighbor.weeds) + if(isxeno(current_run.agent) && !neighbor.weeds) distance_between += NO_WEED_PENALTY for(var/i in neighbor) var/atom/A = i distance_between += A.object_weight - var/list/L = LinkBlocked(X, current_run.current_node, neighbor, current_run.ignore, TRUE) - L += check_special_blockers(X, neighbor) + var/list/L = LinkBlocked(current_run.agent, current_run.current_node, neighbor, current_run.ignore, TRUE) + L += check_special_blockers(current_run.agent, neighbor) if(length(L)) - for(var/i in L) - var/atom/A = i - distance_between += A.xeno_ai_obstacle(X, direction, target) + if(isxeno(current_run.agent)) + for(var/atom/A as anything in L) + distance_between += A.xeno_ai_obstacle(current_run.agent, direction, target) + else + for(var/atom/A as anything in L) + distance_between += A.human_ai_obstacle(current_run.agent, current_run.agent:ai_brain, direction, target) // zonenote unfuck me later if(distance_between < distances[neighbor]) distances[neighbor] = distance_between @@ -126,10 +127,10 @@ SUBSYSTEM_DEF(xeno_pathfinding) current_run.to_return.Invoke(path) QDEL_NULL(current_run) -/datum/controller/subsystem/xeno_pathfinding/proc/check_special_blockers(mob/living/carbon/xenomorph/xeno, turf/checking_turf) +/datum/controller/subsystem/pathfinding/proc/check_special_blockers(mob/agent, turf/checking_turf) var/list/pass_back = list() - for(var/spec_blocker in XENO_AI_SPECIAL_BLOCKERS) + for(var/spec_blocker in AI_SPECIAL_BLOCKERS) pass_back += istype(checking_turf, spec_blocker) ? checking_turf : list() for(var/atom/checked_atom as anything in checking_turf) @@ -137,23 +138,23 @@ SUBSYSTEM_DEF(xeno_pathfinding) return pass_back -/datum/controller/subsystem/xeno_pathfinding/proc/stop_calculating_path(mob/living/carbon/xenomorph/X) - var/datum/xeno_pathinfo/data = hash_path[X] +/datum/controller/subsystem/pathfinding/proc/stop_calculating_path(mob/agent) + var/datum/xeno_pathinfo/data = hash_path[agent] qdel(data) -/datum/controller/subsystem/xeno_pathfinding/proc/calculate_path(atom/start, atom/finish, path_range, mob/living/carbon/xenomorph/travelling_xeno, datum/callback/CB, list/ignore) +/datum/controller/subsystem/pathfinding/proc/calculate_path(atom/start, atom/finish, path_range, mob/agent, datum/callback/CB, list/ignore) if(!get_turf(start) || !get_turf(finish)) return - var/datum/xeno_pathinfo/data = hash_path[travelling_xeno] - SSxeno_pathfinding.current_processing -= data + var/datum/xeno_pathinfo/data = hash_path[agent] + SSpathfinding.current_processing -= data if(!data) data = new() - data.RegisterSignal(travelling_xeno, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/xeno_pathinfo, qdel_wrapper)) + data.RegisterSignal(agent, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/xeno_pathinfo, qdel_wrapper)) - hash_path[travelling_xeno] = data + hash_path[agent] = data paths_to_calculate += data data.current_node = get_turf(start) @@ -162,7 +163,7 @@ SUBSYSTEM_DEF(xeno_pathfinding) var/turf/target = get_turf(finish) data.finish = target - data.travelling_xeno = travelling_xeno + data.agent = agent data.to_return = CB data.path_range = path_range data.ignore = ignore @@ -175,7 +176,7 @@ SUBSYSTEM_DEF(xeno_pathfinding) /datum/xeno_pathinfo var/turf/start var/turf/finish - var/mob/living/carbon/xenomorph/travelling_xeno + var/mob/agent var/datum/callback/to_return var/path_range @@ -198,9 +199,9 @@ SUBSYSTEM_DEF(xeno_pathfinding) prev = list() /datum/xeno_pathinfo/Destroy(force) - SSxeno_pathfinding.hash_path -= travelling_xeno - SSxeno_pathfinding.paths_to_calculate -= src - SSxeno_pathfinding.current_processing -= src + SSpathfinding.hash_path -= agent + SSpathfinding.paths_to_calculate -= src + SSpathfinding.current_processing -= src #ifdef TESTING addtimer(CALLBACK(src, PROC_REF(clear_colors), distances), 5 SECONDS) @@ -208,7 +209,7 @@ SUBSYSTEM_DEF(xeno_pathfinding) start = null finish = null - travelling_xeno = null + agent = null to_return = null visited_nodes = null distances = null diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 7fd82aeb9c..ba91503e8e 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -65,6 +65,8 @@ flags_atom = FPRINT /// flags for item stuff that isn't clothing/equipping specific. var/flags_item = NO_FLAGS + /// flags for human AI to determine what this item does + var/flags_human_ai = NO_FLAGS /// This is used to determine on which slots an item can fit. var/flags_equip_slot = NO_FLAGS @@ -1096,3 +1098,9 @@ cases. Override_icon_state should be a list.*/ ///Called by /mob/living/carbon/swap_hand() when hands are swapped /obj/item/proc/hands_swapped(mob/living/carbon/swapper_of_hands) return + +/obj/item/proc/ai_use(mob/living/carbon/human/user) + return + +/obj/item/proc/ai_can_use(mob/living/carbon/human/user) + return FALSE diff --git a/code/game/objects/items/devices/radio/encryptionkey.dm b/code/game/objects/items/devices/radio/encryptionkey.dm index 160f4beaff..250a2c2911 100644 --- a/code/game/objects/items/devices/radio/encryptionkey.dm +++ b/code/game/objects/items/devices/radio/encryptionkey.dm @@ -35,8 +35,11 @@ return var/obj/item/device/radio/headset/current_headset = loc + var/datum/radio_frequency/old_connections = current_headset.secure_radio_connections[old_name] + if(!old_connections) + return - var/passed_freq = current_headset.secure_radio_connections[old_name].frequency + var/passed_freq = old_connections.frequency current_headset.secure_radio_connections -= old_name SSradio.remove_object(current_headset, passed_freq) diff --git a/code/game/objects/items/explosives/grenades/grenade.dm b/code/game/objects/items/explosives/grenades/grenade.dm index b2f95646a9..b60131dfb6 100644 --- a/code/game/objects/items/explosives/grenades/grenade.dm +++ b/code/game/objects/items/explosives/grenades/grenade.dm @@ -9,6 +9,7 @@ throw_range = 7 flags_atom = FPRINT|CONDUCT flags_equip_slot = SLOT_WAIST + flags_human_ai = GRENADE_ITEM hitsound = 'sound/weapons/smash.ogg' allowed_sensors = list(/obj/item/device/assembly/timer) max_container_volume = 60 @@ -151,3 +152,21 @@ walk(src, null, null) ..() return + +/obj/item/explosive/grenade/ai_can_use(mob/living/carbon/human/user) + return TRUE + +/obj/item/explosive/grenade/ai_use(mob/living/carbon/human/ai/user, turf/target_turf) + attack_self(user) + user.toggle_throw_mode(THROW_MODE_NORMAL) + user.ai_brain.ensure_primary_hand(src) + sleep(det_time * 0.4) + if(QDELETED(src) || (loc != user)) + return + + user.ai_brain.say_grenade_thrown_line() + sleep(det_time * 0.4) + if(QDELETED(src) || (loc != user)) + return + + user.throw_item(user.ai_brain.target_floor) diff --git a/code/game/objects/items/reagent_containers/autoinjectors.dm b/code/game/objects/items/reagent_containers/autoinjectors.dm index 291f02d5f4..e1c11f95df 100644 --- a/code/game/objects/items/reagent_containers/autoinjectors.dm +++ b/code/game/objects/items/reagent_containers/autoinjectors.dm @@ -73,6 +73,22 @@ ..() update_icon() +/obj/item/reagent_container/hypospray/autoinjector/ai_can_use(mob/living/carbon/human/user) + if(!uses_left) + return FALSE + + var/datum/reagent/reagent_datum = GLOB.chemical_reagents_list[chemname] + + if((user.reagents.get_reagent_amount(chemname) + amount_per_transfer_from_this) > reagent_datum.overdose) + return FALSE + + if(skilllock != SKILL_MEDICAL_TRAINED && !skillcheck(user, SKILL_MEDICAL, skilllock)) + return FALSE + + return TRUE + +/obj/item/reagent_container/hypospray/autoinjector/ai_use(mob/living/carbon/human/user) + attack(user, user) /obj/item/reagent_container/hypospray/autoinjector/tricord name = "tricordrazine autoinjector" @@ -117,6 +133,11 @@ display_maptext = TRUE maptext_label = "D+" +/obj/item/reagent_container/hypospray/autoinjector/dexalinp/ai_can_use(mob/living/carbon/human/user) + if(user.reagents.get_reagent_amount(chemname)) + return FALSE + return ..() + /obj/item/reagent_container/hypospray/autoinjector/chloralhydrate name = "anesthetic autoinjector" chemname = "anesthetic" @@ -311,6 +332,23 @@ desc = "An auto-injector loaded with a small amount of painkiller for marines to self-administer." icon_state = "tramadol" +/obj/item/reagent_container/hypospray/autoinjector/dylovene + name = "dylovene autoinjector" + chemname = "dylovene" + desc = "An autoinjector loaded with 3 uses of Dylovene, a general-use anti-toxin." + amount_per_transfer_from_this = LOWM_REAGENTS_OVERDOSE * INJECTOR_PERCENTAGE_OF_OD + volume = (LOWM_REAGENTS_OVERDOSE * INJECTOR_PERCENTAGE_OF_OD) * INJECTOR_USES + display_maptext = TRUE + maptext_label = "Dy" + +/obj/item/reagent_container/hypospray/autoinjector/dylovene/skillless + name = "dylovene EZ autoinjector" + desc = "An EZ autoinjector loaded with 3 uses of Dylovene, a general-use anti-toxin. Doesn't require any training to use." + icon_state = "emptyskill" + item_state = "emptyskill" + skilllock = SKILL_MEDICAL_DEFAULT + + /obj/item/reagent_container/hypospray/autoinjector/empty name = "autoinjector (C-T)" desc = "A custom-made auto-injector, likely from research." diff --git a/code/game/objects/items/reagent_containers/hypospray.dm b/code/game/objects/items/reagent_containers/hypospray.dm index 94477520da..62afcb78bf 100644 --- a/code/game/objects/items/reagent_containers/hypospray.dm +++ b/code/game/objects/items/reagent_containers/hypospray.dm @@ -14,6 +14,7 @@ flags_atom = FPRINT|OPENCONTAINER flags_equip_slot = SLOT_WAIST flags_item = NOBLUDGEON + flags_human_ai = HEALING_ITEM matter = list("plastic" = 1250, "glass" = 250) transparent = TRUE var/skilllock = SKILL_MEDICAL_TRAINED diff --git a/code/game/objects/items/stacks/medical.dm b/code/game/objects/items/stacks/medical.dm index c4a496a123..c4da729b53 100644 --- a/code/game/objects/items/stacks/medical.dm +++ b/code/game/objects/items/stacks/medical.dm @@ -8,6 +8,7 @@ throw_speed = SPEED_VERY_FAST throw_range = 20 attack_speed = 3 + flags_human_ai = HEALING_ITEM var/heal_brute = 0 var/heal_burn = 0 var/alien = FALSE @@ -95,6 +96,16 @@ to_chat(user, SPAN_WARNING("There are no wounds on [possessive] [affecting.display_name].")) return TRUE +/obj/item/stack/medical/bruise_pack/ai_use(mob/living/carbon/human/user) + for(var/obj/limb/limb as anything in user.limbs) + if(QDELETED(src)) + return + + if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list) + user.zone_selected = limb.name + attack(user, user) + sleep(user.ai_brain.short_action_delay) + /obj/item/stack/medical/bruise_pack/two amount = 2 @@ -193,6 +204,42 @@ to_chat(user, SPAN_WARNING("There are no wounds on [possessive] [affecting.display_name].")) return TRUE +/obj/item/stack/medical/advanced/bruise_pack/ai_can_use(mob/living/carbon/human/user) + for(var/obj/limb/limb as anything in user.limbs) + if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list) + return TRUE + + for(var/datum/wound/wound in limb.wounds) + if(wound.internal || wound.damage_type == BURN) + continue + + if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED))) + return TRUE + return FALSE + +/obj/item/stack/medical/advanced/bruise_pack/ai_use(mob/living/carbon/human/user) + for(var/obj/limb/limb as anything in user.limbs) + if(QDELETED(src)) + return + + if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list) + user.zone_selected = limb.name + attack(user, user) + sleep(user.ai_brain.short_action_delay) + continue + + for(var/datum/wound/wound in limb.wounds) + if(wound.internal || wound.damage_type == BURN) + continue + + if(QDELETED(src)) + return + + if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED))) + user.zone_selected = limb.name + attack(user, user) + sleep(user.ai_brain.short_action_delay) + /obj/item/stack/medical/advanced/bruise_pack/predator name = "mending herbs" singular_name = "mending herb" @@ -202,6 +249,7 @@ heal_brute = 15 stack_id = "mending herbs" alien = TRUE + /obj/item/stack/medical/advanced/ointment/predator name = "soothing herbs" singular_name = "soothing herb" @@ -211,6 +259,7 @@ heal_burn = 15 stack_id = "soothing herbs" alien = TRUE + /obj/item/stack/medical/advanced/ointment name = "burn kit" singular_name = "burn kit" @@ -260,6 +309,30 @@ to_chat(user, SPAN_WARNING("There are no burns on [possessive] [affecting.display_name].")) return TRUE +/obj/item/stack/medical/advanced/ointment/ai_can_use(mob/living/carbon/human/user) + for(var/obj/limb/limb as anything in user.limbs) + for(var/datum/wound/wound in limb.wounds) + if(wound.internal || wound.damage_type == BRUTE) + continue + + if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED))) + return TRUE + return FALSE + +/obj/item/stack/medical/advanced/ointment/ai_use(mob/living/carbon/human/user) + for(var/obj/limb/limb as anything in user.limbs) + for(var/datum/wound/wound in limb.wounds) + if(wound.internal || wound.damage_type == BRUTE) + continue + + if(QDELETED(src)) + return + + if(!(wound.bandaged & (WOUND_BANDAGED|WOUND_SUTURED))) + user.zone_selected = limb.name + attack(user, user) + sleep(user.ai_brain.short_action_delay) + /obj/item/stack/medical/splint name = "medical splints" singular_name = "medical splint" @@ -317,3 +390,15 @@ if(affecting.apply_splints(src, user, M, indestructible_splints)) // Referenced in external organ helpers. use(1) playsound(user, 'sound/handling/splint1.ogg', 25, 1, 2) + + +/obj/item/stack/medical/splint/ai_use(mob/living/carbon/human/user) + for(var/obj/limb/limb as anything in user.limbs) + if(QDELETED(src)) + return + + if(limb.is_broken()) + user.zone_selected = limb.name + attack(user, user) + sleep(user.ai_brain.short_action_delay) + continue diff --git a/code/game/objects/items/storage/firstaid.dm b/code/game/objects/items/storage/firstaid.dm index 6535497ad5..874740dd94 100644 --- a/code/game/objects/items/storage/firstaid.dm +++ b/code/game/objects/items/storage/firstaid.dm @@ -348,6 +348,7 @@ ) storage_flags = STORAGE_FLAGS_BOX|STORAGE_CLICK_GATHER|STORAGE_QUICK_GATHER storage_slots = null + flags_human_ai = HEALING_ITEM use_sound = "pillbottle" max_storage_space = 16 var/skilllock = SKILL_MEDICAL_MEDIC @@ -513,6 +514,22 @@ /obj/item/storage/pill_bottle/proc/error_idlock(mob/user) to_chat(user, SPAN_WARNING("It must have some kind of ID lock...")) +/obj/item/storage/pill_bottle/ai_can_use(mob/living/carbon/human/user) + if(!length(contents) || !COOLDOWN_FINISHED(user.ai_brain, pill_use_cooldown)) + return FALSE + + if(skilllock && !skillcheck(user, SKILL_MEDICAL, SKILL_MEDICAL_MEDIC)) + return FALSE + + return TRUE + +/obj/item/storage/pill_bottle/ai_use(mob/living/carbon/human/user) + var/obj/item/pill = contents[1] + if(user.put_in_active_hand(pill)) + remove_from_storage(pill, user) + pill.attack(user, user) + COOLDOWN_START(user.ai_brain, pill_use_cooldown, 10 SECONDS) + /obj/item/storage/pill_bottle/proc/choose_color(mob/user) if(!user) user = usr diff --git a/code/game/objects/items/tools/cleaning_tools.dm b/code/game/objects/items/tools/cleaning_tools.dm index 9fab254a71..c5343f808d 100644 --- a/code/game/objects/items/tools/cleaning_tools.dm +++ b/code/game/objects/items/tools/cleaning_tools.dm @@ -1,3 +1,6 @@ +/obj/item/tool + flags_human_ai = TOOL_ITEM + /obj/item/tool/mop desc = "The world of janitalia wouldn't be complete without a mop." name = "mop" diff --git a/code/game/objects/items/tools/kitchen_tools.dm b/code/game/objects/items/tools/kitchen_tools.dm index a29bf97cac..1c710d9649 100644 --- a/code/game/objects/items/tools/kitchen_tools.dm +++ b/code/game/objects/items/tools/kitchen_tools.dm @@ -134,6 +134,7 @@ matter = list("metal" = 12000) attack_verb = list("slashed", "stabbed", "sliced", "torn", "ripped", "diced", "cut") + flags_human_ai = MELEE_WEAPON_ITEM | TOOL_ITEM /* * Plastic Pizza Cutter diff --git a/code/game/objects/items/tools/maintenance_tools.dm b/code/game/objects/items/tools/maintenance_tools.dm index 7264a8a2e5..1b8abdbe9a 100644 --- a/code/game/objects/items/tools/maintenance_tools.dm +++ b/code/game/objects/items/tools/maintenance_tools.dm @@ -479,6 +479,9 @@ pry_capable = IS_PRY_CAPABLE_CROWBAR preferred_storage = list(/obj/item/clothing/accessory/storage/tool_webbing = WEAR_ACCESSORY) +/obj/item/tool/crowbar/ai_can_use(mob/living/carbon/human/user) + return TRUE + /obj/item/tool/crowbar/red icon = 'icons/obj/items/items.dmi' icon_state = "red_crowbar" diff --git a/code/game/objects/items/weapons/blades.dm b/code/game/objects/items/weapons/blades.dm index b475de36a4..d425d5b20d 100644 --- a/code/game/objects/items/weapons/blades.dm +++ b/code/game/objects/items/weapons/blades.dm @@ -8,6 +8,7 @@ force = MELEE_FORCE_STRONG throwforce = MELEE_FORCE_WEAK sharp = IS_SHARP_ITEM_BIG + flags_human_ai = MELEE_WEAPON_ITEM edge = 1 w_class = SIZE_LARGE hitsound = 'sound/weapons/bladeslice.ogg' diff --git a/code/game/objects/structures/barricade/folding.dm b/code/game/objects/structures/barricade/folding.dm index 8fe00d04a7..fc90b76400 100644 --- a/code/game/objects/structures/barricade/folding.dm +++ b/code/game/objects/structures/barricade/folding.dm @@ -289,3 +289,15 @@ repair_materials = list("metal" = 0.3, "plasteel" = 0.45) linkable = FALSE + +/obj/structure/barricade/plasteel/metal/wired/New() + can_wire = FALSE + is_wired = TRUE + climbable = FALSE + update_icon() + return ..() + +/obj/structure/barricade/plasteel/metal/wired/initialize_pass_flags(datum/pass_flags_container/PF) + ..() + flags_can_pass_front_temp &= ~PASS_OVER_THROW_MOB + flags_can_pass_behind_temp &= ~PASS_OVER_THROW_MOB diff --git a/code/game/objects/structures/barricade/non_folding.dm b/code/game/objects/structures/barricade/non_folding.dm index 575f1da738..1556e8cca4 100644 --- a/code/game/objects/structures/barricade/non_folding.dm +++ b/code/game/objects/structures/barricade/non_folding.dm @@ -294,3 +294,16 @@ barricade_type = "new_plasteel" repair_materials = list("plasteel" = 0.45) +/obj/structure/barricade/metal/plasteel/wired/New() + maxhealth += 50 + update_health(-50) + can_wire = FALSE + is_wired = TRUE + climbable = FALSE + update_icon() + return ..() + +/obj/structure/barricade/metal/plasteel/wired/initialize_pass_flags(datum/pass_flags_container/PF) + ..() + flags_can_pass_front_temp &= ~PASS_OVER_THROW_MOB + flags_can_pass_behind_temp &= ~PASS_OVER_THROW_MOB diff --git a/code/game/objects/structures/barricade/sandbags.dm b/code/game/objects/structures/barricade/sandbags.dm index 0e2b77b4c1..c08cd6d77a 100644 --- a/code/game/objects/structures/barricade/sandbags.dm +++ b/code/game/objects/structures/barricade/sandbags.dm @@ -133,6 +133,10 @@ health += 50 build_stage++ +/obj/structure/barricade/sandbags/full + +/obj/structure/barricade/sandbags/full/New(loc, mob/user, direction, amount = 5) + . = ..() /obj/structure/barricade/sandbags/wired/New() health = BARRICADE_SANDBAG_TRESHOLD_5 diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 2b3db70abe..95a83117e8 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -72,9 +72,17 @@ GLOBAL_LIST_INIT(admin_verbs_default, list( /client/proc/cmd_admin_say, /*staff-only ooc chat*/ /client/proc/cmd_mod_say, /* alternate way of typing asay, no different than cmd_admin_say */ /client/proc/cmd_admin_tacmaps_panel, + /datum/admins/proc/toggle_ai, + /datum/admins/proc/toggle_human_ai, + /datum/admins/proc/create_human_ai_patrol, + /client/proc/open_human_ai_management_panel, + /client/proc/open_human_faction_management_panel, + /client/proc/create_human_ai, /client/proc/other_records, + /client/proc/fortify_room, )) + GLOBAL_LIST_INIT(admin_verbs_admin, list( /datum/admins/proc/togglejoin, /*toggles whether people can join the current game*/ /datum/admins/proc/announce, /*priority announce something to all clients.*/ diff --git a/code/modules/admin/verbs/select_equipment.dm b/code/modules/admin/verbs/select_equipment.dm index edecb81d74..5441cba0e8 100644 --- a/code/modules/admin/verbs/select_equipment.dm +++ b/code/modules/admin/verbs/select_equipment.dm @@ -131,7 +131,7 @@ arm_equipment(M, dresscode, FALSE, count_participant) if(!no_logs) message_admins("[key_name_admin(usr)] changed the equipment of [key_name_admin(M)] to [dresscode].") - return + return TRUE /client/proc/cmd_admin_dress_all() set category = "Debug" diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm index f09023408f..d26378f332 100644 --- a/code/modules/client/client_defines.dm +++ b/code/modules/client/client_defines.dm @@ -134,3 +134,9 @@ /// Holds the game master datum for this client var/datum/game_master/game_master_menu + + /// Holds the human AI manager panel for this client + var/datum/human_ai_management_menu/human_ai_menu + + /// Holds the human faction manager panel for this client + var/datum/human_faction_management_menu/human_faction_menu diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 5bc91dc699..51592dc257 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -458,6 +458,7 @@ GLOBAL_LIST_INIT(whitelisted_client_procs, list( QDEL_NULL(soundOutput) QDEL_NULL(obj_window) QDEL_NULL(game_master_menu) + QDEL_NULL(human_ai_menu) if(prefs) prefs.owner = null QDEL_NULL(prefs.preview_dummy) diff --git a/code/modules/gear_presets/_select_equipment.dm b/code/modules/gear_presets/_select_equipment.dm index 2fc117421c..ee75bf9586 100644 --- a/code/modules/gear_presets/_select_equipment.dm +++ b/code/modules/gear_presets/_select_equipment.dm @@ -633,7 +633,7 @@ GLOBAL_LIST_INIT(rebel_rifles, list( var/gunpath = pick(GLOB.rebel_smgs) var/ammopath = GLOB.rebel_smgs[gunpath] - spawn_weapon(gunpath, ammopath, M, ammo_amount) + spawn_weapon(gunpath, ammopath, M, FALSE, ammo_amount) return 1 @@ -643,7 +643,7 @@ GLOBAL_LIST_INIT(rebel_rifles, list( var/gunpath = pick(GLOB.rebel_shotguns) var/ammopath = GLOB.rebel_shotguns[gunpath] - spawn_weapon(gunpath, ammopath, M, ammo_amount) + spawn_weapon(gunpath, ammopath, M, FALSE, ammo_amount) return 1 @@ -653,7 +653,7 @@ GLOBAL_LIST_INIT(rebel_rifles, list( var/gunpath = pick(GLOB.rebel_rifles) var/ammopath = GLOB.rebel_rifles[gunpath] - spawn_weapon(gunpath, ammopath, M, ammo_amount) + spawn_weapon(gunpath, ammopath, M, FALSE, ammo_amount) return 1 diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index 286645fc70..36233d930a 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -144,10 +144,12 @@ //drop the inventory item on a specific location /mob/proc/drop_inv_item_to_loc(obj/item/I, atom/newloc, nomoveupdate, force) + SEND_SIGNAL(src, COMSIG_MOB_DROP_ITEM, I) return u_equip(I, newloc, nomoveupdate, force) //drop the inventory item on the ground /mob/proc/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force) + SEND_SIGNAL(src, COMSIG_MOB_DROP_ITEM, I) return u_equip(I, get_step(src, 0), nomoveupdate, force) // Drops on turf instead of loc /mob/living/carbon/human/proc/pickup_recent() diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm new file mode 100644 index 0000000000..876ecbba0a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm @@ -0,0 +1,18 @@ +/datum/ongoing_action + var/name + var/datum/human_ai_brain/brain + var/order = FALSE + +/datum/ongoing_action/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + src.brain = brain + +/datum/ongoing_action/Destroy(force, ...) + brain = null + return ..() + +/datum/ongoing_action/proc/trigger_action() + return + +/datum/ongoing_action/proc/tgui_data() + return list() diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm new file mode 100644 index 0000000000..ff5e086cc2 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target.dm @@ -0,0 +1,32 @@ +/datum/ongoing_action/approach_target + name = "Approach Target" + var/atom/movable/target + var/acceptable_distance + var/do_target_dead + +/datum/ongoing_action/approach_target/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + target = arguments[2] + acceptable_distance = arguments[3] + do_target_dead = length(arguments) < 4 ? TRUE : arguments[4] + +/datum/ongoing_action/approach_target/Destroy(force, ...) + target = null + return ..() + +/datum/ongoing_action/approach_target/trigger_action() + if(QDELETED(target)) + return ONGOING_ACTION_COMPLETED + + if(ismob(target) && do_target_dead) + var/mob/M = target + if(M.is_dead()) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target, brain.tied_human) > acceptable_distance) + if(!brain.move_to_next_turf(get_turf(target))) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target, brain.tied_human) > acceptable_distance) + return ONGOING_ACTION_UNFINISHED + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/approach_target_carefully.dm b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target_carefully.dm new file mode 100644 index 0000000000..d55eed4315 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/approach_target_carefully.dm @@ -0,0 +1,7 @@ +/datum/ongoing_action/approach_target/carefully + name = "Approach Target Carefully" + +/datum/ongoing_action/approach_target/carefully/trigger_action() + if(brain.current_target) + return ONGOING_ACTION_COMPLETED + . = ..() diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm new file mode 100644 index 0000000000..b187d3b543 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm @@ -0,0 +1,84 @@ +/datum/ongoing_action/item_pickup + name = "Item Pickup" + var/obj/item/to_pickup + +/datum/ongoing_action/item_pickup/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + to_pickup = arguments[2] + +/datum/ongoing_action/item_pickup/Destroy(force, ...) + to_pickup = null + return ..() + +/datum/ongoing_action/item_pickup/trigger_action() + if(QDELETED(to_pickup) || !isturf(to_pickup.loc)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(to_pickup, brain.tied_human) > 1) + if(!brain.move_to_next_turf(get_turf(to_pickup))) + return ONGOING_ACTION_COMPLETED + + if(get_dist(to_pickup, brain.tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + + if(brain.primary_weapon) + brain.primary_weapon.unwield(brain.tied_human) + + else if(isgun(to_pickup)) + brain.tied_human.put_in_hands(to_pickup, TRUE) + var/obj/item/weapon/gun/primary = to_pickup + // We do the three below lines to make it so that the AI can immediately pick up a gun and open fire. This ensures that we don't need to account for this possibility when firing. + primary.wield_time = world.time + primary.pull_time = world.time + primary.guaranteed_delay_time = world.time + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/storage/belt) && !brain.container_refs["belt"]) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.tied_human.equip_to_slot(to_pickup, WEAR_WAIST) + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/storage/backpack) && !brain.container_refs["backpack"]) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.tied_human.equip_to_slot(to_pickup, WEAR_BACK) + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/storage/pouch) && !brain.container_refs["left_pocket"]) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.tied_human.equip_to_slot(to_pickup, WEAR_L_STORE) + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/storage/pouch) && !brain.container_refs["right_pocket"]) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.tied_human.equip_to_slot(to_pickup, WEAR_R_STORE) + return ONGOING_ACTION_COMPLETED + + var/storage_spot = brain.storage_has_room(to_pickup) + if(!storage_spot || !to_pickup.ai_can_use(brain.tied_human)) + return ONGOING_ACTION_COMPLETED + + if(is_type_in_list(to_pickup, brain.all_medical_items)) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.store_item(to_pickup, storage_spot, HUMAN_AI_HEALTHITEMS) + return ONGOING_ACTION_COMPLETED + + if(brain.primary_weapon && istype(to_pickup, /obj/item/ammo_magazine)) + var/obj/item/ammo_magazine/mag = to_pickup + if(istype(brain.primary_weapon, mag.gun_type)) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.store_item(to_pickup, storage_spot, HUMAN_AI_AMMUNITION) + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/explosive/grenade)) + var/obj/item/explosive/grenade/nade = to_pickup + if(!nade.active) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.store_item(to_pickup, storage_spot, HUMAN_AI_GRENADES) + return ONGOING_ACTION_COMPLETED + + if(to_pickup.flags_human_ai & TOOL_ITEM) + brain.tied_human.put_in_hands(to_pickup, TRUE) + brain.store_item(to_pickup, storage_spot, HUMAN_AI_TOOLS) + return ONGOING_ACTION_COMPLETED + + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/melee_atom.dm b/code/modules/mob/living/carbon/human/ai/action_datums/melee_atom.dm new file mode 100644 index 0000000000..c8a82c6589 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/melee_atom.dm @@ -0,0 +1,33 @@ +/datum/ongoing_action/melee_atom + name = "Melee Atom" + var/atom/target + +/datum/ongoing_action/melee_atom/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + target = arguments[2] + +/datum/ongoing_action/melee_atom/Destroy(force, ...) + target = null + return ..() + +/datum/ongoing_action/melee_atom/trigger_action() + if(QDELETED(target) || brain.in_combat || !brain.primary_weapon) // Lower priority than getting shot at + return ONGOING_ACTION_COMPLETED + + if(get_dist(target, brain.tied_human) > 1) + if(!brain.move_to_next_turf(get_turf(target))) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target, brain.tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + + //if(brain.primary_melee) + // brain.unholster_melee() + + //X.do_click(src, "", list()) + + brain.unholster_primary() // this should eventually have support for melee weapons + //brain.ensure_primary_hand(brain.primary_weapon) + brain.tied_human.do_click(target, "", list()) + + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm b/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm new file mode 100644 index 0000000000..1beefc0843 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/orders/order_action.dm @@ -0,0 +1,10 @@ +/datum/ongoing_action/order + order = TRUE + var/can_perform_in_combat = FALSE + +/datum/ongoing_action/order/New(list/arguments) + return + +/datum/ongoing_action/order/Destroy(force, ...) + SShuman_ai.existing_orders -= src + return ..() diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm b/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm new file mode 100644 index 0000000000..6f77965e0a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/orders/patrol_waypoints.dm @@ -0,0 +1,82 @@ +/datum/ongoing_action/order/patrol_waypoints + name = "Patrol Waypoints" + var/list/waypoints = list() + var/time_at_waypoint = 10 SECONDS + var/current_waypoint_index = 1 + var/turf/current_waypoint_target + var/waiting = FALSE + var/desc = "" + +/datum/ongoing_action/order/patrol_waypoints/New(list/arguments) + . = ..() + waypoints = arguments[2] + time_at_waypoint = (length(arguments) >= 3 ? arguments[3] : 10 SECONDS) + current_waypoint_target = waypoints[current_waypoint_index] + +/datum/ongoing_action/order/patrol_waypoints/Destroy(force, ...) + current_waypoint_target = null + return ..() + +/datum/ongoing_action/order/patrol_waypoints/trigger_action(datum/human_ai_brain/brain) + if(waiting || QDELETED(current_waypoint_target) || !isturf(current_waypoint_target)) // Since this one order applies to the entire squad, everyone stops approaching when one arrives + return ONGOING_ACTION_COMPLETED + + if(get_dist(current_waypoint_target, brain.tied_human) > 1) + if(!brain.move_to_next_turf(current_waypoint_target)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(current_waypoint_target, brain.tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + if(brain.is_squad_leader) + waiting = TRUE + addtimer(CALLBACK(src, PROC_REF(set_next_waypoint)), time_at_waypoint) + return ONGOING_ACTION_COMPLETED + +/datum/ongoing_action/order/patrol_waypoints/proc/set_next_waypoint() + if(current_waypoint_index >= length(waypoints)) + current_waypoint_index = 1 + else + current_waypoint_index++ + current_waypoint_target = waypoints[current_waypoint_index] + waiting = FALSE + +/datum/ongoing_action/order/patrol_waypoints/tgui_data() + return list( + list( + "waypoint_amount", + "waiting", + "desc", + ), + list( + length(waypoints), + waiting, + desc, + ) + ) + +/datum/admins/proc/create_human_ai_patrol() + set name = "Create Human AI Patrol Waypoints" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/list/turf/waypoint_list = list() + while(TRUE) + if(tgui_input_list(usr, "Press Enter to save the turf you are on to the patrol datum. Press Cancel to finalize.", "Save Turf", list("Enter", "Cancel")) == "Enter") + var/turf/user_turf = get_turf(usr) + var/dist = length(waypoint_list) ? get_dist(waypoint_list[length(waypoint_list)], user_turf) : 0 + if(length(waypoint_list) && (dist > HUMAN_AI_MAX_PATHFINDING_RANGE)) + to_chat(usr, SPAN_WARNING("This waypoint is too far from the previous one. Maximum distance is [HUMAN_AI_MAX_PATHFINDING_RANGE] while this node's was [dist].")) + continue + waypoint_list += user_turf + continue + break + + if(length(waypoint_list) <= 0) + return + + var/datum/ongoing_action/order/patrol_waypoints/patrol = new(list(null, waypoint_list, 10 SECONDS)) + patrol.desc = tgui_input_text(usr, "Input a description of the patrol.", "Description") + SShuman_ai.existing_orders += patrol + to_chat(usr, SPAN_NOTICE("Patrol order has been created.")) diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/pickup_primary.dm b/code/modules/mob/living/carbon/human/ai/action_datums/pickup_primary.dm new file mode 100644 index 0000000000..634df82299 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/pickup_primary.dm @@ -0,0 +1,8 @@ +/// In case they drop their primary but get a new one in the time it takes to retrieve their old one +/datum/ongoing_action/item_pickup/pickup_primary + name = "Pickup Primary" + +/datum/ongoing_action/item_pickup/pickup_primary/trigger_action() + if(brain.primary_weapon) + return ONGOING_ACTION_COMPLETED + return ..() diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/retreat_from_target.dm b/code/modules/mob/living/carbon/human/ai/action_datums/retreat_from_target.dm new file mode 100644 index 0000000000..8bcea49b0a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/retreat_from_target.dm @@ -0,0 +1,40 @@ +/datum/ongoing_action/retreat_from_target + name = "Retreat From Target" + var/atom/movable/target + var/acceptable_distance + var/do_target_dead + +/datum/ongoing_action/retreat_from_target/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + target = arguments[2] + acceptable_distance = arguments[3] + do_target_dead = length(arguments) < 4 ? TRUE : arguments[4] + +/datum/ongoing_action/retreat_from_target/Destroy(force, ...) + target = null + return ..() + +/datum/ongoing_action/retreat_from_target/trigger_action() + if(QDELETED(target)) + return ONGOING_ACTION_COMPLETED + + if(ismob(target) && do_target_dead) + var/mob/M = target + if(M.is_dead()) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target, brain.tied_human) < acceptable_distance) + var/relative_dir = Get_Compass_Dir(target, brain.tied_human) + var/moved = FALSE + for(var/D in list(relative_dir, turn(relative_dir, 90), turn(relative_dir, -90))) + if(brain.move_to_next_turf(get_step(get_turf(brain.tied_human), D))) + moved = TRUE + break + + if(!moved) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target, brain.tied_human) < acceptable_distance) + return ONGOING_ACTION_UNFINISHED + + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm new file mode 100644 index 0000000000..85b88b4b25 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm @@ -0,0 +1,32 @@ +/datum/ongoing_action/take_cover + name = "Take Cover" + var/turf/target_turf + var/atom/cover_atom + var/hard_cover + +/datum/ongoing_action/take_cover/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + target_turf = arguments[2] + cover_atom = arguments[3] + hard_cover = arguments[4] + +/datum/ongoing_action/take_cover/Destroy(force, ...) + target_turf = null + cover_atom = null + return ..() + +/datum/ongoing_action/take_cover/trigger_action() + if(!isturf(target_turf)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target_turf, brain.tied_human) > 0) + if(!brain.move_to_next_turf(target_turf)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target_turf, brain.tied_human) > 0) + return ONGOING_ACTION_UNFINISHED + + brain.in_cover = TRUE + brain.hard_cover = hard_cover + brain.RegisterSignal(cover_atom, COMSIG_PARENT_QDELETING, TYPE_PROC_REF(/datum/human_ai_brain, on_cover_destroyed), TRUE) + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm b/code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm new file mode 100644 index 0000000000..d190447c2a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/take_inside_cover.dm @@ -0,0 +1,27 @@ +/datum/ongoing_action/take_inside_cover + name = "Take Cover (Inside)" + var/turf/target_turf + +/datum/ongoing_action/take_inside_cover/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + target_turf = arguments[2] + +/datum/ongoing_action/take_inside_cover/Destroy(force, ...) + target_turf = null + return ..() + +/datum/ongoing_action/take_inside_cover/trigger_action() + if(!isturf(target_turf)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target_turf, brain.tied_human) > 0) + if(!brain.move_to_next_turf(target_turf)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target_turf, brain.tied_human) > 0) + return ONGOING_ACTION_UNFINISHED + + brain.in_cover = TRUE + brain.hard_cover = TRUE + brain.RegisterSignal(brain.tied_human, COMSIG_HUMAN_BULLET_ACT, TYPE_PROC_REF(/datum/human_ai_brain, on_shot_inside_cover)) + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm b/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm new file mode 100644 index 0000000000..3bf5805242 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm @@ -0,0 +1,32 @@ +/datum/ongoing_action/throw_back_nade + name = "Throw Back Grenade" + var/obj/item/explosive/grenade/throwing + var/turf/target_turf + +/datum/ongoing_action/throw_back_nade/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + throwing = arguments[2] + target_turf = arguments[3] + +/datum/ongoing_action/throw_back_nade/Destroy(force, ...) + throwing = null + target_turf = null + return ..() + +/datum/ongoing_action/throw_back_nade/trigger_action() + if(QDELETED(throwing) || !isturf(throwing.loc)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(throwing, brain.tied_human) > 1) + if(!brain.move_to_next_turf(get_turf(throwing))) + return ONGOING_ACTION_COMPLETED + + if(get_dist(throwing, brain.tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + brain.end_gun_fire() + brain.clear_main_hand() + brain.tied_human.put_in_active_hand(throwing) + if(target_turf) + brain.tied_human.toggle_throw_mode(THROW_MODE_NORMAL) + brain.tied_human.throw_item(target_turf) + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm b/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm new file mode 100644 index 0000000000..9cae1ad53d --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm @@ -0,0 +1,31 @@ +/datum/ongoing_action/throw_grenade + name = "Throw Grenade" + var/obj/item/explosive/grenade/throwing + var/turf/target_turf + var/mid_throw = FALSE + +/datum/ongoing_action/throw_grenade/New(datum/human_ai_brain/brain, list/arguments) + . = ..() + throwing = arguments[2] + target_turf = arguments[3] + +/datum/ongoing_action/throw_grenade/Destroy(force, ...) + throwing = null + target_turf = null + return ..() + +/datum/ongoing_action/throw_grenade/trigger_action() + if(QDELETED(throwing) || !target_turf) + return ONGOING_ACTION_COMPLETED + + if(mid_throw) + return ONGOING_ACTION_UNFINISHED_BLOCK + + mid_throw = TRUE + brain.holster_primary() + brain.equip_item_from_equipment_map(HUMAN_AI_GRENADES, throwing) + sleep(brain.short_action_delay * brain.action_delay_mult) + if(QDELETED(throwing) || (throwing.loc != brain.tied_human)) + return ONGOING_ACTION_COMPLETED + throwing.ai_use(brain.tied_human, target_turf) + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/ai_equipment.dm b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm new file mode 100644 index 0000000000..eda03eda1a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm @@ -0,0 +1,74 @@ +/datum/equipment_preset/clf/soldier/ai + name = "CLF Soldier (AI)" + +/datum/equipment_preset/clf/soldier/ai/load_gear(mob/living/carbon/human/new_human) + var/obj/item/clothing/under/colonist/clf/jumpsuit = new() + var/obj/item/clothing/accessory/storage/webbing/W = new() + jumpsuit.attach_accessory(new_human, W) + new_human.equip_to_slot_or_del(jumpsuit, WEAR_BODY) + spawn_rebel_suit(new_human) + spawn_rebel_helmet(new_human) + spawn_rebel_shoes(new_human) + spawn_rebel_gloves(new_human) + new_human.equip_to_slot_or_del(new /obj/item/tool/crowbar, WEAR_IN_JACKET) + new_human.equip_to_slot_or_del(new /obj/item/device/flashlight(new_human), WEAR_L_STORE) + new_human.equip_to_slot_or_del(new /obj/item/storage/pouch/firstaid/ert(new_human), WEAR_R_STORE) + + //new_human.equip_to_slot_or_del(new /obj/item/storage/belt/shotgun/full/random(new_human), WEAR_WAIST) + //new_human.equip_to_slot_or_del(new /obj/item/weapon/gun/shotgun/pump/dual_tube/cmb(new_human), WEAR_BACK) + + //new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/general_belt(new_human), WEAR_WAIST) + //new_human.equip_to_slot_or_del(new /obj/item/ammo_magazine/flamer_tank(new_human), WEAR_IN_BELT) + //new_human.equip_to_slot_or_del(new /obj/item/weapon/gun/flamer(new_human), WEAR_BACK) + + //new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/general_belt(new_human), WEAR_WAIST) + //new_human.equip_to_slot_or_del(new /obj/item/ammo_magazine/rocket/anti_tank(new_human), WEAR_IN_BELT) + //new_human.equip_to_slot_or_del(new /obj/item/weapon/gun/launcher/rocket/anti_tank(new_human), WEAR_R_HAND) + + new_human.equip_to_slot_or_del(new /obj/item/storage/belt/marine(new_human), WEAR_WAIST) + if(prob(50)) + spawn_rebel_smg(new_human) + else + spawn_rebel_rifle(new_human) + + new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/CLF(new_human), WEAR_L_EAR) + +/datum/equipment_preset/clf/soldier/ai/load_preset(mob/living/carbon/human/new_human, randomise, count_participant, client/mob_client, show_job_gear) + . = ..() + new_human.ai_brain?.appraise_inventory() + +/datum/equipment_preset/clf/specialist/ai + name = "CLF Specialist (AI)" + +/datum/equipment_preset/clf/specialist/ai/load_gear(mob/living/carbon/human/new_human) + + //jumpsuit and their webbing + var/obj/item/clothing/under/colonist/clf/CLF = new() + var/obj/item/clothing/accessory/storage/webbing/five_slots/W = new() + CLF.attach_accessory(new_human, W) + new_human.equip_to_slot_or_del(CLF, WEAR_BODY) + //clothing + new_human.equip_to_slot_or_del(new /obj/item/clothing/suit/storage/militia(new_human), WEAR_JACKET) + new_human.equip_to_slot_or_del(new /obj/item/clothing/head/helmet/swat(new_human), WEAR_HEAD) + new_human.equip_to_slot_or_del(new /obj/item/attachable/bayonet/upp(new_human), WEAR_FACE) + new_human.equip_to_slot_or_del(new /obj/item/clothing/shoes/combat(new_human), WEAR_FEET) + new_human.equip_to_slot_or_del(new /obj/item/clothing/gloves/combat(new_human), WEAR_HANDS) + new_human.equip_to_slot_or_del(new /obj/item/storage/belt/gun/m4a3/vp70(new_human), WEAR_WAIST) + new_human.equip_to_slot_or_del(new /obj/item/clothing/glasses/night(new_human), WEAR_EYES) + new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/CLF/cct(new_human), WEAR_L_EAR) + new_human.equip_to_slot_or_del(new /obj/item/clothing/ears/earmuffs(new_human), WEAR_R_EAR) + //standard backpack stuff + new_human.equip_to_slot_or_del(new /obj/item/storage/backpack/lightpack(new_human), WEAR_BACK) + new_human.equip_to_slot_or_del(new /obj/item/device/flashlight(new_human), WEAR_IN_BACK) + new_human.equip_to_slot_or_del(new /obj/item/storage/firstaid/regular/response(new_human), WEAR_IN_BACK) + new_human.equip_to_slot_or_del(new /obj/item/tool/crowbar(new_human), WEAR_IN_BACK) + + //storage items + new_human.equip_to_slot_or_del(new /obj/item/storage/pouch/explosive/C4(new_human), WEAR_L_STORE) + new_human.equip_to_slot_or_del(new /obj/item/storage/pouch/firstaid/ert(new_human), WEAR_R_STORE) + + new_human.put_in_active_hand(new /obj/item/weapon/gun/launcher/rocket/anti_tank/disposable(new_human)) + +/datum/equipment_preset/clf/specialist/ai/load_preset(mob/living/carbon/human/new_human, randomise, count_participant, client/mob_client, show_job_gear) + . = ..() + new_human.ai_brain?.appraise_inventory() diff --git a/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm new file mode 100644 index 0000000000..043620bb20 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm @@ -0,0 +1,146 @@ +/datum/human_ai_management_menu + +/datum/human_ai_management_menu/New() + +/datum/human_ai_management_menu/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "HumanAIManager") + ui.open() + +/datum/human_ai_management_menu/ui_state(mob/user) + return GLOB.admin_state + +/datum/human_ai_management_menu/ui_data(mob/user) + var/list/data = list() + + data["orders"] = list() + for(var/datum/ongoing_action/order as anything in SShuman_ai.existing_orders) + data["orders"] += list(list( + "name" = order.name, + "type" = order.type, + "data" = order.tgui_data(), + "ref" = REF(order), + ) + ) + + data["ai_humans"] = list() + for(var/datum/human_ai_brain/brain as anything in GLOB.human_ai_brains) + if(!brain.tied_human || brain.tied_human.stat == DEAD) + continue + + data["ai_humans"] += list(list( + "name" = brain.tied_human.real_name, + "health" = FLOOR((brain.tied_human.health / brain.tied_human.maxHealth * 100), 1), + "loc" = list(brain.tied_human.x, brain.tied_human.y, brain.tied_human.z), + "faction" = brain.tied_human.faction, + "ref" = REF(brain.tied_human), + "brain_ref" = REF(brain), + "in_combat" = brain.in_combat, + "squad_id" = brain.squad_id, + )) + + data["squads"] = list() + for(var/datum/human_ai_squad/squad as anything in SShuman_ai.squads) + var/list/name_list = list() + for(var/datum/human_ai_brain/brain as anything in squad.ai_in_squad) + name_list += brain.tied_human?.real_name + data["squads"] += list(list( + "id" = squad.id, + "members" = english_list(name_list), + "order" = squad.assigned_order?.name, + "ref" = REF(squad), + "squad_leader" = squad.squad_leader?.tied_human?.real_name, + )) + + return data + +/datum/human_ai_management_menu/ui_static_data(mob/user) + var/list/data = list() + + return data + +/datum/human_ai_management_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("view_variables") + if(!params["ref"]) + return + + var/datum/gotten_ref = locate(params["ref"]) + if(!istype(gotten_ref)) + return + + ui.user.client?.debug_variables(gotten_ref) + return TRUE + + if("create_squad") + SShuman_ai.create_new_squad() + //update_static_data(usr, ui) + return TRUE + + if("assign_to_squad") + if(!params["squad"] || !params["ai"]) + return + + var/datum/brain = locate(params["ai"]) + brain:add_to_squad(params["squad"]) + //update_static_data(usr, ui) + return TRUE + + if("assign_order") + if(!params["squad"] || !params["order"]) + return + + var/datum/human_ai_squad/squad = SShuman_ai.get_squad("[params["squad"]]") + squad.set_order(locate(params["order"])) + //update_static_data(usr, ui) + return TRUE + + if("assign_sl") + if(!params["squad"] || !params["ai"]) + return + + var/datum/brain = locate(params["ai"]) + var/datum/human_ai_squad/squad = SShuman_ai.get_squad("[params["squad"]]") + squad.set_squad_leader(brain) + //update_static_data(usr, ui) + return TRUE + + if("delete_object") // This UI is fully GM-only so I'm not worried about someone abusing this + if(!params["ref"]) + return + + var/datum/ref_to_del = locate(params["ref"]) + qdel(ref_to_del) + return TRUE + +/client/proc/open_human_ai_management_panel() + set name = "Human AI Management Panel" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + if(human_ai_menu) + human_ai_menu.tgui_interact(mob) + return + + human_ai_menu = new /datum/human_ai_management_menu(src) + human_ai_menu.tgui_interact(mob) + +/client/proc/create_human_ai() + set name = "Create Human AI" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/mob/living/carbon/human/ai/ai_human = new() + if(!cmd_admin_dress_human(ai_human)) + qdel(ai_human) + return + ai_human.forceMove(get_turf(mob)) diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm new file mode 100644 index 0000000000..be9f24d320 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm @@ -0,0 +1,323 @@ +GLOBAL_LIST_EMPTY(human_ai_brains) + +/datum/human_ai_brain + var/mob/living/carbon/human/tied_human + + var/micro_action_delay = 0.2 SECONDS + var/short_action_delay = 0.5 SECONDS + var/medium_action_delay = 2 SECONDS + var/long_action_delay = 5 SECONDS + var/action_delay_mult = 1 + + /// Midway through doing something, shouldn't start something else + var/currently_busy = FALSE + + /// If TRUE, shoots until the target is dead. Else, stops when downed + var/shoot_to_kill = TRUE + + /// Distance for view checks + var/view_distance = 7 + + /// How far the AI will chuck a nade if they can't find an enemy to throw it back at + var/nade_direction_throw = 4 + + /// How long we will ignore max fire distance after shot from afar + var/return_fire_duration = 5 SECONDS + + /// List of current action datums + var/list/ongoing_actions = list() + + /// List of semi-permanent "order" action datums. These do not expire + //var/list/ongoing_orders = list() + var/datum/ongoing_action/order/ongoing_order + + var/list/detection_turfs = list() + + var/in_combat = FALSE + var/combat_decay_time = 30 SECONDS + var/squad_covering = FALSE + + var/list/friendly_factions = list() + var/list/neutral_factions = list() + var/previous_faction + +/datum/human_ai_brain/New(mob/living/carbon/human/tied_human) + . = ..() + src.tied_human = tied_human + RegisterSignal(tied_human, COMSIG_PARENT_QDELETING, PROC_REF(on_human_delete)) + RegisterSignal(tied_human, COMSIG_HUMAN_EQUIPPED_ITEM, PROC_REF(on_item_equip)) + RegisterSignal(tied_human, COMSIG_HUMAN_UNEQUIPPED_ITEM, PROC_REF(on_item_unequip)) + RegisterSignal(tied_human, COMSIG_MOB_PICKUP_ITEM, PROC_REF(on_item_pickup)) + RegisterSignal(tied_human, COMSIG_MOB_DROP_ITEM, PROC_REF(on_item_drop)) + RegisterSignal(tied_human, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) + RegisterSignal(tied_human, COMSIG_HUMAN_BULLET_ACT, PROC_REF(on_shot)) + if(!length(all_medical_items)) + all_medical_items = brute_heal_items + burn_heal_items + tox_heal_items + oxy_heal_items + bleed_heal_items + bonebreak_heal_items + painkiller_items + GLOB.human_ai_brains += src + setup_detection_radius() + +/datum/human_ai_brain/Destroy(force, ...) + tied_human = null + set_primary_weapon(null) + current_target = null + target_floor = null + ongoing_order = null + GLOB.human_ai_brains -= src + gun_data = null + //primary_melee = null + return ..() + +/datum/human_ai_brain/process(delta_time) + if(tied_human.is_mob_incapacitated()) + end_gun_fire() + return + + var/order_return + if(can_process_order()) + order_return = ongoing_order.trigger_action(src) + + if(!in_combat && !currently_busy && (order_return != ONGOING_ACTION_UNFINISHED)) + approach_squad_leader() + + for(var/datum/ongoing_action/action as anything in ongoing_actions) + var/retval = action.trigger_action() + switch(retval) + if(ONGOING_ACTION_UNFINISHED_BLOCK) + return + if(ONGOING_ACTION_COMPLETED) + ongoing_actions -= action + qdel(action) + currently_busy = FALSE + + var/list/things_around = range(2, tied_human) + // Might be wise to move these off tick and instead make them signal-based + nade_throwback(things_around) + item_search(things_around) + + if(!currently_busy && should_reload_primary()) + currently_busy = TRUE + reload_primary() + + if(primary_weapon && current_target) + if(!has_ongoing_action(AI_ACTION_APPROACH) && !has_ongoing_action(AI_ACTION_RETREAT)) + var/target_futile = current_target.is_mob_incapacitated() + var/distance = get_dist(tied_human, current_target) + if(distance > gun_data.optimal_range || target_futile) + if(!in_cover) + var/walk_distance = target_futile ? gun_data.minimum_range : gun_data.optimal_range + ADD_ONGOING_ACTION(src, AI_ACTION_APPROACH, current_target, walk_distance) + + else if(distance < gun_data.optimal_range) + var/walk_distance = in_cover ? gun_data.minimum_range : gun_data.optimal_range + ADD_ONGOING_ACTION(src, AI_ACTION_RETREAT, current_target, walk_distance) + + if(!currently_busy && !currently_firing && COOLDOWN_FINISHED(src, fire_overload_cooldown)) + currently_busy = TRUE + attack_target() + + if(!currently_busy && !in_combat && healing_start_check()) + currently_busy = TRUE + start_healing() + + if(!currently_busy && !current_target && primary_weapon) + current_target = get_target(view_distance) + RegisterSignal(current_target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_delete)) + +/datum/human_ai_brain/proc/on_human_delete(datum/source, force) + SIGNAL_HANDLER + tied_human = null + +/datum/human_ai_brain/proc/on_target_delete(datum/source, force) + SIGNAL_HANDLER + current_target = null + +/datum/human_ai_brain/proc/ensure_primary_hand(obj/item/held_item) + if(tied_human.get_inactive_hand() == held_item) + tied_human.swap_hand() + +/datum/human_ai_brain/proc/nade_throwback(list/things_around) + if(has_ongoing_action(AI_ACTION_THROWBACK)) + return + + var/turf/place_to_throw + var/obj/item/explosive/grenade/throw_nade + for(var/obj/item/explosive/grenade/nade in things_around) + if(!nade.active || nade.throwing) + continue + + throw_nade = nade + + var/list/possible_targets = list() + for(var/mob/living/carbon/target in range(view_distance, tied_human)) + if(can_target(target)) + possible_targets += target + + if(length(possible_targets)) + var/mob/living/carbon/chosen_target = pick(possible_targets) + var/list/turf_pathfind_list = AStar(get_turf(tied_human), get_turf(chosen_target), /turf/proc/AdjacentTurfs, /turf/proc/Distance, view_distance) + for(var/i = length(turf_pathfind_list); i >= 4; i--) // We cut it off at 4 because we want to avoid most of the nade blast + var/turf/target_turf = turf_pathfind_list[i] + if(target_turf in viewers(world.view, tied_human)) + place_to_throw = target_turf + goto throw_back_nade + place_to_throw = turf_pathfind_list[4] + goto throw_back_nade + + // We haven't found an enemy in range that we can throw to, so we'll just throw in a direction that doesn't have friendlies + var/list/directions = list( + locate(tied_human.x, tied_human.y + nade_direction_throw, tied_human.z), + locate(tied_human.x + nade_direction_throw, tied_human.y, tied_human.z), + locate(tied_human.x, tied_human.y - nade_direction_throw, tied_human.z), + locate(tied_human.x - nade_direction_throw, tied_human.y, tied_human.z), + ) + location_loop: + for(var/turf/location as anything in directions) + if(location) + var/list/turf/path = get_line(tied_human, location, include_start_atom = FALSE) + for(var/turf/possible_blocker as anything in path) + if(possible_blocker.density) + continue location_loop + + var/has_friendly = FALSE + for(var/mob/possible_friendly in range(3, location)) + if(!can_target(possible_friendly)) + has_friendly = TRUE + break + + if(!has_friendly) + place_to_throw = location + goto throw_back_nade + + // There's friendlies all around us, apparently. Just uh. Die ig. + break + + if(!throw_nade) + return + + throw_back_nade: + ADD_ONGOING_ACTION(src, AI_ACTION_THROWBACK, throw_nade, place_to_throw) + +/// Use ADD_ONGOING_ACTION() macro instead of calling this directly +/datum/human_ai_brain/proc/_add_ongoing_action(datum/ongoing_action/path, ...) + if(path::order) + stack_trace("Order of [path] was attempted to be added as an action.") + ongoing_actions += new path(src, args) + +/datum/human_ai_brain/proc/has_ongoing_action(path) + if(!ispath(path)) + return FALSE + + for(var/datum/ongoing_action/action as anything in ongoing_actions) + if(istype(action, path)) + return TRUE + + return FALSE + +/datum/human_ai_brain/proc/set_ongoing_order(datum/ongoing_action/ref) + if(!ref) + return + + if(!ref.order) + stack_trace("Action of [ref.type] was attempted to be added as an order.") + ongoing_order = ref + +/datum/human_ai_brain/proc/has_ongoing_order(path) + return istype(ongoing_order, path) + +/datum/human_ai_brain/proc/clear_ongoing_order() + ongoing_order = null + +/// Returns TRUE if the target is friendly/neutral to us +/datum/human_ai_brain/proc/faction_check(mob/target) + if(!target) + return FALSE + + if(target.faction == tied_human.faction) + return TRUE + + if(target.faction in friendly_factions) + return TRUE + + if(target.faction in neutral_factions) + return TRUE + + return FALSE + +/datum/human_ai_brain/proc/setup_detection_radius() + if(length(detection_turfs)) + clear_detection_radius() + + for(var/turf/open/floor in range(1, tied_human)) + RegisterSignal(floor, COMSIG_TURF_ENTERED, PROC_REF(on_detection_turf_enter)) + detection_turfs += floor + +/datum/human_ai_brain/proc/clear_detection_radius() + for(var/turf/open/floor as anything in detection_turfs) + UnregisterSignal(floor, COMSIG_TURF_ENTERED) + + detection_turfs.Cut() + +/datum/human_ai_brain/proc/on_detection_turf_enter(datum/source, atom/movable/entering) + SIGNAL_HANDLER + if(entering == tied_human) + return + + if(istype(entering, /obj/projectile)) + var/obj/projectile/bullet = entering + if(ismob(bullet.firer)) + if(!in_cover && !squad_covering && !faction_check(bullet.firer)) // If it's our own bullets, we don't need to be alarmed + enter_combat() + if(!primary_weapon) + locate_cover(bullet, bullet.dir) // Zonenote: cover is fucked and needs work, so it's limited to only unarmed combatants + +/datum/human_ai_brain/proc/on_move(atom/movable/mover, atom/oldloc, direction) + setup_detection_radius() + +/datum/human_ai_brain/proc/enter_combat() + SIGNAL_HANDLER + + if(!in_combat) + say_in_combat_line() + in_combat = TRUE + addtimer(CALLBACK(src, PROC_REF(exit_combat)), combat_decay_time, TIMER_UNIQUE | TIMER_OVERRIDE) + +/datum/human_ai_brain/proc/exit_combat() + if(in_combat) + say_exit_combat_line() + holster_primary() + in_combat = FALSE + +/datum/human_ai_brain/proc/can_process_order() + if(!ongoing_order) + return FALSE + + if(!ongoing_order.can_perform_in_combat && in_combat) + return FALSE + + return TRUE + +/datum/human_ai_brain/proc/on_shot(datum/source, damage_result, ammo_flags, obj/projectile/bullet) + SIGNAL_HANDLER + + var/mob/firer = bullet.firer + if(firer?.faction in neutral_factions) + on_neutral_faction_betray(firer.faction) + + if(primary_weapon?.ammo.max_range <= get_dist(tied_human, firer)) + COOLDOWN_START(src, return_fire, return_fire_duration) + + if(!faction_check(firer)) + if(current_target != firer) + end_gun_fire() + current_target = firer + +/datum/human_ai_brain/proc/on_neutral_faction_betray(faction) + if(!tied_human.faction) + return + + var/datum/human_ai_faction/our_faction = SShuman_ai.human_ai_factions[tied_human.faction] + if(!our_faction) + return + + our_faction.remove_neutral_faction(faction) + our_faction.reapply_faction_data() diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm new file mode 100644 index 0000000000..6d6c0e63a0 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm @@ -0,0 +1,71 @@ +/datum/human_ai_brain + var/list/in_combat_lines = list( + "Taking fire!", + "Getting shot at!", + "Engaging hostiles!", + "Contact!", + "Contact contact!", + "We've got hostiles!", + "Take 'em down!", + "Hostile spotted, engaging!", + "Enemy hostiles here!", + "Being fired upon!", + "Blast 'em!", + ) + + var/list/exit_combat_lines = list( + "No more contacts.", + "Don't see 'em.", + "Going back to regular duties.", + "Nothin' left.", + "Can't find 'em.", + "No hostiles, returning to duties.", + ) + + var/list/squad_member_death_lines = list( + "Fuck! Man down!", + "We lost one!", + "Man down!", + "We're taking losses here!", + "Goddamn it.", + "Fuck!", + "Shit, our squad's down a man!", + "Squad integrity's failing!", + ) + + var/list/grenade_thrown_lines = list( + "Nade out!", + "Tossing a grenade!", + "Smoking 'em out!", + "Throwing a nade!", + "Grenade out!", + "Tossing a nade!", + "Pineapple out!", + "Fragging 'em!", + ) + + var/in_combat_line_chance = 40 + var/exit_combat_line_chance = 40 + var/squad_member_death_line_chance = 20 + var/grenade_thrown_line_chance = 60 + + +/datum/human_ai_brain/proc/say_in_combat_line(chance = in_combat_line_chance) + if(!length(in_combat_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT)) + return + tied_human.say(pick(in_combat_lines)) + +/datum/human_ai_brain/proc/say_exit_combat_line(chance = exit_combat_line_chance) + if(!length(exit_combat_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT)) + return + tied_human.say(pick(exit_combat_lines)) + +/datum/human_ai_brain/proc/on_squad_member_death(mob/living/carbon/human/dead_member) + if(!length(squad_member_death_lines) || !prob(squad_member_death_line_chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT)) + return + tied_human.say(pick(squad_member_death_lines)) + +/datum/human_ai_brain/proc/say_grenade_thrown_line(chance = grenade_thrown_line_chance) + if(!length(grenade_thrown_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT)) + return + tied_human.say(pick(grenade_thrown_lines)) diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm new file mode 100644 index 0000000000..9cd75406f6 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm @@ -0,0 +1,184 @@ +/datum/human_ai_brain + /// If TRUE, AI is currently in some form of cover + var/in_cover = FALSE + + /// If TRUE, the AI is in hard cover, meaning that it's solid and can't be seen through + var/hard_cover = FALSE + + COOLDOWN_DECLARE(cover_search_cooldown) + +/datum/human_ai_brain/proc/on_cover_destroyed(atom/source, force) + UnregisterSignal(source, COMSIG_PARENT_QDELETING) + in_cover = FALSE + hard_cover = FALSE + +/datum/human_ai_brain/proc/on_shot_inside_cover(datum/source, damage_result, ammo_flags, obj/projectile/bullet) + if(faction_check(bullet.firer)) + return // FF + + // Cover isn't working. Charge! + in_cover = FALSE + hard_cover = FALSE + UnregisterSignal(tied_human, COMSIG_HUMAN_BULLET_ACT) + + +/datum/human_ai_brain/proc/locate_cover(obj/projectile/bullet, projectile_dir) + if(!COOLDOWN_FINISHED(src, cover_search_cooldown)) + return + COOLDOWN_START(src, cover_search_cooldown, 15 SECONDS) + var/list/turf_dict = list() + if(!recursive_turf_cover_scan(get_turf(tied_human), turf_dict, reverse_direction(projectile_dir))) + outside_cover_processing(bullet, projectile_dir, FALSE) + squad_cover_processing(FALSE, turf_dict, bullet, projectile_dir) + else + inside_cover_processing(turf_dict, FALSE) + squad_cover_processing(TRUE, turf_dict, bullet, projectile_dir) + + +/datum/human_ai_brain/proc/squad_cover_processing(inside = TRUE, list/turf_dict, obj/projectile/bullet, projectile_dir) + if(!squad_id) + return + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[squad_id]"] + if(!squad) + return + + for(var/datum/human_ai_brain/brain as anything in squad.ai_in_squad) + if(brain == src) + continue + + if(get_dist(tied_human, brain.tied_human) > view_distance) + continue + + if(brain.tied_human.is_mob_incapacitated()) + continue + + brain.squad_covering = TRUE + COOLDOWN_START(brain, cover_search_cooldown, 15 SECONDS) + + if(inside) + brain.inside_cover_processing(turf_dict, TRUE) + else + brain.outside_cover_processing(bullet, projectile_dir, TRUE) + +/datum/human_ai_brain/proc/recursive_turf_cover_scan(turf/scan_turf, list/turf_dict, cover_dir) + if(length(turf_dict) > 128) + return FALSE // abort if the room is too large + //if(istype(scan_turf, /turf/closed)) + // return TRUE // abort if we're a wall + if(scan_turf in turf_dict) + return TRUE // abort if we've already been scanned + turf_dict[scan_turf] = 0 + var/obj/structure/barricade/cade = locate() in scan_turf.contents + if(cade?.dir in get_related_directions(cover_dir)) + turf_dict[scan_turf] += 5 + var/obj/item/explosive/mine/mine = locate() in scan_turf.contents + if(mine) + if(!faction_check(mine.iff_signal)) + turf_dict[scan_turf] -= 50 + else + turf_dict[scan_turf] -= 2 // even if it's our mine, we don't really want to stand on it + for(var/obj/thing in scan_turf.contents) + if(thing.density && !istype(thing, /obj/structure/barricade)) + turf_dict[scan_turf] -= 1000 // If it has something dense on it, don't bother + for(var/cardinal in GLOB.cardinals) + var/turf/nearby_turf = get_step(scan_turf, cardinal) + if(!nearby_turf) + continue + var/obj/structure/reagent_dispensers/fueltank/tank = locate() in nearby_turf.contents + if(tank) + turf_dict[scan_turf] -= 10 // ideally not near any highly explosive fuel tanks if we can help it + if(istype(nearby_turf, /turf/closed)) + turf_dict[scan_turf] += 2 // Near a wall is a bit safer + continue + if(!recursive_turf_cover_scan(nearby_turf, turf_dict, cover_dir)) + return FALSE + return TRUE + + +/datum/human_ai_brain/proc/outside_cover_processing(obj/projectile/bullet, projectile_dir, from_squad = FALSE) + var/shortest_cover_dist = 50 + var/turf/shortest_cover_turf + var/atom/cover_atom + var/list/cardinal_bullet_dirs = make_dir_cardinal(projectile_dir) + var/list/inverse_cardinal_bullet_dirs = list() + for(var/dir in cardinal_bullet_dirs) + inverse_cardinal_bullet_dirs += reverse_direction(dir) + var/list/view_contents = view(7, tied_human) + cade_loop: + for(var/obj/structure/barricade/cade in view_contents) + for(var/atom/thing in cade.loc) + if(thing.density && (cade != thing)) + continue cade_loop + if(cade.dir in inverse_cardinal_bullet_dirs) + var/dist = get_dist(tied_human, cade) + if(dist < shortest_cover_dist) + shortest_cover_dist = dist + shortest_cover_turf = get_turf(cade) + cover_atom = cade + if(shortest_cover_turf) + ADD_ONGOING_ACTION(src, AI_ACTION_COVER, shortest_cover_turf, cover_atom, FALSE) + if(!from_squad) + squad_cover_processing(FALSE, view_contents - shortest_cover_turf) + else + squad_covering = FALSE + return + wall_loop: + for(var/turf/closed/wall in view_contents) + for(var/dir in cardinal_bullet_dirs) + var/turf/open/maybe_cover = get_step(wall, dir) + if(!istype(maybe_cover) || !(tied_human in viewers(world.view, maybe_cover))) + continue + for(var/atom/thing in maybe_cover) + if(thing.density) + continue wall_loop + if(bullet.firer in viewers(world.view, maybe_cover)) + continue + var/dist = get_dist(tied_human, maybe_cover) + if(dist < shortest_cover_dist) + shortest_cover_dist = dist + shortest_cover_turf = maybe_cover + cover_atom = wall + if(shortest_cover_turf) + ADD_ONGOING_ACTION(src, AI_ACTION_COVER, shortest_cover_turf, cover_atom, TRUE) + if(!from_squad) + squad_cover_processing(FALSE, view_contents - shortest_cover_turf) + + squad_covering = FALSE + + + +/datum/human_ai_brain/proc/inside_cover_processing(list/turf_dict, from_squad = FALSE) + var/highest_cover_value = turf_dict[turf_dict[1]] + var/turf/highest_cover_turf + for(var/turf/turf as anything in turf_dict) +#ifdef TESTING + turf.maptext = "

[turf_dict[turf]]

" +#endif + if(turf_dict[turf] > highest_cover_value) + highest_cover_value = turf_dict[turf] + highest_cover_turf = turf + if(!highest_cover_turf) + squad_covering = FALSE + return // damn +#ifdef TESTING + to_chat(world, "highest_cover_value: [highest_cover_value], turf coords: [highest_cover_turf.x], [highest_cover_turf.y], [highest_cover_turf.z]") + addtimer(CALLBACK(src, PROC_REF(clear_cover_value_debug), turf_dict), 60 SECONDS) + ADD_ONGOING_ACTION(src, AI_ACTION_COVER_I, highest_cover_turf) + if(!from_squad) + squad_cover_processing(TRUE, turf_dict - highest_cover_turf) + else + squad_covering = FALSE + +/datum/human_ai_brain/proc/clear_cover_value_debug(list/turf_list) + for(var/turf/T as anything in turf_list) + T.maptext = null + +#else + ADD_ONGOING_ACTION(src, AI_ACTION_COVER_I, highest_cover_turf) + if(!from_squad) + squad_cover_processing(TRUE, turf_dict - highest_cover_turf) + else + squad_covering = FALSE +#endif + diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm new file mode 100644 index 0000000000..ff7fc67efb --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm @@ -0,0 +1,177 @@ +/datum/human_ai_faction + var/faction = FACTION_NEUTRAL + VAR_PROTECTED/shoot_to_kill = TRUE + + VAR_PROTECTED/list/in_combat_lines = list() + VAR_PROTECTED/list/exit_combat_lines = list() + VAR_PROTECTED/list/squad_member_death_lines = list() + VAR_PROTECTED/list/grenade_thrown_lines = list() + + VAR_PROTECTED/list/friendly_factions = list() + VAR_PROTECTED/list/neutral_factions = list() + +/datum/human_ai_faction/proc/apply_faction_data(datum/human_ai_brain/brain) + if(length(in_combat_lines)) + brain.in_combat_lines = in_combat_lines + + if(length(exit_combat_lines)) + brain.exit_combat_lines = exit_combat_lines + + if(length(squad_member_death_lines)) + brain.squad_member_death_lines = squad_member_death_lines + + if(length(grenade_thrown_lines)) + brain.grenade_thrown_lines = grenade_thrown_lines + + brain.shoot_to_kill = shoot_to_kill + brain.friendly_factions = friendly_factions + brain.neutral_factions = neutral_factions + +/datum/human_ai_faction/proc/reapply_faction_data() + for(var/datum/human_ai_brain/brain in GLOB.human_ai_brains) + if(brain.tied_human?.faction == faction) + apply_faction_data(brain) + +/datum/human_ai_faction/proc/add_friendly_faction(faction) + if(faction in friendly_factions) + return + friendly_factions += faction + if(faction in neutral_factions) + neutral_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/add_neutral_faction(faction) + if(faction in neutral_factions) + return + neutral_factions += faction + if(faction in friendly_factions) + friendly_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/remove_friendly_faction(faction) + if(!(faction in friendly_factions)) + return + friendly_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/remove_neutral_faction(faction) + if(!(faction in neutral_factions)) + return + neutral_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/get_friendly_factions() + return friendly_factions + +/datum/human_ai_faction/proc/get_neutral_factions() + return neutral_factions + +/datum/human_ai_faction/proc/set_shoot_to_kill(new_kill = TRUE) + shoot_to_kill = new_kill + reapply_faction_data() + +/datum/human_ai_faction/proc/get_shoot_to_kill() + return shoot_to_kill + +/datum/human_ai_faction/clf + faction = FACTION_CLF + friendly_factions = list( + FACTION_COLONIST, + ) + + +/datum/human_ai_faction/uscm + faction = FACTION_MARINE + friendly_factions = list( + FACTION_COLONIST, + FACTION_TWE, + FACTION_WY, + ) + neutral_factions = list( + FACTION_FREELANCER, + FACTION_CONTRACTOR, + FACTION_UPP, + FACTION_MERCENARY, + ) + +/datum/human_ai_faction/upp + faction = FACTION_UPP + friendly_factions = list( + FACTION_COLONIST, + ) + neutral_factions = list( + FACTION_FREELANCER, + FACTION_CONTRACTOR, + FACTION_MARINE, + FACTION_MERCENARY, + FACTION_TWE, + ) + in_combat_lines = list( // zonenote: tweak these. They're entirely the stereotype of "communist russkie" when we can do better than that. also languages + "For the UPP!", + "Die, you animal!", + "Capitalist dog!", + "Shoot them!", + "For glorious Union!", + "Attacking!", + "We will bury them!", + "Uraaaa!!", + "URAAA!!", + "To your last breath!", + "You're worth nothing!", + "This is the end, for you!", + "Die!", + "*warcry", + ) + exit_combat_lines = list( + "I need a break...", + "Phew, that was tough work.", + "I think we can stop shooting now?", + "One step closer to victory!", + "Finally, break time.", + ) + squad_member_death_lines = list( + "Man down!", + "Comrade!!", + "Get together!", + "Damn!", + "Taking hits!", + ) + + +/datum/human_ai_faction/wy + faction = FACTION_WY + friendly_factions = list( + FACTION_COLONIST, + FACTION_TWE, + FACTION_MARINE, + ) + neutral_factions = list( + FACTION_FREELANCER, + FACTION_CONTRACTOR, + FACTION_MERCENARY, + ) + +/datum/human_ai_faction/wy_deathsquad + faction = FACTION_WY_DEATHSQUAD + friendly_factions = list( + FACTION_WY, + ) + in_combat_lines = list( + "Visual confirmed, engaging.", + "Engaging hostile.", + "Eliminating hostile.", + "Engaging.", + "Contact.", + "Viscon, proceeding." + ) + exit_combat_lines = list( + "Hostilities ceased.", + "Ceasing engagement." + ) + squad_member_death_lines = list( + "Allied unit disabled.", + "Friendly unit decomissioned.", + "Allied unit decomissioned.", + "Friendly unit disabled." + ) + grenade_thrown_lines = list() // Wouldn't need to call this out diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm new file mode 100644 index 0000000000..6cdfd59759 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm @@ -0,0 +1,94 @@ +/datum/human_ai_brain + var/obj/item/weapon/gun/primary_weapon + //var/obj/item/weapon/primary_melee + /// Currently firing a gun + var/currently_firing = FALSE + /// Appraisal datum + var/datum/firearm_appraisal/gun_data + /// How many rounds fired in this burst + var/rounds_burst_fired = 0 + /// If we've tried to reload (and failed) with our current inventory + var/tried_reload = FALSE + /// Cooldown for if we've fired too many rounds in a burst (for recoil) + COOLDOWN_DECLARE(fire_overload_cooldown) + +/datum/human_ai_brain/proc/unholster_primary() + if(tied_human.l_hand == primary_weapon || tied_human.r_hand == primary_weapon) + return + + if(tied_human.get_active_hand()) + tied_human.drop_held_item(tied_human.get_active_hand()) + + tied_human.u_equip(primary_weapon) + tied_human.put_in_active_hand(primary_weapon) + sleep(max(primary_weapon.wield_delay, short_action_delay * action_delay_mult)) + primary_weapon.wield(tied_human) + +/datum/human_ai_brain/proc/holster_primary() + if(tied_human.s_store || (tied_human.l_hand != primary_weapon && tied_human.r_hand != primary_weapon)) + return + + if(currently_firing) + end_gun_fire() + tied_human.equip_to_slot(primary_weapon, WEAR_J_STORE) + +/datum/human_ai_brain/proc/reload_primary() + set waitfor = FALSE + + if(!primary_weapon || tried_reload) + currently_busy = FALSE + return + + currently_busy = TRUE + + var/obj/item/ammo_magazine/mag = primary_ammo_search() + if(!mag) + tried_reload = TRUE +#ifdef TESTING + to_chat(world, "[tied_human.name] tried to reload without ammo.") +#endif + currently_busy = FALSE + return //soz + ensure_primary_hand(primary_weapon) + primary_weapon.unwield(tied_human) + sleep(short_action_delay * action_delay_mult) + if(!(primary_weapon.flags_gun_features & GUN_INTERNAL_MAG)) + primary_weapon.unload(tied_human, FALSE, TRUE, FALSE) + sleep(short_action_delay * action_delay_mult) + tied_human.swap_hand() + equip_item_from_equipment_map(HUMAN_AI_AMMUNITION, mag) + if(istype(mag, /obj/item/ammo_magazine/handful)) + for(var/i in 1 to mag.current_rounds) + primary_weapon.attackby(mag, tied_human) + sleep(micro_action_delay * action_delay_mult) + if(!QDELETED(mag) && (mag.current_rounds > 0)) + var/storage_slot = storage_has_room(mag) + if(storage_slot) + store_item(mag, storage_slot) + else + tied_human.drop_held_item(mag) + else + primary_weapon.attackby(mag, tied_human) + tied_human.swap_hand() + sleep(short_action_delay * action_delay_mult) + primary_weapon.wield(tied_human) +#ifdef TESTING + to_chat(world, "[tied_human.name] reloaded [primary_weapon].") +#endif + currently_busy = FALSE + +/datum/human_ai_brain/proc/primary_ammo_search() + for(var/obj/item/ammo_magazine/mag as anything in equipment_map[HUMAN_AI_AMMUNITION]) + if(istype(primary_weapon, mag.gun_type) && mag.ai_can_use(tied_human)) + return mag + +/datum/human_ai_brain/proc/should_reload_primary() + if(!primary_weapon || gun_data?.disposable) + return FALSE + + if(!primary_weapon.current_mag) + return TRUE + + if(primary_weapon.current_mag.current_rounds > 0 || primary_weapon?.in_chamber) + return FALSE + return TRUE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm new file mode 100644 index 0000000000..e8c1c3e4f7 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm @@ -0,0 +1,238 @@ +/datum/human_ai_brain + var/static/list/brute_heal_items = list( + /obj/item/stack/medical/advanced/bruise_pack, + /obj/item/reagent_container/hypospray/autoinjector/bicaridine, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/bicaridine, + /obj/item/storage/pill_bottle/merabica, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/burn_heal_items = list( + /obj/item/stack/medical/advanced/ointment, + /obj/item/reagent_container/hypospray/autoinjector/kelotane, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/kelotane, + /obj/item/storage/pill_bottle/keloderm, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/tox_heal_items = list( + /obj/item/reagent_container/hypospray/autoinjector/dylovene, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/antitox, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/oxy_heal_items = list( + /obj/item/reagent_container/hypospray/autoinjector/dexalinp, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/dexalin, + /obj/item/storage/pill_bottle/dexalinplus, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/bleed_heal_items = list( + /obj/item/stack/medical/advanced/bruise_pack, + /obj/item/stack/medical/bruise_pack, + ) + + var/static/list/bonebreak_heal_items = list( + /obj/item/stack/medical/splint, + ) + + var/static/list/painkiller_items = list( + /obj/item/reagent_container/hypospray/autoinjector/tramadol, + /obj/item/reagent_container/hypospray/autoinjector/oxycodone, + /obj/item/storage/pill_bottle/tramadol, + ) + + /// Populated in New() + var/static/list/all_medical_items = list() + + /// At what percentage of max HP to start searching for medical treatment + var/healing_start_threshold = 0.7 + /// Requires this much damage of one type to consider it a problem + var/damage_problem_threshold = 5 + /// Pain percentage (out of 100) for the AI to consider using painkillers + var/pain_percentage_threshold = 1 + + /// Cooldown on using pills to avoid OD. This isn't the best solution as it prevents the AI from using more than 1 pill of any kind every 10s, but it'll work for now + COOLDOWN_DECLARE(pill_use_cooldown) + +/datum/human_ai_brain/proc/healing_start_check() + return ((tied_human.health / tied_human.maxHealth) <= healing_start_threshold) || tied_human.is_bleeding() || tied_human.has_broken_limbs() + +/datum/human_ai_brain/proc/start_healing() + set waitfor = FALSE + + currently_busy = TRUE + // Prioritize brute, then bleed, then broken bones, then burn, then pain, then tox, then oxy. + if(tied_human.getBruteLoss() > damage_problem_threshold) + var/obj/item/brute_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, brute_heal_items) && heal_item.ai_can_use(tied_human)) + brute_heal = heal_item + break + + if(!brute_heal) + goto bleed + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, brute_heal) + sleep(short_action_delay) + brute_heal.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(brute_heal)) + store_item(brute_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed brute damage using [brute_heal].") +#endif + + bleed: + if(tied_human.is_bleeding()) + var/obj/item/bleed_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, bleed_heal_items) && heal_item.ai_can_use(tied_human)) + bleed_heal = heal_item + break + + if(!bleed_heal) + goto bone + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, bleed_heal) + sleep(short_action_delay) + bleed_heal.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(bleed_heal)) + store_item(bleed_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] fixed bleeding using [bleed_heal].") +#endif + + // Doesn't support bone-healing chems + bone: + if(tied_human.has_broken_limbs()) + var/obj/item/bone_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, bonebreak_heal_items) && heal_item.ai_can_use(tied_human)) + bone_heal = heal_item + break + + if(!bone_heal) + goto fire + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, bone_heal) + sleep(short_action_delay) + bone_heal.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(bone_heal)) + store_item(bone_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] splinted a fracture using [bone_heal].") +#endif + + fire: + if(tied_human.getFireLoss() > damage_problem_threshold) + var/obj/item/burn_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, burn_heal_items) && heal_item.ai_can_use(tied_human)) + burn_heal = heal_item + break + + if(!burn_heal) + goto pain + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, burn_heal) + sleep(short_action_delay) + burn_heal.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(burn_heal)) + store_item(burn_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed burn damage using [burn_heal].") +#endif + + pain: + // This has the issue of the AI taking multiple painkillers if high on pain, despite them not stacking. Not worth fixing atm + if(tied_human.pain.get_pain_percentage() > pain_percentage_threshold) + var/obj/item/painkiller + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, painkiller_items) && heal_item.ai_can_use(tied_human)) + painkiller = heal_item + break + + if(!painkiller) + goto tox + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, painkiller) + sleep(short_action_delay) + painkiller.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(painkiller)) + store_item(painkiller) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed pain using [painkiller].") +#endif + + tox: + if(tied_human.getToxLoss() > damage_problem_threshold) + var/obj/item/tox_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, tox_heal_items) && heal_item.ai_can_use(tied_human)) + tox_heal = heal_item + break + + if(!tox_heal) + goto oxy + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, tox_heal) + sleep(short_action_delay) + tox_heal.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(tox_heal)) + store_item(tox_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed tox damage using [tox_heal].") +#endif + + oxy: + if(tied_human.getOxyLoss() > damage_problem_threshold) + var/obj/item/oxy_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, oxy_heal_items) && heal_item.ai_can_use(tied_human)) + oxy_heal = heal_item + + if(!oxy_heal) + return + + end_gun_fire() + currently_busy = TRUE + holster_primary() + equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, oxy_heal) + sleep(short_action_delay) + oxy_heal.ai_use(tied_human) + sleep(short_action_delay) + if(!QDELETED(oxy_heal)) + store_item(oxy_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed oxygen damage using [oxy_heal].") +#endif + currently_busy = FALSE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm new file mode 100644 index 0000000000..51e7cff0ce --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm @@ -0,0 +1,353 @@ +/datum/human_ai_brain + var/list/equipped_items_original_loc = list() + + /// list("object_type" = list(object_ref = "slot") + var/list/equipment_map = list( + HUMAN_AI_HEALTHITEMS = list(), + HUMAN_AI_AMMUNITION = list(), + HUMAN_AI_GRENADES = list(), + HUMAN_AI_TOOLS = list(), + ) + + var/list/container_refs = list( + "belt" = null, + "backpack" = null, + "left_pocket" = null, + "right_pocket" = null, + ) + + var/static/list/important_storage_slots = list( + WEAR_BACK, + WEAR_WAIST, + WEAR_L_STORE, + WEAR_R_STORE, + ) + + var/static/important_storage_slots_bitflag = SLOT_BACK | SLOT_WAIST | SLOT_STORE + +/datum/human_ai_brain/proc/get_object_from_loc(object_loc) + RETURN_TYPE(/obj/item/storage) + + var/obj/item/storage/storage_object + switch(object_loc) + if("belt") + storage_object = tied_human.belt + if("backpack") + storage_object = tied_human.back + if("left_pocket") + storage_object = tied_human.l_store + if("right_pocket") + storage_object = tied_human.r_store + return storage_object + +/datum/human_ai_brain/proc/equip_item_from_equipment_map(object_type, obj/item/object_ref) + if(!object_type || !object_ref) + return + + var/object_loc = equipment_map[object_type][object_ref] + var/obj/item/storage/storage_object = get_object_from_loc(object_loc) + + storage_object.remove_from_storage(object_ref, tied_human) + tied_human.put_in_active_hand(object_ref) + equipped_items_original_loc[object_ref] = object_loc + RegisterSignal(object_ref, COMSIG_ITEM_DROPPED, PROC_REF(on_equipment_dropped), override = TRUE) + +/datum/human_ai_brain/proc/get_item_from_equipment_map_path(object_path, object_type) + return (locate(object_path) in equipment_map[object_type]) + +/datum/human_ai_brain/proc/store_item(obj/item/object_ref, object_loc, slot_type) + if(object_ref.loc != tied_human) + return + + if(object_ref in equipped_items_original_loc) + var/obj/item/storage/storage_object = get_object_from_loc(equipped_items_original_loc[object_ref]) + equipped_items_original_loc -= object_ref + storage_object.attempt_item_insertion(object_ref, FALSE, tied_human) + else if(object_loc) // we assume that we've already checked if something will fit or not + var/obj/item/storage/storage_item = container_refs[object_loc] + storage_item.attempt_item_insertion(object_ref, FALSE, tied_human) + if(slot_type) + equipment_map[slot_type][object_ref] = object_loc + +/// Whenever an item is deleted, purge it from anywhere it may be stored in here +/datum/human_ai_brain/proc/on_item_delete(obj/item/source, force) + SIGNAL_HANDLER + for(var/name in container_refs) + if(source == container_refs[name]) + container_refs[name] = null + return + + for(var/id in equipment_map) + for(var/obj/item/item_ref as anything in equipment_map[id]) + if(source == item_ref) + equipment_map[id] -= item_ref + return + +/datum/human_ai_brain/proc/on_item_equip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + + if((slot in important_storage_slots) && istype(equipment, /obj/item/storage)) + recalculate_containers() + appraise_inventory(slot == WEAR_WAIST, slot == WEAR_BACK, slot == WEAR_L_STORE, slot == WEAR_R_STORE) + + if(!primary_weapon && isgun(equipment)) + set_primary_weapon(equipment) + +/datum/human_ai_brain/proc/on_item_unequip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + + if((important_storage_slots_bitflag & slot) && istype(equipment, /obj/item/storage)) + recalculate_containers() + appraise_inventory(slot == SLOT_WAIST, slot == SLOT_BACK, slot == SLOT_STORE, slot == SLOT_STORE) + + if(isgun(equipment)) + appraise_inventory(FALSE, FALSE, FALSE, FALSE) + +/datum/human_ai_brain/proc/recalculate_containers() + container_refs = list() + if(isstorage(tied_human.belt)) + container_refs["belt"] = tied_human.belt + if(isstorage(tied_human.back)) + container_refs["backpack"] = tied_human.back + if(isstorage(tied_human.l_store)) + container_refs["left_pocket"] = tied_human.l_store + if(isstorage(tied_human.r_store)) + container_refs["right_pocket"] = tied_human.r_store + +/// Currently doesn't support recursive storage +/datum/human_ai_brain/proc/appraise_inventory(belt = TRUE, back = TRUE, pocket_l = TRUE, pocket_r = TRUE) + if(previous_faction != tied_human.faction) + previous_faction = tied_human.faction + var/datum/human_ai_faction/our_faction = SShuman_ai.human_ai_factions[tied_human.faction] + our_faction?.apply_faction_data(src) + + /*if(tied_human.shoes && !primary_melee) // snowflake bootknife check + var/obj/item/weapon/knife = locate() in tied_human.shoes + if(knife) + set_primary_melee(knife)*/ + + tried_reload = FALSE // We don't really need to do this in a smart way + if(belt) + if(!istype(tied_human.belt, /obj/item/storage)) // belts can be backpacks, don't ask + goto back_statement + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "belt") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.belt, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.belt, "belt") + + back_statement: + if(back) + if(!istype(tied_human.back, /obj/item/storage/backpack)) + goto l_pocket_statement + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "backpack") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.back, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.back, "backpack") + + l_pocket_statement: + if(pocket_l) + if(!istype(tied_human.l_store, /obj/item/storage/pouch)) + goto r_pocket_statement + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "left_pocket") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.l_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.l_store, "left_pocket") + + r_pocket_statement: + if(pocket_r) + if(!istype(tied_human.r_store, /obj/item/storage/pouch)) + return + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "right_pocket") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.r_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.r_store, "right_pocket") + +/datum/human_ai_brain/proc/item_slot_appraisal_loop(obj/item/container_to_loop, slot_to_assign) + for(var/obj/item/inv_item as anything in container_to_loop) + RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + if(inv_item.flags_human_ai & HEALING_ITEM) + equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = slot_to_assign + else if(inv_item.flags_human_ai & AMMUNITION_ITEM) + equipment_map[HUMAN_AI_AMMUNITION][inv_item] = slot_to_assign + else if(inv_item.flags_human_ai & GRENADE_ITEM) + equipment_map[HUMAN_AI_GRENADES][inv_item] = slot_to_assign + else if(inv_item.flags_human_ai & TOOL_ITEM) + equipment_map[HUMAN_AI_TOOLS][inv_item] = slot_to_assign + //else if((inv_item.flags_human_ai & MELEE_WEAPON_ITEM) && !primary_melee) + // set_primary_melee(inv_item) + +/datum/human_ai_brain/proc/clear_main_hand() + var/obj/item/active_hand = tied_human.get_active_hand() + if(!active_hand) + return + + if(primary_weapon == active_hand) + holster_primary() + return + + var/storage_id = storage_has_room(active_hand) + if(!storage_id) + tied_human.drop_held_item(active_hand) + return + + store_item(active_hand, storage_id) + +/datum/human_ai_brain/proc/storage_has_room(obj/item/inserting) + for(var/container_id in container_refs) + var/obj/item/storage/container = container_refs[container_id] + if(container?.can_be_inserted(inserting, tied_human, TRUE)) + return container_id + +/datum/human_ai_brain/proc/on_equipment_dropped(obj/item/source, mob/dropper) + SIGNAL_HANDLER + + if(isturf(source.loc)) + equipped_items_original_loc -= source + UnregisterSignal(source, COMSIG_ITEM_DROPPED) + +/datum/human_ai_brain/proc/on_item_pickup(datum/source, obj/item/picked_up) + SIGNAL_HANDLER + + if(!primary_weapon && isgun(picked_up)) + set_primary_weapon(picked_up) + +/datum/human_ai_brain/proc/on_item_drop(datum/source, obj/item/dropped) + SIGNAL_HANDLER + + if(dropped == primary_weapon) + set_primary_weapon(null) + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP_GUN, dropped) + + for(var/slot in container_refs) + if(container_refs[slot] == dropped) + appraise_inventory(slot == "belt", slot == "backpack", slot == "left_pocket", slot == "right_pocket") + break + + for(var/id in equipment_map) + for(var/obj/item/item_ref as anything in equipment_map[id]) + if(item_ref == dropped) + equipment_map[id] -= item_ref + return + +/datum/human_ai_brain/proc/set_primary_weapon(obj/item/weapon/gun/new_gun) + if(primary_weapon) + UnregisterSignal(primary_weapon, COMSIG_PARENT_QDELETING) + primary_weapon = new_gun + appraise_primary() + if(primary_weapon) + RegisterSignal(primary_weapon, COMSIG_PARENT_QDELETING, PROC_REF(on_primary_delete)) + +/datum/human_ai_brain/proc/on_primary_delete(datum/source, force) + SIGNAL_HANDLER + + set_primary_weapon(null) + +/*datum/human_ai_brain/proc/set_primary_melee(obj/item/weapon/new_melee) + if(primary_melee) + UnregisterSignal(primary_melee, COMSIG_PARENT_QDELETING) + primary_melee = new_melee + appraise_primary() + if(primary_melee) + RegisterSignal(primary_melee, COMSIG_PARENT_QDELETING, PROC_REF(on_primary_melee_delete)) + +/datum/human_ai_brain/proc/on_primary_melee_delete(datum/source, force) + SIGNAL_HANDLER + + set_primary_melee(null)*/ + +/datum/human_ai_brain/proc/appraise_primary() + if(!primary_weapon) + return + var/static/datum/firearm_appraisal/default = new() + for(var/datum/firearm_appraisal/appraisal as anything in GLOB.firearm_appraisals) + if(is_type_in_list(primary_weapon, appraisal.gun_types)) + gun_data = appraisal + break + + if(!gun_data) + gun_data = default + +/datum/human_ai_brain/proc/item_search(list/things_around) + search_loop: + for(var/obj/item/thing in things_around) + if(!isturf(thing.loc)) + continue + + if(!primary_weapon && isgun(thing)) + var/obj/item/weapon/gun/thing_gun = thing + for(var/datum/firearm_appraisal/appraisal as anything in GLOB.firearm_appraisals) + if(is_type_in_list(thing_gun, appraisal.gun_types)) + if(appraisal.disposable && thing_gun.current_mag?.current_rounds <= 0) + continue search_loop + break + + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + if(istype(thing, /obj/item/storage/belt) && !container_refs["belt"]) + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + if(istype(thing, /obj/item/storage/backpack) && !container_refs["backpack"]) + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + if(istype(thing, /obj/item/storage/pouch) && (!container_refs["left_pocket"] || !container_refs["right_pocket"])) + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + var/storage_spot = storage_has_room(thing) + if(!storage_spot || !thing.ai_can_use(tied_human)) + continue + + if(is_type_in_list(thing, all_medical_items)) + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + if(primary_weapon && istype(thing, /obj/item/ammo_magazine)) + var/obj/item/ammo_magazine/mag = thing + if(istype(primary_weapon, mag.gun_type)) + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + if(istype(thing, /obj/item/explosive/grenade)) + var/obj/item/explosive/grenade/nade = thing + if(nade.active) + continue + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + + if(thing.flags_human_ai & TOOL_ITEM) // zonenote: they can pick up 1 billion crowbars + ADD_ONGOING_ACTION(src, AI_ACTION_PICKUP, thing) + break + +/datum/human_ai_brain/proc/get_tool_from_equipment_map(tool_trait) + RETURN_TYPE(/obj/item) + for(var/obj/item/maybe_tool as anything in equipment_map[HUMAN_AI_TOOLS]) + if(!HAS_TRAIT(maybe_tool, tool_trait)) + continue + return maybe_tool diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm new file mode 100644 index 0000000000..1f7e185ca9 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm @@ -0,0 +1,94 @@ +/datum/human_ai_brain + var/ai_move_delay = 0 + var/list/current_path + var/turf/current_target_turf + var/path_update_period = (0.5 SECONDS) + var/no_path_found = FALSE + var/max_travel_distance = HUMAN_AI_MAX_PATHFINDING_RANGE + var/next_path_generation = 0 + /// Amount of times no path found has occured + var/no_path_found_amount = 0 + var/ai_timeout_time = 0 + var/ai_timeout_period = 2 SECONDS + + /// The time interval between calculating new paths if we cannot find a path + var/no_path_found_period = (2.5 SECONDS) + + /// Cooldown declaration for delaying finding a new path if no path was found + COOLDOWN_DECLARE(no_path_found_cooldown) + +/datum/human_ai_brain/proc/can_move_and_apply_move_delay() + // Unable to move, try next time. + if(ai_move_delay > world.time || !(tied_human.mobility_flags & MOBILITY_MOVE) || tied_human.is_mob_incapacitated(TRUE) || (tied_human.body_position != STANDING_UP && !tied_human.can_crawl) || tied_human.anchored) + return FALSE + + ai_move_delay = world.time + tied_human.move_delay + if(tied_human.recalculate_move_delay) + ai_move_delay = world.time + tied_human.movement_delay() + if(tied_human.next_move_slowdown) + ai_move_delay += tied_human.next_move_slowdown + tied_human.next_move_slowdown = 0 + return TRUE + +/datum/human_ai_brain/proc/move_to_next_turf(turf/T, max_range = max_travel_distance) + if(!T) + return FALSE + + if(no_path_found) + if(no_path_found_amount > 0) + COOLDOWN_START(src, no_path_found_cooldown, no_path_found_period) + no_path_found = FALSE + no_path_found_amount++ + return FALSE + + no_path_found_amount = 0 + + if((!current_path || (next_path_generation < world.time && current_target_turf != T)) && COOLDOWN_FINISHED(src, no_path_found_cooldown)) + if(!CALCULATING_PATH(tied_human) || current_target_turf != T) + SSpathfinding.calculate_path(tied_human, T, max_range, tied_human, CALLBACK(src, PROC_REF(set_path)), list(tied_human, current_target)) + current_target_turf = T + next_path_generation = world.time + path_update_period + + if(CALCULATING_PATH(tied_human)) + return TRUE + + // No possible path to target. + if(!current_path && get_dist(T, tied_human) > 0) + return FALSE + + // We've reached our destination + if(!length(current_path) || get_dist(T, tied_human) <= 0) + current_path = null + return TRUE + + var/turf/next_turf = current_path[length(current_path)] + // We've somehow deviated from our current path. Generate next path whenever possible. + if(get_dist(next_turf, tied_human) > 1) + current_path = null + return TRUE + + // Unable to move, try next time. + if(!can_move_and_apply_move_delay()) + return TRUE + + var/list/L = LinkBlocked(tied_human, tied_human.loc, next_turf, list(tied_human, current_target), TRUE) + L += SSpathfinding.check_special_blockers(tied_human, next_turf) + for(var/a in L) + var/atom/A = a + if(A.human_ai_obstacle(tied_human, src, get_dir(tied_human.loc, next_turf)) == INFINITY) + return FALSE + INVOKE_ASYNC(A, TYPE_PROC_REF(/atom, human_ai_act), tied_human, src) + var/successful_move = tied_human.Move(next_turf, get_dir(tied_human, next_turf)) + if(successful_move) + ai_timeout_time = world.time + current_path.len-- + + if(ai_timeout_time < world.time - ai_timeout_period) + return FALSE + + return TRUE + +/datum/human_ai_brain/proc/set_path(list/path) + current_path = path + if(!path) + no_path_found = TRUE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm new file mode 100644 index 0000000000..002acaf2cd --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm @@ -0,0 +1,110 @@ +/datum/human_ai_squad + /// Numeric ID of the squad + var/id + /// The AI humans in the squad + var/list/ai_in_squad = list() + /// Primary order assigned to this squad + var/datum/ongoing_action/order/assigned_order + /// Ref to the squad leader brain + var/datum/human_ai_brain/squad_leader + +/datum/human_ai_squad/New() + . = ..() + id = SShuman_ai.highest_squad_id + +/datum/human_ai_squad/Destroy(force, ...) + for(var/datum/human_ai_brain/brain as anything in ai_in_squad) + remove_from_squad(brain) + SShuman_ai.squads -= src + squad_leader = null + return ..() + +/datum/human_ai_squad/proc/add_to_squad(datum/human_ai_brain/adding) + if(adding.squad_id && (adding.squad_id in SShuman_ai.squad_id_dict)) + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict[adding.squad_id] + squad.remove_from_squad(adding) + adding.squad_id = id + ai_in_squad += adding + + adding.set_ongoing_order(assigned_order) + RegisterSignal(adding.tied_human, COMSIG_MOB_DEATH, PROC_REF(on_squad_member_death)) + RegisterSignal(adding, COMSIG_PARENT_QDELETING, PROC_REF(on_squad_member_delete)) + +/datum/human_ai_squad/proc/remove_from_squad(datum/human_ai_brain/removing) + if(removing == squad_leader) + set_squad_leader(null) + removing.ongoing_order = null + removing.squad_id = null + removing.is_squad_leader = FALSE + ai_in_squad -= removing + UnregisterSignal(removing?.tied_human, COMSIG_MOB_DEATH) + UnregisterSignal(removing, COMSIG_PARENT_QDELETING) + +/datum/human_ai_squad/proc/set_order(datum/ongoing_action/order) + assigned_order = order + RegisterSignal(order, COMSIG_PARENT_QDELETING, PROC_REF(on_order_delete)) + for(var/datum/human_ai_brain/brain as anything in ai_in_squad) + brain.set_ongoing_order(order) + +/datum/human_ai_squad/proc/set_squad_leader(datum/human_ai_brain/new_leader) + if(squad_leader) + squad_leader.is_squad_leader = FALSE + squad_leader = new_leader + if(squad_leader) + new_leader.is_squad_leader = TRUE + +/datum/human_ai_squad/proc/on_squad_member_death(mob/living/carbon/human/dead_mob) + SIGNAL_HANDLER + + if(istype(dead_mob, /mob/living/carbon/human/ai)) + var/mob/living/carbon/human/ai/dead_squddie = dead_mob + if(squad_leader == dead_squddie.ai_brain) + set_squad_leader(null) + + for(var/datum/human_ai_brain/squaddie as anything in ai_in_squad) + if(squaddie.tied_human.is_mob_incapacitated()) + continue + + squaddie.on_squad_member_death(dead_mob) + +/datum/human_ai_squad/proc/on_squad_member_delete(datum/human_ai_brain/deleting) + SIGNAL_HANDLER + + remove_from_squad(deleting) + +/datum/human_ai_squad/proc/on_order_delete(datum/source, force) + SIGNAL_HANDLER + + for(var/datum/human_ai_brain/squaddie as anything in ai_in_squad) + squaddie.clear_ongoing_order() + + assigned_order = null + +/datum/human_ai_brain + /// Numeric ID of the squad this AI is in, if any + var/squad_id + var/is_squad_leader = FALSE + +/datum/human_ai_brain/proc/add_to_squad(new_id) + if(isnull(new_id) || (new_id == squad_id)) + return + + if(!("[new_id]" in SShuman_ai.squad_id_dict)) + return + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[new_id]"] + squad.add_to_squad(src) + +/datum/human_ai_brain/proc/approach_squad_leader() + if(is_squad_leader || !("[squad_id]" in SShuman_ai.squad_id_dict)) + return FALSE + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[squad_id]"] + if(!squad.squad_leader?.tied_human) + return FALSE + + if(get_dist(tied_human, squad.squad_leader.tied_human) > 2) + if(!move_to_next_turf(squad.squad_leader.tied_human)) + return FALSE + return FALSE + return TRUE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm new file mode 100644 index 0000000000..8894e61600 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm @@ -0,0 +1,322 @@ +#define EXTRA_CHECK_DISTANCE_MULTIPLIER 0.20 + +/datum/human_ai_brain + /// At how far out the AI can see cloaked enemies + var/cloak_visible_range = 3 + /// Maximum range to consider a target + var/ai_range = 7 + /// Ref to the currently focused (and shooting at) target + var/mob/living/current_target + /// Ref to the last turf that the AI shot at + var/turf/open/target_floor + /// If TRUE, the AI is allowed to establish overwatches + var/overwatch_allowed = FALSE + /// If TRUE, the AI will throw grenades at enemies who enter cover + var/grenading_allowed = TRUE + /// List of overwatched turfs + var/list/turf/open/overwatch_turfs = list() + + COOLDOWN_DECLARE(return_fire) + +/datum/human_ai_brain/proc/get_target(range) + var/list/viable_targets = list() + var/atom/movable/closest_target + var/smallest_distance = INFINITY + + for(var/mob/living/carbon/potential_target as anything in GLOB.alive_mob_list) + if(!istype(potential_target)) + continue + + if(tied_human.z != potential_target.z) + continue + + if(!can_target(potential_target)) + continue + + if(!(potential_target in viewers(tied_human))) // for now, only consider targets in view + continue + + var/distance = get_dist(tied_human, potential_target) + + if(distance > ai_range) + continue + + viable_targets += potential_target + + if(smallest_distance <= distance) + continue + + closest_target = potential_target + smallest_distance = distance + + for(var/obj/vehicle/multitile/potential_vehicle_target as anything in GLOB.all_multi_vehicles) + if(tied_human.z != potential_vehicle_target.z) + continue + + var/distance = get_dist(tied_human, potential_vehicle_target) + + if(distance > ai_range) + continue + + if(potential_vehicle_target.health <= 0) + continue + + if(potential_vehicle_target.vehicle_faction == tied_human.faction) + continue + + /*var/skip_vehicle = FALSE + var/list/interior_living_mobs = potential_vehicle_target.interior.get_passengers() + for(var/mob/living/carbon/human/human_mob in interior_living_mobs) + if(!can_target(human_mob)) + continue + + skip_vehicle = TRUE + break + + if(skip_vehicle) + continue*/ + + viable_targets += potential_vehicle_target + + if(smallest_distance <= distance) + continue + + closest_target = potential_vehicle_target + smallest_distance = distance + + for(var/obj/structure/machinery/defenses/potential_defense_target as anything in GLOB.all_active_defenses) + if(tied_human.z != potential_defense_target.z) + continue + + var/distance = get_dist(tied_human, potential_defense_target) + + if(distance > ai_range) + continue + + viable_targets += potential_defense_target + + if(smallest_distance <= distance) + continue + + closest_target = potential_defense_target + smallest_distance = distance + + var/extra_check_distance = round(smallest_distance * EXTRA_CHECK_DISTANCE_MULTIPLIER) + + if(extra_check_distance < 1) + return closest_target + + var/list/extra_checked = orange(extra_check_distance, closest_target) + + var/list/final_targets = extra_checked & viable_targets + + return length(final_targets) ? pick(final_targets) : closest_target + + +/datum/human_ai_brain/proc/can_target(mob/living/carbon/target) + if(!istype(target)) + return FALSE + + if(target.stat == DEAD) + return FALSE + + if(!shoot_to_kill && target.stat == UNCONSCIOUS) + return FALSE + + if(faction_check(target)) + return FALSE + + if(HAS_TRAIT(target, TRAIT_CLOAKED) && get_dist(tied_human, target) > cloak_visible_range) + return FALSE + + return TRUE + +/datum/human_ai_brain/proc/attack_target() + set waitfor = FALSE + + if(!current_target) + currently_busy = FALSE + return + + if(!(primary_weapon in tied_human.get_hands())) + unholster_primary() + primary_weapon.guaranteed_delay_time = world.time + primary_weapon.wield_time = world.time + primary_weapon.pull_time = world.time + + tied_human.face_atom(current_target) + + if((get_dist(tied_human, current_target) > gun_data.maximum_range) && COOLDOWN_FINISHED(src, return_fire)) + currently_busy = FALSE + return + + primary_weapon.set_target(current_target) + gun_data.before_fire(primary_weapon, tied_human, src) + if((!primary_weapon?.current_mag?.current_rounds && !primary_weapon?.in_chamber) || !friendly_check()) + end_gun_fire() + return + + currently_firing = TRUE + enter_combat() + RegisterSignal(tied_human, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fire), TRUE) + primary_weapon.start_fire(object = current_target, bypass_checks = TRUE) + +/datum/human_ai_brain/proc/friendly_check() + var/list/turf_list = get_line(get_turf(tied_human), get_turf(current_target)) + for(var/turf/tile in turf_list) + if(istype(tile, /turf/closed)) + return TRUE + + for(var/mob/living/carbon/human/possible_friendly in tile) + if(tied_human == possible_friendly) + continue + + if(possible_friendly.body_position == LYING_DOWN) + continue + + if(faction_check(possible_friendly)) + return FALSE + return TRUE + +/datum/human_ai_brain/proc/on_gun_fire(datum/source, obj/item/weapon/gun/fired) + SIGNAL_HANDLER + + //var/atom/target = primary_weapon.get_target() + if(QDELETED(current_target)) + end_gun_fire() + return + + if(ismob(current_target)) + //var/mob/targeted_mob = target + if(current_target.stat == DEAD) + end_gun_fire() + return + + else if(current_target.stat == UNCONSCIOUS && !shoot_to_kill) + end_gun_fire() + return + + if(rounds_burst_fired > gun_data.burst_amount_max) + COOLDOWN_START(src, fire_overload_cooldown, max(short_action_delay, short_action_delay * action_delay_mult)) + end_gun_fire() + return + + if(QDELETED(current_target) || !friendly_check()) + end_gun_fire() + return + + if(primary_weapon.current_mag?.current_rounds <= 0 && !primary_weapon.in_chamber) // bullet removal comes after comsig is triggered + end_gun_fire() + if(gun_data?.disposable) + var/obj/item/gun = primary_weapon + set_primary_weapon(null) + tied_human.drop_held_item(gun) + return + + if(!(current_target in viewers(view_distance, tied_human))) + if(COOLDOWN_FINISHED(src, return_fire)) + end_gun_fire() + if(grenading_allowed && length(equipment_map[HUMAN_AI_GRENADES])) + throw_grenade_cover() + return + if(overwatch_allowed) + establish_overwatch() + return + + if(get_dist(tied_human, current_target) > gun_data.maximum_range) + if(COOLDOWN_FINISHED(src, return_fire)) + end_gun_fire() + if(!in_cover) // Doing this independently from viewers() check so we don't chase enemy if it's hid just around the corner. Might move it back + ADD_ONGOING_ACTION(src, AI_ACTION_APPROACH_CAREFUL, target_floor, 0) + return + + if(istype(primary_weapon, /obj/item/weapon/gun/shotgun/pump)) + var/obj/item/weapon/gun/shotgun/pump/shotgun = primary_weapon + addtimer(CALLBACK(shotgun, TYPE_PROC_REF(/obj/item/weapon/gun/shotgun/pump, pump_shotgun), tied_human), shotgun.pump_delay) + addtimer(CALLBACK(shotgun, TYPE_PROC_REF(/obj/item/weapon/gun/shotgun/pump, start_fire), null, current_target, null, null, null, TRUE), max(shotgun.pump_delay, shotgun.get_fire_delay()) + 1) // max with fire delay + + else if(istype(primary_weapon, /obj/item/weapon/gun/boltaction)) + var/obj/item/weapon/gun/boltaction/bolt = primary_weapon + addtimer(CALLBACK(bolt, TYPE_PROC_REF(/obj/item/weapon/gun/boltaction, unique_action), tied_human), 1) + addtimer(CALLBACK(bolt, TYPE_PROC_REF(/obj/item/weapon/gun/boltaction, unique_action), tied_human), bolt.bolt_delay + 1) + addtimer(CALLBACK(bolt, TYPE_PROC_REF(/obj/item/weapon/gun/boltaction, start_fire), null, current_target, null, null, null, TRUE), (bolt.bolt_delay * 2) + 1) + + if(primary_weapon.gun_firemode == GUN_FIREMODE_AUTOMATIC) + rounds_burst_fired++ + + else if(primary_weapon.gun_firemode == GUN_FIREMODE_SEMIAUTO) + addtimer(CALLBACK(primary_weapon, TYPE_PROC_REF(/obj/item/weapon/gun, start_fire), null, current_target, null, null, null, TRUE), primary_weapon.get_fire_delay()) + + target_floor = get_turf(current_target) + tied_human.face_atom(target_floor) + +/datum/human_ai_brain/proc/end_gun_fire() + primary_weapon?.set_target(null) + if(current_target) + UnregisterSignal(current_target, COMSIG_PARENT_QDELETING) + UnregisterSignal(tied_human, COMSIG_MOB_FIRED_GUN) + current_target = null + currently_busy = FALSE + currently_firing = FALSE + rounds_burst_fired = 0 + +/// If a target moves behind cover, the AI will overwatch the tiles around where the target was last scene. +/// If they step on a visible overwatched tile, the AI immediately opens fire. The AI discards all overwatches when moving. +/datum/human_ai_brain/proc/establish_overwatch() + if(!target_floor) + return + +#ifdef TESTING + to_chat(world, "[tied_human.name] has established new overwatch at [target_floor.x], [target_floor.y], [target_floor.z].") +#endif + if(length(overwatch_turfs)) // for now, only 1 overwatch at a time + clear_overwatch() + + for(var/turf/open/floor in range(2, target_floor)) // everything within 2 tiles of the center gets marked + RegisterSignal(floor, COMSIG_TURF_ENTERED, PROC_REF(on_overwatch_turf_enter)) + overwatch_turfs += floor +#ifdef TESTING + floor.color = "#aca43a" +#endif + +/datum/human_ai_brain/proc/clear_overwatch() + for(var/turf/open/floor as anything in overwatch_turfs) + UnregisterSignal(floor, COMSIG_TURF_ENTERED) +#ifdef TESTING + floor.color = null +#endif + +#ifdef TESTING + if(length(overwatch_turfs)) + to_chat(world, "[tied_human.name] has cleared existing overwatch.") +#endif + overwatch_turfs.Cut() + +/datum/human_ai_brain/proc/on_overwatch_turf_enter(datum/source, atom/movable/entering) + SIGNAL_HANDLER + + if(currently_firing || currently_busy) + clear_overwatch() + return + + if(!can_target(entering)) + return + + if(!(entering in viewers(world.view, tied_human))) + return + + clear_overwatch() + current_target = entering + attack_target() + +/datum/human_ai_brain/proc/throw_grenade_cover() + if(!target_floor || has_ongoing_action(AI_ACTION_NADE)) + return + + var/obj/item/explosive/grenade/nade = locate() in equipment_map[HUMAN_AI_GRENADES] + if(!nade) + return + + ADD_ONGOING_ACTION(src, AI_ACTION_NADE, nade, target_floor) + +#undef EXTRA_CHECK_DISTANCE_MULTIPLIER diff --git a/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm b/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm new file mode 100644 index 0000000000..3fa21daca7 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm @@ -0,0 +1,145 @@ +/datum/human_faction_management_menu + +/datum/human_faction_management_menu/New() + +/datum/human_faction_management_menu/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "HumanFactionManager") + ui.open() + +/datum/human_faction_management_menu/ui_state(mob/user) + return GLOB.admin_state + +/datum/human_faction_management_menu/ui_data(mob/user) + var/list/data = list() + + data["datumless_factions"] = FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH + + data["factions"] = list() + for(var/faction_name in SShuman_ai.human_ai_factions) + var/datum/human_ai_faction/ai_faction = SShuman_ai.human_ai_factions[faction_name] + data["factions"] += list(list( + "name" = ai_faction.faction, + "shoot_to_kill" = ai_faction.get_shoot_to_kill(), + "friendly_factions" = english_list(ai_faction.get_friendly_factions()), + "neutral_factions" = english_list(ai_faction.get_neutral_factions()), + "ref" = REF(ai_faction), + )) + data["datumless_factions"] -= ai_faction.faction + + return data + +/datum/human_faction_management_menu/ui_static_data(mob/user) + var/list/data = list() + + data["all_factions"] = FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH + + return data + +/datum/human_faction_management_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("create_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(gotten_faction in SShuman_ai.human_ai_factions) + return + + var/datum/human_ai_faction/new_faction = new() + new_faction.faction = gotten_faction + SShuman_ai.human_ai_factions[new_faction.faction] = new_faction + return TRUE + + if("set_shoot_to_kill") + if(!params["new_value"] || !params["faction_name"]) + return + + var/gotten_faction = params["faction_name"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + faction_obj.set_shoot_to_kill(text2num(params["new_value"])) + return TRUE + + if("remove_neutral_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Remove which faction being neutral to [gotten_faction]?", "Remove Neutral Faction", faction_obj.get_neutral_factions()) + if(!gotten_input) + return + + faction_obj.remove_neutral_faction(gotten_input) + return TRUE + + if("remove_friendly_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Remove which faction being friendly to [gotten_faction]?", "Remove Friendly Faction", faction_obj.get_friendly_factions()) + if(!gotten_input) + return + + faction_obj.remove_friendly_faction(gotten_input) + return TRUE + + if("add_neutral_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Set which faction being neutral to [gotten_faction]?", "Add Neutral Faction", (FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH) - faction_obj.get_neutral_factions() - faction_obj.faction) + if(!gotten_input) + return + + faction_obj.add_neutral_faction(gotten_input) + + if("add_friendly_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Set which faction being friendly to [gotten_faction]?", "Add Friendly Faction", (FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH) - faction_obj.get_friendly_factions() - faction_obj.faction) + if(!gotten_input) + return + + faction_obj.add_friendly_faction(gotten_input) + +/client/proc/open_human_faction_management_panel() + set name = "Human Faction Management Panel" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + if(human_faction_menu) + human_faction_menu.tgui_interact(mob) + return + + human_faction_menu = new /datum/human_faction_management_menu(src) + human_faction_menu.tgui_interact(mob) diff --git a/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm new file mode 100644 index 0000000000..9810cb5512 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm @@ -0,0 +1,106 @@ +GLOBAL_LIST_INIT_TYPED(firearm_appraisals, /datum/firearm_appraisal, build_firearm_appraisal_list()) + +/proc/build_firearm_appraisal_list() + . = list() + for(var/type in subtypesof(/datum/firearm_appraisal)) + . += new type + + +/datum/firearm_appraisal + /// Minimum engagement range with weapon type + var/minimum_range = 2 + /// Optimal engagement range, try to stay at this distance + var/optimal_range = 6 + /// Maximum engagement range, stop firing at this distance + var/maximum_range = 9 + /// How many rounds to fire in 1 burst at most + var/burst_amount_max = 8 + /// List of types that set the human AI to this appraisal type + var/list/gun_types = list() + /// If TRUE, this gun is disposable and isn't worth trying to reload + var/disposable = FALSE + +/// List of things we do before our next fire based on weapon type +/datum/firearm_appraisal/proc/before_fire(obj/item/weapon/gun/firearm, mob/living/carbon/user, datum/human_ai_brain/AI) + SHOULD_CALL_PARENT(TRUE) // Every weapon can be twohanded + + AI.ensure_primary_hand(firearm) + if((firearm.flags_item & TWOHANDED) && !(firearm.flags_item & WIELDED)) + firearm.wield(user) + sleep(max(firearm.wield_delay, AI.short_action_delay * AI.action_delay_mult)) + + if(firearm.flags_gun_features & GUN_TRIGGER_SAFETY) + firearm.flags_gun_features ^= GUN_TRIGGER_SAFETY + firearm.gun_safety_handle(user) + +/datum/firearm_appraisal/rifle + burst_amount_max = 8 + gun_types = list( + /obj/item/weapon/gun/rifle, + ) + +/datum/firearm_appraisal/smartgun + burst_amount_max = 18 + gun_types = list( + /obj/item/weapon/gun/smartgun, + ) + +/datum/firearm_appraisal/smg + burst_amount_max = 10 + minimum_range = 1 + optimal_range = 5 + gun_types = list( + /obj/item/weapon/gun/smg, + ) + +/datum/firearm_appraisal/shotgun + burst_amount_max = 2 + minimum_range = 1 + optimal_range = 1 // point-blank our beloved + maximum_range = 3 + gun_types = list( + /obj/item/weapon/gun/shotgun, + ) + +/datum/firearm_appraisal/shotgun/before_fire(obj/item/weapon/gun/shotgun/firearm, mob/living/carbon/user, datum/human_ai_brain/AI) + . = ..() + if(firearm.in_chamber) + return + firearm.unique_action(user) + +/datum/firearm_appraisal/boltaction + gun_types = list( + /obj/item/weapon/gun/boltaction, + ) + +/datum/firearm_appraisal/boltaction/before_fire(obj/item/weapon/gun/boltaction/firearm, mob/living/carbon/user, datum/human_ai_brain/AI) + . = ..() + if(firearm.in_chamber) + return + firearm.unique_action(user) + firearm.recent_cycle = world.time + firearm.unique_action(user) + firearm.recent_cycle = world.time + +/datum/firearm_appraisal/flamer + burst_amount_max = 1 + minimum_range = 5 // To not try and walk into our flames in tight spaces + optimal_range = 5 + maximum_range = 5 + gun_types = list( + /obj/item/weapon/gun/flamer, + ) + +/datum/firearm_appraisal/rpg + minimum_range = 5 + optimal_range = 6 + gun_types = list( + /obj/item/weapon/gun/launcher/rocket/anti_tank/disposable, + ) + disposable = TRUE + +/datum/firearm_appraisal/rpg/multi_use + gun_types = list( + /obj/item/weapon/gun/launcher/rocket, + ) + disposable = FALSE diff --git a/code/modules/mob/living/carbon/human/ai/fortify_room.dm b/code/modules/mob/living/carbon/human/ai/fortify_room.dm new file mode 100644 index 0000000000..130c076e17 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/fortify_room.dm @@ -0,0 +1,67 @@ +/client/proc/fortify_room() + set name = "Fortify Room" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/list/turf_list = list() + var/retval + + switch(tgui_input_list(mob, "How fortified should this be?", "Fortification Level", list("Wood", "Sandbag", "Sandbag (Wired)", "Metal", "Metal (Wired)", "Plasteel", "Plasteel (Wired)"))) + if("Wood") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/wooden, null) + if("Sandbag") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/sandbags/full, null) + if("Sandbag (Wired)") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/sandbags/wired, null) + if("Metal") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal, /obj/structure/barricade/plasteel/metal) + if("Metal (Wired)") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/wired, /obj/structure/barricade/plasteel/metal/wired) + if("Plasteel") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/plasteel, /obj/structure/barricade/plasteel) + if("Plasteel (Wired)") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/plasteel/wired, /obj/structure/barricade/plasteel/wired) + + if(retval) + to_chat(src, SPAN_NOTICE("Room fortified. Tiles scanned: [length(turf_list)].")) + else + to_chat(src, SPAN_NOTICE("Room too large to fully fortify. Capped at [length(turf_list)].")) + +/proc/recursive_turf_room_fortify(turf/scan_turf, list/turf_list, cade_type, folding_cade_type) + if(length(turf_list) > 195) // We're choosing 195 because 200 is the BYOND recursion limit so we're just playing it safe + return FALSE // abort if the room is too large + if(istype(scan_turf, /turf/closed)) + return TRUE // abort if we're a wall + if(scan_turf in turf_list) + return TRUE // abort if we've already been scanned + if((locate(/obj/structure/machinery/door) in scan_turf) || (locate(/obj/structure/window_frame) in scan_turf) || (locate(/obj/structure/window) in scan_turf)) + return TRUE // abort if there's a door or window here + turf_list += scan_turf + for(var/cardinal in GLOB.cardinals) + var/turf/nearby_turf = get_step(scan_turf, cardinal) + if(!nearby_turf) + continue + + if((locate(/obj/structure/window_frame) in nearby_turf) || (locate(/obj/structure/window) in nearby_turf)) + for(var/obj/structure/barricade/existing_cade in scan_turf) + if(existing_cade.dir == cardinal) + goto next_recurse + + var/obj/structure/barricade/cade = new cade_type(scan_turf) + cade.setDir(cardinal) + + if(folding_cade_type && (locate(/obj/structure/machinery/door) in nearby_turf)) + for(var/obj/structure/barricade/existing_cade in scan_turf) + if(existing_cade.dir == cardinal) + goto next_recurse + + var/obj/structure/barricade/plasteel/cade = new folding_cade_type(scan_turf) + cade.setDir(cardinal) + cade.open(cade) // this closes it + + next_recurse: + if(!recursive_turf_room_fortify(nearby_turf, turf_list, cade_type, folding_cade_type)) + return FALSE + return TRUE diff --git a/code/modules/mob/living/carbon/human/ai/human_ai.dm b/code/modules/mob/living/carbon/human/ai/human_ai.dm new file mode 100644 index 0000000000..4a86607fcc --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/human_ai.dm @@ -0,0 +1,52 @@ +GLOBAL_LIST_EMPTY(non_ai_humans) +GLOBAL_LIST_EMPTY(ai_humans) + +/mob/living/carbon/human + var/has_ai = FALSE + var/datum/human_ai_brain/ai_brain + +/mob/living/carbon/human/Initialize(mapload, new_species, ai) + . = ..() + if(has_ai) + ai = TRUE + if(!ai) + GLOB.non_ai_humans += src + else + real_name = random_name(gender, "Human") + name = real_name + //var/datum/preferences/prefs = new + //prefs.randomize_appearance(src) + //qdel(prefs) + GLOB.ai_humans += src + ai_brain = new(src) + create_hud() + a_intent = INTENT_DISARM + //INVOKE_ASYNC(src) + +/mob/living/carbon/human/Destroy(force) + if(ai_brain) + GLOB.ai_humans -= src + else + GLOB.non_ai_humans -= src + QDEL_NULL(ai_brain) + return ..() + +/mob/living/carbon/human/proc/process_ai() + SHOULD_CALL_PARENT(TRUE) + if(client) + return FALSE + +/mob/living/carbon/human/ai + has_ai = TRUE + mob_flags = AI_CONTROLLED + +/mob/living/carbon/human/ai/set_species(new_species, default_color) + . = ..() + mob_flags |= AI_CONTROLLED + +/obj/item/storage/backpack/satchel/ai + name = "satchel" + +/obj/item/storage/backpack/satchel/ai/fill_preset_inventory() + new /obj/item/reagent_container/hypospray/autoinjector/bicaridine(src) + new /obj/item/ammo_magazine/rifle(src) diff --git a/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm b/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm new file mode 100644 index 0000000000..e9600c343a --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm @@ -0,0 +1,263 @@ +/atom/proc/human_ai_obstacle(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain, direction, turf/target) + if(get_turf(src) == target) + return 0 + return INFINITY + +/atom/proc/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + ai_human.do_click(src, "", list()) + return TRUE + + +///////////////////////////// +// OBJECTS // +///////////////////////////// +/obj/structure/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + if(!density) + return 0 + + if(!climbable) + return + + return OBJECT_PENALTY + +/obj/structure/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + if(climbable && !human_ai.action_busy) + do_climb(human_ai) + + return ..() + +/obj/structure/barricade/plasteel/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + if(!closed) // this means it's closed + ai_human.do_click(src, "", list()) + + return TRUE + +///////////////////////////// +// MINERAL DOOR // +///////////////////////////// +/obj/structure/mineral_door/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(!brain.primary_weapon) + return INFINITY + + return DOOR_PENALTY + +/obj/structure/mineral_door/resin/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + ADD_ONGOING_ACTION(brain, AI_ACTION_MELEE_ATOM, src) + return ..() + + +///////////////////////////// +// PLATFORMS // +///////////////////////////// +/obj/structure/platform/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return DOOR_PENALTY + + +///////////////////////////// +// PODDDOORS // +///////////////////////////// +/obj/structure/machinery/door/poddoor/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + if(!(stat & NOPOWER)) + return INFINITY + + if(density && !operating && !unacidable && brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR)) + return DOOR_PENALTY + + return INFINITY + +/obj/structure/machinery/door/airlock/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + if(locked || welded || isElectrified()) + return + + . = ..() + + if(!(stat & NOPOWER) || !brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR)) + return + + brain.holster_primary() + var/obj/item/crowbar = brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR) + brain.equip_item_from_equipment_map(HUMAN_AI_TOOLS, crowbar) + attackby(crowbar, ai_human) + brain.store_item(crowbar, brain.storage_has_room(crowbar), HUMAN_AI_TOOLS) + +///////////////////////////// +// AIRLOCK // +///////////////////////////// +/obj/structure/machinery/door/airlock/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + if(locked || welded || isElectrified()) + return LOCKED_DOOR_PENALTY + + return DOOR_PENALTY + +/obj/structure/machinery/door/airlock/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + if(locked || welded || isElectrified()) + return + + . = ..() + + if(!(stat & NOPOWER)) + return + + brain.holster_primary() + var/obj/item/crowbar = brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR) + brain.equip_item_from_equipment_map(HUMAN_AI_TOOLS, crowbar) + attackby(crowbar, ai_human) + brain.store_item(crowbar, brain.storage_has_room(crowbar), HUMAN_AI_TOOLS) + +///////////////////////////// +// HUMANS // +///////////////////////////// +/mob/living/carbon/human/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(status_flags & GODMODE) + return ..() + + return HUMAN_PENALTY + +/*/mob/living/carbon/human/xeno_ai_act(mob/living/carbon/xenomorph/X) + if(status_flags & GODMODE) + return + + . = ..() +*/ + + +///////////////////////////// +// XENOS // +///////////////////////////// +/mob/living/carbon/xenomorph/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return XENO_PENALTY + +///////////////////////////// +// VEHICLES // +///////////////////////////// +/obj/vehicle/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return VEHICLE_PENALTY + + +///////////////////////////// +// SENTRY // +///////////////////////////// +/obj/structure/machinery/defenses/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return SENTRY_PENALTY + + +///////////////////////////// +// WINDOW FRAME // +///////////////////////////// +/*obj/structure/window_frame/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(buildstacktype && brain.get_tool_from_equipment_map(TRAIT_TOOL_WRENCH)) + return ..() + return WINDOW_FRAME_PENALTY*/ + + +///////////////////////////// +// BARRICADES // +///////////////////////////// +/obj/structure/barricade/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return BARRICADE_PENALTY + +/obj/structure/barricade/plasteel/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + . = ..() + if(!closed) + close(src) + +/obj/structure/barricade/handrail/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return DOOR_PENALTY + + +///////////////////////////// +// FIRE // +///////////////////////////// +/obj/flamer_fire/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/braineno, direction, turf/target) + . = ..() + if(!.) + return + + return FIRE_PENALTY + + +///////////////////////////// +// WALLS // +///////////////////////////// +/turf/closed/wall/resin/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/braineno, direction, turf/target) + . = ..() + if(!.) + return + + return WALL_PENALTY + + +///////////////////////////// +// FLOOR // +///////////////////////////// +/* + Sometimes open turfs are passed back as obstacles due to platforms and such, + generally it's fast so very slight penalty mainly for handling subtypes properly +*/ +/turf/open/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return OPEN_TURF_PENALTY + +/turf/open/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + return FALSE + +/turf/open/space/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return INFINITY + + +///////////////////////////// +// RIVER // +///////////////////////////// +/turf/open/gm/river/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(. && !covered) + . += base_river_slowdown + +/turf/open/gm/river/desert/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(toxic && !covered) + return FIRE_PENALTY + + return ..() diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 5eadbd57c7..bba361b18e 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1,4 +1,4 @@ -/mob/living/carbon/human/Initialize(mapload, new_species = null) +/mob/living/carbon/human/Initialize(mapload, new_species = null, ai = FALSE) blood_type = pick(7;"O-", 38;"O+", 6;"A-", 34;"A+", 2;"B-", 9;"B+", 1;"AB-", 3;"AB+") GLOB.human_mob_list += src GLOB.alive_human_list += src diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index dbc30b964a..b01a72f4fb 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -102,6 +102,12 @@ . = ..() /mob/living/carbon/human/u_equip(obj/item/I, atom/newloc, nomoveupdate, force) + var/slot + if(I) + if(I == back) + slot = SLOT_BACK + else if(I == wear_mask) + slot = SLOT_FACE . = ..() if(!. || !I) return FALSE @@ -119,6 +125,7 @@ if(I.flags_inv_hide & HIDEJUMPSUIT) update_inv_w_uniform() update_inv_wear_suit() + slot = SLOT_OCLOTHING else if(I == w_uniform) if(r_store) drop_inv_item_on_ground(r_store) @@ -129,6 +136,7 @@ w_uniform = null update_suit_sensors() update_inv_w_uniform() + slot = SLOT_ICLOTHING else if(I == head) var/updatename = 0 if(head.flags_inv_hide & HIDEFACE) @@ -146,43 +154,54 @@ update_inv_glasses() update_tint() update_inv_head() + slot = SLOT_HEAD else if (I == gloves) gloves = null update_inv_gloves() + slot = SLOT_HANDS else if (I == glasses) glasses = null update_tint() update_glass_vision(I) update_inv_glasses() + slot = SLOT_EYES else if (I == wear_l_ear) wear_l_ear = null update_inv_ears() + slot = SLOT_EAR else if (I == wear_r_ear) wear_r_ear = null update_inv_ears() + slot = SLOT_EAR else if (I == shoes) shoes = null update_inv_shoes() + slot = SLOT_FEET else if (I == belt) belt = null update_inv_belt() + slot = SLOT_WAIST else if (I == wear_id) wear_id = null sec_hud_set_ID() hud_set_squad() update_inv_wear_id() name = get_visible_name() + slot = SLOT_ID else if (I == r_store) r_store = null update_inv_pockets() + slot = SLOT_STORE else if (I == l_store) l_store = null update_inv_pockets() + slot = SLOT_STORE else if (I == s_store) s_store = null update_inv_s_store() + slot = SLOT_SUIT_STORE - + SEND_SIGNAL(src, COMSIG_HUMAN_UNEQUIPPED_ITEM, I, slot) /mob/living/carbon/human/wear_mask_update(obj/item/I, equipping) @@ -493,7 +512,7 @@ return ..() /mob/living/carbon/human/proc/get_strip_delay(mob/living/carbon/human/user, mob/living/carbon/human/target) - /// Default delay + /*/// Default delay var/target_delay = HUMAN_STRIP_DELAY /// Multiplier for how quickly the user can strip things. var/user_speed = user.get_skill_duration_multiplier(SKILL_CQC) @@ -506,7 +525,8 @@ target_delay += (target_skills * 2) /// Final result is overall delay * speed multiplier - return target_delay * user_speed + return target_delay * user_speed*/ + return 0 // zonenote /mob/living/carbon/human/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force) remember_dropped_object(I) diff --git a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm index e87de58155..b4bd9c49ad 100644 --- a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm +++ b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm @@ -43,7 +43,7 @@ if(distance > max_travel_distance) return - SSxeno_pathfinding.calculate_path(src, P.firer, distance, src, CALLBACK(src, PROC_REF(set_path)), list(src, P.firer)) + SSpathfinding.calculate_path(src, P.firer, distance, src, CALLBACK(src, PROC_REF(set_path)), list(src, P.firer)) /mob/living/carbon/xenomorph/proc/register_ai_action(datum/action/xeno_action/XA) if(XA.owner != src) @@ -165,12 +165,12 @@ return FALSE if((!current_path || (next_path_generation < world.time && current_target_turf != T)) && COOLDOWN_FINISHED(src, no_path_found_cooldown)) - if(!XENO_CALCULATING_PATH(src) || current_target_turf != T) - SSxeno_pathfinding.calculate_path(src, T, max_range, src, CALLBACK(src, PROC_REF(set_path)), list(src, current_target)) + if(!CALCULATING_PATH(src) || current_target_turf != T) + SSpathfinding.calculate_path(src, T, max_range, src, CALLBACK(src, PROC_REF(set_path)), list(src, current_target)) current_target_turf = T next_path_generation = world.time + path_update_period - if(XENO_CALCULATING_PATH(src)) + if(CALCULATING_PATH(src)) return TRUE // No possible path to target. @@ -193,7 +193,7 @@ var/turf/next_turf = current_path[current_path.len] var/list/L = LinkBlocked(src, loc, next_turf, list(src, current_target), TRUE) - L += SSxeno_pathfinding.check_special_blockers(src, next_turf) + L += SSpathfinding.check_special_blockers(src, next_turf) for(var/a in L) var/atom/A = a if(A.xeno_ai_obstacle(src, get_dir(loc, next_turf)) == INFINITY) diff --git a/code/modules/projectiles/ammunition.dm b/code/modules/projectiles/ammunition.dm index e032d3ebbe..213dfeeee3 100644 --- a/code/modules/projectiles/ammunition.dm +++ b/code/modules/projectiles/ammunition.dm @@ -12,6 +12,7 @@ They're all essentially identical when it comes to getting the job done. var/bonus_overlay = null //Sprite pointer in ammo.dmi to an overlay to add to the gun, for extended mags, box mags, and so on flags_atom = FPRINT|CONDUCT flags_equip_slot = SLOT_WAIST + flags_human_ai = AMMUNITION_ITEM matter = list("metal" = 1000) //Low. throwforce = 2 @@ -191,6 +192,12 @@ They're all essentially identical when it comes to getting the job done. default_ammo = source.default_ammo gun_type = source.gun_type +/obj/item/ammo_magazine/ai_can_use(mob/living/carbon/human/user) + if(current_rounds <= 0) + return FALSE + + return TRUE + //~Art interjecting here for explosion when using flamer procs. /obj/item/ammo_magazine/flamer_fire_act(damage, datum/cause_data/flame_cause_data) if(current_rounds < 1) @@ -246,6 +253,7 @@ bullets/shells. ~N max_rounds = 5 // For shotguns, though this will be determined by the handful type when generated. flags_atom = FPRINT|CONDUCT flags_magazine = AMMUNITION_HANDFUL + flags_human_ai = NONE attack_speed = 3 // should make reloading less painful /obj/item/ammo_magazine/handful/Initialize(mapload, spawn_empty) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 50e735b918..232c932f47 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -2020,3 +2020,8 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed /// Getter for gun_user /obj/item/weapon/gun/proc/get_gun_user() return gun_user + +/// Getter for target +/obj/item/weapon/gun/proc/get_target() + RETURN_TYPE(/atom) + return target diff --git a/code/modules/projectiles/gun_attachables.dm b/code/modules/projectiles/gun_attachables.dm index 2737bc5245..2fca594a83 100644 --- a/code/modules/projectiles/gun_attachables.dm +++ b/code/modules/projectiles/gun_attachables.dm @@ -290,6 +290,7 @@ Defined in conflicts.dm of the #defines folder. flags_equip_slot = SLOT_FACE flags_armor_protection = SLOT_FACE flags_item = CAN_DIG_SHRAPNEL + flags_human_ai = MELEE_WEAPON_ITEM attach_icon = "bayonet_a" melee_mod = 20 diff --git a/code/modules/projectiles/gun_helpers.dm b/code/modules/projectiles/gun_helpers.dm index 46f7f68b6f..e3d6655a0b 100644 --- a/code/modules/projectiles/gun_helpers.dm +++ b/code/modules/projectiles/gun_helpers.dm @@ -157,30 +157,29 @@ DEFINES in setup.dm, referenced here. if(CONFIG_GET(flag/remove_gun_restrictions)) return TRUE //Not if the config removed it. - if(user.mind) - switch(user.job) - if( - "PMC", - "WY Agent", - "Corporate Liaison", - "Event", - "UPP Armsmaster", //this rank is for the Fun - Ivan preset, it allows him to use the PMC guns randomly generated from his backpack - ) return TRUE - switch(user.faction) - if( - FACTION_WY_DEATHSQUAD, - FACTION_PMC, - FACTION_MERCENARY, - FACTION_FREELANCER, - ) return TRUE - - for(var/faction in user.faction_group) - if(faction in FACTION_LIST_WY) - return TRUE - - if(user.faction in FACTION_LIST_WY) + switch(user.job) + if( + "PMC", + "WY Agent", + "Corporate Liaison", + "Event", + "UPP Armsmaster", //this rank is for the Fun - Ivan preset, it allows him to use the PMC guns randomly generated from his backpack + ) return TRUE + switch(user.faction) + if( + FACTION_WY_DEATHSQUAD, + FACTION_PMC, + FACTION_MERCENARY, + FACTION_FREELANCER, + ) return TRUE + + for(var/faction in user.faction_group) + if(faction in FACTION_LIST_WY) return TRUE + if(user.faction in FACTION_LIST_WY) + return TRUE + to_chat(user, SPAN_WARNING("[src] flashes a warning sign indicating unauthorized use!")) // Checks whether there is anything to put your harness diff --git a/code/modules/projectiles/guns/flamer/flamer.dm b/code/modules/projectiles/guns/flamer/flamer.dm index 73ed0a37b5..98c98ad229 100644 --- a/code/modules/projectiles/guns/flamer/flamer.dm +++ b/code/modules/projectiles/guns/flamer/flamer.dm @@ -149,6 +149,8 @@ unleash_foam(target, user) else unleash_flame(target, user) + current_mag.current_rounds = current_mag.get_ammo_percent() + SEND_SIGNAL(user, COMSIG_MOB_FIRED_GUN, src) return AUTOFIRE_CONTINUE return NONE diff --git a/code/modules/projectiles/magazines/lever_action.dm b/code/modules/projectiles/magazines/lever_action.dm index ac1d57dbd4..a32a49d926 100644 --- a/code/modules/projectiles/magazines/lever_action.dm +++ b/code/modules/projectiles/magazines/lever_action.dm @@ -92,6 +92,7 @@ Handfuls of lever_action rounds. For spawning directly on mobs in roundstart, ER gun_type = /obj/item/weapon/gun/lever_action handful_state = "lever_action_bullet" transfer_handful_amount = 9 + flags_human_ai = AMMUNITION_ITEM /obj/item/ammo_magazine/handful/lever_action/training name = "handful of blanks (45-70)" diff --git a/code/modules/projectiles/magazines/shotguns.dm b/code/modules/projectiles/magazines/shotguns.dm index 9fe9f0f440..f47a472bd3 100644 --- a/code/modules/projectiles/magazines/shotguns.dm +++ b/code/modules/projectiles/magazines/shotguns.dm @@ -189,6 +189,7 @@ GLOBAL_LIST_INIT(shotgun_handfuls_12g, list( gun_type = /obj/item/weapon/gun/shotgun handful_state = "slug_shell" transfer_handful_amount = 5 + flags_human_ai = AMMUNITION_ITEM /obj/item/ammo_magazine/handful/shotgun/slug @@ -321,6 +322,7 @@ GLOBAL_LIST_INIT(shotgun_handfuls_12g, list( max_rounds = 8 current_rounds = 8 gun_type = /obj/item/weapon/gun/shotgun/double/cane + flags_human_ai = AMMUNITION_ITEM /obj/item/ammo_magazine/handful/revolver/marksman name = "handful of marksman revolver bullets (.44)" diff --git a/colonialmarines.dme b/colonialmarines.dme index 2acf1dec00..2142ee9258 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -67,6 +67,7 @@ #include "code\__DEFINES\html.dm" #include "code\__DEFINES\hud.dm" #include "code\__DEFINES\human.dm" +#include "code\__DEFINES\human_ai.dm" #include "code\__DEFINES\job.dm" #include "code\__DEFINES\keybinding.dm" #include "code\__DEFINES\language.dm" @@ -271,6 +272,7 @@ #include "code\controllers\subsystem\garbage.dm" #include "code\controllers\subsystem\hijack.dm" #include "code\controllers\subsystem\human.dm" +#include "code\controllers\subsystem\human_ai.dm" #include "code\controllers\subsystem\inactivity.dm" #include "code\controllers\subsystem\influxdriver.dm" #include "code\controllers\subsystem\influxmcstats.dm" @@ -1976,6 +1978,36 @@ #include "code\modules\mob\living\carbon\human\unarmed_attacks.dm" #include "code\modules\mob\living\carbon\human\update_icons.dm" #include "code\modules\mob\living\carbon\human\whisper.dm" +#include "code\modules\mob\living\carbon\human\ai\ai_equipment.dm" +#include "code\modules\mob\living\carbon\human\ai\ai_management_menu.dm" +#include "code\modules\mob\living\carbon\human\ai\faction_management_panel.dm" +#include "code\modules\mob\living\carbon\human\ai\fortify_room.dm" +#include "code\modules\mob\living\carbon\human\ai\human_ai_interaction.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\melee_atom.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\pickup_primary.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\throw_grenade.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\approach_target_carefully.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\retreat_from_target.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\orders\order_action.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\orders\patrol_waypoints.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_communication.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_factions.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_squad.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\take_inside_cover.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_cover.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\take_cover.dm" +#include "code\modules\mob\living\carbon\human\ai\firearm_appraisal.dm" +#include "code\modules\mob\living\carbon\human\ai\human_ai.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\action_datums.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\approach_target.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\item_pickup.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\throw_back_nade.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_guns.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_health.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_items.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_pathfinding.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_targeting.dm" #include "code\modules\mob\living\carbon\human\life\handle_breath.dm" #include "code\modules\mob\living\carbon\human\life\handle_chemicals_in_body.dm" #include "code\modules\mob\living\carbon\human\life\handle_disabilities.dm" diff --git a/tgui/packages/tgui/interfaces/HumanAIManager.tsx b/tgui/packages/tgui/interfaces/HumanAIManager.tsx new file mode 100644 index 0000000000..f1dc85214f --- /dev/null +++ b/tgui/packages/tgui/interfaces/HumanAIManager.tsx @@ -0,0 +1,383 @@ +import { useBackend, useLocalState } from '../backend'; +import { Button, Section, Stack, Divider } from '../components'; +import { Window } from '../layouts'; +import { BooleanLike } from 'common/react'; + +type Squad = { + id: number; + order: string; + members: string; + ref: string; + primary_order: string; + squad_leader: string; +}; + +type AIHuman = { + name: string; + health: number; + loc: number[]; + faction: string; + ref: string; + brain_ref: string; + in_combat: BooleanLike; + squad_id: number; +}; + +type Order = { + name: string; + type: string; + data: any[]; + ref: string; +}; + +type BackendContext = { + orders: Order[]; + ai_humans: AIHuman[]; + squads: Squad[]; +}; + +const AIContext = (props, context) => { + const { data, act } = useBackend(); + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( + 'squad_assignment_mode', + false, + ); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + 'order_assignment_mode', + false, + ); + return ( + +
+
+
+
+
+
+
+
+ +
+ {data.orders.map((order) => ( + + ))} +
+ +
+ {data.ai_humans.map((human) => ( + + ))} +
+ +
+ {data.squads.map((squad) => ( + + ))} +
+
+ ); +}; + +const CreatedOrder = (props) => { + const order: Order = props.order; + const context: BackendContext = props.context; + const { data, act } = useBackend(); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + 'order_assignment_mode', + false, + ); + const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1); + return ( +
+
+ {orderAssignmentMode ? ( + <> +
+
+ ); +}; + +const HumanAIReadout = (props) => { + const human: AIHuman = props.human; + const context: BackendContext = props.context; + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( + 'squad_assignment_mode', + false, + ); + const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1); + const { data, act } = useBackend(); + const gottenSquad: Squad = data.squads[selectedSquad]; + return ( +
+
+
+ {squadAssignmentMode ? ( + <> +
+
+
+ Health: {human.health}%
+ Faction: {human.faction}
+ In Combat: {human.in_combat}
+ Squad #: {human.squad_id}
+ Loc: {human.loc[0]}, {human.loc[1]}, {human.loc[2]} +
+
+ ); +}; + +const SquadReadout = (props) => { + const squad: Squad = props.squad; + const context: BackendContext = props.context; + const { data, act } = useBackend(); + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( + 'squad_assignment_mode', + false, + ); + const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + 'order_assignment_mode', + false, + ); + return ( +
+
+ {squadAssignmentMode || orderAssignmentMode ? ( +
+
+ ); +}; + +export const HumanAIManager = (props) => { + return ( + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/HumanFactionManager.tsx b/tgui/packages/tgui/interfaces/HumanFactionManager.tsx new file mode 100644 index 0000000000..07fbf02c8e --- /dev/null +++ b/tgui/packages/tgui/interfaces/HumanFactionManager.tsx @@ -0,0 +1,161 @@ +import { useBackend, useLocalState } from '../backend'; +import { Button, Dropdown, Section, Stack, Divider } from '../components'; +import { Window } from '../layouts'; +import { BooleanLike } from 'common/react'; + +type Faction = { + name: string; + shoot_to_kill: BooleanLike; + friendly_factions: string; + neutral_factions: string; + ref: string; +}; + +type BackendContext = { + factions: Faction[]; + all_factions: string[]; + datumless_factions: string[]; +}; + +const FactionContext = (props, context) => { + const { data, act } = useBackend(); + const [selectedToCreateFaction, setSelectedToCreateFaction] = useLocalState( + 'selected_to_create_faction', + '', + ); + return ( + +
+
+ setSelectedToCreateFaction(value)} + /> +
+
+
+
+ +
+ {data.factions.map((faction) => ( + + ))} +
+
+ ); +}; + +const ExistingFaction = (props) => { + const context: BackendContext = props.context; + const { data, act } = useBackend(); + const faction: Faction = props.faction; + return ( +
+
+ Shoot To Kill: {faction.shoot_to_kill} + + act('set_shoot_to_kill', { + faction_name: faction.name, + new_value: value, + }) + } + /> + Friendly Factions: {faction.friendly_factions} + <> +
+
+ ); +}; + +export const HumanFactionManager = (props) => { + return ( + + + + + + ); +};