From d52ec77972ef34d31b176a2912e5ad8886c162d0 Mon Sep 17 00:00:00 2001 From: Shadowmech88 Date: Sat, 17 Jun 2017 07:35:19 -0500 Subject: [PATCH] Basic Human AI (#15172) * Adds a basic NPC AI for humans, capable of feeding itself when hungry and clothing itself. * Does more stuff. * Adds some more stuff. * Forgot this. --- code/__DEFINES/component_desires.dm | 29 +++ code/__DEFINES/component_signals.dm | 37 +++ code/_onclick/click.dm | 2 +- .../ai/controllers/mob_controller.dm | 7 + code/modules/components/ai/human/brain.dm | 210 ++++++++++++++++++ .../modules/components/ai/human/hand_usage.dm | 32 +++ .../components/ai/human/human_attack.dm | 10 + .../ai/human/human_target_finder.dm | 21 ++ code/modules/mob/living/carbon/human/death.dm | 4 + code/modules/mob/living/carbon/human/human.dm | 16 ++ .../mob/living/carbon/human/human_defines.dm | 2 + code/modules/mob/living/carbon/human/life.dm | 2 + vgstation13.dme | 5 + 13 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 code/__DEFINES/component_desires.dm create mode 100644 code/modules/components/ai/human/brain.dm create mode 100644 code/modules/components/ai/human/hand_usage.dm create mode 100644 code/modules/components/ai/human/human_attack.dm create mode 100644 code/modules/components/ai/human/human_target_finder.dm diff --git a/code/__DEFINES/component_desires.dm b/code/__DEFINES/component_desires.dm new file mode 100644 index 00000000000..2f8eee9ee4c --- /dev/null +++ b/code/__DEFINES/component_desires.dm @@ -0,0 +1,29 @@ +//Defines for the personal desires that can be held by human AI +#define DESIRE_FOOD "desire_food" +#define DESIRE_UNDERCLOTHING "desire_underclothing" +#define DESIRE_SHOES "desire_shoes" +#define DESIRE_BACK "desire_back" +#define DESIRE_GLOVES "desire_gloves" +#define DESIRE_HAT "desire_hat" +#define DESIRE_BELT "desire_belt" +#define DESIRE_EXOSUIT "desire_exosuit" +#define DESIRE_GLASSES "desire_glasses" +#define DESIRE_MASK "desire_mask" +#define DESIRE_ID "desire_id" +#define DESIRE_HAVE_WEAPON "desire_have_weapon" +#define DESIRE_CONFLICT "desire_conflict" + +var/list/desire_ranks = list( + DESIRE_HAVE_WEAPON, + DESIRE_CONFLICT, + DESIRE_FOOD, + DESIRE_UNDERCLOTHING, + DESIRE_SHOES, + DESIRE_BACK, + DESIRE_GLOVES, + DESIRE_HAT, + DESIRE_BELT, + DESIRE_EXOSUIT, + DESIRE_GLASSES, + DESIRE_MASK, + DESIRE_ID) \ No newline at end of file diff --git a/code/__DEFINES/component_signals.dm b/code/__DEFINES/component_signals.dm index 6aed075864c..7328cb8e703 100644 --- a/code/__DEFINES/component_signals.dm +++ b/code/__DEFINES/component_signals.dm @@ -31,6 +31,11 @@ */ #define COMSIG_MOVE "move" +/** Sent when a mob wants to take a single step. + * @param dir integer: NORTH/SOUTH/WEST/EAST/etc to move in that direction. + */ +#define COMSIG_STEP "step" + /** BLURB * @param temp decimal: Adds value to body temperature @@ -70,3 +75,35 @@ * @param component /datum/component: Component being removed. */ #define COMSIG_COMPONENT_REMOVING "component removing" + +/** Sent when a mob wants to drop the item in its active hand. No arguments. + */ +#define COMSIG_DROP "drop" + +/** Sent when a mob wants to click on something. + * @param target /atom: The thing to be clicked on. + */ +#define COMSIG_CLICKON "clickon" + +/** Sent when a mob wants to activate a hand which is holding a specific item. + * @param target /atom: The item in question. + */ +#define COMSIG_ACTVHANDBYITEM "actvhandbyitem" + +/** Sent when a mob wants to activate an empty hand. No arguments. + */ +#define COMSIG_ACTVEMPTYHAND "actvemptyhand" + +/** Sent when a mob wants to throw the item in its active hand at something. + * @param target /atom: The atom at which to throw. + */ +#define COMSIG_THROWAT "throwat" + +/** Sent when a mob wants to call attack_self() on the item in its active hand. No arguments. + */ +#define COMSIG_ITMATKSELF "itmatkself" + +/** Sent when a mob wants to quick-equip the item in its active hand. No arguments. + */ +#define COMSIG_EQUIPACTVHAND "equipactvhand" + diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index 4d4429c6e51..9f7bb80a449 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -43,7 +43,7 @@ return click_delayer.setDelay(1) - if(client.buildmode) + if(client && client.buildmode) build_click(src, client.buildmode, params, A) return diff --git a/code/modules/components/ai/controllers/mob_controller.dm b/code/modules/components/ai/controllers/mob_controller.dm index 5c51d6e71db..95171891ccb 100644 --- a/code/modules/components/ai/controllers/mob_controller.dm +++ b/code/modules/components/ai/controllers/mob_controller.dm @@ -6,6 +6,10 @@ var/mob/living/M=container.holder //testing("Got command: \[[message_type]\]: [json_encode(args)]") switch(message_type) + if(COMSIG_CLICKON) + var/atom/A = args["target"] + M.ClickOn(A) + if(COMSIG_MOVE) // list("loc"=turf) // list("dir"=NORTH) if("loc" in args) @@ -16,6 +20,9 @@ // walk(M, get_dir(src,M), MISSILE_SPEED) walk(M, args["dir"], walk_delay) + if(COMSIG_STEP) + step(M, args["dir"], walk_delay) + if(COMSIG_ADJUST_BODYTEMP) // list("temp"=TEMP_IN_KELVIN) M.bodytemperature += args["temp"] diff --git a/code/modules/components/ai/human/brain.dm b/code/modules/components/ai/human/brain.dm new file mode 100644 index 00000000000..8b550063a87 --- /dev/null +++ b/code/modules/components/ai/human/brain.dm @@ -0,0 +1,210 @@ +//Basic thought processes +/datum/component/ai/human_brain + var/life_tick=0 + var/wander = TRUE //Whether the mob will walk around searching for goals, or wait for them to become visible + var/lastdir = null + + var/list/desired_items = list() //Specific items + var/list/personal_desires = list() //General desires for events/situations/types of items + + var/list/friendly_factions = list() + var/list/enemy_factions = list() + var/list/enemy_types = list() + var/list/enemy_species = list("Tajaran") + var/list/friends = list() + var/list/enemies = list() + + var/datum/component/ai/target_holder/target_holder = null + var/atom/current_target + +/datum/component/ai/human_brain/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if(COMSIG_LIFE) // no arguments + OnLife() + +/datum/component/ai/human_brain/proc/OnLife() + life_tick++ + if(!target_holder) + target_holder = GetComponent(/datum/component/ai/target_holder) + if(!controller) + controller = GetComponent(/datum/component/controller) + if(controller.getBusy()) + return + if(!ishuman(container.holder)) + return + var/mob/living/carbon/human/H = container.holder + + if(H.stat != CONSCIOUS || !H.canmove || !isturf(H.loc)) + SendSignal(COMSIG_MOVE, list("dir" = 0)) + + current_target = target_holder.GetBestTarget(src, "target_evaluator") + if(!isnull(current_target)) + personal_desires.Add(DESIRE_CONFLICT) + SendSignal(COMSIG_TARGET, list("target"=current_target)) + if(IsBetterWeapon(H)) + personal_desires.Add(DESIRE_HAVE_WEAPON) + if(IsBetterWeapon(H, H.contents)) + if(WieldBestWeapon(H)) + personal_desires.Remove(DESIRE_HAVE_WEAPON) + + AssessNeeds(H) + var/obj/item/I = AttainExternalItemGoal(H) + if(I) + if(H.Adjacent(I)) + AcquireItem(H, I) + SendSignal(COMSIG_MOVE, list("dir" = 0)) + else + if(H.stat == CONSCIOUS && H.canmove && isturf(H.loc)) + SendSignal(COMSIG_MOVE, list("loc" = get_turf(I))) + return + + if(!isnull(current_target)) + container.SendSignalToFirst(/datum/component/ai, COMSIG_ATTACKING, list("target"=current_target)) + var/turf/T = get_turf(current_target) + if(T) + if(H.stat == CONSCIOUS && H.canmove && isturf(H.loc)) + SendSignal(COMSIG_MOVE, list("loc" = T)) + return + else + personal_desires.Remove(DESIRE_CONFLICT) + + if(wander && prob(70)) + var/dir = pick(NORTH,SOUTH,EAST,WEST) + if(lastdir) + var/roll = rand(1,100) + if(roll <= 50) + dir = lastdir + else if(roll <= 90) + dir = pick(turn(lastdir,90),turn(lastdir,270)) + else + dir = turn(lastdir, 180) + if(H.stat == CONSCIOUS && H.canmove && isturf(H.loc)) + SendSignal(COMSIG_STEP, list("dir" = dir)) + lastdir = dir + +/datum/component/ai/human_brain/proc/AssessNeeds(mob/living/carbon/human/H) + personal_desires = list() + if(H.nutrition < 250) + personal_desires.Add(DESIRE_FOOD) + if(!H.get_item_by_slot(slot_w_uniform)) + personal_desires.Add(DESIRE_UNDERCLOTHING) + if(!H.get_item_by_slot(slot_shoes)) + personal_desires.Add(DESIRE_SHOES) + if(!H.get_item_by_slot(slot_back)) + personal_desires.Add(DESIRE_BACK) + if(!H.get_item_by_slot(slot_gloves)) + personal_desires.Add(DESIRE_GLOVES) + if(!H.get_item_by_slot(slot_head)) + personal_desires.Add(DESIRE_HAT) + if(!H.get_item_by_slot(slot_belt)) + personal_desires.Add(DESIRE_BELT) + if(!H.get_item_by_slot(slot_wear_suit)) + personal_desires.Add(DESIRE_EXOSUIT) + if(!H.get_item_by_slot(slot_glasses)) + personal_desires.Add(DESIRE_GLASSES) + if(!H.get_item_by_slot(slot_wear_mask)) + personal_desires.Add(DESIRE_MASK) + if(!H.get_item_by_slot(slot_wear_id)) + personal_desires.Add(DESIRE_ID) + +/datum/component/ai/human_brain/proc/AttainExternalItemGoal(mob/living/carbon/human/H) + var/obj/item/goal = null + processing_desires: + for(var/D in desire_ranks) + if(D in personal_desires) + for(var/obj/item/I in view(H)) + if(I in H.contents) + continue + switch(D) + if(DESIRE_HAVE_WEAPON) + if((!goal && I.force > 2) || (goal && (I.force > goal.force || (I.force == goal.force && I.sharpness > goal.sharpness)))) + goal = I + if(DESIRE_CONFLICT) + break processing_desires + if(DESIRE_FOOD) + if(istype(I, /obj/item/weapon/reagent_containers/food/snacks)) + goal = I + if(DESIRE_UNDERCLOTHING) + if((I.slot_flags & SLOT_ICLOTHING) && I.mob_can_equip(H, slot_w_uniform)) + goal = I + if(DESIRE_SHOES) + if((I.slot_flags & SLOT_FEET) && I.mob_can_equip(H, slot_shoes)) + goal = I + if(DESIRE_BACK) + if((I.slot_flags & SLOT_BACK) && I.mob_can_equip(H, slot_back)) + goal = I + if(DESIRE_GLOVES) + if((I.slot_flags & SLOT_GLOVES) && I.mob_can_equip(H, slot_gloves)) + goal = I + if(DESIRE_HAT) + if((I.slot_flags & SLOT_HEAD) && I.mob_can_equip(H, slot_head)) + goal = I + if(DESIRE_BELT) + if((I.slot_flags & SLOT_BELT) && I.mob_can_equip(H, slot_belt)) + goal = I + if(DESIRE_EXOSUIT) + if((I.slot_flags & SLOT_OCLOTHING) && I.mob_can_equip(H, slot_wear_suit)) + goal = I + if(DESIRE_GLASSES) + if((I.slot_flags & SLOT_EYES) && I.mob_can_equip(H, slot_glasses)) + goal = I + if(DESIRE_MASK) + if((I.slot_flags & SLOT_MASK) && I.mob_can_equip(H, slot_wear_mask)) + goal = I + if(DESIRE_ID) + if((I.slot_flags & SLOT_ID) && I.mob_can_equip(H, slot_wear_id)) + goal = I + if(goal) + break + return goal + +/datum/component/ai/human_brain/proc/AcquireItem(mob/living/carbon/human/H, obj/item/I) + SendSignal(COMSIG_ACTVEMPTYHAND, list()) + SendSignal(COMSIG_CLICKON, list("target" = I)) + if(istype(I, /obj/item/weapon/reagent_containers/food/snacks)) + ConsumeFood(H, I) + else + SendSignal(COMSIG_EQUIPACTVHAND, list()) + +/datum/component/ai/human_brain/proc/ConsumeFood(mob/living/carbon/human/H, obj/item/I) + while(H.nutrition < 250 && !I.gcDestroyed) + SendSignal(COMSIG_ITMATKSELF, list()) + sleep(1) + SendSignal(COMSIG_DROP, list()) + +/datum/component/ai/human_brain/proc/target_evaluator(var/atom/target) + return TRUE + +/datum/component/ai/human_brain/proc/WieldBestWeapon(mob/living/carbon/human/H, var/list/excluded) + SendSignal(COMSIG_ACTVEMPTYHAND, list()) + if(H.get_active_hand()) + SendSignal(COMSIG_DROP, list()) + if(H.get_active_hand()) + return 1 + if(!excluded) + excluded = list() + var/obj/item/current_candidate = null + for(var/obj/item/I in H.contents) + if(!current_candidate) + if(I.force > 2) + current_candidate = I + else + if(I.force > current_candidate.force || (I.force == current_candidate.force && I.sharpness > current_candidate.sharpness)) + current_candidate = I + if(current_candidate) + SendSignal(COMSIG_CLICKON, list("target" = current_candidate)) + if(current_candidate != H.get_active_hand()) + excluded.Add(current_candidate) + .(H, excluded) + else + return 1 + else + return 0 + +/datum/component/ai/human_brain/proc/IsBetterWeapon(mob/living/carbon/human/H, var/list/search_location) + if(!search_location) + search_location = view(H) + var/obj/item/O = H.get_active_hand() + for(var/obj/item/I in search_location) + if((!O && I.force > 2) || (O && (I.force > O.force || (I.force == O.force && I.sharpness > O.sharpness)))) + return 1 \ No newline at end of file diff --git a/code/modules/components/ai/human/hand_usage.dm b/code/modules/components/ai/human/hand_usage.dm new file mode 100644 index 00000000000..4694d1898ce --- /dev/null +++ b/code/modules/components/ai/human/hand_usage.dm @@ -0,0 +1,32 @@ +/datum/component/ai/hand_control/RecieveSignal(var/message_type, var/list/args) + if(iscarbon(container.holder)) + var/mob/living/carbon/M = container.holder + //testing("Got command: \[[message_type]\]: [json_encode(args)]") + switch(message_type) + if(COMSIG_DROP) // list("pickup" = item) + if(M.get_active_hand()) + M.drop_item() + if(COMSIG_ACTVHANDBYITEM) // list("target" = item) + var/obj/item/I = args["target"] + for(var/j = 1 to M.held_items.len) + if(M.held_items[j] == I) + M.active_hand = j + break + if(COMSIG_ACTVEMPTYHAND) + for(var/j = 1 to M.held_items.len) + if(M.held_items[j] == null) + M.active_hand = j + break + if(COMSIG_THROWAT) // list("target" = atom) + var/atom/A = args["target"] + M.throw_mode_on() + M.ClickOn(A) + M.throw_mode_off() + if(COMSIG_ITMATKSELF) + var/obj/item/I = M.get_active_hand() + if(I) + I.attack_self(M) + if(COMSIG_EQUIPACTVHAND) + var/obj/item/I = M.get_active_hand() + if(I) + M.equip_to_appropriate_slot(I) \ No newline at end of file diff --git a/code/modules/components/ai/human/human_attack.dm b/code/modules/components/ai/human/human_attack.dm new file mode 100644 index 00000000000..343e8340ff5 --- /dev/null +++ b/code/modules/components/ai/human/human_attack.dm @@ -0,0 +1,10 @@ +/datum/component/ai/melee/attack_human +// var/datum/component/ai/human_brain/B + +/datum/component/ai/melee/attack_human/OnAttackingTarget(var/atom/target) + if(..(target)) + var/mob/living/carbon/human/H = container.holder + H.a_intent = I_HURT + SendSignal(COMSIG_CLICKON, list("target" = target)) + return 1 // Accepted + return 0 // Unaccepted \ No newline at end of file diff --git a/code/modules/components/ai/human/human_target_finder.dm b/code/modules/components/ai/human/human_target_finder.dm new file mode 100644 index 00000000000..3f08d62c926 --- /dev/null +++ b/code/modules/components/ai/human/human_target_finder.dm @@ -0,0 +1,21 @@ +/datum/component/ai/target_finder/human + range = 7 + var/datum/component/ai/human_brain/B + +/datum/component/ai/target_finder/human/GetTargets() + ASSERT(container.holder!=null) + if(!B) + B = GetComponent(/datum/component/ai/human_brain) + var/list/o = list() + for(var/mob/M in view(range, container.holder)) + if(is_type_in_list(M, exclude_types)) + continue + if(M.stat) + continue + if((M in B.enemies) || (M.faction && M.faction in B.enemy_factions) || (M.type in B.enemy_types)) + o += M + else if(ishuman(M)) + var/mob/living/carbon/human/H = M + if(H.species && H.species.name in B.enemy_species) + o += M + return o \ No newline at end of file diff --git a/code/modules/mob/living/carbon/human/death.dm b/code/modules/mob/living/carbon/human/death.dm index 8383e49ac2f..ac7cc042bb1 100644 --- a/code/modules/mob/living/carbon/human/death.dm +++ b/code/modules/mob/living/carbon/human/death.dm @@ -59,6 +59,10 @@ qdel(vessel) vessel = null + if(NPC_brain) + qdel(NPC_brain) + NPC_brain = null + ..() for(var/obj/abstract/Overlays/O in obj_overlays) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 631ac53370f..27b3b489bf6 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -75,6 +75,10 @@ h_style = "Bald" ..(new_loc, "Slime") +/mob/living/carbon/human/NPC/New(var/new_loc, delay_ready_dna = 0) + ..(new_loc) + initialize_basic_NPC_components() + /mob/living/carbon/human/frankenstein/New(var/new_loc, delay_ready_dna = 0) //Just fuck my shit up: the mob f_style = pick(facial_hair_styles_list) h_style = pick(hair_styles_list) @@ -1817,3 +1821,15 @@ mob/living/carbon/human/remove_internal_organ(var/mob/living/user, var/datum/org if(M_HULK in mutations) return image(icon = 'icons/mob/attackanims.dmi', icon_state = "hulk") else return image(icon = 'icons/mob/attackanims.dmi', icon_state = "default") + +/mob/living/carbon/human/proc/initialize_barebones_NPC_components() //doesn't actually do anything, but contains tools needed for other types to do things + NPC_brain = new (src) + NPC_brain.AddComponent(/datum/component/controller/mob) + NPC_brain.AddComponent(/datum/component/ai/hand_control) + +/mob/living/carbon/human/proc/initialize_basic_NPC_components() //will wander around + initialize_barebones_NPC_components() + NPC_brain.AddComponent(/datum/component/ai/human_brain) + NPC_brain.AddComponent(/datum/component/ai/target_finder/human) + NPC_brain.AddComponent(/datum/component/ai/target_holder/prioritizing) + NPC_brain.AddComponent(/datum/component/ai/melee/attack_human) diff --git a/code/modules/mob/living/carbon/human/human_defines.dm b/code/modules/mob/living/carbon/human/human_defines.dm index aa1d4224929..97e5b28a0fa 100644 --- a/code/modules/mob/living/carbon/human/human_defines.dm +++ b/code/modules/mob/living/carbon/human/human_defines.dm @@ -83,6 +83,8 @@ var/obj/item/weapon/organ/head/decapitated = null //to keep track of a decapitated head, for debug and soulstone purposes + var/datum/component_container/NPC_brain + fire_dmi = 'icons/mob/OnFire.dmi' fire_sprite = "Standing" plane = HUMAN_PLANE \ No newline at end of file diff --git a/code/modules/mob/living/carbon/human/life.dm b/code/modules/mob/living/carbon/human/life.dm index eaa006a1993..74db593848c 100644 --- a/code/modules/mob/living/carbon/human/life.dm +++ b/code/modules/mob/living/carbon/human/life.dm @@ -187,6 +187,8 @@ var/global/list/organ_damage_overlays = list( if(update_overlays) update_overlays = 0 UpdateDamageIcon() + if(NPC_brain) + NPC_brain.SendSignal(COMSIG_LIFE,list()) cycle++ ..() diff --git a/vgstation13.dme b/vgstation13.dme index 532ff72ed9f..0c45bb6b146 100644 --- a/vgstation13.dme +++ b/vgstation13.dme @@ -40,6 +40,7 @@ #include "code\hub.dm" #include "code\names.dm" #include "code\world.dm" +#include "code\__DEFINES\component_desires.dm" #include "code\__DEFINES\component_signals.dm" #include "code\__DEFINES\disease2.dm" #include "code\__DEFINES\hydro.dm" @@ -1164,6 +1165,10 @@ #include "code\modules\components\ai\hostile\melee\attack_animal.dm" #include "code\modules\components\ai\hostile\melee\inject_reagent.dm" #include "code\modules\components\ai\hostile\melee\melee.dm" +#include "code\modules\components\ai\human\brain.dm" +#include "code\modules\components\ai\human\hand_usage.dm" +#include "code\modules\components\ai\human\human_attack.dm" +#include "code\modules\components\ai\human\human_target_finder.dm" #include "code\modules\components\ai\mobs\component_mob.dm" #include "code\modules\components\ai\mobs\giant_spider.dm" #include "code\modules\components\ai\target_finders\simple_view.dm"