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"