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 e2e2bb3a1d4..60565bf451d 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) @@ -74,3 +77,12 @@ /// From /mob/living/carbon/human/UnarmedAttack() #define COMSIG_HUMAN_BEFORE_ATTACK_HAND "human_before_attack_hand" #define COMPONENT_CANCEL_HUMAN_ATTACK_HAND (1<<0) + +/// From /obj/item/restraint/proc/place_handcuffs() : () +#define COMSIG_HUMAN_HANDCUFFED "human_handcuffed" + +/// From /mob/living/carbon/human/proc/set_species() : (new_species) +#define COMSIG_HUMAN_SET_SPECIES "human_set_species" + +/// From /mob/living/carbon/human/proc/get_human_ai_brain() : (datum/human_ai_brain/out_brain) +#define COMSIG_HUMAN_GET_AI_BRAIN "human_get_ai_brain" diff --git a/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm b/code/__DEFINES/dcs/signals/atom/mob/signals_mob.dm index fa50103e081..8df1ede132a 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 5d5b81bf8c5..79c72950d65 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 00000000000..667a8e72cef --- /dev/null +++ b/code/__DEFINES/human_ai.dm @@ -0,0 +1,18 @@ +#define HUMAN_AI_HEALTHITEMS "health" +#define HUMAN_AI_AMMUNITION "ammo" +#define HUMAN_AI_GRENADES "grenades" +#define HUMAN_AI_TOOLS "tools" + +#define ACTION_USING_HANDS (1<<0) +#define ACTION_USING_LEGS (1<<1) + +/// 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 HUMAN_AI_MAX_PATHFINDING_RANGE 45 + +GLOBAL_LIST_EMPTY(ai_humans) diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 989c39cd7d6..49c7a71a770 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 d7cbe1bf22c..72383fc969a 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 b9a9f276c69..41a22001b6b 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,55 @@ 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) + +//straight directions get priority over diagonal directions in edge cases +/proc/angle2dir4ai(angle) + switch(angle) // 80/10 degrees diagonals/cardinals respectively + if (40 to 50) + return NORTHEAST + if (130 to 140) + return SOUTHEAST + if (220 to 230) + return SOUTHWEST + if (310 to 320) + return NORTHWEST + if (0 to 40) + return NORTH + if (50 to 130) + return EAST + if (140 to 220) + return SOUTH + if (230 to 310) + return WEST + else + return NORTH + /// 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 20aa2081318..7545d04b304 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 00000000000..aacf67c3c0a --- /dev/null +++ b/code/controllers/subsystem/human_ai.dm @@ -0,0 +1,73 @@ + +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/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/ai_order/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] diff --git a/code/controllers/subsystem/pathfinding.dm b/code/controllers/subsystem/pathfinding.dm index bf7d6ed1d9c..7dbbc7d11a5 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,24 @@ 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 + var/datum/component/human_ai/ai_component = current_run.agent.GetComponent(/datum/component/human_ai) // zonenote unfuck me later + var/datum/human_ai_brain/brain = ai_component.ai_brain + for(var/atom/A as anything in L) + distance_between += A.human_ai_obstacle(current_run.agent, brain, direction, target) if(distance_between < distances[neighbor]) distances[neighbor] = distance_between @@ -107,7 +110,6 @@ SUBSYSTEM_DEF(xeno_pathfinding) var/atom/A = l A.color = "#[red][green]00" T.color = "#[red][green]00" - T.maptext = distance #endif if(!prev[target]) @@ -126,10 +128,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 +139,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 +164,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 +177,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 +200,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 +210,7 @@ SUBSYSTEM_DEF(xeno_pathfinding) start = null finish = null - travelling_xeno = null + agent = null to_return = null visited_nodes = null distances = null @@ -224,5 +226,4 @@ SUBSYSTEM_DEF(xeno_pathfinding) var/atom/A = l A.color = null T.color = null - T.maptext = null #endif diff --git a/code/datums/components/human_ai.dm b/code/datums/components/human_ai.dm new file mode 100644 index 00000000000..15cd06f11a2 --- /dev/null +++ b/code/datums/components/human_ai.dm @@ -0,0 +1,44 @@ +/datum/component/human_ai + dupe_mode = COMPONENT_DUPE_UNIQUE + /// Ref to the AI brain + var/datum/human_ai_brain/ai_brain + /// Ref to the owning human + var/mob/living/carbon/human/ai_human + +/datum/component/human_ai/Initialize() + . = ..() + ai_human = parent + if(!istype(ai_human)) + return COMPONENT_INCOMPATIBLE + + ai_brain = new(ai_human) + GLOB.ai_humans += ai_human + ai_human.mob_flags |= AI_CONTROLLED + +/datum/component/human_ai/Destroy(force, silent) + handle_qdel() + return ..() + +/datum/component/human_ai/RegisterWithParent() + ..() + RegisterSignal(ai_human, COMSIG_PARENT_QDELETING, PROC_REF(handle_qdel)) + RegisterSignal(ai_human, COMSIG_HUMAN_SET_SPECIES, PROC_REF(on_species_set)) + +/datum/component/human_ai/UnregisterFromParent() + ..() + if(ai_human) + UnregisterSignal(ai_human, COMSIG_PARENT_QDELETING) + UnregisterSignal(ai_human, COMSIG_HUMAN_SET_SPECIES) + +/datum/component/human_ai/proc/handle_qdel() + SIGNAL_HANDLER + + GLOB.ai_humans -= ai_human + ai_brain?.tied_human = null + QDEL_NULL(ai_brain) + ai_human = null + +/datum/component/human_ai/proc/on_species_set(datum/source, new_species) + SIGNAL_HANDLER + + ai_human.mob_flags |= AI_CONTROLLED diff --git a/code/datums/skills/clf.dm b/code/datums/skills/clf.dm index 64a8864d3c5..7270c1d8004 100644 --- a/code/datums/skills/clf.dm +++ b/code/datums/skills/clf.dm @@ -56,6 +56,16 @@ COLONIAL LIBERATION FRONT SKILL_JTAC = SKILL_JTAC_TRAINED ) +/datum/skills/clf/sniper + name = "CLF Specialist" + skills = list( + SKILL_MEDICAL = SKILL_MEDICAL_TRAINED, + SKILL_CQC = SKILL_CQC_TRAINED, + SKILL_ENDURANCE = SKILL_ENDURANCE_TRAINED, + SKILL_MELEE_WEAPONS = SKILL_MELEE_TRAINED, + SKILL_FIREARMS = SKILL_FIREARMS_EXPERT, // (: + ) + /datum/skills/clf/leader name = "CLF Leader" skills = list( diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 7fd82aeb9c1..672b24a464a 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, datum/human_ai_brain/ai_brain) + return + +/obj/item/proc/ai_can_use(mob/living/carbon/human/user, datum/human_ai_brain/ai_brain) + return FALSE diff --git a/code/game/objects/items/devices/radio/encryptionkey.dm b/code/game/objects/items/devices/radio/encryptionkey.dm index 160f4beafff..250a2c2911c 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 b2f95646a96..50016ae16d0 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,23 @@ walk(src, null, null) ..() return + +/obj/item/explosive/grenade/ai_can_use(mob/living/carbon/human/user, datum/human_ai_brain/ai_brain) + return TRUE + +/obj/item/explosive/grenade/ai_use(mob/living/carbon/human/user, datum/human_ai_brain/ai_brain, turf/target_turf) + sleep(ai_brain.short_action_delay * ai_brain.action_delay_mult) + attack_self(user) + user.toggle_throw_mode(THROW_MODE_NORMAL) + ai_brain.ensure_primary_hand(src) + sleep(det_time * 0.4) + if(QDELETED(src) || (loc != user)) + return + + ai_brain.say_grenade_thrown_line() + sleep(det_time * 0.4) + if(QDELETED(src) || (loc != user)) + return + + user.face_atom(target_turf) + user.throw_item(target_turf) diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm index af71b806ed4..266b658d4e0 100644 --- a/code/game/objects/items/handcuffs.dm +++ b/code/game/objects/items/handcuffs.dm @@ -53,6 +53,7 @@ user.drop_inv_item_on_ground(src) human_mob.equip_to_slot_if_possible(src, WEAR_HANDCUFFS, 1, 0, 1, 1) user.count_niche_stat(STATISTICS_NICHE_HANDCUFF) + SEND_SIGNAL(target, COMSIG_HUMAN_HANDCUFFED) else if(ismonkey(target)) user.visible_message(SPAN_NOTICE("[user] tries to put [src] on [target].")) diff --git a/code/game/objects/items/reagent_containers/autoinjectors.dm b/code/game/objects/items/reagent_containers/autoinjectors.dm index 291f02d5f47..ce5f810a091 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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + if(!uses_left) + return FALSE + + var/datum/reagent/reagent_datum = GLOB.chemical_reagents_list[chemname] + + if((target.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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + attack(target, 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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + if(target.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 94477520dae..62afcb78bf2 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 c4a496a1236..acdc53819b0 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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + for(var/obj/limb/limb as anything in target.limbs) + if(QDELETED(src)) + return + + if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list) + user.zone_selected = limb.name + attack(target, user) + sleep(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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + for(var/obj/limb/limb as anything in target.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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + for(var/obj/limb/limb as anything in target.limbs) + if(QDELETED(src)) + return + + if(locate(/datum/effects/bleeding/external) in limb.bleeding_effects_list) + user.zone_selected = limb.name + attack(target, user) + sleep(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(target, user) + sleep(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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + for(var/obj/limb/limb as anything in target.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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + for(var/obj/limb/limb as anything in target.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(target, user) + sleep(ai_brain.short_action_delay) + /obj/item/stack/medical/splint name = "medical splints" singular_name = "medical splint" @@ -271,7 +344,7 @@ var/indestructible_splints = FALSE -/obj/item/stack/medical/splint/attack(mob/living/carbon/M, mob/user) +/obj/item/stack/medical/splint/attack(mob/living/carbon/M, mob/user, mob/living/carbon/target) if(..()) return 1 if(user.action_busy) @@ -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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + for(var/obj/limb/limb as anything in target.limbs) + if(QDELETED(src)) + return + + if(limb.is_broken()) + user.zone_selected = limb.name + attack(target, user) + sleep(ai_brain.short_action_delay) + continue diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 51ac1635072..d18c12b3b2b 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -681,6 +681,10 @@ for(var/i = 1 to storage_slots) new /obj/item/ammo_magazine/handful/shotgun/buckshot(src) +/obj/item/storage/belt/marine/svd/fill_preset_inventory() // SVD + for(var/i in 1 to storage_slots) + new /obj/item/ammo_magazine/sniper/svd(src) + /obj/item/storage/belt/marine/smartgunner name = "\improper M280 pattern smartgunner drum belt" desc = "Despite the fact that 1. drum magazines are incredibly non-ergonomical, and 2. require incredibly precise machining in order to fit universally (spoiler, they don't, adding further to the myth of 'Smartgun Personalities'), the USCM decided to issue a modified marine belt (more formally known by the designation M280) with hooks and dust covers (overly complex for the average jarhead) for the M56B system's drum munitions. When the carry catch on the drum isn't getting stuck in the oiled up velcro, the rig actually does do a decent job at holding a plentiful amount of drums. But at the end of the day, compared to standard rigs... it sucks, but isn't that what being a Marine is all about?" diff --git a/code/game/objects/items/storage/firstaid.dm b/code/game/objects/items/storage/firstaid.dm index 6535497ad57..6b31e65c629 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,26 @@ /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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + ai_brain.appraise_inventory() + + if(!length(contents) || !COOLDOWN_FINISHED(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, datum/human_ai_brain/ai_brain, mob/living/carbon/human/target) + var/obj/item/pill = contents[1] + user.swap_hand() + if(user.put_in_active_hand(pill)) + remove_from_storage(pill, user) + pill.attack(target, user) + COOLDOWN_START(ai_brain, pill_use_cooldown, 20 SECONDS) + sleep(ai_brain.medium_action_delay * ai_brain.action_delay_mult) + /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 9fab254a715..c5343f808d4 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 a29bf97cacd..1c710d96499 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 7264a8a2e57..8e88648e6dc 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, datum/human_ai_brain/ai_brain) + 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 b475de36a47..d425d5b20db 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 8fe00d04a70..fc90b76400f 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 575f1da738b..1556e8cca4b 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 0e2b77b4c1e..c08cd6d77af 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 5b5252dfdb0..53100dc26af 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -73,9 +73,20 @@ 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, + /client/proc/make_human_ai, + /datum/admins/proc/create_human_ai_sniper, + /client/proc/quick_order_ai_approach, )) + 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 edecb81d746..46d13a09b9f 100644 --- a/code/modules/admin/verbs/select_equipment.dm +++ b/code/modules/admin/verbs/select_equipment.dm @@ -104,7 +104,7 @@ cmd_admin_dress_human(M) -/client/proc/cmd_admin_dress_human(mob/living/carbon/human/M in GLOB.human_mob_list, datum/equipment_preset/dresscode, no_logs = 0, count_participant = FALSE) +/client/proc/cmd_admin_dress_human(mob/living/carbon/human/M in GLOB.human_mob_list, datum/equipment_preset/dresscode, no_logs = 0, count_participant = FALSE, randomize = FALSE) if (!no_logs) dresscode = tgui_input_list(usr, "Select dress for [M]", "Robust quick dress shop", GLOB.gear_name_presets_list) @@ -128,10 +128,10 @@ if(!M.hud_used) M.create_hud() - arm_equipment(M, dresscode, FALSE, count_participant) + arm_equipment(M, dresscode, randomize, 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 f09023408fc..d26378f3325 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 5bc91dc6993..51592dc2574 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 cd61c25d94e..293e25cdb52 100644 --- a/code/modules/gear_presets/_select_equipment.dm +++ b/code/modules/gear_presets/_select_equipment.dm @@ -624,7 +624,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 @@ -634,7 +634,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 @@ -644,7 +644,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 286645fc700..36233d930ad 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 00000000000..21af1d77c3f --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/action_datums.dm @@ -0,0 +1,50 @@ +GLOBAL_LIST_INIT_TYPED(AI_actions, /datum/ai_action, setup_ai_actions()) + +/proc/setup_ai_actions() + var/list/action_list = list() + for(var/action in subtypesof(/datum/ai_action)) + var/datum/ai_action/A = new action + action_list[A.type] = A + return action_list + + +/datum/ai_action + var/name + var/datum/human_ai_brain/brain + var/action_flags = null + +/datum/ai_action/proc/get_weight(datum/human_ai_brain/brain) + return 0 + +/datum/ai_action/proc/get_conflicts(datum/human_ai_brain/brain) + RETURN_TYPE(/list) + . = list() + + if(!action_flags) + return + + for(var/action_type as anything in GLOB.AI_actions) + if(GLOB.AI_actions[action_type].action_flags & action_flags) + . += action_type + +/datum/ai_action/New(datum/human_ai_brain/brain) + . = ..() + + if(!brain) + return + + src.brain = brain + Added() + +/datum/ai_action/proc/Added() + return + +/datum/ai_action/Destroy(force, ...) + brain.ongoing_actions -= src + brain = null + return ..() + +/datum/ai_action/proc/trigger_action() + SHOULD_NOT_SLEEP(TRUE) + if(!brain) + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/chase_target.dm b/code/modules/mob/living/carbon/human/ai/action_datums/chase_target.dm new file mode 100644 index 00000000000..f8b8cca6441 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/chase_target.dm @@ -0,0 +1,42 @@ +/datum/ai_action/chase_target + name = "Chase Target" + action_flags = ACTION_USING_LEGS + +/datum/ai_action/chase_target/get_weight(datum/human_ai_brain/brain) + if(brain.in_cover) + return 0 + + if(!brain.target_turf) + return 0 + + if(brain.current_target) + return 0 + + return 6 + +/datum/ai_action/chase_target/get_conflicts(datum/human_ai_brain/brain) + . = ..() + . += /datum/ai_action/throw_grenade + +/datum/ai_action/chase_target/trigger_action() + . = ..() + + var/turf/target_turf = brain.target_turf + if(QDELETED(target_turf) || brain.current_target) + return ONGOING_ACTION_COMPLETED + + var/mob/tied_human = brain.tied_human + if(get_dist(target_turf, tied_human) > 0) + if(!brain.move_to_next_turf(target_turf)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(target_turf, tied_human) > 0) + return ONGOING_ACTION_COMPLETED + + // Turn around as we're seeking for the lost target + var/direction = turn(tied_human.dir, pick(90,-90)) + tied_human.face_dir(direction) + + // Scouted, found nothing, discard + brain.target_turf = null + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/depricated/melee_atom.dm b/code/modules/mob/living/carbon/human/ai/action_datums/depricated/melee_atom.dm new file mode 100644 index 00000000000..f4682d30bdb --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/depricated/melee_atom.dm @@ -0,0 +1,34 @@ +/datum/ai_action/melee_atom + name = "Melee Atom" + var/atom/target + +/datum/ai_action/melee_atom/New(datum/human_ai_brain/brain) + . = ..() + //target = arguments[2] + +/datum/ai_action/melee_atom/Destroy(force, ...) + target = null + return ..() + +/datum/ai_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.wield_primary() + //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/fire_at_target.dm b/code/modules/mob/living/carbon/human/ai/action_datums/fire_at_target.dm new file mode 100644 index 00000000000..d3d47e810f2 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/fire_at_target.dm @@ -0,0 +1,187 @@ +/datum/ai_action/fire_at_target + name = "Fire At Target" + action_flags = ACTION_USING_HANDS + var/rounds_burst_fired = 0 + var/currently_firing + +/datum/ai_action/fire_at_target/get_weight(datum/human_ai_brain/brain) + if(!brain.in_combat) + return 0 + + if(brain.tried_reload) + return 0 + + if(!brain.primary_weapon) + return 0 + + var/turf/target_turf = brain.target_turf + var/should_fire_offscreen = (target_turf && !COOLDOWN_FINISHED(brain, fire_offscreen)) + + if(!brain.current_target && !should_fire_offscreen) + return 0 + + if((get_dist(brain.tied_human, target_turf) > brain.view_distance) && !should_fire_offscreen) + return 0 + + if(brain.should_reload()) + return 0 + + return 10 + +/datum/ai_action/fire_at_target/Destroy(force, ...) + stop_firing(brain) + return ..() + +/datum/ai_action/fire_at_target/proc/stop_firing(datum/human_ai_brain/brain) + currently_firing = FALSE + rounds_burst_fired = 0 + + UnregisterSignal(brain.tied_human, COMSIG_MOB_FIRED_GUN) + brain.primary_weapon?.set_target(null) + +/datum/ai_action/fire_at_target/trigger_action() + . = ..() + + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + if(!primary_weapon || brain.active_grenade_found) + return ONGOING_ACTION_COMPLETED + + var/should_fire_offscreen = (brain.target_turf && !COOLDOWN_FINISHED(brain, fire_offscreen)) + if(!brain.current_target && !should_fire_offscreen) + return ONGOING_ACTION_COMPLETED + + if(currently_firing || !COOLDOWN_FINISHED(brain, fire_overload_cooldown)) + return ONGOING_ACTION_UNFINISHED + + var/mob/living/carbon/tied_human = brain.tied_human + if(!(primary_weapon in tied_human.get_hands())) + brain.unholster_primary() + + var/datum/firearm_appraisal/gun_data = brain.gun_data + gun_data.before_fire(primary_weapon, tied_human, brain) + if(brain.should_reload()) + if(gun_data?.disposable) + tied_human.drop_held_item(primary_weapon) + brain.set_primary_weapon(null) + return ONGOING_ACTION_COMPLETED + + var/turf/target_turf = brain.target_turf + if((get_dist(tied_human, target_turf) > gun_data.maximum_range) && !should_fire_offscreen) + return ONGOING_ACTION_COMPLETED + + if(!firing_line_check(brain, target_turf)) + return ONGOING_ACTION_UNFINISHED + + tied_human.face_atom(target_turf) + tied_human.a_intent = INTENT_HARM + + RegisterSignal(tied_human, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fire), TRUE) + + primary_weapon?.set_target(target_turf) + primary_weapon?.start_fire(object = target_turf, bypass_checks = TRUE) + return ONGOING_ACTION_UNFINISHED + +/datum/ai_action/fire_at_target/proc/firing_line_check(datum/human_ai_brain/brain, atom/target) + var/mob/living/carbon/tied_human = brain.tied_human + var/list/turf_list = get_line(get_turf(tied_human), get_turf(target)) + for(var/turf/tile in turf_list) + if(get_dist(tied_human, tile) > brain.view_distance) + continue + + if(tile.density) + return FALSE + + for(var/mob/living/carbon/human/possible_friendly in tile) + if(possible_friendly == tied_human) + continue + + if(possible_friendly.body_position == LYING_DOWN) + continue + + if(brain.faction_check(possible_friendly)) + return FALSE + + return TRUE + +/datum/ai_action/fire_at_target/proc/on_gun_fire(datum/source, obj/item/weapon/gun/fired) + SIGNAL_HANDLER + + if(!brain) + qdel(src) + return + + var/mob/living/current_target = brain.current_target + var/turf/target_turf = brain.target_turf + + var/mob/living/carbon/tied_human = brain.tied_human + tied_human.a_intent = INTENT_HARM + + brain.shot_at = get_turf(target_turf) + tied_human.face_atom(target_turf) + + currently_firing = TRUE + + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + if(istype(primary_weapon, /obj/item/weapon/gun/shotgun/pump)) + currently_firing = FALSE + 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 +/* Basira doesn't need cocking??? + else if(istype(primary_weapon, /obj/item/weapon/gun/boltaction)) + var/obj/item/weapon/gun/boltaction/bolt = primary_weapon + currently_firing = FALSE + 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) +*/ + else if(primary_weapon.gun_firemode == GUN_FIREMODE_SEMIAUTO) + currently_firing = FALSE + 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()) + + else if(primary_weapon.gun_firemode == GUN_FIREMODE_AUTOMATIC) + rounds_burst_fired++ + + var/datum/firearm_appraisal/gun_data = brain.gun_data + if(brain.should_reload()) // note that bullet removal comes after comsig is triggered + if(gun_data?.disposable) + tied_human.drop_held_item(primary_weapon) + brain.set_primary_weapon(null) + qdel(src) + return + + var/should_fire_offscreen = (target_turf && !COOLDOWN_FINISHED(brain, fire_offscreen)) + var/shoot_next = current_target + + if(QDELETED(current_target)) + if(!should_fire_offscreen) + qdel(src) + return + shoot_next = target_turf + + else if(ismob(current_target)) + if(current_target.stat == DEAD) + qdel(src) + return + + var/is_unconscious = (current_target.stat == UNCONSCIOUS || (locate(/datum/effects/crit) in current_target.effects_list)) + if(!brain.shoot_to_kill && is_unconscious) + brain.lose_target() + qdel(src) + return + + if(rounds_burst_fired >= gun_data.burst_amount_max) + var/short_action_delay = brain.short_action_delay + COOLDOWN_START(brain, fire_overload_cooldown, max(short_action_delay, short_action_delay * brain.action_delay_mult)) + stop_firing(brain) + return + + if((get_dist(tied_human, shoot_next) > gun_data.maximum_range) && !should_fire_offscreen) + qdel(src) + return + + if(!firing_line_check(brain, shoot_next)) + stop_firing(brain) + return + + primary_weapon?.set_target(shoot_next) diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/fire_at_target_pb.dm b/code/modules/mob/living/carbon/human/ai/action_datums/fire_at_target_pb.dm new file mode 100644 index 00000000000..83ae1164528 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/fire_at_target_pb.dm @@ -0,0 +1,101 @@ +/datum/ai_action/fire_at_target/point_blank + name = "Fire At Target (Point Blank)" + +/datum/ai_action/fire_at_target/point_blank/get_weight(datum/human_ai_brain/brain) + if(!brain.in_combat) + return 0 + + if(brain.tried_reload) + return 0 + + if(!brain.primary_weapon) + return 0 + + if(!brain.target_turf) + return 0 + + if(get_dist(brain.tied_human, brain.target_turf) > 1) + return 0 + + if(brain.should_reload()) + return 0 + + return 11 // higher weight than regular firing + +/datum/ai_action/fire_at_target/point_blank/trigger_action() + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + if(!primary_weapon || brain.active_grenade_found) + return ONGOING_ACTION_COMPLETED + + if(currently_firing) + stop_firing() + + var/mob/living/carbon/tied_human = brain.tied_human + if(!(primary_weapon in tied_human.get_hands())) + brain.unholster_primary() + + var/datum/firearm_appraisal/gun_data = brain.gun_data + gun_data.before_fire(primary_weapon, tied_human, brain) + if(brain.should_reload()) + if(gun_data?.disposable) + tied_human.drop_held_item(primary_weapon) + brain.set_primary_weapon(null) + return ONGOING_ACTION_COMPLETED + + tied_human.face_atom(brain.target_turf) + tied_human.a_intent = INTENT_HARM + + RegisterSignal(tied_human, COMSIG_MOB_FIRED_GUN, PROC_REF(on_gun_fire), TRUE) + + var/mob/living/target + for(var/mob/living/being in brain.target_turf) + if(!brain.faction_check(being)) + target = being + break + + if(!target) + return + + INVOKE_ASYNC(primary_weapon, TYPE_PROC_REF(/obj/item/weapon/gun, attack), target, brain.tied_human) + return ONGOING_ACTION_UNFINISHED + + +/datum/ai_action/fire_at_target/point_blank/on_gun_fire(datum/source, obj/item/weapon/gun/fired) + if(!brain) + qdel(src) + return + + var/mob/living/carbon/tied_human = brain.tied_human + tied_human.a_intent = INTENT_HARM + + brain.shot_at = get_turf(brain.target_turf) + tied_human.face_atom(brain.target_turf) + + currently_firing = TRUE + + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + if(istype(primary_weapon, /obj/item/weapon/gun/shotgun/pump)) + currently_firing = FALSE + 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) +/* Basira doesn't need cocking??? + else if(istype(primary_weapon, /obj/item/weapon/gun/boltaction)) + var/obj/item/weapon/gun/boltaction/bolt = primary_weapon + currently_firing = FALSE + 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) +*/ + else if(primary_weapon.gun_firemode == GUN_FIREMODE_AUTOMATIC) + rounds_burst_fired++ + + var/datum/firearm_appraisal/gun_data = brain.gun_data + if(brain.should_reload()) // note that bullet removal comes after comsig is triggered + if(gun_data?.disposable) + tied_human.drop_held_item(primary_weapon) + brain.set_primary_weapon(null) + qdel(src) + return + + qdel(src) + return + diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/follow_leader.dm b/code/modules/mob/living/carbon/human/ai/action_datums/follow_leader.dm new file mode 100644 index 00000000000..c9c64afe619 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/follow_leader.dm @@ -0,0 +1,53 @@ +/datum/ai_action/follow_leader + name = "Follow Leader" + action_flags = ACTION_USING_LEGS + var/follow_distance = 1 + +/datum/ai_action/follow_leader/get_weight(datum/human_ai_brain/brain) + if(brain.in_cover) + return 0 + + if(brain.is_squad_leader) + return 0 + + if(length(brain.to_pickup)) + return 0 + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[brain.squad_id]"] + if(!squad) + return 0 + + var/mob/squad_leader = squad.squad_leader?.tied_human + if(!squad_leader) + return 0 + + if(get_dist(brain.tied_human, squad_leader) <= (1 + length(squad.ai_in_squad) / 2)) + return 0 + + return 5 + +/datum/ai_action/follow_leader/Added() + if(!brain.squad_id) + return + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[brain.squad_id]"] + follow_distance = 1 + length(squad.ai_in_squad) / 2 + +/datum/ai_action/follow_leader/trigger_action() + . = ..() + + if(brain.in_combat || length(brain.to_pickup)) + return ONGOING_ACTION_COMPLETED + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[brain.squad_id]"] + var/mob/squad_leader = squad.squad_leader?.tied_human + + var/mob/tied_human = brain.tied_human + if(get_dist(tied_human, squad_leader) > follow_distance) + if(!brain.move_to_next_turf(get_turf(squad_leader))) + return ONGOING_ACTION_COMPLETED + + if(get_dist(tied_human, squad_leader) > follow_distance) + return ONGOING_ACTION_UNFINISHED + + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm new file mode 100644 index 00000000000..2d87980fbf5 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/item_pickup.dm @@ -0,0 +1,107 @@ +/datum/ai_action/item_pickup + name = "Item Pickup" + action_flags = ACTION_USING_HANDS | ACTION_USING_LEGS + var/obj/item/to_pickup + +/datum/ai_action/item_pickup/get_weight(datum/human_ai_brain/brain) + if(!length(brain.to_pickup)) + return 0 + + return 11 + +/datum/ai_action/item_pickup/Added() + // If we already have a primary weapon, don't set to_pickup and action will be killed immideately + if(isgun(to_pickup) && brain.primary_weapon) + return + + to_pickup = brain.to_pickup[1] + +/datum/ai_action/item_pickup/Destroy(force, ...) + to_pickup = null + return ..() + +/datum/ai_action/item_pickup/trigger_action() + . = ..() + + if(QDELETED(to_pickup) || !isturf(to_pickup.loc)) + brain.UnregisterSignal(to_pickup, COMSIG_PARENT_QDELETING) + brain.to_pickup -= to_pickup + return ONGOING_ACTION_COMPLETED + + var/mob/living/carbon/human/tied_human = brain.tied_human + if(get_dist(to_pickup, tied_human) > 1) + if(!brain.move_to_next_turf(get_turf(to_pickup))) + brain.UnregisterSignal(to_pickup, COMSIG_PARENT_QDELETING) + brain.to_pickup -= to_pickup + return ONGOING_ACTION_COMPLETED + + if(get_dist(to_pickup, tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + + if(brain.primary_weapon) + brain.primary_weapon.unwield(tied_human) + + if(tied_human.get_held_item()) + tied_human.swap_hand() + + if(isgun(to_pickup)) + 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"]) + tied_human.put_in_hands(to_pickup, TRUE) + INVOKE_ASYNC(tied_human, TYPE_PROC_REF(/mob, equip_to_slot), to_pickup, WEAR_WAIST) + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/storage/backpack) && !brain.container_refs["backpack"]) + tied_human.put_in_hands(to_pickup, TRUE) + INVOKE_ASYNC(tied_human, TYPE_PROC_REF(/mob, equip_to_slot), to_pickup, WEAR_BACK) + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/storage/pouch) && !brain.container_refs["left_pocket"]) + tied_human.put_in_hands(to_pickup, TRUE) + INVOKE_ASYNC(tied_human, TYPE_PROC_REF(/mob, 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"]) + tied_human.put_in_hands(to_pickup, TRUE) + INVOKE_ASYNC(tied_human, TYPE_PROC_REF(/mob, 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(tied_human, brain, tied_human)) + brain.UnregisterSignal(to_pickup, COMSIG_PARENT_QDELETING) + brain.to_pickup -= to_pickup + return ONGOING_ACTION_COMPLETED + + if(to_pickup.flags_human_ai & HEALING_ITEM) + 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)) + tied_human.put_in_hands(to_pickup, TRUE) + brain.store_item(to_pickup, storage_spot, HUMAN_AI_AMMUNITION) + brain.tried_reload = FALSE // not appraising inventory there, let's say we can reload now + return ONGOING_ACTION_COMPLETED + + if(istype(to_pickup, /obj/item/explosive/grenade)) + var/obj/item/explosive/grenade/nade = to_pickup + if(!nade.active) + 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) + 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/keep_distance.dm b/code/modules/mob/living/carbon/human/ai/action_datums/keep_distance.dm new file mode 100644 index 00000000000..d7f22cf61dc --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/keep_distance.dm @@ -0,0 +1,90 @@ +/datum/ai_action/keep_distance + name = "Keep Distance" + action_flags = ACTION_USING_LEGS + +/datum/ai_action/keep_distance/get_weight(datum/human_ai_brain/brain) + var/mob/living/current_target = brain.current_target + if(!current_target) + return 0 + + if(!brain.primary_weapon) + return 0 + + var/distance = get_dist(brain.tied_human, current_target) + var/datum/firearm_appraisal/gun_data = brain.gun_data + + if(current_target.is_mob_incapacitated()) + if(distance != gun_data.minimum_range) + return 10 + + else if(brain.in_cover) + if(distance < gun_data.minimum_range) + return 10 + + else if(distance != gun_data.optimal_range) + return 10 + + return 0 + +/datum/ai_action/keep_distance/trigger_action() + . = ..() + + if(!brain.current_target) + return ONGOING_ACTION_COMPLETED + + if(!brain.primary_weapon) + return ONGOING_ACTION_COMPLETED + + if(brain.active_grenade_found) + return ONGOING_ACTION_COMPLETED + + if(brain.current_cover && !brain.in_cover) + return ONGOING_ACTION_COMPLETED + + return approach() || back_up() || ONGOING_ACTION_COMPLETED + +/datum/ai_action/keep_distance/proc/approach() + var/mob/living/current_target = brain.current_target + + var/range + if(current_target.is_mob_incapacitated()) + range = brain.gun_data.minimum_range + else + range = brain.gun_data.optimal_range + + if(get_dist(brain.tied_human, current_target) <= range) + return + + if(brain.in_cover) + return ONGOING_ACTION_UNFINISHED + + if(!brain.move_to_next_turf(get_turf(current_target))) + return ONGOING_ACTION_COMPLETED + + return ONGOING_ACTION_UNFINISHED + +/datum/ai_action/keep_distance/proc/back_up() + var/mob/living/carbon/human/tied_human = brain.tied_human + var/mob/living/current_target = brain.current_target + + var/range + if(brain.in_cover || current_target.is_mob_incapacitated()) + range = brain.gun_data.minimum_range + else + range = brain.gun_data.optimal_range + + if(get_dist(brain.tied_human, current_target) >= range) + return + + var/moved = FALSE + var/relative_dir = Get_Compass_Dir(current_target, tied_human) + for(var/direction in list(relative_dir, turn(relative_dir, 90), turn(relative_dir, -90))) + var/turf/destination = get_step(tied_human, direction) + if(brain.move_to_next_turf(destination)) + moved = TRUE + break + + if(!moved) + return ONGOING_ACTION_COMPLETED + + return ONGOING_ACTION_UNFINISHED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/patrol_waypoints.dm b/code/modules/mob/living/carbon/human/ai/action_datums/patrol_waypoints.dm new file mode 100644 index 00000000000..2af6462ce85 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/patrol_waypoints.dm @@ -0,0 +1,53 @@ +/datum/ai_action/patrol_waypoints + name = "Patrol Waypoints" + action_flags = ACTION_USING_LEGS + +/datum/ai_action/patrol_waypoints/get_weight(datum/human_ai_brain/brain) + if(brain.in_combat) + return 0 + + var/datum/ai_order/patrol/current_order = brain.current_order + if(!istype(brain.current_order)) + return 0 + + if(length(brain.to_pickup)) + return 0 + + if(current_order.waiting) + return 0 + + if(!brain.is_squad_leader) + if(get_dist(brain.tied_human, current_order.current_waypoint) <= 1) + return 0 + + return 4 + +/datum/ai_action/patrol_waypoints/trigger_action() + . = ..() + + var/datum/ai_order/patrol/current_order = brain.current_order + if(current_order.waiting || QDELETED(current_order) || !istype(current_order) || length(brain.to_pickup) || brain.in_combat) + return ONGOING_ACTION_COMPLETED + + var/turf/current_waypoint = current_order.current_waypoint + if(QDELETED(current_waypoint)) + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[brain.squad_id]"] + if(squad) + squad.remove_current_order() // Our brain is included + else + brain.remove_current_order() + return ONGOING_ACTION_COMPLETED + + var/mob/tied_human = brain.tied_human + if(get_dist(current_waypoint, tied_human) > 1) + if(!brain.move_to_next_turf(current_waypoint)) + return ONGOING_ACTION_COMPLETED + + if(get_dist(current_waypoint, tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + + if(brain.is_squad_leader) + current_order.waiting = TRUE + addtimer(CALLBACK(current_order, TYPE_PROC_REF(/datum/ai_order/patrol, set_next_waypoint)), current_order.time_at_waypoint) + + return ONGOING_ACTION_COMPLETED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/reload.dm b/code/modules/mob/living/carbon/human/ai/action_datums/reload.dm new file mode 100644 index 00000000000..3609a6e7d89 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/reload.dm @@ -0,0 +1,67 @@ +/datum/ai_action/reload + name = "Reload" + action_flags = ACTION_USING_HANDS + var/currently_reloading + +/datum/ai_action/reload/get_weight(datum/human_ai_brain/brain) + if(brain.tried_reload) + return 0 + + if(!brain.gun_data) + return 0 + + if(!brain.should_reload()) + return 0 + + return 15 + +/datum/ai_action/reload/Destroy(force, ...) + currently_reloading = FALSE + return ..() + +/datum/ai_action/reload/trigger_action() + . = ..() + + if(currently_reloading) + return ONGOING_ACTION_UNFINISHED + + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + if(!primary_weapon || brain.tried_reload || !brain.should_reload()) + return ONGOING_ACTION_COMPLETED + + reload() + return ONGOING_ACTION_UNFINISHED + +/datum/ai_action/reload/proc/reload() + set waitfor = FALSE + + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + var/mob/living/carbon/tied_human = brain.tied_human + + var/datum/firearm_appraisal/gun_data = brain.gun_data + if(gun_data.disposable) + tied_human.drop_held_item(primary_weapon) + brain.to_pickup -= primary_weapon + brain.set_primary_weapon(null) + qdel(src) + return + + currently_reloading = TRUE + + /// Find ammo + var/obj/item/ammo_magazine/mag = primary_ammo_search() + if(!mag) + brain.tried_reload = TRUE + qdel(src) + return + + brain.say_reload_line() + gun_data.do_reload(primary_weapon, mag, tied_human, brain) + + /// When do_reload() stops sleeping, let us check things one last time + currently_reloading = FALSE + +/datum/ai_action/reload/proc/primary_ammo_search() + for(var/obj/item/ammo_magazine/mag as anything in brain.equipment_map[HUMAN_AI_AMMUNITION]) + if(istype(brain.primary_weapon, mag.gun_type) && mag.ai_can_use(brain.tied_human, src)) + return mag diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/sniper_nest.dm b/code/modules/mob/living/carbon/human/ai/action_datums/sniper_nest.dm new file mode 100644 index 00000000000..527f8202ecb --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/sniper_nest.dm @@ -0,0 +1,117 @@ +/datum/human_ai_brain + var/turf/sniper_home + var/sniper_dir = SOUTH + +/datum/ai_action/sniper_nest + name = "Sniper Nest" + action_flags = ACTION_USING_LEGS + var/initial_view + var/initial_reload_line_chance + +/datum/ai_action/sniper_nest/get_weight(datum/human_ai_brain/brain) + if(!brain.sniper_home) + return 0 + + if(brain.tried_reload) + return 0 + + if(brain.current_cover) + return 0 + + if(!brain.primary_weapon) + return 0 + + if(brain.healing_someone) + return 0 + + return 12 + +/datum/ai_action/sniper_nest/Added() + initial_view = brain.view_distance + initial_reload_line_chance = brain.reload_line_chance + brain.reload_line_chance = 0 + +/datum/ai_action/sniper_nest/Destroy(force, ...) + brain.view_distance = initial_view + brain.reload_line_chance = initial_reload_line_chance + return ..() + +/datum/ai_action/sniper_nest/trigger_action() + . = ..() + + if(brain.tried_reload || brain.current_cover || brain.healing_someone) + return ONGOING_ACTION_COMPLETED + + var/obj/item/weapon/gun/primary_weapon = brain.primary_weapon + if(!primary_weapon) + return ONGOING_ACTION_COMPLETED + + var/turf/sniper_home = brain.sniper_home + if(QDELETED(sniper_home)) + return ONGOING_ACTION_COMPLETED + + var/mob/living/carbon/tied_human = brain.tied_human + if(get_dist(tied_human, sniper_home) > 0) + if(!brain.move_to_next_turf(sniper_home)) + return ONGOING_ACTION_COMPLETED + + if(!get_dist(tied_human, sniper_home)) + brain.view_distance = 30 + brain.tied_human.face_dir(brain.sniper_dir) + + if(!brain.should_reload()) + brain.unholster_primary() + brain.ensure_primary_hand(primary_weapon) + brain.wield_primary() + + return ONGOING_ACTION_UNFINISHED + + +/datum/admins/proc/create_human_ai_sniper() + set name = "Create Human AI Sniper" + set category = "Game Master.HumanAI" + + var/static/list/sniper_equipment_presets = list( + /datum/equipment_preset/clf/sniper::name = /datum/equipment_preset/clf/sniper, + /datum/equipment_preset/clf/sniper/svd::name = /datum/equipment_preset/clf/sniper/svd, + ) + + if(!check_rights(R_DEBUG)) + return + + if(tgui_input_list(usr, "Press Enter to select the home turf of the sniper.", "Home Turf", list("Enter", "Cancel")) != "Enter") + return + + var/turf/home_turf = get_turf(usr) + var/turf/target_turf + + while(TRUE) + if(tgui_input_list(usr, "Press Enter to select the center of the sniper's overwatch. This must be within 30 tiles and not be blocked.", "Target Turf", list("Enter", "Cancel")) == "Enter") + var/turf/maybe_target_turf = get_turf(usr) + if(get_dist(home_turf, maybe_target_turf) > 30) + to_chat(usr, SPAN_WARNING("This turf is too far away. Max range 30, attempted range [get_dist(home_turf, target_turf)].")) + continue + + if(locate(/turf/closed) in get_line(home_turf, maybe_target_turf)) + to_chat(usr, SPAN_WARNING("A wall is located between the home and target turf.")) + continue + target_turf = maybe_target_turf + break + + if(!home_turf || !target_turf) + return + + var/mob/living/carbon/human/ai_human = new() + var/datum/component/human_ai/ai_comp = ai_human.AddComponent(/datum/component/human_ai) + var/chosen_equipment_name = tgui_input_list(usr, "Select sniper equipment.", "Sniper Equipment", sniper_equipment_presets) + if(!chosen_equipment_name) + qdel(ai_human) + return + arm_equipment(ai_human, sniper_equipment_presets[chosen_equipment_name], TRUE) + + ai_human.forceMove(home_turf) + ai_comp.ai_brain.sniper_home = home_turf + ai_comp.ai_brain.sniper_dir = get_cardinal_dir(home_turf, target_turf) + + to_chat(usr, SPAN_NOTICE("Sniper has been created.")) + diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm new file mode 100644 index 00000000000..6e19eb3e24d --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/take_cover.dm @@ -0,0 +1,31 @@ +/datum/ai_action/take_cover + name = "Take Cover" + action_flags = ACTION_USING_LEGS + +/datum/ai_action/take_cover/get_weight(datum/human_ai_brain/brain) + if(!brain.current_cover) + return 0 + + if(brain.in_cover) + return 0 + + return 15 + +/datum/ai_action/take_cover/trigger_action() + . = ..() + + var/turf/current_cover = brain.current_cover + if(!brain.current_cover) + return ONGOING_ACTION_COMPLETED + + var/mob/living/carbon/human/tied_human = brain.tied_human + if(get_dist(current_cover, tied_human) > 0) + if(!brain.move_to_next_turf(current_cover)) + brain.end_cover() + return ONGOING_ACTION_COMPLETED + + if(get_dist(current_cover, tied_human) > 0) + return ONGOING_ACTION_UNFINISHED + + brain.in_cover = TRUE + 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 00000000000..b846893ba9f --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/throw_back_nade.dm @@ -0,0 +1,93 @@ +/datum/human_ai_brain + /// A nearby found active grenade which AI will try and toss back + var/obj/item/explosive/grenade/active_grenade_found + +/datum/ai_action/throw_back_nade + name = "Throw Back Grenade" + action_flags = ACTION_USING_HANDS | ACTION_USING_LEGS + +/datum/ai_action/throw_back_nade/get_weight(datum/human_ai_brain/brain) + if(QDELETED(brain.active_grenade_found)) + return 0 + + if(get_dist(brain.tied_human, brain.active_grenade_found) > 4) + return 0 + + return 50 + +/datum/ai_action/throw_back_nade/Destroy(force, ...) + brain.active_grenade_found = null // Mr. Grenade is not our friend now + return ..() + +/datum/ai_action/throw_back_nade/trigger_action() + . = ..() + + var/atom/active_grenade_found = brain.active_grenade_found + if(QDELETED(active_grenade_found) || !isturf(active_grenade_found.loc)) + return ONGOING_ACTION_COMPLETED + + var/mob/living/carbon/human/tied_human = brain.tied_human + if(get_dist(active_grenade_found, tied_human) > 1) + if(!brain.move_to_next_turf(get_turf(active_grenade_found))) + return ONGOING_ACTION_COMPLETED + + if(get_dist(active_grenade_found, tied_human) > 1) + return ONGOING_ACTION_UNFINISHED + + var/view_distance = brain.view_distance + var/list/possible_targets = list() + + for(var/mob/living/carbon/target in range(view_distance, tied_human)) + if(brain.can_target(target)) + possible_targets += target + + var/turf/place_to_throw + 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(tied_human in viewers(view_distance, target_turf)) + place_to_throw = target_turf + break + + if(!place_to_throw) + place_to_throw = turf_pathfind_list[4] + + else // 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 + 4, tied_human.z), + locate(tied_human.x + 4, tied_human.y, tied_human.z), + locate(tied_human.x, tied_human.y - 4, tied_human.z), + locate(tied_human.x - 4, tied_human.y, tied_human.z), + ) + 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 + + var/has_friendly = FALSE + for(var/mob/possible_friendly in range(3, location)) + if(!brain.can_target(possible_friendly)) + has_friendly = TRUE + break + + if(!has_friendly) + place_to_throw = location + break + + if(!place_to_throw) + // There's friendlies all around us, apparently. Just uh. Die ig. + return ONGOING_ACTION_COMPLETED + + brain.clear_main_hand() + tied_human.put_in_active_hand(active_grenade_found) + + tied_human.toggle_throw_mode(THROW_MODE_NORMAL) + INVOKE_ASYNC(tied_human, TYPE_PROC_REF(/mob, throw_item), place_to_throw) + + tied_human.face_atom(place_to_throw) + brain.to_pickup -= active_grenade_found // Do NOT play fetch. Please. + 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 00000000000..32282c260f0 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/throw_grenade.dm @@ -0,0 +1,80 @@ +/datum/ai_action/throw_grenade + name = "Throw Grenade" + action_flags = ACTION_USING_HANDS + var/obj/item/explosive/grenade/throwing + var/mid_throw + +/datum/ai_action/throw_grenade/get_weight(datum/human_ai_brain/brain) + if(!brain.in_combat) + return 0 + + var/turf/target_turf = brain.target_turf + if(!target_turf) + return 0 + + if(!length(brain.equipment_map[HUMAN_AI_GRENADES])) + return 0 + + if(!brain.primary_weapon) + return 10 + + if(locate(/turf/closed) in get_line(brain.tied_human, target_turf)) + return 10 + + return 0 + +/datum/ai_action/throw_grenade/get_conflicts(datum/human_ai_brain/brain) + . = ..() + . += /datum/ai_action/chase_target + . += /datum/ai_action/sniper_nest + +/datum/ai_action/throw_grenade/Added() + throwing = locate() in brain.equipment_map[HUMAN_AI_GRENADES] + +/datum/ai_action/throw_grenade/Destroy(force, ...) + throwing = null + return ..() + +/datum/ai_action/throw_grenade/trigger_action() + . = ..() + + var/turf/target_turf = brain.target_turf + if(QDELETED(throwing) || !target_turf) + return ONGOING_ACTION_COMPLETED + + if(mid_throw) + return ONGOING_ACTION_UNFINISHED + + var/mob/living/carbon/human/tied_human = brain.tied_human + if(brain.primary_weapon) + brain.primary_weapon.unwield(tied_human) + if(tied_human.get_active_hand() == brain.primary_weapon) + tied_human.swap_hand() + + mid_throw = TRUE + brain.equip_item_from_equipment_map(HUMAN_AI_GRENADES, throwing) + + if(QDELETED(throwing) || (throwing.loc != tied_human)) + return ONGOING_ACTION_COMPLETED + + var/turf/throw_target + for(var/turf/turf in shuffle(RANGE_TURFS(2, target_turf))) + var/distance = get_dist(tied_human, turf) + if(distance <= 2) // basic precautions + continue + + if(distance > brain.view_distance) + continue + + if(locate(/turf/closed) in get_line(tied_human, turf)) + continue + + throw_target = turf + break + + if(!throw_target) + return ONGOING_ACTION_COMPLETED + + tied_human.face_atom(throw_target) + INVOKE_ASYNC(throwing, TYPE_PROC_REF(/obj/item, ai_use), tied_human, brain, throw_target) + return ONGOING_ACTION_UNFINISHED diff --git a/code/modules/mob/living/carbon/human/ai/action_datums/treat_self.dm b/code/modules/mob/living/carbon/human/ai/action_datums/treat_self.dm new file mode 100644 index 00000000000..08fe5aa0cfe --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/action_datums/treat_self.dm @@ -0,0 +1,42 @@ +/datum/ai_action/treat_self + name = "Treat Self" + action_flags = ACTION_USING_HANDS + +/datum/ai_action/treat_self/get_weight(datum/human_ai_brain/brain) + if(brain.healing_someone) + return 0 + + var/should_fire_offscreen = (brain.target_turf && !COOLDOWN_FINISHED(brain, fire_offscreen)) + if(brain.current_target || should_fire_offscreen) + return 0 + + if(length(brain.to_pickup)) + return 0 + + if(!length(brain.equipment_map[HUMAN_AI_HEALTHITEMS])) + return 0 + + if(!brain.healing_start_check(brain.tied_human)) + return 0 + + return 4 + +/datum/ai_action/treat_self/Destroy(force, ...) + brain.healing_someone = FALSE + return ..() + +/datum/ai_action/treat_self/trigger_action() + . = ..() + + if(brain.current_target) + return ONGOING_ACTION_COMPLETED + + if(brain.healing_someone) + return ONGOING_ACTION_UNFINISHED + + var/mob/tied_human = brain.tied_human + if(brain.healing_start_check(tied_human)) + brain.start_healing(tied_human) + return ONGOING_ACTION_UNFINISHED + + 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 00000000000..3be6f31b2fd --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/ai_equipment.dm @@ -0,0 +1,141 @@ +/// Every AI with a preset should appraise inventory on spawn +/datum/equipment_preset/load_preset(mob/living/carbon/human/new_human, randomise, count_participant, client/mob_client, show_job_gear) + . = ..() + var/datum/human_ai_brain/ai_brain = new_human.get_ai_brain() + if(ai_brain) + ai_brain.appraise_inventory() + +/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/shotgunner + name = "CLF Shotgunner (AI)" + +/datum/equipment_preset/clf/soldier/ai/shotgunner/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) + spawn_rebel_shotgun(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/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/sniper + name = "CLF Sniper (AI) (Basira)" + flags = EQUIPMENT_PRESET_EXTRA + assignment = JOB_CLF + rank = JOB_CLF + role_comm_title = "SNP" + skills = /datum/skills/clf/sniper + +/datum/equipment_preset/clf/sniper/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/marine/boltaction(new_human), WEAR_WAIST) + new_human.put_in_active_hand(new /obj/item/weapon/gun/boltaction(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/sniper/svd + name = "CLF Sniper (AI) (SVD)" + +/datum/equipment_preset/clf/sniper/svd/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/marine/svd(new_human), WEAR_WAIST) + new_human.put_in_active_hand(new /obj/item/weapon/gun/rifle/sniper/svd(new_human)) + + new_human.equip_to_slot_or_del(new /obj/item/device/radio/headset/distress/CLF(new_human), WEAR_L_EAR) diff --git a/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm new file mode 100644 index 00000000000..f2518dfd234 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/ai_management_menu.dm @@ -0,0 +1,174 @@ +/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/ai_order/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, + "can_assign_squad" = brain.can_assign_squad, + )) + + 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.current_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() + return TRUE + + if("assign_to_squad") + if(!params["squad"] || !params["ai"]) + return + + var/datum/human_ai_brain/brain = locate(params["ai"]) + if(!brain.can_assign_squad) + return TRUE + + brain.add_to_squad(params["squad"]) + 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_current_order(locate(params["order"])) + 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) + 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_human = new() + ai_human.AddComponent(/datum/component/human_ai) + + if(!cmd_admin_dress_human(ai_human, randomize = TRUE)) + qdel(ai_human) + return + + ai_human.get_ai_brain().appraise_inventory() + ai_human.face_dir(pick(GLOB.cardinals)) + ai_human.forceMove(get_turf(mob)) + +/client/proc/make_human_ai(mob/living/carbon/human/mob in GLOB.human_mob_list) + set name = "Make AI" + set desc = "Add AI functionality to a human." + set category = null + + if(!check_rights(R_DEBUG|R_ADMIN)) + return + + if(QDELETED(mob)) + return //mob is garbage collected + + if(mob.GetComponent(/datum/component/human_ai)) + to_chat(usr, SPAN_WARNING("[mob] already has an assigned AI.")) + return + + if(mob.ckey && alert("This mob is being controlled by [mob.ckey]. Are you sure you wish to add AI to it?","Make AI","Yes","No") != "Yes") + return + + mob.AddComponent(/datum/component/human_ai) + mob.get_ai_brain().appraise_inventory() + + message_admins("[key_name_admin(usr)] assigned an AI component to [mob.real_name].") 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 00000000000..2b6cdd132c6 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain.dm @@ -0,0 +1,379 @@ +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 + + /// 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 + + /// Should we limit our FOV in case view_distance is more than 7 + var/scope_vision = TRUE + + /// List of whitelisted/blacklisted action datums + var/list/action_whitelist = null + var/list/action_blacklist = null + + /// List of current action datums + var/list/ongoing_actions = list() + + /// Semi-permanent "order" datum. Does not expire + var/datum/ai_order/current_order + + var/list/detection_turfs = list() + + var/in_combat = FALSE + var/combat_decay_time_min = 15 SECONDS + var/combat_decay_time_max = 30 SECONDS + + var/peek_cover_chance = 60 + + var/list/friendly_factions = list() + var/list/neutral_factions = list() + var/previous_faction + + var/squad_covering = FALSE + + /// If false, cannot be assigned to a squad + var/can_assign_squad = TRUE + +/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_MOB_DEATH, PROC_REF(reset_ai)) + RegisterSignal(tied_human, COMSIG_MOVABLE_MOVED, PROC_REF(on_move)) + RegisterSignal(tied_human, COMSIG_HUMAN_BULLET_ACT, PROC_REF(on_shot)) + RegisterSignal(tied_human, COMSIG_HUMAN_HANDCUFFED, PROC_REF(on_handcuffed)) + RegisterSignal(tied_human, COMSIG_HUMAN_GET_AI_BRAIN, PROC_REF(get_ai_brain)) + GLOB.human_ai_brains += src + setup_detection_radius() + appraise_inventory() + +/datum/human_ai_brain/Destroy(force, ...) + GLOB.human_ai_brains -= src + tied_human = null + + reset_ai() + + return ..() + +/datum/human_ai_brain/proc/reset_ai() + end_cover() + + in_combat = FALSE + + target_turf = null + shot_at = null + lose_target() + + for(var/action in ongoing_actions) + qdel(action) + + ongoing_actions.Cut() + +/datum/human_ai_brain/process(delta_time) + if(tied_human.is_mob_incapacitated()) + for(var/action in ongoing_actions) + qdel(action) + ongoing_actions.Cut() + lose_target() + return + + var/possible_target = get_target() + if(possible_target) + lose_target() + set_target(possible_target) + + if(current_target) + enter_combat() + + // Might be wise to move this off tick and instead make it signal-based + item_search(range(2, tied_human)) + + // List all allowed action types for AI to consider + var/list/allowed_actions = action_whitelist || (GLOB.AI_actions.Copy() - action_blacklist) + for(var/datum/ongoing_action as anything in ongoing_actions) + if(is_type_in_list(ongoing_action, allowed_actions)) + allowed_actions -= ongoing_action.type + + // Create assoc list of selected AI actions and their weight + var/list/possible_actions = list() + for(var/action_type in shuffle(allowed_actions)) + var/datum/ai_action/glob_ref = GLOB.AI_actions[action_type] + var/weight = glob_ref.get_weight(src) + if(weight) // No weight means we shouldn't consider this action at all + possible_actions[action_type] = weight + + // Sorts all allowed actions by their weight + var/list/sorted_actions = sortTim(possible_actions, GLOBAL_PROC_REF(cmp_numeric_dsc), TRUE) + + // Choose what actions to start in current process() iteration + for(var/action_type as anything in sorted_actions) + var/datum/ai_action/possible_action = GLOB.AI_actions[action_type] + + var/list/conflicting_actions = possible_action.get_conflicts(src) + for(var/datum/ai_action/ongoing_action as anything in ongoing_actions) + if(ongoing_action.type in conflicting_actions) + possible_action = null + break + + if(!possible_action) + continue + + ongoing_actions += new action_type(src) +#ifdef TESTING + message_admins("action of type [action_type] was added to [tied_human.real_name]") +#endif + + for(var/datum/ai_action/action as anything in ongoing_actions) + var/retval = action.trigger_action() + switch(retval) + if(ONGOING_ACTION_UNFINISHED_BLOCK) + return + if(ONGOING_ACTION_COMPLETED) + qdel(action) + +/datum/human_ai_brain/proc/set_target(mob/living/new_target) + if(!new_target) + return + + RegisterSignal(new_target, COMSIG_PARENT_QDELETING, PROC_REF(on_target_delete), TRUE) + RegisterSignal(new_target, COMSIG_MOB_DEATH, PROC_REF(on_target_death), TRUE) + RegisterSignal(new_target, COMSIG_MOVABLE_MOVED, PROC_REF(on_target_move), TRUE) + current_target = new_target + target_turf = get_turf(current_target) + +/datum/human_ai_brain/proc/lose_target() + if(istype(current_target)) + UnregisterSignal(current_target, COMSIG_PARENT_QDELETING) + UnregisterSignal(current_target, COMSIG_MOB_DEATH) + UnregisterSignal(current_target, COMSIG_MOVABLE_MOVED) + current_target = null + +/datum/human_ai_brain/proc/update_target_pos() + if(current_target) + if(tied_human in viewers(view_distance, current_target)) + target_turf = get_turf(current_target) + else + COOLDOWN_START(src, fire_offscreen, 2 SECONDS) + lose_target() + +/datum/human_ai_brain/proc/on_target_delete(datum/source, force) + SIGNAL_HANDLER + lose_target() + target_turf = null + +/datum/human_ai_brain/proc/on_target_death(datum/source) + SIGNAL_HANDLER + lose_target() + target_turf = null + +/datum/human_ai_brain/proc/on_target_move(atom/oldloc, dir, forced) + SIGNAL_HANDLER + update_target_pos() + +/datum/human_ai_brain/proc/on_human_delete(datum/source, force) + SIGNAL_HANDLER + tied_human = 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/has_ongoing_action(path) + if(!ispath(path)) + return FALSE + + for(var/datum/ai_action/action as anything in ongoing_actions) + if(istype(action, path)) + return TRUE + + return FALSE + +/datum/human_ai_brain/proc/set_current_order(datum/ai_order/ref) + if(!ref) + return + + current_order = ref + current_order.brains += src + +/datum/human_ai_brain/proc/remove_current_order() + if(current_order) + current_order.brains -= src + current_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(tied_human.client) + return + + if(entering == tied_human) + return + + if(istype(entering, /obj/projectile)) + var/obj/projectile/bullet = entering + + enter_combat() + + var/mob/firer = bullet.firer + if(firer?.faction in neutral_factions) + on_neutral_faction_betray(firer.faction) + + if(faction_check(firer)) + return + + if(get_dist(tied_human, firer) <= view_distance) + set_target(firer) + else + COOLDOWN_START(src, fire_offscreen, 4 SECONDS) + target_turf = get_turf(firer) + +/datum/human_ai_brain/proc/on_move(atom/oldloc, direction, forced) + setup_detection_radius() + + if(in_cover) + end_cover() + + update_target_pos() + +/datum/human_ai_brain/proc/enter_combat() + SIGNAL_HANDLER + if(squad_id) // call for help + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[squad_id]"] + for(var/datum/human_ai_brain/squaddie as anything in squad.ai_in_squad) + if(squaddie.target_turf) + continue + if(get_dist(squaddie.tied_human, tied_human) > squaddie.view_distance) + continue + if(!squaddie.can_target(current_target)) + continue + squaddie.target_turf = target_turf + + if(tied_human.client) + return + + if(!in_combat) + say_in_combat_line() + in_combat = TRUE + addtimer(CALLBACK(src, PROC_REF(exit_combat)), rand(combat_decay_time_min, combat_decay_time_max), TIMER_UNIQUE | TIMER_NO_HASH_WAIT | TIMER_OVERRIDE) + +/datum/human_ai_brain/proc/exit_combat() + if(tied_human.client) + return + + if(in_combat) + tied_human.a_intent = INTENT_DISARM + lose_target() + say_exit_combat_line() + if(!sniper_home) + holster_primary() + + if(current_cover) + if(!prob(peek_cover_chance)) + target_turf = null + end_cover() + else + target_turf = null + + in_combat = FALSE + +/datum/human_ai_brain/proc/on_shot(datum/source, damage_result, ammo_flags, obj/projectile/bullet) + SIGNAL_HANDLER + if(tied_human.client) + return + + enter_combat() + + var/mob/firer = bullet.firer + if(firer?.faction in neutral_factions) + on_neutral_faction_betray(firer.faction) + + if(faction_check(firer)) + return + + if(get_dist(tied_human, firer) <= view_distance) + set_target(firer) + else + COOLDOWN_START(src, fire_offscreen, 4 SECONDS) + target_turf = get_turf(firer) + + if(!current_cover) + try_cover(bullet) + else if(in_cover) + on_shot_inside_cover(bullet) + +/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() + +/datum/human_ai_brain/proc/on_handcuffed(datum/source) + SIGNAL_HANDLER + + if((tied_human.stat >= DEAD) || tied_human.client) + return + + message_admins("AI human [tied_human.real_name] has been handcuffed while alive or unconscious.", tied_human.x, tied_human.y, tied_human.z) + +/datum/human_ai_brain/proc/get_ai_brain(datum/source, list/out_brain) + SIGNAL_HANDLER + + out_brain += src + + +/mob/living/carbon/human/proc/get_ai_brain() + RETURN_TYPE(/datum/human_ai_brain) + + var/list/out_brain = list() + SEND_SIGNAL(src, COMSIG_HUMAN_GET_AI_BRAIN, out_brain) + if(length(out_brain)) + return out_brain[1] 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 00000000000..a541b582072 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_communication.dm @@ -0,0 +1,117 @@ +/datum/human_ai_brain + var/list/in_combat_lines = list( + "*warcry", + "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( + "*warcry", + "Nade out!", + "Tossing a grenade!", + "Smoking 'em out!", + "Throwing a nade!", + "Grenade out!", + "Tossing a nade!", + "Pineapple out!", + "Fragging 'em!", + ) + + var/list/reload_lines = list( + "Mag's dry.", + "Reloading.", + "Reloading!", + "I'm out, cover me!", + "Reloading, cover me!", + "Swapping mags!", + "Swapping mags, cover me!", + "Need some cover, reloading!", + "Reloading! Cover me, quick!", + "Out of ammo!", + "Hold up, I’m reloading now!", + "Reloading! Keep me covered!", + "Switching mags—hold them off!", + "I’m dry! Reloading here!", + "New mag going in! Cover me!", + "Reloading! Watch my six!", + ) + + var/list/reload_internal_mag_lines = list( + "Tube's dry.", + "Reloading.", + "Reloading!", + "I'm out, cover me!", + "Reloading, cover me!", + "Need some cover, reloading!", + "Reloading! Cover me, quick!", + "Out of ammo!", + "Hold up, I’m reloading now!", + "Reloading! Keep me covered!", + "Reloading—hold them off!", + "I’m dry! Reloading here!", + "Shells going in! Cover me!", + "Reloading! Watch my six!", + ) + + 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 + var/reload_line_chance = 40 + +/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)) + +/datum/human_ai_brain/proc/say_reload_line(chance = reload_line_chance) + if(!length(reload_lines) || !prob(chance) || (tied_human.health < HEALTH_THRESHOLD_CRIT) || !primary_weapon) + return + if(istype(primary_weapon.current_mag, /obj/item/ammo_magazine/internal)) + tied_human.say(pick(reload_internal_mag_lines)) + else + tied_human.say(pick(reload_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 00000000000..a1360b651ef --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_cover.dm @@ -0,0 +1,147 @@ +/datum/human_ai_brain + /// If TRUE, AI is currently in some form of cover + var/in_cover = FALSE + + /// Reference to atom currently selected as a cover place + var/atom/current_cover + + COOLDOWN_DECLARE(cover_search_cooldown) + +/datum/human_ai_brain/proc/end_cover(atom/source) // 'source' argument reserved for unregistering cade/wall/etc delete comsigs + current_cover = null + in_cover = FALSE + +/datum/human_ai_brain/proc/on_shot_inside_cover(obj/projectile/bullet) + // Cover isn't working. Charge! + end_cover() + +/datum/human_ai_brain/proc/try_cover(obj/projectile/bullet) + if(!COOLDOWN_FINISHED(src, cover_search_cooldown)) + return + + COOLDOWN_START(src, cover_search_cooldown, 10 SECONDS) + + var/list/turf_dict = list() + var/cover_dir = reverse_direction(angle2dir4ai(bullet.angle)) + + recursive_turf_cover_scan(get_turf(tied_human), turf_dict, cover_dir) + +#ifdef TESTING + addtimer(CALLBACK(src, PROC_REF(clear_cover_value_debug), turf_dict.Copy()), 60 SECONDS) +#endif + + cover_processing(turf_dict) + squad_cover_processing(turf_dict) + + +/datum/human_ai_brain/proc/squad_cover_processing(list/turf_dict) + 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) + + brain.cover_processing(turf_dict, TRUE) + + +/datum/human_ai_brain/proc/recursive_turf_cover_scan(turf/scan_turf, list/turf_dict, cover_dir, first_iteration = TRUE) + if(length(turf_dict) > 200) + return FALSE // abort if the room is too large + + if(scan_turf in turf_dict) + return TRUE // abort if we've already been scanned + + turf_dict[scan_turf] = 0 + + for(var/atom/movable/thing as anything in scan_turf.contents) + if(thing.density && !istype(thing, /obj/structure/barricade)) + turf_dict[scan_turf] -= 1000 + if(first_iteration) + break // We don't wanna end our cover search on self + return TRUE // If it has something dense on it, don't bother + + var/obj/structure/barricade/cade = locate() in scan_turf.contents + if(cade?.density && (cade?.dir in get_related_directions(cover_dir))) + turf_dict[scan_turf] += cade.projectile_coverage / 2 + + 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] -= 5 // even if it's our mine, we don't really want to stand on it + + turf_dict[scan_turf] -= get_dist(tied_human, scan_turf) + if(current_target) // Might be smarter to hide in a different direction + turf_dict[scan_turf] += get_dist(current_target, scan_turf) * 0.5 + + if(get_dir(current_target, scan_turf) in get_related_directions(cover_dir)) + turf_dict[scan_turf] -= 20 + +#ifdef TESTING + //sleep(1) +#endif + + for(var/cardinal in shuffle(GLOB.cardinals)) + var/turf/nearby_turf = get_step(scan_turf, cardinal) + if(!nearby_turf) + continue + + if(istype(nearby_turf, /turf/closed)) + turf_dict[scan_turf] += 2 // Near a wall is a bit safer + if(cardinal in get_related_directions(cover_dir)) + turf_dict[scan_turf] += 8 + 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 + +#ifdef TESTING + scan_turf.maptext = "