From a590870d000af7d7d8c466f831ef89aaa4da12f5 Mon Sep 17 00:00:00 2001 From: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Date: Fri, 26 Dec 2025 17:44:52 -0500 Subject: [PATCH] Monke Business - Ports monkey AI from TG (#31067) * Monke Mode * Punch * Some comment stuff * Linters, excess * Linters * Emote stuff * Pause monkey AI during do afters * Small improvements * Oops * Fixes monkeys trying to drink forever from a glass * Knockdowns and stamcrit fixes * Removes eating/drinking from Pun Pun * Monkey controller improvement, bug fix * Fixes monkey item giving * Fixes brain swaps * Fixes * Update code/datums/ai/monkey/monkey_controller.dm Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> * Addresses code review * Oops * Oops round 2 * Fixes monkeys staying in trip mode when evolved --------- Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com> Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> --- code/__DEFINES/ai/ai_defines.dm | 5 + code/__DEFINES/ai/blackboard_defines.dm | 6 + code/__DEFINES/ai/monkey_ai_defines.dm | 18 ++ code/__DEFINES/dcs/mob_signals.dm | 8 + code/__DEFINES/monkey_defines.dm | 40 +++ code/__HELPERS/basic_ai.dm | 40 +++ code/datums/ai/generic/find_and_set.dm | 29 ++ code/datums/ai/generic/generic_behaviors.dm | 173 ++++++++++ code/datums/ai/generic/generic_subtrees.dm | 56 ++++ code/datums/ai/idle_behaviors/idle_monkey.dm | 37 +++ code/datums/ai/monkey/monkey_behaviors.dm | 304 ++++++++++++++++++ code/datums/ai/monkey/monkey_controller.dm | 203 ++++++++++++ code/datums/ai/monkey/monkey_subtrees.dm | 79 +++++ code/datums/ai/monkey/punpun_subtrees.dm | 22 ++ code/datums/keybindings/emote_keybinds.dm | 8 + code/game/objects/items/weapons/handcuffs.dm | 1 + code/modules/mob/inventory_procs.dm | 16 + .../mob/living/carbon/human/human_emote.dm | 8 + .../mob/living/carbon/human/human_mob.dm | 6 + .../mob/living/carbon/human/species/monkey.dm | 19 +- code/modules/mob/living/living_emote.dm | 8 + .../reagents/reagent_containers/syringes.dm | 8 +- code/modules/recycling/disposal.dm | 45 +-- paradise.dme | 9 + 24 files changed, 1115 insertions(+), 33 deletions(-) create mode 100644 code/__DEFINES/ai/monkey_ai_defines.dm create mode 100644 code/__DEFINES/monkey_defines.dm create mode 100644 code/__HELPERS/basic_ai.dm create mode 100644 code/datums/ai/generic/generic_subtrees.dm create mode 100644 code/datums/ai/idle_behaviors/idle_monkey.dm create mode 100644 code/datums/ai/monkey/monkey_behaviors.dm create mode 100644 code/datums/ai/monkey/monkey_controller.dm create mode 100644 code/datums/ai/monkey/monkey_subtrees.dm create mode 100644 code/datums/ai/monkey/punpun_subtrees.dm diff --git a/code/__DEFINES/ai/ai_defines.dm b/code/__DEFINES/ai/ai_defines.dm index 4c893f37f0e..3e69252c738 100644 --- a/code/__DEFINES/ai/ai_defines.dm +++ b/code/__DEFINES/ai/ai_defines.dm @@ -71,3 +71,8 @@ ///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 #define AI_BOT_PATH_LENGTH 60 + +/// 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 || source.restrained() || source.pulledby) diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm index eb8194fcca8..6a31c33eef3 100644 --- a/code/__DEFINES/ai/blackboard_defines.dm +++ b/code/__DEFINES/ai/blackboard_defines.dm @@ -92,6 +92,12 @@ // Food and eating +/// 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" +/// are we hungry? determined by the udder component +#define BB_CHECK_HUNGRY "BB_check_hungry" /// list of foods this mob likes #define BB_BASIC_FOODS "BB_BASIC_FOODS" /// key holding any food we've found diff --git a/code/__DEFINES/ai/monkey_ai_defines.dm b/code/__DEFINES/ai/monkey_ai_defines.dm new file mode 100644 index 00000000000..f42e96df946 --- /dev/null +++ b/code/__DEFINES/ai/monkey_ai_defines.dm @@ -0,0 +1,18 @@ +// Monkey AI controller blackboard keys + +#define BB_MONKEY_AGGRESSIVE "BB_monkey_aggressive" +#define BB_MONKEY_GUN_NEURONS_ACTIVATED "BB_monkey_gun_aware" +#define BB_MONKEY_BEST_FORCE_FOUND "BB_monkey_bestforcefound" +#define BB_MONKEY_ENEMIES "BB_monkey_enemies" +#define BB_MONKEY_BLACKLISTITEMS "BB_monkey_blacklistitems" +#define BB_MONKEY_PICKUPTARGET "BB_monkey_pickuptarget" +#define BB_MONKEY_PICKPOCKETING "BB_monkey_pickpocketing" +#define BB_MONKEY_CURRENT_ATTACK_TARGET "BB_monkey_current_attack_target" +#define BB_MONKEY_CURRENT_PRESS_TARGET "BB_monkey_current_press_target" +#define BB_MONKEY_CURRENT_GIVE_TARGET "BB_monkey_current_give_target" +#define BB_MONKEY_TARGET_DISPOSAL "BB_monkey_target_disposal" +#define BB_MONKEY_TARGET_MONKEYS "BB_monkey_target_monkeys" +#define BB_MONKEY_DISPOSING "BB_monkey_disposing" +#define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown" +#define BB_BATTLE_CRY_COOLDOWN "BB_battle_cry_cooldown" +#define BB_RESISTING "BB_resisting" diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm index dc6f8fd0f6f..7de55d13c33 100644 --- a/code/__DEFINES/dcs/mob_signals.dm +++ b/code/__DEFINES/dcs/mob_signals.dm @@ -192,6 +192,11 @@ #define COMSIG_LIVING_WRITE_MEMORY "living_write_memory" #define COMPONENT_DONT_WRITE_MEMORY (1<<0) +/// Sent to a mob being injected with a syringe when the do_after initiates +#define COMSIG_LIVING_TRY_SYRINGE_INJECT "living_try_syringe_inject" +/// Sent to a mob being withdrawn from with a syringe when the do_after initiates +#define COMSIG_LIVING_TRY_SYRINGE_WITHDRAW "living_try_syringe_withdraw" + // /mob/living/simple_animal signals ///from /mob/living/simple_animal/handle_environment() #define COMSIG_SIMPLEANIMAL_HANDLE_ENVIRONMENT "simpleanimal_handle_environment" @@ -291,3 +296,6 @@ /// From base of /client/Move(): (direction, old_dir) #define COMSIG_MOB_CLIENT_MOVED "mob_client_moved" + +/// Called when someone attempts to cuff a carbon +#define COMSIG_CARBON_CUFF_ATTEMPTED "carbon_attempt_cuff" diff --git a/code/__DEFINES/monkey_defines.dm b/code/__DEFINES/monkey_defines.dm new file mode 100644 index 00000000000..bdfdec4d747 --- /dev/null +++ b/code/__DEFINES/monkey_defines.dm @@ -0,0 +1,40 @@ +// Monkey defines, placed here so they can be read by other things! + +/// below this health value the monkey starts to flee from enemies +#define MONKEY_FLEE_HEALTH 40 +/// how close an enemy must be to trigger aggression +#define MONKEY_ENEMY_VISION 9 +/// how close an enemy must be before it triggers flee +#define MONKEY_FLEE_VISION 4 +/// How long does it take the item to be taken from a mobs hand +#define MONKEY_ITEM_SNATCH_DELAY 25 +/// Probability monkey will aggro when cuffed +#define MONKEY_CUFF_RETALIATION_PROB 20 +/// Probability monkey will aggro when syringed +#define MONKEY_SYRINGE_RETALIATION_PROB 20 + +// Probability per Life tick that the monkey will: +/// 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 +#define MONKEY_SHENANIGAN_PROB 20 +/// probability that monkey will disarm an armed attacker +#define MONKEY_ATTACK_DISARM_PROB 20 +/// probability that monkey will get recruited when friend is attacked +#define MONKEY_RECRUIT_PROB 25 + +/// probability for the monkey to aggro when attacked +#define MONKEY_RETALIATE_PROB 85 + +/// amount of aggro to add to an enemy when they attack user +#define MONKEY_HATRED_AMOUNT 4 +/// amount of aggro to add to an enemy when a monkey is recruited +#define MONKEY_RECRUIT_HATED_AMOUNT 2 +/// probability of reducing aggro by one when the monkey attacks +#define MONKEY_HATRED_REDUCTION_PROB 20 + +/// Monkey recruit cooldown +#define MONKEY_RECRUIT_COOLDOWN (1 MINUTES) + +/// Cooldown on screeching +#define MONKEY_BATTLE_CRY_COOLDOWN (15 SECONDS) diff --git a/code/__HELPERS/basic_ai.dm b/code/__HELPERS/basic_ai.dm new file mode 100644 index 00000000000..5b32f84dbea --- /dev/null +++ b/code/__HELPERS/basic_ai.dm @@ -0,0 +1,40 @@ +/// returns if something can be consumed, drink or food +/proc/IsEdible(obj/item/thing) + if(!istype(thing)) + return FALSE + if(istype(thing, /obj/item/food)) + return TRUE + if(istype(thing, /obj/item/reagent_containers/drinks/drinkingglass)) + var/obj/item/reagent_containers/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 + + +/// 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 in held_weapons) + if(controller.blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && isgun(item)) + // 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 in choices) + if(controller.blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && isgun(item)) + return item + if(item.force <= top_force) + continue + top_force_item = item + top_force = item.force + + return top_force_item diff --git a/code/datums/ai/generic/find_and_set.dm b/code/datums/ai/generic/find_and_set.dm index e610769556c..dd531881836 100644 --- a/code/datums/ai/generic/find_and_set.dm +++ b/code/datums/ai/generic/find_and_set.dm @@ -42,3 +42,32 @@ if(can_see(controller.pawn, possible_turf, search_range)) return possible_turf return null + +/** + * 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.held_items()) + return // we want to fail the search if we don't have something held + return ..() + +/** + * 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 + + for(var/atom/held_candidate as anything in living_pawn.held_items()) + if(IsEdible(held_candidate)) + return held_candidate + + for(var/atom/local_candidate as anything in oview(search_range, controller.pawn)) + if(IsEdible(local_candidate) && istype(local_candidate, locate_path)) + return local_candidate + + return null diff --git a/code/datums/ai/generic/generic_behaviors.dm b/code/datums/ai/generic/generic_behaviors.dm index 3f66de7af8c..8fa131701d5 100644 --- a/code/datums/ai/generic/generic_behaviors.dm +++ b/code/datums/ai/generic/generic_behaviors.dm @@ -25,3 +25,176 @@ if(speech_sound) playsound(living_pawn, speech_sound, 80, vary = TRUE) return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/resist + +/datum/ai_behavior/resist/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + living_pawn.ai_controller.set_blackboard_key(BB_RESISTING, TRUE) + living_pawn.run_resist() + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/battle_screech + /// List of possible screeches the behavior has + var/list/screeches + +/datum/ai_behavior/battle_screech/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + controller.set_blackboard_key(BB_BATTLE_CRY_COOLDOWN, world.time + MONKEY_BATTLE_CRY_COOLDOWN) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick(screeches)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/// Use in hand the currently held item +/datum/ai_behavior/use_in_hand + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/use_in_hand/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held = pawn.get_active_hand() + if(!held) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + pawn.activate_hand() + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/// Use the currently held item, or unarmed, on a weakref to an object in the world +/datum/ai_behavior/use_on_object + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/use_on_object/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/use_on_object/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand()) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + if(held_item) + held_item.melee_attack_chain(pawn, target) + else + controller.ai_interact(target, INTENT_HELP) + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/** + * 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(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + var/list/my_held_items = living_pawn.held_items() - GetBestWeapon(controller, null, living_pawn.held_items()) + if(!length(my_held_items)) + return AI_BEHAVIOR_FAILED | AI_BEHAVIOR_DELAY + living_pawn.drop_item_to_ground(pick(my_held_items)) + return AI_BEHAVIOR_SUCCEEDED | AI_BEHAVIOR_DELAY + +/datum/ai_behavior/consume + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + action_cooldown = 2 SECONDS + +/datum/ai_behavior/consume/setup(datum/ai_controller/controller, target_key) + . = ..() + set_movement_target(controller, controller.blackboard[target_key]) + +/datum/ai_behavior/consume/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hunger_timer_key) + . = ..() + var/mob/living/living_pawn = controller.pawn + var/obj/item/target = controller.blackboard[target_key] + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + if(!(target in living_pawn.held_items())) + if(!living_pawn.get_empty_held_indexes() || !living_pawn.put_in_hands(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + 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 + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + +/datum/ai_behavior/find_nearby + +/datum/ai_behavior/find_nearby/perform(seconds_per_tick, 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 + if(isitem(thing)) + var/obj/item/item = thing + if(item.flags & ABSTRACT) + continue + possible_targets += thing + if(!length(possible_targets)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + controller.set_blackboard_key(target_key, pick(possible_targets)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/give + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/give/setup(datum/ai_controller/controller, target_key) + . = ..() + set_movement_target(controller, controller.blackboard[target_key]) + +/datum/ai_behavior/give/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held_item = pawn.get_active_hand() + var/atom/target = controller.blackboard[target_key] + + if(!held_item) // if held_item is null, we pretend that action was successful + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + if(!target || !isliving(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/mob/living/living_target = target + var/perform_flags = try_to_give_item(controller, living_target, held_item) + if(perform_flags & AI_BEHAVIOR_FAILED) + return perform_flags + controller.pause_ai(1.5 SECONDS) + living_target.visible_message( + "[pawn] starts trying to give [held_item] to [living_target]!", + "[pawn] tries to give you [held_item]!" + ) + if(!do_after(pawn, 1 SECONDS, living_target)) + return AI_BEHAVIOR_DELAY | perform_flags + + perform_flags |= try_to_give_item(controller, living_target, held_item, actually_give = TRUE) + return AI_BEHAVIOR_DELAY | perform_flags + +/datum/ai_behavior/give/proc/try_to_give_item(datum/ai_controller/controller, mob/living/carbon/target, obj/item/held_item, actually_give) + if(QDELETED(held_item) || QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + if(!iscarbon(controller.pawn)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + if(!iscarbon(target)) + controller.clear_blackboard_key(BB_MONKEY_CURRENT_GIVE_TARGET) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + if(HAS_TRAIT(target, TRAIT_HANDS_BLOCKED) || target.r_hand && target.l_hand) + controller.clear_blackboard_key(BB_MONKEY_CURRENT_GIVE_TARGET) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/mob/living/carbon/C = controller.pawn + C.drop_item_to_ground(held_item) + target.put_in_hands(held_item) + held_item.add_fingerprint(target) + held_item.on_give(C, target) + held_item.pickup(target) + controller.clear_blackboard_key(BB_MONKEY_CURRENT_GIVE_TARGET) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED diff --git a/code/datums/ai/generic/generic_subtrees.dm b/code/datums/ai/generic/generic_subtrees.dm new file mode 100644 index 00000000000..af0fd99a9aa --- /dev/null +++ b/code/datums/ai/generic/generic_subtrees.dm @@ -0,0 +1,56 @@ +/** + * 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/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + + if(SHOULD_RESIST(living_pawn) && SPT_PROB(RESIST_SUBTREE_PROB, seconds_per_tick)) + controller.queue_behavior(/datum/ai_behavior/resist) // BRO IM ON FUCKING FIRE BRO + return SUBTREE_RETURN_FINISH_PLANNING // IM NOT DOING ANYTHING ELSE BUT EXTINGUISH 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/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + if(living_pawn.incapacitated()) // We're restrained or stunned - what are we gonna do? + return SUBTREE_RETURN_FINISH_PLANNING + + var/next_eat = controller.blackboard[BB_NEXT_HUNGRY] + if(!next_eat) + // inits the blackboard timer + next_eat = world.time + rand(0, 30 SECONDS) + controller.set_blackboard_key(BB_NEXT_HUNGRY, next_eat) + + if(world.time < next_eat) + return + + var/atom/food_target = controller.blackboard[BB_FOOD_TARGET] + + if(isnull(food_target)) + controller.queue_behavior(/datum/ai_behavior/find_and_set/edible, BB_FOOD_TARGET, /obj/item, 2) + return + + if(!length(living_pawn.get_empty_held_indexes()) && !(food_target in living_pawn.held_items())) + controller.queue_behavior(/datum/ai_behavior/drop_item) + return SUBTREE_RETURN_FINISH_PLANNING + + if(istype(food_target, /obj/item/reagent_containers)) + var/obj/item/reagent_containers/container = food_target + if(!container.reagents.total_volume) // This thing empty. + controller.queue_behavior(/datum/ai_behavior/drop_item) + controller.clear_blackboard_key(BB_FOOD_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + controller.queue_behavior(/datum/ai_behavior/consume, BB_FOOD_TARGET, BB_NEXT_HUNGRY) + return SUBTREE_RETURN_FINISH_PLANNING diff --git a/code/datums/ai/idle_behaviors/idle_monkey.dm b/code/datums/ai/idle_behaviors/idle_monkey.dm new file mode 100644 index 00000000000..6af40117ae1 --- /dev/null +++ b/code/datums/ai/idle_behaviors/idle_monkey.dm @@ -0,0 +1,37 @@ +/datum/idle_behavior/idle_monkey + /// Emotes that will be played commonly during idle behavior. + var/list/common_emotes = list( + "screech", + "roar", + ) + /// Emotes that will be played rarely during idle behavior. + var/list/rare_emotes = list( + "scratch", + "jump", + "roll", + "tail", + ) + +/datum/idle_behavior/idle_monkey/perform_idle_behavior(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + + if(SPT_PROB(25, seconds_per_tick) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) + var/move_dir = pick(GLOB.alldirs) + living_pawn.Move(get_step(living_pawn, move_dir), move_dir) + else if(SPT_PROB(5, seconds_per_tick)) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick(common_emotes)) + else if(SPT_PROB(1, seconds_per_tick)) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick(rare_emotes)) + +/datum/idle_behavior/idle_monkey/pun_pun + common_emotes = list( + "tunesing", + "dance", + "bow", + ) + rare_emotes = list( + "clear", + "sign", + "tail", + ) diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm new file mode 100644 index 00000000000..19693e501e2 --- /dev/null +++ b/code/datums/ai/monkey/monkey_behaviors.dm @@ -0,0 +1,304 @@ +/datum/ai_behavior/battle_screech/monkey + screeches = list("scream") + +/datum/ai_behavior/monkey_equip + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + +/datum/ai_behavior/monkey_equip/setup(datum/ai_controller/controller, target_key) + . = ..() + var/obj/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/monkey_equip/finish_action(datum/ai_controller/controller, success, target_key) + . = ..() + + if(!success) // Don't try again on this item if we failed + controller.set_blackboard_key_assoc(BB_MONKEY_BLACKLISTITEMS, controller.blackboard[target_key], TRUE) + + controller.clear_blackboard_key(BB_MONKEY_PICKUPTARGET) + +/// Equips an item on the monkey +/// Returns TRUE if it works out, FALSE otherwise +/datum/ai_behavior/monkey_equip/proc/equip_item(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + var/best_force = controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] + if(!isturf(living_pawn.loc)) + return FALSE + + if(!target) + return FALSE + + if(target.anchored) // Can't pick it up, so stop trying. + return FALSE + + // Strong weapon + else if(target.force > best_force) + living_pawn.drop_l_hand() + living_pawn.drop_r_hand() + living_pawn.put_in_hands(target) + controller.set_blackboard_key(BB_MONKEY_BEST_FORCE_FOUND, target.force) + return TRUE + + else if(target.slot_flags) // Clothing == top priority + living_pawn.drop_item_to_ground(target, TRUE) + living_pawn.update_icons() + if(!living_pawn.equip_to_appropriate_slot(target)) + return FALSE // Already wearing something, in the future this should probably replace the current item but the code didn't actually do that, and I dont want to support it right now. + return TRUE + + // EVERYTHING ELSE + else if(living_pawn.get_empty_held_indexes()) + living_pawn.put_in_hands(target) + return TRUE + + return FALSE + +/datum/ai_behavior/monkey_equip/ground + +/datum/ai_behavior/monkey_equip/ground/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + if(equip_item(controller)) + return . | AI_BEHAVIOR_SUCCEEDED + return . | AI_BEHAVIOR_FAILED + +/datum/ai_behavior/monkey_equip/pickpocket + +/datum/ai_behavior/monkey_equip/pickpocket/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + if(controller.blackboard[BB_MONKEY_PICKPOCKETING]) // We are pickpocketing, don't do ANYTHING!!!! + return + INVOKE_ASYNC(src, PROC_REF(attempt_pickpocket), controller) + +/datum/ai_behavior/monkey_equip/pickpocket/proc/attempt_pickpocket(datum/ai_controller/controller) + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + var/mob/living/victim = target.loc + var/mob/living/living_pawn = controller.pawn + + if(!istype(victim)) + finish_action(controller, FALSE) + return + + victim.visible_message(SPAN_WARNING("[living_pawn] starts trying to take [target] from [victim]!"), SPAN_DANGER("[living_pawn] tries to take [target]!")) + + controller.set_blackboard_key(BB_MONKEY_PICKPOCKETING, TRUE) + + var/success = FALSE + + if(do_after(living_pawn, MONKEY_ITEM_SNATCH_DELAY, victim) && target && living_pawn.Adjacent(victim)) + for(var/obj/item/I in victim.held_items()) + if(I == target) + victim.visible_message(SPAN_DANGER("[living_pawn] snatches [target] from [victim]."), SPAN_USERDANGER("[living_pawn] snatched [target]!")) + if(victim.unequip(target)) + if(!QDELETED(target) && !equip_item(controller)) + target.forceMove(living_pawn.drop_location()) + success = TRUE + break + else + victim.visible_message(SPAN_DANGER("[living_pawn] tried to snatch [target] from [victim], but failed!"), SPAN_USERDANGER("[living_pawn] tried to grab [target]!?")) + + finish_action(controller, success) // We either fucked up or got the item. + +/datum/ai_behavior/monkey_equip/pickpocket/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.set_blackboard_key(BB_MONKEY_PICKPOCKETING, FALSE) + controller.clear_blackboard_key(BB_MONKEY_PICKUPTARGET) + +/datum/ai_behavior/monkey_flee + +/datum/ai_behavior/monkey_flee/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + + if(living_pawn.health >= MONKEY_FLEE_HEALTH) // we're back in business + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + var/mob/living/target = null + + // flee from anyone who attacked us and we didn't beat down + for(var/mob/living/L in view(living_pawn, MONKEY_FLEE_VISION)) + if(controller.blackboard[BB_MONKEY_ENEMIES][L] && L.stat == CONSCIOUS) + target = L + break + + if(target) + GLOB.move_manager.move_away(living_pawn, target, max_dist=MONKEY_ENEMY_VISION, delay=5) + return AI_BEHAVIOR_DELAY + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/monkey_attack_mob + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION // performs to increase frustration + +/datum/ai_behavior/monkey_attack_mob/setup(datum/ai_controller/controller, target_key) + . = ..() + set_movement_target(controller, controller.blackboard[target_key]) + +/datum/ai_behavior/monkey_attack_mob/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/target = controller.blackboard[target_key] + var/mob/living/living_pawn = controller.pawn + var/datum/targeting_strategy/strategy = GET_TARGETING_STRATEGY(controller.blackboard[BB_TARGETING_STRATEGY]) + + if(QDELETED(target) || !strategy.can_attack(living_pawn, target)) // Target == owned + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + // check if target has a weapon + var/holding_weapon + for(var/obj/item/potential_weapon in target.held_items()) + if(!(potential_weapon.flags & ABSTRACT)) + holding_weapon = potential_weapon + break + + var/attack_results = monkey_attack(controller, target, seconds_per_tick, holding_weapon && SPT_PROB(MONKEY_ATTACK_DISARM_PROB, seconds_per_tick), holding_weapon) + + if(!attack_results || controller.blackboard[BB_MONKEY_AGGRESSIVE]) + return AI_BEHAVIOR_DELAY + + // check if we can de-aggro on the enemy... + var/hatred_value = controller.blackboard[BB_MONKEY_ENEMIES][target] + + if(isnull(hatred_value)) + hatred_value = 1 + controller.set_blackboard_key_assoc(BB_MONKEY_ENEMIES, target, hatred_value) + + if(!SPT_PROB(MONKEY_HATRED_REDUCTION_PROB, seconds_per_tick)) + return AI_BEHAVIOR_DELAY + + // we decrease our hatred value to them by 1 + hatred_value-- + if(hatred_value <= 0) + controller.remove_thing_from_blackboard_key(BB_MONKEY_ENEMIES, target) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + controller.set_blackboard_key_assoc(BB_MONKEY_ENEMIES, target, hatred_value) + return AI_BEHAVIOR_DELAY + +/datum/ai_behavior/monkey_attack_mob/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + +/// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little +/datum/ai_behavior/monkey_attack_mob/proc/monkey_attack(datum/ai_controller/controller, mob/living/target, seconds_per_tick, disarm, holding_weapon) + var/mob/living/living_pawn = controller.pawn + + if(living_pawn.next_move > world.time) + return FALSE + + // are we holding a gun? can we shoot it? if so, FIRE + var/obj/item/gun/gun_to_shoot = locate() in living_pawn.held_items() + if(gun_to_shoot?.can_shoot()) + if(gun_to_shoot != living_pawn.get_active_hand()) + living_pawn.swap_hand() + controller.ai_interact(target, INTENT_HARM) + return TRUE + + // look for any potential weapons we're holding + var/obj/item/potential_weapon = locate() in living_pawn.held_items() + if(!living_pawn.Adjacent(target, potential_weapon)) + return FALSE + + if(isnull(potential_weapon)) + controller.ai_interact(target, INTENT_HARM) + if(disarm && !isnull(holding_weapon) && controller.blackboard[BB_MONKEY_BLACKLISTITEMS][holding_weapon]) + controller.remove_thing_from_blackboard_key(BB_MONKEY_BLACKLISTITEMS, holding_weapon) // lets try to pickpocket it again! + return TRUE + + if(potential_weapon != living_pawn.get_active_hand()) + living_pawn.swap_hand() + controller.ai_interact(target, INTENT_HARM) + return TRUE + +/datum/ai_behavior/disposal_mob + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM // performs to increase frustration + +/datum/ai_behavior/disposal_mob/setup(datum/ai_controller/controller, attack_target_key, disposal_target_key) + . = ..() + set_movement_target(controller, controller.blackboard[attack_target_key]) + +/datum/ai_behavior/disposal_mob/finish_action(datum/ai_controller/controller, succeeded, attack_target_key, disposal_target_key) + . = ..() + controller.clear_blackboard_key(attack_target_key) // Reset attack target + controller.set_blackboard_key(BB_MONKEY_DISPOSING, FALSE) // No longer disposing + controller.clear_blackboard_key(disposal_target_key) // No target disposal + +/datum/ai_behavior/disposal_mob/perform(seconds_per_tick, datum/ai_controller/controller, attack_target_key, disposal_target_key) + . = ..() + if(controller.blackboard[BB_MONKEY_DISPOSING]) // We are disposing, don't do ANYTHING!!!! + return AI_BEHAVIOR_DELAY + + var/mob/living/target = controller.blackboard[attack_target_key] + var/mob/living/living_pawn = controller.pawn + + set_movement_target(controller, target) + + if(!target) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + if(target.pulledby != living_pawn && !HAS_AI_CONTROLLER_TYPE(target.pulledby, /datum/ai_controller/monkey)) // Dont steal from my fellow monkeys. + if(living_pawn.Adjacent(target) && isturf(target.loc)) + living_pawn.start_pulling(target) + return AI_BEHAVIOR_DELAY // Do the rest next turn + + var/obj/machinery/disposal/disposal = controller.blackboard[disposal_target_key] + set_movement_target(controller, disposal) + + if(!disposal) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + if(living_pawn.Adjacent(disposal)) + INVOKE_ASYNC(src, PROC_REF(try_disposal_mob), controller, attack_target_key, disposal_target_key) // put him in! + return AI_BEHAVIOR_DELAY + // This means we might be getting pissed! + return AI_BEHAVIOR_DELAY + +/datum/ai_behavior/disposal_mob/proc/try_disposal_mob(datum/ai_controller/controller, attack_target_key, disposal_target_key) + var/mob/living/living_pawn = controller.pawn + var/mob/living/target = controller.blackboard[attack_target_key] + var/obj/machinery/disposal/disposal = controller.blackboard[disposal_target_key] + + controller.set_blackboard_key(BB_MONKEY_DISPOSING, TRUE) + + if(target && disposal?.stuff_mob_in(target, living_pawn)) + disposal.flush() + finish_action(controller, TRUE, attack_target_key, disposal_target_key) + +/datum/ai_behavior/recruit_monkeys/perform(seconds_per_tick, datum/ai_controller/controller) + . = ..() + controller.set_blackboard_key(BB_MONKEY_RECRUIT_COOLDOWN, world.time + MONKEY_RECRUIT_COOLDOWN) + var/mob/living/living_pawn = controller.pawn + + for(var/mob/living/nearby_monkey in view(living_pawn, MONKEY_ENEMY_VISION)) + if(QDELETED(nearby_monkey) || !HAS_AI_CONTROLLER_TYPE(nearby_monkey, /datum/ai_controller/monkey)) + continue + if(!SPT_PROB(MONKEY_RECRUIT_PROB, seconds_per_tick)) + continue + // Recruited a monkey to our side + controller.set_blackboard_key(BB_MONKEY_RECRUIT_COOLDOWN, world.time + MONKEY_RECRUIT_COOLDOWN) + // Other monkeys now also hate the guy we're currently targeting + nearby_monkey.ai_controller.add_blackboard_key_assoc(BB_MONKEY_ENEMIES, controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET], MONKEY_RECRUIT_HATED_AMOUNT) + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/monkey_set_combat_target/perform(seconds_per_tick, 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) + continue // don't target ourselves + if(!enemies[possible_enemy]) // We don't hate this creature! But we might still attack it! + if(!controller.blackboard[BB_MONKEY_AGGRESSIVE]) // We are not aggressive either, so we won't attack! + continue + if(faction_check(possible_enemy.faction, list("monkey", "jungle"), exact_match = FALSE) && !controller.blackboard[BB_MONKEY_TARGET_MONKEYS]) // do not target your team. includes monkys gorillas etc. + 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(!length(valids)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + + controller.set_blackboard_key(set_key, pickweight(valids)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm new file mode 100644 index 00000000000..0772a590339 --- /dev/null +++ b/code/datums/ai/monkey/monkey_controller.dm @@ -0,0 +1,203 @@ +/* +AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a mob. What this means is that these datums +have ways of interacting with a specific mob and control it. +*/ +/// OOK OOK OOK + +/datum/ai_controller/monkey + ai_movement = /datum/ai_movement/jps + movement_delay = 0.4 SECONDS + planning_subtrees = list( + /datum/ai_planning_subtree/generic_resist, + /datum/ai_planning_subtree/monkey_combat, + /datum/ai_planning_subtree/generic_hunger, + /datum/ai_planning_subtree/monkey_shenanigans, + ) + + ai_traits = AI_FLAG_PAUSE_DURING_DO_AFTER + + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/monkey, + BB_MONKEY_AGGRESSIVE = FALSE, + BB_MONKEY_BEST_FORCE_FOUND = 0, + BB_MONKEY_ENEMIES = list(), + BB_MONKEY_BLACKLISTITEMS = list(), + BB_MONKEY_PICKPOCKETING = FALSE, + BB_MONKEY_DISPOSING = FALSE, + BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE, + BB_RESISTING = FALSE, + ) + idle_behavior = /datum/idle_behavior/idle_monkey + /// Is this mob tripping things? + var/tripping = FALSE + +/datum/targeting_strategy/basic/monkey + +/datum/targeting_strategy/basic/monkey/faction_check(datum/ai_controller/controller, mob/living/living_mob, mob/living/the_target) + if(controller.blackboard[BB_MONKEY_ENEMIES][the_target]) + return FALSE + return ..() + +/datum/ai_controller/monkey/process(seconds_per_tick) + var/mob/living/living_pawn = src.pawn + movement_delay = living_pawn.movement_delay() // Circumstances change. Update speed frequently. + + if(!length(living_pawn.do_afters) && living_pawn.ai_controller.blackboard[BB_RESISTING]) + living_pawn.ai_controller.set_blackboard_key(BB_RESISTING, FALSE) + + if(living_pawn.ai_controller.blackboard[BB_RESISTING]) + return + + if(living_pawn.IsWeakened() || living_pawn.IsStunned()) // We're stunned - what are we gonna do? + cancel_actions() + return + + . = ..() + +/datum/ai_controller/monkey/pun_pun + movement_delay = 7 DECISECONDS // pun pun moves slower so the bartender can keep track of them + planning_subtrees = list( + /datum/ai_planning_subtree/generic_resist, + /datum/ai_planning_subtree/monkey_combat, + /datum/ai_planning_subtree/punpun_shenanigans, + ) + idle_behavior = /datum/idle_behavior/idle_monkey/pun_pun + +/datum/ai_controller/monkey/angry + +/datum/ai_controller/monkey/angry/try_possess_pawn(atom/new_pawn) + . = ..() + if(. & AI_CONTROLLER_INCOMPATIBLE) + return + pawn = new_pawn + set_blackboard_key(BB_MONKEY_AGGRESSIVE, TRUE) // Angry monke + set_trip_mode(mode = FALSE) + +/datum/ai_controller/monkey/try_possess_pawn(atom/new_pawn) + if(!isliving(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + + var/mob/living/living_pawn = new_pawn + if(!HAS_TRAIT(living_pawn, TRAIT_RELAYING_ATTACKER)) + living_pawn.AddElement(/datum/element/relay_attackers) + RegisterSignal(new_pawn, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + RegisterSignal(new_pawn, COMSIG_ATOM_PULLED, PROC_REF(on_startpulling)) + RegisterSignals(new_pawn, list(COMSIG_LIVING_TRY_SYRINGE_INJECT, COMSIG_LIVING_TRY_SYRINGE_WITHDRAW), PROC_REF(on_try_syringe)) + RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, PROC_REF(on_attempt_cuff)) + RegisterSignal(new_pawn, COMSIG_LIVING_MOB_BUMP, PROC_REF(on_mob_bump)) + return ..() // Run parent at end + +/datum/ai_controller/monkey/unpossess_pawn(destroy) + UnregisterSignal(pawn, list( + COMSIG_ATOM_WAS_ATTACKED, + COMSIG_ATOM_PULLED, + COMSIG_LIVING_TRY_SYRINGE_INJECT, + COMSIG_LIVING_TRY_SYRINGE_WITHDRAW, + COMSIG_CARBON_CUFF_ATTEMPTED, + COMSIG_LIVING_MOB_BUMP + )) + + return ..() // Run parent at end + +/datum/ai_controller/monkey/on_sentience_lost() + . = ..() + set_trip_mode(mode = TRUE) + +/datum/ai_controller/monkey/get_able_to_run() + var/mob/living/living_pawn = pawn + + if(living_pawn.stat || living_pawn.incapacitated()) + return AI_UNABLE_TO_RUN + + if(living_pawn.ckey || living_pawn.client) + return AI_UNABLE_TO_RUN + return ..() + +/datum/ai_controller/monkey/proc/set_trip_mode(mode = TRUE) + if(!ismonkeybasic(pawn)) // In case the monkey was humanized. + tripping = FALSE + return + tripping = mode + +/// re-used behavior pattern by monkeys for finding a weapon +/datum/ai_controller/monkey/proc/TryFindWeapon() + var/mob/living/living_pawn = pawn + + if(!living_pawn.held_items()) + set_blackboard_key(BB_MONKEY_BEST_FORCE_FOUND, 0) + + if(blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] && is_type_in_list(/obj/item/gun, living_pawn.held_items())) + // We have a gun, what could we possibly want? + return FALSE + + var/obj/item/weapon + var/list/nearby_items = list() + for(var/obj/item/item in oview(2, living_pawn)) + if(item.force < 2) + continue + nearby_items += item + + for(var/obj/item/item in living_pawn.held_items()) // If we've got some garbage in our hands that's going to stop us from effectively attacking, we should get rid of it. + if(item.force < 2) + living_pawn.drop_item_to_ground(item) + + 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(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 + weapon = held_weapon + + if(!weapon || (weapon in living_pawn.held_items())) + return FALSE + + if(weapon.force < 2) // our bite does 2 damage on average, no point in settling for anything less + return FALSE + + set_blackboard_key(BB_MONKEY_PICKUPTARGET, weapon) + if(pickpocket) + queue_behavior(/datum/ai_behavior/monkey_equip/pickpocket, BB_MONKEY_PICKUPTARGET) + else + queue_behavior(/datum/ai_behavior/monkey_equip/ground, BB_MONKEY_PICKUPTARGET) + return TRUE + +/// Reactive events to being hit +/datum/ai_controller/monkey/proc/retaliate(mob/living/living_mob) + // just to be safe + if(QDELETED(living_mob)) + return + + add_blackboard_key_assoc(BB_MONKEY_ENEMIES, living_mob, MONKEY_HATRED_AMOUNT) + +/datum/ai_controller/monkey/proc/on_attacked(datum/source, mob/attacker) + SIGNAL_HANDLER + if(prob(MONKEY_RETALIATE_PROB)) + retaliate(attacker) + +/datum/ai_controller/monkey/proc/on_startpulling(datum/source, atom/movable/puller, state, force) + SIGNAL_HANDLER + var/mob/living/living_pawn = pawn + if(living_pawn.stat && prob(MONKEY_PULL_AGGRO_PROB)) // nuh uh you don't pull me! + retaliate(living_pawn.pulledby) + return TRUE + +/datum/ai_controller/monkey/proc/on_try_syringe(datum/source, mob/user) + SIGNAL_HANDLER + // chance of monkey retaliation + if(prob(MONKEY_SYRINGE_RETALIATION_PROB)) + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attempt_cuff(datum/source, mob/user) + SIGNAL_HANDLER + // chance of monkey retaliation + if(prob(MONKEY_CUFF_RETALIATION_PROB)) + retaliate(user) + +/datum/ai_controller/monkey/proc/on_mob_bump(mob/source, mob/living/crossing_mob) + SIGNAL_HANDLER + if(!tripping) + return + crossing_mob.KnockDown(4 SECONDS) diff --git a/code/datums/ai/monkey/monkey_subtrees.dm b/code/datums/ai/monkey/monkey_subtrees.dm new file mode 100644 index 00000000000..fd93c5bcb25 --- /dev/null +++ b/code/datums/ai/monkey/monkey_subtrees.dm @@ -0,0 +1,79 @@ +/datum/ai_planning_subtree/monkey_shenanigans/select_behaviors(datum/ai_controller/monkey/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + if(living_pawn.incapacitated()) // We're restrained or stunned - what are we gonna do? + return SUBTREE_RETURN_FINISH_PLANNING + + if(prob(5)) + controller.queue_behavior(/datum/ai_behavior/use_in_hand) + + if(!SPT_PROB(MONKEY_SHENANIGAN_PROB, seconds_per_tick)) + 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)) + controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + 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) + else + 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/select_behaviors(datum/ai_controller/monkey/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] + + if(living_pawn.incapacitated()) // We're restrained or stunned - what are we gonna do? + return SUBTREE_RETURN_FINISH_PLANNING + + 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 + living_pawn.a_intent = INTENT_HELP + 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) + living_pawn.a_intent = INTENT_HELP + return SUBTREE_RETURN_FINISH_PLANNING + + var/mob/living/selected_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + + if(QDELETED(selected_enemy)) + living_pawn.a_intent = INTENT_HELP + return + + if(!selected_enemy.stat) // He's up, get him! + living_pawn.a_intent = INTENT_HARM + 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) + return + + if(controller.blackboard[BB_BATTLE_CRY_COOLDOWN] < world.time) + 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 + + living_pawn.a_intent = INTENT_HELP + + 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/monkey/punpun_subtrees.dm b/code/datums/ai/monkey/punpun_subtrees.dm new file mode 100644 index 00000000000..64cf489319e --- /dev/null +++ b/code/datums/ai/monkey/punpun_subtrees.dm @@ -0,0 +1,22 @@ +/datum/ai_planning_subtree/punpun_shenanigans/select_behaviors(datum/ai_controller/monkey/controller, seconds_per_tick) + + controller.set_trip_mode(mode = FALSE) // pun pun doesn't fuck around + + if(prob(5)) + controller.queue_behavior(/datum/ai_behavior/use_in_hand) + + if(!SPT_PROB(MONKEY_SHENANIGAN_PROB, seconds_per_tick)) + return + + if(!controller.blackboard[BB_MONKEY_CURRENT_PRESS_TARGET]) + controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_MONKEY_CURRENT_PRESS_TARGET, /obj/item/desk_bell, 2) + else if(prob(50)) + controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + 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) + else if(prob(30)) + controller.queue_behavior(/datum/ai_behavior/give, BB_MONKEY_CURRENT_GIVE_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + diff --git a/code/datums/keybindings/emote_keybinds.dm b/code/datums/keybindings/emote_keybinds.dm index dca3626a79f..05484cc58a5 100644 --- a/code/datums/keybindings/emote_keybinds.dm +++ b/code/datums/keybindings/emote_keybinds.dm @@ -37,6 +37,10 @@ linked_emote = /datum/emote/living/choke name = "Choke" +/datum/keybinding/emote/sing_tune + linked_emote = /datum/emote/living/sing_tune + name = "Sing Tune" + /datum/keybinding/emote/collapse linked_emote = /datum/emote/living/collapse name = "Collapse" @@ -349,6 +353,10 @@ linked_emote = /datum/emote/living/carbon/human/eyebrow name = "Eyebrow" +/datum/keybinding/emote/carbon/human/clear_throat + linked_emote = /datum/emote/living/carbon/human/clear_throat + name = "Clear Throat" + /datum/keybinding/emote/carbon/human/facepalm linked_emote = /datum/emote/living/carbon/human/facepalm name = "Facepalm" diff --git a/code/game/objects/items/weapons/handcuffs.dm b/code/game/objects/items/weapons/handcuffs.dm index 5ba20045032..21c6233fab9 100644 --- a/code/game/objects/items/weapons/handcuffs.dm +++ b/code/game/objects/items/weapons/handcuffs.dm @@ -95,6 +95,7 @@ SPAN_USERDANGER("[user] is trying to put [src.name] on [C]!")) playsound(loc, cuffsound, 15, TRUE, -10) + SEND_SIGNAL(C, COMSIG_CARBON_CUFF_ATTEMPTED, user) if(do_mob(user, C, 30)) apply_cuffs(C, user, remove_src) to_chat(user, SPAN_NOTICE("You handcuff [C].")) diff --git a/code/modules/mob/inventory_procs.dm b/code/modules/mob/inventory_procs.dm index 21be8839b99..8e550c0062e 100644 --- a/code/modules/mob/inventory_procs.dm +++ b/code/modules/mob/inventory_procs.dm @@ -32,6 +32,22 @@ /mob/proc/is_holding(obj/item/I) return istype(I) && (I == r_hand || I == l_hand) +/// Returns a list of items being held +/mob/proc/held_items() + var/list/items = list() + items += get_item_by_slot(ITEM_SLOT_LEFT_HAND) + items += get_item_by_slot(ITEM_SLOT_RIGHT_HAND) + return items + +/// Returns a number corresponding to an open hand, or null if no open hands. +/mob/proc/get_empty_held_indexes() + var/list/L + var/list/held = held_items() + for(var/i in 1 to length(held)) + if(!held[i]) + LAZYADD(L, i) + return L + //Checks if we're holding an item of type: typepath /mob/proc/is_holding_item_of_type(typepath) . = FALSE diff --git a/code/modules/mob/living/carbon/human/human_emote.dm b/code/modules/mob/living/carbon/human/human_emote.dm index 2effeb43047..d4c62041068 100644 --- a/code/modules/mob/living/carbon/human/human_emote.dm +++ b/code/modules/mob/living/carbon/human/human_emote.dm @@ -130,6 +130,14 @@ muzzled_noises = list("bothered") emote_type = EMOTE_AUDIBLE | EMOTE_MOUTH +/datum/emote/living/carbon/human/clear_throat + key = "clear" + key_third_person = "clears throat" + message = "clears their throat." + message_mime = "silently clears their throat!" + muzzled_noises = list("clears their throat.") + emote_type = EMOTE_AUDIBLE | EMOTE_MOUTH + /datum/emote/living/carbon/human/hug key = "hug" key_third_person = "hugs" diff --git a/code/modules/mob/living/carbon/human/human_mob.dm b/code/modules/mob/living/carbon/human/human_mob.dm index e6959e2c806..20459e7c3f1 100644 --- a/code/modules/mob/living/carbon/human/human_mob.dm +++ b/code/modules/mob/living/carbon/human/human_mob.dm @@ -215,21 +215,27 @@ /mob/living/carbon/human/monkey/Initialize(mapload) . = ..(mapload, /datum/species/monkey) + ai_controller = new /datum/ai_controller/monkey(src) /mob/living/carbon/human/farwa/Initialize(mapload) . = ..(mapload, /datum/species/monkey/tajaran) + ai_controller = new /datum/ai_controller/monkey(src) /mob/living/carbon/human/wolpin/Initialize(mapload) . = ..(mapload, /datum/species/monkey/vulpkanin) + ai_controller = new /datum/ai_controller/monkey(src) /mob/living/carbon/human/neara/Initialize(mapload) . = ..(mapload, /datum/species/monkey/skrell) + ai_controller = new /datum/ai_controller/monkey(src) /mob/living/carbon/human/stok/Initialize(mapload) . = ..(mapload, /datum/species/monkey/unathi) + ai_controller = new /datum/ai_controller/monkey(src) /mob/living/carbon/human/nian_worme/Initialize(mapload) . = ..(mapload, /datum/species/monkey/nian_worme) + ai_controller = new /datum/ai_controller/monkey(src) /mob/living/carbon/human/moth/Initialize(mapload) . = ..(mapload, /datum/species/moth) diff --git a/code/modules/mob/living/carbon/human/species/monkey.dm b/code/modules/mob/living/carbon/human/species/monkey.dm index 73149cd8e97..79df021e042 100644 --- a/code/modules/mob/living/carbon/human/species/monkey.dm +++ b/code/modules/mob/living/carbon/human/species/monkey.dm @@ -45,18 +45,6 @@ if(H.radiation > RAD_MOB_GORILLIZE && prob(RAD_MOB_GORILLIZE_PROB)) H.gorillize() -/datum/species/monkey/handle_npc(mob/living/carbon/human/H) - if(H.stat != CONSCIOUS) - return - if(prob(1)) - H.emote(pick("scratch","jump","roll","tail")) - if(prob(33) && (H.mobility_flags & MOBILITY_MOVE) && isturf(H.loc) && !H.pulledby) //won't move if being pulled - var/dir_to_go = pick(GLOB.cardinal) - var/turf/to_go = get_step(H, dir_to_go) - if(islava(to_go) || ischasm(to_go)) - return - step(H, dir_to_go) - /datum/species/monkey/get_random_name() return "[lowertext(name)] ([rand(100,999)])" @@ -65,8 +53,15 @@ H.real_name = get_random_name() H.name = H.real_name H.butcher_results = list(/obj/item/food/meat/monkey = 5) + H.ai_controller = new /datum/ai_controller/monkey(H) H.dna.blood_type = pick(4;"O-", 36;"O+", 3;"A-", 28;"A+", 1;"B-", 20;"B+", 1;"AB-", 5;"AB+") +/datum/species/monkey/on_species_loss(mob/living/carbon/human/H) + . = ..() + if(H.ai_controller && istype(H.ai_controller, /datum/ai_controller/monkey)) + var/datum/ai_controller/monkey/controller = H.ai_controller + controller.set_trip_mode(mode = FALSE) + /datum/species/monkey/handle_dna(mob/living/carbon/human/H, remove) ..() if(!remove) diff --git a/code/modules/mob/living/living_emote.dm b/code/modules/mob/living/living_emote.dm index aa3b3d456f7..b97a64cd63e 100644 --- a/code/modules/mob/living/living_emote.dm +++ b/code/modules/mob/living/living_emote.dm @@ -24,6 +24,14 @@ message_param = "bows to %t." message_postfix = "to %t." +/datum/emote/living/sing_tune + key = "tunesing" + key_third_person = "sings a tune" + message = "sings a tune." + message_mime = "opens their mouth rather obnoxiously." + emote_type = EMOTE_AUDIBLE + muzzled_noises = list("melodic") + /datum/emote/living/burp key = "burp" key_third_person = "burps" diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm index 04cf7b0bb43..743728cdcae 100644 --- a/code/modules/reagents/reagent_containers/syringes.dm +++ b/code/modules/reagents/reagent_containers/syringes.dm @@ -65,13 +65,14 @@ return if(L != user) L.visible_message(SPAN_DANGER("[user] is trying to inject [L]!"), \ - SPAN_USERDANGER("[user] is trying to inject you!")) + SPAN_USERDANGER("[user] is trying to inject you!")) + SEND_SIGNAL(L, COMSIG_LIVING_TRY_SYRINGE_INJECT, user) if(!do_mob(user, L)) return if(L.reagents.total_volume >= L.reagents.maximum_volume) return - L.visible_message("[user] injects [L] with the syringe!", \ - "[user] injects [L] with the syringe!") + L.visible_message(SPAN_DANGER("[user] injects [L] with the syringe!"), \ + SPAN_USERDANGER("[user] injects [L] with the syringe!")) var/list/rinject = list() for(var/datum/reagent/R in reagents.reagent_list) @@ -88,6 +89,7 @@ if(L != user) L.visible_message(SPAN_DANGER("[user] is trying to take a blood sample from [L]!"), \ SPAN_USERDANGER("[user] is trying to take a blood sample from [L]!")) + SEND_SIGNAL(L, COMSIG_LIVING_TRY_SYRINGE_WITHDRAW, user) busy = TRUE if(!do_mob(user, L, syringe_draw_time)) busy = FALSE diff --git a/code/modules/recycling/disposal.dm b/code/modules/recycling/disposal.dm index bee60e72b57..bd31f4ccf98 100644 --- a/code/modules/recycling/disposal.dm +++ b/code/modules/recycling/disposal.dm @@ -179,25 +179,8 @@ return ITEM_INTERACT_COMPLETE var/mob/GM = G.affecting - user.visible_message( - SPAN_WARNING("[user] starts stuffing [GM] into the disposal unit!"), - SPAN_WARNING("You start stuffing [GM] into the disposal unit."), - SPAN_WARNING("You hear someone trying to stuff someone else into a disposal unit!") - ) - - // Abort if the target manages to scurry away. - if(!do_after(user, 2 SECONDS, target = GM)) - return ITEM_INTERACT_COMPLETE - - GM.forceMove(src) - user.visible_message( - SPAN_WARNING("[GM] has been stuffed into the disposal unit by [user]!"), - SPAN_WARNING("You stuff [GM] into the disposal unit."), - SPAN_WARNING("You hear someone being stuffed into a disposal unit!") - ) + stuff_mob_in(GM, user) qdel(G) - update() - add_attack_logs(user, GM, "Disposal'ed", !GM.ckey ? null : ATKLOG_ALL) return ITEM_INTERACT_COMPLETE if(!user.drop_item() || QDELETED(used)) @@ -214,6 +197,32 @@ return ITEM_INTERACT_COMPLETE +/// Handles stuffing a grabbed mob into the disposal +/obj/machinery/disposal/proc/stuff_mob_in(mob/living/target, mob/living/user) + // If there's not actually a mob in the grab, stop it. Get some help. + if(!istype(target)) + return FALSE + + user.visible_message( + SPAN_WARNING("[user] starts stuffing [target] into the disposal unit!"), + SPAN_WARNING("You start stuffing [target] into the disposal unit."), + SPAN_WARNING("You hear someone trying to stuff someone else into a disposal unit!") + ) + + // Abort if the target manages to scurry away. + if(!do_after(user, 2 SECONDS, target = target)) + return ITEM_INTERACT_COMPLETE + + target.forceMove(src) + user.visible_message( + SPAN_WARNING("[target] has been stuffed into the disposal unit by [user]!"), + SPAN_WARNING("You stuff [target] into the disposal unit."), + SPAN_WARNING("You hear someone being stuffed into a disposal unit!") + ) + update() + add_attack_logs(user, target, "Disposal'ed", !target.ckey ? null : ATKLOG_ALL) + return TRUE + /obj/machinery/disposal/screwdriver_act(mob/user, obj/item/I) if(mode != DISPOSALS_OFF) // It's on to_chat(user, SPAN_WARNING("You need to turn the disposal unit off first!")) diff --git a/paradise.dme b/paradise.dme index 1706170a025..252d1532be7 100644 --- a/paradise.dme +++ b/paradise.dme @@ -102,6 +102,7 @@ #include "code\__DEFINES\misc_defines.dm" #include "code\__DEFINES\mob_defines.dm" #include "code\__DEFINES\mod.dm" +#include "code\__DEFINES\monkey_defines.dm" #include "code\__DEFINES\move_force.dm" #include "code\__DEFINES\movement_defines.dm" #include "code\__DEFINES\muzzle_flash.dm" @@ -164,6 +165,7 @@ #include "code\__DEFINES\ai\ai_defines.dm" #include "code\__DEFINES\ai\blackboard_defines.dm" #include "code\__DEFINES\ai\changeling_giant_spider_ai_defines.dm" +#include "code\__DEFINES\ai\monkey_ai_defines.dm" #include "code\__DEFINES\ai\incursion_blackboard_defines.dm" #include "code\__DEFINES\ai\swarmer_defines.dm" #include "code\__DEFINES\dcs\ai_signals.dm" @@ -189,6 +191,7 @@ #include "code\__HELPERS\AnimationLibrary.dm" #include "code\__HELPERS\api.dm" #include "code\__HELPERS\atom_helpers.dm" +#include "code\__HELPERS\basic_ai.dm" #include "code\__HELPERS\bitflag_lists.dm" #include "code\__HELPERS\cmp.dm" #include "code\__HELPERS\colour_helpers.dm" @@ -522,8 +525,14 @@ #include "code\datums\ai\basic_mobs\hunting_behavior\hunting_mouse.dm" #include "code\datums\ai\generic\find_and_set.dm" #include "code\datums\ai\generic\generic_behaviors.dm" +#include "code\datums\ai\generic\generic_subtrees.dm" #include "code\datums\ai\idle_behaviors\idle_behavior.dm" +#include "code\datums\ai\idle_behaviors\idle_monkey.dm" #include "code\datums\ai\idle_behaviors\idle_random_walk.dm" +#include "code\datums\ai\monkey\monkey_behaviors.dm" +#include "code\datums\ai\monkey\monkey_controller.dm" +#include "code\datums\ai\monkey\monkey_subtrees.dm" +#include "code\datums\ai\monkey\punpun_subtrees.dm" #include "code\datums\ai\movement\ai_movement.dm" #include "code\datums\ai\movement\ai_movement_jps.dm" #include "code\datums\ai\movement\basic_avoidance.dm"