code:
/datum/ai_controller/dog
	blackboard = list(\
		BB_SIMPLE_CARRY_ITEM = null,\
		BB_FETCH_TARGET = null,\
		BB_FETCH_DELIVER_TO = null,\
		BB_DOG_FRIENDS = list(),\
		BB_FETCH_IGNORE_LIST = list(),\
		BB_DOG_ORDER_MODE = DOG_COMMAND_NONE,\
		BB_DOG_PLAYING_DEAD = FALSE,\
		BB_DOG_HARASS_TARGET = null)
	ai_movement = /datum/ai_movement/jps
	planning_subtrees = list(/datum/ai_planning_subtree/dog)

	COOLDOWN_DECLARE(heel_cooldown)
	COOLDOWN_DECLARE(command_cooldown)

/datum/ai_controller/dog/process(delta_time)
	if(ismob(pawn))
		var/mob/living/living_pawn = pawn
		movement_delay = living_pawn.cached_multiplicative_slowdown
	return ..()

/datum/ai_controller/dog/TryPossessPawn(atom/new_pawn)
	if(!isliving(new_pawn))
		return AI_CONTROLLER_INCOMPATIBLE

	RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand)
	RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, .proc/on_examined)
	RegisterSignal(new_pawn, COMSIG_CLICK_ALT, .proc/check_altclicked)
	RegisterSignal(new_pawn, list(COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING), .proc/on_death)
	RegisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING, .proc/listened_throw)
	return ..() //Run parent at end

/datum/ai_controller/dog/UnpossessPawn(destroy)
	var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
	if(carried_item)
		pawn.visible_message("[pawn] drops [carried_item].")
		carried_item.forceMove(pawn.drop_location())
		blackboard[BB_SIMPLE_CARRY_ITEM] = null
	UnregisterSignal(pawn, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_PARENT_EXAMINE, COMSIG_CLICK_ALT, COMSIG_LIVING_DEATH, COMSIG_GLOB_CARBON_THROW_THING, COMSIG_PARENT_QDELETING))
	return ..() //Run parent at end

/datum/ai_controller/dog/able_to_run()
	var/mob/living/living_pawn = pawn

	if(IS_DEAD_OR_INCAP(living_pawn))
		return FALSE
	return ..()

/datum/ai_controller/dog/get_access()
	var/mob/living/simple_animal/simple_pawn = pawn
	if(!istype(simple_pawn))
		return

	return simple_pawn.access_card

/// Someone has thrown something, see if it's someone we care about and start listening to the thrown item so we can see if we want to fetch it when it lands
/datum/ai_controller/dog/proc/listened_throw(datum/source, mob/living/carbon/carbon_thrower)
	SIGNAL_HANDLER
	if(blackboard[BB_FETCH_TARGET] || blackboard[BB_FETCH_DELIVER_TO] || blackboard[BB_DOG_PLAYING_DEAD]) // we're already busy
		return
	if(!COOLDOWN_FINISHED(src, heel_cooldown))
		return
	if(!can_see(pawn, carbon_thrower, length=AI_DOG_VISION_RANGE))
		return
	var/obj/item/thrown_thing = carbon_thrower.get_active_held_item()
	if(!isitem(thrown_thing))
		return
	if(blackboard[BB_FETCH_IGNORE_LIST][WEAKREF(thrown_thing)])
		return

	RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, .proc/listen_throw_land)

/// A throw we were listening to has finished, see if it's in range for us to try grabbing it
/datum/ai_controller/dog/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum)
	SIGNAL_HANDLER

	UnregisterSignal(thrown_thing, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_THROW_LANDED))
	if(!istype(thrown_thing) || !isturf(thrown_thing.loc) || !can_see(pawn, thrown_thing, length=AI_DOG_VISION_RANGE))
		return

	current_movement_target = thrown_thing
	blackboard[BB_FETCH_TARGET] = thrown_thing
	blackboard[BB_FETCH_DELIVER_TO] = throwing_datum.thrower
	queue_behavior(/datum/ai_behavior/fetch)

/// Someone's interacting with us by hand, see if they're being nice or mean
/datum/ai_controller/dog/proc/on_attack_hand(datum/source, mob/living/user)
	SIGNAL_HANDLER

	if(user.combat_mode)
		unfriend(user)
	else
		if(prob(AI_DOG_PET_FRIEND_PROB))
			befriend(user)
		// if the dog has something in their mouth that they're not bringing to someone for whatever reason, have them drop it when pet by a friend
		var/list/friends = blackboard[BB_DOG_FRIENDS]
		if(blackboard[BB_SIMPLE_CARRY_ITEM] && !current_movement_target && friends[WEAKREF(user)])
			var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
			pawn.visible_message("[pawn] drops [carried_item] at [user]'s feet!")
			// maybe have a dedicated proc for dropping things
			carried_item.forceMove(get_turf(user))
			blackboard[BB_SIMPLE_CARRY_ITEM] = null

/// Someone is being nice to us, let's make them a friend!
/datum/ai_controller/dog/proc/befriend(mob/living/new_friend)
	var/list/friends = blackboard[BB_DOG_FRIENDS]
	var/datum/weakref/friend_ref = WEAKREF(new_friend)
	if(friends[friend_ref])
		return
	if(in_range(pawn, new_friend))
		new_friend.visible_message("[pawn] licks at [new_friend] in a friendly manner!", span_notice("[pawn] licks at you in a friendly manner!"))
	friends[friend_ref] = TRUE
	RegisterSignal(new_friend, COMSIG_MOB_POINTED, .proc/check_point)
	RegisterSignal(new_friend, COMSIG_MOB_SAY, .proc/check_verbal_command)

/// Someone is being mean to us, take them off our friends (add actual enemies behavior later)
/datum/ai_controller/dog/proc/unfriend(mob/living/ex_friend)
	var/list/friends = blackboard[BB_DOG_FRIENDS]
	friends -= WEAKREF(ex_friend)
	UnregisterSignal(ex_friend, list(COMSIG_MOB_POINTED, COMSIG_MOB_SAY))

/// Someone is looking at us, if we're currently carrying something then show what it is, and include a message if they're our friend
/datum/ai_controller/dog/proc/on_examined(datum/source, mob/user, list/examine_text)
	SIGNAL_HANDLER

	var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
	if(carried_item)
		examine_text += span_notice("[pawn.p_they(TRUE)] [pawn.p_are()] carrying [carried_item.get_examine_string(user)] in [pawn.p_their()] mouth.")
	if(blackboard[BB_DOG_FRIENDS][WEAKREF(user)])
		var/mob/living/living_pawn = pawn
		if(!IS_DEAD_OR_INCAP(living_pawn))
			examine_text += span_notice("[pawn.p_they(TRUE)] seem[pawn.p_s()] happy to see you!")

/// If we died, drop anything we were carrying
/datum/ai_controller/dog/proc/on_death(mob/living/ol_yeller)
	SIGNAL_HANDLER

	var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM]
	if(!carried_item)
		return

	ol_yeller.visible_message("[ol_yeller] drops [carried_item] as [ol_yeller.p_they()] die[ol_yeller.p_s()].")
	carried_item.forceMove(ol_yeller.drop_location())
	blackboard[BB_SIMPLE_CARRY_ITEM] = null

// next section is regarding commands

/// Someone alt clicked us, see if they're someone we should show the radial command menu to
/datum/ai_controller/dog/proc/check_altclicked(datum/source, mob/living/clicker)
	SIGNAL_HANDLER

	if(!COOLDOWN_FINISHED(src, command_cooldown))
		return
	if(!istype(clicker) || !blackboard[BB_DOG_FRIENDS][WEAKREF(clicker)])
		return
	. = COMPONENT_CANCEL_CLICK_ALT
	INVOKE_ASYNC(src, .proc/command_radial, clicker)

/// Show the command radial menu
/datum/ai_controller/dog/proc/command_radial(mob/living/clicker)
	var/list/commands = list(
		COMMAND_HEEL = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow"),
		COMMAND_FETCH = image(icon = 'icons/mob/actions/actions_spells.dmi', icon_state = "summons"),
		COMMAND_ATTACK = image(icon = 'icons/effects/effects.dmi', icon_state = "bite"),
		COMMAND_DIE = image(icon = 'icons/mob/pets.dmi', icon_state = "puppy_dead")
		)

	var/choice = show_radial_menu(clicker, pawn, commands, custom_check = CALLBACK(src, .proc/check_menu, clicker), tooltips = TRUE)
	if(!choice || !check_menu(clicker))
		return
	set_command_mode(clicker, choice)

/datum/ai_controller/dog/proc/check_menu(mob/user)
	if(!istype(user))
		CRASH("A non-mob is trying to issue an order to [pawn].")
	if(user.incapacitated() || !can_see(user, pawn))
		return FALSE
	return TRUE

/// One of our friends said something, see if it's a valid command, and if so, take action
/datum/ai_controller/dog/proc/check_verbal_command(mob/speaker, speech_args)
	SIGNAL_HANDLER

	if(!blackboard[BB_DOG_FRIENDS][WEAKREF(speaker)])
		return

	if(!COOLDOWN_FINISHED(src, command_cooldown))
		return

	var/mob/living/living_pawn = pawn
	if(IS_DEAD_OR_INCAP(living_pawn))
		return

	var/spoken_text = speech_args[SPEECH_MESSAGE] // probably should check for full words
	var/command
	if(findtext(spoken_text, "heel") || findtext(spoken_text, "sit") || findtext(spoken_text, "stay"))
		command = COMMAND_HEEL
	else if(findtext(spoken_text, "fetch") || findtext(spoken_text, "get it"))
		command = COMMAND_FETCH
	else if(findtext(spoken_text, "attack") || findtext(spoken_text, "sic"))
		command = COMMAND_ATTACK
	else if(findtext(spoken_text, "play dead"))
		command = COMMAND_DIE
	else
		return

	if(!can_see(pawn, speaker, length=AI_DOG_VISION_RANGE))
		return
	set_command_mode(speaker, command)

/// Whether we got here via radial menu or a verbal command, this is where we actually process what our new command will be
/datum/ai_controller/dog/proc/set_command_mode(mob/commander, command)
	COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN)

	switch(command)
		// heel: stop what you're doing, relax and try not to do anything for a little bit
		if(COMMAND_HEEL)
			pawn.visible_message(span_notice("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] sit[pawn.p_s()] down obediently, awaiting further orders."))
			blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE
			COOLDOWN_START(src, heel_cooldown, AI_DOG_HEEL_DURATION)
			CancelActions()
		// fetch: whatever the commander points to, try and bring it back
		if(COMMAND_FETCH)
			pawn.visible_message(span_notice("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] bounce[pawn.p_s()] slightly in anticipation."))
			blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_FETCH
		// attack: harass whoever the commander points to
		if(COMMAND_ATTACK)
			pawn.visible_message(span_danger("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] growl[pawn.p_s()] intensely.")) // imagine getting intimidated by a corgi
			blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_ATTACK
		if(COMMAND_DIE)
			blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE
			CancelActions()
			queue_behavior(/datum/ai_behavior/play_dead)

/// Someone we like is pointing at something, see if it's something we might want to interact with (like if they might want us to fetch something for them)
/datum/ai_controller/dog/proc/check_point(mob/pointing_friend, atom/movable/pointed_movable)
	SIGNAL_HANDLER

	var/mob/living/living_pawn = pawn
	if(IS_DEAD_OR_INCAP(living_pawn))
		return

	if(!COOLDOWN_FINISHED(src, command_cooldown))
		return
	if(pointed_movable == pawn || blackboard[BB_FETCH_TARGET] || !istype(pointed_movable) || blackboard[BB_DOG_ORDER_MODE] == DOG_COMMAND_NONE) // busy or no command
		return
	if(!can_see(pawn, pointing_friend, length=AI_DOG_VISION_RANGE) || !can_see(pawn, pointed_movable, length=AI_DOG_VISION_RANGE))
		return

	COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN)

	switch(blackboard[BB_DOG_ORDER_MODE])
		if(DOG_COMMAND_FETCH)
			if(!isitem(pointed_movable) || pointed_movable.anchored)
				return
			var/obj/item/pointed_item = pointed_movable
			if(pointed_item.obj_flags & ABSTRACT)
				return
			pawn.visible_message(span_notice("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and barks excitedly!"))
			current_movement_target = pointed_movable
			blackboard[BB_FETCH_TARGET] = pointed_movable
			blackboard[BB_FETCH_DELIVER_TO] = pointing_friend
			if(living_pawn.buckled)
				queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something
			queue_behavior(/datum/ai_behavior/fetch)
		if(DOG_COMMAND_ATTACK)
			pawn.visible_message(span_notice("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and growls intensely!"))
			current_movement_target = pointed_movable
			blackboard[BB_DOG_HARASS_TARGET] = WEAKREF(pointed_movable)
			if(living_pawn.buckled)
				queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something
			queue_behavior(/datum/ai_behavior/harass)


code:
/datum/ai_planning_subtree/dog
	COOLDOWN_DECLARE(heel_cooldown)
	COOLDOWN_DECLARE(reset_ignore_cooldown)

/datum/ai_planning_subtree/dog/SelectBehaviors(datum/ai_controller/dog/controller, delta_time)
	var/mob/living/living_pawn = controller.pawn

	// occasionally reset our ignore list
	if(COOLDOWN_FINISHED(src, reset_ignore_cooldown) && length(controller.blackboard[BB_FETCH_IGNORE_LIST]))
		COOLDOWN_START(src, reset_ignore_cooldown, AI_FETCH_IGNORE_DURATION)
		controller.blackboard[BB_FETCH_IGNORE_LIST] = list()

	// if we were just ordered to heel, chill out for a bit
	if(!COOLDOWN_FINISHED(src, heel_cooldown))
		return

	// if we're not already carrying something and we have a fetch target (and we're not already doing something with it), see if we can eat/equip it
	if(!controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_TARGET])
		var/atom/movable/interact_target = controller.blackboard[BB_FETCH_TARGET]
		if(in_range(living_pawn, interact_target) && (isturf(interact_target.loc)))
			controller.current_movement_target = interact_target
			if(IS_EDIBLE(interact_target))
				controller.queue_behavior(/datum/ai_behavior/eat_snack)
			else if(isitem(interact_target))
				controller.queue_behavior(/datum/ai_behavior/simple_equip)
			else
				controller.blackboard[BB_FETCH_TARGET] = null
				controller.blackboard[BB_FETCH_DELIVER_TO] = null
			return

	// if we're carrying something and we have a destination to deliver it, do that
	if(controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_DELIVER_TO])
		var/atom/return_target = controller.blackboard[BB_FETCH_DELIVER_TO]
		if(!can_see(controller.pawn, return_target, length=AI_DOG_VISION_RANGE))
			// if the return target isn't in sight, we'll just forget about it and carry the thing around
			controller.blackboard[BB_FETCH_DELIVER_TO] = null
			return
		controller.current_movement_target = return_target
		controller.queue_behavior(/datum/ai_behavior/deliver_item)
		return

repo_name: AnalWerewolf/sundown

license: mit


code:
/datum/ai_behavior/resist/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	living_pawn.resist()
	finish_action(controller, TRUE)

/datum/ai_behavior/battle_screech
	///List of possible screeches the behavior has
	var/list/screeches

/datum/ai_behavior/battle_screech/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	INVOKE_ASYNC(living_pawn, /mob.proc/emote, pick(screeches))
	finish_action(controller, TRUE)

///Moves to target then finishes
/datum/ai_behavior/move_to_target
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT

/datum/ai_behavior/move_to_target/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	finish_action(controller, TRUE)

/datum/ai_behavior/break_spine
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
	action_cooldown = 0.7 SECONDS
	var/give_up_distance = 10

/datum/ai_behavior/break_spine/setup(datum/ai_controller/controller, target_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[target_key]

/datum/ai_behavior/break_spine/perform(delta_time, datum/ai_controller/controller, target_key)
	var/mob/living/batman = controller.blackboard[target_key]
	var/mob/living/big_guy = controller.pawn //he was molded by the darkness

	if(batman.stat)
		finish_action(controller, TRUE, target_key)

	if(get_dist(batman, big_guy) >= give_up_distance)
		finish_action(controller, FALSE, target_key)

	big_guy.start_pulling(batman)
	big_guy.setDir(get_dir(big_guy, batman))

	batman.visible_message(span_warning("[batman] gets a slightly too tight hug from [big_guy]!"), span_userdanger("You feel your body break as [big_guy] embraces you!"))

	if(iscarbon(batman))
		var/mob/living/carbon/carbon_batman = batman
		for(var/obj/item/bodypart/bodypart_to_break in carbon_batman.bodyparts)
			if(bodypart_to_break.body_zone == BODY_ZONE_HEAD)
				continue
			bodypart_to_break.receive_damage(brute = 15, wound_bonus = 35)
	else
		batman.adjustBruteLoss(150)

	finish_action(controller, TRUE, target_key)

/datum/ai_behavior/break_spine/finish_action(datum/ai_controller/controller, succeeded, target_key)
	if(succeeded)
		controller.blackboard -= target_key
	return ..()

/// 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(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/pawn = controller.pawn
	var/obj/item/held = pawn.get_item_by_slot(pawn.get_active_hand())
	if(!held)
		finish_action(controller, FALSE)
		return
	pawn.activate_hand(pawn.get_active_hand())
	finish_action(controller, TRUE)

/// Use the currently held item, or unarmed, on an object in the world
/datum/ai_behavior/use_on_object
	required_distance = 1
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT

/datum/ai_behavior/use_on_object/setup(datum/ai_controller/controller, target_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[target_key]

/datum/ai_behavior/use_on_object/perform(delta_time, 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[BB_MONKEY_CURRENT_PRESS_TARGET]

	if(!target || !pawn.CanReach(target))
		finish_action(controller, FALSE)
		return

	pawn.set_combat_mode(FALSE)
	if(held_item)
		held_item.melee_attack_chain(pawn, target)
	else
		pawn.UnarmedAttack(target, TRUE)

	finish_action(controller, TRUE)

/datum/ai_behavior/give
	required_distance = 1
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT

/datum/ai_behavior/give/setup(datum/ai_controller/controller, target_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[target_key]

/datum/ai_behavior/give/perform(delta_time, datum/ai_controller/controller, target_key)
	. = ..()
	var/mob/living/pawn = controller.pawn
	var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand())
	var/atom/target = controller.blackboard[target_key]

	if(!target || !pawn.CanReach(target) || !isliving(target))
		finish_action(controller, FALSE)
		return

	var/mob/living/living_target = target
	controller.PauseAi(1.5 SECONDS)
	living_target.visible_message(
		span_info("[pawn] starts trying to give [held_item] to [living_target]!"),
		span_warning("[pawn] tries to give you [held_item]!")
	)
	if(!do_mob(pawn, living_target, 1 SECONDS))
		return
	if(QDELETED(held_item) || QDELETED(living_target))
		finish_action(controller, FALSE)
		return
	var/pocket_choice = prob(50) ? ITEM_SLOT_RPOCKET : ITEM_SLOT_LPOCKET
	if(prob(50) && living_target.can_put_in_hand(held_item))
		living_target.put_in_hand(held_item)
	else if(held_item.mob_can_equip(living_target, pawn, pocket_choice, TRUE))
		living_target.equip_to_slot(held_item, pocket_choice)

	finish_action(controller, TRUE)

/datum/ai_behavior/consume
	required_distance = 1
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
	action_cooldown = 2 SECONDS

/datum/ai_behavior/consume/setup(datum/ai_controller/controller, target_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[target_key]

/datum/ai_behavior/consume/perform(delta_time, 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(!(target in living_pawn.held_items))
		if(!living_pawn.put_in_hand_check(target))
			finish_action(controller, FALSE, target, hunger_timer_key)
			return

		living_pawn.put_in_hands(target)

	target.melee_attack_chain(living_pawn, living_pawn)

	if(QDELETED(target) || prob(10)) // Even if we don't finish it all we can randomly decide to be done
		finish_action(controller, TRUE, null, hunger_timer_key)

/datum/ai_behavior/consume/finish_action(datum/ai_controller/controller, succeeded, target_key, hunger_timer_key)
	. = ..()
	if(succeeded)
		controller.blackboard[hunger_timer_key] = world.time + rand(12 SECONDS, 60 SECONDS)

/**find and set
 * Finds an item near themselves, sets a blackboard key as it. Very useful for ais that need to use machines or something.
 * if you want to do something more complicated than find a single atom, change the search_tactic() proc
 * cool tip: search_tactic() can set lists
 */
/datum/ai_behavior/find_and_set
	action_cooldown = 5 SECONDS

/datum/ai_behavior/find_and_set/perform(delta_time, datum/ai_controller/controller, set_key, locate_path, search_range)
	. = ..()
	var/find_this_thing = search_tactic(controller, locate_path, search_range)
	if(find_this_thing)
		controller.blackboard[set_key] = find_this_thing
		finish_action(controller, TRUE)
	else
		finish_action(controller, FALSE)

/datum/ai_behavior/find_and_set/proc/search_tactic(datum/ai_controller/controller, locate_path, search_range)
	return locate(locate_path) in oview(search_range, controller.pawn)

/**
 * Variant of find and set that fails if the living pawn doesn't hold something
 */
/datum/ai_behavior/find_and_set/pawn_must_hold_item

/datum/ai_behavior/find_and_set/pawn_must_hold_item/search_tactic(datum/ai_controller/controller)
	var/mob/living/living_pawn = controller.pawn
	if(!living_pawn.get_num_held_items())
		return //we want to fail the search if we don't have something held
	. = ..()

/**
 * Variant of find and set that also requires the item to be edible. checks hands too
 */
/datum/ai_behavior/find_and_set/edible

/datum/ai_behavior/find_and_set/edible/search_tactic(datum/ai_controller/controller, locate_path, search_range)
	var/mob/living/living_pawn = controller.pawn
	var/list/food_candidates = list()
	for(var/held_candidate as anything in living_pawn.held_items)
		if(!held_candidate || !IsEdible(held_candidate))
			continue
		food_candidates += held_candidate

	var/list/local_results = locate(locate_path) in oview(search_range, controller.pawn)
	for(var/local_candidate in local_results)
		if(!IsEdible(local_candidate))
			continue
		food_candidates += local_candidate
	if(food_candidates.len)
		return pick(food_candidates)

/**
 * Variant of find and set that only checks in hands, search range should be excluded for this
 */
/datum/ai_behavior/find_and_set/in_hands

/datum/ai_behavior/find_and_set/in_hands/search_tactic(datum/ai_controller/controller, locate_path)
	var/mob/living/living_pawn = controller.pawn
	return locate(locate_path) in living_pawn.held_items

/**
 * Drops items in hands, very important for future behaviors that require the pawn to grab stuff
 */
/datum/ai_behavior/drop_item

/datum/ai_behavior/drop_item/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	var/obj/item/best_held = GetBestWeapon(controller, null, living_pawn.held_items)
	for(var/obj/item/held as anything in living_pawn.held_items)
		if(!held || held == best_held)
			continue
		living_pawn.dropItemToGround(held)

/// This behavior involves attacking a target.
/datum/ai_behavior/attack
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
	required_distance = 1

/datum/ai_behavior/attack/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	if(!istype(living_pawn) || !isturf(living_pawn.loc))
		return

	var/datum/weakref/attack_ref = controller.blackboard[BB_ATTACK_TARGET]
	var/atom/movable/attack_target = attack_ref?.resolve()
	if(!attack_target || !can_see(living_pawn, attack_target, length=controller.blackboard[BB_VISION_RANGE]))
		finish_action(controller, FALSE)
		return

	var/mob/living/living_target = attack_target
	if(istype(living_target) && (living_target.stat == DEAD))
		finish_action(controller, TRUE)
		return

	controller.current_movement_target = living_target
	attack(controller, living_target)

/datum/ai_behavior/attack/finish_action(datum/ai_controller/controller, succeeded)
	. = ..()
	controller.blackboard[BB_ATTACK_TARGET] = null

/// A proc representing when the mob is pushed to actually attack the target. Again, subtypes can be used to represent different attacks from different animals, or it can be some other generic behavior
/datum/ai_behavior/attack/proc/attack(datum/ai_controller/controller, mob/living/living_target)
	var/mob/living/living_pawn = controller.pawn
	if(!istype(living_pawn))
		return
	living_pawn.ClickOn(living_target, list())

/// This behavior involves attacking a target.
/datum/ai_behavior/follow
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
	required_distance = 1

/datum/ai_behavior/follow/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	if(!istype(living_pawn) || !isturf(living_pawn.loc))
		return

	var/datum/weakref/follow_ref = controller.blackboard[BB_FOLLOW_TARGET]
	var/atom/movable/follow_target = follow_ref?.resolve()
	if(!follow_target || get_dist(living_pawn, follow_target) > controller.blackboard[BB_VISION_RANGE])
		finish_action(controller, FALSE)
		return

	var/mob/living/living_target = follow_target
	if(istype(living_target) && (living_target.stat == DEAD))
		finish_action(controller, TRUE)
		return

	controller.current_movement_target = living_target

/datum/ai_behavior/follow/finish_action(datum/ai_controller/controller, succeeded)
	. = ..()
	controller.blackboard[BB_FOLLOW_TARGET] = null

/datum/ai_behavior/perform_emote

/datum/ai_behavior/perform_emote/perform(delta_time, datum/ai_controller/controller, emote)
	var/mob/living/living_pawn = controller.pawn
	if(!istype(living_pawn))
		return
	living_pawn.manual_emote(emote)
	finish_action(controller, TRUE)

/datum/ai_behavior/perform_speech

/datum/ai_behavior/perform_speech/perform(delta_time, datum/ai_controller/controller, speech)
	var/mob/living/living_pawn = controller.pawn
	if(!istype(living_pawn))
		return
	living_pawn.say(speech, forced = "AI Controller")
	finish_action(controller, TRUE)

//song behaviors

/datum/ai_behavior/setup_instrument

/datum/ai_behavior/setup_instrument/perform(delta_time, datum/ai_controller/controller, song_instrument_key, song_lines_key)
	. = ..()

	var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key]
	var/datum/song/song = song_instrument.song
	var/song_lines = controller.blackboard[song_lines_key]

	//just in case- it won't do anything if the instrument isn't playing
	song.stop_playing()
	song.ParseSong(song_lines)
	song.repeat = 10
	song.volume = song.max_volume - 10
	finish_action(controller, TRUE)

/datum/ai_behavior/play_instrument

/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller, song_instrument_key)
	. = ..()

	var/obj/item/instrument/song_instrument = controller.blackboard[song_instrument_key]
	var/datum/song/song = song_instrument.song

	song.start_playing(controller.pawn)
	finish_action(controller, TRUE)

/datum/ai_behavior/find_nearby

/datum/ai_behavior/find_nearby/perform(delta_time, datum/ai_controller/controller, target_key)
	. = ..()

	var/list/possible_targets = list()
	for(var/atom/thing in view(2, controller.pawn))
		if(!thing.mouse_opacity)
			continue
		if(thing.IsObscured())
			continue
		possible_targets += thing
	if(!possible_targets.len)
		finish_action(controller, FALSE)
	controller.blackboard[target_key] = pick(possible_targets)
	finish_action(controller, TRUE)


code:
/**
 * Generic Instrument Subtree, For your pawn playing instruments
 *
 * Requires at least a living mob that can hold items.
 *
 * relevant blackboards:
 * * BB_SONG_INSTRUMENT - set by this subtree, is the song datum the pawn plays music from.
 * * BB_SONG_LINES - not set by this subtree, is the song loaded into the song datum.
 */
/datum/ai_planning_subtree/generic_play_instrument/SelectBehaviors(datum/ai_controller/controller, delta_time)
	if(!controller.blackboard[BB_SONG_INSTRUMENT])
		controller.queue_behavior(/datum/ai_behavior/find_and_set/in_hands, BB_SONG_INSTRUMENT, /obj/item/instrument)
		return //we can't play a song since we do not have an instrument

	var/obj/item/instrument/song_player = controller.blackboard[BB_SONG_INSTRUMENT]

	var/list/parsed_song_lines = splittext(controller.blackboard[BB_SONG_LINES], "\n")
	popleft(parsed_song_lines) //remove BPM as it is parsed out
	if(!compare_list(song_player.song.lines, parsed_song_lines) || !song_player.song.repeat)
		controller.queue_behavior(/datum/ai_behavior/setup_instrument, BB_SONG_INSTRUMENT, BB_SONG_LINES)

	if(!song_player.song.playing) //we may stop playing if we weren't playing before, were setting up dk theme, or ran out of repeats (also causing setup behavior)
		controller.queue_behavior(/datum/ai_behavior/play_instrument, BB_SONG_INSTRUMENT)

/**
 * Generic Resist Subtree, resist if it makes sense to!
 *
 * Requires nothing beyond a living pawn, makes sense on a good amount of mobs since anything can get buckled.
 *
 * relevant blackboards:
 * * None!
 */
/datum/ai_planning_subtree/generic_resist/SelectBehaviors(datum/ai_controller/controller, delta_time)
	var/mob/living/living_pawn = controller.pawn

	if(SHOULD_RESIST(living_pawn) && DT_PROB(RESIST_SUBTREE_PROB, delta_time))
		controller.queue_behavior(/datum/ai_behavior/resist) //BRO IM ON FUCKING FIRE BRO
		return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY.

/**
 * Generic Hunger Subtree,
 *
 * Requires at least a living mob that can hold items.
 *
 * relevant blackboards:
 * * BB_NEXT_HUNGRY - set by this subtree, is when the controller is next hungry
 */
/datum/ai_planning_subtree/generic_hunger/SelectBehaviors(datum/ai_controller/controller, delta_time)
	//inits the blackboard timer
	if(!controller.blackboard[BB_NEXT_HUNGRY])
		controller.blackboard[BB_NEXT_HUNGRY] = world.time + rand(0, 30 SECONDS)

	if(world.time < controller.blackboard[BB_NEXT_HUNGRY])
		return

	if(!controller.blackboard[BB_FOOD_TARGET])
		controller.queue_behavior(/datum/ai_behavior/find_and_set/edible, BB_FOOD_TARGET, /obj/item, 2)
		return

	controller.queue_behavior(/datum/ai_behavior/drop_item)
	controller.queue_behavior(/datum/ai_behavior/consume, BB_FOOD_TARGET, BB_NEXT_HUNGRY)
	return SUBTREE_RETURN_FINISH_PLANNING


code:
/datum/ai_controller/haunted
	movement_delay = 0.4 SECONDS
	blackboard = list(
		BB_TO_HAUNT_LIST = list(),
		BB_LIKES_EQUIPPER = FALSE,
		BB_HAUNT_TARGET,
		BB_HAUNTED_THROW_ATTEMPT_COUNT,
	)
	planning_subtrees = list(/datum/ai_planning_subtree/haunted)
	idle_behavior = /datum/idle_behavior/idle_ghost_item

/datum/ai_controller/haunted/TryPossessPawn(atom/new_pawn)
	if(!isitem(new_pawn))
		return AI_CONTROLLER_INCOMPATIBLE
	RegisterSignal(new_pawn, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
	return ..() //Run parent at end

/datum/ai_controller/haunted/UnpossessPawn()
	UnregisterSignal(pawn, COMSIG_ITEM_EQUIPPED)
	return ..() //Run parent at end

///Signal response for when the item is picked up; stops listening for follow up equips, just waits for a drop.
/datum/ai_controller/haunted/proc/on_equip(datum/source, mob/equipper, slot)
	SIGNAL_HANDLER
	UnregisterSignal(pawn, COMSIG_ITEM_EQUIPPED)
	var/haunt_equipper = TRUE
	if(isliving(equipper))
		var/mob/living/possibly_cool = equipper
		if(possibly_cool.mob_biotypes & MOB_UNDEAD)
			haunt_equipper = FALSE
	if(!haunt_equipper)
		blackboard[BB_LIKES_EQUIPPER] = TRUE
	else
		var/list/hauntee_list = blackboard[BB_TO_HAUNT_LIST]
		hauntee_list[equipper] = hauntee_list[equipper] + HAUNTED_ITEM_AGGRO_ADDITION //You have now become one of the victims of the HAAAAUNTTIIIINNGGG OOOOOO~~~
	RegisterSignal(pawn, COMSIG_ITEM_DROPPED, .proc/on_dropped)
	SIGNAL_HANDLER

///Flip it so we listen for equip again but not for drop.
/datum/ai_controller/haunted/proc/on_dropped(datum/source, mob/user)
	SIGNAL_HANDLER
	RegisterSignal(pawn, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
	blackboard[BB_LIKES_EQUIPPER] = FALSE
	UnregisterSignal(pawn, COMSIG_ITEM_DROPPED)


code:
/datum/ai_planning_subtree/haunted/SelectBehaviors(datum/ai_controller/controller, delta_time)
	var/obj/item/item_pawn = controller.pawn

	if(ismob(item_pawn.loc)) //We're being held, maybe escape?
		if(controller.blackboard[BB_LIKES_EQUIPPER])//don't unequip from people it's okay with
			return
		if(DT_PROB(HAUNTED_ITEM_ESCAPE_GRASP_CHANCE, delta_time))
			controller.queue_behavior(/datum/ai_behavior/item_escape_grasp)
		return SUBTREE_RETURN_FINISH_PLANNING

	if(!DT_PROB(HAUNTED_ITEM_ATTACK_HAUNT_CHANCE, delta_time))
		return

	var/list/to_haunt_list = controller.blackboard[BB_TO_HAUNT_LIST]

	for(var/i in to_haunt_list)
		if(to_haunt_list[i] <= 0)
			continue
		var/mob/living/potential_target = i
		if(get_dist(potential_target, item_pawn) <= 7)
			controller.blackboard[BB_HAUNT_TARGET] = potential_target
			controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/haunted, BB_HAUNT_TARGET, BB_HAUNTED_THROW_ATTEMPT_COUNT)
			return SUBTREE_RETURN_FINISH_PLANNING


code:
/datum/ai_planning_subtree/find_and_hunt_target
	var/list/hunt_targets = list(/obj/effect/decal/cleanable/ants)

/datum/ai_planning_subtree/find_and_hunt_target/New()
	. = ..()
	hunt_targets = typecacheof(hunt_targets)

/datum/ai_planning_subtree/find_and_hunt_target/SelectBehaviors(datum/ai_controller/controller, delta_time)
	. = ..()
	if(controller.blackboard[BB_HUNTING_COOLDOWN] >= world.time)
		return
	var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]
	if(!target || QDELETED(target))
		controller.queue_behavior(/datum/ai_behavior/find_hunt_target, BB_CURRENT_HUNTING_TARGET, hunt_targets)
	else
		controller.queue_behavior(/datum/ai_behavior/hunt_target, BB_CURRENT_HUNTING_TARGET, BB_HUNTING_COOLDOWN)
		return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else

///Literaly for hunting specific mobs.
/datum/ai_behavior/find_hunt_target

/datum/ai_behavior/find_hunt_target/perform(delta_time, datum/ai_controller/controller, hunting_target_key, types_to_hunt)
	. = ..()

	var/mob/living/living_mob = controller.pawn

	for(var/possible_dinner in typecache_filter_list(range(2, living_mob), types_to_hunt))
		if(isliving(possible_dinner))
			var/mob/living/living_target = possible_dinner
			if(living_target.stat == DEAD) //bitch is dead
				continue
		if(can_see(living_mob, possible_dinner, 2))
			controller.blackboard[hunting_target_key] = possible_dinner
			finish_action(controller, TRUE)
			return

	finish_action(controller, FALSE)
/datum/ai_behavior/hunt_target
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT

/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[hunting_target_key]

/datum/ai_behavior/hunt_target/perform(delta_time, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key)
	. = ..()

	if(!controller.blackboard[hunting_target_key]) //Target is gone for some reason. forget about this task!
		controller[hunting_target_key] = null
		finish_action(controller, FALSE, hunting_target_key)
		return

	var/mob/living/hunter = controller.pawn
	var/atom/hunted = controller.blackboard[hunting_target_key]

	if(isliving(hunted)) // Are we hunting a living mob?
		var/mob/living/living_target = hunted
		hunter.manual_emote("chomps [living_target]!")
		living_target.death()

	else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs.
		hunter.manual_emote("chomps [hunted]!")
		qdel(hunted)
	finish_action(controller, TRUE, hunting_target_key, hunting_cooldown_key)
	return

/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key)
	. = ..()
	if(succeeded)
		controller.blackboard[hunting_cooldown_key] = world.time + SUCCESFUL_HUNT_COOLDOWN
	else if(hunting_target_key)
		controller.blackboard[hunting_target_key] = null


code:
/datum/idle_behavior

/datum/idle_behavior/proc/perform_idle_behavior(delta_time, datum/ai_controller/controller)
	return


code:
///Dog specific idle behavior.
/datum/idle_behavior/idle_dog/perform_idle_behavior(delta_time, datum/ai_controller/dog/controller)
	var/mob/living/living_pawn = controller.pawn
	if(!isturf(living_pawn.loc) || living_pawn.pulledby)
		return

	// if we were just ordered to heel, chill out for a bit
	if(!COOLDOWN_FINISHED(controller, heel_cooldown))
		return

	// if we're just ditzing around carrying something, occasionally print a message so people know we have something
	if(controller.blackboard[BB_SIMPLE_CARRY_ITEM] && DT_PROB(5, delta_time))
		var/obj/item/carry_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
		living_pawn.visible_message(span_notice("[living_pawn] gently teethes on \the [carry_item] in [living_pawn.p_their()] mouth."), vision_distance=COMBAT_MESSAGE_RANGE)

	if(DT_PROB(5, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE))
		var/move_dir = pick(GLOB.alldirs)
		living_pawn.Move(get_step(living_pawn, move_dir), move_dir)
	else if(DT_PROB(10, delta_time))
		living_pawn.manual_emote(pick("dances around.","chases [living_pawn.p_their()] tail!"))
		living_pawn.AddComponent(/datum/component/spinny)


code:
///If not held, teleport somewhere else
/datum/idle_behavior/idle_ghost_item
	///Chance for item to teleport somewhere else
	var/teleport_chance = 4

/datum/idle_behavior/idle_ghost_item/perform_idle_behavior(delta_time, datum/ai_controller/controller)
	var/obj/item/item_pawn = controller.pawn
	if(ismob(item_pawn.loc)) //Being held. dont teleport
		return
	if(DT_PROB(teleport_chance, delta_time))
		playsound(item_pawn.loc, 'sound/items/haunted/ghostitemattack.ogg', 100, TRUE)
		do_teleport(item_pawn, get_turf(item_pawn), 4, channel = TELEPORT_CHANNEL_MAGIC)


code:
/datum/idle_behavior/idle_monkey/perform_idle_behavior(delta_time, datum/ai_controller/controller)
	var/mob/living/living_pawn = controller.pawn

	if(DT_PROB(25, delta_time) && (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(DT_PROB(5, delta_time))
		INVOKE_ASYNC(living_pawn, /mob.proc/emote, pick("screech"))
	else if(DT_PROB(1, delta_time))
		INVOKE_ASYNC(living_pawn, /mob.proc/emote, pick("scratch","jump","roll","tail"))


code:
/datum/idle_behavior/idle_random_walk
	///Chance that the mob random walks per second
	var/walk_chance = 25

/datum/idle_behavior/idle_random_walk/perform_idle_behavior(delta_time, datum/ai_controller/controller)
	. = ..()
	var/mob/living/living_pawn = controller.pawn

	if(DT_PROB(walk_chance, delta_time) && (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)


code:
## Introduction

This is a step by step guide for making an AI Controller for your atom. It teaches the basics of each part of an AI Controller so the target for this guide is someone who doesn't know anything about Controllers and wants to hop in.

### Note on examples used

At the moment the quality of ai datums has some dubious code lying all around, and I wanted to show the best examples. So while I walk through this with the basic cow ai as an example, I do swap to other datums involving items, generic instrument planning, and some other stuff to help explain singular concepts. I make it clear later in the guide when I'm getting back to following along with filling out the cow ai, so watch out for that.

## Starting out

We're simply starting out with our definition of what we're modifying. Any atom can have an ai controller.

```dm
/mob/living/basic/cow
	name = "cow"
	desc = "Known for their milk, just don't tip them over."
```

## Initial AI Controller Definition

Next, we'll want to define the AI Controller. This is the "brain" of the AI. It starts as a type, but is turned into an instance once the object is instanced.

### Object Declaraction

For clarity, i've included all the variables we're going to set up but haven't yet as nulls. In reality, some of these are always expected to be something and you should take a look at the base controller for which.

```dm
/mob/living/basic/cow
    name = "cow"
    desc = "Known for their milk, just don't tip them over."

    ai_controller = /datum/ai_controller/basic_controller/cow

/datum/ai_controller/basic_controller/cow
	blackboard = list()

	ai_traits = null
	ai_movement = null
	idle_behavior = null
	planning_subtrees = list()

```

### AI Movement & Idle Behavior

AI Movement is a datum that decides how the AI you're making pathfinds. This has to at least be set to dumb movement, it cannot be null. We're making a basic mob, so we're just going to go inbetween complex and simple pathfinding with the `basic_avoidance` type.

```dm
/datum/ai_controller/basic_controller/cow
	blackboard = list()

	ai_traits = null
	ai_movement = /datum/ai_movement/basic_avoidance
	idle_behavior = null
	planning_subtrees = list()

```

Idle Behavior is very similar, datum that decides what the AI should do when it decides it doesn't need to do anything (No planned behaviors, we'll walk through that later). Cows having some idle movement sounds nice, so we're going to pick that.

```dm
/datum/ai_controller/basic_controller/cow
	blackboard = list()

	ai_traits = null
	ai_movement = /datum/ai_movement/basic_avoidance
	idle_behavior = /datum/idle_behavior/idle_random_walk
	planning_subtrees = list()

```

### AI Traits

AI traits are flags you can set to modify generic idle and movement behavior. In this case, we want farm animals to be able to be coralled, so we're going to add the `STOP_MOVING_WHEN_PULLED` flag.

```dm
/datum/ai_controller/basic_controller/cow
	blackboard = list()

	ai_traits = STOP_MOVING_WHEN_PULLED
	ai_movement = /datum/ai_movement/basic_avoidance
	idle_behavior = /datum/idle_behavior/idle_random_walk
	planning_subtrees = list()

```

### Blackboard?

The blackboard is the variables of the ai controller. They are set up by the subtrees that use them, or are defaults set by the ai controller that the subtrees read. As we don't have our subtrees set up, we don't know what the blackboard should have! We're going to come back to this.

## Subtrees

So we have all the fundamentals of the cow set in stone, but we do not have the actual behaviors that make cows... act like cows! We introduce these through subtrees. They're singletons that ai controllers hold references to that plan out each step of how an AI should act, loading up behaviors.

Let's take a look at a simple subtree:

```dm
/datum/ai_planning_subtree/item_throw_attack

/datum/ai_planning_subtree/item_throw_attack/SelectBehaviors(datum/ai_controller/controller, delta_time)
	var/obj/item/item_pawn = controller.pawn

	if(!controller.blackboard[BB_ITEM_TARGET] || !DT_PROB(ITEM_AGGRO_ATTACK_CHANCE, delta_time))
		return //no target, or didn't aggro

	controller.queue_behavior(controller.blackboard[BB_ITEM_MOVE_AND_ATTACK_TYPE], BB_ITEM_TARGET, BB_ITEM_THROW_ATTEMPT_COUNT)
	return SUBTREE_RETURN_FINISH_PLANNING
```

This subtree takes a blackboard named `BB_ITEM_TARGET`, the target of the item set by other subtrees, and if that exists alongside a probability to aggro, the subtree queues the behavior to attack that mob.

So, neat. When you have a target, queue an attack. This item attack subtree is pretty basic, but a more complicated one may queue different attacks depending on the target. How does this fit into the subtrees?

Let's look where it's used, specifically in the subtrees variable:

```dm
/datum/ai_controller/haunted
    planning_subtrees = list(
        ///this applies aggro for picking up the item
        /datum/ai_planning_subtree/item_ghost_resist,
        ///this picks targets from the aggro list
        /datum/ai_planning_subtree/item_target_from_aggro_list,
        ///this uses the target to attack.
        /datum/ai_planning_subtree/item_throw_attack,
    )
```

As you can see the subtrees go top to bottom on their processing. `SUBTREE_RETURN_FINISH_PLANNING` will prematurely end the subtrees, so we can be sure the ai will focus on the behaviors planned so far in a "priority list" kind of way.

Let's visualize this in a case where the subtrees should stop prematurely!

```dm
/datum/ai_controller/haunted
    planning_subtrees = list(
        ///someone is currently holding the item,
        ///preventing it from attacking!
        ///resist and end planning.
        /datum/ai_planning_subtree/item_ghost_resist,
        ///this does not fire this time around
        /datum/ai_planning_subtree/item_target_from_aggro_list,
        ///this does not fire this time around
        /datum/ai_planning_subtree/item_throw_attack,
    )
```

### Subtree Setup

Subtrees also have procs for when the mob first starts using them and when they stop. You can use this to make subtrees "react" to events via signals, and this is where we set defaults for blackboards if necessary (we want lists to be empty, not null!)

Example:

```dm
/datum/ai_planning_subtree/item_ghost_resist/SetupSubtree(datum/ai_controller/controller)
	RegisterSignal(controller.pawn, COMSIG_ITEM_EQUIPPED, .proc/on_equip)
	controller.blackboard[BB_LIKES_EQUIPPER] = FALSE
	controller.blackboard[BB_ITEM_AGGRO_LIST] = list()

/datum/ai_planning_subtree/item_ghost_resist/ForgetSubtree(datum/ai_controller/controller)
	UnregisterSignal(controller.pawn, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED))
```

### Lil' Subtree Warning

**Do not set blackboards on the subtree!** Subtrees are there to sort and optimize behavior selection, putting logic for setting blackboards is essentially skipping a behavior. I'm putting this here because unfortunately a lot of our current ai datum code has this exact mistake, and I'm hoping we can move on from it!

BAD:

```dm
	if(prob(50))
		var/list/possible_targets = list()
		for(var/atom/thing in view(2, living_pawn))
			if(!thing.mouse_opacity)
				continue
			if(thing.IsObscured())
				continue
			possible_targets += thing
		var/atom/target = pick(possible_targets)
		if(target)
			controller.blackboard[BB_MONKEY_CURRENT_PRESS_TARGET] = target
			controller.queue_behavior(/datum/ai_behavior/use_on_object, BB_MONKEY_CURRENT_PRESS_TARGET)
			return
```

GOOD:

```dm
	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
```

As you can see we're putting the search behavior... on a behavior! and since the planning subtree passes to other subtrees afterwards, the monkey will still find things to do. The next pass, if the search behavior was successful the action can be completed.

### Behaviors for subtrees

Finally, we've reached the final stop on this controller rabbit hole: Behaviors! These are what subtrees are planning, and the AI will do **these** from first planned all the way to the end, just like it runs through subtrees.

As before, let's take a look at a basic example of one:

```dm
/datum/ai_behavior/follow
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
	required_distance = 1

/datum/ai_behavior/follow/perform(delta_time, datum/ai_controller/controller, follow_key, range_key)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	if(!istype(living_pawn) || !isturf(living_pawn.loc))
		return

	var/datum/weakref/follow_ref = controller.blackboard[follow_key]
	var/atom/movable/follow_target = follow_ref?.resolve()
	if(!follow_target || get_dist(living_pawn, follow_target) > controller.blackboard[range_key])
		finish_action(controller, FALSE)
		return

	var/mob/living/living_target = follow_target
	if(istype(living_target) && (living_target.stat == DEAD))
		finish_action(controller, TRUE)
		return

	controller.current_movement_target = living_target

/datum/ai_behavior/follow/finish_action(datum/ai_controller/controller, succeeded, follow_key, range_key)
	. = ..()
	controller.blackboard[follow_key] = null
```

This behavior makes the ai move to one tile away and finish the action, only finishing the action if the target is dead (success) or out of range (fail). When the action finishes, the follow target is unset by finish_action() regardless of success. Nice!

The last important thing to know is that behaviors take the keys from subtree planning as arguments. **They do not search for the blackboards they need themselves.**

BAD:

```dm
/datum/ai_behavior/play_instrument

/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller)
	. = ..()

	//bzzt! using blackboard keys directly! let the subtree pass this in!
	var/datum/song/song = controller.blackboard[BB_SONG_DATUM]

	song.start_playing(controller.pawn)
	finish_action(controller, TRUE)
```

GOOD:

```dm
/datum/ai_behavior/play_instrument

/datum/ai_behavior/play_instrument/perform(delta_time, datum/ai_controller/controller, song_datum_key)
	. = ..()

	var/datum/song/song = controller.blackboard[song_datum_key]

	song.start_playing(controller.pawn)
	finish_action(controller, TRUE) //NOTE: you may forget, but this doesn't end the proc! return after it if you have code later
```

## "Okay, back to what we were doing!"

Wow, what a tangent! But it's important to understand subtree planning as it is the core of our AI. We have a subtree for the cows to occasionally make sounds, which can be interrupted by the tipping subtree (since cows can be tipped!) The Blackboard stays empty for our cows, since the tipped subtree does not have any blackboards it needs to read that could change per-ai controller. The Tipping blackboards are handled by the subtree's setup.

```dm
/datum/ai_controller/basic_controller/cow
	blackboard = list()

	ai_traits = STOP_MOVING_WHEN_PULLED
	ai_movement = /datum/ai_movement/basic_avoidance
	idle_behavior = /datum/idle_behavior/idle_random_walk
	planning_subtrees = list(
		/datum/ai_planning_subtree/tip_reaction,
		/datum/ai_planning_subtree/random_speech/cow,
	)
```

### Finished Product: A COW.

And... we're finished! The tip_reaction subtree hooks into signals and runs behaviors when the cow is tipped, the random speech occasionally plans speech, the idle behavior runs when no behaviors are planned, and the cow acts like a cow! We used a mob in this case because everyone knows how a cow works as it's a very simple creature, but AI Controllers work on anything! It's just as valid of a use case to make, say, the staff of animation apply AI Controllers to items.


code:
/datum/ai_behavior/battle_screech/monkey
	screeches = list("roar","screech")

/datum/ai_behavior/monkey_equip
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT

/datum/ai_behavior/monkey_equip/finish_action(datum/ai_controller/controller, success)
	. = ..()

	if(!success) //Don't try again on this item if we failed
		var/list/item_blacklist = controller.blackboard[BB_MONKEY_BLACKLISTITEMS]
		var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET]

		item_blacklist[target] = TRUE
		if(istype(controller, /datum/ai_controller/monkey)) //What the fuck
			controller.RegisterSignal(target, COMSIG_PARENT_QDELETING, /datum/ai_controller/monkey/proc/target_del)

	controller.blackboard[BB_MONKEY_PICKUPTARGET] = null

/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))
		finish_action(controller, FALSE)
		return

	if(!target)
		finish_action(controller, FALSE)
		return

	if(target.anchored) //Can't pick it up, so stop trying.
		finish_action(controller, FALSE)
		return

	// Strong weapon
	else if(target.force > best_force)
		living_pawn.drop_all_held_items()
		living_pawn.put_in_hands(target)
		controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] = target.force
		finish_action(controller, TRUE)
		return

	else if(target.slot_flags) //Clothing == top priority
		living_pawn.dropItemToGround(target, TRUE)
		living_pawn.update_icons()
		if(!living_pawn.equip_to_appropriate_slot(target))
			finish_action(controller, FALSE)
			return //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.
		finish_action(controller, TRUE)
		return

	// EVERYTHING ELSE
	else if(living_pawn.get_empty_held_index_for_side(LEFT_HANDS) || living_pawn.get_empty_held_index_for_side(RIGHT_HANDS))
		living_pawn.put_in_hands(target)
		finish_action(controller, TRUE)
		return

	finish_action(controller, FALSE)

/datum/ai_behavior/monkey_equip/ground
	required_distance = 0

/datum/ai_behavior/monkey_equip/ground/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	equip_item(controller)

/datum/ai_behavior/monkey_equip/pickpocket

/datum/ai_behavior/monkey_equip/pickpocket/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	if(controller.blackboard[BB_MONKEY_PICKPOCKETING]) //We are pickpocketing, don't do ANYTHING!!!!
		return
	INVOKE_ASYNC(src, .proc/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) || !living_pawn.CanReach(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.blackboard[BB_MONKEY_PICKPOCKETING] = TRUE

	var/success = FALSE

	if(do_mob(living_pawn, victim, MONKEY_ITEM_SNATCH_DELAY) && target && living_pawn.CanReach(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.temporarilyRemoveItemFromInventory(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.blackboard[BB_MONKEY_PICKPOCKETING] = FALSE
	controller.blackboard[BB_MONKEY_PICKUPTARGET] = null

/datum/ai_behavior/monkey_flee

/datum/ai_behavior/monkey_flee/perform(delta_time, datum/ai_controller/controller)
	. = ..()

	var/mob/living/living_pawn = controller.pawn

	if(living_pawn.health >= MONKEY_FLEE_HEALTH)
		finish_action(controller, TRUE) //we're back in bussiness

	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)
		walk_away(living_pawn, target, MONKEY_ENEMY_VISION, 5)
	else
		finish_action(controller, TRUE)

/datum/ai_behavior/monkey_attack_mob
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration

/datum/ai_behavior/monkey_attack_mob/setup(datum/ai_controller/controller, target_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]

/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller, target_key)
	. = ..()

	var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
	var/mob/living/living_pawn = controller.pawn

	if(!target || target.stat != CONSCIOUS)
		finish_action(controller, TRUE) //Target == owned

	if(isturf(target.loc) && !IS_DEAD_OR_INCAP(living_pawn)) // Check if they're a valid target
		// check if target has a weapon
		var/obj/item/W
		for(var/obj/item/I in target.held_items)
			if(!(I.item_flags & ABSTRACT))
				W = I
				break

		// if the target has a weapon, chance to disarm them
		if(W && DT_PROB(MONKEY_ATTACK_DISARM_PROB, delta_time))
			monkey_attack(controller, target, delta_time, TRUE)
		else
			monkey_attack(controller, target, delta_time, FALSE)

/datum/ai_behavior/monkey_attack_mob/finish_action(datum/ai_controller/controller, succeeded)
	. = ..()
	var/mob/living/living_pawn = controller.pawn
	walk(living_pawn, 0)
	controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = null

/// 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, delta_time, disarm)
	var/mob/living/living_pawn = controller.pawn

	if(living_pawn.next_move > world.time)
		return

	living_pawn.changeNext_move(CLICK_CD_MELEE) //We play fair

	var/obj/item/weapon = locate(/obj/item) in living_pawn.held_items

	living_pawn.face_atom(target)

	//living_pawn.set_combat_mode(TRUE) //SEPTIC EDIT REMOVAL
	//SEPTIC EDIT BEGIN
	SET_HARM_INTENT(living_pawn)
	//SEPTIC EDIT END

	if(isnull(controller.blackboard[BB_MONKEY_GUN_WORKED]))
		controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE

	// attack with weapon if we have one
	if(living_pawn.CanReach(target, weapon))
		if(weapon)
			weapon.melee_attack_chain(living_pawn, target)
		else
			living_pawn.UnarmedAttack(target, null, disarm ? list("right" = TRUE) : null) //Fake a right click if we're disarmin
		controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // We reset their memory of the gun being 'broken' if they accomplish some other attack
	else if(weapon)
		var/atom/real_target = target
		if(prob(10)) // Artificial miss
			real_target = pick(oview(2, target))

		var/obj/item/gun/gun = locate() in living_pawn.held_items
		var/can_shoot = gun?.can_shoot() || FALSE
		if(gun && controller.blackboard[BB_MONKEY_GUN_WORKED] && prob(95))
			// We attempt to attack even if we can't shoot so we get the effects of pulling the trigger
			gun.afterattack(real_target, living_pawn, FALSE)
			controller.blackboard[BB_MONKEY_GUN_WORKED] = can_shoot ? TRUE : prob(80) // Only 20% likely to notice it didn't work
			if(can_shoot)
				controller.blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] = TRUE
		else
			living_pawn.throw_item(real_target)
			controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // 'worked'

	// no de-aggro
	if(controller.blackboard[BB_MONKEY_AGGRESSIVE])
		return

	if(DT_PROB(MONKEY_HATRED_REDUCTION_PROB, delta_time))
		controller.blackboard[BB_MONKEY_ENEMIES][target]--

	// if we are not angry at our target, go back to idle
	if(controller.blackboard[BB_MONKEY_ENEMIES][target] <= 0)
		var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES]
		enemies.Remove(target)
		if(controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] == target)
			finish_action(controller, 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)
	. = ..()
	controller.current_movement_target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]

/datum/ai_behavior/disposal_mob/finish_action(datum/ai_controller/controller, succeeded, attack_target_key, disposal_target_key)
	. = ..()
	controller.blackboard[attack_target_key] = null //Reset attack target
	controller.blackboard[BB_MONKEY_DISPOSING] = FALSE //No longer disposing
	controller.blackboard[disposal_target_key] = null //No target disposal

/datum/ai_behavior/disposal_mob/perform(delta_time, datum/ai_controller/controller, attack_target_key, disposal_target_key)
	. = ..()

	if(controller.blackboard[BB_MONKEY_DISPOSING]) //We are disposing, don't do ANYTHING!!!!
		return

	var/mob/living/target = controller.blackboard[attack_target_key]
	var/mob/living/living_pawn = controller.pawn

	controller.current_movement_target = target

	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))
			target.grabbedby(living_pawn)
		return //Do the rest next turn

	var/obj/machinery/disposal/disposal = controller.blackboard[disposal_target_key]
	controller.current_movement_target = disposal

	if(living_pawn.Adjacent(disposal))
		INVOKE_ASYNC(src, .proc/try_disposal_mob, controller, attack_target_key, disposal_target_key) //put him in!
	else //This means we might be getting pissed!
		return

/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.blackboard[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(delta_time, datum/ai_controller/controller)
	. = ..()

	controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN
	var/mob/living/living_pawn = controller.pawn

	for(var/mob/living/L in view(living_pawn, MONKEY_ENEMY_VISION))
		if(!HAS_AI_CONTROLLER_TYPE(L, /datum/ai_controller/monkey))
			continue

		if(!DT_PROB(MONKEY_RECRUIT_PROB, delta_time))
			continue
		var/datum/ai_controller/monkey/monkey_ai = L.ai_controller
		var/atom/your_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
		var/list/enemies = L.ai_controller.blackboard[BB_MONKEY_ENEMIES]
		enemies[your_enemy] = MONKEY_RECRUIT_HATED_AMOUNT
		monkey_ai.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN
	finish_action(controller, TRUE)

/datum/ai_behavior/monkey_set_combat_target/perform(delta_time, datum/ai_controller/controller, set_key, enemies_key)
	var/list/enemies = controller.blackboard[enemies_key]
	var/list/valids = list()
	for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, controller.pawn))
		if(possible_enemy == controller.pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?)
			continue
		// Weighted list, so the closer they are the more likely they are to be chosen as the enemy
		valids[possible_enemy] = CEILING(100 / (get_dist(controller.pawn, possible_enemy) || 1), 1)

	if(!valids.len)
		finish_action(controller, FALSE)
	controller.blackboard[set_key] = pick_weight(valids)
	finish_action(controller, TRUE)


code:
/*
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
	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/generic_play_instrument,
		/datum/ai_planning_subtree/monkey_shenanigans,
	)
	blackboard = list(
		BB_MONKEY_AGGRESSIVE = FALSE,
		BB_MONKEY_BEST_FORCE_FOUND = 0,
		BB_MONKEY_ENEMIES = list(),
		BB_MONKEY_BLACKLISTITEMS = list(),
		BB_MONKEY_PICKUPTARGET = null,
		BB_MONKEY_PICKPOCKETING = FALSE,
		BB_MONKEY_DISPOSING = FALSE,
		BB_MONKEY_TARGET_DISPOSAL = null,
		BB_MONKEY_CURRENT_ATTACK_TARGET = null,
		BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE,
		BB_MONKEY_GUN_WORKED = TRUE,
		BB_SONG_LINES = MONKEY_SONG,
	)
	var/static/list/loc_connections = list(
		COMSIG_ATOM_ENTERED = .proc/on_entered,
	)
	idle_behavior = /datum/idle_behavior/idle_monkey

/datum/ai_controller/monkey/angry

/datum/ai_controller/monkey/angry/TryPossessPawn(atom/new_pawn)
	. = ..()
	if(. & AI_CONTROLLER_INCOMPATIBLE)
		return
	blackboard[BB_MONKEY_AGGRESSIVE] = TRUE //Angry cunt

/datum/ai_controller/monkey/TryPossessPawn(atom/new_pawn)
	if(!isliving(new_pawn))
		return AI_CONTROLLER_INCOMPATIBLE

	var/mob/living/living_pawn = new_pawn
	RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)
	RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, .proc/on_attack_hand)
	RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_PAW, .proc/on_attack_paw)
	RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_ANIMAL, .proc/on_attack_animal)
	RegisterSignal(new_pawn, COMSIG_MOB_ATTACK_ALIEN, .proc/on_attack_alien)
	RegisterSignal(new_pawn, COMSIG_ATOM_BULLET_ACT, .proc/on_bullet_act)
	RegisterSignal(new_pawn, COMSIG_ATOM_HITBY, .proc/on_hitby)
	RegisterSignal(new_pawn, COMSIG_LIVING_START_PULL, .proc/on_startpulling)
	RegisterSignal(new_pawn, COMSIG_LIVING_TRY_SYRINGE, .proc/on_try_syringe)
	RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, .proc/on_attack_hulk)
	RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, .proc/on_attempt_cuff)
	RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, .proc/update_movespeed)

	AddComponent(/datum/component/connect_loc_behalf, new_pawn, loc_connections)
	movement_delay = living_pawn.cached_multiplicative_slowdown
	return ..() //Run parent at end

/datum/ai_controller/monkey/UnpossessPawn(destroy)
	UnregisterSignal(pawn, list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_LIVING_START_PULL,\
	COMSIG_LIVING_TRY_SYRINGE, COMSIG_ATOM_HULK_ATTACK, COMSIG_CARBON_CUFF_ATTEMPTED, COMSIG_MOB_MOVESPEED_UPDATED, COMSIG_ATOM_ATTACK_ANIMAL, COMSIG_MOB_ATTACK_ALIEN))
	qdel(GetComponent(/datum/component/connect_loc_behalf))

	return ..() //Run parent at end

// Stops sentient monkeys from being knocked over like weak dunces.
/datum/ai_controller/monkey/on_sentience_gained()
	. = ..()
	qdel(GetComponent(/datum/component/connect_loc_behalf))

/datum/ai_controller/monkey/on_sentience_lost()
	. = ..()
	AddComponent(/datum/component/connect_loc_behalf, pawn, loc_connections)

/datum/ai_controller/monkey/able_to_run()
	. = ..()
	var/mob/living/living_pawn = pawn

	if(IS_DEAD_OR_INCAP(living_pawn))
		return FALSE

///re-used behavior pattern by monkeys for finding a weapon
/datum/ai_controller/monkey/proc/TryFindWeapon()
	var/mob/living/living_pawn = pawn

	if(!locate(/obj/item) in living_pawn.held_items)
		blackboard[BB_MONKEY_BEST_FORCE_FOUND] = 0

	if(blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] && (locate(/obj/item/gun) in 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))
		nearby_items += 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

	blackboard[BB_MONKEY_PICKUPTARGET] = weapon
	current_movement_target = weapon
	if(pickpocket)
		queue_behavior(/datum/ai_behavior/monkey_equip/pickpocket)
	else
		queue_behavior(/datum/ai_behavior/monkey_equip/ground)
	return TRUE

///Reactive events to being hit
/datum/ai_controller/monkey/proc/retaliate(mob/living/L)
	var/list/enemies = blackboard[BB_MONKEY_ENEMIES]
	enemies[L] += MONKEY_HATRED_AMOUNT

/datum/ai_controller/monkey/proc/on_attackby(datum/source, obj/item/I, mob/user)
	SIGNAL_HANDLER
	if(I.force && I.damtype != STAMINA)
		retaliate(user)

/datum/ai_controller/monkey/proc/on_attack_hand(datum/source, mob/living/user)
	SIGNAL_HANDLER
	if(prob(MONKEY_RETALIATE_PROB))
		retaliate(user)

/datum/ai_controller/monkey/proc/on_attack_paw(datum/source, mob/living/user)
	SIGNAL_HANDLER
	if(prob(MONKEY_RETALIATE_PROB))
		retaliate(user)

/datum/ai_controller/monkey/proc/on_attack_animal(datum/source, mob/living/user)
	SIGNAL_HANDLER
	if(user.melee_damage_upper > 0 && prob(MONKEY_RETALIATE_PROB))
		retaliate(user)

/datum/ai_controller/monkey/proc/on_attack_alien(datum/source, mob/living/user)
	SIGNAL_HANDLER
	if(prob(MONKEY_RETALIATE_PROB))
		retaliate(user)

/datum/ai_controller/monkey/proc/on_bullet_act(datum/source, obj/projectile/Proj)
	SIGNAL_HANDLER
	var/mob/living/living_pawn = pawn
	if(istype(Proj , /obj/projectile/beam)||istype(Proj, /obj/projectile/bullet))
		if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE))
			if(!Proj.nodamage && Proj.damage < living_pawn.health && isliving(Proj.firer))
				retaliate(Proj.firer)

/datum/ai_controller/monkey/proc/on_hitby(datum/source, atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum)
	SIGNAL_HANDLER
	if(istype(AM, /obj/item))
		var/mob/living/living_pawn = pawn
		var/obj/item/I = AM
		var/mob/thrown_by = I.thrownby?.resolve()
		if(I.throwforce < living_pawn.health && ishuman(thrown_by))
			var/mob/living/carbon/human/H = thrown_by
			retaliate(H)

/datum/ai_controller/monkey/proc/on_entered(datum/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
	SIGNAL_HANDLER
	var/mob/living/living_pawn = pawn
	if(!IS_DEAD_OR_INCAP(living_pawn) && isliving(arrived))
		var/mob/living/in_the_way_mob = arrived
		in_the_way_mob.knockOver(living_pawn)
		return

/datum/ai_controller/monkey/proc/on_startpulling(datum/source, atom/movable/puller, state, force)
	SIGNAL_HANDLER
	var/mob/living/living_pawn = pawn
	if(!IS_DEAD_OR_INCAP(living_pawn) && 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_attack_hulk(datum/source, mob/user)
	SIGNAL_HANDLER
	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/update_movespeed(mob/living/pawn)
	SIGNAL_HANDLER
	movement_delay = pawn.cached_multiplicative_slowdown

/datum/ai_controller/monkey/proc/target_del(target)
	SIGNAL_HANDLER
	blackboard[BB_MONKEY_BLACKLISTITEMS] -= target


code:
/datum/ai_planning_subtree/monkey_shenanigans/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)

	if(prob(5))
		controller.queue_behavior(/datum/ai_behavior/use_in_hand)

	if(!DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time))
		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)
		return

	if(prob(5))
		controller.queue_behavior(/datum/ai_behavior/give, BB_MONKEY_CURRENT_GIVE_TARGET)
		return SUBTREE_RETURN_FINISH_PLANNING

	controller.TryFindWeapon()

///monkey combat subtree.
/datum/ai_planning_subtree/monkey_combat/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)
	var/mob/living/living_pawn = controller.pawn
	var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES]

	if((HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) || (!length(enemies) && !controller.blackboard[BB_MONKEY_AGGRESSIVE])) //Pacifist, or we have no enemies and we're not pissed
		return

	if(!controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET])
		controller.queue_behavior(/datum/ai_behavior/monkey_set_combat_target, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_ENEMIES)
		return SUBTREE_RETURN_FINISH_PLANNING

	var/mob/living/selected_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]

	if(!selected_enemy.stat) //He's up, get him!
		if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle
			controller.queue_behavior(/datum/ai_behavior/monkey_flee)
			return SUBTREE_RETURN_FINISH_PLANNING //I'm running fuck you guys

		if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing.
			return SUBTREE_RETURN_FINISH_PLANNING

		if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time)
			controller.queue_behavior(/datum/ai_behavior/recruit_monkeys, BB_MONKEY_CURRENT_ATTACK_TARGET)
		controller.queue_behavior(/datum/ai_behavior/battle_screech/monkey)
		controller.queue_behavior(/datum/ai_behavior/monkey_attack_mob, BB_MONKEY_CURRENT_ATTACK_TARGET)
		return SUBTREE_RETURN_FINISH_PLANNING

	//by this point we have a target but they're down, let's try dumpstering this loser

	if(!controller.blackboard[BB_MONKEY_TARGET_DISPOSAL])
		controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_MONKEY_TARGET_DISPOSAL, /obj/machinery/disposal, MONKEY_ENEMY_VISION)
		return

	controller.queue_behavior(/datum/ai_behavior/disposal_mob, BB_MONKEY_CURRENT_ATTACK_TARGET, BB_MONKEY_TARGET_DISPOSAL)
	return SUBTREE_RETURN_FINISH_PLANNING


code:
///This datum is an abstract class that can be overriden for different types of movement
/datum/ai_movement
	///Assoc list ist of controllers that are currently moving as key, and what they are moving to as value
	var/list/moving_controllers = list()
	///Does this type require processing?
	var/requires_processing = TRUE
	///How many times a given controller can fail on their route before they just give up
	var/max_pathing_attempts

/datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance)
	controller.pathing_attempts = 0
	controller.blackboard[BB_CURRENT_MIN_MOVE_DISTANCE] = min_distance
	if(!moving_controllers.len && requires_processing)
		START_PROCESSING(SSai_movement, src)
	moving_controllers[controller] = current_movement_target

/datum/ai_movement/proc/stop_moving_towards(datum/ai_controller/controller)
	controller.pathing_attempts = 0
	moving_controllers -= controller

	if(!moving_controllers.len && requires_processing)
		STOP_PROCESSING(SSai_movement, src)


code:
///Uses Byond's basic obstacle avoidance mvovement
/datum/ai_movement/basic_avoidance
	requires_processing = TRUE
	max_pathing_attempts = 10

///Put your movement behavior in here!
/datum/ai_movement/basic_avoidance/process(delta_time)
	for(var/datum/ai_controller/controller as anything in moving_controllers)
		if(!COOLDOWN_FINISHED(controller, movement_cooldown))
			continue
		COOLDOWN_START(controller, movement_cooldown, controller.movement_delay)

		var/atom/movable/movable_pawn = controller.pawn

		var/can_move = TRUE

		if(controller.ai_traits & STOP_MOVING_WHEN_PULLED && movable_pawn.pulledby)
			can_move = FALSE

		if(!isturf(movable_pawn.loc)) //No moving if not on a turf
			can_move = FALSE

		var/current_loc = get_turf(movable_pawn)

		var/turf/target_turf = get_step_towards(movable_pawn, controller.current_movement_target)

		if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs) && can_move)
			step_to(movable_pawn, controller.current_movement_target, controller.blackboard[BB_CURRENT_MIN_MOVE_DISTANCE], controller.movement_delay)

		if(current_loc == get_turf(movable_pawn)) //Did we even move after trying to move?
			controller.pathing_attempts++
			if(controller.pathing_attempts >= max_pathing_attempts)
				controller.CancelActions()


code:
///The most braindead type of movement, bee-line to the target with no concern of whats infront of us.
/datum/ai_movement/dumb
	max_pathing_attempts = 16

///Put your movement behavior in here!
/datum/ai_movement/dumb/process(delta_time)
	for(var/datum/ai_controller/controller as anything in moving_controllers)
		if(!COOLDOWN_FINISHED(controller, movement_cooldown))
			continue
		COOLDOWN_START(controller, movement_cooldown, controller.movement_delay)

		var/atom/movable/movable_pawn = controller.pawn

		var/can_move = TRUE

		if(controller.ai_traits & STOP_MOVING_WHEN_PULLED && movable_pawn.pulledby)
			can_move = FALSE

		if(!isturf(movable_pawn.loc)) //No moving if not on a turf
			can_move = FALSE

		var/current_loc = get_turf(movable_pawn)

		var/turf/target_turf = get_step_towards(movable_pawn, controller.current_movement_target)

		if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs) && can_move)
			movable_pawn.Move(target_turf, get_dir(current_loc, target_turf))

		if(current_loc == get_turf(movable_pawn)) //Did we even move after trying to move?
			controller.pathing_attempts++
			if(controller.pathing_attempts >= max_pathing_attempts)
				controller.CancelActions()


code:
/**
 * This movement datum represents smart-pathing
 */
/datum/ai_movement/jps
	max_pathing_attempts = 4

///Put your movement behavior in here!
/datum/ai_movement/jps/process(delta_time)
	for(var/datum/ai_controller/controller as anything in moving_controllers)
		if(!COOLDOWN_FINISHED(controller, movement_cooldown))
			continue
		COOLDOWN_START(controller, movement_cooldown, controller.movement_delay)

		var/atom/movable/movable_pawn = controller.pawn
		if(!isturf(movable_pawn.loc)) //No moving if not on a turf
			continue

		if(controller.ai_traits & STOP_MOVING_WHEN_PULLED && movable_pawn.pulledby)
			continue

		var/minimum_distance = controller.max_target_distance
		// right now I'm just taking the shortest minimum distance of our current behaviors, at some point in the future
		// we should let whatever sets the current_movement_target also set the min distance and max path length
		// (or at least cache it on the controller)
		if(LAZYLEN(controller.current_behaviors))
			for(var/datum/ai_behavior/iter_behavior as anything in controller.current_behaviors)
				if(iter_behavior.required_distance < minimum_distance)
					minimum_distance = iter_behavior.required_distance

		if(get_dist(movable_pawn, controller.current_movement_target) <= minimum_distance)
			continue

		var/generate_path = FALSE // set to TRUE when we either have no path, or we failed a step
		if(length(controller.movement_path))
			var/turf/next_step = controller.movement_path[1]
			movable_pawn.Move(next_step)

			// this check if we're on exactly the next tile may be overly brittle for dense pawns who may get bumped slightly
			// to the side while moving but could maybe still follow their path without needing a whole new path
			if(get_turf(movable_pawn) == next_step)
				controller.movement_path.Cut(1,2)
			else
				generate_path = TRUE
		else
			generate_path = TRUE

		if(generate_path)
			if(!COOLDOWN_FINISHED(controller, repath_cooldown))
				continue
			controller.pathing_attempts++
			if(controller.pathing_attempts >= max_pathing_attempts)
				controller.CancelActions()
				continue

			COOLDOWN_START(controller, repath_cooldown, 2 SECONDS)
			controller.movement_path = get_path_to(movable_pawn, controller.current_movement_target, AI_MAX_PATH_LENGTH, minimum_distance, id=controller.get_access())

/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target)
	controller.movement_path = null
	return ..()

/datum/ai_movement/jps/stop_moving_towards(datum/ai_controller/controller)
	controller.movement_path = null
	return ..()


code:
/datum/ai_planning_subtree/cursed/SelectBehaviors(datum/ai_controller/controller, delta_time)
	var/obj/item/item_pawn = controller.pawn

	//make sure we have a target
	var/mob/living/carbon/curse_target = controller.blackboard[BB_ITEM_TARGET]

	if(curse_target && get_dist(curse_target, item_pawn) > ITEM_AGGRO_VIEW_RANGE)
		controller.blackboard[BB_ITEM_TARGET] = null
		return

	if(!controller.blackboard[BB_ITEM_TARGET])
		controller.queue_behavior(/datum/ai_behavior/find_and_set/item_target, BB_ITEM_TARGET, /mob/living/carbon, ITEM_AGGRO_VIEW_RANGE)

	controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/ghostly, BB_ITEM_TARGET, BB_ITEM_THROW_ATTEMPT_COUNT)


code:
///This behavior is for obj/items, it is used to free themselves out of the hands of whoever is holding them
/datum/ai_behavior/item_escape_grasp

/datum/ai_behavior/item_escape_grasp/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/obj/item/item_pawn = controller.pawn
	var/mob/item_holder = item_pawn.loc
	if(!istype(item_holder))
		finish_action(controller, FALSE) //We're no longer beind held. abort abort!!
	item_pawn.visible_message(span_warning("[item_pawn] slips out of the hands of [item_holder]!"))
	item_holder.dropItemToGround(item_pawn, TRUE)
	finish_action(controller, TRUE)

///This behavior is for obj/items, it is used to move closer to a target and throw themselves towards them.
/datum/ai_behavior/item_move_close_and_attack
	required_distance = 3
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
	action_cooldown = 20
	///Sound to use
	var/attack_sound
	///Max attempts to make
	var/max_attempts = 3

/datum/ai_behavior/item_move_close_and_attack/setup(datum/ai_controller/controller, aggro_list_key, target_key, throw_count_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[target_key]

/datum/ai_behavior/item_move_close_and_attack/perform(delta_time, datum/ai_controller/controller, aggro_list_key, target_key, throw_count_key)
	. = ..()
	var/obj/item/item_pawn = controller.pawn
	var/atom/throw_target = controller.blackboard[target_key]

	item_pawn.visible_message(span_warning("[item_pawn] hurls towards [throw_target]!"))
	item_pawn.throw_at(throw_target, rand(4,5), 9)
	playsound(item_pawn.loc, attack_sound, 100, TRUE)
	controller.blackboard[throw_count_key]++
	if(controller.blackboard[throw_count_key] >= max_attempts)
		finish_action(controller, TRUE, target_key, throw_count_key)

/datum/ai_behavior/item_move_close_and_attack/finish_action(datum/ai_controller/controller, succeeded, aggro_list_key, target_key, throw_count_key)
	. = ..()
	var/atom/throw_target = controller.blackboard[target_key]

	controller.blackboard -= target_key
	controller.blackboard[throw_count_key] = 0
	if(aggro_list_key)
		var/list/aggro_list = controller.blackboard[aggro_list_key]
		aggro_list[throw_target]--

/datum/ai_behavior/item_move_close_and_attack/ghostly
	attack_sound = 'sound/items/haunted/ghostitemattack.ogg'
	max_attempts = 4


code:
/datum/ai_behavior/vendor_crush
	behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
	///Time before machine can untilt itself after tilting
	var/untilt_cooldown = 1 SECONDS
	///Time to telegraph and tilt over
	var/time_to_tilt = 0.8 SECONDS

/datum/ai_behavior/vendor_crush/setup(datum/ai_controller/controller, target_key)
	. = ..()
	controller.current_movement_target = controller.blackboard[target_key]

/datum/ai_behavior/vendor_crush/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	if(controller.blackboard[BB_VENDING_BUSY_TILTING])
		return

	controller.ai_movement.stop_moving_towards(controller)
	controller.blackboard[BB_VENDING_BUSY_TILTING] = TRUE
	var/turf/target_turf = get_turf(controller.blackboard[BB_VENDING_CURRENT_TARGET])
	new /obj/effect/temp_visual/telegraphing/vending_machine_tilt(target_turf)
	addtimer(CALLBACK(src, .proc/tiltonmob, controller, target_turf), time_to_tilt)

/datum/ai_behavior/vendor_crush/proc/tiltonmob(datum/ai_controller/controller, turf/target_turf)
	var/obj/machinery/vending/vendor_pawn = controller.pawn
	if(vendor_pawn.tilt(target_turf)) //We hit something
		vendor_pawn.say(pick("Supersize this!", "Eat my shiny metal ass!", "Want to consume some of my products?", "SMASH!", "Don't you love these smashing prices!"))
		controller.blackboard[BB_VENDING_LAST_HIT_SUCCESFUL] = TRUE
	else
		vendor_pawn.say(pick("Get back here!", "Don't you want my well priced love?"))
		controller.blackboard[BB_VENDING_LAST_HIT_SUCCESFUL] = FALSE
	finish_action(controller, TRUE)

/datum/ai_behavior/vendor_crush/finish_action(datum/ai_controller/controller, succeeded)
	. = ..()
	controller.blackboard[BB_VENDING_BUSY_TILTING] = FALSE
	controller.blackboard[BB_VENDING_UNTILT_COOLDOWN] = world.time + untilt_cooldown

/datum/ai_behavior/vendor_rise_up //what a gamer
	///Time before machine can tilt again after untilting if last hit was a success
	var/succes_tilt_cooldown = 5 SECONDS

/datum/ai_behavior/vendor_rise_up/perform(delta_time, datum/ai_controller/controller)
	. = ..()
	var/obj/machinery/vending/vendor_pawn = controller.pawn
	vendor_pawn.visible_message(span_warning("[vendor_pawn] untilts itself!"))
	if(controller.blackboard[BB_VENDING_LAST_HIT_SUCCESFUL])
		controller.blackboard[BB_VENDING_TILT_COOLDOWN] = world.time + succes_tilt_cooldown
	vendor_pawn.untilt()
	finish_action(controller, TRUE)


code:
///AI controller for vending machine gone rogue, Don't try using this on anything else, it wont work.
/datum/ai_controller/vending_machine
	movement_delay = 0.4 SECONDS
	blackboard = list(BB_VENDING_CURRENT_TARGET = null,
	BB_VENDING_TILT_COOLDOWN = 0,
	BB_VENDING_UNTILT_COOLDOWN = 0,
	BB_VENDING_BUSY_TILTING = FALSE,
	BB_VENDING_LAST_HIT_SUCCESFUL = FALSE)
	var/vision_range = 7
	var/search_for_enemy_cooldown = 2 SECONDS

/datum/ai_controller/vending_machine/TryPossessPawn(atom/new_pawn)
	if(!istype(new_pawn, /obj/machinery/vending))
		return AI_CONTROLLER_INCOMPATIBLE
	var/obj/machinery/vending/vendor_pawn = new_pawn
	vendor_pawn.tiltable = FALSE  //Not manually tiltable by hitting it anymore. We are now aggressively doing it ourselves.
	vendor_pawn.AddElement(/datum/element/waddling)
	vendor_pawn.AddElement(/datum/element/footstep, FOOTSTEP_OBJ_MACHINE, 1, -6, sound_vary = TRUE)
	vendor_pawn.squish_damage = 15
	return ..() //Run parent at end

/datum/ai_controller/vending_machine/UnpossessPawn(destroy)
	var/obj/machinery/vending/vendor_pawn = pawn
	vendor_pawn.tiltable = TRUE
	vendor_pawn.RemoveElement(/datum/element/waddling)
	vendor_pawn.squish_damage = initial(vendor_pawn.squish_damage)
	RemoveElement(/datum/element/footstep, FOOTSTEP_OBJ_MACHINE, 1, -6, sound_vary = TRUE)
	return ..() //Run parent at end

/datum/ai_controller/vending_machine/SelectBehaviors(delta_time)
	current_behaviors = list()
	var/obj/machinery/vending/vendor_pawn = pawn

	if(vendor_pawn.tilted) //We're tilted, try to untilt
		if(blackboard[BB_VENDING_UNTILT_COOLDOWN] > world.time)
			return
		queue_behavior(/datum/ai_behavior/vendor_rise_up)
		return
	else //Not tilted, try to find target to tilt onto.
		if(blackboard[BB_VENDING_TILT_COOLDOWN] > world.time)
			return
		for(var/mob/living/living_target in oview(vision_range, pawn))
			if(living_target.stat || living_target.incorporeal_move) //They're already fucked up or incorporeal
				continue
			blackboard[BB_VENDING_CURRENT_TARGET] = living_target
			queue_behavior(/datum/ai_behavior/vendor_crush, BB_VENDING_CURRENT_TARGET)
			return
		blackboard[BB_VENDING_TILT_COOLDOWN] = world.time + search_for_enemy_cooldown


code:
///This code needs to be removed at some point as it doesn't actually utilize the AI.

/datum/ai_controller/hostile_friend
	blackboard = list(
		BB_HOSTILE_ORDER_MODE = null,
		BB_HOSTILE_FRIEND = null,
		BB_FOLLOW_TARGET = null,
		BB_ATTACK_TARGET = null,
		BB_VISION_RANGE = BB_HOSTILE_VISION_RANGE,
		BB_HOSTILE_ATTACK_WORD = "growls",
	)
	ai_movement = /datum/ai_movement/basic_avoidance
	idle_behavior = /datum/idle_behavior/idle_random_walk/hostile_tameable

	var/ride_penalty_movement = 1 SECONDS

	COOLDOWN_DECLARE(command_cooldown)

/datum/ai_controller/hostile_friend/process(delta_time)
	if(isliving(pawn))
		var/mob/living/living_pawn = pawn
		movement_delay = living_pawn.cached_multiplicative_slowdown
	return ..()

/datum/ai_controller/hostile_friend/TryPossessPawn(atom/new_pawn)
	if(!ishostile(new_pawn))
		return AI_CONTROLLER_INCOMPATIBLE

	RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, .proc/on_examined)
	RegisterSignal(new_pawn, COMSIG_CLICK_ALT, .proc/check_altclicked)
	RegisterSignal(new_pawn, COMSIG_RIDDEN_DRIVER_MOVE, .proc/on_ridden_driver_move)
	RegisterSignal(new_pawn, COMSIG_MOVABLE_PREBUCKLE, .proc/on_prebuckle)
	return ..() //Run parent at end

/datum/ai_controller/hostile_friend/UnpossessPawn(destroy)
	UnregisterSignal(pawn, list(
		COMSIG_ATOM_ATTACK_HAND,
		COMSIG_PARENT_EXAMINE,
		COMSIG_CLICK_ALT,
		COMSIG_LIVING_DEATH,
		COMSIG_PARENT_QDELETING
	))
	unfriend()
	return ..() //Run parent at end

/datum/ai_controller/hostile_friend/proc/on_prebuckle(mob/source, mob/living/buckler, force, buckle_mob_flags)
	SIGNAL_HANDLER
	if(force || ai_status == AI_STATUS_OFF)
		return
	if(WEAKREF(buckler) != blackboard[BB_HOSTILE_FRIEND])
		return COMPONENT_BLOCK_BUCKLE

/datum/ai_controller/hostile_friend/able_to_run()
	var/mob/living/living_pawn = pawn

	if(IS_DEAD_OR_INCAP(living_pawn))
		return FALSE
	return ..()

/datum/ai_controller/hostile_friend/get_access()
	var/mob/living/simple_animal/simple_pawn = pawn
	if(!istype(simple_pawn))
		return

	return simple_pawn.access_card

/datum/ai_controller/hostile_friend/proc/on_ridden_driver_move(atom/movable/movable_parent, mob/living/user, direction)
	SIGNAL_HANDLER
	PauseAi(ride_penalty_movement)

/// Befriends someone
/datum/ai_controller/hostile_friend/proc/befriend(mob/living/new_friend)
	var/datum/weakref/current_ref = blackboard[BB_HOSTILE_FRIEND]
	var/datum/weakref/friend_ref = WEAKREF(new_friend)
	var/mob/living/old_friend = current_ref?.resolve()
	if(old_friend)
		unfriend(old_friend)
	else
		blackboard[BB_HOSTILE_FRIEND] = null

	if(in_range(pawn, new_friend))
		new_friend.visible_message("<b>[pawn]</b> looks at [new_friend] in a friendly manner!", span_notice("[pawn] looks at you in a friendly manner!"))
	blackboard[BB_HOSTILE_FRIEND] = friend_ref
	RegisterSignal(new_friend, COMSIG_MOB_POINTED, .proc/check_point)
	RegisterSignal(new_friend, COMSIG_MOB_SAY, .proc/check_verbal_command)

/// Someone is being mean to us, take them off our friends (add actual enemies behavior later)
/datum/ai_controller/hostile_friend/proc/unfriend()
	var/datum/weakref/friend_ref = blackboard[BB_HOSTILE_FRIEND]
	var/mob/living/old_friend = friend_ref?.resolve()
	if(old_friend)
		UnregisterSignal(old_friend, list(COMSIG_MOB_POINTED, COMSIG_MOB_SAY))
	blackboard[BB_HOSTILE_FRIEND] = null

/// Someone is looking at us, if we're currently carrying something then show what it is, and include a message if they're our friend
/datum/ai_controller/hostile_friend/proc/on_examined(datum/source, mob/user, list/examine_text)
	SIGNAL_HANDLER

	if(blackboard[BB_HOSTILE_FRIEND] == WEAKREF(user))
		var/mob/living/living_pawn = pawn
		if(!IS_DEAD_OR_INCAP(living_pawn))
			examine_text += span_notice("[pawn.p_they(TRUE)] seem[pawn.p_s()] happy to see you!")

// next section is regarding commands

/// Someone alt clicked us, see if they're someone we should show the radial command menu to
/datum/ai_controller/hostile_friend/proc/check_altclicked(datum/source, mob/living/clicker)
	SIGNAL_HANDLER

	if(!COOLDOWN_FINISHED(src, command_cooldown))
		return
	if(!istype(clicker) || blackboard[BB_HOSTILE_FRIEND] == WEAKREF(clicker))
		return
	. = COMPONENT_CANCEL_CLICK_ALT
	INVOKE_ASYNC(src, .proc/command_radial, clicker)

/// Show the command radial menu
/datum/ai_controller/hostile_friend/proc/command_radial(mob/living/clicker)
	var/list/commands = list(
		COMMAND_STOP = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow"),
		COMMAND_FOLLOW = image(icon = 'icons/mob/actions/actions_spells.dmi', icon_state = "summons"),
		COMMAND_ATTACK = image(icon = 'icons/effects/effects.dmi', icon_state = "bite"),
		)

	var/choice = show_radial_menu(clicker, pawn, commands, custom_check = CALLBACK(src, .proc/check_menu, clicker), tooltips = TRUE)
	if(!choice || !check_menu(clicker))
		return
	set_command_mode(clicker, choice)

/datum/ai_controller/hostile_friend/proc/check_menu(mob/user)
	if(!istype(user))
		CRASH("A non-mob is trying to issue an order to [pawn].")
	if(user.incapacitated() || !can_see(user, pawn))
		return FALSE
	return TRUE

/// One of our friends said something, see if it's a valid command, and if so, take action
/datum/ai_controller/hostile_friend/proc/check_verbal_command(mob/speaker, speech_args)
	SIGNAL_HANDLER

	if(blackboard[BB_HOSTILE_FRIEND] != WEAKREF(speaker))
		return

	if(!COOLDOWN_FINISHED(src, command_cooldown))
		return

	var/mob/living/living_pawn = pawn
	if(IS_DEAD_OR_INCAP(living_pawn))
		return

	var/spoken_text = speech_args[SPEECH_MESSAGE] // probably should check for full words
	var/command
	if(findtext(spoken_text, "stop") || findtext(spoken_text, "stay"))
		command = COMMAND_STOP
	else if(findtext(spoken_text, "follow") || findtext(spoken_text, "come"))
		command = COMMAND_FOLLOW
	else if(findtext(spoken_text, "attack") || findtext(spoken_text, "sic"))
		command = COMMAND_ATTACK
	else
		return

	if(!can_see(pawn, speaker, length=blackboard[BB_VISION_RANGE]))
		return
	set_command_mode(speaker, command)

/// Whether we got here via radial menu or a verbal command, this is where we actually process what our new command will be
/datum/ai_controller/hostile_friend/proc/set_command_mode(mob/commander, command)
	COOLDOWN_START(src, command_cooldown, AI_HOSTILE_COMMAND_COOLDOWN)

	switch(command)
		// heel: stop what you're doing, relax and try not to do anything for a little bit
		if(COMMAND_STOP)
			pawn.visible_message(span_notice("[pawn] [blackboard[BB_HOSTILE_ATTACK_WORD]] at [commander]'s command, and [pawn.p_they()] stop[pawn.p_s()] obediently, awaiting further orders."))
			blackboard[BB_HOSTILE_ORDER_MODE] = HOSTILE_COMMAND_NONE
			CancelActions()
		// fetch: whatever the commander points to, try and bring it back
		if(COMMAND_FOLLOW)
			pawn.visible_message(span_notice("[pawn] [blackboard[BB_HOSTILE_ATTACK_WORD]] at [commander]'s command, and [pawn.p_they()] follow[pawn.p_s()] slightly in anticipation."))
			CancelActions()
			blackboard[BB_HOSTILE_ORDER_MODE] = HOSTILE_COMMAND_FOLLOW
			blackboard[BB_FOLLOW_TARGET] = WEAKREF(commander)
			current_movement_target = commander
			var/mob/living/living_pawn = pawn
			if(living_pawn.buckled)
				queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something
			queue_behavior(/datum/ai_behavior/follow)
		// attack: harass whoever the commander points to
		if(COMMAND_ATTACK)
			pawn.visible_message(span_danger("[pawn] [blackboard[BB_HOSTILE_ATTACK_WORD]] at [commander]'s command, and [pawn.p_they()] growl[pawn.p_s()] intensely.")) // imagine getting intimidated by a corgi
			CancelActions()
			blackboard[BB_HOSTILE_ORDER_MODE] = HOSTILE_COMMAND_ATTACK

/// Someone we like is pointing at something, see if it's something we might want to interact with (like if they might want us to fetch something for them)
/datum/ai_controller/hostile_friend/proc/check_point(mob/pointing_friend, atom/movable/pointed_movable)
	SIGNAL_HANDLER

	var/mob/living/simple_animal/hostile/living_pawn = pawn
	if(IS_DEAD_OR_INCAP(living_pawn))
		return

	if(!COOLDOWN_FINISHED(src, command_cooldown))
		return
	if(blackboard[BB_HOSTILE_FRIEND] == WEAKREF(pointed_movable) || pointed_movable == pawn || !istype(pointed_movable) || blackboard[BB_HOSTILE_ORDER_MODE] == HOSTILE_COMMAND_NONE) // busy or no command
		return
	if(!can_see(pawn, pointing_friend, length=blackboard[BB_VISION_RANGE]) || !can_see(pawn, pointed_movable, length=blackboard[BB_VISION_RANGE]))
		return

	CancelActions()
	COOLDOWN_START(src, command_cooldown, AI_HOSTILE_COMMAND_COOLDOWN)

	if(blackboard[BB_HOSTILE_ORDER_MODE] == HOSTILE_COMMAND_ATTACK)
		pawn.visible_message(span_notice("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and [blackboard[BB_HOSTILE_ATTACK_WORD]] intensely!"))
		current_movement_target = pointed_movable
		blackboard[BB_ATTACK_TARGET] = WEAKREF(pointed_movable)
		if(living_pawn.buckled)
			queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something
		queue_behavior(/datum/ai_behavior/attack)

/datum/idle_behavior/idle_random_walk/hostile_tameable
	walk_chance = 5