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 = "

[turf_dict[scan_turf]]

" +#endif + + if(!recursive_turf_cover_scan(nearby_turf, turf_dict, cover_dir, FALSE)) + return FALSE + +#ifdef TESTING + scan_turf.maptext = "

[turf_dict[scan_turf]]

" +#endif + + return TRUE + +/datum/human_ai_brain/proc/clear_cover_value_debug(list/turf_list) + for(var/turf/T as anything in turf_list) + T.maptext = null + +/datum/human_ai_brain/proc/cover_processing(list/turf_dict, from_squad = FALSE) + var/most_weight = -INFINITY + var/turf/best_cover + for(var/turf/T as anything in turf_dict) + var/weight = turf_dict[T] + if(weight > most_weight) + most_weight = weight + best_cover = T + + if(best_cover && best_cover != get_turf(tied_human)) + turf_dict -= best_cover + // insert cover atom deletion/move comsigs here + current_cover = best_cover + if(!from_squad) + squad_cover_processing(FALSE, turf_dict) + + squad_covering = FALSE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm new file mode 100644 index 00000000000..8fc037db9b6 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_factions.dm @@ -0,0 +1,243 @@ +/datum/human_ai_faction + var/faction = FACTION_NEUTRAL + VAR_PROTECTED/shoot_to_kill = TRUE + + VAR_PROTECTED/list/in_combat_lines = list() + VAR_PROTECTED/list/exit_combat_lines = list() + VAR_PROTECTED/list/squad_member_death_lines = list() + VAR_PROTECTED/list/grenade_thrown_lines = list() + VAR_PROTECTED/list/reload_lines = list() + VAR_PROTECTED/list/reload_internal_mag_lines = list() + + VAR_PROTECTED/list/friendly_factions = list() + VAR_PROTECTED/list/neutral_factions = list() + +/datum/human_ai_faction/proc/apply_faction_data(datum/human_ai_brain/brain) + if(length(in_combat_lines)) + brain.in_combat_lines = in_combat_lines + + if(length(exit_combat_lines)) + brain.exit_combat_lines = exit_combat_lines + + if(length(squad_member_death_lines)) + brain.squad_member_death_lines = squad_member_death_lines + + if(length(grenade_thrown_lines)) + brain.grenade_thrown_lines = grenade_thrown_lines + + if(length(reload_lines)) + brain.reload_lines = reload_lines + + if(length(reload_internal_mag_lines)) + brain.reload_internal_mag_lines = reload_internal_mag_lines + + brain.shoot_to_kill = shoot_to_kill + brain.friendly_factions = friendly_factions + brain.neutral_factions = neutral_factions + +/datum/human_ai_faction/proc/reapply_faction_data() + for(var/datum/human_ai_brain/brain in GLOB.human_ai_brains) + if(brain.tied_human?.faction == faction) + apply_faction_data(brain) + +/datum/human_ai_faction/proc/add_friendly_faction(faction) + if(faction in friendly_factions) + return + friendly_factions += faction + if(faction in neutral_factions) + neutral_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/add_neutral_faction(faction) + if(faction in neutral_factions) + return + neutral_factions += faction + if(faction in friendly_factions) + friendly_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/remove_friendly_faction(faction) + if(!(faction in friendly_factions)) + return + friendly_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/remove_neutral_faction(faction) + if(!(faction in neutral_factions)) + return + neutral_factions -= faction + reapply_faction_data() + +/datum/human_ai_faction/proc/get_friendly_factions() + return friendly_factions + +/datum/human_ai_faction/proc/get_neutral_factions() + return neutral_factions + +/datum/human_ai_faction/proc/set_shoot_to_kill(new_kill = TRUE) + shoot_to_kill = new_kill + reapply_faction_data() + +/datum/human_ai_faction/proc/get_shoot_to_kill() + return shoot_to_kill + +/datum/human_ai_faction/clf + faction = FACTION_CLF + friendly_factions = list( + FACTION_COLONIST, + ) + in_combat_lines = list( + "You will never defeat us!", + "I will kill you!", + "You'll never take our homeland!", + "For the colonies!", + "Free colony!", + "We will triumph over these infidels!", + "Attack!", + "Charge!", + "Die with freedom!", + "Wipe 'em out!", + "Run for your life! You little...!", + "You haven't got enough yet?!", + "Die! You bastard!", + "Damn rat!", + "Victory is ours!", + "No mercy!", + "There will be no mercy for you!", + "Die!", + "We will die trying to protect our homes!", + "Give up!", + "For the cause!", + "'Till our last breath!", + ) + exit_combat_lines = list( + "Where are they.", + "There's no one here.", + "The enemy is quiet.", + "It's quiet.", + "There may be more.", + "Quiet now...!", + "Are we done yet?", + "We live to fight again.", + "Where are they now?", + ) + squad_member_death_lines = list( + "Get back! Get back!", + "I'm sorry...", + "Shoot! Run!", + "Get away!", + "Forgive me...!", + "This is for our colony!", + "I will avenge you!", + "Time for payback!", + "Not good!", + "Damn!", + ) + grenade_thrown_lines = list( + "EAT THIS!", + "Grenade!", + "Throwing bomb!", + "*warcry", + "This is for you, invader!", + ) + +/datum/human_ai_faction/uscm + faction = FACTION_MARINE + friendly_factions = list( + FACTION_COLONIST, + FACTION_TWE, + FACTION_WY, + ) + neutral_factions = list( + FACTION_FREELANCER, + FACTION_CONTRACTOR, + FACTION_UPP, + FACTION_MERCENARY, + FACTION_SURVIVOR, + ) + +/datum/human_ai_faction/upp + faction = FACTION_UPP + friendly_factions = list( + FACTION_COLONIST, + ) + neutral_factions = list( + FACTION_FREELANCER, + FACTION_CONTRACTOR, + FACTION_MARINE, + FACTION_MERCENARY, + FACTION_TWE, + FACTION_SURVIVOR, + ) + in_combat_lines = list( // zonenote: tweak these. They're entirely the stereotype of "communist russkie" when we can do better than that. also languages + "*warcry", + "For the UPP!", + "Die, you animal!", + "Capitalist dog!", + "Shoot them!", + "For glorious Union!", + "Attacking!", + "We will bury them!", + "Uraaaa!!", + "URAAA!!", + "To your last breath!", + "You're worth nothing!", + "This is the end, for you!", + "Die!", + ) + exit_combat_lines = list( + "I need a break...", + "Phew, that was tough work.", + "I think we can stop shooting now?", + "One step closer to victory!", + "Finally, break time.", + ) + squad_member_death_lines = list( + "Man down!", + "Comrade!!", + "Get together!", + "Damn!", + "Taking hits!", + ) + + +/datum/human_ai_faction/wy + faction = FACTION_WY + friendly_factions = list( + FACTION_COLONIST, + FACTION_TWE, + FACTION_MARINE, + ) + neutral_factions = list( + FACTION_FREELANCER, + FACTION_CONTRACTOR, + FACTION_MERCENARY, + FACTION_SURVIVOR, + ) + +/datum/human_ai_faction/wy_deathsquad + faction = FACTION_WY_DEATHSQUAD + friendly_factions = list( + FACTION_WY, + ) + in_combat_lines = list( + "Visual confirmed, engaging.", + "Engaging hostile.", + "Eliminating hostile.", + "Engaging.", + "Contact.", + "Viscon, proceeding.", + ) + exit_combat_lines = list( + "Hostilities ceased.", + "Ceasing engagement.", + ) + squad_member_death_lines = list( + "Allied unit disabled.", + "Friendly unit decomissioned.", + "Allied unit decomissioned.", + "Friendly unit disabled.", + ) + grenade_thrown_lines = list() // Wouldn't need to call this out + reload_lines = list() // same here + reload_internal_mag_lines = list() diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm new file mode 100644 index 00000000000..2d36d29972b --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_guns.dm @@ -0,0 +1,52 @@ +/datum/human_ai_brain + var/obj/item/weapon/gun/primary_weapon + //var/obj/item/weapon/primary_melee + /// Appraisal datum + var/datum/firearm_appraisal/gun_data + /// If we've tried to reload (and failed) with our current inventory + var/tried_reload = FALSE + /// Cooldown for if we've fired too many rounds in a burst (for recoil) + COOLDOWN_DECLARE(fire_overload_cooldown) + +/datum/human_ai_brain/proc/should_reload() + if(!primary_weapon) + return FALSE + + if(primary_weapon.in_chamber) + return FALSE + + if(!primary_weapon.current_mag) + return TRUE + + if(primary_weapon.current_mag.current_rounds > 0) + return FALSE + + return TRUE + +/datum/human_ai_brain/proc/unholster_primary() + if(!primary_weapon || tied_human.l_hand == primary_weapon || tied_human.r_hand == primary_weapon) + return + + var/cur_hand = tied_human.get_active_hand() + if(cur_hand) + tied_human.drop_held_item(cur_hand) + + tied_human.u_equip(primary_weapon) + tied_human.put_in_active_hand(primary_weapon) + + primary_weapon.guaranteed_delay_time = world.time + primary_weapon.wield_time = world.time + primary_weapon.pull_time = world.time + +/datum/human_ai_brain/proc/wield_primary() + primary_weapon?.wield(tied_human) + +/datum/human_ai_brain/proc/wield_primary_sleep() + wield_primary() + sleep(max(primary_weapon?.wield_delay, short_action_delay * action_delay_mult)) + +/datum/human_ai_brain/proc/holster_primary() + if(tied_human.s_store || (tied_human.l_hand != primary_weapon && tied_human.r_hand != primary_weapon)) + return FALSE + + return tied_human.equip_to_slot_if_possible(primary_weapon, WEAR_J_STORE, TRUE) diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm new file mode 100644 index 00000000000..c3283fbc3ae --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_health.dm @@ -0,0 +1,290 @@ +/datum/human_ai_brain + var/static/list/brute_heal_items = list( + /obj/item/stack/medical/advanced/bruise_pack, + /obj/item/reagent_container/hypospray/autoinjector/bicaridine, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/bicaridine, + /obj/item/storage/pill_bottle/merabica, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/burn_heal_items = list( + /obj/item/stack/medical/advanced/ointment, + /obj/item/reagent_container/hypospray/autoinjector/kelotane, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/kelotane, + /obj/item/storage/pill_bottle/keloderm, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/tox_heal_items = list( + /obj/item/reagent_container/hypospray/autoinjector/dylovene, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/antitox, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/oxy_heal_items = list( + /obj/item/reagent_container/hypospray/autoinjector/dexalinp, + /obj/item/reagent_container/hypospray/autoinjector/tricord, + /obj/item/storage/pill_bottle/dexalin, + /obj/item/storage/pill_bottle/dexalinplus, + /obj/item/storage/pill_bottle/tricord, + ) + + var/static/list/bleed_heal_items = list( + /obj/item/stack/medical/advanced/bruise_pack, + /obj/item/stack/medical/bruise_pack, + ) + + var/static/list/bonebreak_heal_items = list( + /obj/item/stack/medical/splint, + ) + + var/static/list/painkiller_items = list( + /obj/item/reagent_container/hypospray/autoinjector/tramadol, + /obj/item/reagent_container/hypospray/autoinjector/oxycodone, + /obj/item/storage/pill_bottle/tramadol, + ) + + /// At what percentage of max HP to start searching for medical treatment + var/healing_start_threshold = 0.7 + /// Requires this much damage of one type to consider it a problem + var/damage_problem_threshold = 5 + /// Pain percentage (out of 100) for the AI to consider using painkillers + var/pain_percentage_threshold = 1 + + /// Are we currently treating someone? + var/healing_someone + + /// Cooldown on using pills to avoid OD. This isn't the best solution as it prevents the AI from using more than 1 pill of any kind every 10s, but it'll work for now + COOLDOWN_DECLARE(pill_use_cooldown) + +/datum/human_ai_brain/proc/healing_start_check(mob/living/carbon/human/target) + return ((target.health / target.maxHealth) <= healing_start_threshold) || target.is_bleeding() || target.has_broken_limbs() + +/datum/human_ai_brain/proc/start_healing(mob/living/carbon/human/target) + set waitfor = FALSE + + healing_someone = TRUE + + // Prioritize brute, then bleed, then broken bones, then burn, then pain, then tox, then oxy. + if(target.getBruteLoss() > damage_problem_threshold) + var/obj/item/brute_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, brute_heal_items) && heal_item.ai_can_use(tied_human, src, target)) + brute_heal = heal_item + break + + if(!brute_heal) + goto bleed + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, brute_heal)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + brute_heal.ai_use(tied_human, src, target) + if(QDELETED(brute_heal)) + goto bleed + + var/storage_slot = storage_has_room(brute_heal) + if(storage_slot) + store_item(brute_heal, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(brute_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed brute damage using [brute_heal].") +#endif + + bleed: + if(tied_human.is_bleeding()) + var/obj/item/bleed_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, bleed_heal_items) && heal_item.ai_can_use(tied_human, src, target)) + bleed_heal = heal_item + break + + if(!bleed_heal) + goto bone + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, bleed_heal)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + bleed_heal.ai_use(tied_human, src, target) + if(QDELETED(bleed_heal)) + goto bone + + var/storage_slot = storage_has_room(bleed_heal) + if(storage_slot) + store_item(bleed_heal, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(bleed_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] fixed bleeding using [bleed_heal].") +#endif + + // Doesn't support bone-healing chems + bone: + if(tied_human.has_broken_limbs()) + var/obj/item/bone_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, bonebreak_heal_items) && heal_item.ai_can_use(tied_human, src, target)) + bone_heal = heal_item + break + + if(!bone_heal) + goto fire + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, bone_heal)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + bone_heal.ai_use(tied_human, src, target) + if(QDELETED(bone_heal)) + goto fire + + var/storage_slot = storage_has_room(bone_heal) + if(storage_slot) + store_item(bone_heal, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(bone_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] splinted a fracture using [bone_heal].") +#endif + + fire: + if(tied_human.getFireLoss() > damage_problem_threshold) + var/obj/item/burn_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, burn_heal_items) && heal_item.ai_can_use(tied_human, src, target)) + burn_heal = heal_item + break + + if(!burn_heal) + goto pain + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, burn_heal)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + burn_heal.ai_use(tied_human, src, target) + if(QDELETED(burn_heal)) + goto pain + + var/storage_slot = storage_has_room(burn_heal) + if(storage_slot) + store_item(burn_heal, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(burn_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed burn damage using [burn_heal].") +#endif + + pain: + // This has the issue of the AI taking multiple painkillers if high on pain, despite them not stacking. Not worth fixing atm + if(tied_human.pain.get_pain_percentage() > pain_percentage_threshold) + var/obj/item/painkiller + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, painkiller_items) && heal_item.ai_can_use(tied_human, src, target)) + painkiller = heal_item + break + + if(!painkiller) + goto tox + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, painkiller)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + painkiller.ai_use(tied_human, src, target) + if(QDELETED(painkiller)) + goto tox + + var/storage_slot = storage_has_room(painkiller) + if(storage_slot) + store_item(painkiller, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(painkiller) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed pain using [painkiller].") +#endif + + tox: + if(tied_human.getToxLoss() > damage_problem_threshold) + var/obj/item/tox_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, tox_heal_items) && heal_item.ai_can_use(tied_human, src, target)) + tox_heal = heal_item + break + + if(!tox_heal) + goto oxy + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, tox_heal)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + tox_heal.ai_use(tied_human, src, target) + if(QDELETED(tox_heal)) + goto oxy + + var/storage_slot = storage_has_room(tox_heal) + if(storage_slot) + store_item(tox_heal, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(tox_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed tox damage using [tox_heal].") +#endif + + oxy: + if(tied_human.getOxyLoss() > damage_problem_threshold) + var/obj/item/oxy_heal + for(var/obj/item/heal_item as anything in equipment_map[HUMAN_AI_HEALTHITEMS]) + if(is_type_in_list(heal_item, oxy_heal_items) && heal_item.ai_can_use(tied_human, src, target)) + oxy_heal = heal_item + + if(!oxy_heal) + healing_someone = FALSE + return + + if(!equip_item_from_equipment_map(HUMAN_AI_HEALTHITEMS, oxy_heal)) + healing_someone = FALSE + return + + clear_main_hand() + healing_someone = TRUE + sleep(short_action_delay * action_delay_mult) + oxy_heal.ai_use(tied_human, src, target) + if(QDELETED(oxy_heal)) + healing_someone = FALSE + return + + var/storage_slot = storage_has_room(oxy_heal) + if(storage_slot) + store_item(oxy_heal, storage_slot, HUMAN_AI_HEALTHITEMS) + else + tied_human.drop_held_item(oxy_heal) +#ifdef TESTING + to_chat(world, "[tied_human.name] healed oxygen damage using [oxy_heal].") +#endif + healing_someone = FALSE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm new file mode 100644 index 00000000000..40c99bfec8e --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_items.dm @@ -0,0 +1,372 @@ +/datum/human_ai_brain + var/list/equipped_items_original_loc = list() + + var/list/obj/item/to_pickup = list() + + /// list("object_type" = list(object_ref = "slot") + var/list/equipment_map = list( + HUMAN_AI_HEALTHITEMS = list(), + HUMAN_AI_AMMUNITION = list(), + HUMAN_AI_GRENADES = list(), + HUMAN_AI_TOOLS = list(), + ) + + var/list/container_refs = list( + "belt" = null, + "backpack" = null, + "left_pocket" = null, + "right_pocket" = null, + ) + + var/static/list/important_storage_slots = list( + WEAR_BACK, + WEAR_WAIST, + WEAR_L_STORE, + WEAR_R_STORE, + ) + + var/static/important_storage_slots_bitflag = SLOT_BACK | SLOT_WAIST | SLOT_STORE + +/datum/human_ai_brain/proc/get_object_from_loc(object_loc) + RETURN_TYPE(/obj/item/storage) + + var/obj/item/storage/storage_object + switch(object_loc) + if("belt") + storage_object = tied_human.belt + if("backpack") + storage_object = tied_human.back + if("left_pocket") + storage_object = tied_human.l_store + if("right_pocket") + storage_object = tied_human.r_store + return storage_object + +/datum/human_ai_brain/proc/equip_item_from_equipment_map(object_type, obj/item/object_ref) + if(!object_type || !object_ref) + return + + var/object_loc = equipment_map[object_type][object_ref] + var/obj/item/storage/storage_object = get_object_from_loc(object_loc) + + storage_object.remove_from_storage(object_ref, tied_human) + equipped_items_original_loc[object_ref] = object_loc + RegisterSignal(object_ref, COMSIG_ITEM_DROPPED, PROC_REF(on_equipment_dropped), override = TRUE) + + return tied_human.put_in_active_hand(object_ref) + +/datum/human_ai_brain/proc/get_item_from_equipment_map_path(object_path, object_type) + return (locate(object_path) in equipment_map[object_type]) + +/datum/human_ai_brain/proc/store_item(obj/item/object_ref, object_loc, slot_type) + if(object_ref.loc != tied_human) + return + + if(object_ref in equipped_items_original_loc) + var/obj/item/storage/storage_object = get_object_from_loc(equipped_items_original_loc[object_ref]) + equipped_items_original_loc -= object_ref + storage_object.attempt_item_insertion(object_ref, FALSE, tied_human) + if(slot_type) + equipment_map[slot_type][object_ref] = object_loc + else if(object_loc) // we assume that we've already checked if something will fit or not + var/obj/item/storage/storage_item = container_refs[object_loc] + storage_item.attempt_item_insertion(object_ref, FALSE, tied_human) + if(slot_type) + equipment_map[slot_type][object_ref] = object_loc + + to_pickup -= object_ref + +/// Whenever an item is deleted, purge it from anywhere it may be stored in here +/datum/human_ai_brain/proc/on_item_delete(obj/item/source, force) + SIGNAL_HANDLER + + UnregisterSignal(source, COMSIG_PARENT_QDELETING) + to_pickup -= source + + for(var/name in container_refs) + if(source == container_refs[name]) + container_refs[name] = null + return + + for(var/id in equipment_map) + for(var/obj/item/item_ref as anything in equipment_map[id]) + if(source == item_ref) + equipment_map[id] -= item_ref + return + +/datum/human_ai_brain/proc/on_item_equip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + to_pickup -= equipment + + if((slot in important_storage_slots) && istype(equipment, /obj/item/storage)) + recalculate_containers() + appraise_inventory(slot == WEAR_WAIST, slot == WEAR_BACK, slot == WEAR_L_STORE, slot == WEAR_R_STORE) + + if((!primary_weapon || (primary_weapon?.w_class < equipment.w_class)) && isgun(equipment)) + set_primary_weapon(equipment) + +/datum/human_ai_brain/proc/on_item_unequip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + + if((important_storage_slots_bitflag & slot) && istype(equipment, /obj/item/storage)) + recalculate_containers() + appraise_inventory(slot == SLOT_WAIST, slot == SLOT_BACK, slot == SLOT_STORE, slot == SLOT_STORE) + + if(isgun(equipment)) + appraise_inventory(FALSE, FALSE, FALSE, FALSE) + +/datum/human_ai_brain/proc/recalculate_containers() + container_refs = list() + if(isstorage(tied_human.belt)) + container_refs["belt"] = tied_human.belt + if(isstorage(tied_human.back)) + container_refs["backpack"] = tied_human.back + if(isstorage(tied_human.l_store)) + container_refs["left_pocket"] = tied_human.l_store + if(isstorage(tied_human.r_store)) + container_refs["right_pocket"] = tied_human.r_store + +/// Currently doesn't support recursive storage +/datum/human_ai_brain/proc/appraise_inventory(belt = TRUE, back = TRUE, pocket_l = TRUE, pocket_r = TRUE) + if(previous_faction != tied_human.faction) + previous_faction = tied_human.faction + var/datum/human_ai_faction/our_faction = SShuman_ai.human_ai_factions[tied_human.faction] + our_faction?.apply_faction_data(src) + + /*if(tied_human.shoes && !primary_melee) // snowflake bootknife check + var/obj/item/weapon/knife = locate() in tied_human.shoes + if(knife) + set_primary_melee(knife)*/ + + tried_reload = FALSE // We don't really need to do this in a smart way + if(belt) + if(!istype(tied_human.belt, /obj/item/storage)) // belts can be backpacks, don't ask + goto back_statement + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "belt") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.belt, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.belt, "belt") + + back_statement: + if(back) + if(!istype(tied_human.back, /obj/item/storage/backpack)) + goto l_pocket_statement + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "backpack") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.back, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.back, "backpack") + + l_pocket_statement: + if(pocket_l) + if(!istype(tied_human.l_store, /obj/item/storage/pouch)) + goto r_pocket_statement + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "left_pocket") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.l_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.l_store, "left_pocket") + + r_pocket_statement: + if(pocket_r) + if(!istype(tied_human.r_store, /obj/item/storage/pouch)) + return + + for(var/id in equipment_map) + for(var/obj/item/item as anything in equipment_map[id]) + if(equipment_map[id][item] != "right_pocket") + continue + + equipment_map[id] -= item + + RegisterSignal(tied_human.r_store, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + item_slot_appraisal_loop(tied_human.r_store, "right_pocket") + +/datum/human_ai_brain/proc/item_slot_appraisal_loop(obj/item/container_to_loop, slot_to_assign) + for(var/obj/item/inv_item as anything in container_to_loop) + RegisterSignal(inv_item, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + if(inv_item.flags_human_ai & HEALING_ITEM) + equipment_map[HUMAN_AI_HEALTHITEMS][inv_item] = slot_to_assign + else if(inv_item.flags_human_ai & AMMUNITION_ITEM) + equipment_map[HUMAN_AI_AMMUNITION][inv_item] = slot_to_assign + else if(inv_item.flags_human_ai & GRENADE_ITEM) + equipment_map[HUMAN_AI_GRENADES][inv_item] = slot_to_assign + else if(inv_item.flags_human_ai & TOOL_ITEM) + equipment_map[HUMAN_AI_TOOLS][inv_item] = slot_to_assign + //else if((inv_item.flags_human_ai & MELEE_WEAPON_ITEM) && !primary_melee) + // set_primary_melee(inv_item) + +/datum/human_ai_brain/proc/clear_main_hand() + var/obj/item/active_hand = tied_human.get_active_hand() + if(!active_hand) + return + + if(primary_weapon == active_hand) + if(!holster_primary()) + tied_human.drop_held_item(active_hand) + return + + var/storage_id = storage_has_room(active_hand) + if(!storage_id) + tied_human.drop_held_item(active_hand) + return + + store_item(active_hand, storage_id) + +/datum/human_ai_brain/proc/storage_has_room(obj/item/inserting) + for(var/container_id in container_refs) + var/obj/item/storage/container = container_refs[container_id] + if(container?.can_be_inserted(inserting, tied_human, TRUE)) + return container_id + +/datum/human_ai_brain/proc/on_equipment_dropped(obj/item/source, mob/dropper) + SIGNAL_HANDLER + + if(isturf(source.loc)) + equipped_items_original_loc -= source + UnregisterSignal(source, COMSIG_ITEM_DROPPED) + +/datum/human_ai_brain/proc/on_item_pickup(datum/source, obj/item/picked_up) + SIGNAL_HANDLER + + if((!primary_weapon || (primary_weapon.w_class < picked_up.w_class)) && isgun(picked_up)) + set_primary_weapon(picked_up) + + to_pickup -= picked_up + +/datum/human_ai_brain/proc/on_item_drop(datum/source, obj/item/dropped) + SIGNAL_HANDLER + + if(dropped == primary_weapon) + if(!(gun_data.disposable && !primary_weapon.ai_can_use(tied_human, src))) + to_pickup |= dropped + set_primary_weapon(null) + + for(var/slot in container_refs) + if(container_refs[slot] == dropped) + appraise_inventory(slot == "belt", slot == "backpack", slot == "left_pocket", slot == "right_pocket") + break + + for(var/id in equipment_map) + for(var/obj/item/item_ref as anything in equipment_map[id]) + if(item_ref == dropped) + equipment_map[id] -= item_ref + return + +/datum/human_ai_brain/proc/set_primary_weapon(obj/item/weapon/gun/new_gun) + if(primary_weapon) + UnregisterSignal(primary_weapon, COMSIG_PARENT_QDELETING) + primary_weapon = new_gun + appraise_primary() + if(primary_weapon) + RegisterSignal(primary_weapon, COMSIG_PARENT_QDELETING, PROC_REF(on_primary_delete), TRUE) + +/datum/human_ai_brain/proc/on_primary_delete(datum/source, force) + SIGNAL_HANDLER + + set_primary_weapon(null) + to_pickup -= source + +/*datum/human_ai_brain/proc/set_primary_melee(obj/item/weapon/new_melee) + if(primary_melee) + UnregisterSignal(primary_melee, COMSIG_PARENT_QDELETING) + primary_melee = new_melee + appraise_primary() + if(primary_melee) + RegisterSignal(primary_melee, COMSIG_PARENT_QDELETING, PROC_REF(on_primary_melee_delete)) + +/datum/human_ai_brain/proc/on_primary_melee_delete(datum/source, force) + SIGNAL_HANDLER + + set_primary_melee(null)*/ + +/datum/human_ai_brain/proc/appraise_primary() + if(!primary_weapon) + return + var/static/datum/firearm_appraisal/default = new() + for(var/datum/firearm_appraisal/appraisal as anything in GLOB.firearm_appraisals) + if(is_type_in_list(primary_weapon, appraisal.gun_types)) + gun_data = appraisal + break + + if(!gun_data) + gun_data = default + +/datum/human_ai_brain/proc/item_search(list/things_around) + search_loop: + for(var/obj/item/thing in things_around) + if(!isturf(thing.loc)) + continue + + if(thing in to_pickup) + continue + + if(thing.flags_human_ai & GRENADE_ITEM) + var/obj/item/explosive/grenade/nade = thing + if(nade.active) + active_grenade_found = thing + continue + + if(!primary_weapon && isgun(thing)) + var/obj/item/weapon/gun/thing_gun = thing + for(var/datum/firearm_appraisal/appraisal as anything in GLOB.firearm_appraisals) + if(is_type_in_list(thing_gun, appraisal.gun_types)) + if(appraisal.disposable && thing_gun.current_mag?.current_rounds <= 0) + continue search_loop + break + + add_to_pickup(thing) + + if(istype(thing, /obj/item/storage/belt) && !container_refs["belt"]) + add_to_pickup(thing) + + if(istype(thing, /obj/item/storage/backpack) && !container_refs["backpack"]) + add_to_pickup(thing) + + if(istype(thing, /obj/item/storage/pouch) && (!container_refs["left_pocket"] || !container_refs["right_pocket"])) + add_to_pickup(thing) + + var/storage_spot = storage_has_room(thing) + if(!storage_spot || !thing.ai_can_use(tied_human, src, tied_human)) + continue + + if(thing.flags_human_ai & HEALING_ITEM) + add_to_pickup(thing) + + if((thing.flags_human_ai & AMMUNITION_ITEM) && primary_weapon) + var/obj/item/ammo_magazine/mag = thing + if(istype(primary_weapon, mag.gun_type)) + add_to_pickup(thing) + + if(thing.flags_human_ai & GRENADE_ITEM) + add_to_pickup(thing) + + if(thing.flags_human_ai & TOOL_ITEM) // zonenote: they can pick up 1 billion crowbars + add_to_pickup(thing) + +/datum/human_ai_brain/proc/add_to_pickup(obj/item/thing) + RegisterSignal(thing, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), TRUE) + to_pickup += thing + +/datum/human_ai_brain/proc/get_tool_from_equipment_map(tool_trait) + RETURN_TYPE(/obj/item) + for(var/obj/item/maybe_tool as anything in equipment_map[HUMAN_AI_TOOLS]) + if(!HAS_TRAIT(maybe_tool, tool_trait)) + continue + return maybe_tool diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm new file mode 100644 index 00000000000..b25c8abe44c --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_pathfinding.dm @@ -0,0 +1,90 @@ +/datum/human_ai_brain + var/ai_move_delay = 0 + var/list/current_path + var/turf/current_path_target + var/path_update_period = (0.5 SECONDS) + var/no_path_found = FALSE + var/max_travel_distance = HUMAN_AI_MAX_PATHFINDING_RANGE + var/next_path_generation = 0 + /// Amount of times no path found has occured + var/no_path_found_amount = 0 + var/ai_timeout_time = 0 + + /// The time interval between calculating new paths if we cannot find a path + var/no_path_found_period = (2.5 SECONDS) + + /// Cooldown declaration for delaying finding a new path if no path was found + COOLDOWN_DECLARE(no_path_found_cooldown) + +/datum/human_ai_brain/proc/can_move_and_apply_move_delay() + // Unable to move, try next time. + if(ai_move_delay > world.time || !(tied_human.mobility_flags & MOBILITY_MOVE) || tied_human.is_mob_incapacitated(TRUE) || (tied_human.body_position != STANDING_UP && !tied_human.can_crawl) || tied_human.anchored) + return FALSE + + ai_move_delay = world.time + tied_human.move_delay + if(tied_human.recalculate_move_delay) + ai_move_delay = world.time + tied_human.movement_delay() + if(tied_human.next_move_slowdown) + ai_move_delay += tied_human.next_move_slowdown + tied_human.next_move_slowdown = 0 + return TRUE + +/datum/human_ai_brain/proc/move_to_next_turf(turf/T, max_range = max_travel_distance) + if(!T) + return FALSE + + if(no_path_found) + if(no_path_found_amount > 0) + COOLDOWN_START(src, no_path_found_cooldown, no_path_found_period) + no_path_found = FALSE + no_path_found_amount++ + return FALSE + + no_path_found_amount = 0 + + if((!current_path || (next_path_generation < world.time && current_path_target != T)) && COOLDOWN_FINISHED(src, no_path_found_cooldown)) + if(!CALCULATING_PATH(tied_human) || current_path_target != T) + SSpathfinding.calculate_path(tied_human, T, max_range, tied_human, CALLBACK(src, PROC_REF(set_path)), list(tied_human, current_target)) + current_path_target = T + next_path_generation = world.time + path_update_period + + if(CALCULATING_PATH(tied_human)) + return TRUE + + // No possible path to target. + if(!current_path && get_dist(T, tied_human) > 0) + return FALSE + + // We've reached our destination + if(!length(current_path) || get_dist(T, tied_human) <= 0) + current_path = null + return TRUE + + var/turf/next_turf = current_path[length(current_path)] + // We've somehow deviated from our current path. Generate next path whenever possible. + if(get_dist(next_turf, tied_human) > 1) + current_path = null + return TRUE + + // Unable to move, try next time. + if(!can_move_and_apply_move_delay()) + return TRUE + + var/list/L = LinkBlocked(tied_human, tied_human.loc, next_turf, list(tied_human, current_target), TRUE) + L += SSpathfinding.check_special_blockers(tied_human, next_turf) + for(var/a in L) + var/atom/A = a + if(A.human_ai_obstacle(tied_human, src, get_dir(tied_human.loc, next_turf)) == INFINITY) + return FALSE + INVOKE_ASYNC(A, TYPE_PROC_REF(/atom, human_ai_act), tied_human, src) + var/successful_move = tied_human.Move(next_turf, get_dir(tied_human, next_turf)) + if(successful_move) + ai_timeout_time = world.time + current_path.len-- + + return TRUE + +/datum/human_ai_brain/proc/set_path(list/path) + current_path = path + if(!path) + no_path_found = TRUE diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm new file mode 100644 index 00000000000..6466960df6f --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_squad.dm @@ -0,0 +1,100 @@ +/datum/human_ai_squad + /// Numeric ID of the squad + var/id + /// The AI humans in the squad + var/list/ai_in_squad = list() + /// Primary order assigned to this squad + var/datum/ai_order/current_order + /// Ref to the squad leader brain + var/datum/human_ai_brain/squad_leader + +/datum/human_ai_squad/New() + . = ..() + id = SShuman_ai.highest_squad_id + +/datum/human_ai_squad/Destroy(force, ...) + for(var/datum/human_ai_brain/brain as anything in ai_in_squad) + remove_from_squad(brain) + SShuman_ai.squads -= src + squad_leader = null + return ..() + +/datum/human_ai_squad/proc/add_to_squad(datum/human_ai_brain/adding) + if(adding.squad_id && (adding.squad_id in SShuman_ai.squad_id_dict)) + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict[adding.squad_id] + squad.remove_from_squad(adding) + adding.squad_id = id + ai_in_squad += adding + + adding.set_current_order(current_order) + RegisterSignal(adding.tied_human, COMSIG_MOB_DEATH, PROC_REF(on_squad_member_death)) + RegisterSignal(adding, COMSIG_PARENT_QDELETING, PROC_REF(on_squad_member_delete)) + +/datum/human_ai_squad/proc/remove_from_squad(datum/human_ai_brain/removing) + if(removing == squad_leader) + set_squad_leader(null) + removing.remove_current_order() + removing.squad_id = null + removing.is_squad_leader = FALSE + ai_in_squad -= removing + UnregisterSignal(removing?.tied_human, COMSIG_MOB_DEATH) + UnregisterSignal(removing, COMSIG_PARENT_QDELETING) + +/datum/human_ai_squad/proc/set_current_order(datum/ai_order/order) + current_order = order + RegisterSignal(order, COMSIG_PARENT_QDELETING, PROC_REF(on_order_delete)) + for(var/datum/human_ai_brain/brain as anything in ai_in_squad) + brain.set_current_order(order) + +/datum/human_ai_squad/proc/remove_current_order() + UnregisterSignal(current_order, COMSIG_PARENT_QDELETING) + current_order = null + for(var/datum/human_ai_brain/brain as anything in ai_in_squad) + brain.remove_current_order() + +/datum/human_ai_squad/proc/set_squad_leader(datum/human_ai_brain/new_leader) + if(squad_leader) + squad_leader.is_squad_leader = FALSE + squad_leader = new_leader + if(squad_leader) + new_leader.is_squad_leader = TRUE + +/datum/human_ai_squad/proc/on_squad_member_death(mob/living/carbon/human/dead_mob) + SIGNAL_HANDLER + + var/datum/human_ai_brain/brain = dead_mob.get_ai_brain() + if(brain && (squad_leader == brain)) + set_squad_leader(null) + + for(var/datum/human_ai_brain/squaddie as anything in ai_in_squad) + if(squaddie?.tied_human.client) + continue + + if(squaddie.tied_human.is_mob_incapacitated()) + continue + + squaddie.on_squad_member_death(dead_mob) + +/datum/human_ai_squad/proc/on_squad_member_delete(datum/human_ai_brain/deleting) + SIGNAL_HANDLER + + remove_from_squad(deleting) + +/datum/human_ai_squad/proc/on_order_delete(datum/source, force) + SIGNAL_HANDLER + remove_current_order() + +/datum/human_ai_brain + /// Numeric ID of the squad this AI is in, if any + var/squad_id + var/is_squad_leader = FALSE + +/datum/human_ai_brain/proc/add_to_squad(new_id) + if(isnull(new_id) || (new_id == squad_id)) + return + + if(!("[new_id]" in SShuman_ai.squad_id_dict)) + return + + var/datum/human_ai_squad/squad = SShuman_ai.squad_id_dict["[new_id]"] + squad.add_to_squad(src) diff --git a/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm new file mode 100644 index 00000000000..0e7bac05309 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/brain/ai_brain_targeting.dm @@ -0,0 +1,161 @@ +#define EXTRA_CHECK_DISTANCE_MULTIPLIER 0.20 + +/datum/human_ai_brain + /// At how far out the AI can see cloaked enemies + var/cloak_visible_range = 3 + /// Ref to the currently focused (and shooting at) target + var/mob/living/current_target + /// Last turf our target was seen at + var/turf/target_turf + /// Ref to the last turf that the AI shot at + var/turf/shot_at + /// If TRUE, the AI will throw grenades at enemies who enter cover + var/grenading_allowed = TRUE + /// If TRUE, we care about the target being in view after shooting at them. If not, then we only do a line check instead + var/requires_vision = TRUE + + COOLDOWN_DECLARE(fire_offscreen) + +/datum/human_ai_brain/proc/get_target() + var/list/viable_targets = list() + var/atom/movable/closest_target + var/smallest_distance = INFINITY + + /// FOV dirs for if our target is out of base world.view range + var/list/dir_cone = reverse_nearby_direction(reverse_direction(tied_human.dir)) + var/rear_view_penalty = scope_vision ? view_distance / 7 - 1 : 0 + + for(var/mob/living/carbon/potential_target as anything in GLOB.alive_mob_list) + if(!istype(potential_target)) + continue + + if(tied_human.z != potential_target.z) + continue + + if(!can_target(potential_target)) + continue + + if(!(tied_human in viewers(view_distance, potential_target))) + continue + + var/distance = get_dist(tied_human, potential_target) + if(distance > view_distance) + continue + + if(scope_vision && (distance > 7) && !(get_dir(tied_human, potential_target) in dir_cone)) + continue + + var/rear_view_check = scope_vision && (get_dir(tied_human, potential_target) in reverse_nearby_direction(tied_human.dir)) + if(rear_view_check && (distance > view_distance - rear_view_penalty)) + continue + + viable_targets += potential_target + + if(smallest_distance <= distance) + continue + + closest_target = potential_target + smallest_distance = distance + + for(var/obj/vehicle/multitile/potential_vehicle_target as anything in GLOB.all_multi_vehicles) + if(tied_human.z != potential_vehicle_target.z) //todo: make this work + continue + + if(!(tied_human in viewers(view_distance, potential_vehicle_target))) + continue + + var/distance = get_dist(tied_human, potential_vehicle_target) + + // Vehicles are big and lousy, no need to consider our rear view penalty + if(distance > view_distance) + continue + + if(scope_vision && (distance > 7) && !(get_dir(tied_human, potential_vehicle_target) in dir_cone)) + continue + + if(potential_vehicle_target.health <= 0) + continue + + if(potential_vehicle_target.vehicle_faction == tied_human.faction) + continue + + viable_targets += potential_vehicle_target + + if(smallest_distance <= distance) + continue + + closest_target = potential_vehicle_target + smallest_distance = distance + + for(var/obj/structure/machinery/defenses/potential_defense_target as anything in GLOB.all_active_defenses) + if(tied_human.z != potential_defense_target.z) + continue + + if(!(tied_human in viewers(view_distance, potential_defense_target))) + continue + + var/distance = get_dist(tied_human, potential_defense_target) + + // Let's just not rear check a loud ass CLANK CLANK CLANK servo sentry + if(distance > view_distance) + continue + + if(scope_vision && (distance > 7) && !(get_dir(tied_human, potential_defense_target) in dir_cone)) + continue + + viable_targets += potential_defense_target + + if(smallest_distance <= distance) + continue + + closest_target = potential_defense_target + smallest_distance = distance + + var/extra_check_distance = round(smallest_distance * EXTRA_CHECK_DISTANCE_MULTIPLIER) + + if(extra_check_distance < 1) + return closest_target + + var/list/extra_checked = orange(extra_check_distance, closest_target) + + var/list/final_targets = extra_checked & viable_targets + + return length(final_targets) ? pick(final_targets) : closest_target + + +/datum/human_ai_brain/proc/can_target(mob/living/carbon/target) + if(!istype(target)) + return FALSE + + if(target.stat == DEAD) + return FALSE + + if(!shoot_to_kill && (target.stat == UNCONSCIOUS || (locate(/datum/effects/crit) in target.effects_list))) + return FALSE + + if(faction_check(target)) + return FALSE + + if(HAS_TRAIT(target, TRAIT_CLOAKED) && get_dist(tied_human, target) > cloak_visible_range) + return FALSE + + return TRUE + +/datum/human_ai_brain/proc/friendly_check() + var/list/turf_list = get_line(get_turf(tied_human), get_turf(current_target)) + for(var/turf/tile in turf_list) + if(istype(tile, /turf/closed)) + return TRUE + + for(var/mob/living/carbon/human/possible_friendly in tile) + if(tied_human == possible_friendly) + continue + + if(possible_friendly.body_position == LYING_DOWN) + continue + + if(faction_check(possible_friendly)) + return FALSE + return TRUE + +#undef EXTRA_CHECK_DISTANCE_MULTIPLIER diff --git a/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm b/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm new file mode 100644 index 00000000000..3fa21daca70 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/faction_management_panel.dm @@ -0,0 +1,145 @@ +/datum/human_faction_management_menu + +/datum/human_faction_management_menu/New() + +/datum/human_faction_management_menu/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "HumanFactionManager") + ui.open() + +/datum/human_faction_management_menu/ui_state(mob/user) + return GLOB.admin_state + +/datum/human_faction_management_menu/ui_data(mob/user) + var/list/data = list() + + data["datumless_factions"] = FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH + + data["factions"] = list() + for(var/faction_name in SShuman_ai.human_ai_factions) + var/datum/human_ai_faction/ai_faction = SShuman_ai.human_ai_factions[faction_name] + data["factions"] += list(list( + "name" = ai_faction.faction, + "shoot_to_kill" = ai_faction.get_shoot_to_kill(), + "friendly_factions" = english_list(ai_faction.get_friendly_factions()), + "neutral_factions" = english_list(ai_faction.get_neutral_factions()), + "ref" = REF(ai_faction), + )) + data["datumless_factions"] -= ai_faction.faction + + return data + +/datum/human_faction_management_menu/ui_static_data(mob/user) + var/list/data = list() + + data["all_factions"] = FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH + + return data + +/datum/human_faction_management_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return + + switch(action) + if("create_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(gotten_faction in SShuman_ai.human_ai_factions) + return + + var/datum/human_ai_faction/new_faction = new() + new_faction.faction = gotten_faction + SShuman_ai.human_ai_factions[new_faction.faction] = new_faction + return TRUE + + if("set_shoot_to_kill") + if(!params["new_value"] || !params["faction_name"]) + return + + var/gotten_faction = params["faction_name"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + faction_obj.set_shoot_to_kill(text2num(params["new_value"])) + return TRUE + + if("remove_neutral_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Remove which faction being neutral to [gotten_faction]?", "Remove Neutral Faction", faction_obj.get_neutral_factions()) + if(!gotten_input) + return + + faction_obj.remove_neutral_faction(gotten_input) + return TRUE + + if("remove_friendly_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Remove which faction being friendly to [gotten_faction]?", "Remove Friendly Faction", faction_obj.get_friendly_factions()) + if(!gotten_input) + return + + faction_obj.remove_friendly_faction(gotten_input) + return TRUE + + if("add_neutral_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Set which faction being neutral to [gotten_faction]?", "Add Neutral Faction", (FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH) - faction_obj.get_neutral_factions() - faction_obj.faction) + if(!gotten_input) + return + + faction_obj.add_neutral_faction(gotten_input) + + if("add_friendly_faction") + if(!params["faction"]) + return + + var/gotten_faction = params["faction"] + if(!(gotten_faction in SShuman_ai.human_ai_factions)) + return + + var/datum/human_ai_faction/faction_obj = SShuman_ai.human_ai_factions[gotten_faction] + var/gotten_input = tgui_input_list(ui.user, "Set which faction being friendly to [gotten_faction]?", "Add Friendly Faction", (FACTION_LIST_HUMANOID + FACTION_LIST_XENOMORPH) - faction_obj.get_friendly_factions() - faction_obj.faction) + if(!gotten_input) + return + + faction_obj.add_friendly_faction(gotten_input) + +/client/proc/open_human_faction_management_panel() + set name = "Human Faction Management Panel" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + if(human_faction_menu) + human_faction_menu.tgui_interact(mob) + return + + human_faction_menu = new /datum/human_faction_management_menu(src) + human_faction_menu.tgui_interact(mob) diff --git a/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm new file mode 100644 index 00000000000..87527c00c06 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/firearm_appraisal.dm @@ -0,0 +1,198 @@ +GLOBAL_LIST_INIT_TYPED(firearm_appraisals, /datum/firearm_appraisal, build_firearm_appraisal_list()) + +/proc/build_firearm_appraisal_list() + . = list() + for(var/type in subtypesof(/datum/firearm_appraisal)) + . += new type + + +/datum/firearm_appraisal + /// Minimum engagement range with weapon type + var/minimum_range = 2 + /// Optimal engagement range, try to stay at this distance + var/optimal_range = 6 + /// Maximum engagement range, stop firing at this distance + var/maximum_range = 16 + /// How many rounds to fire in 1 burst at most + var/burst_amount_max = 8 + /// List of types that set the human AI to this appraisal type + var/list/gun_types = list() + /// If TRUE, this gun is disposable and isn't worth trying to reload + var/disposable = FALSE + +/// List of things we do before beginning to spray bullets based off weapon type +/datum/firearm_appraisal/proc/before_fire(obj/item/weapon/gun/firearm, mob/living/carbon/user, datum/human_ai_brain/AI) + SHOULD_CALL_PARENT(TRUE) // Every weapon may be twohanded or have safety + set waitfor = FALSE + + AI.ensure_primary_hand(firearm) + if((firearm.flags_item & TWOHANDED) && !(firearm.flags_item & WIELDED)) + AI.wield_primary_sleep() + + if(firearm.flags_gun_features & GUN_TRIGGER_SAFETY) + firearm.flags_gun_features ^= GUN_TRIGGER_SAFETY + firearm.gun_safety_handle(user) + +/// Reload sequence per weapon type, override as needed +/datum/firearm_appraisal/proc/do_reload(obj/item/weapon/gun/firearm, obj/item/ammo_magazine/mag, mob/living/carbon/user, datum/human_ai_brain/AI) + AI.unholster_primary() + AI.ensure_primary_hand(firearm) + firearm.unwield(user) + sleep(AI.short_action_delay * AI.action_delay_mult) + if(!(firearm?.flags_gun_features & GUN_INTERNAL_MAG) && firearm?.current_mag) + firearm?.unload(user, FALSE, TRUE, FALSE) + user.swap_hand() + sleep(AI.micro_action_delay * AI.action_delay_mult) + AI.equip_item_from_equipment_map(HUMAN_AI_AMMUNITION, mag) + sleep(AI.short_action_delay * AI.action_delay_mult) + if(istype(mag, /obj/item/ammo_magazine/handful)) + for(var/i in 1 to mag.current_rounds) + firearm?.attackby(mag, user) + sleep(AI.micro_action_delay * AI.action_delay_mult) + if(!QDELETED(mag) && (mag.current_rounds > 0)) + var/storage_slot = AI.storage_has_room(mag) + if(storage_slot) + AI.store_item(mag, storage_slot, HUMAN_AI_AMMUNITION) + else + user.drop_held_item(mag) + else + firearm?.attackby(mag, user) + sleep(AI.short_action_delay * AI.action_delay_mult) + user.swap_hand() + AI.wield_primary_sleep() + +/datum/firearm_appraisal/sniper + optimal_range = 7 + maximum_range = 30 + burst_amount_max = 1 + gun_types = list( + /obj/item/weapon/gun/rifle/sniper, + ) + +/datum/firearm_appraisal/rifle + burst_amount_max = 8 + gun_types = list( + /obj/item/weapon/gun/rifle, + ) + +/datum/firearm_appraisal/smartgun + burst_amount_max = 18 + gun_types = list( + /obj/item/weapon/gun/smartgun, + ) + +/datum/firearm_appraisal/smartgun/do_reload(obj/item/weapon/gun/firearm, obj/item/ammo_magazine/mag, mob/living/carbon/user, datum/human_ai_brain/AI) + AI.unholster_primary() + AI.ensure_primary_hand(firearm) + firearm.unwield(user) + user.swap_hand() + firearm.clicked(user, list("alt" = TRUE)) + sleep(AI.short_action_delay * AI.action_delay_mult) + user.swap_hand() + if(!(firearm?.flags_gun_features & GUN_INTERNAL_MAG) && firearm?.current_mag) + firearm?.unload(user, FALSE, TRUE, FALSE) + user.swap_hand() + sleep(AI.micro_action_delay * AI.action_delay_mult) + AI.equip_item_from_equipment_map(HUMAN_AI_AMMUNITION, mag) + sleep(AI.short_action_delay * AI.action_delay_mult) + firearm?.attackby(mag, user) + sleep(AI.short_action_delay * AI.action_delay_mult) + firearm.clicked(user, list("alt" = TRUE)) + sleep(AI.short_action_delay * AI.action_delay_mult) + user.swap_hand() + AI.wield_primary_sleep() + +/datum/firearm_appraisal/smg + burst_amount_max = 10 + minimum_range = 1 + optimal_range = 5 + maximum_range = 10 + gun_types = list( + /obj/item/weapon/gun/smg, + ) + +/datum/firearm_appraisal/shotgun_db + burst_amount_max = 2 + minimum_range = 1 + optimal_range = 1 + maximum_range = 3 + gun_types = list( + /obj/item/weapon/gun/shotgun/double, + ) + +/datum/firearm_appraisal/shotgun_db/do_reload(obj/item/weapon/gun/firearm, obj/item/ammo_magazine/mag, mob/living/carbon/user, datum/human_ai_brain/AI) + AI.unholster_primary() + AI.ensure_primary_hand(firearm) + firearm.unwield(user) + firearm.unique_action() + user.swap_hand() + sleep(AI.short_action_delay * AI.action_delay_mult) + AI.equip_item_from_equipment_map(HUMAN_AI_AMMUNITION, mag) + sleep(AI.short_action_delay * AI.action_delay_mult) + firearm.attackby(mag, user) + sleep(AI.micro_action_delay * AI.action_delay_mult) + firearm.attackby(mag, user) + if(!QDELETED(mag)) + var/storage_spot = AI.storage_has_room(mag) + if(storage_spot) + sleep(AI.micro_action_delay * AI.action_delay_mult) + AI.store_item(mag, storage_spot, HUMAN_AI_AMMUNITION) + sleep(AI.short_action_delay * AI.action_delay_mult) + user.swap_hand() + firearm.unique_action() + AI.wield_primary_sleep() + +/datum/firearm_appraisal/shotgun + burst_amount_max = 2 + minimum_range = 1 + optimal_range = 1 // point-blank our beloved + maximum_range = 3 + gun_types = list( + /obj/item/weapon/gun/shotgun, + ) + +/datum/firearm_appraisal/shotgun/before_fire(obj/item/weapon/gun/shotgun/firearm, mob/living/carbon/user, datum/human_ai_brain/AI) + . = ..() + if(firearm.in_chamber) + return + firearm.unique_action(user) + +/datum/firearm_appraisal/boltaction + optimal_range = 7 + maximum_range = 30 + burst_amount_max = 1 + gun_types = list( + /obj/item/weapon/gun/boltaction, + ) + +/datum/firearm_appraisal/boltaction/before_fire(obj/item/weapon/gun/boltaction/firearm, mob/living/carbon/user, datum/human_ai_brain/AI) + . = ..() + if(firearm.in_chamber) + return + firearm.unique_action(user) + firearm.recent_cycle = world.time - firearm.bolt_delay + firearm.unique_action(user) + firearm.recent_cycle = world.time - firearm.bolt_delay + +/datum/firearm_appraisal/flamer + burst_amount_max = 1 + minimum_range = 5 // To not try and walk into our flames in tight spaces + optimal_range = 5 + maximum_range = 5 + gun_types = list( + /obj/item/weapon/gun/flamer, + ) + +/datum/firearm_appraisal/rpg + minimum_range = 5 + optimal_range = 6 + gun_types = list( + /obj/item/weapon/gun/launcher/rocket/anti_tank/disposable, + ) + disposable = TRUE + +/datum/firearm_appraisal/rpg/multi_use + gun_types = list( + /obj/item/weapon/gun/launcher/rocket, + ) + disposable = FALSE diff --git a/code/modules/mob/living/carbon/human/ai/fortify_room.dm b/code/modules/mob/living/carbon/human/ai/fortify_room.dm new file mode 100644 index 00000000000..130c076e174 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/fortify_room.dm @@ -0,0 +1,67 @@ +/client/proc/fortify_room() + set name = "Fortify Room" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/list/turf_list = list() + var/retval + + switch(tgui_input_list(mob, "How fortified should this be?", "Fortification Level", list("Wood", "Sandbag", "Sandbag (Wired)", "Metal", "Metal (Wired)", "Plasteel", "Plasteel (Wired)"))) + if("Wood") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/wooden, null) + if("Sandbag") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/sandbags/full, null) + if("Sandbag (Wired)") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/sandbags/wired, null) + if("Metal") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal, /obj/structure/barricade/plasteel/metal) + if("Metal (Wired)") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/wired, /obj/structure/barricade/plasteel/metal/wired) + if("Plasteel") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/plasteel, /obj/structure/barricade/plasteel) + if("Plasteel (Wired)") + retval = recursive_turf_room_fortify(get_turf(mob), turf_list, /obj/structure/barricade/metal/plasteel/wired, /obj/structure/barricade/plasteel/wired) + + if(retval) + to_chat(src, SPAN_NOTICE("Room fortified. Tiles scanned: [length(turf_list)].")) + else + to_chat(src, SPAN_NOTICE("Room too large to fully fortify. Capped at [length(turf_list)].")) + +/proc/recursive_turf_room_fortify(turf/scan_turf, list/turf_list, cade_type, folding_cade_type) + if(length(turf_list) > 195) // We're choosing 195 because 200 is the BYOND recursion limit so we're just playing it safe + return FALSE // abort if the room is too large + if(istype(scan_turf, /turf/closed)) + return TRUE // abort if we're a wall + if(scan_turf in turf_list) + return TRUE // abort if we've already been scanned + if((locate(/obj/structure/machinery/door) in scan_turf) || (locate(/obj/structure/window_frame) in scan_turf) || (locate(/obj/structure/window) in scan_turf)) + return TRUE // abort if there's a door or window here + turf_list += scan_turf + for(var/cardinal in GLOB.cardinals) + var/turf/nearby_turf = get_step(scan_turf, cardinal) + if(!nearby_turf) + continue + + if((locate(/obj/structure/window_frame) in nearby_turf) || (locate(/obj/structure/window) in nearby_turf)) + for(var/obj/structure/barricade/existing_cade in scan_turf) + if(existing_cade.dir == cardinal) + goto next_recurse + + var/obj/structure/barricade/cade = new cade_type(scan_turf) + cade.setDir(cardinal) + + if(folding_cade_type && (locate(/obj/structure/machinery/door) in nearby_turf)) + for(var/obj/structure/barricade/existing_cade in scan_turf) + if(existing_cade.dir == cardinal) + goto next_recurse + + var/obj/structure/barricade/plasteel/cade = new folding_cade_type(scan_turf) + cade.setDir(cardinal) + cade.open(cade) // this closes it + + next_recurse: + if(!recursive_turf_room_fortify(nearby_turf, turf_list, cade_type, folding_cade_type)) + return FALSE + return TRUE diff --git a/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm b/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm new file mode 100644 index 00000000000..d4da6c67e73 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/human_ai_interaction.dm @@ -0,0 +1,265 @@ +/atom/proc/human_ai_obstacle(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain, direction, turf/target) + if(get_turf(src) == target) + return 0 + return INFINITY + +/atom/proc/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + ai_human.do_click(src, "", list()) + return TRUE + + +///////////////////////////// +// OBJECTS // +///////////////////////////// +/obj/structure/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + if(!density) + return 0 + + if(!climbable) + return + + return OBJECT_PENALTY + +/obj/structure/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + if(climbable && !human_ai.action_busy) + do_climb(human_ai) + + return ..() + +/obj/structure/barricade/plasteel/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + if(!closed) // this means it's closed + ai_human.do_click(src, "", list()) + + return TRUE + +///////////////////////////// +// MINERAL DOOR // +///////////////////////////// +/obj/structure/mineral_door/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(!brain.primary_weapon) + return INFINITY + + return DOOR_PENALTY + +/obj/structure/mineral_door/resin/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + //ADD_ONGOING_ACTION(brain, AI_ACTION_MELEE_ATOM, src) + return ..() + + +///////////////////////////// +// PLATFORMS // +///////////////////////////// +/obj/structure/platform/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return DOOR_PENALTY + + +///////////////////////////// +// PODDDOORS // +///////////////////////////// +/obj/structure/machinery/door/poddoor/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + if(!(stat & NOPOWER)) + return INFINITY + + if(density && !operating && !unacidable && brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR)) + return DOOR_PENALTY + + return INFINITY + +/obj/structure/machinery/door/airlock/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + if(locked || welded || isElectrified()) + return + + . = ..() + + if(!(stat & NOPOWER) || !brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR)) + return + + brain.holster_primary() + var/obj/item/crowbar = brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR) + brain.equip_item_from_equipment_map(HUMAN_AI_TOOLS, crowbar) + attackby(crowbar, ai_human) + brain.store_item(crowbar, brain.storage_has_room(crowbar), HUMAN_AI_TOOLS) + +///////////////////////////// +// AIRLOCK // +///////////////////////////// +/obj/structure/machinery/door/airlock/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + if(locked || welded || isElectrified()) + return LOCKED_DOOR_PENALTY + + return DOOR_PENALTY + +/obj/structure/machinery/door/airlock/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + if(locked || welded || isElectrified()) + return + + . = ..() + + if(!(stat & NOPOWER)) + return + + brain.holster_primary() + var/obj/item/crowbar = brain.get_tool_from_equipment_map(TRAIT_TOOL_CROWBAR) + brain.equip_item_from_equipment_map(HUMAN_AI_TOOLS, crowbar) + attackby(crowbar, ai_human) + brain.store_item(crowbar, brain.storage_has_room(crowbar), HUMAN_AI_TOOLS) + +///////////////////////////// +// HUMANS // +///////////////////////////// +/mob/living/carbon/human/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(status_flags & GODMODE) + return ..() + + return HUMAN_PENALTY + +/mob/living/carbon/human/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + if(brain.faction_check(src)) + var/try_intent = pick(INTENT_DISARM, INTENT_HARM, INTENT_HELP) + human_ai.a_intent = try_intent + a_intent = try_intent + return TRUE + return ..() + +///////////////////////////// +// XENOS // +///////////////////////////// +/mob/living/carbon/xenomorph/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return XENO_PENALTY + +///////////////////////////// +// VEHICLES // +///////////////////////////// +/obj/vehicle/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return VEHICLE_PENALTY + + +///////////////////////////// +// SENTRY // +///////////////////////////// +/obj/structure/machinery/defenses/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return SENTRY_PENALTY + + +///////////////////////////// +// WINDOW FRAME // +///////////////////////////// +/*obj/structure/window_frame/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(buildstacktype && brain.get_tool_from_equipment_map(TRAIT_TOOL_WRENCH)) + return ..() + return WINDOW_FRAME_PENALTY*/ + + +///////////////////////////// +// BARRICADES // +///////////////////////////// +/obj/structure/barricade/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return BARRICADE_PENALTY + +/obj/structure/barricade/plasteel/human_ai_act(mob/living/carbon/human/ai_human, datum/human_ai_brain/brain) + . = ..() + if(!closed) + close(src) + +/obj/structure/barricade/handrail/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return DOOR_PENALTY + + +///////////////////////////// +// FIRE // +///////////////////////////// +/obj/flamer_fire/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/braineno, direction, turf/target) + . = ..() + if(!.) + return + + if(human_ai.on_fire) + return FIRE_PENALTY + + return INFINITY // STOP. TOUCHING. THE FLAMES! + +///////////////////////////// +// WALLS // +///////////////////////////// +/turf/closed/wall/resin/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/braineno, direction, turf/target) + . = ..() + if(!.) + return + + return WALL_PENALTY + + +///////////////////////////// +// FLOOR // +///////////////////////////// +/* + Sometimes open turfs are passed back as obstacles due to platforms and such, + generally it's fast so very slight penalty mainly for handling subtypes properly +*/ +/turf/open/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return OPEN_TURF_PENALTY + +/turf/open/human_ai_act(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain) + return FALSE + +/turf/open/space/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(!.) + return + + return INFINITY + + +///////////////////////////// +// RIVER // +///////////////////////////// +/turf/open/gm/river/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + . = ..() + if(. && !covered) + . += base_river_slowdown + +/turf/open/gm/river/desert/human_ai_obstacle(mob/living/carbon/human/human_ai, datum/human_ai_brain/brain, direction, turf/target) + if(toxic && !covered) + return FIRE_PENALTY + + return ..() diff --git a/code/modules/mob/living/carbon/human/ai/order_datums/order.dm b/code/modules/mob/living/carbon/human/ai/order_datums/order.dm new file mode 100644 index 00000000000..ead88b4a447 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/order_datums/order.dm @@ -0,0 +1,16 @@ +/datum/ai_order + var/name = "You shouldn't see this" + var/desc = "" + var/list/datum/human_ai_brain/brains = list() + var/should_display = TRUE + +/datum/ai_order/New(list/arguments) + . = ..() + SShuman_ai.existing_orders += src + +/datum/ai_order/Destroy(force, ...) + SShuman_ai.existing_orders -= src + return ..() + +/datum/ai_order/proc/tgui_data() + return list() diff --git a/code/modules/mob/living/carbon/human/ai/order_datums/order_patrol.dm b/code/modules/mob/living/carbon/human/ai/order_datums/order_patrol.dm new file mode 100644 index 00000000000..665b5763c99 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/order_datums/order_patrol.dm @@ -0,0 +1,72 @@ +/datum/ai_order/patrol + name = "Patrol Waypoints" + var/turf/current_waypoint + var/list/waypoints = list() + + var/time_at_waypoint = 10 SECONDS + var/current_waypoint_index = 1 + + var/increment = 1 + var/waiting = FALSE + +/datum/ai_order/patrol/New(list/arguments) + . = ..() + waypoints = arguments[1] + current_waypoint = waypoints[1] + + time_at_waypoint = length(arguments) > 1 ? arguments[2] : 10 SECONDS + +/datum/ai_order/patrol/tgui_data() + return list( + list( + "waypoint_amount", + "waiting", + "desc", + ), + list( + length(waypoints), + waiting, + desc, + ) + ) + +/datum/ai_order/patrol/proc/set_next_waypoint() + var/patrol_length = length(waypoints) + if(current_waypoint_index <= 1) + increment = 1 + else if(current_waypoint_index >= patrol_length) + increment = -1 + current_waypoint_index += increment + current_waypoint = waypoints[current_waypoint_index] + waiting = FALSE + +/datum/admins/proc/create_human_ai_patrol() + set name = "Create Human AI Patrol Waypoints" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + var/list/turf/waypoint_list = list() + while(TRUE) + if(tgui_input_list(usr, "Press Enter to save the turf you are on to the patrol datum. Press Cancel to finalize.", "Save Turf", list("Enter", "Cancel")) == "Enter") + var/turf/user_turf = get_turf(usr) + var/dist = length(waypoint_list) ? get_dist(waypoint_list[length(waypoint_list)], user_turf) : 0 + if(length(waypoint_list) && (dist > HUMAN_AI_MAX_PATHFINDING_RANGE)) + to_chat(usr, SPAN_WARNING("This waypoint is too far from the previous one. Maximum distance is [HUMAN_AI_MAX_PATHFINDING_RANGE] while this node's was [dist].")) + continue + waypoint_list += user_turf + continue + break + + if(length(waypoint_list) <= 0) + return + + var/description = tgui_input_text(usr, "Input a description of the patrol.", "Description") + if(!description) + return + + var/datum/ai_order/patrol/order = new(list(waypoint_list, 10 SECONDS)) + order.desc = description + + to_chat(usr, SPAN_NOTICE("Patrol order has been created.")) diff --git a/code/modules/mob/living/carbon/human/ai/quick_order.dm b/code/modules/mob/living/carbon/human/ai/quick_order.dm new file mode 100644 index 00000000000..68f799fbf03 --- /dev/null +++ b/code/modules/mob/living/carbon/human/ai/quick_order.dm @@ -0,0 +1,103 @@ +#define AREASELECT_CORNERA "corner A" +#define AREASELECT_CORNERB "corner B" + +/datum/human_ai_quick_order + var/client/holder + var/list/preview = list() + var/turf/cornerA + var/turf/cornerB + var/list/ai_humans_selected = list() + +/datum/human_ai_quick_order/Destroy(force, ...) + holder.click_intercept = src + cornerA = null + cornerB = null + holder = null + holder.images -= preview + preview.Cut() + return ..() + +/datum/human_ai_quick_order/proc/deselect_region() + set waitfor = FALSE + + cornerA = null + cornerB = null + + sleep(0.5 SECONDS) + holder.images -= preview + preview.Cut() + +/datum/human_ai_quick_order/proc/InterceptClickOn(mob/user, params, atom/object) + var/list/modifiers = params2list(params) + if(LAZYACCESS(modifiers, ALT_CLICK)) + if(!length(ai_humans_selected)) + to_chat(holder, SPAN_BOLDNOTICE("You need to have an area selected first.")) + return + + for(var/datum/human_ai_brain/brain as anything in ai_humans_selected) + brain.target_turf = get_turf(object) + + to_chat(holder, SPAN_BOLDNOTICE("Order sent.")) + deselect_region() + + else if(LAZYACCESS(modifiers, LEFT_CLICK)) + if(!cornerA) + cornerA = select_tile(get_turf(object), AREASELECT_CORNERA) + return + if(!cornerB) + cornerB = select_tile(get_turf(object), AREASELECT_CORNERB) + handle_selected_area(params) + deselect_region() + return + else + to_chat(holder, SPAN_NOTICE("Region selection canceled!")) + deselect_region() + ai_humans_selected.Cut() + +/datum/human_ai_quick_order/proc/select_tile(turf/T, corner_to_select) + var/overlaystate + holder.images -= preview + switch(corner_to_select) + if(AREASELECT_CORNERA) + overlaystate = "greenOverlay" + if(AREASELECT_CORNERB) + overlaystate = "blueOverlay" + + var/image/I = image('icons/turf/overlays.dmi', T, overlaystate) + I.plane = ABOVE_LIGHTING_PLANE + preview += I + holder.images += preview + return T + +/datum/human_ai_quick_order/proc/handle_selected_area(params) + if(!cornerA || !cornerB) + return + + ai_humans_selected.Cut() + for(var/turf/block_turf as anything in block(cornerA.x, cornerA.y, cornerA.z, cornerB.x, cornerB.y, cornerB.z)) + for(var/mob/living/carbon/human/maybe_ai in block_turf.contents) + var/datum/human_ai_brain/brain = maybe_ai.get_ai_brain() + if(brain) + ai_humans_selected += brain + + to_chat(holder, SPAN_BOLDNOTICE("[length(ai_humans_selected)] AI selected in region.")) + +/client/proc/quick_order_ai_approach() + set name = "Quick Order: Approach" + set category = "Game Master.HumanAI" + + if(!check_rights(R_DEBUG)) + return + + if(istype(click_intercept, /datum/human_ai_quick_order)) + QDEL_NULL(click_intercept) + to_chat(src, SPAN_BOLDNOTICE("Quick ordering stopped.")) + return + + var/datum/human_ai_quick_order/order_datum = new + order_datum.holder = src + click_intercept = order_datum + to_chat(src, SPAN_BOLDNOTICE("Left click two corners to select all AI in the area. Then, alt-click on where you would like them to go. To stop quick ordering, press the verb again.")) + +#undef AREASELECT_CORNERA +#undef AREASELECT_CORNERB diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 5eadbd57c7e..2660cabf52f 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1,4 +1,4 @@ -/mob/living/carbon/human/Initialize(mapload, new_species = null) +/mob/living/carbon/human/Initialize(mapload, new_species = null, ai = FALSE) blood_type = pick(7;"O-", 38;"O+", 6;"A-", 34;"A+", 2;"B-", 9;"B+", 1;"AB-", 3;"AB+") GLOB.human_mob_list += src GLOB.alive_human_list += src @@ -1066,6 +1066,8 @@ default_lighting_alpha = species.default_lighting_alpha update_sight() + SEND_SIGNAL(src, COMSIG_HUMAN_SET_SPECIES, new_species) + if(species) return TRUE else diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm index dbc30b964ae..b01a72f4fb3 100644 --- a/code/modules/mob/living/carbon/human/inventory.dm +++ b/code/modules/mob/living/carbon/human/inventory.dm @@ -102,6 +102,12 @@ . = ..() /mob/living/carbon/human/u_equip(obj/item/I, atom/newloc, nomoveupdate, force) + var/slot + if(I) + if(I == back) + slot = SLOT_BACK + else if(I == wear_mask) + slot = SLOT_FACE . = ..() if(!. || !I) return FALSE @@ -119,6 +125,7 @@ if(I.flags_inv_hide & HIDEJUMPSUIT) update_inv_w_uniform() update_inv_wear_suit() + slot = SLOT_OCLOTHING else if(I == w_uniform) if(r_store) drop_inv_item_on_ground(r_store) @@ -129,6 +136,7 @@ w_uniform = null update_suit_sensors() update_inv_w_uniform() + slot = SLOT_ICLOTHING else if(I == head) var/updatename = 0 if(head.flags_inv_hide & HIDEFACE) @@ -146,43 +154,54 @@ update_inv_glasses() update_tint() update_inv_head() + slot = SLOT_HEAD else if (I == gloves) gloves = null update_inv_gloves() + slot = SLOT_HANDS else if (I == glasses) glasses = null update_tint() update_glass_vision(I) update_inv_glasses() + slot = SLOT_EYES else if (I == wear_l_ear) wear_l_ear = null update_inv_ears() + slot = SLOT_EAR else if (I == wear_r_ear) wear_r_ear = null update_inv_ears() + slot = SLOT_EAR else if (I == shoes) shoes = null update_inv_shoes() + slot = SLOT_FEET else if (I == belt) belt = null update_inv_belt() + slot = SLOT_WAIST else if (I == wear_id) wear_id = null sec_hud_set_ID() hud_set_squad() update_inv_wear_id() name = get_visible_name() + slot = SLOT_ID else if (I == r_store) r_store = null update_inv_pockets() + slot = SLOT_STORE else if (I == l_store) l_store = null update_inv_pockets() + slot = SLOT_STORE else if (I == s_store) s_store = null update_inv_s_store() + slot = SLOT_SUIT_STORE - + SEND_SIGNAL(src, COMSIG_HUMAN_UNEQUIPPED_ITEM, I, slot) /mob/living/carbon/human/wear_mask_update(obj/item/I, equipping) @@ -493,7 +512,7 @@ return ..() /mob/living/carbon/human/proc/get_strip_delay(mob/living/carbon/human/user, mob/living/carbon/human/target) - /// Default delay + /*/// Default delay var/target_delay = HUMAN_STRIP_DELAY /// Multiplier for how quickly the user can strip things. var/user_speed = user.get_skill_duration_multiplier(SKILL_CQC) @@ -506,7 +525,8 @@ target_delay += (target_skills * 2) /// Final result is overall delay * speed multiplier - return target_delay * user_speed + return target_delay * user_speed*/ + return 0 // zonenote /mob/living/carbon/human/drop_inv_item_on_ground(obj/item/I, nomoveupdate, force) remember_dropped_object(I) diff --git a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm index e87de581552..b4bd9c49adc 100644 --- a/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm +++ b/code/modules/mob/living/carbon/xenomorph/ai/xeno_ai.dm @@ -43,7 +43,7 @@ if(distance > max_travel_distance) return - SSxeno_pathfinding.calculate_path(src, P.firer, distance, src, CALLBACK(src, PROC_REF(set_path)), list(src, P.firer)) + SSpathfinding.calculate_path(src, P.firer, distance, src, CALLBACK(src, PROC_REF(set_path)), list(src, P.firer)) /mob/living/carbon/xenomorph/proc/register_ai_action(datum/action/xeno_action/XA) if(XA.owner != src) @@ -165,12 +165,12 @@ return FALSE if((!current_path || (next_path_generation < world.time && current_target_turf != T)) && COOLDOWN_FINISHED(src, no_path_found_cooldown)) - if(!XENO_CALCULATING_PATH(src) || current_target_turf != T) - SSxeno_pathfinding.calculate_path(src, T, max_range, src, CALLBACK(src, PROC_REF(set_path)), list(src, current_target)) + if(!CALCULATING_PATH(src) || current_target_turf != T) + SSpathfinding.calculate_path(src, T, max_range, src, CALLBACK(src, PROC_REF(set_path)), list(src, current_target)) current_target_turf = T next_path_generation = world.time + path_update_period - if(XENO_CALCULATING_PATH(src)) + if(CALCULATING_PATH(src)) return TRUE // No possible path to target. @@ -193,7 +193,7 @@ var/turf/next_turf = current_path[current_path.len] var/list/L = LinkBlocked(src, loc, next_turf, list(src, current_target), TRUE) - L += SSxeno_pathfinding.check_special_blockers(src, next_turf) + L += SSpathfinding.check_special_blockers(src, next_turf) for(var/a in L) var/atom/A = a if(A.xeno_ai_obstacle(src, get_dir(loc, next_turf)) == INFINITY) diff --git a/code/modules/projectiles/ammunition.dm b/code/modules/projectiles/ammunition.dm index e032d3ebbe5..52600e0682b 100644 --- a/code/modules/projectiles/ammunition.dm +++ b/code/modules/projectiles/ammunition.dm @@ -12,6 +12,7 @@ They're all essentially identical when it comes to getting the job done. var/bonus_overlay = null //Sprite pointer in ammo.dmi to an overlay to add to the gun, for extended mags, box mags, and so on flags_atom = FPRINT|CONDUCT flags_equip_slot = SLOT_WAIST + flags_human_ai = AMMUNITION_ITEM matter = list("metal" = 1000) //Low. throwforce = 2 @@ -191,6 +192,12 @@ They're all essentially identical when it comes to getting the job done. default_ammo = source.default_ammo gun_type = source.gun_type +/obj/item/ammo_magazine/ai_can_use(mob/living/carbon/human/user, datum/human_ai_brain/ai_brain) + if(current_rounds <= 0) + return FALSE + + return TRUE + //~Art interjecting here for explosion when using flamer procs. /obj/item/ammo_magazine/flamer_fire_act(damage, datum/cause_data/flame_cause_data) if(current_rounds < 1) @@ -246,6 +253,7 @@ bullets/shells. ~N max_rounds = 5 // For shotguns, though this will be determined by the handful type when generated. flags_atom = FPRINT|CONDUCT flags_magazine = AMMUNITION_HANDFUL + flags_human_ai = NONE attack_speed = 3 // should make reloading less painful /obj/item/ammo_magazine/handful/Initialize(mapload, spawn_empty) diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index 50e735b9181..d26f2cff963 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -1651,7 +1651,7 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed if(flags_gun_features & GUN_AMMO_COUNTER && current_mag) // toggleable spam control. - if(user.client.prefs.toggle_prefs & TOGGLE_AMMO_DISPLAY_TYPE && gun_firemode == GUN_FIREMODE_SEMIAUTO && current_mag.current_rounds % 5 != 0 && current_mag.current_rounds > 15) + if(user.client?.prefs.toggle_prefs & TOGGLE_AMMO_DISPLAY_TYPE && gun_firemode == GUN_FIREMODE_SEMIAUTO && current_mag.current_rounds % 5 != 0 && current_mag.current_rounds > 15) return var/chambered = in_chamber ? TRUE : FALSE to_chat(user, SPAN_DANGER("[current_mag.current_rounds][chambered ? "+1" : ""] / [current_mag.max_rounds] ROUNDS REMAINING")) @@ -2020,3 +2020,8 @@ not all weapons use normal magazines etc. load_into_chamber() itself is designed /// Getter for gun_user /obj/item/weapon/gun/proc/get_gun_user() return gun_user + +/// Getter for target +/obj/item/weapon/gun/proc/get_target() + RETURN_TYPE(/atom) + return target diff --git a/code/modules/projectiles/gun_attachables.dm b/code/modules/projectiles/gun_attachables.dm index 701fb420aa9..28460ea27db 100644 --- a/code/modules/projectiles/gun_attachables.dm +++ b/code/modules/projectiles/gun_attachables.dm @@ -290,6 +290,7 @@ Defined in conflicts.dm of the #defines folder. flags_equip_slot = SLOT_FACE flags_armor_protection = SLOT_FACE flags_item = CAN_DIG_SHRAPNEL + flags_human_ai = MELEE_WEAPON_ITEM attach_icon = "bayonet_a" melee_mod = 20 diff --git a/code/modules/projectiles/gun_helpers.dm b/code/modules/projectiles/gun_helpers.dm index 46f7f68b6f1..e3d6655a0be 100644 --- a/code/modules/projectiles/gun_helpers.dm +++ b/code/modules/projectiles/gun_helpers.dm @@ -157,30 +157,29 @@ DEFINES in setup.dm, referenced here. if(CONFIG_GET(flag/remove_gun_restrictions)) return TRUE //Not if the config removed it. - if(user.mind) - switch(user.job) - if( - "PMC", - "WY Agent", - "Corporate Liaison", - "Event", - "UPP Armsmaster", //this rank is for the Fun - Ivan preset, it allows him to use the PMC guns randomly generated from his backpack - ) return TRUE - switch(user.faction) - if( - FACTION_WY_DEATHSQUAD, - FACTION_PMC, - FACTION_MERCENARY, - FACTION_FREELANCER, - ) return TRUE - - for(var/faction in user.faction_group) - if(faction in FACTION_LIST_WY) - return TRUE - - if(user.faction in FACTION_LIST_WY) + switch(user.job) + if( + "PMC", + "WY Agent", + "Corporate Liaison", + "Event", + "UPP Armsmaster", //this rank is for the Fun - Ivan preset, it allows him to use the PMC guns randomly generated from his backpack + ) return TRUE + switch(user.faction) + if( + FACTION_WY_DEATHSQUAD, + FACTION_PMC, + FACTION_MERCENARY, + FACTION_FREELANCER, + ) return TRUE + + for(var/faction in user.faction_group) + if(faction in FACTION_LIST_WY) return TRUE + if(user.faction in FACTION_LIST_WY) + return TRUE + to_chat(user, SPAN_WARNING("[src] flashes a warning sign indicating unauthorized use!")) // Checks whether there is anything to put your harness diff --git a/code/modules/projectiles/guns/flamer/flamer.dm b/code/modules/projectiles/guns/flamer/flamer.dm index 73ed0a37b54..98c98ad2292 100644 --- a/code/modules/projectiles/guns/flamer/flamer.dm +++ b/code/modules/projectiles/guns/flamer/flamer.dm @@ -149,6 +149,8 @@ unleash_foam(target, user) else unleash_flame(target, user) + current_mag.current_rounds = current_mag.get_ammo_percent() + SEND_SIGNAL(user, COMSIG_MOB_FIRED_GUN, src) return AUTOFIRE_CONTINUE return NONE diff --git a/code/modules/projectiles/magazines/lever_action.dm b/code/modules/projectiles/magazines/lever_action.dm index ac1d57dbd41..a32a49d9260 100644 --- a/code/modules/projectiles/magazines/lever_action.dm +++ b/code/modules/projectiles/magazines/lever_action.dm @@ -92,6 +92,7 @@ Handfuls of lever_action rounds. For spawning directly on mobs in roundstart, ER gun_type = /obj/item/weapon/gun/lever_action handful_state = "lever_action_bullet" transfer_handful_amount = 9 + flags_human_ai = AMMUNITION_ITEM /obj/item/ammo_magazine/handful/lever_action/training name = "handful of blanks (45-70)" diff --git a/code/modules/projectiles/magazines/shotguns.dm b/code/modules/projectiles/magazines/shotguns.dm index 9fe9f0f4404..f47a472bd38 100644 --- a/code/modules/projectiles/magazines/shotguns.dm +++ b/code/modules/projectiles/magazines/shotguns.dm @@ -189,6 +189,7 @@ GLOBAL_LIST_INIT(shotgun_handfuls_12g, list( gun_type = /obj/item/weapon/gun/shotgun handful_state = "slug_shell" transfer_handful_amount = 5 + flags_human_ai = AMMUNITION_ITEM /obj/item/ammo_magazine/handful/shotgun/slug @@ -321,6 +322,7 @@ GLOBAL_LIST_INIT(shotgun_handfuls_12g, list( max_rounds = 8 current_rounds = 8 gun_type = /obj/item/weapon/gun/shotgun/double/cane + flags_human_ai = AMMUNITION_ITEM /obj/item/ammo_magazine/handful/revolver/marksman name = "handful of marksman revolver bullets (.44)" diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 1c04208596a..89dd9f9f543 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -664,8 +664,8 @@ //an object's "projectile_coverage" var indicates the maximum probability of blocking a projectile var/effective_accuracy = P.get_effective_accuracy() - var/distance_limit = 6 //number of tiles needed to max out block probability - var/accuracy_factor = 50 //degree to which accuracy affects probability (if accuracy is 100, probability is unaffected. Lower accuracies will increase block chance) + var/distance_limit = 3 //number of tiles needed to max out block probability + var/accuracy_factor = 45 //degree to which accuracy affects probability (if accuracy is 100, probability is unaffected. Lower accuracies will increase block chance) var/hitchance = min(projectile_coverage, (projectile_coverage * distance/distance_limit) + accuracy_factor * (1 - effective_accuracy/100)) diff --git a/colonialmarines.dme b/colonialmarines.dme index c753d4bf273..35a73a3ce30 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -67,6 +67,7 @@ #include "code\__DEFINES\html.dm" #include "code\__DEFINES\hud.dm" #include "code\__DEFINES\human.dm" +#include "code\__DEFINES\human_ai.dm" #include "code\__DEFINES\job_ru.dm" #include "code\__DEFINES\keybinding.dm" #include "code\__DEFINES\language.dm" @@ -272,6 +273,7 @@ #include "code\controllers\subsystem\garbage.dm" #include "code\controllers\subsystem\hijack.dm" #include "code\controllers\subsystem\human.dm" +#include "code\controllers\subsystem\human_ai.dm" #include "code\controllers\subsystem\inactivity.dm" #include "code\controllers\subsystem\influxdriver.dm" #include "code\controllers\subsystem\influxmcstats.dm" @@ -418,6 +420,7 @@ #include "code\datums\components\disk_reader.dm" #include "code\datums\components\footstep.dm" #include "code\datums\components\healing_reduction.dm" +#include "code\datums\components\human_ai.dm" #include "code\datums\components\id_lock.dm" #include "code\datums\components\iff_fire_prevention.dm" #include "code\datums\components\label.dm" @@ -1982,6 +1985,40 @@ #include "code\modules\mob\living\carbon\human\unarmed_attacks.dm" #include "code\modules\mob\living\carbon\human\update_icons.dm" #include "code\modules\mob\living\carbon\human\whisper.dm" +#include "code\modules\mob\living\carbon\human\ai\ai_equipment.dm" +#include "code\modules\mob\living\carbon\human\ai\ai_management_menu.dm" +#include "code\modules\mob\living\carbon\human\ai\faction_management_panel.dm" +#include "code\modules\mob\living\carbon\human\ai\fortify_room.dm" +#include "code\modules\mob\living\carbon\human\ai\human_ai_interaction.dm" +#include "code\modules\mob\living\carbon\human\ai\quick_order.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\fire_at_target_pb.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\chase_target.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\fire_at_target.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\follow_leader.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\keep_distance.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\patrol_waypoints.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\reload.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\sniper_nest.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\throw_grenade.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\treat_self.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\depricated\melee_atom.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_communication.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_factions.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_squad.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_cover.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\take_cover.dm" +#include "code\modules\mob\living\carbon\human\ai\firearm_appraisal.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\action_datums.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\item_pickup.dm" +#include "code\modules\mob\living\carbon\human\ai\action_datums\throw_back_nade.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_guns.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_health.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_items.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_pathfinding.dm" +#include "code\modules\mob\living\carbon\human\ai\brain\ai_brain_targeting.dm" +#include "code\modules\mob\living\carbon\human\ai\order_datums\order.dm" +#include "code\modules\mob\living\carbon\human\ai\order_datums\order_patrol.dm" #include "code\modules\mob\living\carbon\human\life\handle_breath.dm" #include "code\modules\mob\living\carbon\human\life\handle_chemicals_in_body.dm" #include "code\modules\mob\living\carbon\human\life\handle_disabilities.dm" diff --git a/tgui/packages/tgui/interfaces/HumanAIManager.tsx b/tgui/packages/tgui/interfaces/HumanAIManager.tsx new file mode 100644 index 00000000000..c5a97367414 --- /dev/null +++ b/tgui/packages/tgui/interfaces/HumanAIManager.tsx @@ -0,0 +1,386 @@ +import { useBackend, useLocalState } from '../backend'; +import { Button, Section, Stack, Divider } from '../components'; +import { Window } from '../layouts'; +import { BooleanLike } from 'common/react'; + +type Squad = { + id: number; + order: string; + members: string; + ref: string; + primary_order: string; + squad_leader: string; +}; + +type AIHuman = { + name: string; + health: number; + loc: number[]; + faction: string; + ref: string; + brain_ref: string; + in_combat: BooleanLike; + squad_id: number; + can_assign_squad: BooleanLike; +}; + +type Order = { + name: string; + type: string; + data: any[]; + ref: string; +}; + +type BackendContext = { + orders: Order[]; + ai_humans: AIHuman[]; + squads: Squad[]; +}; + +const AIContext = (props, context) => { + const { data, act } = useBackend(); + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( + 'squad_assignment_mode', + false, + ); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + 'order_assignment_mode', + false, + ); + return ( + +
+
+
+
+
+
+
+
+ +
+ {data.orders.map((order) => ( + + ))} +
+ +
+ {data.ai_humans.map((human) => ( + + ))} +
+ +
+ {data.squads.map((squad) => ( + + ))} +
+
+ ); +}; + +const CreatedOrder = (props) => { + const order: Order = props.order; + const context: BackendContext = props.context; + const { data, act } = useBackend(); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + 'order_assignment_mode', + false, + ); + const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1); + return ( +
+
+ {orderAssignmentMode ? ( + <> +
+
+ ); +}; + +const HumanAIReadout = (props) => { + const human: AIHuman = props.human; + const context: BackendContext = props.context; + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( + 'squad_assignment_mode', + false, + ); + const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1); + const { data, act } = useBackend(); + const gottenSquad: Squad = data.squads[selectedSquad]; + return ( +
+
+
+ {squadAssignmentMode ? ( + <> +
+
+
+ Health: {human.health}%
+ Faction: {human.faction}
+ In Combat: {human.in_combat}
+ Squad #: {human.squad_id}
+ Loc: {human.loc[0]}, {human.loc[1]}, {human.loc[2]} +
+
+ ); +}; + +const SquadReadout = (props) => { + const squad: Squad = props.squad; + const context: BackendContext = props.context; + const { data, act } = useBackend(); + const [squadAssignmentMode, setSquadAssignmentMode] = useLocalState( + 'squad_assignment_mode', + false, + ); + const [selectedSquad, setSelectedSquad] = useLocalState('selected_squad', -1); + const [orderAssignmentMode, setOrderAssignmentMode] = useLocalState( + 'order_assignment_mode', + false, + ); + return ( +
+
+ {squadAssignmentMode || orderAssignmentMode ? ( +
+
+ ); +}; + +export const HumanAIManager = (props) => { + return ( + + + + + + ); +}; diff --git a/tgui/packages/tgui/interfaces/HumanFactionManager.tsx b/tgui/packages/tgui/interfaces/HumanFactionManager.tsx new file mode 100644 index 00000000000..07fbf02c8e1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/HumanFactionManager.tsx @@ -0,0 +1,161 @@ +import { useBackend, useLocalState } from '../backend'; +import { Button, Dropdown, Section, Stack, Divider } from '../components'; +import { Window } from '../layouts'; +import { BooleanLike } from 'common/react'; + +type Faction = { + name: string; + shoot_to_kill: BooleanLike; + friendly_factions: string; + neutral_factions: string; + ref: string; +}; + +type BackendContext = { + factions: Faction[]; + all_factions: string[]; + datumless_factions: string[]; +}; + +const FactionContext = (props, context) => { + const { data, act } = useBackend(); + const [selectedToCreateFaction, setSelectedToCreateFaction] = useLocalState( + 'selected_to_create_faction', + '', + ); + return ( + +
+
+ setSelectedToCreateFaction(value)} + /> +
+
+
+
+ +
+ {data.factions.map((faction) => ( + + ))} +
+
+ ); +}; + +const ExistingFaction = (props) => { + const context: BackendContext = props.context; + const { data, act } = useBackend(); + const faction: Faction = props.faction; + return ( +
+
+ Shoot To Kill: {faction.shoot_to_kill} + + act('set_shoot_to_kill', { + faction_name: faction.name, + new_value: value, + }) + } + /> + Friendly Factions: {faction.friendly_factions} + <> +
+
+ ); +}; + +export const HumanFactionManager = (props) => { + return ( + + + + + + ); +};