From 87f9a90ceafbd6d9b43c0bbbc7f05b8d6d156aed Mon Sep 17 00:00:00 2001 From: SkyratBot <59378654+SkyratBot@users.noreply.github.com> Date: Mon, 11 Oct 2021 17:16:48 +0200 Subject: [PATCH] [MIRROR] Monkey subtree breakup refactor! [MDB IGNORE] (#8748) * Monkey subtree breakup refactor! (#61741) Splitting up monkey ai into subtrees allows me to make a punpun ai after this pr is merged, makes stopping planning matter for the AI subtrees, and more generic subtrees that can be used by most ais It also gets rid of bad practices like setting blackboards in the ai controller. * Monkey subtree breakup refactor! Co-authored-by: tralezab <40974010+tralezab@users.noreply.github.com> --- code/__DEFINES/ai.dm | 26 ++-- code/__DEFINES/monkeys.dm | 2 - code/__HELPERS/ai.dm | 46 ++++++ code/datums/ai/cursed/cursed_behaviors.dm | 5 - code/datums/ai/cursed/cursed_subtrees.dm | 2 +- code/datums/ai/generic/generic_behaviors.dm | 121 ++++++++++++--- code/datums/ai/generic/generic_subtrees.dm | 68 +++++++-- code/datums/ai/monkey/monkey_behaviors.dm | 20 ++- code/datums/ai/monkey/monkey_controller.dm | 61 +------- code/datums/ai/monkey/monkey_subtrees.dm | 140 +++++++----------- .../ai/objects/cursed/cursed_subtrees.dm | 14 ++ code/datums/ai/objects/item_behaviors.dm | 55 +++++++ tgstation.dme | 1 + 13 files changed, 363 insertions(+), 198 deletions(-) create mode 100644 code/__HELPERS/ai.dm create mode 100644 code/datums/ai/objects/cursed/cursed_subtrees.dm create mode 100644 code/datums/ai/objects/item_behaviors.dm diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm index 18515b5241d..8737eaa7506 100644 --- a/code/__DEFINES/ai.dm +++ b/code/__DEFINES/ai.dm @@ -4,11 +4,6 @@ #define AI_STATUS_ON 1 #define AI_STATUS_OFF 2 - -///Monkey checks -#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE)) -#define IS_DEAD_OR_INCAP(source) (source.incapacitated() || source.stat) - ///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on #define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles @@ -27,22 +22,34 @@ ///AI flags #define STOP_MOVING_WHEN_PULLED (1<<0) -///Subtree defines +//Base Subtree defines ///This subtree should cancel any further planning, (Including from other subtrees) #define SUBTREE_RETURN_FINISH_PLANNING 1 +//Generic subtree defines + +/// probability that the pawn should try resisting out of restraints +#define RESIST_SUBTREE_PROB 50 +///macro for whether it's appropriate to resist right now, used by resist subtree +#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE)) +///macro for whether the pawn can act, used generally to prevent some horrifying ai disasters +#define IS_DEAD_OR_INCAP(source) (source.incapacitated() || source.stat) + //Generic BB keys #define BB_CURRENT_MIN_MOVE_DISTANCE "min_move_distance" +///time until we should next eat, set by the generic hunger subtree +#define BB_NEXT_HUNGRY "BB_NEXT_HUNGRY" +///what we're going to eat next +#define BB_FOOD_TARGET "bb_food_target" //for songs -///song datum blackboard, set by instrument subtrees -#define BB_SONG_DATUM "BB_SONG_DATUM" +///song instrument blackboard, set by instrument subtrees +#define BB_SONG_INSTRUMENT "BB_SONG_INSTRUMENT" ///song lines blackboard, set by default on controllers #define BB_SONG_LINES "song_lines" - // Monkey AI controller blackboard keys #define BB_MONKEY_AGGRESSIVE "BB_monkey_aggressive" @@ -59,7 +66,6 @@ #define BB_MONKEY_TARGET_DISPOSAL "BB_monkey_target_disposal" #define BB_MONKEY_DISPOSING "BB_monkey_disposing" #define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown" -#define BB_MONKEY_NEXT_HUNGRY "BB_monkey_next_hungry" ///Haunted item controller defines diff --git a/code/__DEFINES/monkeys.dm b/code/__DEFINES/monkeys.dm index 39f687c2ea7..284fc4c7d1e 100644 --- a/code/__DEFINES/monkeys.dm +++ b/code/__DEFINES/monkeys.dm @@ -14,8 +14,6 @@ #define MONKEY_SYRINGE_RETALIATION_PROB 20 // Probability per Life tick that the monkey will: -/// probability that monkey resist out of restraints -#define MONKEY_RESIST_PROB 50 /// probability that monkey aggro against the mob pulling it #define MONKEY_PULL_AGGRO_PROB 5 /// probability that monkey will get into mischief, i.e. finding/stealing items diff --git a/code/__HELPERS/ai.dm b/code/__HELPERS/ai.dm new file mode 100644 index 00000000000..a030490c8e0 --- /dev/null +++ b/code/__HELPERS/ai.dm @@ -0,0 +1,46 @@ +//this file has procs that help ais think good. used by behaviors mostly + + +/// Returns either the best weapon from the given choices or null if held weapons are better +/proc/GetBestWeapon(datum/ai_controller/controller, list/choices, list/held_weapons) + var/gun_neurons_activated = controller.blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] + var/top_force = 0 + var/obj/item/top_force_item + for(var/obj/item/item as anything in held_weapons) + if(!item) + continue + if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || controller.blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && istype(item, /obj/item/gun)) + // We have a gun, why bother looking for something inferior + // Also yes it is intentional that pawns dont know how to pick the best gun + return item + if(item.force > top_force) + top_force = item.force + top_force_item = item + + for(var/obj/item/item as anything in choices) + if(!item) + continue + if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || controller.blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && istype(item, /obj/item/gun)) + return item + if(item.force <= top_force) + continue + top_force_item = item + top_force = item.force + + return top_force_item + +///returns if something can be consumed, drink or food +/proc/IsEdible(obj/item/thing) + if(!istype(thing)) + return FALSE + if(IS_EDIBLE(thing)) + return TRUE + if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass)) + var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing + if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid! + return TRUE + return FALSE diff --git a/code/datums/ai/cursed/cursed_behaviors.dm b/code/datums/ai/cursed/cursed_behaviors.dm index 8ed55fa21b7..876b99a09cc 100644 --- a/code/datums/ai/cursed/cursed_behaviors.dm +++ b/code/datums/ai/cursed/cursed_behaviors.dm @@ -1,9 +1,4 @@ -/datum/ai_behavior/find_and_set/cursed - //optional, don't use if you're changing search_tactic() - locate_path = /mob/living/carbon - bb_key_to_set = BB_CURSE_TARGET - /datum/ai_behavior/item_move_close_and_attack/cursed attack_sound = 'sound/items/haunted/ghostitemattack.ogg' max_attempts = 4 diff --git a/code/datums/ai/cursed/cursed_subtrees.dm b/code/datums/ai/cursed/cursed_subtrees.dm index ca7e6263c51..85a85bdb895 100644 --- a/code/datums/ai/cursed/cursed_subtrees.dm +++ b/code/datums/ai/cursed/cursed_subtrees.dm @@ -4,7 +4,7 @@ //make sure we have a target var/mob/living/carbon/curse_target = controller.blackboard[BB_CURSE_TARGET] if(!curse_target) - controller.queue_behavior(/datum/ai_behavior/find_and_set/cursed) + controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_CURSE_TARGET, /mob/living/carbon, CURSED_VIEW_RANGE) return //make sure attack is valid if(get_dist(curse_target, item_pawn) > CURSED_VIEW_RANGE) diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm index 2978b22fb38..faf28988869 100644 --- a/code/datums/ai/generic/generic_behaviors.dm +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -115,7 +115,6 @@ . = ..() controller.current_movement_target = controller.blackboard[target_key] - /datum/ai_behavior/give/perform(delta_time, datum/ai_controller/controller, target_key) . = ..() var/mob/living/pawn = controller.pawn @@ -150,25 +149,31 @@ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT action_cooldown = 2 SECONDS -/datum/ai_behavior/consume/setup(datum/ai_controller/controller, obj/item/target) +/datum/ai_behavior/consume/setup(datum/ai_controller/controller, target_key) . = ..() - controller.current_movement_target = target + controller.current_movement_target = controller.blackboard[target_key] -/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, obj/item/target) +/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, target_key, hunger_timer_key) . = ..() - var/mob/living/pawn = controller.pawn + var/mob/living/living_pawn = controller.pawn + var/obj/item/target = controller.blackboard[target_key] - if(!(target in pawn.held_items)) - if(!pawn.put_in_hand_check(target)) - finish_action(controller, FALSE) + if(!(target in living_pawn.held_items)) + if(!living_pawn.put_in_hand_check(target)) + finish_action(controller, FALSE, target, hunger_timer_key) return - pawn.put_in_hands(target) + living_pawn.put_in_hands(target) - target.melee_attack_chain(pawn, pawn) + target.melee_attack_chain(living_pawn, living_pawn) if(QDELETED(target) || prob(10)) // Even if we don't finish it all we can randomly decide to be done - finish_action(controller, TRUE) + finish_action(controller, TRUE, null, hunger_timer_key) + +/datum/ai_behavior/consume/finish_action(datum/ai_controller/controller, succeeded, target_key, hunger_timer_key) + . = ..() + if(succeeded) + controller.blackboard[hunger_timer_key] = world.time + rand(12 SECONDS, 60 SECONDS) /**find and set * Finds an item near themselves, sets a blackboard key as it. Very useful for ais that need to use machines or something. @@ -177,24 +182,73 @@ */ /datum/ai_behavior/find_and_set action_cooldown = 5 SECONDS - ///search range in how many tiles around the pawn to look for the path - var/search_range = 7 - //optional, don't use if you're changing search_tactic() - var/locate_path - var/bb_key_to_set -/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller) +/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller, set_key, locate_path, search_range) . = ..() - var/find_this_thing = search_tactic(controller) + var/find_this_thing = search_tactic(controller, locate_path, search_range) if(find_this_thing) - controller.blackboard[bb_key_to_set] = find_this_thing + controller.blackboard[set_key] = find_this_thing finish_action(controller, TRUE) else finish_action(controller, FALSE) -/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller) +/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller, locate_path, search_range) return locate(locate_path) in oview(search_range, controller.pawn) +/** + * Variant of find and set that fails if the living pawn doesn't hold something + */ +/datum/ai_behavior/find_and_set/pawn_must_hold_item + +/datum/ai_behavior/find_and_set/pawn_must_hold_item/search_tactic(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + if(!living_pawn.get_num_held_items()) + return //we want to fail the search if we don't have something held + . = ..() + +/** + * Variant of find and set that also requires the item to be edible. checks hands too + */ +/datum/ai_behavior/find_and_set/edible + +/datum/ai_behavior/find_and_set/edible/search_tactic(datum/ai_controller/controller, locate_path, search_range) + var/mob/living/living_pawn = controller.pawn + var/list/food_candidates = list() + for(var/held_candidate as anything in living_pawn.held_items) + if(!held_candidate || !IsEdible(held_candidate)) + continue + food_candidates += held_candidate + + var/list/local_results = locate(locate_path) in oview(search_range, controller.pawn) + for(var/local_candidate in local_results) + if(!IsEdible(local_candidate)) + continue + food_candidates += local_candidate + if(food_candidates.len) + return pick(food_candidates) + +/** + * Variant of find and set that only checks in hands, search range should be excluded for this + */ +/datum/ai_behavior/find_and_set/in_hands + +/datum/ai_behavior/find_and_set/in_hands/search_tactic(datum/ai_controller/controller, locate_path) + var/mob/living/living_pawn = controller.pawn + return locate(locate_path) in living_pawn.held_items + +/** + * Drops items in hands, very important for future behaviors that require the pawn to grab stuff + */ +/datum/ai_behavior/drop_item + +/datum/ai_behavior/drop_item/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + var/obj/item/best_held = GetBestWeapon(controller, null, living_pawn.held_items) + for(var/obj/item/held as anything in living_pawn.held_items) + if(!held || held == best_held) + continue + living_pawn.dropItemToGround(held) /// This behavior involves attacking a target. /datum/ai_behavior/attack @@ -284,10 +338,11 @@ /datum/ai_behavior/setup_instrument -/datum/ai_behavior/setup_instrument/perform(delta_time, datum/ai_controller/controller, song_datum_key, song_lines_key) +/datum/ai_behavior/setup_instrument/perform(delta_time, datum/ai_controller/controller, song_instrument_key, song_lines_key) . = ..() - var/datum/song/song = controller.blackboard[song_datum_key] + var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key] + var/datum/song/song = song_instrument.song var/song_lines = controller.blackboard[song_lines_key] //just in case- it won't do anything if the instrument isn't playing @@ -299,10 +354,28 @@ /datum/ai_behavior/play_instrument -/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller, song_datum_key) +/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller, song_instrument_key) . = ..() - var/datum/song/song = controller.blackboard[song_datum_key] + var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key] + var/datum/song/song = song_instrument.song song.start_playing(controller.pawn) finish_action(controller, TRUE) + +/datum/ai_behavior/find_nearby + +/datum/ai_behavior/find_nearby/perform(delta_time, datum/ai_controller/controller, target_key) + . = ..() + + var/list/possible_targets = list() + for(var/atom/thing in view(2, controller.pawn)) + if(!thing.mouse_opacity) + continue + if(thing.IsObscured()) + continue + possible_targets += thing + if(!possible_targets.len) + finish_action(controller, FALSE) + controller.blackboard[target_key] = pick(possible_targets) + finish_action(controller, TRUE) diff --git a/code/datums/ai/generic/generic_subtrees.dm b/code/datums/ai/generic/generic_subtrees.dm index cfb6ec6fb6e..138b47e2efe 100644 --- a/code/datums/ai/generic/generic_subtrees.dm +++ b/code/datums/ai/generic/generic_subtrees.dm @@ -1,16 +1,62 @@ -/datum/ai_planning_subtree/play_instrument/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time) - var/mob/living/living_pawn = controller.pawn - var/obj/item/instrument/song_player = locate(/obj/item/instrument) in living_pawn.held_items - if(!song_player) - controller.blackboard[BB_SONG_DATUM] = null +/** + * Generic Instrument Subtree, For your pawn playing instruments + * + * Requires at least a living mob that can hold items. + * + * relevant blackboards: + * * BB_SONG_INSTRUMENT - set by this subtree, is the song datum the pawn plays music from. + * * BB_SONG_LINES - not set by this subtree, is the song loaded into the song datum. + */ +/datum/ai_planning_subtree/generic_play_instrument/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(!controller.blackboard[BB_SONG_INSTRUMENT]) + controller.queue_behavior(/datum/ai_behavior/find_and_set/in_hands, BB_SONG_INSTRUMENT, /obj/item/instrument) return //we can't play a song since we do not have an instrument - controller.blackboard[BB_SONG_DATUM] = song_player.song + var/obj/item/instrument/song_player = controller.blackboard[BB_SONG_INSTRUMENT] - var/list/donkey_kong_lines = splittext(MONKEY_SONG, "\n") - popleft(donkey_kong_lines) //remove BPM as it is parsed out - if(!compare_list(song_player.song.lines, donkey_kong_lines) || !song_player.song.repeat) - controller.queue_behavior(/datum/ai_behavior/setup_instrument, BB_SONG_DATUM, BB_SONG_LINES) + var/list/parsed_song_lines = splittext(controller.blackboard[BB_SONG_LINES], "\n") + popleft(parsed_song_lines) //remove BPM as it is parsed out + if(!compare_list(song_player.song.lines, parsed_song_lines) || !song_player.song.repeat) + controller.queue_behavior(/datum/ai_behavior/setup_instrument, BB_SONG_INSTRUMENT, BB_SONG_LINES) if(!song_player.song.playing) //we may stop playing if we weren't playing before, were setting up dk theme, or ran out of repeats (also causing setup behavior) - controller.queue_behavior(/datum/ai_behavior/play_instrument, BB_SONG_DATUM) + controller.queue_behavior(/datum/ai_behavior/play_instrument, BB_SONG_INSTRUMENT) + +/** + * Generic Resist Subtree, resist if it makes sense to! + * + * Requires nothing beyond a living pawn, makes sense on a good amount of mobs since anything can get buckled. + * + * relevant blackboards: + * * None! + */ +/datum/ai_planning_subtree/generic_resist/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/mob/living/living_pawn = controller.pawn + + if(SHOULD_RESIST(living_pawn) && DT_PROB(RESIST_SUBTREE_PROB, delta_time)) + controller.queue_behavior(/datum/ai_behavior/resist) //BRO IM ON FUCKING FIRE BRO + return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY. + +/** + * Generic Hunger Subtree, + * + * Requires at least a living mob that can hold items. + * + * relevant blackboards: + * * BB_NEXT_HUNGRY - set by this subtree, is when the controller is next hungry + */ +/datum/ai_planning_subtree/generic_hunger/SelectBehaviors(datum/ai_controller/controller, delta_time) + //inits the blackboard timer + if(!controller.blackboard[BB_NEXT_HUNGRY]) + controller.blackboard[BB_NEXT_HUNGRY] = world.time + rand(0, 30 SECONDS) + + if(world.time < controller.blackboard[BB_NEXT_HUNGRY]) + return + + if(!controller.blackboard[BB_FOOD_TARGET]) + controller.queue_behavior(/datum/ai_behavior/find_and_set/edible, BB_FOOD_TARGET, /obj/item, 2) + return + + controller.queue_behavior(/datum/ai_behavior/drop_item) + controller.queue_behavior(/datum/ai_behavior/consume, BB_FOOD_TARGET, BB_NEXT_HUNGRY) + return SUBTREE_RETURN_FINISH_PLANNING diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm index 60db04a3893..22f2c8938ee 100644 --- a/code/datums/ai/monkey/monkey_behaviors.dm +++ b/code/datums/ai/monkey/monkey_behaviors.dm @@ -139,7 +139,11 @@ /datum/ai_behavior/monkey_attack_mob behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration -/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller) +/datum/ai_behavior/monkey_attack_mob/setup(datum/ai_controller/controller, target_key) + . = ..() + controller.current_movement_target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + +/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller, target_key) . = ..() var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] @@ -288,3 +292,17 @@ enemies[your_enemy] = MONKEY_RECRUIT_HATED_AMOUNT monkey_ai.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN finish_action(controller, TRUE) + +/datum/ai_behavior/monkey_set_combat_target/perform(delta_time, datum/ai_controller/controller, set_key, enemies_key) + var/list/enemies = controller.blackboard[enemies_key] + var/list/valids = list() + for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, controller.pawn)) + if(possible_enemy == controller.pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?) + continue + // Weighted list, so the closer they are the more likely they are to be chosen as the enemy + valids[possible_enemy] = CEILING(100 / (get_dist(controller.pawn, possible_enemy) || 1), 1) + + if(!valids.len) + finish_action(controller, FALSE) + controller.blackboard[set_key] = pickweight(valids) + finish_action(controller, TRUE) diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm index 55266890360..96ea93a2a4a 100644 --- a/code/datums/ai/monkey/monkey_controller.dm +++ b/code/datums/ai/monkey/monkey_controller.dm @@ -7,9 +7,11 @@ have ways of interacting with a specific mob and control it. /datum/ai_controller/monkey movement_delay = 0.4 SECONDS planning_subtrees = list( - /datum/ai_planning_subtree/monkey_tree, - //the monkey tree is mostly combat related, if it isn't busy with that then it may do instrument related things - /datum/ai_planning_subtree/play_instrument, + /datum/ai_planning_subtree/generic_resist, + /datum/ai_planning_subtree/monkey_combat, + /datum/ai_planning_subtree/generic_hunger, + /datum/ai_planning_subtree/generic_play_instrument, + /datum/ai_planning_subtree/monkey_shenanigans, ) blackboard = list( BB_MONKEY_AGGRESSIVE = FALSE, @@ -23,7 +25,6 @@ have ways of interacting with a specific mob and control it. BB_MONKEY_CURRENT_ATTACK_TARGET = null, BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE, BB_MONKEY_GUN_WORKED = TRUE, - BB_MONKEY_NEXT_HUNGRY = 0, BB_SONG_LINES = MONKEY_SONG, ) var/static/list/loc_connections = list( @@ -43,8 +44,6 @@ have ways of interacting with a specific mob and control it. if(!isliving(new_pawn)) return AI_CONTROLLER_INCOMPATIBLE - blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(0, 300) - var/mob/living/living_pawn = new_pawn RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, .proc/on_attackby) RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand) @@ -58,7 +57,6 @@ have ways of interacting with a specific mob and control it. RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, .proc/on_attack_hulk) RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, .proc/on_attempt_cuff) RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, .proc/update_movespeed) - RegisterSignal(new_pawn, COMSIG_FOOD_EATEN, .proc/on_eat) AddComponent(/datum/component/connect_loc_behalf, new_pawn, loc_connections) movement_delay = living_pawn.cached_multiplicative_slowdown @@ -103,11 +101,11 @@ have ways of interacting with a specific mob and control it. for(var/obj/item/item in oview(2, living_pawn)) nearby_items += item - weapon = GetBestWeapon(nearby_items, living_pawn.held_items) + weapon = GetBestWeapon(src, nearby_items, living_pawn.held_items) var/pickpocket = FALSE for(var/mob/living/carbon/human/human in oview(5, living_pawn)) - var/obj/item/held_weapon = GetBestWeapon(human.held_items + weapon, living_pawn.held_items) + var/obj/item/held_weapon = GetBestWeapon(src, human.held_items + weapon, living_pawn.held_items) if(held_weapon == weapon) // It's just the same one, not a held one continue pickpocket = TRUE @@ -124,47 +122,6 @@ have ways of interacting with a specific mob and control it. queue_behavior(/datum/ai_behavior/monkey_equip/ground) return TRUE -/// Returns either the best weapon from the given choices or null if held weapons are better -/datum/ai_controller/monkey/proc/GetBestWeapon(list/choices, list/held_weapons) - var/gun_neurons_activated = blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] - var/top_force = 0 - var/obj/item/top_force_item - for(var/obj/item/item as anything in held_weapons) - if(!item) - continue - if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || blackboard[BB_MONKEY_BLACKLISTITEMS][item]) - continue - if(gun_neurons_activated && istype(item, /obj/item/gun)) - // We have a gun, why bother looking for something inferior - // Also yes it is intentional that monkeys dont know how to pick the best gun - return item - if(item.force > top_force) - top_force = item.force - top_force_item = item - - for(var/obj/item/item as anything in choices) - if(!item) - continue - if(HAS_TRAIT(item, TRAIT_NEEDS_TWO_HANDS) || blackboard[BB_MONKEY_BLACKLISTITEMS][item]) - continue - if(gun_neurons_activated && istype(item, /obj/item/gun)) - return item - if(item.force <= top_force) - continue - top_force_item = item - top_force = item.force - - return top_force_item - -/datum/ai_controller/monkey/proc/IsEdible(obj/item/thing) - if(IS_EDIBLE(thing)) - return TRUE - if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass)) - var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing - if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid! - return TRUE - return FALSE - ///Reactive events to being hit /datum/ai_controller/monkey/proc/retaliate(mob/living/L) var/list/enemies = blackboard[BB_MONKEY_ENEMIES] @@ -251,7 +208,3 @@ have ways of interacting with a specific mob and control it. /datum/ai_controller/monkey/proc/target_del(target) SIGNAL_HANDLER blackboard[BB_MONKEY_BLACKLISTITEMS] -= target - -/datum/ai_controller/monkey/proc/on_eat(mob/living/pawn) - SIGNAL_HANDLER - blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(120, 600) SECONDS diff --git a/code/datums/ai/monkey/monkey_subtrees.dm b/code/datums/ai/monkey/monkey_subtrees.dm index 6cf98dc4701..3655c02df75 100644 --- a/code/datums/ai/monkey/monkey_subtrees.dm +++ b/code/datums/ai/monkey/monkey_subtrees.dm @@ -1,102 +1,62 @@ -/datum/ai_planning_subtree/monkey_tree/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time) - var/mob/living/living_pawn = controller.pawn - - if(SHOULD_RESIST(living_pawn) && DT_PROB(MONKEY_RESIST_PROB, delta_time)) - controller.queue_behavior(/datum/ai_behavior/resist) //BRO IM ON FUCKING FIRE BRO - return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY. - - var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] - - if(HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) //Not a pacifist? lets try some combat behavior. - return - - var/mob/living/selected_enemy - if(length(enemies) || controller.blackboard[BB_MONKEY_AGGRESSIVE]) //We have enemies or are pissed - var/list/valids = list() - for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, living_pawn)) - if(possible_enemy == living_pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?) - continue - // Weighted list, so the closer they are the more likely they are to be chosen as the enemy - valids[possible_enemy] = CEILING(100 / (get_dist(living_pawn, possible_enemy) || 1), 1) - - selected_enemy = pickweight(valids) - - if(selected_enemy) - if(!selected_enemy.stat) //He's up, get him! - if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle - controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy - controller.queue_behavior(/datum/ai_behavior/monkey_flee) - return //I'm running fuck you guys - - if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing. - return SUBTREE_RETURN_FINISH_PLANNING - - controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy - controller.current_movement_target = selected_enemy - if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time) - controller.queue_behavior(/datum/ai_behavior/recruit_monkeys) - controller.queue_behavior(/datum/ai_behavior/battle_screech/monkey) - controller.queue_behavior(/datum/ai_behavior/monkey_attack_mob) - return SUBTREE_RETURN_FINISH_PLANNING //Focus on this - - else //He's down, can we disposal him? - var/obj/machinery/disposal/bodyDisposal = locate(/obj/machinery/disposal/) in view(MONKEY_ENEMY_VISION, living_pawn) - if(bodyDisposal) - controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy - controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = bodyDisposal - controller.queue_behavior(/datum/ai_behavior/disposal_mob, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_TARGET_DISPOSAL) - return SUBTREE_RETURN_FINISH_PLANNING +/datum/ai_planning_subtree/monkey_shenanigans/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time) if(prob(5)) controller.queue_behavior(/datum/ai_behavior/use_in_hand) - if(selected_enemy || !DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time)) + if(!DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time)) return - if(world.time >= controller.blackboard[BB_MONKEY_NEXT_HUNGRY]) - var/list/food_candidates = list() - for(var/obj/item as anything in living_pawn.held_items) - if(!item || !controller.IsEdible(item)) - continue - food_candidates += item - - for(var/obj/item/candidate in oview(2, living_pawn)) - if(!controller.IsEdible(candidate)) - continue - food_candidates += candidate - - if(length(food_candidates)) - var/obj/item/best_held = controller.GetBestWeapon(null, living_pawn.held_items) - for(var/obj/item/held as anything in living_pawn.held_items) - if(!held || held == best_held) - continue - living_pawn.dropItemToGround(held) - - controller.queue_behavior(/datum/ai_behavior/consume, pick(food_candidates)) - return + if(!controller.blackboard[BB_MONKEY_CURRENT_PRESS_TARGET]) + controller.queue_behavior(/datum/ai_behavior/find_nearby, BB_MONKEY_CURRENT_PRESS_TARGET) + return if(prob(50)) - var/list/possible_targets = list() - for(var/atom/thing in view(2, living_pawn)) - if(!thing.mouse_opacity) - continue - if(thing.IsObscured()) - continue - possible_targets += thing - var/atom/target = pick(possible_targets) - if(target) - controller.blackboard[BB_MONKEY_CURRENT_PRESS_TARGET] = target - controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET) - return + controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING - if(prob(5) && (locate(/obj/item) in living_pawn.held_items)) - var/list/possible_receivers = list() - for(var/mob/living/candidate in oview(2, controller.pawn)) - possible_receivers += candidate + if(!controller.blackboard[BB_MONKEY_CURRENT_GIVE_TARGET]) + controller.queue_behavior(/datum/ai_behavior/find_and_set/pawn_must_hold_item, BB_MONKEY_CURRENT_GIVE_TARGET, /mob/living, 2) + return - if(length(possible_receivers)) - controller.blackboard[BB_MONKEY_CURRENT_GIVE_TARGET] = pick(possible_receivers) - controller.queue_behavior(/datum/ai_behavior/give, BB_MONKEY_CURRENT_GIVE_TARGET) - return + if(prob(5)) + controller.queue_behavior(/datum/ai_behavior/give, BB_MONKEY_CURRENT_GIVE_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING controller.TryFindWeapon() + +///monkey combat subtree. +/datum/ai_planning_subtree/monkey_combat/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time) + var/mob/living/living_pawn = controller.pawn + var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] + + if((HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) || (!length(enemies) && !controller.blackboard[BB_MONKEY_AGGRESSIVE])) //Pacifist, or we have no enemies and we're not pissed + return + + if(!controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]) + controller.queue_behavior(/datum/ai_behavior/monkey_set_combat_target, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_ENEMIES) + return SUBTREE_RETURN_FINISH_PLANNING + + var/mob/living/selected_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + + if(!selected_enemy.stat) //He's up, get him! + if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle + controller.queue_behavior(/datum/ai_behavior/monkey_flee) + return SUBTREE_RETURN_FINISH_PLANNING //I'm running fuck you guys + + if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing. + return SUBTREE_RETURN_FINISH_PLANNING + + if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time) + controller.queue_behavior(/datum/ai_behavior/recruit_monkeys, BB_MONKEY_CURRENT_ATTACK_TARGET) + controller.queue_behavior(/datum/ai_behavior/battle_screech/monkey) + controller.queue_behavior(/datum/ai_behavior/monkey_attack_mob, BB_MONKEY_CURRENT_ATTACK_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + //by this point we have a target but they're down, let's try dumpstering this loser + + if(!controller.blackboard[BB_MONKEY_TARGET_DISPOSAL]) + controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_MONKEY_TARGET_DISPOSAL, /obj/machinery/disposal, MONKEY_ENEMY_VISION) + return + + controller.queue_behavior(/datum/ai_behavior/disposal_mob, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_TARGET_DISPOSAL) + return SUBTREE_RETURN_FINISH_PLANNING diff --git a/code/datums/ai/objects/cursed/cursed_subtrees.dm b/code/datums/ai/objects/cursed/cursed_subtrees.dm new file mode 100644 index 00000000000..75cc202c9ec --- /dev/null +++ b/code/datums/ai/objects/cursed/cursed_subtrees.dm @@ -0,0 +1,14 @@ +/datum/ai_planning_subtree/cursed/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/obj/item/item_pawn = controller.pawn + + //make sure we have a target + var/mob/living/carbon/curse_target = controller.blackboard[BB_ITEM_TARGET] + + if(curse_target && get_dist(curse_target, item_pawn) > ITEM_AGGRO_VIEW_RANGE) + controller.blackboard[BB_ITEM_TARGET] = null + return + + if(!controller.blackboard[BB_ITEM_TARGET]) + controller.queue_behavior(/datum/ai_behavior/find_and_set/item_target, BB_ITEM_TARGET, /mob/living/carbon, ITEM_AGGRO_VIEW_RANGE) + + controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/ghostly, BB_ITEM_TARGET, BB_ITEM_THROW_ATTEMPT_COUNT) diff --git a/code/datums/ai/objects/item_behaviors.dm b/code/datums/ai/objects/item_behaviors.dm new file mode 100644 index 00000000000..5db7f2eb789 --- /dev/null +++ b/code/datums/ai/objects/item_behaviors.dm @@ -0,0 +1,55 @@ + +///This behavior is for obj/items, it is used to free themselves out of the hands of whoever is holding them +/datum/ai_behavior/item_escape_grasp + +/datum/ai_behavior/item_escape_grasp/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/obj/item/item_pawn = controller.pawn + var/mob/item_holder = item_pawn.loc + if(!istype(item_holder)) + finish_action(controller, FALSE) //We're no longer beind held. abort abort!! + item_pawn.visible_message(span_warning("[item_pawn] slips out of the hands of [item_holder]!")) + item_holder.dropItemToGround(item_pawn, TRUE) + finish_action(controller, TRUE) + +///This behavior is for obj/items, it is used to move closer to a target and throw themselves towards them. +/datum/ai_behavior/item_move_close_and_attack + required_distance = 3 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + action_cooldown = 20 + ///Sound to use + var/attack_sound + ///Max attempts to make + var/max_attempts = 3 + + +/datum/ai_behavior/item_move_close_and_attack/setup(datum/ai_controller/controller, aggro_list_key, target_key, throw_count_key) + . = ..() + controller.current_movement_target = controller.blackboard[target_key] + + +/datum/ai_behavior/item_move_close_and_attack/perform(delta_time, datum/ai_controller/controller, aggro_list_key, target_key, throw_count_key) + . = ..() + var/obj/item/item_pawn = controller.pawn + var/atom/throw_target = controller.blackboard[target_key] + + item_pawn.visible_message(span_warning("[item_pawn] hurls towards [throw_target]!")) + item_pawn.throw_at(throw_target, rand(4,5), 9) + playsound(item_pawn.loc, attack_sound, 100, TRUE) + controller.blackboard[throw_count_key]++ + if(controller.blackboard[throw_count_key] >= max_attempts) + finish_action(controller, TRUE, target_key, throw_count_key) + +/datum/ai_behavior/item_move_close_and_attack/finish_action(datum/ai_controller/controller, succeeded, aggro_list_key, target_key, throw_count_key) + . = ..() + var/atom/throw_target = controller.blackboard[target_key] + + controller.blackboard -= target_key + controller.blackboard[throw_count_key] = 0 + if(aggro_list_key) + var/list/aggro_list = controller.blackboard[aggro_list_key] + aggro_list[throw_target]-- + +/datum/ai_behavior/item_move_close_and_attack/ghostly + attack_sound = 'sound/items/haunted/ghostitemattack.ogg' + max_attempts = 4 diff --git a/tgstation.dme b/tgstation.dme index 07c9a5e65b5..4bac842a710 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -214,6 +214,7 @@ #include "code\__HELPERS\_logging.dm" #include "code\__HELPERS\_string_lists.dm" #include "code\__HELPERS\admin.dm" +#include "code\__HELPERS\ai.dm" #include "code\__HELPERS\areas.dm" #include "code\__HELPERS\atmospherics.dm" #include "code\__HELPERS\atoms.dm"