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"