>1) //getting reverse direction throught swapping even and odd bits.((f & 01010101)<<1)|((f & 10101010)>>1)
- var/newg = cur.g + call(cur.source,dist)(T)
- if(CN)
- //is already in open list, check if it's a better way from the current turf
- CN.bf &= 15^r //we have no closed, so just cut off exceed dir.00001111 ^ reverse_dir.We don't need to expand to checked turf.
- if((newg < CN.g))
- if(call(cur.source,adjacent)(caller, T, id, simulated_only))
- CN.setp(cur,newg,CN.h,cur.nt+1)
- open.ReSort(CN)//reorder the changed element in the list
- else
- //is not already in open list, so add it
- if(call(cur.source,adjacent)(caller, T, id, simulated_only))
- CN = new(T,cur,newg,call(T,dist)(end),cur.nt+1,15^r)
- open.Insert(CN)
- openc[T] = CN
- cur.bf = 0
- CHECK_TICK
- //reverse the path to get it from start to finish
- if(path)
- for(var/i = 1 to round(0.5*path.len))
- path.Swap(i,path.len-i+1)
- openc = null
- //cleaning after us
- return path
-
-//Returns adjacent turfs in cardinal directions that are reachable
-//simulated_only controls whether only simulated turfs are considered or not
-
-/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only)
- var/list/L = new()
- var/turf/T
- var/static/space_type_cache = typecacheof(/turf/open/space)
-
- for(var/k in 1 to GLOB.cardinals.len)
- T = get_step(src,GLOB.cardinals[k])
- if(!T || (simulated_only && space_type_cache[T.type]))
- continue
- if(!T.density && !LinkBlockedWithAccess(T,caller, ID))
- L.Add(T)
- return L
-
-/turf/proc/reachableTurftest(caller, turf/T, ID, simulated_only)
- if(T && !T.density && !(simulated_only && SSpathfinder.space_type_cache[T.type]) && !LinkBlockedWithAccess(T,caller, ID))
- return TRUE
-
-//Returns adjacent turfs in cardinal directions that are reachable via atmos
-/turf/proc/reachableAdjacentAtmosTurfs()
- return atmos_adjacent_turfs
-
-/turf/proc/LinkBlockedWithAccess(turf/T, caller, ID)
- var/adir = get_dir(src, T)
- var/rdir = ((adir & MASK_ODD)<<1)|((adir & MASK_EVEN)>>1)
- for(var/obj/structure/window/W in src)
- if(!W.CanAStarPass(ID, adir))
- return TRUE
- for(var/obj/machinery/door/window/W in src)
- if(!W.CanAStarPass(ID, adir))
- return TRUE
- for(var/obj/O in T)
- if(!O.CanAStarPass(ID, rdir, caller))
- return TRUE
- for(var/obj/machinery/door/firedoor/border_only/W in src)
- if(!W.CanAStarPass(ID, adir, caller))
- return TRUE
-
- return FALSE
diff --git a/code/__HELPERS/heap.dm b/code/__HELPERS/heap.dm
index 1e369fd7e181..82ef9011bd09 100644
--- a/code/__HELPERS/heap.dm
+++ b/code/__HELPERS/heap.dm
@@ -1,39 +1,45 @@
//////////////////////
-//datum/Heap object
+//datum/heap object
//////////////////////
-/datum/Heap
+/datum/heap
var/list/L
var/cmp
-/datum/Heap/New(compare)
+/datum/heap/New(compare)
L = new()
cmp = compare
-/datum/Heap/proc/IsEmpty()
- return !L.len
+/datum/heap/Destroy(force, ...)
+ for(var/i in L) // because this is before the list helpers are loaded
+ qdel(i)
+ L = null
+ return ..()
+
+/datum/heap/proc/is_empty()
+ return !length(L)
//Insert and place at its position a new node in the heap
-/datum/Heap/proc/Insert(atom/A)
+/datum/heap/proc/insert(atom/A)
L.Add(A)
- Swim(L.len)
+ swim(L.len)
//removes and returns the first element of the heap
//(i.e the max or the min dependant on the comparison function)
-/datum/Heap/proc/Pop()
- if(!L.len)
+/datum/heap/proc/pop()
+ if(!length(L))
return 0
. = L[1]
- L[1] = L[L.len]
- L.Cut(L.len)
- if(L.len)
- Sink(1)
+ L[1] = L[length(L)]
+ L.Cut(length(L))
+ if(length(L))
+ sink(1)
//Get a node up to its right position in the heap
-/datum/Heap/proc/Swim(index)
+/datum/heap/proc/swim(index)
var/parent = round(index * 0.5)
while(parent > 0 && (call(cmp)(L[index],L[parent]) > 0))
@@ -42,17 +48,17 @@
parent = round(index * 0.5)
//Get a node down to its right position in the heap
-/datum/Heap/proc/Sink(index)
- var/g_child = GetGreaterChild(index)
+/datum/heap/proc/sink(index)
+ var/g_child = get_greater_child(index)
while(g_child > 0 && (call(cmp)(L[index],L[g_child]) < 0))
L.Swap(index,g_child)
index = g_child
- g_child = GetGreaterChild(index)
+ g_child = get_greater_child(index)
//Returns the greater (relative to the comparison proc) of a node children
//or 0 if there's no child
-/datum/Heap/proc/GetGreaterChild(index)
+/datum/heap/proc/get_greater_child(index)
if(index * 2 > L.len)
return 0
@@ -65,12 +71,12 @@
return index * 2
//Replaces a given node so it verify the heap condition
-/datum/Heap/proc/ReSort(atom/A)
+/datum/heap/proc/resort(atom/A)
var/index = L.Find(A)
- Swim(index)
- Sink(index)
+ swim(index)
+ sink(index)
-/datum/Heap/proc/List()
+/datum/heap/proc/List()
. = L.Copy()
diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm
new file mode 100644
index 000000000000..dc9231c0c93b
--- /dev/null
+++ b/code/__HELPERS/path.dm
@@ -0,0 +1,347 @@
+/**
+ * This file contains the stuff you need for using JPS (Jump Point Search) pathing, an alternative to A* that skips
+ * over large numbers of uninteresting tiles resulting in much quicker pathfinding solutions. Mind that diagonals
+ * cost the same as cardinal moves currently, so paths may look a bit strange, but should still be optimal.
+ */
+
+/**
+ * This is the proc you use whenever you want to have pathfinding more complex than "try stepping towards the thing"
+ *
+ * Arguments:
+ * * caller: The movable atom that's trying to find the path
+ * * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway
+ * * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite)
+ * * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example.
+ * * id: An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
+ * * simulated_only: Whether we consider turfs without atmos simulation (AKA do we want to ignore space)
+ * * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf
+ */
+/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude)
+ if(!caller || !get_turf(end))
+ return
+
+ var/l = SSpathfinder.mobs.getfree(caller)
+ while(!l)
+ stoplag(3)
+ l = SSpathfinder.mobs.getfree(caller)
+
+ var/list/path
+ var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude)
+ path = pathfind_datum.search()
+ qdel(pathfind_datum)
+
+ SSpathfinder.mobs.found(l)
+ return path
+
+/**
+ * A helper macro to see if it's possible to step from the first turf into the second one, minding things like door access and directional windows.
+ * Note that this can only be used inside the [datum/pathfind][pathfind datum] since it uses variables from said datum
+ * If you really want to optimize things, optimize this, cuz this gets called a lot
+ */
+#define CAN_STEP(cur_turf, next) (next && !next.density && cur_turf.Adjacent(next) && !(simulated_only && SSpathfinder.space_type_cache[next.type]) && !cur_turf.LinkBlockedWithAccess(next,caller, id) && (next != avoid))
+/// Another helper macro for JPS, for telling when a node has forced neighbors that need expanding
+#define STEP_NOT_HERE_BUT_THERE(cur_turf, dirA, dirB) ((!CAN_STEP(cur_turf, get_step(cur_turf, dirA)) && CAN_STEP(cur_turf, get_step(cur_turf, dirB))))
+
+/// The JPS Node datum represents a turf that we find interesting enough to add to the open list and possibly search for new tiles from
+/datum/jps_node
+ /// The turf associated with this node
+ var/turf/tile
+ /// The node we just came from
+ var/datum/jps_node/previous_node
+ /// The A* node weight (f_value = number_of_tiles + heuristic)
+ var/f_value
+ /// The A* node heuristic (a rough estimate of how far we are from the goal)
+ var/heuristic
+ /// How many steps it's taken to get here from the start (currently pulling double duty as steps taken & cost to get here, since all moves incl diagonals cost 1 rn)
+ var/number_tiles
+ /// How many steps it took to get here from the last node
+ var/jumps
+ /// Nodes store the endgoal so they can process their heuristic without a reference to the pathfind datum
+ var/turf/node_goal
+
+/datum/jps_node/New(turf/our_tile, datum/jps_node/incoming_previous_node, jumps_taken, turf/incoming_goal)
+ tile = our_tile
+ jumps = jumps_taken
+ if(incoming_goal) // if we have the goal argument, this must be the first/starting node
+ node_goal = incoming_goal
+ else if(incoming_previous_node) // if we have the parent, this is from a direct lateral/diagonal scan, we can fill it all out now
+ previous_node = incoming_previous_node
+ number_tiles = previous_node.number_tiles + jumps
+ node_goal = previous_node.node_goal
+ heuristic = get_dist(tile, node_goal)
+ f_value = number_tiles + heuristic
+ // otherwise, no parent node means this is from a subscan lateral scan, so we just need the tile for now until we call [datum/jps/proc/update_parent] on it
+
+/datum/jps_node/Destroy(force, ...)
+ previous_node = null
+ return ..()
+
+/datum/jps_node/proc/update_parent(datum/jps_node/new_parent)
+ previous_node = new_parent
+ node_goal = previous_node.node_goal
+ jumps = get_dist(tile, previous_node.tile)
+ number_tiles = previous_node.number_tiles + jumps
+ heuristic = get_dist(tile, node_goal)
+ f_value = number_tiles + heuristic
+
+/// TODO: Macro this to reduce proc overhead
+/proc/HeapPathWeightCompare(datum/jps_node/a, datum/jps_node/b)
+ return b.f_value - a.f_value
+
+/// The datum used to handle the JPS pathfinding, completely self-contained
+/datum/pathfind
+ /// The thing that we're actually trying to path for
+ var/atom/movable/caller
+ /// The turf where we started at
+ var/turf/start
+ /// The turf we're trying to path to (note that this won't track a moving target)
+ var/turf/end
+ /// The open list/stack we pop nodes out from (TODO: make this a normal list and macro-ize the heap operations to reduce proc overhead)
+ var/datum/heap/open
+ ///An assoc list that serves as the closed list & tracks what turfs came from where. Key is the turf, and the value is what turf it came from
+ var/list/sources
+ /// The list we compile at the end if successful to pass back
+ var/list/path
+
+ // general pathfinding vars/args
+ /// An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant
+ var/obj/item/card/id/id
+ /// How far away we have to get to the end target before we can call it quits
+ var/mintargetdist = 0
+ /// I don't know what this does vs , but they limit how far we can search before giving up on a path
+ var/max_distance = 30
+ /// Space is big and empty, if this is TRUE then we ignore pathing through unsimulated tiles
+ var/simulated_only
+ /// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through
+ var/turf/avoid
+
+/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid)
+ src.caller = caller
+ end = get_turf(goal)
+ open = new /datum/heap(/proc/HeapPathWeightCompare)
+ sources = new()
+ src.id = id
+ src.max_distance = max_distance
+ src.mintargetdist = mintargetdist
+ src.simulated_only = simulated_only
+ src.avoid = avoid
+
+/// The proc you use to run the search, returns a list with the steps to the destination if one is available, or nothing if one couldn't be found
+/datum/pathfind/proc/search()
+ start = get_turf(caller)
+ if(!start || !end)
+ stack_trace("Invalid A* start or destination")
+ return FALSE
+ if(start.z != end.z || start == end) //no pathfinding between z levels
+ return FALSE
+ if(max_distance && (max_distance < get_dist(start, end))) //if start turf is farther than max_distance from end turf, no need to do anything
+ return FALSE
+
+ //initialization
+ var/datum/jps_node/current_processed_node = new (start, -1, 0, end)
+ open.insert(current_processed_node)
+ sources[start] = start // i'm sure this is fine
+
+ //then run the main loop
+ while(!open.is_empty() && !path)
+ if(!caller)
+ return
+ current_processed_node = open.pop() //get the lower f_value turf in the open list
+ if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path
+ continue
+
+ var/turf/current_turf = current_processed_node.tile
+ for(var/scan_direction in list(EAST, WEST, NORTH, SOUTH))
+ lateral_scan_spec(current_turf, scan_direction, current_processed_node)
+
+ for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST))
+ diag_scan_spec(current_turf, scan_direction, current_processed_node)
+
+ CHECK_TICK
+
+ //we're done! reverse the path to get it from start to finish
+ if(path)
+ for(var/i = 1 to round(0.5 * length(path)))
+ path.Swap(i, length(path) - i + 1)
+ sources = null
+ qdel(open)
+ return path
+
+/// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search]
+/datum/pathfind/proc/unwind_path(datum/jps_node/unwind_node)
+ path = new()
+ var/turf/iter_turf = unwind_node.tile
+ path.Add(iter_turf)
+
+ while(unwind_node.previous_node)
+ var/dir_goal = get_dir(iter_turf, unwind_node.previous_node.tile)
+ for(var/i = 1 to unwind_node.jumps)
+ iter_turf = get_step(iter_turf,dir_goal)
+ path.Add(iter_turf)
+ unwind_node = unwind_node.previous_node
+
+/**
+ * For performing lateral scans from a given starting turf.
+ *
+ * These scans are called from both the main search loop, as well as subscans for diagonal scans, and they treat finding interesting turfs slightly differently.
+ * If we're doing a normal lateral scan, we already have a parent node supplied, so we just create the new node and immediately insert it into the heap, ezpz.
+ * If we're part of a subscan, we still need for the diagonal scan to generate a parent node, so we return a node datum with just the turf and let the diag scan
+ * proc handle transferring the values and inserting them into the heap.
+ *
+ * Arguments:
+ * * original_turf: What turf did we start this scan at?
+ * * heading: What direction are we going in? Obviously, should be cardinal
+ * * parent_node: Only given for normal lateral scans, if we don't have one, we're a diagonal subscan.
+*/
+/datum/pathfind/proc/lateral_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node)
+ var/steps_taken = 0
+
+ var/turf/current_turf = original_turf
+ var/turf/lag_turf = original_turf
+
+ while(TRUE)
+ if(path)
+ return
+ lag_turf = current_turf
+ current_turf = get_step(current_turf, heading)
+ steps_taken++
+ if(!CAN_STEP(lag_turf, current_turf))
+ return
+
+ if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist)))
+ var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken)
+ sources[current_turf] = original_turf
+ if(parent_node) // if this is a direct lateral scan we can wrap up, if it's a subscan from a diag, we need to let the diag make their node first, then finish
+ unwind_path(final_node)
+ return final_node
+ else if(sources[current_turf]) // already visited, essentially in the closed list
+ return
+ else
+ sources[current_turf] = original_turf
+
+ if(parent_node && parent_node.number_tiles + steps_taken > max_distance)
+ return
+
+ var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list?
+
+ switch(heading)
+ if(NORTH)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST))
+ interesting = TRUE
+ if(SOUTH)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST))
+ interesting = TRUE
+ if(EAST)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST))
+ interesting = TRUE
+ if(WEST)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST))
+ interesting = TRUE
+
+ if(interesting)
+ var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken)
+ if(parent_node) // if we're a diagonal subscan, we'll handle adding ourselves to the heap in the diag
+ open.insert(newnode)
+ return newnode
+
+/**
+ * For performing diagonal scans from a given starting turf.
+ *
+ * Unlike lateral scans, these only are called from the main search loop, so we don't need to worry about returning anything,
+ * though we do need to handle the return values of our lateral subscans of course.
+ *
+ * Arguments:
+ * * original_turf: What turf did we start this scan at?
+ * * heading: What direction are we going in? Obviously, should be diagonal
+ * * parent_node: We should always have a parent node for diagonals
+*/
+/datum/pathfind/proc/diag_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node)
+ var/steps_taken = 0
+ var/turf/current_turf = original_turf
+ var/turf/lag_turf = original_turf
+
+ while(TRUE)
+ if(path)
+ return
+ lag_turf = current_turf
+ current_turf = get_step(current_turf, heading)
+ steps_taken++
+ if(!CAN_STEP(lag_turf, current_turf))
+ return
+
+ if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist)))
+ var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken)
+ sources[current_turf] = original_turf
+ unwind_path(final_node)
+ return
+ else if(sources[current_turf]) // already visited, essentially in the closed list
+ return
+ else
+ sources[current_turf] = original_turf
+
+ if(parent_node.number_tiles + steps_taken > max_distance)
+ return
+
+ var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list?
+ var/datum/jps_node/possible_child_node // otherwise, did one of our lateral subscans turn up something?
+
+ switch(heading)
+ if(NORTHWEST)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST))
+ interesting = TRUE
+ else
+ possible_child_node = (lateral_scan_spec(current_turf, WEST) || lateral_scan_spec(current_turf, NORTH))
+ if(NORTHEAST)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST))
+ interesting = TRUE
+ else
+ possible_child_node = (lateral_scan_spec(current_turf, EAST) || lateral_scan_spec(current_turf, NORTH))
+ if(SOUTHWEST)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST))
+ interesting = TRUE
+ else
+ possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, WEST))
+ if(SOUTHEAST)
+ if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST))
+ interesting = TRUE
+ else
+ possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, EAST))
+
+ if(interesting || possible_child_node)
+ var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken)
+ open.insert(newnode)
+ if(possible_child_node)
+ possible_child_node.update_parent(newnode)
+ open.insert(possible_child_node)
+ if(possible_child_node.tile == end || (mintargetdist && (get_dist(possible_child_node.tile, end) <= mintargetdist)))
+ unwind_path(possible_child_node)
+ return
+
+/**
+ * For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags
+ *
+ * Arguments:
+ * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach
+ * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf
+ * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space?
+*/
+/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID)
+ var/actual_dir = get_dir(src, destination_turf)
+
+ for(var/obj/structure/window/iter_window in src)
+ if(!iter_window.CanAStarPass(ID, actual_dir))
+ return TRUE
+
+ for(var/obj/machinery/door/window/iter_windoor in src)
+ if(!iter_windoor.CanAStarPass(ID, actual_dir))
+ return TRUE
+
+ var/reverse_dir = get_dir(destination_turf, src)
+ for(var/obj/iter_object in destination_turf)
+ if(!iter_object.CanAStarPass(ID, reverse_dir, caller))
+ return TRUE
+
+ return FALSE
+
+#undef CAN_STEP
+#undef STEP_NOT_HERE_BUT_THERE
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index a21147e7860e..12bf5edd6ff6 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -468,6 +468,8 @@ Turf and target are separate in case you want to teleport some distance from a t
/proc/can_see(atom/source, atom/target, length=5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate.
var/turf/current = get_turf(source)
var/turf/target_turf = get_turf(target)
+ if(get_dist(source, target) > length)
+ return FALSE
var/steps = 1
if(current != target_turf)
current = get_step_towards(current, target_turf)
diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm
index fb00d8bdf283..f8140f71e978 100644
--- a/code/_globalvars/lists/mobs.dm
+++ b/code/_globalvars/lists/mobs.dm
@@ -8,6 +8,12 @@ GLOBAL_PROTECT(mentors)
GLOBAL_LIST_EMPTY_TYPED(directory, /client) //all ckeys with associated client
GLOBAL_LIST_EMPTY(stealthminID) //reference list with IDs that store ckeys, for stealthmins
+GLOBAL_LIST_INIT(dangerous_turfs, typecacheof(list(
+ /turf/open/lava,
+ /turf/open/chasm,
+ /turf/open/space,
+ /turf/open/openspace)))
+
//Since it didn't really belong in any other category, I'm putting this here
//This is for procs to replace all the goddamn 'in world's that are chilling around the code
diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm
index c387afaace70..475ec8a10c26 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -345,11 +345,12 @@
A.AltClick(src)
/atom/proc/AltClick(mob/user)
- var/result = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user)
+ . = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user)
+ if(. & COMPONENT_CANCEL_CLICK_ALT)
+ return
var/turf/T = get_turf(src)
if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T))
user.set_listed_turf(T)
- return result
/// Use this instead of [/mob/proc/AltClickOn] where you only want turf content listing without additional atom alt-click interaction
/atom/proc/AltClickNoInteract(mob/user, atom/A)
diff --git a/code/controllers/subsystem/ai_controllers.dm b/code/controllers/subsystem/ai_controllers.dm
new file mode 100644
index 000000000000..5319d7316fb9
--- /dev/null
+++ b/code/controllers/subsystem/ai_controllers.dm
@@ -0,0 +1,33 @@
+/// The subsystem used to tick [/datum/ai_controllers] instances. Handling the re-checking of plans.
+SUBSYSTEM_DEF(ai_controllers)
+ name = "AI Controller Ticker"
+ flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
+ priority = FIRE_PRIORITY_NPC
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ init_order = INIT_ORDER_AI_CONTROLLERS
+ wait = 0.5 SECONDS //Plan every half second if required, not great not terrible.
+
+ ///List of all ai_subtree singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_subtrees()
+ var/list/ai_subtrees = list()
+ ///List of all ai controllers currently running
+ var/list/active_ai_controllers = list()
+
+/datum/controller/subsystem/ai_controllers/Initialize(timeofday)
+ setup_subtrees()
+ return ..()
+
+/datum/controller/subsystem/ai_controllers/proc/setup_subtrees()
+ ai_subtrees = list()
+ for(var/subtree_type in subtypesof(/datum/ai_planning_subtree))
+ var/datum/ai_planning_subtree/subtree = new subtree_type
+ ai_subtrees[subtree_type] = subtree
+
+/datum/controller/subsystem/ai_controllers/fire(resumed)
+ for(var/datum/ai_controller/ai_controller as anything in active_ai_controllers)
+ if(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown))
+ continue
+
+ if(!LAZYLEN(ai_controller.current_behaviors))
+ ai_controller.SelectBehaviors(wait * 0.1)
+ if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan
+ COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN)
diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm
index 21ee7ea60b3c..12ed31d0af7f 100644
--- a/code/controllers/subsystem/pathfinder.dm
+++ b/code/controllers/subsystem/pathfinder.dm
@@ -3,13 +3,11 @@ SUBSYSTEM_DEF(pathfinder)
init_order = INIT_ORDER_PATH
flags = SS_NO_FIRE
var/datum/flowcache/mobs
- var/datum/flowcache/circuits
var/static/space_type_cache
/datum/controller/subsystem/pathfinder/Initialize()
space_type_cache = typecacheof(/turf/open/space)
mobs = new(10)
- circuits = new(3)
return ..()
/datum/flowcache
diff --git a/code/controllers/subsystem/processing/ai_behaviors.dm b/code/controllers/subsystem/processing/ai_behaviors.dm
new file mode 100644
index 000000000000..4c98567405cc
--- /dev/null
+++ b/code/controllers/subsystem/processing/ai_behaviors.dm
@@ -0,0 +1,20 @@
+/// The subsystem used to tick [/datum/ai_behavior] instances. Handling the individual actions an AI can take like punching someone in the fucking NUTS
+PROCESSING_SUBSYSTEM_DEF(ai_behaviors)
+ name = "AI Behavior Ticker"
+ flags = SS_POST_FIRE_TIMING|SS_BACKGROUND
+ priority = FIRE_PRIORITY_NPC_ACTIONS
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ init_order = INIT_ORDER_AI_CONTROLLERS
+ wait = 1
+ ///List of all ai_behavior singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors()
+ var/list/ai_behaviors
+
+/datum/controller/subsystem/processing/ai_behaviors/Initialize(timeofday)
+ SetupAIBehaviors()
+ return ..()
+
+/datum/controller/subsystem/processing/ai_behaviors/proc/SetupAIBehaviors()
+ ai_behaviors = list()
+ for(var/behavior_type in subtypesof(/datum/ai_behavior))
+ var/datum/ai_behavior/ai_behavior = new behavior_type
+ ai_behaviors[behavior_type] = ai_behavior
diff --git a/code/controllers/subsystem/processing/ai_movement.dm b/code/controllers/subsystem/processing/ai_movement.dm
new file mode 100644
index 000000000000..6a6d64548ca7
--- /dev/null
+++ b/code/controllers/subsystem/processing/ai_movement.dm
@@ -0,0 +1,21 @@
+/// The subsystem used to tick [/datum/ai_movement] instances. Handling the movement of individual AI instances
+PROCESSING_SUBSYSTEM_DEF(ai_movement)
+ name = "AI movement"
+ flags = SS_KEEP_TIMING|SS_BACKGROUND
+ priority = FIRE_PRIORITY_NPC_MOVEMENT
+ runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
+ init_order = INIT_ORDER_AI_MOVEMENT
+ wait = 1
+
+ ///an assoc list of all ai_movement types. Assoc type to instance
+ var/list/movement_types
+
+/datum/controller/subsystem/processing/ai_movement/Initialize(timeofday)
+ SetupAIMovementInstances()
+ return ..()
+
+/datum/controller/subsystem/processing/ai_movement/proc/SetupAIMovementInstances()
+ movement_types = list()
+ for(var/key as anything in subtypesof(/datum/ai_movement))
+ var/datum/ai_movement/ai_movement = new key
+ movement_types[key] = ai_movement
diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm
index b4ad1d56df7e..c4dc415d0080 100644
--- a/code/controllers/subsystem/processing/processing.dm
+++ b/code/controllers/subsystem/processing/processing.dm
@@ -4,7 +4,7 @@ SUBSYSTEM_DEF(processing)
name = "Processing"
priority = FIRE_PRIORITY_PROCESS
flags = SS_BACKGROUND|SS_POST_FIRE_TIMING|SS_NO_INIT
- wait = 10
+ wait = 1 SECONDS
var/stat_tag = "P" //Used for logging
var/list/processing = list()
@@ -31,12 +31,12 @@ SUBSYSTEM_DEF(processing)
current_run.len--
if(QDELETED(thing))
processing -= thing
- else if(thing.process(wait) == PROCESS_KILL)
+ else if(thing.process(wait * 0.1) == PROCESS_KILL)
// fully stop so that a future START_PROCESSING will work
STOP_PROCESSING(src, thing)
if (MC_TICK_CHECK)
return
-/datum/proc/process()
+/datum/proc/process(delta_time)
set waitfor = 0
return PROCESS_KILL
diff --git a/code/controllers/subsystem/throwing.dm b/code/controllers/subsystem/throwing.dm
index b64dab12d301..3d78d5871779 100644
--- a/code/controllers/subsystem/throwing.dm
+++ b/code/controllers/subsystem/throwing.dm
@@ -207,4 +207,7 @@ SUBSYSTEM_DEF(throwing)
if(T && thrownthing.has_gravity(T))
T.zFall(thrownthing)
+ if(thrownthing)
+ SEND_SIGNAL(thrownthing, COMSIG_MOVABLE_THROW_LANDED, src)
+
qdel(src)
diff --git a/code/datums/ai/README.md b/code/datums/ai/README.md
new file mode 100644
index 000000000000..f219b11bb247
--- /dev/null
+++ b/code/datums/ai/README.md
@@ -0,0 +1,21 @@
+# AI controllers
+
+## Introduction
+
+Our AI controller system is an attempt at making it possible to create modularized AI that stores its behavior in datums, while keeping state and decision making in a controller. This allows a more versatile way of creating AI that doesn't rely on OOP as much, and doesn't clutter up the Life() code in Mobs.
+
+## AI Controllers
+
+A datum that can be added to any atom in the game. Similarly to components, they might only support a given subtype (e.g. /mob/living), but the idea is that theoretically, you could apply a specific AI controller to a big a group of different types as possible and it would still work.
+
+These datums handle both the normal movement of mobs, but also their decision making, deciding which actions they will take based on the checks you put into their SelectBehaviors proc.
+
+If behaviors are selected, and the AI is in range, it will try to perform them. It runs all the behaviors it currently has in parallel; allowing for it to for example screech at someone while trying to attack them. Aslong as it has behaviors running, it will not try to generate new plans, making it not waste CPU when it already has an active goal.
+
+They also hold data for any of the actions they might need to use, such as cooldowns, whether or not they're currently fighting, etcetera this is stored in the blackboard, more information on that below.
+
+### Blackboard
+The blackboard is an associated list keyed with strings and with values of whatever you want. These store information the mob has such as "Am I attacking someone", "Do I have a weapon". By using an associated list like this, no data needs to be stored on the actions themselves, and you could make actions that work on multiple ai controllers if you so pleased by making the key to use a variable.
+
+## AI Behavior
+AI behaviors are the actions an AI can take. These can range from "Do an emote" to "Attack this target until he is dead". They are singletons and should contain nothing but static data. Any dynamic data should be stored in the blackboard, to allow different controllers to use the same behaviors.
diff --git a/code/datums/ai/_ai_behavoir.dm b/code/datums/ai/_ai_behavoir.dm
new file mode 100644
index 000000000000..fad64f6e97d6
--- /dev/null
+++ b/code/datums/ai/_ai_behavoir.dm
@@ -0,0 +1,25 @@
+///Abstract class for an action an AI can take, can range from movement to grabbing a nearby weapon.
+/datum/ai_behavior
+ ///What distance you need to be from the target to perform the action
+ var/required_distance = 1
+ ///Flags for extra behavior
+ var/behavior_flags = NONE
+ ///Cooldown between actions performances
+ var/action_cooldown = 0
+
+/// Called by the ai controller when first being added. Additional arguments depend on the behavior type.
+/// Return FALSE to cancel
+/datum/ai_behavior/proc/setup(datum/ai_controller/controller, ...)
+ return TRUE
+
+///Called by the AI controller when this action is performed
+/datum/ai_behavior/proc/perform(delta_time, datum/ai_controller/controller, ...)
+ controller.behavior_cooldowns[src] = world.time + action_cooldown
+ return
+
+///Called when the action is finished.
+/datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded)
+ controller.current_behaviors.Remove(src)
+ controller.behavior_args -= type
+ if(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //If this was a movement task, reset our movement target.
+ controller.current_movement_target = null
diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm
new file mode 100644
index 000000000000..ce11df446aa4
--- /dev/null
+++ b/code/datums/ai/_ai_controller.dm
@@ -0,0 +1,254 @@
+/*
+AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a atom. What this means is that these datums
+have ways of interacting with a specific atom and control it. They posses a blackboard with the information the AI knows and has, and will plan behaviors it will try to execute through
+multiple modular subtrees with behaviors
+*/
+
+/datum/ai_controller
+ ///The atom this controller is controlling
+ var/atom/pawn
+ ///Bitfield of traits for this AI to handle extra behavior
+ var/ai_traits
+ ///Current actions being performed by the AI.
+ var/list/current_behaviors
+ ///Current actions and their respective last time ran as an assoc list.
+ var/list/behavior_cooldowns = list()
+ ///Current status of AI (OFF/ON/IDLE)
+ var/ai_status
+ ///Current movement target of the AI, generally set by decision making.
+ var/atom/current_movement_target
+ ///Delay between atom movements, if this is not a multiplication of the delay in
+ var/move_delay
+ ///This is a list of variables the AI uses and can be mutated by actions. When an action is performed you pass this list and any relevant keys for the variables it can mutate.
+ var/list/blackboard = list()
+ ///Stored arguments for behaviors given during their initial creation
+ var/list/behavior_args = list()
+ ///Tracks recent pathing attempts, if we fail too many in a row we fail our current plans.
+ var/pathing_attempts
+ ///Can the AI remain in control if there is a client?
+ var/continue_processing_when_client = FALSE
+ ///distance to give up on target
+ var/max_target_distance = 14
+ ///Cooldown for new plans, to prevent AI from going nuts if it can't think of new plans and looping on end
+ COOLDOWN_DECLARE(failed_planning_cooldown)
+ ///All subtrees this AI has available, will run them in order, so make sure they're in the order you want them to run. On initialization of this type, it will start as a typepath(s) and get converted to references of ai_subtrees found in SSai_controllers when init_subtrees() is called
+ var/list/planning_subtrees
+
+ // Movement related things here
+ ///Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards.
+ var/datum/ai_movement/ai_movement = /datum/ai_movement/dumb
+ ///Cooldown until next movement
+ COOLDOWN_DECLARE(movement_cooldown)
+ ///Delay between movements. This is on the controller so we can keep the movement datum singleton
+ var/movement_delay = 0.1 SECONDS
+
+ // The variables below are fucking stupid and should be put into the blackboard at some point.
+ ///A list for the path we're currently following, if we're using JPS pathing
+ var/list/movement_path
+ ///Cooldown for JPS movement, how often we're allowed to try making a new path
+ COOLDOWN_DECLARE(repath_cooldown)
+ ///AI paused time
+ var/paused_until = 0
+
+/datum/ai_controller/New(atom/new_pawn)
+ change_ai_movement_type(ai_movement)
+ init_subtrees()
+ PossessPawn(new_pawn)
+
+/datum/ai_controller/Destroy(force, ...)
+ set_ai_status(AI_STATUS_OFF)
+ UnpossessPawn(FALSE)
+ return ..()
+
+///Overrides the current ai_movement of this controller with a new one
+/datum/ai_controller/proc/change_ai_movement_type(datum/ai_movement/new_movement)
+ ai_movement = SSai_movement.movement_types[new_movement]
+
+///Completely replaces the planning_subtrees with a new set based on argument provided, list provided must contain specifically typepaths
+/datum/ai_controller/proc/replace_planning_subtrees(list/typepaths_of_new_subtrees)
+ planning_subtrees = typepaths_of_new_subtrees
+ init_subtrees()
+
+///Loops over the subtrees in planning_subtrees and looks at the ai_controllers to grab a reference, ENSURE planning_subtrees ARE TYPEPATHS AND NOT INSTANCES/REFERENCES BEFORE EXECUTING THIS
+/datum/ai_controller/proc/init_subtrees()
+ if(!LAZYLEN(planning_subtrees))
+ return
+ var/list/temp_subtree_list = list()
+ for(var/subtree in planning_subtrees)
+ var/subtree_instance = SSai_controllers.ai_subtrees[subtree]
+ temp_subtree_list += subtree_instance
+ planning_subtrees = temp_subtree_list
+
+///Proc to move from one pawn to another, this will destroy the target's existing controller.
+/datum/ai_controller/proc/PossessPawn(atom/new_pawn)
+ if(pawn) //Reset any old signals
+ UnpossessPawn(FALSE)
+
+ if(istype(new_pawn.ai_controller)) //Existing AI, kill it.
+ QDEL_NULL(new_pawn.ai_controller)
+
+ if(TryPossessPawn(new_pawn) & AI_CONTROLLER_INCOMPATIBLE)
+ qdel(src)
+ CRASH("[src] attached to [new_pawn] but these are not compatible!")
+
+ pawn = new_pawn
+ pawn.ai_controller = src
+
+ if(!continue_processing_when_client && istype(new_pawn, /mob))
+ var/mob/possible_client_holder = new_pawn
+ if(possible_client_holder.client)
+ set_ai_status(AI_STATUS_OFF)
+ else
+ set_ai_status(AI_STATUS_ON)
+ else
+ set_ai_status(AI_STATUS_ON)
+
+ RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
+
+///Abstract proc for initializing the pawn to the new controller
+/datum/ai_controller/proc/TryPossessPawn(atom/new_pawn)
+ return
+
+///Proc for deinitializing the pawn to the old controller
+/datum/ai_controller/proc/UnpossessPawn(destroy)
+ UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT))
+ pawn.ai_controller = null
+ pawn = null
+ if(destroy)
+ qdel(src)
+ return
+
+///Returns TRUE if the ai controller can actually run at the moment.
+/datum/ai_controller/proc/able_to_run()
+ if(world.time < paused_until)
+ return FALSE
+ return TRUE
+
+/// Generates a plan and see if our existing one is still valid.
+/datum/ai_controller/process(delta_time)
+ if(!able_to_run())
+ walk(pawn, 0) //stop moving
+ return //this should remove them from processing in the future through event-based stuff.
+ if(!LAZYLEN(current_behaviors))
+ PerformIdleBehavior(delta_time) //Do some stupid shit while we have nothing to do
+ return
+
+ if(current_movement_target && get_dist(pawn, current_movement_target) > max_target_distance) //The distance is out of range
+ CancelActions()
+ return
+
+ for(var/i in current_behaviors)
+ var/datum/ai_behavior/current_behavior = i
+
+ if(behavior_cooldowns[current_behavior] > world.time) //Still on cooldown
+ continue
+
+ if(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT && current_movement_target) //Might need to move closer
+ if(current_behavior.required_distance >= get_dist(pawn, current_movement_target)) ///Are we close enough to engage?
+ if(ai_movement.moving_controllers[src] == current_movement_target) //We are close enough, if we're moving stop.else
+ ai_movement.stop_moving_towards(src)
+ ProcessBehavior(delta_time, current_behavior)
+ return
+
+ else if(ai_movement.moving_controllers[src] != current_movement_target) //We're too far, if we're not already moving start doing it.
+ ai_movement.start_moving_towards(src, current_movement_target) //Then start moving
+
+ if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) //If we can move and perform then do so.
+ ProcessBehavior(delta_time, current_behavior)
+ return
+ else //No movement required
+ ProcessBehavior(delta_time, current_behavior)
+ return
+
+
+///Move somewhere using dumb movement (byond base)
+/datum/ai_controller/proc/MoveTo(delta_time)
+ var/current_loc = get_turf(pawn)
+ var/atom/movable/movable_pawn = pawn
+
+ var/turf/target_turf = get_step_towards(movable_pawn, current_movement_target)
+
+ if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs))
+ movable_pawn.Move(target_turf, get_dir(current_loc, target_turf))
+ if(current_loc == get_turf(movable_pawn))
+ if(++pathing_attempts >= AI_MAX_PATH_LENGTH)
+ CancelActions()
+ pathing_attempts = 0
+
+
+///Perform some dumb idle behavior.
+/datum/ai_controller/proc/PerformIdleBehavior(delta_time)
+ return
+
+///This is where you decide what actions are taken by the AI.
+/datum/ai_controller/proc/SelectBehaviors(delta_time)
+ SHOULD_NOT_SLEEP(TRUE) //Fuck you don't sleep in procs like this.
+ if(!COOLDOWN_FINISHED(src, failed_planning_cooldown))
+ return FALSE
+
+ LAZYINITLIST(current_behaviors)
+
+ if(LAZYLEN(planning_subtrees))
+ for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees)
+ if(subtree.SelectBehaviors(src, delta_time) == SUBTREE_RETURN_FINISH_PLANNING)
+ break
+
+///This proc handles changing ai status, and starts/stops processing if required.
+/datum/ai_controller/proc/set_ai_status(new_ai_status)
+ if(ai_status == new_ai_status)
+ return FALSE //no change
+
+ ai_status = new_ai_status
+ switch(ai_status)
+ if(AI_STATUS_ON)
+ SSai_controllers.active_ai_controllers += src
+ START_PROCESSING(SSai_behaviors, src)
+ if(AI_STATUS_OFF)
+ STOP_PROCESSING(SSai_behaviors, src)
+ SSai_controllers.active_ai_controllers -= src
+ CancelActions()
+
+/datum/ai_controller/proc/PauseAi(time)
+ paused_until = world.time + time
+
+/datum/ai_controller/proc/AddBehavior(behavior_type, ...)
+ var/datum/ai_behavior/behavior = GET_AI_BEHAVIOR(behavior_type)
+ if(!behavior)
+ CRASH("Behavior [behavior_type] not found.")
+ var/list/arguments = args.Copy()
+ arguments[1] = src
+ if(!behavior.setup(arglist(arguments)))
+ return
+ LAZYADD(current_behaviors, behavior)
+ arguments.Cut(1, 2)
+ if(length(arguments))
+ behavior_args[behavior_type] = arguments
+
+/datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior)
+ var/list/arguments = list(delta_time, src)
+ var/list/stored_arguments = behavior_args[behavior.type]
+ if(stored_arguments)
+ arguments += stored_arguments
+ behavior.perform(arglist(arguments))
+
+/datum/ai_controller/proc/CancelActions()
+ if(!LAZYLEN(current_behaviors))
+ return
+ for(var/i in current_behaviors)
+ var/datum/ai_behavior/current_behavior = i
+ current_behavior.finish_action(src, FALSE)
+
+/datum/ai_controller/proc/on_sentience_gained()
+ UnregisterSignal(pawn, COMSIG_MOB_LOGIN)
+ if(!continue_processing_when_client)
+ set_ai_status(AI_STATUS_OFF) //Can't do anything while player is connected
+ RegisterSignal(pawn, COMSIG_MOB_LOGOUT, PROC_REF(on_sentience_lost))
+
+/datum/ai_controller/proc/on_sentience_lost()
+ UnregisterSignal(pawn, COMSIG_MOB_LOGOUT)
+ set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected
+ RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained))
+
+/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding, likely pointing to whatever ID slot is relevant
+/datum/ai_controller/proc/get_access()
+ return
diff --git a/code/datums/ai/_ai_planning_subtree.dm b/code/datums/ai/_ai_planning_subtree.dm
new file mode 100644
index 000000000000..8f186d586a45
--- /dev/null
+++ b/code/datums/ai/_ai_planning_subtree.dm
@@ -0,0 +1,6 @@
+///A subtree is attached to a controller and is occasionally called by /ai_controller/SelectBehaviors(), this mainly exists to act as a way to subtype and modify SelectBehaviors() without needing to subtype the ai controller itself
+/datum/ai_planning_subtree
+
+///Determines what behaviors should the controller try processing; if this returns SUBTREE_RETURN_FINISH_PLANNING then the controller won't go through the other subtrees should multiple exist in controller.planning_subtrees
+/datum/ai_planning_subtree/proc/SelectBehaviors(datum/ai_controller/controller, delta_time)
+ return
diff --git a/code/datums/ai/dog/dog_behaviors.dm b/code/datums/ai/dog/dog_behaviors.dm
new file mode 100644
index 000000000000..3672b348118a
--- /dev/null
+++ b/code/datums/ai/dog/dog_behaviors.dm
@@ -0,0 +1,208 @@
+/datum/ai_behavior/battle_screech/dog
+ screeches = list("barks","howls")
+
+/// Fetching makes the pawn chase after whatever it's targeting and pick it up when it's in range, with the dog_equip behavior
+/datum/ai_behavior/fetch
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+/datum/ai_behavior/fetch/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ var/obj/item/fetch_thing = controller.blackboard[BB_FETCH_TARGET]
+
+ if(fetch_thing.anchored || !isturf(fetch_thing.loc) || IS_EDIBLE(fetch_thing)) //either we can't pick it up, or we'd rather eat it, so stop trying.
+ finish_action(controller, FALSE)
+ return
+
+ if(in_range(living_pawn, fetch_thing))
+ finish_action(controller, TRUE)
+ return
+
+ finish_action(controller, FALSE)
+
+/datum/ai_behavior/fetch/finish_action(datum/ai_controller/controller, success)
+ . = ..()
+
+ if(!success) //Don't try again on this item if we failed
+ var/obj/item/target = controller.blackboard[BB_FETCH_TARGET]
+ if(target)
+ controller.blackboard[BB_FETCH_IGNORE_LIST][target] = TRUE
+ controller.blackboard[BB_FETCH_TARGET] = null
+ controller.blackboard[BB_FETCH_DELIVER_TO] = null
+
+
+/// This is simply a behaviour to pick up a fetch target
+/datum/ai_behavior/simple_equip/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/obj/item/fetch_target = controller.blackboard[BB_FETCH_TARGET]
+ if(!isturf(fetch_target?.loc)) // someone picked it up or something happened to it
+ finish_action(controller, FALSE)
+ return
+
+ if(in_range(controller.pawn, fetch_target))
+ pickup_item(controller, fetch_target)
+ finish_action(controller, TRUE)
+ else
+ finish_action(controller, FALSE)
+
+/datum/ai_behavior/simple_equip/finish_action(datum/ai_controller/controller, success)
+ . = ..()
+ controller.blackboard[BB_FETCH_TARGET] = null
+
+/datum/ai_behavior/simple_equip/proc/pickup_item(datum/ai_controller/controller, obj/item/target)
+ var/atom/pawn = controller.pawn
+ drop_item(controller)
+ pawn.visible_message("[pawn] picks up [target] in [pawn.p_their()] mouth.")
+ target.forceMove(pawn)
+ controller.blackboard[BB_SIMPLE_CARRY_ITEM] = target
+ return TRUE
+
+/datum/ai_behavior/simple_equip/proc/drop_item(datum/ai_controller/controller)
+ var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
+ if(!carried_item)
+ return
+
+ var/atom/pawn = controller.pawn
+ pawn.visible_message("[pawn] drops [carried_item].")
+ carried_item.forceMove(get_turf(pawn))
+ controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null
+ return TRUE
+
+
+
+/// This behavior involves dropping off a carried item to a specified person (or place)
+/datum/ai_behavior/deliver_item
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+/datum/ai_behavior/deliver_item/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/return_target = controller.blackboard[BB_FETCH_DELIVER_TO]
+ if(!return_target)
+ finish_action(controller, FALSE)
+ if(in_range(controller.pawn, return_target))
+ deliver_item(controller)
+ finish_action(controller, TRUE)
+
+/datum/ai_behavior/deliver_item/finish_action(datum/ai_controller/controller, success)
+ . = ..()
+ controller.blackboard[BB_FETCH_DELIVER_TO] = null
+
+/// Actually drop the fetched item to the target
+/datum/ai_behavior/deliver_item/proc/deliver_item(datum/ai_controller/controller)
+ var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM]
+ var/atom/movable/return_target = controller.blackboard[BB_FETCH_DELIVER_TO]
+ if(!carried_item || !return_target)
+ finish_action(controller, FALSE)
+ return
+
+ if(ismob(return_target))
+ controller.pawn.visible_message("[controller.pawn] delivers [carried_item] at [return_target]'s feet.")
+ else // not sure how to best phrase this
+ controller.pawn.visible_message("[controller.pawn] delivers [carried_item] to [return_target].")
+
+ carried_item.forceMove(get_turf(return_target))
+ controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null
+ return TRUE
+
+/// This behavior involves either eating a snack we can reach, or begging someone holding a snack
+/datum/ai_behavior/eat_snack
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
+
+/datum/ai_behavior/eat_snack/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/obj/item/snack = controller.current_movement_target
+ if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc)))
+ finish_action(controller, FALSE)
+
+ var/mob/living/living_pawn = controller.pawn
+ if(!in_range(living_pawn, snack))
+ return
+
+ if(isturf(snack.loc))
+ snack.attack_animal(living_pawn) // snack attack!
+ else if(iscarbon(snack.loc) && DT_PROB(10, delta_time))
+ living_pawn.manual_emote("stares at [snack.loc]'s [snack.name] with a sad puppy-face.")
+
+ if(QDELETED(snack)) // we ate it!
+ finish_action(controller, TRUE)
+
+
+/// This behavior involves either eating a snack we can reach, or begging someone holding a snack
+/datum/ai_behavior/play_dead
+ behavior_flags = NONE
+
+/datum/ai_behavior/play_dead/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/simple_animal/simple_pawn = controller.pawn
+ if(!istype(simple_pawn))
+ return
+
+ if(!controller.blackboard[BB_DOG_PLAYING_DEAD])
+ controller.blackboard[BB_DOG_PLAYING_DEAD] = TRUE
+ simple_pawn.emote("deathgasp", intentional=FALSE)
+ simple_pawn.icon_state = simple_pawn.icon_dead
+ if(simple_pawn.flip_on_death)
+ simple_pawn.transform = simple_pawn.transform.Turn(180)
+ simple_pawn.density = FALSE
+
+ if(DT_PROB(10, delta_time))
+ finish_action(controller, TRUE)
+
+/datum/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded)
+ . = ..()
+ var/mob/living/simple_animal/simple_pawn = controller.pawn
+ if(!istype(simple_pawn) || simple_pawn.stat) // imagine actually dying while playing dead. hell, imagine being the kid waiting for your pup to get back up :(
+ return
+ controller.blackboard[BB_DOG_PLAYING_DEAD] = FALSE
+ simple_pawn.visible_message("[simple_pawn] springs to [simple_pawn.p_their()] feet, panting excitedly!")
+ simple_pawn.icon_state = simple_pawn.icon_living
+ if(simple_pawn.flip_on_death)
+ simple_pawn.transform = simple_pawn.transform.Turn(180)
+ simple_pawn.density = initial(simple_pawn.density)
+
+/// This behavior involves either eating a snack we can reach, or begging someone holding a snack
+/datum/ai_behavior/harass
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
+ required_distance = 3
+
+/datum/ai_behavior/harass/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/living_pawn = controller.pawn
+ if(!istype(living_pawn))
+ return
+
+ var/atom/movable/harass_target = controller.blackboard[BB_DOG_HARASS_TARGET]
+ if(!harass_target || !can_see(living_pawn, harass_target, length=AI_DOG_VISION_RANGE))
+ finish_action(controller, FALSE)
+ return
+
+ if(controller.blackboard[BB_DOG_FRIENDS][harass_target])
+ living_pawn.visible_message("[living_pawn] looks sideways at [harass_target] for a moment, then shakes [living_pawn.p_their()] head and ceases aggression.")
+ finish_action(controller, FALSE)
+ return
+
+ var/mob/living/living_target = harass_target
+ if(istype(living_target) && (living_target.stat || HAS_TRAIT(living_target, TRAIT_FAKEDEATH)))
+ finish_action(controller, TRUE)
+ return
+
+ // subtypes of this behavior can change behavior for how eager/averse the pawn is to attack the target as opposed to falling back/making noise/getting help
+ if(in_range(living_pawn, living_target))
+ attack(controller, living_target)
+ else if(DT_PROB(50, delta_time))
+ living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [harass_target]!")
+
+/datum/ai_behavior/harass/finish_action(datum/ai_controller/controller, succeeded)
+ . = ..()
+ controller.blackboard[BB_DOG_HARASS_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/harass/proc/attack(datum/ai_controller/controller, mob/living/living_target)
+ var/mob/living/living_pawn = controller.pawn
+ if(!istype(living_pawn))
+ return
+ living_pawn.do_attack_animation(living_target, ATTACK_EFFECT_BITE)
+ living_target.visible_message("[living_pawn] bites at [living_target]!", "[living_pawn] bites at you!", vision_distance = COMBAT_MESSAGE_RANGE)
+ if(istype(living_target))
+ living_target.take_bodypart_damage(rand(5, 10))
+ log_combat(living_pawn, living_target, "bit (AI)")
diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm
new file mode 100644
index 000000000000..5cd65654db7c
--- /dev/null
+++ b/code/datums/ai/dog/dog_controller.dm
@@ -0,0 +1,271 @@
+/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_REF(on_attack_hand))
+ RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined))
+ RegisterSignal(new_pawn, COMSIG_CLICK_ALT, PROC_REF(check_altclicked))
+ RegisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING, PROC_REF(listened_throw))
+ return ..() //Run parent at end
+
+/datum/ai_controller/dog/UnpossessPawn(destroy)
+ UnregisterSignal(pawn, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_PARENT_EXAMINE, COMSIG_GLOB_CARBON_THROW_THING, COMSIG_CLICK_ALT))
+ 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
+
+
+/datum/ai_controller/dog/PerformIdleBehavior(delta_time)
+ var/mob/living/living_pawn = 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(src, heel_cooldown))
+ return
+
+ // if we're just ditzing around carrying something, occasionally print a message so people know we have something
+ if(blackboard[BB_SIMPLE_CARRY_ITEM] && DT_PROB(5, delta_time))
+ var/obj/item/carry_item = blackboard[BB_SIMPLE_CARRY_ITEM]
+ living_pawn.visible_message("[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)
+
+/// 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][thrown_thing])
+ return
+
+ RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(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
+ LAZYADD(current_behaviors, GET_AI_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.a_intent == INTENT_HARM)
+ 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[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]
+ if(friends[new_friend])
+ return
+ if(in_range(pawn, new_friend))
+ new_friend.visible_message("[pawn] licks at [new_friend] in a friendly manner!", "[pawn] licks at you in a friendly manner!")
+ friends[new_friend] = TRUE
+ RegisterSignal(new_friend, COMSIG_MOB_POINTED, PROC_REF(check_point))
+ RegisterSignal(new_friend, COMSIG_MOB_SAY, PROC_REF(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[ex_friend] = null
+ 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 += "[pawn.p_they(TRUE)] [pawn.p_are()] carrying [carried_item.get_examine_string(user)] in [pawn.p_their()] mouth."
+ if(blackboard[BB_DOG_FRIENDS][user])
+ examine_text += "[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(get_turf(ol_yeller))
+ 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][clicker])
+ return
+ . = COMPONENT_CANCEL_CLICK_ALT
+ INVOKE_ASYNC(src, PROC_REF(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_REF(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][speaker])
+ return
+
+ if(!COOLDOWN_FINISHED(src, command_cooldown))
+ 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("[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("[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("[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()
+ LAZYADD(current_behaviors, GET_AI_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
+
+ 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(ismob(pointed_movable) || pointed_movable.anchored)
+ return
+ pawn.visible_message("[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
+ current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/fetch)
+ if(DOG_COMMAND_ATTACK)
+ pawn.visible_message("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and growls intensely!")
+ current_movement_target = pointed_movable
+ blackboard[BB_DOG_HARASS_TARGET] = pointed_movable
+ current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/harass)
diff --git a/code/datums/ai/dog/dog_subtrees.dm b/code/datums/ai/dog/dog_subtrees.dm
new file mode 100644
index 000000000000..1eab7b87251b
--- /dev/null
+++ b/code/datums/ai/dog/dog_subtrees.dm
@@ -0,0 +1,40 @@
+/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))
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/eat_snack))
+ else if(isitem(interact_target))
+ LAZYADD(controller.current_behaviors, GET_AI_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
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/deliver_item))
+ return
diff --git a/code/datums/ai/generic_actions.dm b/code/datums/ai/generic_actions.dm
new file mode 100644
index 000000000000..fdcc978857fd
--- /dev/null
+++ b/code/datums/ai/generic_actions.dm
@@ -0,0 +1,111 @@
+
+/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, TYPE_PROC_REF(/mob, emote), pick(screeches))
+ finish_action(controller, TRUE)
+
+/// 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/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/pawn = controller.pawn
+ var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand())
+ var/atom/target = controller.current_movement_target
+
+ if(!target || !pawn.CanReach(target))
+ finish_action(controller, FALSE)
+ return
+
+ pawn.a_intent = INTENT_HELP
+
+ 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/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+ var/mob/living/pawn = controller.pawn
+ var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand())
+ var/atom/target = controller.current_movement_target
+
+ 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(
+ "[pawn] starts trying to give [held_item] to [living_target]!",
+ "[pawn] tries to give you [held_item]!"
+ )
+ if(!do_after(pawn, 1 SECONDS, living_target))
+ return
+ 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, obj/item/target)
+ . = ..()
+ controller.current_movement_target = target
+
+/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, obj/item/target)
+ . = ..()
+ var/mob/living/pawn = controller.pawn
+
+ if(!(target in pawn.held_items))
+ if(!pawn.put_in_hand_check(target))
+ finish_action(controller, FALSE)
+ return
+
+ pawn.put_in_hands(target)
+
+ target.melee_attack_chain(pawn, 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)
diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm
new file mode 100644
index 000000000000..822dae22eb23
--- /dev/null
+++ b/code/datums/ai/monkey/monkey_behaviors.dm
@@ -0,0 +1,279 @@
+/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
+
+ 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(!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_REF(attempt_pickpocket), controller)
+
+/datum/ai_behavior/monkey_equip/pickpocket/proc/attempt_pickpocket(datum/ai_controller/controller)
+ var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET]
+
+ var/mob/living/victim = target.loc
+
+ var/mob/living/living_pawn = controller.pawn
+
+ victim.visible_message("[living_pawn] starts trying to take [target] from [victim]!", "[living_pawn] tries to take [target]!")
+
+ controller.blackboard[BB_MONKEY_PICKPOCKETING] = TRUE
+
+ var/success = FALSE
+
+ if(do_after(living_pawn, MONKEY_ITEM_SNATCH_DELAY, victim) && target)
+
+ for(var/obj/item/I in victim.held_items)
+ if(I == target)
+ victim.visible_message("[living_pawn] snatches [target] from [victim].", "[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("[living_pawn] tried to snatch [target] from [victim], but failed!", "[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/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+
+ 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))
+ living_pawn.a_intent = INTENT_DISARM
+ monkey_attack(controller, target, delta_time)
+ else
+ living_pawn.a_intent = INTENT_HARM
+ monkey_attack(controller, target, delta_time)
+
+
+/datum/ai_behavior/monkey_attack_mob/finish_action(datum/ai_controller/controller, succeeded)
+ . = ..()
+ 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)
+ 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)
+
+ if(isnull(controller.blackboard[BB_MONKEY_GUN_WORKED]))
+ controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE
+
+ living_pawn.a_intent = INTENT_HARM
+ if(living_pawn.CanReach(target, weapon))
+ if(weapon)
+ weapon.melee_attack_chain(living_pawn, target)
+ else
+ target.attack_paw(living_pawn)
+ 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_AGRESSIVE])
+ 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/finish_action(datum/ai_controller/controller, succeeded)
+ . = ..()
+ controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = null //Reset attack target
+ controller.blackboard[BB_MONKEY_DISPOSING] = FALSE //No longer disposing
+ controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = null //No target disposal
+
+/datum/ai_behavior/disposal_mob/perform(delta_time, datum/ai_controller/controller)
+ . = ..()
+
+ if(controller.blackboard[BB_MONKEY_DISPOSING]) //We are disposing, don't do ANYTHING!!!!
+ return
+
+ var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
+ 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))
+ living_pawn.a_intent = INTENT_GRAB
+ target.grabbedby(living_pawn)
+ return //Do the rest next turn
+
+ var/obj/machinery/disposal/disposal = controller.blackboard[BB_MONKEY_TARGET_DISPOSAL]
+ controller.current_movement_target = disposal
+
+ if(living_pawn.Adjacent(disposal))
+ INVOKE_ASYNC(src, PROC_REF(try_disposal_mob), controller) //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)
+ var/mob/living/living_pawn = controller.pawn
+ var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET]
+ var/obj/machinery/disposal/disposal = controller.blackboard[BB_MONKEY_TARGET_DISPOSAL]
+
+ controller.blackboard[BB_MONKEY_DISPOSING] = TRUE
+
+ if(target && disposal?.stuff_mob_in(target, living_pawn))
+ disposal.flush()
+ finish_action(controller, TRUE)
+
+
+/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)
diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm
new file mode 100644
index 000000000000..4cb8605d185f
--- /dev/null
+++ b/code/datums/ai/monkey/monkey_controller.dm
@@ -0,0 +1,255 @@
+/*
+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/monkey_tree)
+ blackboard = list(
+ BB_MONKEY_AGRESSIVE = 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_MONKEY_NEXT_HUNGRY = 0
+ )
+/datum/ai_controller/monkey/angry
+
+/datum/ai_controller/monkey/angry/TryPossessPawn(atom/new_pawn)
+ . = ..()
+ if(. & AI_CONTROLLER_INCOMPATIBLE)
+ return
+ blackboard[BB_MONKEY_AGRESSIVE] = TRUE //Angry cunt
+
+/datum/ai_controller/monkey/TryPossessPawn(atom/new_pawn)
+ if(!isliving(new_pawn))
+ return AI_CONTROLLER_INCOMPATIBLE
+
+ blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(0, 300)
+
+ var/mob/living/living_pawn = new_pawn
+ RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby))
+ RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand))
+ RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_PAW, PROC_REF(on_attack_paw))
+ RegisterSignal(new_pawn, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act))
+ RegisterSignal(new_pawn, COMSIG_ATOM_HITBY, PROC_REF(on_hitby))
+ RegisterSignal(new_pawn, COMSIG_LIVING_START_PULL, PROC_REF(on_startpulling))
+ RegisterSignal(new_pawn, COMSIG_LIVING_TRY_SYRINGE, PROC_REF(on_try_syringe))
+ RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk))
+ RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, PROC_REF(on_attempt_cuff))
+ RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, PROC_REF(update_movespeed))
+ RegisterSignal(new_pawn, COMSIG_FOOD_EATEN, PROC_REF(on_eat))
+ 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))
+ return ..() //Run parent at end
+
+/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(nearby_items, living_pawn.held_items)
+
+ var/pickpocket = FALSE
+ for(var/mob/living/carbon/human/human in oview(5, living_pawn))
+ var/obj/item/held_weapon = GetBestWeapon(human.held_items + weapon, living_pawn.held_items)
+ 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)
+ LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_equip/pickpocket))
+ else
+ LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_equip/ground))
+ return TRUE
+
+/// Returns either the best weapon from the given choices or null if held weapons are better
+/datum/ai_controller/monkey/proc/GetBestWeapon(list/choices, list/held_weapons)
+ var/gun_neurons_activated = blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED]
+ var/top_force = 0
+ var/obj/item/top_force_item
+ for(var/obj/item/item as anything in held_weapons)
+ if(!item)
+ continue
+ if(blackboard[BB_MONKEY_BLACKLISTITEMS][item])
+ continue
+ if(gun_neurons_activated && istype(item, /obj/item/gun))
+ // We have a gun, why bother looking for something inferior
+ // Also yes it is intentional that monkeys dont know how to pick the best gun
+ return item
+ if(item.force > top_force)
+ top_force = item.force
+ top_force_item = item
+
+ for(var/obj/item/item as anything in choices)
+ if(!item)
+ continue
+ if(blackboard[BB_MONKEY_BLACKLISTITEMS][item])
+ continue
+ if(gun_neurons_activated && istype(item, /obj/item/gun))
+ return item
+ if(item.force <= top_force)
+ continue
+ top_force_item = item
+ top_force = item.force
+
+ return top_force_item
+
+/datum/ai_controller/monkey/proc/TryFindFood()
+ . = FALSE
+ var/mob/living/living_pawn = pawn
+
+ // Held items
+
+ var/list/food_candidates = list()
+ for(var/obj/item as anything in living_pawn.held_items)
+ if(!item || !IsEdible(item))
+ continue
+ food_candidates += item
+
+ for(var/obj/item/candidate in oview(2, living_pawn))
+ if(!IsEdible(candidate))
+ continue
+ food_candidates += candidate
+
+ if(length(food_candidates))
+ var/obj/item/best_held = GetBestWeapon(null, living_pawn.held_items)
+ for(var/obj/item/held as anything in living_pawn.held_items)
+ if(!held || held == best_held)
+ continue
+ living_pawn.dropItemToGround(held)
+
+ AddBehavior(/datum/ai_behavior/consume, pick(food_candidates))
+ return TRUE
+
+/datum/ai_controller/monkey/proc/IsEdible(obj/item/thing)
+ if(IS_EDIBLE(thing))
+ return TRUE
+ if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass))
+ var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing
+ if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid!
+ return TRUE
+ return FALSE
+
+//When idle just kinda fuck around.
+/datum/ai_controller/monkey/PerformIdleBehavior(delta_time)
+ var/mob/living/living_pawn = pawn
+
+ if(DT_PROB(25, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby)
+ step(living_pawn, pick(GLOB.cardinals))
+ else if(DT_PROB(5, delta_time))
+ INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick("screech"))
+ else if(DT_PROB(1, delta_time))
+ INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick("scratch","jump","roll","tail"))
+
+///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/L)
+ SIGNAL_HANDLER
+ if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB))
+ retaliate(L)
+ else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB))
+ retaliate(L)
+
+/datum/ai_controller/monkey/proc/on_attack_paw(datum/source, mob/living/L)
+ SIGNAL_HANDLER
+ if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB))
+ retaliate(L)
+ else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB))
+ retaliate(L)
+
+/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
+ if(I.throwforce < living_pawn.health && ishuman(I.thrownby))
+ var/mob/living/carbon/human/H = I.thrownby
+ retaliate(H)
+
+/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
+
+/datum/ai_controller/monkey/proc/on_eat(mob/living/pawn)
+ SIGNAL_HANDLER
+ blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(120, 600) SECONDS
diff --git a/code/datums/ai/monkey/monkey_subtrees.dm b/code/datums/ai/monkey/monkey_subtrees.dm
new file mode 100644
index 000000000000..4e7317de5a56
--- /dev/null
+++ b/code/datums/ai/monkey/monkey_subtrees.dm
@@ -0,0 +1,84 @@
+/datum/ai_planning_subtree/monkey_tree/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time)
+ var/mob/living/living_pawn = controller.pawn
+
+ if(SHOULD_RESIST(living_pawn) && DT_PROB(MONKEY_RESIST_PROB, delta_time))
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/resist)) //BRO IM ON FUCKING FIRE BRO
+ return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY.
+
+ var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES]
+
+ if(HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) //Not a pacifist? lets try some combat behavior.
+ return
+
+ var/mob/living/selected_enemy
+ if(length(enemies) || controller.blackboard[BB_MONKEY_AGRESSIVE]) //We have enemies or are pissed
+ var/list/valids = list()
+ for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, living_pawn))
+ if(possible_enemy == living_pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?)
+ continue
+ // Weighted list, so the closer they are the more likely they are to be chosen as the enemy
+ valids[possible_enemy] = CEILING(100 / (get_dist(living_pawn, possible_enemy) || 1), 1)
+
+ selected_enemy = pick_weight(valids)
+
+ if(selected_enemy)
+ if(!selected_enemy.stat) //He's up, get him!
+ if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle
+ controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_flee))
+ return //I'm running fuck you guys
+
+ if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing.
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy
+ controller.current_movement_target = selected_enemy
+ if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time)
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/recruit_monkeys))
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/battle_screech/monkey))
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_attack_mob))
+ return SUBTREE_RETURN_FINISH_PLANNING //Focus on this
+
+ else //He's down, can we disposal him?
+ var/obj/machinery/disposal/bodyDisposal = locate(/obj/machinery/disposal/) in view(MONKEY_ENEMY_VISION, living_pawn)
+ if(bodyDisposal)
+ controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy
+ controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = bodyDisposal
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/disposal_mob))
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ if(prob(5))
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/use_in_hand))
+
+ if(selected_enemy || !DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time))
+ return
+
+ if(world.time >= controller.blackboard[BB_MONKEY_NEXT_HUNGRY] && controller.TryFindFood())
+ return
+
+ if(prob(50))
+ var/list/possible_targets = list()
+ for(var/atom/thing in view(2, living_pawn))
+ if(!thing.mouse_opacity)
+ continue
+ if(thing.IsObscured())
+ continue
+ possible_targets += thing
+ var/atom/target = pick(possible_targets)
+ if(target)
+ controller.current_movement_target = target
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/use_on_object))
+ return
+
+ if(prob(5) && (locate(/obj/item) in living_pawn.held_items))
+ var/list/possible_receivers = list()
+ for(var/mob/living/candidate in oview(2, controller.pawn))
+ possible_receivers += candidate
+
+ if(length(possible_receivers))
+ var/mob/living/target = pick(possible_receivers)
+ controller.current_movement_target = target
+ LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/give))
+ return
+
+ controller.TryFindWeapon()
diff --git a/code/datums/ai/movement/_ai_movement.dm b/code/datums/ai/movement/_ai_movement.dm
new file mode 100644
index 000000000000..c9d47bc6d66b
--- /dev/null
+++ b/code/datums/ai/movement/_ai_movement.dm
@@ -0,0 +1,19 @@
+///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()
+ ///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)
+ controller.pathing_attempts = 0
+ if(!moving_controllers.len)
+ 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)
+ STOP_PROCESSING(SSai_movement, src)
diff --git a/code/datums/ai/movement/ai_movement_dumb.dm b/code/datums/ai/movement/ai_movement_dumb.dm
new file mode 100644
index 000000000000..0ce64669d373
--- /dev/null
+++ b/code/datums/ai/movement/ai_movement_dumb.dm
@@ -0,0 +1,27 @@
+///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
+
+ if(!isturf(movable_pawn.loc)) //No moving if not on a turf
+ continue
+
+ 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))
+ 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()
diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm
new file mode 100644
index 000000000000..ea05b0fc899e
--- /dev/null
+++ b/code/datums/ai/movement/ai_movement_jps.dm
@@ -0,0 +1,61 @@
+/**
+ * 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
+
+ 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 ..()
diff --git a/code/datums/components/spinny.dm b/code/datums/components/spinny.dm
new file mode 100644
index 000000000000..cdf5262ab31b
--- /dev/null
+++ b/code/datums/components/spinny.dm
@@ -0,0 +1,33 @@
+/**
+ * spinny.dm
+ *
+ * It's a component that spins things a whole bunch, like [proc/dance_rotate] but without the sleeps
+*/
+/datum/component/spinny
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+ /// How many turns are left?
+ var/steps_left
+ /// Turns clockwise by default, or counterclockwise if the reverse argument is TRUE
+ var/turn_degrees = 90
+
+/datum/component/spinny/Initialize(steps = 12, reverse = FALSE)
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ steps_left = steps
+ turn_degrees = (reverse ? -90 : 90)
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/component/spinny/Destroy(force, silent)
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
+
+/datum/component/spinny/process(delta_time)
+ steps_left--
+ var/atom/spinny_boy = parent
+ if(!istype(spinny_boy) || steps_left <= 0)
+ qdel(src)
+ return
+
+ // 25% chance to make 2 turns instead of 1 since the old dance_rotate wasn't strictly clockwise/counterclockwise
+ spinny_boy.setDir(turn(spinny_boy.dir, turn_degrees * (prob(25) ? 2 : 1)))
diff --git a/code/datums/mutations/body.dm b/code/datums/mutations/body.dm
index 0954c2a35bc8..d520c3bae5ed 100644
--- a/code/datums/mutations/body.dm
+++ b/code/datums/mutations/body.dm
@@ -179,11 +179,11 @@
/datum/mutation/human/race/on_acquiring(mob/living/carbon/human/owner)
if(..())
return
- . = owner.monkeyize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE)
+ . = owner.monkeyize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE | TR_KEEPAI)
/datum/mutation/human/race/on_losing(mob/living/carbon/monkey/owner)
if(owner && istype(owner) && owner.stat != DEAD && (owner.dna.mutations.Remove(src)))
- . = owner.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE)
+ . = owner.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE | TR_KEEPAI)
/datum/mutation/human/glow
name = "Glowy"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index 46b08169f829..75a36e1aa677 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -136,6 +136,9 @@
///List of smoothing groups this atom can smooth with. If this is null and atom is smooth, it smooths only with itself.
var/list/canSmoothWith = null
+ ///AI controller that controls this atom. type on init, then turned into an instance during runtime
+ var/datum/ai_controller/ai_controller
+
/// The icon file of the connector to use when smoothing.
/// Use of connectors requires the smoothing flags SMOOTH_BITMASK and SMOOTH_CONNECTORS.
var/connector_icon = null
@@ -265,6 +268,7 @@
set_custom_materials(temp_list)
ComponentInitialize()
+ InitializeAIController()
return INITIALIZE_HINT_NORMAL
@@ -311,6 +315,7 @@
LAZYCLEARLIST(managed_overlays)
QDEL_NULL(light)
+ QDEL_NULL(ai_controller)
if(smoothing_flags & SMOOTH_QUEUED)
SSicon_smooth.remove_from_queues(src)
@@ -737,6 +742,7 @@
* throw lots of items around - singularity being a notable example)
*/
/atom/proc/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
+ SEND_SIGNAL(src, COMSIG_ATOM_HITBY, AM, skipcatch, hitpush, blocked, throwingdatum)
if(density && !has_gravity(AM)) //thrown stuff bounces off dense stuff in no grav, unless the thrown stuff ends up inside what it hit(embedding, bola, etc...).
addtimer(CALLBACK(src, PROC_REF(hitby_react), AM), 2)
@@ -1068,6 +1074,7 @@
VV_DROPDOWN_OPTION(VV_HK_RADIATE, "Radiate")
VV_DROPDOWN_OPTION(VV_HK_EDIT_FILTERS, "Edit Filters")
VV_DROPDOWN_OPTION(VV_HK_SELL, "Export Item")
+ VV_DROPDOWN_OPTION(VV_HK_ADD_AI, "Add AI controller")
/atom/vv_do_topic(list/href_list)
. = ..()
@@ -1112,6 +1119,15 @@
var/strength = input(usr, "Choose the radiation strength.", "Choose the strength.") as num|null
if(!isnull(strength))
AddComponent(/datum/component/radioactive, strength, src)
+
+ if(href_list[VV_HK_ADD_AI])
+ if(!check_rights(R_VAREDIT))
+ return
+ var/result = input(usr, "Choose the AI controller to apply to this atom WARNING: Not all AI works on all atoms.", "AI controller") as null|anything in subtypesof(/datum/ai_controller)
+ if(!result)
+ return
+ ai_controller = new result(src)
+
if(href_list[VV_HK_MODIFY_TRANSFORM] && check_rights(R_VAREDIT))
var/result = input(usr, "Choose the transformation to apply","Transform Mod") as null|anything in list("Scale","Translate","Rotate")
var/matrix/M = transform
@@ -1710,3 +1726,12 @@
*/
/atom/proc/setClosed()
return
+
+/**
+* Instantiates the AI controller of this atom. Override this if you want to assign variables first.
+*
+* This will work fine without manually passing arguments.
++*/
+/atom/proc/InitializeAIController()
+ if(ai_controller)
+ ai_controller = new ai_controller(src)
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index ea51bac01b50..93225b2af9a9 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -513,9 +513,6 @@
if(!is_type_in_typecache(target_mob, dangerous_fauna))
return FALSE
- if(ismonkey(target_mob))
- var/mob/living/carbon/monkey/monke = target_mob
- return monke.mode == MONKEY_HUNT && target(target_mob)
if(istype(target_mob, /mob/living/simple_animal/hostile/retaliate))
var/mob/living/simple_animal/hostile/retaliate/target_animal = target_mob
return length(target_animal.enemies) && target(target_mob)
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 23de618975a8..17bcfd78d5c9 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -867,7 +867,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
. = ""
/obj/item/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
- return
+ return SEND_SIGNAL(src, COMSIG_ATOM_HITBY, AM, skipcatch, hitpush, blocked, throwingdatum)
/obj/item/attack_hulk(mob/living/carbon/human/user)
return FALSE
diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm
index f36c27bb244d..66d829baee25 100644
--- a/code/game/objects/items/handcuffs.dm
+++ b/code/game/objects/items/handcuffs.dm
@@ -40,17 +40,13 @@
if(!istype(C))
return
+ SEND_SIGNAL(C, COMSIG_CARBON_CUFF_ATTEMPTED, user)
+
if(iscarbon(user) && (HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50)))
to_chat(user, "Uh... how do those things work?!")
apply_cuffs(user,user)
return
- // chance of monkey retaliation
- if(ismonkey(C) && prob(MONKEY_CUFF_RETALIATION_PROB))
- var/mob/living/carbon/monkey/M
- M = C
- M.retaliate(user)
-
if(!C.handcuffed)
if(C.canBeHandcuffed())
C.visible_message("[user] is trying to put [src.name] on [C]!", \
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index bbcaa94f0867..aa63701ce0e9 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -243,11 +243,20 @@
/obj/get_dumping_location(datum/component/storage/source,mob/user)
return get_turf(src)
-/obj/proc/CanAStarPass(ID, dir, caller)
- if(ismovable(caller))
- var/atom/movable/AM = caller
- if(AM.pass_flags & pass_flags_self)
- return TRUE
+/**
+ * This proc is used for telling whether something can pass by this object in a given direction, for use by the pathfinding system.
+ *
+ * Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely
+ * multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible.
+ *
+ * Arguments:
+ * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference
+ * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions
+ * * caller- The movable we're checking pass flags for, if we're making any such checks
+ **/
+/obj/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller)
+ if(istype(caller) && (caller.pass_flags & pass_flags_self))
+ return TRUE
. = !density
/obj/proc/check_uplink_validity()
diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm
index d30f28801f76..85af7c9bb8eb 100644
--- a/code/game/objects/structures/girders.dm
+++ b/code/game/objects/structures/girders.dm
@@ -304,11 +304,10 @@
if((mover.pass_flags & PASSGRILLE) || istype(mover, /obj/projectile))
return prob(girderpasschance)
-/obj/structure/girder/CanAStarPass(ID, dir, caller)
+/obj/structure/girder/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller)
. = !density
- if(ismovable(caller))
- var/atom/movable/mover = caller
- . = . || (mover.pass_flags & PASSGRILLE)
+ if(istype(caller))
+ . = . || (caller.pass_flags & PASSGRILLE)
/obj/structure/girder/deconstruct(disassembled = TRUE)
if(!(flags_1 & NODECONSTRUCT_1))
diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm
index 10a4413f442f..7e2527c11dae 100644
--- a/code/game/objects/structures/grille.dm
+++ b/code/game/objects/structures/grille.dm
@@ -135,11 +135,10 @@
if(!. && istype(mover, /obj/projectile))
return prob(30)
-/obj/structure/grille/CanAStarPass(ID, dir, caller)
+/obj/structure/grille/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller)
. = !density
- if(ismovable(caller))
- var/atom/movable/mover = caller
- . = . || (mover.pass_flags & PASSGRILLE)
+ if(istype(caller))
+ . = . || (caller.pass_flags & PASSGRILLE)
/obj/structure/grille/attackby(obj/item/W, mob/user, params)
user.changeNext_move(CLICK_CD_MELEE)
diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm
index 35cc9fba1aae..e7a0fa946e23 100644
--- a/code/game/objects/structures/window.dm
+++ b/code/game/objects/structures/window.dm
@@ -379,7 +379,7 @@
/obj/structure/window/get_dumping_location(obj/item/storage/source,mob/user)
return null
-/obj/structure/window/CanAStarPass(ID, to_dir)
+/obj/structure/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller)
if(!density)
return TRUE
if(fulltile || (dir == to_dir))
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 4da6e25703bb..1f9dfc08f7da 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -676,3 +676,23 @@ GLOBAL_LIST_EMPTY(created_baseturf_lists)
/turf/bullet_act(obj/projectile/hitting_projectile)
. = ..()
bullet_hit_sfx(hitting_projectile)
+
+/**
+ * Returns adjacent turfs to this turf that are reachable, in all cardinal directions
+ *
+ * Arguments:
+ * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach
+ * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf
+ * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space?
+*/
+/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only)
+ var/static/space_type_cache = typecacheof(/turf/open/space)
+ . = list()
+
+ for(var/iter_dir in GLOB.cardinals)
+ var/turf/turf_to_check = get_step(src,iter_dir)
+ if(!turf_to_check || (simulated_only && space_type_cache[turf_to_check.type]))
+ continue
+ if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, caller, ID))
+ continue
+ . += turf_to_check
diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm
index 033b71b6df5b..0ed035002f09 100644
--- a/code/modules/antagonists/changeling/powers/tiny_prick.dm
+++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm
@@ -47,7 +47,7 @@
return
if(!isturf(user.loc))
return
- if(!AStar(user, target.loc, /turf/proc/Distance, changeling.sting_range, simulated_only = FALSE))
+ if(!get_path_to(user, target, max_distance = changeling.sting_range, simulated_only = FALSE))
return
if(target.mind && target.mind.has_antag_datum(/datum/antagonist/changeling))
sting_feedback(user, target)
@@ -106,7 +106,7 @@
C.real_name = NewDNA.real_name
NewDNA.transfer_identity(C)
if(ismonkey(C))
- C.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG)
+ C.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG | TR_KEEPAI)
C.updateappearance(mutcolor_update=1)
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index 5276cf514d65..bcf78e60c8d1 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -108,6 +108,7 @@
/mob/proc/throw_item(atom/target)
SEND_SIGNAL(src, COMSIG_MOB_THROW, target)
+ SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CARBON_THROW_THING, src, target)
return
/mob/living/carbon/throw_item(atom/target)
diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm
index 51815282406d..48747f0106f8 100644
--- a/code/modules/mob/living/carbon/carbon_defense.dm
+++ b/code/modules/mob/living/carbon/carbon_defense.dm
@@ -138,6 +138,9 @@
//ATTACK HAND IGNORING PARENT RETURN VALUE
/mob/living/carbon/attack_hand(mob/living/carbon/human/user)
+ if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_HAND, user) & COMPONENT_CANCEL_ATTACK_CHAIN)
+ . = TRUE
+
for(var/datum/surgery/S in surgeries)
if(body_position != LYING_DOWN && S.lying_required)
continue
diff --git a/code/modules/mob/living/carbon/emote.dm b/code/modules/mob/living/carbon/emote.dm
index 358fa0626092..d96bbd72531a 100644
--- a/code/modules/mob/living/carbon/emote.dm
+++ b/code/modules/mob/living/carbon/emote.dm
@@ -82,7 +82,22 @@
key = "screech"
key_third_person = "screeches"
message = "screeches."
- mob_type_allowed_typecache = list(/mob/living/carbon/monkey, /mob/living/carbon/alien)
+ mob_type_allowed_typecache = list(/mob/living/carbon/monkey)
+ emote_type = EMOTE_AUDIBLE
+
+/datum/emote/living/carbon/screech/get_sound(mob/living/user)
+ return pick('sound/creatures/monkey/monkey_screech_1.ogg',
+ 'sound/creatures/monkey/monkey_screech_2.ogg',
+ 'sound/creatures/monkey/monkey_screech_3.ogg',
+ 'sound/creatures/monkey/monkey_screech_4.ogg',
+ 'sound/creatures/monkey/monkey_screech_5.ogg',
+ 'sound/creatures/monkey/monkey_screech_6.ogg',
+ 'sound/creatures/monkey/monkey_screech_7.ogg')
+
+/datum/emote/living/carbon/screech/roar
+ key = "roar"
+ key_third_person = "roars"
+ message = "roars."
/datum/emote/living/carbon/sign
key = "sign"
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 0c8782129698..c4a447b59d5b 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -7,6 +7,7 @@
var/t_him = p_them()
var/t_has = p_have()
var/t_is = p_are()
+ var/t_es = p_es()
var/obscure_name
var/list/obscured = check_obscured_slots()
var/skipface = ((wear_mask?.flags_inv & HIDEFACE) || (head?.flags_inv & HIDEFACE))
@@ -330,6 +331,8 @@
if(HAS_TRAIT(src, TRAIT_DUMB))
msg += "[t_He] [t_has] a stupid expression on [t_his] face.\n"
if(getorgan(/obj/item/organ/brain))
+ if(ai_controller?.ai_status == AI_STATUS_ON)
+ msg += "[t_He] do[t_es]n't appear to be [t_him]self.\n"
if(!key)
msg += "[t_He] [t_is] totally catatonic. The stresses of life in deep-space must have been too much for [t_him]. Any recovery is unlikely.\n"
else if(!client)
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index a4d89a53b548..b33a751df628 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -1289,6 +1289,9 @@
return known_name
return .
+/mob/living/carbon/human/monkeybrain
+ ai_controller = /datum/ai_controller/monkey
+
/mob/living/carbon/human/species
var/race = null
diff --git a/code/modules/mob/living/carbon/monkey/combat.dm b/code/modules/mob/living/carbon/monkey/combat.dm
deleted file mode 100644
index 8fd4e89566c7..000000000000
--- a/code/modules/mob/living/carbon/monkey/combat.dm
+++ /dev/null
@@ -1,426 +0,0 @@
-#define MAX_RANGE_FIND 32
-
-/mob/living/carbon/monkey
- var/aggressive=0 // set to 1 using VV for an angry monkey
- var/frustration=0
- var/pickupTimer=0
- var/list/enemies = list()
- var/mob/living/target
- var/obj/item/pickupTarget
- var/mode = MONKEY_IDLE
- var/list/myPath = list()
- var/list/blacklistItems = list()
- var/maxStepsTick = 6
- var/best_force = 0
- var/martial_art = new/datum/martial_art
- var/resisting = FALSE
- var/pickpocketing = FALSE
- var/disposing_body = FALSE
- var/obj/machinery/disposal/bodyDisposal = null
- var/next_battle_screech = 0
- var/battle_screech_cooldown = 50
-
-/mob/living/carbon/monkey/proc/IsStandingStill()
- return resisting || pickpocketing || disposing_body
-
-// blocks
-// taken from /mob/living/carbon/human/interactive/
-/mob/living/carbon/monkey/proc/walk2derpless(target)
- if(!target || IsStandingStill())
- return 0
-
- if(myPath.len <= 0)
- myPath = get_path_to(src, get_turf(target), /turf/proc/Distance, MAX_RANGE_FIND + 1, 250,1)
-
- if(myPath)
- if(myPath.len > 0)
- for(var/i = 0; i < maxStepsTick; ++i)
- if(!IsDeadOrIncap())
- if(myPath.len >= 1)
- walk_to(src,myPath[1],0,5)
- myPath -= myPath[1]
- return 1
-
- // failed to path correctly so just try to head straight for a bit
- walk_to(src,get_turf(target),0,5)
- sleep(1)
- walk_to(src,0)
-
- return 0
-
-
-// taken from /mob/living/carbon/human/interactive/
-/mob/living/carbon/monkey/proc/IsDeadOrIncap()
- return HAS_TRAIT(src, TRAIT_INCAPACITATED) || HAS_TRAIT(src, TRAIT_HANDS_BLOCKED)
-
-
-/mob/living/carbon/monkey/proc/battle_screech()
- if(next_battle_screech < world.time)
- emote(pick("roar","screech"))
- for(var/mob/living/carbon/monkey/M in view(7,src))
- M.next_battle_screech = world.time + battle_screech_cooldown
-
-/mob/living/carbon/monkey/proc/equip_item(obj/item/I)
- if(I.loc == src)
- return TRUE
-
- if(I.anchored)
- blacklistItems[I] ++
- return FALSE
-
- // WEAPONS
- if(istype(I, /obj/item))
- var/obj/item/W = I
- if(W.force >= best_force)
- put_in_hands(W)
- best_force = W.force
- return TRUE
-
- // CLOTHING
- else if(istype(I, /obj/item/clothing))
- var/obj/item/clothing/C = I
- monkeyDrop(C)
- addtimer(CALLBACK(src, PROC_REF(pickup_and_wear), C), 5)
- return TRUE
-
- // EVERYTHING ELSE
- else
- if(!get_item_for_held_index(1) || !get_item_for_held_index(2))
- put_in_hands(I)
- return TRUE
-
- blacklistItems[I] ++
- return FALSE
-
-/mob/living/carbon/monkey/proc/pickup_and_wear(obj/item/clothing/C)
- if(!equip_to_appropriate_slot(C))
- monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn
- addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5)
-
-/mob/living/carbon/monkey/resist_restraints()
- var/obj/item/I = null
- if(handcuffed)
- I = handcuffed
- else if(legcuffed)
- I = legcuffed
- if(I)
- changeNext_move(CLICK_CD_BREAKOUT)
- last_special = world.time + CLICK_CD_BREAKOUT
- cuff_resist(I)
-
-/mob/living/carbon/monkey/proc/should_target(mob/living/L)
- if(HAS_TRAIT(src, TRAIT_PACIFISM))
- return FALSE
-
- if(enemies[L])
- return TRUE
-
- // target non-monkey mobs when aggressive, with a small probability of monkey v monkey
- if(aggressive && (!istype(L, /mob/living/carbon/monkey/) || prob(MONKEY_AGGRESSIVE_MVM_PROB)))
- return TRUE
-
- return FALSE
-
-/mob/living/carbon/monkey/proc/handle_combat()
- if(pickupTarget)
- if(IsDeadOrIncap() || blacklistItems[pickupTarget] || HAS_TRAIT(pickupTarget, TRAIT_NODROP))
- pickupTarget = null
- else
- pickupTimer++
- if(pickupTimer >= 4)
- blacklistItems[pickupTarget] ++
- pickupTarget = null
- pickupTimer = 0
- else
- INVOKE_ASYNC(src, PROC_REF(walk2derpless), pickupTarget.loc)
- if(Adjacent(pickupTarget) || Adjacent(pickupTarget.loc)) // next to target
- drop_all_held_items() // who cares about these items, i want that one!
- if(isturf(pickupTarget.loc)) // on floor
- equip_item(pickupTarget)
- pickupTarget = null
- pickupTimer = 0
- else if(ismob(pickupTarget.loc)) // in someones hand
- var/mob/M = pickupTarget.loc
- if(!pickpocketing)
- pickpocketing = TRUE
- M.visible_message("[src] starts trying to take [pickupTarget] from [M]!", "[src] tries to take [pickupTarget]!")
- INVOKE_ASYNC(src, PROC_REF(pickpocket), M)
- return TRUE
-
- switch(mode)
- if(MONKEY_IDLE) // idle
- if(enemies.len)
- var/list/around = view(src, MONKEY_ENEMY_VISION) // scan for enemies
- for(var/mob/living/L in around)
- if(should_target(L))
- if(L.stat == CONSCIOUS)
- battle_screech()
- retaliate(L)
- return TRUE
- else
- bodyDisposal = locate(/obj/machinery/disposal/) in around
- if(bodyDisposal)
- target = L
- mode = MONKEY_DISPOSE
- return TRUE
-
- // pickup any nearby objects
- if(!pickupTarget)
- var/obj/item/I = locate(/obj/item/) in oview(2,src)
- if(I && !blacklistItems[I])
- pickupTarget = I
- else
- var/mob/living/carbon/human/H = locate(/mob/living/carbon/human/) in oview(2,src)
- if(H)
- pickupTarget = pick(H.held_items)
-
- if(MONKEY_HUNT) // hunting for attacker
- if(health < MONKEY_FLEE_HEALTH)
- mode = MONKEY_FLEE
- return TRUE
-
- if(target != null)
- INVOKE_ASYNC(src, PROC_REF(walk2derpless), target)
-
- // pickup any nearby weapon
- if(!pickupTarget && prob(MONKEY_WEAPON_PROB))
- var/obj/item/W = locate(/obj/item/) in oview(2,src)
- if(!locate(/obj/item) in held_items)
- best_force = 0
- if(W && !blacklistItems[W] && W.force > best_force)
- pickupTarget = W
-
- // recruit other monkies
- var/list/around = view(src, MONKEY_ENEMY_VISION)
- for(var/mob/living/carbon/monkey/M in around)
- if(M.mode == MONKEY_IDLE && prob(MONKEY_RECRUIT_PROB))
- M.battle_screech()
- M.target = target
- M.mode = MONKEY_HUNT
-
- // switch targets
- for(var/mob/living/L in around)
- if(L != target && should_target(L) && L.stat == CONSCIOUS && prob(MONKEY_SWITCH_TARGET_PROB))
- target = L
- return TRUE
-
- // if can't reach target for long enough, go idle
- if(frustration >= MONKEY_HUNT_FRUSTRATION_LIMIT)
- back_to_idle()
- return TRUE
-
- if(target && target.stat == CONSCIOUS) // make sure target exists
- if(Adjacent(target) && isturf(target.loc) && !IsDeadOrIncap()) // if right next to perp
-
- // 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 && prob(MONKEY_ATTACK_DISARM_PROB))
- pickupTarget = W
- a_intent = INTENT_DISARM
- monkey_attack(target)
-
- else
- a_intent = INTENT_HARM
- monkey_attack(target)
-
- return TRUE
-
- else // not next to perp
- var/turf/olddist = get_dist(src, target)
- if((get_dist(src, target)) >= (olddist))
- frustration++
- else
- frustration = 0
- else
- back_to_idle()
-
- if(MONKEY_FLEE)
- var/list/around = view(src, MONKEY_FLEE_VISION)
- target = null
-
- // flee from anyone who attacked us and we didn't beat down
- for(var/mob/living/L in around)
- if(enemies[L] && L.stat == CONSCIOUS)
- target = L
-
- if(target != null)
- walk_away(src, target, MONKEY_ENEMY_VISION, 5)
- else
- back_to_idle()
-
- return TRUE
-
- if(MONKEY_DISPOSE)
-
- // if can't dispose of body go back to idle
- if(!target || !bodyDisposal || frustration >= MONKEY_DISPOSE_FRUSTRATION_LIMIT)
- back_to_idle()
- return TRUE
-
- if(target.pulledby != src && !istype(target.pulledby, /mob/living/carbon/monkey/))
-
- INVOKE_ASYNC(src, PROC_REF(walk2derpless), target.loc)
-
- if(Adjacent(target) && isturf(target.loc))
- a_intent = INTENT_GRAB
- target.grabbedby(src)
- else
- var/turf/olddist = get_dist(src, target)
- if((get_dist(src, target)) >= (olddist))
- frustration++
- else
- frustration = 0
-
- else if(!disposing_body)
- INVOKE_ASYNC(src, PROC_REF(walk2derpless), bodyDisposal.loc)
-
- if(Adjacent(bodyDisposal))
- disposing_body = TRUE
- addtimer(CALLBACK(src, PROC_REF(stuff_mob_in)), 5)
-
- else
- var/turf/olddist = get_dist(src, bodyDisposal)
- if((get_dist(src, bodyDisposal)) >= (olddist))
- frustration++
- else
- frustration = 0
-
- return TRUE
-
- return IsStandingStill()
-
-/mob/living/carbon/monkey/proc/pickpocket(mob/M)
- if(do_after(src, MONKEY_ITEM_SNATCH_DELAY, M) && pickupTarget)
- for(var/obj/item/I in M.held_items)
- if(I == pickupTarget)
- M.visible_message("[src] snatches [pickupTarget] from [M].", "[src] snatched [pickupTarget]!")
- if(M.temporarilyRemoveItemFromInventory(pickupTarget))
- if(!QDELETED(pickupTarget) && !equip_item(pickupTarget))
- pickupTarget.forceMove(drop_location())
- else
- M.visible_message("[src] tried to snatch [pickupTarget] from [M], but failed!", "[src] tried to grab [pickupTarget]!")
- pickpocketing = FALSE
- pickupTarget = null
- pickupTimer = 0
-
-/mob/living/carbon/monkey/proc/stuff_mob_in()
- if(bodyDisposal && target && Adjacent(bodyDisposal))
- bodyDisposal.stuff_mob_in(target, src)
- disposing_body = FALSE
- back_to_idle()
-
-/mob/living/carbon/monkey/proc/back_to_idle()
-
- if(pulling)
- stop_pulling()
-
- mode = MONKEY_IDLE
- target = null
- a_intent = INTENT_HELP
- frustration = 0
- walk_to(src,0)
-
-// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little
-/mob/living/carbon/monkey/proc/monkey_attack(mob/living/L)
- var/obj/item/Weapon = locate(/obj/item) in held_items
-
- // attack with weapon if we have one
- if(Weapon)
- Weapon.melee_attack_chain(src, L)
- else
- L.attack_paw(src)
-
- // no de-aggro
- if(aggressive)
- return
-
- // if we arn't enemies, we were likely recruited to attack this target, jobs done if we calm down so go back to idle
- if(!enemies[L])
- if(target == L && prob(MONKEY_HATRED_REDUCTION_PROB))
- back_to_idle()
- return // already de-aggroed
-
- if(prob(MONKEY_HATRED_REDUCTION_PROB))
- enemies[L] --
-
- // if we are not angry at our target, go back to idle
- if(enemies[L] <= 0)
- enemies.Remove(L)
- if(target == L)
- back_to_idle()
-
-// get angry are a mob
-/mob/living/carbon/monkey/proc/retaliate(mob/living/L)
- mode = MONKEY_HUNT
- target = L
- if(L != src)
- enemies[L] += MONKEY_HATRED_AMOUNT
-
- if(a_intent != INTENT_HARM)
- battle_screech()
- a_intent = INTENT_HARM
-
-/mob/living/carbon/monkey/attack_hand(mob/living/L)
- if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB))
- retaliate(L)
- else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB))
- retaliate(L)
- return ..()
-
-/mob/living/carbon/monkey/attack_paw(mob/living/L)
- if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB))
- retaliate(L)
- else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB))
- retaliate(L)
- return ..()
-
-/mob/living/carbon/monkey/attackby(obj/item/W, mob/user, params)
- ..()
- if((W.force) && (!target) && (W.damtype != STAMINA))
- retaliate(user)
-
-/mob/living/carbon/monkey/bullet_act(obj/projectile/Proj)
- 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 < src.health && isliving(Proj.firer))
- retaliate(Proj.firer)
- . = ..()
-
-/mob/living/carbon/monkey/hitby(atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum)
- if(istype(AM, /obj/item))
- var/obj/item/I = AM
- if(I.throwforce < src.health && I.thrownby && ishuman(I.thrownby))
- var/mob/living/carbon/human/H = I.thrownby
- retaliate(H)
- ..()
-
-/mob/living/carbon/monkey/on_entered(datum/source, atom/movable/AM)
- . = ..()
- if(!IsDeadOrIncap() && ismob(AM) && target)
- var/mob/living/carbon/monkey/M = AM
- if(!istype(M) || !M)
- return
- knockOver(M)
- return
-
-/mob/living/carbon/monkey/proc/monkeyDrop(obj/item/A)
- if(A)
- dropItemToGround(A, TRUE)
- update_icons()
-
-/mob/living/carbon/monkey/grabbedby(mob/living/carbon/user)
- . = ..()
- if(!IsDeadOrIncap() && pulledby && (mode != MONKEY_IDLE || prob(MONKEY_PULL_AGGRO_PROB))) // nuh uh you don't pull me!
- if(Adjacent(pulledby))
- a_intent = INTENT_DISARM
- monkey_attack(pulledby)
- retaliate(pulledby)
- return TRUE
-
-#undef MAX_RANGE_FIND
diff --git a/code/modules/mob/living/carbon/monkey/life.dm b/code/modules/mob/living/carbon/monkey/life.dm
index b4469ea5b63c..01423b1aa2ee 100644
--- a/code/modules/mob/living/carbon/monkey/life.dm
+++ b/code/modules/mob/living/carbon/monkey/life.dm
@@ -1,33 +1,5 @@
-
-
/mob/living/carbon/monkey
-
-/mob/living/carbon/monkey/Life()
- set invisibility = 0
-
- if (notransform)
- return
-
- if(..() && !IS_IN_STASIS(src))
-
- if(!client)
- if(stat == CONSCIOUS)
- if(on_fire || buckled || HAS_TRAIT(src, TRAIT_RESTRAINED) || (pulledby && pulledby.grab_state > GRAB_PASSIVE))
- if(!resisting && prob(MONKEY_RESIST_PROB))
- resisting = TRUE
- walk_to(src,0)
- execute_resist()
- else if(resisting)
- resisting = FALSE
- else if((mode == MONKEY_IDLE && !pickupTarget && !prob(MONKEY_SHENANIGAN_PROB)) || !handle_combat())
- if(prob(25) && (mobility_flags & MOBILITY_MOVE) && isturf(loc) && !pulledby)
- step(src, pick(GLOB.cardinals))
- else if(prob(1))
- emote(pick("scratch","jump","roll","tail"))
- else
- walk_to(src,0)
-
/mob/living/carbon/monkey/handle_mutations_and_radiation()
if(radiation)
if(radiation > RAD_MOB_KNOCKDOWN && prob(RAD_MOB_KNOCKDOWN_PROB))
diff --git a/code/modules/mob/living/carbon/monkey/monkey.dm b/code/modules/mob/living/carbon/monkey/monkey.dm
index 755c674a107d..6056ac83fa7d 100644
--- a/code/modules/mob/living/carbon/monkey/monkey.dm
+++ b/code/modules/mob/living/carbon/monkey/monkey.dm
@@ -26,6 +26,8 @@
hud_type = /datum/hud/monkey
melee_damage_lower = 1
melee_damage_upper = 3
+ ai_controller = /datum/ai_controller/monkey
+ faction = list("neutral", "monkey")
/mob/living/carbon/monkey/Initialize(mapload, cubespawned=FALSE, mob/spawner)
add_verb(src, /mob/living/proc/mob_sleep)
@@ -169,10 +171,10 @@
return 1
/mob/living/carbon/monkey/angry
- aggressive = TRUE
/mob/living/carbon/monkey/angry/Initialize()
. = ..()
+ ai_controller.blackboard[BB_MONKEY_AGRESSIVE] = TRUE
if(prob(10))
var/obj/item/clothing/head/helmet/justice/escape/helmet = new(src)
equip_to_slot_or_del(helmet,ITEM_SLOT_HEAD)
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 99db31b26c0f..ba9b99822600 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1125,7 +1125,7 @@
if(G.trigger_guard == TRIGGER_GUARD_NONE)
to_chat(src, "You are unable to fire this!")
return FALSE
- if(G.trigger_guard != TRIGGER_GUARD_ALLOW_ALL && !IsAdvancedToolUser())
+ if(G.trigger_guard != TRIGGER_GUARD_ALLOW_ALL && (!IsAdvancedToolUser(src) && !HAS_TRAIT(src, TRAIT_GUN_NATURAL)))
to_chat(src, "You try to fire [G], but can't use the trigger!")
return FALSE
return TRUE
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index 3c7736c06230..1e81bd48b63f 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -564,7 +564,7 @@ Pass a positive integer as an argument to override a bot's default speed.
var/datum/job/captain/All = new/datum/job/captain
all_access.access = All.get_access()
- set_path(get_path_to(src, waypoint, /turf/proc/Distance_cardinal, 0, 200, id=all_access))
+ set_path(get_path_to(src, waypoint, 200, id=all_access))
calling_ai = caller //Link the AI to the bot!
ai_waypoint = waypoint
@@ -782,16 +782,16 @@ Pass a positive integer as an argument to override a bot's default speed.
// given an optional turf to avoid
/mob/living/simple_animal/bot/proc/calc_path(turf/avoid)
check_bot_access()
- set_path(get_path_to(src, patrol_target, /turf/proc/Distance_cardinal, 0, 120, id=access_card, exclude=avoid))
+ set_path(get_path_to(src, patrol_target, 120, id=access_card, exclude=avoid))
/mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid)
check_bot_access()
INVOKE_ASYNC(src, PROC_REF(do_calc_summon_path), avoid)
/mob/living/simple_animal/bot/proc/do_calc_summon_path(turf/avoid)
- set_path(get_path_to(src, summon_target, /turf/proc/Distance_cardinal, 0, 150, id=access_card, exclude=avoid))
+ set_path(get_path_to(src, summon_target, 150, id=access_card, exclude=avoid))
if(!length(path)) //Cannot reach target. Give up and announce the issue.
- speak("Summon command failed, destination unreachable.",radio_channel)
+ speak("Summon command failed, destination unreachable.", radio_channel)
bot_reset()
/mob/living/simple_animal/bot/proc/summon_step()
@@ -816,7 +816,9 @@ Pass a positive integer as an argument to override a bot's default speed.
calc_summon_path()
/mob/living/simple_animal/bot/proc/summon_step_not_moved()
- calc_summon_path()
+ //calc_summon_path()
+ speak("Summon command failed, destination unreachable.",radio_channel)
+ bot_reset()
tries = 0
/mob/living/simple_animal/bot/Bump(atom/A) //Leave no door unopened!
diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
index e53b675c95bc..aad4a7a63f86 100644
--- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm
@@ -261,11 +261,11 @@
mode = BOT_IDLE
return
- if(target && path.len == 0 && (get_dist(src,target) > 1))
- path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 30, id=access_card)
+ if(target && (!path || path.len == 0) && (get_dist(src,target) > 1))
+ path = get_path_to(src, target, 30, id=access_card)
mode = BOT_MOVING
if(!path.len) //try to get closer if you can't reach the target directly
- path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 30, 1, id=access_card)
+ path = get_path_to(src, target, 30, id=access_card)
if(!path.len) //Do not chase a target we cannot reach.
add_to_ignore(target)
target = null
diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm
index 0fabc6c7fb53..1b04fbb51669 100644
--- a/code/modules/mob/living/simple_animal/bot/firebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/firebot.dm
@@ -269,7 +269,7 @@
if(get_dist(src, target_fire) > 2)
- path = get_path_to(src, get_turf(target_fire), /turf/proc/Distance_cardinal, 0, 30, 1, id=access_card)
+ path = get_path_to(src, target_fire, 30, 1, id=access_card)
mode = BOT_MOVING
if(!length(path))
soft_reset()
diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm
index 980f12897e70..662386649186 100644
--- a/code/modules/mob/living/simple_animal/bot/floorbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm
@@ -255,9 +255,9 @@
if(path.len == 0)
if(!isturf(target))
var/turf/TL = get_turf(target)
- path = get_path_to(src, TL, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE)
+ path = get_path_to(src, TL, 30, id=access_card,simulated_only = FALSE)
else
- path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE)
+ path = get_path_to(src, target, 30, id=access_card,simulated_only = FALSE)
if(!bot_move(target))
add_to_ignore(target)
diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm
index 22d68c8a6190..6bcd39abb6a6 100644
--- a/code/modules/mob/living/simple_animal/bot/medbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/medbot.dm
@@ -413,10 +413,10 @@
return
if(patient && path.len == 0 && (get_dist(src,patient) > 1))
- path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,id=access_card)
+ path = get_path_to(src, patient, 30,id=access_card)
mode = BOT_MOVING
if(!path.len) //try to get closer if you can't reach the patient directly
- path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,1,id=access_card)
+ path = get_path_to(src, patient, 30,1,id=access_card)
if(!path.len) //Do not chase a patient we cannot reach.
soft_reset()
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
index 1c10311f7b3c..59a6125b5fbd 100644
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm
@@ -615,7 +615,7 @@
// calculates a path to the current destination
// given an optional turf to avoid
/mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null)
- path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 250, id=access_card, exclude=avoid)
+ path = get_path_to(src, target, 250, id=access_card, exclude=avoid)
// sets the current destination
// signals all beacons matching the delivery code
diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm
index 2a47d4f0c220..6dc3bbf55048 100644
--- a/code/modules/mob/living/simple_animal/friendly/dog.dm
+++ b/code/modules/mob/living/simple_animal/friendly/dog.dm
@@ -16,65 +16,11 @@
see_in_dark = 5
speak_chance = 1
turns_per_move = 10
- var/turns_since_scan = 0
- var/obj/movement_target
+ ai_controller = /datum/ai_controller/dog
+ stop_automated_movement = TRUE
footstep_type = FOOTSTEP_MOB_CLAW
-/mob/living/simple_animal/pet/dog/Life()
- ..()
-
- //Feeding, chasing food, FOOOOODDDD
- if(!stat && !resting && !buckled)
- turns_since_scan++
- if(turns_since_scan > 5)
- turns_since_scan = 0
- if((movement_target) && !(isturf(movement_target.loc) || ishuman(movement_target.loc)))
- movement_target = null
- stop_automated_movement = 0
- if(!movement_target || !(movement_target.loc in oview(src, 3)))
- movement_target = null
- stop_automated_movement = 0
- for(var/obj/item/reagent_containers/food/snacks/S in oview(src,3))
- if(isturf(S.loc) || ishuman(S.loc))
- movement_target = S
- break
- if(movement_target)
- stop_automated_movement = 1
- step_to(src,movement_target,1)
- sleep(3)
- step_to(src,movement_target,1)
- sleep(3)
- step_to(src,movement_target,1)
-
- if(movement_target) //Not redundant due to sleeps, Item can be gone in 6 decisecomds
- var/turf/T = get_turf(movement_target)
- if(!T)
- return
- if (T.x < src.x)
- setDir(WEST)
- else if (T.x > src.x)
- setDir(EAST)
- else if (T.y < src.y)
- setDir(SOUTH)
- else if (T.y > src.y)
- setDir(NORTH)
- else
- setDir(SOUTH)
-
- if(!Adjacent(movement_target)) //can't reach food through windows.
- return
-
- if(isturf(movement_target.loc))
- movement_target.attack_animal(src)
- else if(ishuman(movement_target.loc))
- if(prob(20))
- manual_emote("stares at [movement_target.loc]'s [movement_target] with a sad puppy-face")
-
- if(prob(1))
- manual_emote(pick("dances around.","chases its tail!"))
- INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(dance_rotate), src)
-
//Corgis and pugs are now under one dog subtype
/mob/living/simple_animal/pet/dog/corgi
@@ -165,6 +111,7 @@
dat += "Head: | [inventory_head]" : "add_inv=head'>Empty"] |
"
dat += "Back: | [inventory_back]" : "add_inv=back'>Empty"] |
"
dat += "Collar: | [pcollar]" : "add_inv=collar'>Empty"] |
"
+ dat += "ID Card: | [access_card]" : "add_inv=card'>Empty"] |
"
dat += {"
Close
"}
@@ -248,6 +195,10 @@
pcollar = null
update_corgi_fluff()
regenerate_icons()
+ if("card")
+ if(access_card)
+ usr.put_in_hands(access_card)
+ access_card = null
show_inv(usr)
@@ -300,9 +251,22 @@
return
item_to_add.forceMove(src)
- src.inventory_back = item_to_add
+ inventory_back = item_to_add
update_corgi_fluff()
regenerate_icons()
+ if("card")
+ if(access_card)
+ to_chat(usr, "[src] already has \an [access_card] pinned to [p_them()]!")
+ return
+ var/obj/item/item_to_add = usr.get_active_held_item()
+ if(!usr.temporarilyRemoveItemFromInventory(item_to_add))
+ to_chat(usr, "\The [item_to_add] is stuck to your hand, you cannot pin it to [src]!")
+ return
+ if(!istype(item_to_add, /obj/item/card/id))
+ to_chat(usr, "You can't pin [item_to_add] to [src]!")
+ return
+ item_to_add.forceMove(src)
+ access_card = item_to_add
show_inv(usr)
else
diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm
index 92b955d3a841..5a900e64e199 100644
--- a/code/modules/mob/living/simple_animal/parrot.dm
+++ b/code/modules/mob/living/simple_animal/parrot.dm
@@ -648,7 +648,7 @@
item = I
break
if(item)
- if(!AStar(src, get_turf(item), /turf/proc/Distance_cardinal))
+ if(!get_path_to(src, item))
item = null
continue
return item
diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm
index a1a0886a2362..738428592d9a 100644
--- a/code/modules/mob/living/simple_animal/simple_animal.dm
+++ b/code/modules/mob/living/simple_animal/simple_animal.dm
@@ -208,7 +208,8 @@
. = ..()
if(stat == DEAD)
. += "Upon closer examination, [p_they()] appear[p_s()] to be dead."
-
+ if(access_card)
+ . += "There appears to be [icon2html(access_card, user)] \a [access_card] pinned to [p_them()]."
/mob/living/simple_animal/update_stat()
if(status_flags & GODMODE)
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index ef21915e1fca..1be945b3d8f4 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -1,6 +1,6 @@
#define TRANSFORMATION_DURATION 22
-/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG))
+/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG| TR_KEEPAI))
if (notransform || transformation_timer)
return
@@ -150,6 +150,8 @@
changeling.purchasedpowers += hf
changeling.regain_powers()
+ if(tr_flags & TR_KEEPAI)
+ ai_controller.PossessPawn(O)
if (tr_flags & TR_DEFAULTMSG)
to_chat(O, "You are now a monkey.")
@@ -167,7 +169,7 @@
////////////////////////// Humanize //////////////////////////////
//Could probably be merged with monkeyize but other transformations got their own procs, too
-/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG))
+/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG | TR_KEEPAI))
if (notransform || transformation_timer)
return
@@ -329,6 +331,9 @@
else
O.set_species(/datum/species/human)
+ if(tr_flags & TR_KEEPAI)
+ ai_controller.PossessPawn(O)
+
O.a_intent = INTENT_HELP
if (tr_flags & TR_DEFAULTMSG)
to_chat(O, "You are now a human.")
diff --git a/code/modules/movespeed/_movespeed_modifier.dm b/code/modules/movespeed/_movespeed_modifier.dm
index 06cbaf0b99cd..4befe2458faa 100644
--- a/code/modules/movespeed/_movespeed_modifier.dm
+++ b/code/modules/movespeed/_movespeed_modifier.dm
@@ -200,6 +200,7 @@ GLOBAL_LIST_EMPTY(movespeed_modification_cache)
continue
. += amt
cached_multiplicative_slowdown = .
+ SEND_SIGNAL(src, COMSIG_MOB_MOVESPEED_UPDATED)
/// Get the move speed modifiers list of the mob
/mob/proc/get_movespeed_modifiers()
diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm
index 5d11dcb720ee..3241695c7e78 100644
--- a/code/modules/reagents/reagent_containers/syringes.dm
+++ b/code/modules/reagents/reagent_containers/syringes.dm
@@ -67,11 +67,7 @@
if(!L.can_inject(user, 1))
return
- // chance of monkey retaliation
- if(ismonkey(target) && prob(MONKEY_SYRINGE_RETALIATION_PROB))
- var/mob/living/carbon/monkey/M
- M = target
- M.retaliate(user)
+ SEND_SIGNAL(target, COMSIG_LIVING_TRY_SYRINGE, user)
switch(mode)
if(SYRINGE_DRAW)
diff --git a/shiptest.dme b/shiptest.dme
index ef4500858dca..d7a9f1aa48d9 100644
--- a/shiptest.dme
+++ b/shiptest.dme
@@ -160,6 +160,7 @@
#include "code\__DEFINES\vv.dm"
#include "code\__DEFINES\wall_dents.dm"
#include "code\__DEFINES\wires.dm"
+#include "code\__DEFINES\ai\ai.dm"
#include "code\__DEFINES\dcs\flags.dm"
#include "code\__DEFINES\dcs\helpers.dm"
#include "code\__DEFINES\dcs\signals\signals.dm"
@@ -185,7 +186,6 @@
#include "code\__HELPERS\_planes.dm"
#include "code\__HELPERS\_string_lists.dm"
#include "code\__HELPERS\areas.dm"
-#include "code\__HELPERS\AStar.dm"
#include "code\__HELPERS\atoms.dm"
#include "code\__HELPERS\bindings.dm"
#include "code\__HELPERS\bitflag_lists.dm"
@@ -212,6 +212,7 @@
#include "code\__HELPERS\mouse_control.dm"
#include "code\__HELPERS\nameof.dm"
#include "code\__HELPERS\names.dm"
+#include "code\__HELPERS\path.dm"
#include "code\__HELPERS\priority_announce.dm"
#include "code\__HELPERS\pronouns.dm"
#include "code\__HELPERS\qdel.dm"
@@ -325,6 +326,7 @@
#include "code\controllers\configuration\entries\resources.dm"
#include "code\controllers\subsystem\achievements.dm"
#include "code\controllers\subsystem\acid.dm"
+#include "code\controllers\subsystem\ai_controllers.dm"
#include "code\controllers\subsystem\air.dm"
#include "code\controllers\subsystem\ambience.dm"
#include "code\controllers\subsystem\assets.dm"
@@ -401,6 +403,8 @@
#include "code\controllers\subsystem\vis_overlays.dm"
#include "code\controllers\subsystem\vote.dm"
#include "code\controllers\subsystem\weather.dm"
+#include "code\controllers\subsystem\processing\ai_behaviors.dm"
+#include "code\controllers\subsystem\processing\ai_movement.dm"
#include "code\controllers\subsystem\processing\fastprocess.dm"
#include "code\controllers\subsystem\processing\fluids.dm"
#include "code\controllers\subsystem\processing\instruments.dm"
@@ -468,6 +472,19 @@
#include "code\datums\achievements\skill_achievements.dm"
#include "code\datums\actions\beam_rifle.dm"
#include "code\datums\actions\ninja.dm"
+#include "code\datums\ai\_ai_behavoir.dm"
+#include "code\datums\ai\_ai_controller.dm"
+#include "code\datums\ai\_ai_planning_subtree.dm"
+#include "code\datums\ai\generic_actions.dm"
+#include "code\datums\ai\dog\dog_behaviors.dm"
+#include "code\datums\ai\dog\dog_controller.dm"
+#include "code\datums\ai\dog\dog_subtrees.dm"
+#include "code\datums\ai\monkey\monkey_behaviors.dm"
+#include "code\datums\ai\monkey\monkey_controller.dm"
+#include "code\datums\ai\monkey\monkey_subtrees.dm"
+#include "code\datums\ai\movement\_ai_movement.dm"
+#include "code\datums\ai\movement\ai_movement_dumb.dm"
+#include "code\datums\ai\movement\ai_movement_jps.dm"
#include "code\datums\atmosphere\_atmosphere.dm"
#include "code\datums\atmosphere\planetary.dm"
#include "code\datums\brain_damage\brain_trauma.dm"
@@ -554,6 +571,7 @@
#include "code\datums\components\sizzle.dm"
#include "code\datums\components\slippery.dm"
#include "code\datums\components\spill.dm"
+#include "code\datums\components\spinny.dm"
#include "code\datums\components\spooky.dm"
#include "code\datums\components\squeak.dm"
#include "code\datums\components\stationstuck.dm"
@@ -2634,7 +2652,6 @@
#include "code\modules\mob\living\carbon\human\species_types\vampire.dm"
#include "code\modules\mob\living\carbon\human\species_types\vox.dm"
#include "code\modules\mob\living\carbon\human\species_types\zombies.dm"
-#include "code\modules\mob\living\carbon\monkey\combat.dm"
#include "code\modules\mob\living\carbon\monkey\death.dm"
#include "code\modules\mob\living\carbon\monkey\inventory.dm"
#include "code\modules\mob\living\carbon\monkey\life.dm"
diff --git a/sound/creatures/monkey/monkey_screech_1.ogg b/sound/creatures/monkey/monkey_screech_1.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..a4d5bc45429aa0416a1a189c0aaefca1ab8af5bd
GIT binary patch
literal 15300
zcmeIYbyOYA@+dl3u;A_+3GVJraCaxTy9D1@u%N-+A!u-ygkT{+@ZbT0ThJZY$lJ+x
z?m6eK`__7E{ocQ~X3eyebai!2bx%*L+u3OY@W8(yOoLkMPd0xUi3pAo&ezS;(!uLb
z1-xwSp9BUT{}6Y;ssG{pH~is*gOzA4nDH^6n4kUwf(ZYJQHEg+9Ng{M)jjP%&JLD(
ze}#h-LEM~N+??E;JRnAO7gq;&Pa7{AS8pZ}kh;6K!@r1pIoY|{IYEptIvzGIZZ8A@C)&B@pJKUxxg?o
z>Z+=8YC76tQfi8-QYx$<83jdE4JkDvIXMv9KLwDsjFy~~sx~V~N<)qnq^ho_EN2MP
z@N{r_sqn?AS_bWRakd}>=o2Rvds~r=@A62xZ
zWR%r(Ri)*$#JGrI74mXwT5|s{&BG!6V<;wR89g`v1puB60+RNl%m6Gu0OR`y{vss-
zfDl<8egFU%(a=!A2mtW68VnmKK#=^$K$r|%A}w_aBOR{f763@n{27^l(STsAv9hV;
zi9ZbzUnr*138mU7X2>?t{nHXQAPFM`f3z5YCI5zE>KXt*1pt_szgd#M*+1yNMah2`
z|IP7Nnj}0RDbFQH9v3lK{9lY3MX(D%BS%99@mdr7=^f}_eDMG12`l}Bfdecyfsb4O
zHfa;uAH_H|i1Gi?69fzt!N#8z1NdX&B1w2~dGTLn$r+e|5dd(-zeU0D|I22SgO-W_
zW8zB>Ue*DRWO5h={4H4*)*1jNRBdJ*FEL~#jFs7P7bH)#O)(%Pw{!~cau}}Odm|CV-P-e)QP+?fG2lwBWeHJ0L>HfF%
ze;$8%lL6qH2mW{yxpFGGoCo;`+}~bu|8W5g@`x=){1I47|2?YzB>?!xq<=-wjium^
zc;G9h&|&@~7Y?R8*h&E{TDBNw3E22x7{XpQ_&*-`zXPm6c)%I}Fabiq!Vnh#c_`o(
zG2$9zyf7qWb!P<#!AP+7(cnP52m>G?u(ct$-2Yz^K#f4bGkej0m6GkjyKub!yYT;6
zDVZ1R2OKZfznJa+BZL$HDF0LXpXcaQ5qK#r&i^Gq5&`((@UJ`=8Tj$P>i_5R|1anN
zwZQ+k1z@oVQ4n?;phUf?MFF-Df#<1+W8pSZvp{O)$4gKErx>`*pHu9$McjXc8B`-r
zMC?o9!(|}nAgf0*{WFpO!CUxK@?sS{n?N)uLDr1elOp&}@cbr=Q-Y!y$q3Io9cd?x
zzZv@G5`kfzDO2Rv%rG|;ed!zn2NCA01SA*u&_Dm|3AR`{rpeAvoxNe
zVHlzUvTy(~x%?&gr#-1Vxhg_xxdWhzU{5MJ3zVS1f-r#phxmIDQ={M?t>D0n01&-x
zkg=CdGt$VaiYJ$7Wtc;sLpRpMtIGSzFvm9T8?U;ys(r!Udm7m(JYBrk74Oktb1W?O
zqI{6jw)6o14SCdX049uIokw-nTXkAjH__TKhasTEA*b4*8YW9r9bZn97lt!Twec_5
z&n|hNQ{g1t%sY)QH>+c+kEDDlo6eY1QPaiDI&rC@jc+>bt@=xJnnw8D0LJ)TU^(sW*0svT3VFs#j$gx#QwfUQ2Qp_oL$f$5Au`z@_b1JgwCuUex
zXL(ch9SZhhbM|oflxIcNXR}In{Yx0)C{;ACL{%?UO6=oG7;;LOxW&}xM78l@YoZ^p
z8OWk^bQh6U3Z@?-09!jgPb@JMm7HZA-}3*Vm|EsgPyt7ofDDeV@CT+!&qcw=u(1B`
z5Drt70XDV--#-X{Qk*uh_&d7K0pK$z5}O-^Ix<;vl(;laS^?&KFwc_&quLaGOHKzQ
zVcq+WZ~veEC}-KQ)4LgUQ4`3XV8epCqn2>_Cb+W<0NBwm@oee-~dTv*gFOHQpYGJ-`2z*F_F{}TTxV`KmQ{6LI=
zdHWyu^ZD^7p5+T3u1V--afG0;Q`DAjN}{wGmBoGB~^mKmc&(7_#)m1U1XdzAr;7llbu{m4msx&mo%Hj8#9Y!B7pANs
z8WplO<|=iV9lM_xZJ|2NgU5!`K&EEmiaL^`EZm3}{wvJD)R479HDaRh&YkOmED>5(
zBW}T(BuksmLjHLzCIn`t@|``Jgg+3PW7qB`;=DrEuCM(+mMQV}1JaL$+&p4%X%E!_
z+cd~t-o)-{@k%I&xW3A?lkS_+gJX(aT0B!LT{Z%5_!^0IorCdR>!PGzS&&)k+paXO
zH?-1pim7keZ9uOktthRyL?&H*7q+PKk77oJh6Qx!KEb6sIe1c~}I(2gD$vJZ0}j
zI0m(pUP7@6Udy>`JXEJTlQIXYrYuenFXta>QBAtt&Yv-tULHB*hl&M9;+{Z6T}+{m
z=ee=2Ny0bk2vd+^#4?JcIlnKT%awR2b5=aqr5}jY6iV-hMoM+yGOYP1236u|cZafg
zLn(09P2j>}{SC8D9dj0*iE-m;>xE>liVx3l6si{RWxPwCjYtG9Rl)8dDk2EXD0u08xa8)^vnABcS_yj@S6y?8Eu{Xq0|qRwNr
z7sjH~M}r3EjVvbw(euoZJ)RId1>+Yw_(wcas;!D#v6_j(8TsBTe)!OFzAj%-Z4^(N
zBQWv1S~_RU08KtF$EBK|s;_gce-iiIM&T7(K|9}t(~i>Y*Gw@I4P7x3Z9RKOdjr`!
zX`qTBV67m7GOTI2yXKmCBc!nEYvPO6F4}VTsKBSWMTfiJ0h2#wM7LF<=ZS4|5|?FkIA@E38D|WoJ~iia$%Z*Ld!$
z8Y?DkI84-u4d=?I)ChyrQmQ(<7Rc$vZNHK)89@4^{Z-C
zN9%5;On&~_4y0}`@%2QYn~>8%CK2mH7ADTJ$;ngvP;Xz$_};hp@nlLP=a7xlpr@0(
zwIQwL_Ef5!sIlEoR=4iHzn70T0qaDN6$i7J{DR;JoIVj6twDI;Qp*
zHw5qoGFb>t)W>DYsRr@g_oBEM*E)wca+Y&wJXTLofh$3WfV5{*uMsiS;mUrmhz6OR
zd?S19Dmq_x5HThyK4QyxRZ_Q@l%wxJd+9|JAEN)+B9Bt-+B>j>PF_{Bkt|9wy9`(Q
z1+!))6H;WvtoHqnx@wGN)>)rQ0wb{`F-;YUyg?1|x7D0Z`o{|Cg#_Nywj4h6EHN3i
zMH0h?Rl8@x*OHk@;~%~KAmIU7>r#X#D$3=9k|Jgwl>&G~`Ot;f)0!FVX5L=4@VjQ-
zo{SOVURsGjt{v@aK)7;ZZzo%m1}{B)&pnn_qaWJ8Cw*Hyzc?ZE*utx%rX7LwLiD_h
zf=eNGEDT9nVHj1xHNttl1rQJRDJ+et3^tc8^Rc_}oc#?yb#t~PRDx^P-D;idaW>=5
zvy#!iC1J6?c0q{8f}6l@kL%Xb&j-Zegf})VW&zi@=3x_0J_TA;(|gkoj_;hr>USXh
z5f?Y=d(ipI(C3T*7~Z)uR96_ZISrehWusxCeEkhMZX0%RZRVEn6Fur^nr0+PL&?tm
z$DY?MZqyjx)%&pOFwv8e$ns9IPG@XtjJ^|At}*DmhH?Jd>%FC-J!j74&s-qk<1l6#
znvT<d6CTv#*K2M)
z`4FNeeQ(n##+&whh}GA30jE_A;xg;}Q|y_3vwqV7*Cv^JPt89MXDNhU4s*0`i|D-0
z>$w~X*2%z(-w|le$H+`HC%=?m=D>v9I-90W`(``_9Ssh9Nz4{5E_l`4o-L~?FqXIY
z)NIKIszR1r&Rp21Ik=SL(TAxXLh>dvqL#v^dUbde;BjsV)KZ4RwSo0H%JttS)Y_|5
zVe2!V7?n423(^uk0Ql(UT_K!VMINR|So={pO%KWtYbb^gqN9zE|06G9C}xNy6=X=O
z!>rr;jYK&g8~4uJRV~=oOz&MlcZewE*fZSbC_XOUG*&LsAU+fbZc+Er{^MQJEWG}Fz8%7H&Txc*RrL;icX|v;j+oL
z1fmsCEj7^$f4pa1#gG6;L^Kb}ic;7EN;Wjt}35nC#TN
zRP^S~@;q724km0rxaVAA&DGz{HnkH@Vu=@qzFxY=
zxqW#r=Q6my(+aOM+vo1Uwa!jLyYAhJ@yKP?mbdEKzqP72MXdvvEJ1<(hu@j>W|@Y1~_H>K*-lGoqxQJg{9YY$GmzmFS3eDg+=T
z!vx8=jK@HFQ)jedIP|7~uP<;`2YHBKV!s8=9zr+M1YvG-_D^oy{
z=z;X)m-kI`r}*`U-hhI#&ttJ&_6a&|>eI+{3dj}oc!XQS@9wfBC+$FA%~
zk$Lt6tjaYO1TsZ>MH$9lbA*=)6_~^jGr`Xfg_CA>xY7)Ni0|78;_Ir^*}c;GsG4?NmIc(|^R8NpLxtR)u{m
zt%hk!W1T&ZM04~D(55{$(oMg}H95GJ%*|jH*OhhJeKd6Cxf`?V(HBg*c^7!-dV5u0
z$PzU)HI!D~@$hD;<2kwJ=B~)bcT>ABXa4dc8(f0{d14<1O>+R+0EomxP@LCY*ZFae
z$K>Vk!rsl{oe@+J((^dD;A-o6wwcSnY#6IR6^wH(94SwJfkz|8lb_9GwsDn}w)JiK
zsO{Nz*Cos!*(3}gj;Xf_(uXVO7=^#<0Yd_=z6b?u0z*+qE*v1DTu|`YSd(?O|7Z(~}u9wG?z9=BDdaxED^k
zmS`BrCV790F;Y9-Rhi8W)dU%j7<)jWNb?~powTg{e0_owhtt&B!c>?DnOjsB4l9lkNF^g`*_0C69S^g9Ah?t9BJE;x;~iTi
zi*%`c7EW}3TN4n)QY1r
z-VeVuE98`Nj%3$LMr^^#}WPb`M2DTbRS)(Fu+V^B^>_(BV0y|P!F;S{RY+sOOWbCsuIBp_{;
z8NX=)dHWsJD=4oZ6hHeeV4dsAjz%uPYU6x-cTn{cV6?{k}kUGrkUmKT}K_AHMii
z_sg=X6lDFhX5w3<+oTl6QAD%FTDmHeZB_@?Ku(&gJcqA*k;EfOdl(Lsa(;c4g3Y8}
zZ;DgpR6wTA)S?*@L$o$B-wWv*mC4D-8i!g~jk23yJD^XBJORUo4Vrq}@3KzwEKd~{
zK3))Nree27ll_*C+}nG*6igA6-_>1dTpHk`6)%`9h^8myJfNexF8LhK-y*5V8Oh#6
z^WkZYtN*t#l0GptmNFAjm^Uv8Kj!AQwr?po+ZolY*j^}x;Q=qp$;#76Xfjb{>a?#c
zlMpoDl0GxxR2V64lf!la*;2Ck_hULz($mt!uHwthvH$uy=5lQP-DOCIrrp$x-jQ}~
zY?F7#(zeFP?3%SHYDB4GGu^Eq;3qWwZgVe*wzBtVeVbWFOD*P7$YPDiPLJD0_H)3(
z7rmcN*V04)oSdxAV3MXH8PCd6gO2UHS^l^HA2bnO{yyPBI(LmgLd#k=1q0OS0+Q;S
z`xn%4ZNps*Jpqw=@Yc)^H=DmqZM1|
z?~)tVHq1ZB8g(&KBzh%^*d5KmYf@}@SU-|LhkuV1em@4E=g)8FYUn
z{VKpn`Yvp%KiD&I8LK!>#gK>f3xWj(0&L{%nB+A=ojjCOYDbWOS)r>}KS?_b4zhB_
zS4#HVPbYD(TAq2n@bD^9)~lUvn6kxMoI4YOF(xHA(rusM=p$CE;}?o!
z5nPS0vnyV#7oi9q&4`4j%?6%4Vq#PrW#3F&TbBig54qgr?x9M$kTXqX
zmM6(+Wi`jRLg|7aFP{A4?Q72Af%Tgn>6e)K0ihn$tX$c}4pZ?W7Ad)jJa4`oeB2C)
zd_Dstv7;E+==llRj0EV(r5{F+7KA%hwxI*R);&6}Up{&(CM6RG$LvBnGJVaM=Z1$m
ze{HC-B@9A=)GT|F#aZXV*iBb80#!KBg6SwjTo861Z^g$>`xto>pu7YBVo#-m)CD(eInZ`~c@=6|=g<*ez?ltPDa-
zMOpg-X<^`Jr@pyam6e$<<0XO|&j+**zQ*w~X|*Fz37G@tU49vrhN`bs<~58*6C61Q
zD`Yw#RLYF>|7Q09!MP)T_kpb|9i+_9TyaaA#d%JHTU{wbt)jP4EQB`DN4i_1tilk)
zq=zT4>P*#!LK=qdH{vQ67nsp7s$d&uX)eEOlExJuC{ONq^|2nQ&u&uI}*
z2xo5`YGqVE3tyB{!XGU$WE-yP?n+56L|=u@8HvIHeb0L5wtYqG$qnAlwn<@;Se!hq
zr&yGlH@e9#bkgQVzIQ9wysUT)N7I7w{atyWts_&sJwX|vV*qT+DjvRD+Z?N%_0<7^
zgIh0pMR<+LFQ29lg}9oYj?U~xHwv_T-h+14c=i;`DOw!bR(d)q8t!S~hC4>FI#|^$
zG@fCS#~SZ({EzI(I+sbAqg3OV0i&4139Qjyi7rC5i*;IIe
zgCH;w%vRMLVg~z{uudq(+nm5(I`DN_pA+LwlJKpCmz9DTg2%YNN~n@MyXo4;FP(Nx
zI%3;ZEj(Y9*a)HQfaH!J86ihOtB)lYTBu$kx^Q7aX<#))V44_*mQ`y{4E9$AmT@vJ
z*2bzMIZI3JbPo938ICu2D~MJm@hpy@QrtjE8uKGPRX~;mz=)RNmsMJ
z_xeNh1zWmohT0B9i=6^rr8{WqqqrY5xtr83&9`;#Rhy_9B%rKip4#(Qsqgy!vN3^e
zdZwM+Pfnf~GVeFhONnWe1h(|^0kOU&gim<_b|LIHPHND%8W$st=K(D>+DFwjsrAr}
zr2Si`+TPNsXuOV_%~&n7$y1V!LR$d8MOvUY$*(}J5Gr`JbXJ#??bZ*~R_}ch2^K$^
zGox${l;E;kH!UIRz7Dx@JzYTTu5-J2Q1c*Fc&!R8cps#_5M$xes9xEt+4BqpsNBLv
zr6}4CK&%kirwoWFrbKDIVXkt*zUvc=kqy7pyib86AmEal63=J_C)&B{0U->gKEtye
z{bk{bz6tNx`S5sG1L2#)b|RSE1IznGJ{dk@Q)SPWg(MWg+WkL?N`3dW32b(w(&qhK~^k
zukWmt85=O@I+jthGY%qHrbnd!u~_V!dGdt^O?VJ7cDVn*3AJotVhreHS}`Hq;;!TPSkj3Sg)8-+PNgT=)0k
zJ}mf6QRKa~^j+L{y|!+E~pHx_so)sv{l|?_BRaN8|T9cku8X%8bMo6b4dOoJ0LSMchcNilFmeq8C
z;~7#A-&0PMKd?|tF4ifPY-y2Oz!m1lq}VCvRac6BKFdb~(jL`owh1dow>Zmguc6QUVD9>_3^rJzhcSlmJ(A?HR|MF7kM);+RXI(X@;{14x2vh~R4nLnvFCsEVa3z!h0>}dH1T8gk{Ox}b
zIeHzFvk%Wo(fgDJq{a(BI3-}Iiuzk**%2G6ost-o5RS6d%2G$>vog@2%D(-c9FX3U
zV8`{G7r*G^JCm5=tq!Yz>(R^{it3U&wHj{$u0Ug3h#>0tc#MkiHCprg0w?+~&5D
zAlh96xA#4Uv{=ki-I6{wYvlXb7KaK#On9a}=!*dtEmm5k}+iO!3oqK*Ni@`dr%Y7>A@B1qX
zCVS`SR#Fbc>UPYSazEvK4m`R(atXOi`!RLo6N)!I*}uNmZbulT@qDMSU#`Z}woxz^
zmF?Zag8n*?_gneflejkRxlP^NDM?s8OumN#_{DPAdbwwaif+3&$MB3$)_(wxSS+zq*MW{@I
zNM7E`ItumfCH$UmgRt>zifep^KL`!hl*-`u_o!Q)~@RQ
z$O2(MGEIHKSr3*^>tGL1X-!_Q#|TjgH-!JVpYvkYQX`UTbG+DuH$*NlhoKys5*r+%
zSnH%6pBx)m@tE$zemGcv*X4MyytTN!ydj~Brqr+4gVtP0Y2zp!{+pW7RB7&AeO=&d
zt>A`{{kovQCNtA(ar_ag4y@+(0?^Uq(#&rB9N?0Dw;N_aPVa`#AiQ+)LMC3Piw#Qr(
zX(i%NHTA>l=7r?-k}o&TL;&c@A?{8;U2>go;1d_L{hh1;oh{>}?ZUb}w|gj0QoVHc
zrMN-bSUo;PhZ`6LebtjR=6Zn{uU0`4aT#7yX1@eJZvO>+l#vvhs*<27vyc&=Jp%TC
z**<>}v5cMWCOr789{Y%<1F=VEjO_08%0N~=ZlAZzwJlz)p|_e=yQQX?T55-)>mm4i
zpnJ?D=5)I3cya5O?AD}%bpC?;Vkl+6+%(>Ap_SE1fknWm>hXD3-0zI@h5S|M{KD?*
zOM8Exdz2b;3B8T_$^PEeyAi|sVJ|ad_h6?NIBflnK0szzF2buViWPEJ
zE1gvy46ZawgX5i%YmM4tR^>souwT>geD8Ehb&Bb&TUF(;ksWy#++7Aeible-ULYFt
z3PB2!*X=3z&DQ2~JZ(jjK2S*#aQ4Onq1A9apTr_1aU6z^oTs)IJOaX&wQPyLq%IVE
zDdH6@P}V>@j-yO@Ga3)3c9?BsPI$
z{x;Ew?RsI0;_(SH^wuSR<7tyuY{P~uai`x;mW<+NWO2|ds8eFH9@{k-B-zW=zOdn{
zednhnn*KwX)2~V+T7vmnPdt!6wDktl)>c0^JO6fG9FFl7vPXdp+sBXH8B0_PSWm~c
zK@iT}=)w_?mi%0~pR_N38sT(-!A#t6zd|?ccy^Lq8&M_N&^3u;T^U1bnow8wbOR(%
z`s>e+cuwmeHIJ|2m^6OQ`%>xPOJWlcSb%^Sv?8F0+Y&ZJ3@7ERm1Oy{q@=3#xxNY9
zuladU1#g&0b9$=^18VOQNbF30-l$hf8oX=1$P)=J)cl1f;^f9y)f;r~5k+@#sr8xQ
zIl!qL)}r!UMO5gaO|s#$1qWtgA$rnCm|lC+YYyfbSGDKi(~K7C$BGI&hZ0dwOjFmb
z9D=J&;ll5KgckT=cbK^`RD(0D0@U`hB`^xoieh#zP}bK!j^di8g62>ya0X~J>C%+;
zl6^nmOGU$DwqQ6Sk+E@kUgg?Oy=y+SlEZF3gTTqC_WZN{EF&bHT6*uZD)^>-bVCp>-G
z5wtrb5*;1Rj6HyQP2%M<1lP>@NxHj5-uR6g*~3;@XzOqDtEH9L`jhzOL9Khw`1c4O
zZv$tR;%BL30$a_4e%pL~V5=BG&+Q8A>TWwLU#RH1-k|c6rwT)ae=+g%8rd4S>6d&4
zf3>lOw6{5VcdC4IYa7O*pip?$YwFA^iVau#MRxG^K6L`9+Vs#C*A@*O-3{%@mkW=7
zD*IrY-IjZp)?uXUxW7Vyq}kb%K&DeX9M|>d2!uG%3FS`bR34&?>|(uaJ|x9fKLJ+f={+X}Lt?UJz59iCcluF#vS-%PW4+#R>#
zuI(B%1N@D&paP&i+;DvmANLbCcg%)F>RTiF_#ndyrc0Y8sD~?XDKKH*CdNGGtN*H74)l&z`Bif7ulN>i=^`&&N=NX1$H6_&tJX70y`=zJz$OkmGH
z{AW)RSE>hIz$1#&Q-k)HP4o|;-1CQmAhHewaA-r3ISSDB^7^%p*pyG#f_6pQeRox&
zkVoj^)7HxPVBHSYV%^gU4@G8xLo(1^VnHt({^(Z$!lvMT?-F*RN`&?JQ{3OEq^+nw
z5Be6w9V9vJUrzP(@H>z}Kc19V^gt8Aaw!)`u$5hOU8@W}LVhnY^WK?=^hHpZ%ALQncQX&rJSMndgiakD0ZWZ&6uBOyz%vCy6SvQ$8$HPA%i>DL&4F$NN>k%r!k`9#Nt%E_w{I)
z8f)im5dP#o<&uI^3J2qAM%xjOqQeR!M0p__M$F0|#*Thtnhe1J|LCsDc
z;+WSnfk=L*X3*mo;@3^vA32$RSgAFvuSM~UD|voDxqGbITV7fae)5sv+Re^AyXGKd
zhnRMaQzER1J!T$7ddQoZ1E?F-^ru$BtIZqRA!oO@ruZ9MB0dtu&+Piff^7yk;7CB&
zSVkiFsX~d7CiQ>&)g%U`BVKo2*oPU~
zt&4}IBdId^(BCbkdU%deXQ~Lm!=G2GJq_}3_)}R<_4if=ncYC4
z&&S(?hj7lyZ5+P*hVoGdjg^ZT-31X^Fjxn(f#Dzpy}?1hHY~+hJH`5^pl48zkd5`H
zsnq~!o~o&Ny+vdTSFwIO+GnTLK2wFwe!6{-IX@>_JCo-RKCVOlVKj@Yi2j!+z3^X-
zNkH}8U^8w#_$n|O+OJ~pH}Tf1!(bx3!T=0ura?P6;4|eV^mg~WbOXNgMaY8aC%K;?
zlY)7PjUyR+STzREL^&cqmGB%RC!E7kVZ;@7GaoQ6-nV#18t`u*0RxrJ#d^ebvK9GN
zugUp%GG-E$#w+Zziqi7GjYo!Ly>MT2I8H?sU~ANH=3}ip-a-o8_q5hzuGVBnQ2?Q8
zROOer8yM5Xh7o_97}LDXtDxLt&CzYVAnu0Lwl-N!EG!0~M3}a^zN#YnRvot#qHS*4
zzmsEv>3hHxU3M_+;E~ksuupSX>~nfPZ!=i_tXkbO?fM)BCEIrlL2Si`s
z$MvPt^Q!ii>Bh-+Rmc9(^CbKkH%PNV-cz=SIPX~vvZM#XWDQjMS^{D&a+3d^=dAgu
zRp9H=yr+D9Pkn+n`cHX|-r#}gU#LDHH%Y%=adW*+Nr$N{dBf@VS9_Im9oS*)kI_t>
zrcH^A0Z!vev^(A}s8(UScHvcwPL-G(_SN!djHoE!?A19^m5Yv7QqE6C;;+7Z|yB9l=;ln2g>KK&Ue&){=nFwc^35euJL-hO-HXt4UMmP
zZ|j$9YxyD}{WdGJ*hoRSprAiCcU8kW(}t1UGtIpk+Ots#5Qlx>-c?K+o^;HLC+UUz
zSMd-uj)}q}hknRK!~{~M!hm^P#2$Q_8|v-P&;L
zpagM)i0;ve=8z3+UmTbl7Kg~2hB~#>Gu7E-Y9Y1R^_x%jYaHLn%5RdLZ;29vjlRvu
z$EBT*i^Z?~QqQ!!K%jAzl?@8ZA-D}3L(GoW{`wTOwDRXn>3YV!CjJDYW|?x?j@Uf3
zBB;_i&ORVl+t-KzKR4Lfc|d-=ZK=7t1M|+!MW`3Y1tJPa}+EI%a~z#r<~H
z86T+C(*n+LLd+ARzENA|w4}A8SNZ>V_jXJ=HDn;DP
z#jr5c$#o?WpZRRwtSvkAi_^VY?j?);mCxzI*^IZJ%tQ9&Pgb!$stk3n-Aiw(vn&42
zxe+4a-+c@;Pz2Bqn=tAK+lS(|;qjWK>tErdS4K}mV*QZ7)EHLV$4UKgl@(-M<;|sQ
zji?4-J?cJtSq<}=_va5^$~QAU)cqzcVw2O0C0(TjT#=8##nJgH2m9>)+HPf0j&EAf
z0shhZ#Vd;a@~09ES@sKYi@ia
z(u-c@HNI{b^5tW~WrufxB;&FevmacKJc={*v3q?y&3k8!OGjARIib8KJ9bA_=Fr-b
z9-n}t7Og$2M4t=F0!Z>#?!t^^j?83h>BagTdy%ZQ1F}6eDk?A0AgVZ5XhrkMQTjLp
z`fATclxHgl*Jix2Fqlf+N_nqQO*sAl>NH@K<&IDg?8>quGPM@5ou538&5fPIyU*EI
z&t)f8cj@)%>xuy!_;&rW0Af7v6_LN*Y!)52aUAXqXA&?OL|yTlBdW8$AOt^NkZYuW&9@ryes2_`Geq+)3Lan%
zE$qj^42PXZ(m#BfzlCxu*>)$1sq5gf96EkNJzb+N^uqM5%xL>OIHJWZIl#%u)VfV-
zAoR)8okjgQ53k#83vuaw@|ny+J;j;UG+?{`J@QTXLvY+vc0Gy6`vn8Vg=X8}b)hr=
zO7h-!5aZlehR_o6Ueh)FmI}R_(1&@$H>_e*Q}Qb^iS>3)6lWgher*|8@`snxDN(f|
z##h<#TRCfL>Ti1%%#R+ks$CE=WW~PTO~>A&YNnahH>Y#J$Yv8u1FL&!HU+
z&q9=s-a`uObuT#{C8P%Ptw=+WA5CI9MSrznsbdAT8v3pC7h9iwTbmpwuerZRUuB=k
z;hycb8$AxqcxsiKoD5#*YoHK0yDTWIkSb$`7IEVyw9ILQffu<2AjUh;{r&hohnL=+hNSjpl3)k-Gyt^q#*>O-CW+3Hgg
zWp96k*d|h7_?Q;=3@Z(-_>C=}X3V^N_^>TWNw*^;($BE^4Fa
zpuf8g6(^&MARo>pIfQt{?a_wq2G=nqM!!WI1;x_z{{zLbM@j$y
literal 0
HcmV?d00001
diff --git a/sound/creatures/monkey/monkey_screech_2.ogg b/sound/creatures/monkey/monkey_screech_2.ogg
new file mode 100644
index 0000000000000000000000000000000000000000..ea44bcbcd814b7dd2a59bfc0f3fc8193135335a6
GIT binary patch
literal 15649
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