From deb43b2c063a6067274322181516411f9f3a13ff Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Mon, 18 Dec 2023 15:26:49 -0800
Subject: [PATCH 1/8] afasdfads
---
code/modules/mob/living/carbon/human/knpc.dm | 1636 ++++++++++++++++++
shiptest.dme | 1 +
2 files changed, 1637 insertions(+)
create mode 100644 code/modules/mob/living/carbon/human/knpc.dm
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
new file mode 100644
index 000000000000..5af384342fc1
--- /dev/null
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -0,0 +1,1636 @@
+#define AI_TRAIT_BRAWLER 1 //The "marines". These guys will attempt to stand and fight
+#define AI_TRAIT_SUPPORT 2 //The "medics", civvies, etc. These lads will call for backup
+
+//KNPC flags
+#define KNPC_IS_MARTIAL_ARTIST 1<<0 //Does the KNPC combo you right into the next round?
+#define KNPC_IS_DODGER 1<<1 //Does the KNPC become like the wind in CQC?
+#define KNPC_IS_MERCIFUL 1<<2 //Does the KNPC consider mobs in crit to be valid?
+#define KNPC_IS_AREA_SPECIFIC 1<<3 //Does the KNPC scream out the area when calling for backup?
+#define KNPC_IS_DOOR_BASHER 1<<4 //Does the KNPC kick the door off its hinges if it doesn't have a valid ID?
+#define KNPC_IS_DOOR_HACKER 1<<5 //Does the KNPC hack the door open if it doesn't have a valid ID?
+
+//Knpc pathfinding return values
+#define KNPC_PATHFIND_SUCCESS 0 //Path successfully generated
+#define KNPC_PATHFIND_SKIP 1 //Regenerating of path isn't needed, incapable of moving, etc
+#define KNPC_PATHFIND_TIMEOUT 2 //Pathfinding is currently in timeout due to having failed previously.
+#define KNPC_PATHFIND_FAIL 3 //No path found
+
+//Knpc pathfinding timeout defines
+#define KNPC_TIMEOUT_BASE (3 SECONDS) //The base timeout applied if an knpc's pathfinding fails.
+#define KNPC_TIMEOUT_STACK_CAP 9 //Every consecutive pathfinding fail adds a stacks; Timeout applied uses them as multiplier up to a cap of this value.
+
+//AI behaviour
+#define AI_AGGRESSIVE 1
+#define AI_PASSIVE 2
+#define AI_RETALIATE 3
+#define AI_GUARD 4
+
+DEFINE_BITFIELD(knpc_traits, list(
+ "KNPC_IS_MARTIAL_ARTIST" = KNPC_IS_MARTIAL_ARTIST,
+ "KNPC_IS_DODGER" = KNPC_IS_DODGER,
+ "KNPC_IS_MERCIFUL" = KNPC_IS_MERCIFUL,
+ "KNPC_IS_AREA_SPECIFIC" = KNPC_IS_AREA_SPECIFIC,
+ "KNPC_IS_DOOR_BASHER" = KNPC_IS_DOOR_BASHER,
+ "KNPC_IS_DOOR_HACKER" = KNPC_IS_DOOR_HACKER,
+))
+
+//AI score defines
+#define AI_SCORE_MAXIMUM 1000 //No goal combination should ever exceed this.
+#define AI_SCORE_SUPERCRITICAL 500
+#define AI_SCORE_CRITICAL 100
+#define AI_SCORE_SUPERPRIORITY 75
+#define AI_SCORE_HIGH_PRIORITY 60
+#define AI_SCORE_PRIORITY 50
+#define AI_SCORE_DEFAULT 25
+#define AI_SCORE_LOW_PRIORITY 15
+#define AI_SCORE_VERY_LOW_PRIORITY 5 //Very low priority, acts as a failsafe to ensure that the AI always picks _something_ to do.
+
+#define KNPC_DIFFICULTY_EASY 1 //The "marines". These guys will attempt to stand and fight
+#define KNPC_DIFFICULTY_HARD 2 //The "marines". These guys will attempt to stand and fight
+
+///This proc takes a dir and turns it into a list of its cardinal dirs. Useful for, say, handling things on multiple cardinals, and only cardinals, in case of diagonal positioning.
+/proc/dir_to_cardinal_dirs(input_dir)
+ if(input_dir in list(NORTH, EAST, SOUTH, WEST))
+ return list(input_dir)
+ switch(input_dir)
+ if(NORTHEAST)
+ return list(NORTH, EAST)
+ if(NORTHWEST)
+ return list(NORTH, WEST)
+ if(SOUTHEAST)
+ return list(SOUTH, EAST)
+ if(SOUTHWEST)
+ return list(SOUTH, WEST)
+
+//heap.dm
+//////////////////////
+//datum/heap object
+//////////////////////
+
+/datum/heap
+ var/list/L
+ var/cmp
+
+/datum/heap/New(compare)
+ L = new()
+ cmp = compare
+
+/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)
+
+ L.Add(A)
+ swim(length(L))
+
+//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(!length(L))
+ return 0
+ . = L[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)
+ var/parent = round(index * 0.5)
+
+ while(parent > 0 && (call(cmp)(L[index],L[parent]) > 0))
+ L.Swap(index,parent)
+ index = parent
+ parent = round(index * 0.5)
+
+//Get a node down to its right position in the heap
+/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 = 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/get_greater_child(index)
+ if(index * 2 > length(L))
+ return 0
+
+ if(index * 2 + 1 > length(L))
+ return index * 2
+
+ if(call(cmp)(L[index * 2],L[index * 2 + 1]) < 0)
+ return index * 2 + 1
+ else
+ return index * 2
+
+//Replaces a given node so it verify the heap condition
+/datum/heap/proc/resort(atom/A)
+ var/index = L.Find(A)
+
+ swim(index)
+ sink(index)
+
+/datum/heap/proc/List()
+ . = L.Copy()
+//heap.dm end
+
+//path.dm
+/**
+ * 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".
+ * If no path was found, returns an empty list, which is important for bots like medibots who expect an empty list rather than nothing.
+ *
+ * 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
+ * * skip_first: Whether or not to delete the first item in the path. This would be done because the first item is the starting tile, which can break movement for some creatures.
+ */
+/proc/jps_get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude, skip_first = TRUE)
+ 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)
+ if(!path)
+ path = list()
+ if(length(path) > 0 && skip_first)
+ path.Cut(1,2)
+ 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.jps_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/jps_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, avoid_mobs)
+ src.caller = caller
+ end = get_turf(goal)
+ open = new /datum/heap(GLOBAL_PROC_REF(jps_HeapPathWeightCompare))
+ sources = new()
+ src.id = id
+ src.max_distance = max_distance
+ src.mintargetdist = mintargetdist
+ src.simulated_only = simulated_only
+ src.avoid = avoid
+
+/**
+ * search() is the proc you call to kick off and handle the actual pathfinding, and kills the pathfind datum instance when it's done.
+ *
+ * If a valid path was found, it's returned as a list. If invalid or cross-z-level params are entered, or if there's no valid path found, we
+ * return null, which [/proc/jps_get_path_to] translates to an empty list (notable for simple bots, who need empty lists)
+ */
+/datum/pathfind/proc/search()
+ start = get_turf(caller)
+ if(!start || !end)
+ stack_trace("Invalid A* start or destination")
+ return
+ if(start.z != end.z || start == end ) //no pathfinding between z levels
+ return
+ 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
+
+ //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/jps_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
+//end path.dm
+
+GLOBAL_LIST_EMPTY(knpcs)
+
+/datum/component/knpc
+ var/ai_trait = AI_AGGRESSIVE
+ var/static/list/ai_goals = null
+ var/datum/ai_goal/human/current_goal = null
+ var/view_range = 8 //How good is this mob's "eyes"?
+ var/guess_range = 12 //How far away will we assume they're still there after seeing them?
+ var/list/last_aggressors = list()
+ var/next_backup_call = 0 //Delay for calling for backup to avoid spam.
+ var/list/path = list()
+ var/turf/dest = null
+ var/tries = 0 //How quickly do we give up on following a path? To avoid lag...
+ var/max_tries = 10
+ var/next_action = 0
+ var/next_move = 0
+ var/obj/effect/landmark/patrol_node/last_node = null //What was the last patrol node we visited?
+ var/stealing_id = FALSE
+ var/next_internals_attempt = 0
+ var/static/list/climbable = typecacheof(list(/obj/structure/table, /obj/structure/railing)) // climbable structures
+ var/pathfind_timeout = 0 //If pathfinding fails, it is püt in timeout for a while to avoid spamming the server with pathfinding calls.
+ var/timeout_stacks = 0 //Consecutive pathfind fails add additional delay stacks to further counteract the effects of knpcs in unreachable locations.
+
+/mob/living/carbon/human/ai_boarder
+ faction = list("neutral")
+ var/move_delay = 4 //How quickly do the boys travel?
+ var/action_delay = 6 //How long we delay between actions
+ var/knpc_traits = KNPC_IS_DODGER | KNPC_IS_MERCIFUL | KNPC_IS_AREA_SPECIFIC
+ var/difficulty = KNPC_DIFFICULTY_HARD //Whether to ignore overmap difficulty or not
+ var/list/outfit = list (
+ /datum/outfit/job/assistant
+ )
+ var/list/taunts = list("Engaging the enemy!")
+ var/list/call_lines = list("Enemy spotted!")
+ var/list/response_lines = list("On my way!")
+
+/mob/living/carbon/human/ai_boarder/Initialize(mapload)
+ . = ..()
+ randomize_human(src)
+ var/outfit_path = pick(outfit)
+ var/datum/outfit/O = new outfit_path
+ O.equip(src)
+ AddComponent(/datum/component/knpc)
+
+/mob/living/carbon/human/ai_boarder/hermit
+ faction = list("hermit")
+ taunts = list("...")
+ call_lines = list("...!!!")
+ response_lines = list("!")
+
+ outfit = list (/datum/outfit/whitesands)
+
+ var/survivor_type
+
+/mob/living/carbon/human/ai_boarder/hermit/survivor
+ survivor_type = "survivor"
+
+/mob/living/carbon/human/ai_boarder/hermit/hunter
+ survivor_type = "hunter"
+
+/mob/living/carbon/human/ai_boarder/hermit/gunslinger
+ survivor_type = "gunslinger"
+
+/mob/living/carbon/human/ai_boarder/hermit/Initialize(mapload)
+ . = ..()
+ if(!survivor_type)
+ survivor_type = pick("survivor","hunter","gunslinger")
+ var/mob_species = pickweight(list(
+ /datum/species/human = 50,
+ /datum/species/lizard = 25,
+ /datum/species/ethereal = 10,
+ /datum/species/moth = 10,
+ /datum/species/spider = 3,
+ /datum/species/fly = 2
+ )
+ )
+ INVOKE_ASYNC(src, PROC_REF(set_species), mob_species)
+ var/picked
+ //to-do: learn how to make mobsprites for other survivors
+ //w_uniforms are random to show varied backgrounds, but similar goal
+ if(survivor_type == "survivor")
+ picked = pickweight(list(
+ /obj/item/clothing/under/color/random = 65,
+ /obj/item/clothing/under/rank/cargo/miner/lavaland = 10,
+ /obj/item/clothing/under/rank/prisoner = 10,
+ /obj/item/clothing/under/rank/cargo/miner/lavaland/old = 5,
+ /obj/item/clothing/under/color/khaki/buster = 5,
+ /obj/item/clothing/under/rank/cargo/miner = 5
+ )
+ )
+ else if (survivor_type == "hunter")
+ picked = pickweight(list(
+ /obj/item/clothing/under/color/random = 50,
+ /obj/item/clothing/under/rank/cargo/miner/lavaland = 25,
+ /obj/item/clothing/under/rank/cargo/miner/lavaland/old = 15,
+ /obj/item/clothing/under/rank/security/officer/camo = 5,
+ /obj/item/clothing/under/utility = 5
+ )
+ )
+ else if (survivor_type == "gunslinger")
+ picked = pickweight(list(
+ /obj/item/clothing/under/rank/cargo/miner/lavaland = 35,
+ /obj/item/clothing/under/color/random = 25,
+ /obj/item/clothing/under/rank/cargo/miner/lavaland/old = 15,
+ /obj/item/clothing/under/rank/security/officer/camo = 10,
+ /obj/item/clothing/under/syndicate/camo = 10,
+ /obj/item/clothing/under/syndicate/combat = 5
+ )
+ )
+ else
+ picked = /obj/item/clothing/under/color/random
+
+ w_uniform = new picked(src)
+
+ //storage is semi-randomized, giving some variety
+ if(survivor_type == "survivor")
+ picked = pickweight(list(
+ /obj/item/storage/belt/fannypack = 40,
+ /obj/item/storage/belt/mining = 20,
+ /obj/item/storage/belt/mining/alt = 15,
+ /obj/item/storage/belt/utility = 10,
+ /obj/item/storage/belt/bandolier = 9,
+ /obj/item/storage/belt/utility/full = 5,
+ /obj/item/storage/belt/chameleon= 1,
+ )
+ )
+ else if(survivor_type == "hunter")
+ picked = pickweight(list(
+ /obj/item/storage/belt/mining = 30,
+ /obj/item/storage/belt/fannypack = 20,
+ /obj/item/storage/belt/mining/alt = 15,
+ /obj/item/storage/belt/mining/primitive = 15,
+ /obj/item/storage/belt/bandolier = 10,
+ /obj/item/storage/belt/military = 7,
+ /obj/item/storage/belt/mining/vendor = 3,
+ )
+ )
+ else if(survivor_type == "gunslinger")
+ picked = pickweight(list(
+ /obj/item/storage/belt/mining = 30,
+ /obj/item/storage/belt/bandolier = 30,
+ /obj/item/storage/belt/military = 20,
+ /obj/item/storage/belt/fannypack = 15,
+ /obj/item/storage/belt/mining/alt = 5,
+ /obj/item/storage/belt/mining/primitive = 5
+ )
+ )
+ else
+ picked = /obj/item/storage/belt/fannypack
+
+ belt = new picked(src)
+
+ //everyone wears the same suit
+ wear_suit = new /obj/item/clothing/suit/hooded/survivor(src)
+
+ if (survivor_type == "gunslinger")
+ if(prob(30))
+ picked = /obj/item/clothing/shoes/combat //but sometimes there are nicer shoes
+ else
+ picked = /obj/item/clothing/shoes/workboots/mining
+ else
+ picked = /obj/item/clothing/shoes/workboots/mining
+
+ shoes = new picked(src)
+
+
+
+
+ //gloves are a tossup
+ picked = pickweight(list(
+ /obj/item/clothing/gloves/color/black = 60,
+ /obj/item/clothing/gloves/explorer = 30,
+ /obj/item/clothing/gloves/explorer/old = 10
+ )
+ )
+
+ gloves = new picked(src)
+
+ //bags are semi-random.
+ picked = pickweight(list(
+ /obj/item/storage/backpack = 20,
+ /obj/item/storage/backpack/explorer = 20,
+ /obj/item/storage/backpack/satchel = 20,
+ /obj/item/storage/backpack/satchel/explorer = 20,
+ /obj/item/storage/backpack/messenger = 20
+ )
+ )
+
+ back = new picked(src)
+
+ //as are bag contents
+
+ var/list/backpack_contents = list()
+ if(prob(70))
+ backpack_contents += pickweight(list( //these could stand to be expanded, right now they're just mildly modified miner ones, and I don't know how to plus that up.
+ /obj/item/soap = 10,
+ /obj/item/stack/marker_beacon/ten = 15,
+ /obj/item/mining_scanner = 5,
+ /obj/item/extinguisher/mini = 10,
+ /obj/item/kitchen/knife/combat = 5,
+ /obj/item/flashlight/seclite = 10,
+ /obj/item/stack/sheet/sinew = 10,
+ /obj/item/stack/sheet/bone = 5,
+ /obj/item/stack/sheet/animalhide/goliath_hide = 10,
+ /obj/item/stack/sheet/bone = 8,
+ /obj/item/reagent_containers/food/drinks/waterbottle = 10,
+ /obj/item/reagent_containers/food/drinks/waterbottle/empty = 2,
+ )
+ )
+ if(prob(70))
+ backpack_contents += pickweight(list(
+ /obj/item/stack/sheet/animalhide/goliath_hide = 20,
+ /obj/item/stack/marker_beacon/ten = 10,
+ /obj/item/mining_scanner = 20,
+ /obj/item/extinguisher/mini = 10,
+ /obj/item/kitchen/knife/combat/survival = 10,
+ /obj/item/flashlight/seclite = 10,
+ /obj/item/stack/sheet/sinew = 10,
+ /obj/item/stack/sheet/bone = 10
+ )
+ )
+ if(prob(70))
+ backpack_contents += pickweight(list(
+ /obj/item/stack/sheet/animalhide/goliath_hide = 5,
+ /obj/item/stack/marker_beacon/ten = 5,
+ /obj/item/mining_scanner = 5,
+ /obj/item/extinguisher/mini = 10,
+ /obj/item/kitchen/knife/combat/survival = 12,
+ /obj/item/flashlight/seclite = 10,
+ /obj/item/stack/sheet/sinew = 5,
+ /obj/item/stack/sheet/bone = 5,
+ /obj/item/kitchen/knife/combat = 3,
+ /obj/item/reagent_containers/food/snacks/rationpack = 30
+ )
+ )
+ if (prob(25)) //mayhaps a medkit
+ backpack_contents += pickweight(list(
+ /obj/item/storage/firstaid/regular = 50,
+ /obj/item/storage/firstaid/brute = 15,
+ /obj/item/storage/firstaid/medical = 15,
+ /obj/item/storage/firstaid/fire = 10,
+ /obj/item/storage/firstaid/advanced = 5,
+ /obj/item/storage/firstaid/ancient = 5
+ )
+ )
+ if(prob(30)) //some pens maybe?
+ backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
+
+ var/obj/item/storage/backpack/current_bag = back
+ if(current_bag)
+ for(var/obj/thing as anything in backpack_contents)
+ new thing(current_bag)
+
+ //pockets
+ if(survivor_type == "survivor") //could also use fleshing out
+ if(prob(30))
+ l_store = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
+ else
+ l_store = /obj/item/tank/internals/emergency_oxygen/engi
+ if (prob(20))
+ r_store = /obj/item/spacecash/bundle/mediumrand
+ else
+ r_store = null
+
+ if(survivor_type == "hunter")
+ l_store = /obj/item/tank/internals/emergency_oxygen/engi
+ if (prob(20))
+ r_store = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
+ else if (prob(60))
+ r_store = /obj/item/ammo_box/aac_300blk_stripper
+ else
+ r_store = null
+
+ if(survivor_type == "gunslinger")
+ if(prob(50))
+ l_store = /obj/item/ammo_box/magazine/aks74u
+ r_store = /obj/item/tank/internals/emergency_oxygen/engi
+
+ else
+ r_store = /obj/item/tank/internals/emergency_oxygen/engi
+ l_store = /obj/item/radio
+
+ //masks
+ picked = pickweight(list(
+ /obj/item/clothing/mask/gas = 40,
+ /obj/item/clothing/mask/gas/explorer = 20,
+ /obj/item/clothing/mask/gas/explorer/old = 20,
+ /obj/item/clothing/mask/gas/syndicate = 20,
+ )
+ )
+
+ picked = new picked(src)
+
+ //the eyes are the window into the soul. I don't think these things have souls but whatever.
+ if(prob(70))
+ picked = pickweight(list(
+ /obj/item/clothing/glasses/heat = 20,
+ /obj/item/clothing/glasses/cold = 20,
+ /obj/item/clothing/glasses/meson = 40,
+ /obj/item/clothing/glasses = 20
+ )
+ )
+ glasses = new picked(src)
+ else
+ picked = null
+
+ //and of course, ears.
+ if(prob(1)) //oh my god they can't hear the sandstorm coming they've got airpods in
+ picked = /obj/item/instrument/piano_synth/headphones/spacepods
+ else
+ picked = pickweight(list(
+ /obj/item/radio/headset = 50,
+ /obj/item/radio/headset/alt = 50
+ )
+ )
+ ears = new picked(src)
+
+ //exosuit bits
+ s_store = null
+ var/spare_ammo_count = rand(1,4)
+
+ if (survivor_type == "hunter")
+ INVOKE_ASYNC(src, PROC_REF(put_in_hands), new /obj/item/gun/ballistic/rifle/boltaction/polymer(src))
+ spare_ammo_count = rand(2,7)
+ for(var/i in 1 to spare_ammo_count)
+ new /obj/item/ammo_box/magazine/internal/boltaction/polymer(back)
+
+ if(survivor_type == "gunslinger")
+ INVOKE_ASYNC(src, PROC_REF(put_in_hands), new /obj/item/gun/ballistic/automatic/smg/aks74u(src))
+ for(var/i in 1 to spare_ammo_count)
+ new /obj/item/ammo_box/magazine/aks74u(back)
+
+/datum/component/knpc/Initialize()
+ if(!iscarbon(parent))
+ return COMPONENT_INCOMPATIBLE
+ if(!ai_goals)
+ for(var/gtype in subtypesof(/datum/ai_goal/human))
+ LAZYADD(ai_goals, new gtype)
+ START_PROCESSING(SSfastprocess, src)
+ //They're alive!
+ GLOB.knpcs.Add(src)
+ RegisterSignal(parent, COMSIG_LIVING_REVIVE, PROC_REF(restart))
+ RegisterSignal(parent, COMSIG_ATOM_BULLET_ACT, PROC_REF(register_bullet))
+
+//Swiper! no swiping
+/datum/component/knpc/proc/steal_id(obj/item/card/id/their_id)
+ //Time to teach them about IDs :)))
+ stealing_id = FALSE
+ var/mob/living/carbon/human/H = parent
+ if(get_dist(H, their_id.loc) > 1)
+ return FALSE
+ var/obj/item/card/id/ID = H.get_idcard()
+ if(ID)
+ if(istype(ID, /obj/item/card/id/))
+ their_id.forceMove(get_turf(H))
+ H.visible_message("[H] snatches [their_id]. ")
+ ID.access |= their_id.access
+ else
+ H.put_in_inactive_hand(their_id)
+ if(H.equip_to_appropriate_slot(their_id))
+ H.update_inv_hands()
+ return TRUE
+
+/datum/component/knpc/Destroy(force, silent)
+ GLOB.knpcs -= src
+ return ..()
+
+/datum/component/knpc/proc/pathfind_to(atom/target, turf/avoid)
+ if(pathfind_timeout > 0)
+ return KNPC_PATHFIND_TIMEOUT
+ var/mob/living/carbon/human/ai_boarder/H = parent
+ if(target == null)
+ path = list()
+ dest = null
+ return KNPC_PATHFIND_SKIP
+ if((dest && dest == get_turf(target) && length(path)) || H.incapacitated())
+ return KNPC_PATHFIND_SKIP //No need to recalculate this path.
+ path = list()
+ dest = null
+ var/obj/item/card/id/access_card = H.wear_id
+ if(target)
+ dest = get_turf(target)
+ path = jps_get_path_to(H, dest, 120, 0, access_card, !(H.wear_suit?.clothing_flags & STOPSPRESSUREDAMAGE && H.head?.clothing_flags & STOPSPRESSUREDAMAGE), avoid)
+ //There's no valid path, try run against the wall.
+ if(!length(path) && !H.incapacitated())
+ pathfind_timeout += KNPC_TIMEOUT_BASE * (1 + timeout_stacks)
+ timeout_stacks = min(timeout_stacks+1, KNPC_TIMEOUT_STACK_CAP)
+ return KNPC_PATHFIND_FAIL
+ timeout_stacks = 0
+ return KNPC_PATHFIND_SUCCESS
+
+/datum/component/knpc/proc/next_path_step()
+ if(world.time < next_move)
+ return FALSE
+ var/mob/living/carbon/human/ai_boarder/H = parent
+ next_move = world.time + H.move_delay
+ if(H.incapacitated() || H.stat == DEAD)
+ return FALSE
+ if(pathfind_timeout > 0) //Pathfinding in timeout, move around aimlessly
+ H.set_resting(FALSE, FALSE)
+ H.Move(get_step(H,pick(GLOB.cardinals)))
+ return FALSE
+ if(!path)
+ return FALSE
+ if(tries > 5)
+ //Add a bit of randomness to their movement to reduce "traffic jams"
+ H.Move(get_step(H,pick(GLOB.cardinals)))
+ if(prob(10))
+ H.toggle_resting()
+ return FALSE
+
+ if(tries >= max_tries)
+ tries = 0
+ if(length(last_node?.next_nodes)) //Skip this one.
+ pathfind_to(pick(last_node.next_nodes))
+ else
+ pathfind_to(null)
+ last_node = null //Reset pathfinding fully.
+ return FALSE
+ if(length(path) > 1)
+ var/turf/next_turf = get_step_towards(H, path[1])
+ var/turf/this_turf = get_turf(H)
+ //Walk when you see a wet floor
+ if(next_turf.GetComponent(/datum/component/wet_floor))
+ H.m_intent = MOVE_INTENT_WALK
+ else
+ H.m_intent = MOVE_INTENT_RUN
+
+ for(var/obj/machinery/door/firedoor/blocking_firelock in next_turf)
+ if((blocking_firelock.flags_1 & ON_BORDER_1) && !(blocking_firelock.dir in dir_to_cardinal_dirs(get_dir(next_turf, this_turf))))
+ continue
+ if(!blocking_firelock.density || blocking_firelock.operating)
+ continue
+ if(blocking_firelock.welded)
+ break //If at least one firedoor in our way is welded shut, welp!
+ blocking_firelock.open() //Open one firelock per tile per try.
+ break
+ for(var/obj/machinery/door/firedoor/blocking_firelock in this_turf)
+ if(!((blocking_firelock.flags_1 & ON_BORDER_1) && (blocking_firelock.dir in dir_to_cardinal_dirs(get_dir(this_turf, next_turf))))) //Here, only firelocks on the border matter since fulltile firelocks let you exit.
+ continue
+ if(!blocking_firelock.density || blocking_firelock.operating)
+ continue
+ if(blocking_firelock.welded)
+ break //If at least one firedoor in our way is welded shut, welp!
+ blocking_firelock.open() //Open one firelock per tile per try.
+ break
+ for(var/obj/structure/possible_barrier in next_turf) //If we're stuck
+ if(!climbable.Find(possible_barrier.type))
+ continue
+ H.forceMove(next_turf)
+ H.visible_message("[H] climbs onto [possible_barrier]!")
+ H.Stun(2 SECONDS) //Table.
+ if(get_turf(H) == path[1])
+ increment_path()
+ return TRUE
+ step_towards(H, path[1])
+ if(get_turf(H) == path[1]) //Successful move
+ increment_path()
+ tries = 0
+ if(H.resting)
+ //Gotta bypass the do-after here...
+ H.set_resting(FALSE, FALSE)
+ else
+ tries++
+ return FALSE
+ else if(length(path) == 1)
+ step_to(H, dest)
+ pathfind_to(null)
+ return TRUE
+
+/datum/component/knpc/proc/increment_path()
+ path.Cut(1, 2)
+
+///Allows the AI humans to kite around
+/datum/component/knpc/proc/kite(atom/movable/target)
+ if(world.time < next_move)
+ return
+ var/mob/living/carbon/human/ai_boarder/H = parent
+ next_move = world.time + (H.move_delay * 2)
+ if(!target || !isturf(target.loc) || !isturf(H.loc) || H.stat == DEAD)
+ return
+ var/target_dir = get_dir(H,target)
+ var/static/list/cardinal_sidestep_directions = list(-90,-45,0,45,90)
+ var/static/list/diagonal_sidestep_directions = list(-45,0,45)
+ var/chosen_dir = 0
+ if (target_dir & (target_dir - 1))
+ chosen_dir = pick(diagonal_sidestep_directions)
+ else
+ chosen_dir = pick(cardinal_sidestep_directions)
+ if(chosen_dir)
+ chosen_dir = turn(target_dir,chosen_dir)
+ H.Move(get_step(H,chosen_dir))
+ H.face_atom(target) //Looks better if they keep looking at you when dodging
+
+///Allows the AI actor to be revived by a medic, and get straight back into the fight!
+/datum/component/knpc/proc/restart()
+ SIGNAL_HANDLER
+ START_PROCESSING(SSfastprocess, src)
+
+///Pick a goal from the available goals!
+/datum/component/knpc/proc/pick_goal()
+ var/best_score = -1000
+ var/datum/ai_goal/chosen = null
+ for(var/datum/ai_goal/human/H in ai_goals)
+ var/this_score = H.check_score(src)
+ if(this_score > best_score)
+ chosen = H
+ best_score = this_score
+ if(chosen)
+ chosen.assume(src)
+
+///Add someone to our threat list when they shoot us. Shamelessly lifted from monkey AI code.
+/datum/component/knpc/proc/register_bullet(datum/source, obj/projectile/Proj)
+ SIGNAL_HANDLER
+ if(istype(Proj , /obj/projectile/beam)||istype(Proj, /obj/projectile/bullet))
+ if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE))
+ if(!Proj.nodamage && isliving(Proj.firer))
+ last_aggressors += Proj.firer
+
+///Handles actioning on the goal every tick.
+/datum/component/knpc/process()
+ var/mob/living/carbon/human/ai_boarder/H = parent
+ if(H.stat == DEAD)
+ return PROCESS_KILL
+ if(!H.can_resist())
+ if(H.incapacitated()) //In crit or something....
+ return
+ if(world.time >= next_action)
+ next_action = world.time + H.action_delay
+ pick_goal()
+ current_goal?.action(src)
+ if(length(path))
+ next_path_step()
+ else //They should always be pathing somewhere...
+ dest = null
+ tries = 0
+ path = list()
+ last_node = null
+ current_goal?.get_next_patrol_node(src)
+ pathfind_timeout = max(0, pathfind_timeout - 1)
+
+/datum/ai_goal
+ var/name = "Placeholder goal" //Please keep these human readable for debugging!
+ var/score = 0
+ var/required_ai_flags = NONE //Set this if you want this task to only be achievable by certain types of ship. This is a bitfield.
+
+//Method to get the score of a certain action. This can change the "base" score if the score of a specific action goes up, to encourage skynet to go for that one instead.
+//@param OM - If you want this score to be affected by the stats of an overmap.
+/datum/ai_goal/proc/check_score()
+ return FALSE
+
+//Delete the AI's last orders, tell the AI ship what to do.
+/datum/ai_goal/proc/assume()
+ return FALSE
+
+/datum/ai_goal/proc/action()
+ return FALSE
+
+/datum/ai_goal/human
+ name = "Placeholder goal" //Please keep these human readable for debugging!
+ score = 0
+ required_ai_flags = null //Set this if you want this task to only be achievable by certain types of ship.
+
+/**
+Method to get the score of a certain action. This can change the "base" score if the score
+of a specific action goes up, to encourage skynet to go for that one instead.
+
+@param OM - If you want this score to be affected by the stats of an overmap.
+*/
+/datum/ai_goal/check_score(datum/component/knpc/HA)
+ if(!istype(HA)) // why is this here >:(
+ return ..()
+ if(required_ai_flags && !(HA.ai_trait & required_ai_flags))
+ return 0
+ var/mob/M = HA.parent
+ if(M.client) //AI disabled...
+ return 0
+ return score //Children sometimes NEED this true value to run their own checks. We also cancel here if the mob has been overtaken by someone.
+
+///Delete the AI's last orders, tell the AI ship what to do.
+/datum/ai_goal/human/assume(datum/component/knpc/HA)
+ //message_admins("Goal [src] chosen!")
+ HA.current_goal = src
+
+///Method that gets all the potential aggressors for this target.
+/datum/ai_goal/proc/get_aggressors(datum/component/knpc/HA)
+ . = list()
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ var/list/detected_objects = view(HA.view_range, HA.parent)
+ var/list/guessed_objects = view(HA.guess_range, HA.parent)
+ for(var/mob/living/M in guessed_objects)
+ //Invis is a no go. Non-human, -cyborg or -hostile mobs are ignored.
+ if(M.invisibility >= INVISIBILITY_ABSTRACT || M.alpha <= 0 || (!ishuman(M) && !iscyborg(M) && !ishostile(M)))
+ continue
+ // Dead mobs are ignored.
+ if(H.knpc_traits & KNPC_IS_MERCIFUL && M.stat >= UNCONSCIOUS)
+ continue
+ else if(M.stat == DEAD)
+ continue
+ if(H.faction_check_mob(M))
+ continue
+ if(!(M in detected_objects) && !(M in HA.last_aggressors))
+ continue
+ . += M
+ //Check for nearby mechas....
+ if(length(GLOB.mechas_list))
+ for(var/obj/mecha/OM as() in GLOB.mechas_list)
+ if(OM.z != H.z)
+ continue
+ if(get_dist(H, OM) > HA.view_range || !can_see(H, OM, HA.view_range))
+ continue
+ if(OM.occupant && !H.faction_check_mob(OM.occupant))
+ . += OM.occupant
+
+///What happens when this action is selected? You'll override this and check_score mostly.
+/datum/ai_goal/human/action(datum/component/knpc/HA)
+ var/mob/living/carbon/human/H = HA.parent
+ if(H.incapacitated() || H.client) //An admin overtook this mob or something, ignore.
+ return FALSE
+
+/datum/ai_goal/human/proc/can_action(datum/component/knpc/HA)
+ var/mob/living/carbon/human/H = HA.parent
+ return (!H.incapacitated() && !H.client) //An admin overtook this mob or something, ignore.
+
+/**
+Goal #1, get a weapon!
+If we don't have a weapon, we really ought to grab one...
+This is to account for sec Ju-Jitsuing boarding commandos.
+*/
+/datum/ai_goal/human/acquire_weapon
+ name = "Acquire Weapon" //Please keep these human readable for debugging!
+ score = AI_SCORE_PRIORITY //Fighting takes priority
+ required_ai_flags = null //Set this if you want this task to only be achievable by certain types of ship.
+
+/datum/ai_goal/human/acquire_weapon/check_score(datum/component/knpc/HA)
+ if(!..())
+ return 0
+ var/mob/living/carbon/human/H = HA.parent
+ var/obj/item/gun/G = H.get_active_held_item()
+ //We already have a gun
+ if(G && istype(G))
+ return 0
+ var/obj/item/gun/G_New = locate(/obj/item/gun) in oview(HA.view_range, H)
+ if(G_New && gun_suitable(H, G_New))
+ return AI_SCORE_CRITICAL //There is a gun really obviously in the open....
+ return score
+
+/datum/ai_goal/human/proc/CheckFriendlyFire(mob/living/us, mob/living/them)
+ for(var/turf/T as() in getline(us,them)) // Not 100% reliable but this is faster than simulating actual trajectory
+ for(var/mob/living/L in T)
+ if(L == us || L == them)
+ continue
+ if(us.faction_check_mob(L))
+ return TRUE
+
+/datum/ai_goal/human/acquire_weapon/action(datum/component/knpc/HA)
+ if(!can_action(HA))
+ return
+ var/mob/living/carbon/human/H = HA.parent
+ var/obj/item/storage/S = H.back
+ var/obj/item/gun/target_item = null
+ //Okay first off, is the gun already on our person?
+ if(S)
+ var/list/expanded_contents = S.contents + H.contents
+ target_item = locate(/obj/item/gun) in expanded_contents
+
+ if(target_item)
+ H.visible_message("[H] rummages around in their backpack...")
+ target_item.forceMove(get_turf(H)) //Put it on the floor so they can grab it
+ if(H.put_in_hands(target_item))
+ return TRUE //We're done!
+ //Now we run the more expensive check to find a gun laying on the ground.
+ var/best_distance = world.maxx
+ for(var/obj/O in oview(HA.view_range, H))
+ var/dist = get_dist(O, H)
+ if(istype(O, /obj/structure/closet) && dist <= best_distance)
+ var/obj/structure/closet/C = O
+ var/obj/item/gun/G = locate(/obj/item/gun) in C.contents
+ if(G && C.allowed(H) && gun_suitable(H, G))
+ target_item = G
+ best_distance = dist
+ if(istype(O, /obj/item/gun) && dist <= best_distance)
+ var/obj/item/gun/G = O
+ if(G && gun_suitable(H, G))
+ target_item = O
+ best_distance = dist
+ if(target_item)
+ var/dist = get_dist(H, target_item)
+ if(dist > 1)
+ HA.pathfind_to(target_item)
+ else
+ if(istype(target_item.loc, /obj/structure/closet))
+ var/obj/structure/closet/C = target_item.loc
+ if(C.open(H))
+ H.visible_message("[H] pops open [C]...")
+ if(istype(target_item, /obj/item/gun/ballistic))
+ var/obj/item/gun/ballistic/B = target_item
+ var/obj/item/ammo_box/magazine/M = locate(B.mag_type) in oview(3, target_item)
+ if(M && S) //If they have a backpack, put the ammo in the backpack.
+ H.put_in_hands(M)
+ M.forceMove(S)
+
+ if(H.put_in_hands(target_item))
+ H.visible_message("[H] grabs [target_item]!")
+
+///Checks that G exists, has ammo and that H can fire it. Returns G if yes, FALSE otherwise.
+/datum/ai_goal/human/acquire_weapon/proc/gun_suitable(mob/living/carbon/human/H, obj/item/gun/G)
+ G.safety = FALSE
+ return G.can_shoot() && G.can_trigger_gun(H)
+
+
+/datum/ai_goal/human/engage_targets
+ name = "Engage targets"
+ score = AI_SCORE_SUPERPRIORITY //If we find a target, we want to engage!
+ required_ai_flags = null
+
+/datum/ai_goal/human/engage_targets/check_score(datum/component/knpc/HA)
+ if(!..())
+ return 0
+ var/list/enemies = get_aggressors(HA)
+ HA.last_aggressors = enemies
+ //We have people to fight
+ if(length(enemies) >= 1)
+ return score
+ return 0 //You can still fight with your bare hands...
+
+/datum/ai_goal/human/proc/reload(datum/component/knpc/HA, obj/item/gun)
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ if(istype(gun, /obj/item/gun/energy))
+ var/obj/item/gun/energy/E = gun
+ if(E.selfcharge) //Okay good, it self charges we can just wait.
+ return TRUE
+ else //Discard it, we're not gonna teach them to use rechargers yet.
+ E.forceMove(get_turf(H))
+ return FALSE
+ if(istype(gun, /obj/item/gun/ballistic))
+ var/obj/item/gun/ballistic/B = gun
+ if(istype(B.mag_type, /obj/item/ammo_box/magazine/internal))
+ //Not dealing with this. They'll just ditch the revolver when they're done with it.
+ B.forceMove(get_turf(H))
+ return FALSE
+ ///message_admins("Issa gun")
+ var/obj/item/storage/S = H.back
+ //Okay first off, is the gun already on our person?
+ var/list/expanded_contents = H.contents
+ if(S)
+ expanded_contents = S.contents + H.contents
+ var/obj/item/ammo_box/magazine/target_mag = locate(B.mag_type) in expanded_contents
+ //message_admins("Found [target_mag]")
+ if(target_mag)
+ //Dump that old mag
+ H.put_in_inactive_hand(target_mag)
+ B?.magazine?.forceMove(get_turf(H))
+ B.attackby(target_mag, H)
+ B.rack(H) //Rack the bolt.
+ if(istype(B, /obj/item/gun/ballistic/rifle))
+ B.rack(H) //Rack the bolt.
+// addtimer(CALLBACK(B, PROC_REF(rack), H), 0.5 SECONDS, TIMER_UNIQUE)
+ else
+ if(!S)
+ gun.forceMove(get_turf(H))
+ return FALSE
+ gun.forceMove(S)
+
+
+/datum/ai_goal/human/engage_targets/action(datum/component/knpc/HA)
+ if(!can_action(HA))
+ return
+ HA.last_node = null //Reset their pathfinding
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ var/list/enemies = get_aggressors(HA)
+ var/obj/item/A = H.get_active_held_item()
+ var/closest_dist = 1000
+ var/mob/living/target = null
+ for(var/mob/living/L as() in enemies)
+ var/dist = get_dist(L, H)
+ if(dist < closest_dist)
+ closest_dist = dist
+ target = L
+ if(!target)
+ return
+ var/dist = get_dist(H, target)
+ //We're holding a gun. See if we can shoot it....
+ HA.pathfind_to(target) //Walk up close and YEET SLAM
+ var/obj/item/gun/G = null
+ if(istype(A, /obj/item/gun))
+ G = A
+ var/obj/item/gun/ballistic/B = null
+ if(istype(A, /obj/item/gun/ballistic))
+ B = A
+ H.a_intent = (prob(65)) ? INTENT_HARM : INTENT_DISARM
+ if(G && dist > 0)
+ if(!G.can_shoot())
+ //We need to reload first....
+ reload(HA, G)
+ //Fire! If they're in a ship, we don't want to scrap them directly.
+ if(!CheckFriendlyFire(H, target))
+ //Okay, we have a line of sight, shoot!
+ if(B && !(B.semi_auto) && !G.chambered)
+ //Pump the shotty
+ G.attack_self(H)
+ //Let's help them use E-Guns....
+ if(istype(G, /obj/item/gun/energy/e_gun))
+ var/obj/item/gun/energy/e_gun/E = G
+ if(prob(20))
+ E.select_fire(H)
+
+ if(isobj(target.loc))
+ G.afterattack(target.loc, H)
+ else
+ G.afterattack(target, H)
+ if(istype(B, /obj/item/gun/ballistic/rifle))
+ B.rack(H) //Rack the bolt.
+ B.rack(H)
+// addtimer(CALLBACK(G, PROC_REF(rack), H), 0.5 SECONDS, TIMER_UNIQUE)
+ //Call your friends to help :))
+ if(world.time >= HA.next_backup_call)
+ call_backup(HA)
+
+ if(dist <= 1)
+ if(A && !istype(A, /obj/item/gun) && A.force > 0)
+ target.attackby(A, H)
+ A.afterattack(target, H, TRUE)
+
+ else
+ H.dna.species.spec_attack_hand(H, target)
+ if(target.incapacitated())
+ //I know kung-fu.
+
+ var/obj/item/card/id/their_id = target.get_idcard()
+ if(their_id && !HA.stealing_id)
+ H.visible_message("[H] starts to take [their_id] from [target]!")
+ HA.stealing_id = TRUE
+ addtimer(CALLBACK(HA, TYPE_PROC_REF(/datum/component/knpc, steal_id), their_id), 5 SECONDS)
+
+ if(istype(H) && H.knpc_traits & KNPC_IS_MARTIAL_ARTIST)
+ switch(rand(0, 2))
+ //Throw!
+ if(0)
+ H.start_pulling(target, supress_message = FALSE)
+ H.setGrabState(GRAB_AGGRESSIVE)
+ H.visible_message("[H] judo throws [target]!")
+// playsound(get_turf(target), 'nsv13/sound/effects/judo_throw.ogg', 100, TRUE)
+ target.shake_animation(10)
+ target.throw_at(get_turf(get_step(H, pick(GLOB.cardinals))), 5, 5)
+ if(1)
+ H.do_attack_animation(target, ATTACK_EFFECT_PUNCH)
+ target.visible_message("[H] grabs [target]'s wrist and wrenches it sideways!", \
+ "[H] grabs your wrist and violently wrenches it to the side!")
+ playsound(get_turf(H), 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
+ target.emote("scream")
+ target.dropItemToGround(target.get_active_held_item())
+ target.apply_damage(5, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ if(2)
+ H.do_attack_animation(target, ATTACK_EFFECT_KICK)
+ target.visible_message("[H] knees [target] in the stomach!", \
+ "[H] winds you with a knee in the stomach!")
+ target.audible_message("[target] gags!")
+ target.losebreath += 3
+
+ else
+ //So they actually execute the curbstomp.
+ if(dist <= 1)
+ H.forceMove(get_turf(target))
+ H.zone_selected = BODY_ZONE_HEAD
+ //Curbstomp!
+ H.MouseDrop(target)
+ return
+ if(H.knpc_traits & KNPC_IS_DODGER)
+ HA.kite(target)
+
+/datum/ai_goal/human/proc/call_backup(datum/component/knpc/HA)
+ HA.next_backup_call = world.time + 30 SECONDS //So it's not horribly spammy.
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ var/obj/item/radio/headset/radio = H.ears
+ H.do_alert_animation(H)
+ playsound(H, 'sound/machines/chime.ogg', 50, 1, -1)
+ //Lets the AIs call for help over comms... This is quite deadly.
+ var/support_text = (radio) ? "; " : ""
+ if(H.knpc_traits & KNPC_IS_AREA_SPECIFIC)
+ var/text = pick(H.call_lines)
+ text += " [get_area(H)]!"
+ support_text += text
+ else
+ support_text += pick(H.call_lines)
+ H.say(support_text)
+
+ // Call for other intelligent AIs
+ for(var/datum/component/knpc/HH as() in GLOB.knpcs - HA)
+ var/mob/living/carbon/human/ai_boarder/other = HH.parent
+ var/obj/item/radio/headset/other_radio = other.ears
+ if(other.z != H.z || !other.can_hear() || other.incapacitated())
+ continue //Yeah no. Radio is good, but not THAT good....yet
+ //They both have radios and can hear each other!
+ if((radio?.on && other_radio?.on) || get_dist(other, H) <= HA.view_range || H.faction_check_mob(other, TRUE))
+ var/thetext = (other_radio) ? "; " : ""
+ thetext += pick(H.response_lines)
+ HH.pathfind_to(H)
+ other.say(thetext)
+ //Firstly! Call for the simplemobs..
+ for(var/mob/living/simple_animal/hostile/M in oview(HA.view_range, HA.parent))
+ if(H.faction_check_mob(M, TRUE))
+ if(M.AIStatus == AI_OFF)
+ return
+ else
+ M.Goto(HA.parent,M.move_to_delay,M.minimum_distance)
+
+/datum/ai_goal/human/patrol
+ name = "Patrol Nodes"
+ score = AI_SCORE_LOW_PRIORITY //The default task for most AIs is to just patrol
+ required_ai_flags = null //Set this if you want this task to only be achievable by certain types of ship.
+
+
+/datum/ai_goal/human/patrol/action(datum/component/knpc/HA)
+ if(!can_action(HA))
+ return
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ if(HA.last_node && get_dist(HA.last_node, H) > 2)
+ HA.pathfind_to(HA.last_node) //Restart pathfinding
+ return FALSE
+ get_next_patrol_node(HA)
+
+/obj/effect/landmark/patrol_node
+ name = "AI patrol node"
+// icon = 'nsv13/icons/effects/mapping_helpers.dmi'
+ icon_state = "patrol_node"
+ var/id = null
+ var/next_id = null //id of the node that this one goes to. Alternatively, a list of ids which will all be possible next destinations.
+ var/previous_id = null //id of the node that precedes this one
+ var/obj/effect/landmark/patrol_node/previous //-- This isn't actually used anywhere.. - Delta
+ var/list/next_nodes = list() //List of possible followup nodes set by next_id. If multiple entities exist in the list, one will be chosen at random on every occasion.
+
+/obj/effect/landmark/patrol_node/Initialize(mapload)
+ . = ..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/effect/landmark/patrol_node/LateInitialize()
+ . = ..()
+ for(var/obj/effect/landmark/patrol_node/node in GLOB.landmarks_list)
+ if(!node.id)
+ continue
+ if(next_id)
+ if(islist(next_id))
+ var/list/next_id_list = next_id
+ if(node.id in next_id_list)
+ next_nodes += node
+ else
+ if(node.id == next_id)
+ next_nodes += node
+ if(previous_id && node.id == previous_id)
+ previous = node
+ if(next_id && !length(next_nodes))
+ CRASH("WARNING: Patrol node in [get_area(src)] has no next node(s) despite set id(s).")
+ if(previous_id && !previous)
+ CRASH("WARNING: Patrol node in [get_area(src)] has no previous node despite a set id.")
+
+/datum/ai_goal/human/proc/get_next_patrol_node(datum/component/knpc/HA)
+ //Okay, we need to pick a starting point.
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ if(!HA.last_node)
+ var/best_dist = 10000
+ var/obj/effect/landmark/patrol_node/best
+ for(var/obj/effect/landmark/patrol_node/node in GLOB.landmarks_list)
+ var/dist = get_dist(H, node.loc)
+ if(dist < best_dist && node.z == H.z)
+ best_dist = dist
+ best = node
+ HA.last_node = best
+ //Start the patrol.
+ HA.pathfind_to(best)
+ return
+
+ var/obj/effect/landmark/patrol_node/next_node = pick(HA.last_node.next_nodes)
+ if(HA.last_node.z != next_node.z)
+ var/obj/structure/ladder/L = locate(/obj/structure/ladder) in get_turf(HA.last_node)
+ if(!L)
+ L = locate(/obj/structure/ladder) in orange(1, get_turf(HA.last_node))
+ if(L)
+ //Use the ladder....
+ if(next_node.z > HA.last_node.z)
+ L.travel(TRUE, H, FALSE, L.up, FALSE)
+ else
+ L.travel(FALSE, H, FALSE, L.down, FALSE)
+ //No Ladder, lets check for stairs
+ else if(!L && next_node.z > HA.last_node.z) //If going up a Z
+ var/obj/structure/stairs/S = locate(/obj/structure/stairs) in orange(1, get_turf(HA.last_node))
+ if(S)
+ step_towards(H, S)
+ step(H, S.dir)
+ else //If down
+ var/obj/structure/stairs/S = locate(/obj/structure/stairs) in orange(1, get_step_multiz(get_turf(HA.last_node), DOWN))
+ if(S)
+ step_towards(H, S)
+
+ HA.last_node = next_node
+ HA.pathfind_to(next_node)
+
+/datum/ai_goal/human/set_internals
+ name = "Set Internals"
+ score = AI_SCORE_CRITICAL //The lads need to be able to breathe.
+ required_ai_flags = null
+
+/datum/ai_goal/human/set_internals/check_score(datum/component/knpc/HA)
+ if(!..())
+ return 0
+ var/mob/living/carbon/human/H = HA.parent
+ //We need to breathe....
+ if(H.failed_last_breath && world.time >= HA.next_internals_attempt)
+ HA.next_internals_attempt = world.time + 5 SECONDS
+ var/obj/item/storage/S = H.back
+ if(S && locate(/obj/item/tank/internals) in S.contents + H.contents)
+ return score
+ return 0
+
+/datum/ai_goal/human/set_internals/action(datum/component/knpc/HA)
+ if(!can_action(HA))
+ return
+ var/mob/living/carbon/human/ai_boarder/C = HA.parent
+
+ if(C.incapacitated())
+ return
+
+ if(C.internal)
+ C.internal = null
+ else
+ if(!C.getorganslot(ORGAN_SLOT_BREATHING_TUBE))
+ if(!istype(C.wear_mask))
+ return 1
+ else
+ var/obj/item/clothing/mask/M = C.wear_mask
+ if(M.mask_adjusted) // if mask on face but pushed down
+ M.adjustmask(C) // adjust it back
+ if(!(M.clothing_flags & ALLOWINTERNALS))
+ return
+
+ var/obj/item/I = C.is_holding_item_of_type(/obj/item/tank)
+ if(I)
+ C.internal = I
+ else if(ishuman(C))
+ var/mob/living/carbon/human/H = C
+ H.internal = locate(/obj/item/tank/internals) in H
+
+ //Separate so CO2 jetpacks are a little less cumbersome.
+ if(!C.internal && istype(C.back, /obj/item/tank))
+ C.internal = C.back
+
+/datum/ai_goal/human/stop_drop_n_roll
+ name = "Stop drop & roll"
+ score = AI_SCORE_HIGH_PRIORITY //Putting out fires is important, but not more important than killing the target setting us on fire
+ required_ai_flags = null
+
+/datum/ai_goal/human/stop_drop_n_roll/check_score(datum/component/knpc/HA)
+ if(!..())
+ return 0
+ var/mob/living/carbon/human/H = HA.parent
+ //We need to breathe....
+ if(H.fire_stacks > 0)
+ return score
+ return 0
+
+/datum/ai_goal/human/stop_drop_n_roll/action(datum/component/knpc/HA)
+ var/mob/living/carbon/human/H = HA.parent
+ if(!can_action(HA))
+ return
+ H.resist() //Stop drop and roll!
+
+/datum/ai_goal/human/escape_custody
+ name = "Escape Custody"
+ score = AI_SCORE_CRITICAL //Not being contained is fairly important
+ required_ai_flags = null
+
+/datum/ai_goal/human/escape_custody/check_score(datum/component/knpc/HA)
+ if(!..())
+ return 0
+ var/mob/living/carbon/human/H = HA.parent
+ if(istype(H.loc, /obj/structure/closet)) //Check if we are in a closet
+ return score
+ if(H.handcuffed || H.buckled || H.legcuffed) //Or locked up somehow
+ return score
+ return 0
+
+/datum/ai_goal/human/escape_custody/action(datum/component/knpc/HA)
+ var/mob/living/carbon/human/H = HA.parent
+ if(!H.can_resist()) //We use a different check here
+ return
+
+ if(istype(H.loc, /obj/structure/closet))
+ var/obj/structure/closet/C = H.loc
+ C.open() //Open that closet
+ if(H.handcuffed || H.buckled || H.legcuffed)
+ H.resist() //Trigger the universal escape button
+
+/datum/ai_goal/human/heal_self
+ name = "Heal Self"
+ score = AI_SCORE_PRIORITY //Heal ourselves if outside of combat
+ required_ai_flags = null
+
+/datum/ai_goal/human/heal_self/check_score(datum/component/knpc/HA)
+ if(!..())
+ return 0
+ var/mob/living/carbon/human/H = HA.parent
+ if(H.health <= (H.maxHealth / 2)) //If half health
+ if(locate(/obj/item/reagent_containers/hypospray/medipen/survival) in H.contents) //And has survival pen
+ return score
+ return 0
+
+/datum/ai_goal/human/heal_self/action(datum/component/knpc/HA)
+ if(!can_action(HA))
+ return
+ var/mob/living/carbon/human/H = HA.parent
+ var/obj/item/storage/S = H.back
+ var/list/expanded_contents = H.contents
+ if(S) //Checking for a backpack
+ expanded_contents += S.contents
+ for(var/obj/item/I in H.held_items) //If we are holding anything
+ I.forceMove(S) //Put it in our pack
+ else
+ for(var/obj/item/I in H.held_items) //If we are holding anything
+ I.forceMove(get_turf(H)) //Drop it on the floor
+ var/obj/item/reagent_containers/hypospray/medipen/survival/P = locate(/obj/item/reagent_containers/hypospray/medipen/survival) in expanded_contents
+ if(P)
+ H.put_in_active_hand(P) //Roll Up Your Sleeve
+ P.attack(H, H) //Self Vax
+ P.forceMove(get_turf(H)) //Litter because doing one good thing is enugh for today
+
+#undef AI_TRAIT_BRAWLER
+#undef AI_TRAIT_SUPPORT
diff --git a/shiptest.dme b/shiptest.dme
index e933dfe020ce..ea9a42849f61 100644
--- a/shiptest.dme
+++ b/shiptest.dme
@@ -2519,6 +2519,7 @@
#include "code\modules\mob\living\carbon\human\human_movement.dm"
#include "code\modules\mob\living\carbon\human\human_say.dm"
#include "code\modules\mob\living\carbon\human\inventory.dm"
+#include "code\modules\mob\living\carbon\human\knpc.dm"
#include "code\modules\mob\living\carbon\human\life.dm"
#include "code\modules\mob\living\carbon\human\physiology.dm"
#include "code\modules\mob\living\carbon\human\species.dm"
From e806ed926ca514edbe6337ca070917302b869776 Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Mon, 19 Feb 2024 19:22:52 -0800
Subject: [PATCH 2/8] wow
---
code/modules/mob/living/carbon/human/knpc.dm | 206 +++++++++++++------
1 file changed, 143 insertions(+), 63 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index 5af384342fc1..68bc313ac458 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -8,6 +8,7 @@
#define KNPC_IS_AREA_SPECIFIC 1<<3 //Does the KNPC scream out the area when calling for backup?
#define KNPC_IS_DOOR_BASHER 1<<4 //Does the KNPC kick the door off its hinges if it doesn't have a valid ID?
#define KNPC_IS_DOOR_HACKER 1<<5 //Does the KNPC hack the door open if it doesn't have a valid ID?
+#define KNPC_STEAL_ID 1<<5 //Do we steal IDs? used to prevent a bug
//Knpc pathfinding return values
#define KNPC_PATHFIND_SUCCESS 0 //Path successfully generated
@@ -32,6 +33,7 @@ DEFINE_BITFIELD(knpc_traits, list(
"KNPC_IS_AREA_SPECIFIC" = KNPC_IS_AREA_SPECIFIC,
"KNPC_IS_DOOR_BASHER" = KNPC_IS_DOOR_BASHER,
"KNPC_IS_DOOR_HACKER" = KNPC_IS_DOOR_HACKER,
+ "KNPC_STEAL_ID" = KNPC_STEAL_ID,
))
//AI score defines
@@ -543,6 +545,8 @@ GLOBAL_LIST_EMPTY(knpcs)
var/list/call_lines = list("Enemy spotted!")
var/list/response_lines = list("On my way!")
+ var/shut_up = TRUE
+
/mob/living/carbon/human/ai_boarder/Initialize(mapload)
. = ..()
randomize_human(src)
@@ -551,12 +555,18 @@ GLOBAL_LIST_EMPTY(knpcs)
O.equip(src)
AddComponent(/datum/component/knpc)
+/mob/living/carbon/human/ai_boarder/frontier
+ faction = list("frontier")
+ outfit = list (/datum/outfit/frontier)
+
/mob/living/carbon/human/ai_boarder/hermit
faction = list("hermit")
taunts = list("...")
call_lines = list("...!!!")
response_lines = list("!")
+ knpc_traits = KNPC_IS_DODGER | KNPC_IS_MERCIFUL
+
outfit = list (/datum/outfit/whitesands)
var/survivor_type
@@ -571,9 +581,13 @@ GLOBAL_LIST_EMPTY(knpcs)
survivor_type = "gunslinger"
/mob/living/carbon/human/ai_boarder/hermit/Initialize(mapload)
- . = ..()
if(!survivor_type)
survivor_type = pick("survivor","hunter","gunslinger")
+ ..()
+ for(var/obj/item/gun/searching as obj in contents)
+ if(istype(searching, /obj/item/gun))
+ searching.safety = FALSE
+
var/mob_species = pickweight(list(
/datum/species/human = 50,
/datum/species/lizard = 25,
@@ -584,6 +598,13 @@ GLOBAL_LIST_EMPTY(knpcs)
)
)
INVOKE_ASYNC(src, PROC_REF(set_species), mob_species)
+
+ var/obj/item/clothing/suit/hooded/survivor_hood = wear_suit
+ survivor_hood.ToggleHood()
+
+/datum/outfit/whitesands/pre_equip(mob/living/carbon/human/H, visualsOnly)
+ var/mob/living/carbon/human/ai_boarder/hermit/hermit = H
+ var/survivor_type = hermit.survivor_type
var/picked
//to-do: learn how to make mobsprites for other survivors
//w_uniforms are random to show varied backgrounds, but similar goal
@@ -619,7 +640,7 @@ GLOBAL_LIST_EMPTY(knpcs)
else
picked = /obj/item/clothing/under/color/random
- w_uniform = new picked(src)
+ uniform = picked
//storage is semi-randomized, giving some variety
if(survivor_type == "survivor")
@@ -657,10 +678,10 @@ GLOBAL_LIST_EMPTY(knpcs)
else
picked = /obj/item/storage/belt/fannypack
- belt = new picked(src)
+ belt = picked
//everyone wears the same suit
- wear_suit = new /obj/item/clothing/suit/hooded/survivor(src)
+ suit = /obj/item/clothing/suit/hooded/survivor
if (survivor_type == "gunslinger")
if(prob(30))
@@ -670,10 +691,7 @@ GLOBAL_LIST_EMPTY(knpcs)
else
picked = /obj/item/clothing/shoes/workboots/mining
- shoes = new picked(src)
-
-
-
+ shoes = picked
//gloves are a tossup
picked = pickweight(list(
@@ -683,7 +701,7 @@ GLOBAL_LIST_EMPTY(knpcs)
)
)
- gloves = new picked(src)
+ gloves = picked
//bags are semi-random.
picked = pickweight(list(
@@ -695,11 +713,11 @@ GLOBAL_LIST_EMPTY(knpcs)
)
)
- back = new picked(src)
+ back = picked
//as are bag contents
- var/list/backpack_contents = list()
+ backpack_contents = list()
if(prob(70))
backpack_contents += pickweight(list( //these could stand to be expanded, right now they're just mildly modified miner ones, and I don't know how to plus that up.
/obj/item/soap = 10,
@@ -754,40 +772,29 @@ GLOBAL_LIST_EMPTY(knpcs)
)
if(prob(30)) //some pens maybe?
backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
-
- var/obj/item/storage/backpack/current_bag = back
- if(current_bag)
- for(var/obj/thing as anything in backpack_contents)
- new thing(current_bag)
+ if(prob(5))
+ backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
//pockets
if(survivor_type == "survivor") //could also use fleshing out
if(prob(30))
- l_store = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
- else
- l_store = /obj/item/tank/internals/emergency_oxygen/engi
- if (prob(20))
- r_store = /obj/item/spacecash/bundle/mediumrand
+ l_pocket = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
else
- r_store = null
+ l_pocket = /obj/item/tank/internals/emergency_oxygen/engi
if(survivor_type == "hunter")
- l_store = /obj/item/tank/internals/emergency_oxygen/engi
+ l_pocket = /obj/item/tank/internals/emergency_oxygen/engi
if (prob(20))
- r_store = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
- else if (prob(60))
- r_store = /obj/item/ammo_box/aac_300blk_stripper
- else
- r_store = null
+ l_pocket = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
if(survivor_type == "gunslinger")
if(prob(50))
- l_store = /obj/item/ammo_box/magazine/aks74u
- r_store = /obj/item/tank/internals/emergency_oxygen/engi
+ l_pocket = /obj/item/ammo_box/magazine/aks74u
+ r_pocket = /obj/item/tank/internals/emergency_oxygen/engi
else
- r_store = /obj/item/tank/internals/emergency_oxygen/engi
- l_store = /obj/item/radio
+ r_pocket = /obj/item/tank/internals/emergency_oxygen/engi
+ l_pocket = /obj/item/radio
//masks
picked = pickweight(list(
@@ -798,7 +805,7 @@ GLOBAL_LIST_EMPTY(knpcs)
)
)
- picked = new picked(src)
+ mask = picked
//the eyes are the window into the soul. I don't think these things have souls but whatever.
if(prob(70))
@@ -809,9 +816,9 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/clothing/glasses = 20
)
)
- glasses = new picked(src)
+ glasses = picked
else
- picked = null
+ glasses = null
//and of course, ears.
if(prob(1)) //oh my god they can't hear the sandstorm coming they've got airpods in
@@ -822,22 +829,24 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/radio/headset/alt = 50
)
)
- ears = new picked(src)
+ ears = picked
//exosuit bits
- s_store = null
- var/spare_ammo_count = rand(1,4)
+ suit_store = null
+ var/spare_ammo_count = rand(1,2)
if (survivor_type == "hunter")
- INVOKE_ASYNC(src, PROC_REF(put_in_hands), new /obj/item/gun/ballistic/rifle/boltaction/polymer(src))
- spare_ammo_count = rand(2,7)
+ r_hand = /obj/item/gun/ballistic/rifle/polymer
+ spare_ammo_count = rand(2,5)
for(var/i in 1 to spare_ammo_count)
- new /obj/item/ammo_box/magazine/internal/boltaction/polymer(back)
+ backpack_contents += /obj/item/ammo_box/aac_300blk_stripper
if(survivor_type == "gunslinger")
- INVOKE_ASYNC(src, PROC_REF(put_in_hands), new /obj/item/gun/ballistic/automatic/smg/aks74u(src))
+ r_hand = /obj/item/gun/ballistic/automatic/smg/aks74u
for(var/i in 1 to spare_ammo_count)
- new /obj/item/ammo_box/magazine/aks74u(back)
+ backpack_contents += /obj/item/ammo_box/magazine/aks74u
+
+ internals_slot = ITEM_SLOT_RPOCKET
/datum/component/knpc/Initialize()
if(!iscarbon(parent))
@@ -1245,24 +1254,35 @@ This is to account for sec Ju-Jitsuing boarding commandos.
if(istype(gun, /obj/item/gun/ballistic))
var/obj/item/gun/ballistic/B = gun
if(istype(B.mag_type, /obj/item/ammo_box/magazine/internal))
- //Not dealing with this. They'll just ditch the revolver when they're done with it.
- B.forceMove(get_turf(H))
- return FALSE
+ if(!istype(B, /obj/item/gun/ballistic/rifle/polymer) || !istype(B, /obj/item/gun/ballistic/rifle/illestren))
+ //Not dealing with this. They'll just ditch the revolver when they're done with it.
+ B.forceMove(get_turf(H))
+ return FALSE
///message_admins("Issa gun")
var/obj/item/storage/S = H.back
//Okay first off, is the gun already on our person?
var/list/expanded_contents = H.contents
if(S)
expanded_contents = S.contents + H.contents
- var/obj/item/ammo_box/magazine/target_mag = locate(B.mag_type) in expanded_contents
+ var/obj/item/ammo_box/target_mag = locate(B.mag_type) in expanded_contents
+ if(istype(B, /obj/item/gun/ballistic/rifle/polymer))
+ target_mag = locate(/obj/item/ammo_box/aac_300blk_stripper) in expanded_contents
+ if(istype(B, /obj/item/gun/ballistic/rifle))
+ if(!B.bolt_locked)
+ B.rack(H)
+ return TRUE
//message_admins("Found [target_mag]")
if(target_mag)
- //Dump that old mag
- H.put_in_inactive_hand(target_mag)
- B?.magazine?.forceMove(get_turf(H))
- B.attackby(target_mag, H)
- B.rack(H) //Rack the bolt.
- if(istype(B, /obj/item/gun/ballistic/rifle))
+ if(istype(B, /obj/item/gun/ballistic/rifle/polymer))
+ H.put_in_inactive_hand(target_mag)
+ B.attackby(target_mag, H)
+ target_mag.forceMove(get_turf(H))
+ B.rack(H) //Rack the bolt.
+ else
+ //Dump that old mag
+ H.put_in_inactive_hand(target_mag)
+ B?.magazine?.forceMove(get_turf(H))
+ B.attackby(target_mag, H)
B.rack(H) //Rack the bolt.
// addtimer(CALLBACK(B, PROC_REF(rack), H), 0.5 SECONDS, TIMER_UNIQUE)
else
@@ -1271,6 +1291,15 @@ This is to account for sec Ju-Jitsuing boarding commandos.
return FALSE
gun.forceMove(S)
+/datum/ai_goal/human/engage_targets/proc/check_ammo(obj/item/gun/ballistic/B = null)
+ if(!B)
+ return FALSE
+ if(!B.magazine)
+ return FALSE
+ if(!B.magazine.stored_ammo.len)
+ return FALSE
+ return TRUE
+
/datum/ai_goal/human/engage_targets/action(datum/component/knpc/HA)
if(!can_action(HA))
@@ -1280,6 +1309,7 @@ This is to account for sec Ju-Jitsuing boarding commandos.
var/list/enemies = get_aggressors(HA)
var/obj/item/A = H.get_active_held_item()
var/closest_dist = 1000
+ var/just_racked = FALSE
var/mob/living/target = null
for(var/mob/living/L as() in enemies)
var/dist = get_dist(L, H)
@@ -1298,10 +1328,18 @@ This is to account for sec Ju-Jitsuing boarding commandos.
if(istype(A, /obj/item/gun/ballistic))
B = A
H.a_intent = (prob(65)) ? INTENT_HARM : INTENT_DISARM
+ G.safety = FALSE
if(G && dist > 0)
- if(!G.can_shoot())
- //We need to reload first....
- reload(HA, G)
+ if(!G.can_shoot() || !G?.chambered.BB)
+ if(istype(B, /obj/item/gun/ballistic/rifle) && check_ammo(B))
+ B.rack(H)
+ just_racked = TRUE
+ else
+ G.safety = FALSE
+ if(!G.can_shoot() || !G?.chambered.BB) //still cant shoot?
+ //We need to reload first....
+ reload(HA, G)
+ just_racked = TRUE
//Fire! If they're in a ship, we don't want to scrap them directly.
if(!CheckFriendlyFire(H, target))
//Okay, we have a line of sight, shoot!
@@ -1315,14 +1353,17 @@ This is to account for sec Ju-Jitsuing boarding commandos.
E.select_fire(H)
if(isobj(target.loc))
- G.afterattack(target.loc, H)
+ if(!just_racked)
+ G.afterattack(target.loc, H)
else
- G.afterattack(target, H)
- if(istype(B, /obj/item/gun/ballistic/rifle))
- B.rack(H) //Rack the bolt.
- B.rack(H)
+ if(!just_racked)
+ G.afterattack(target, H)
+
+// if(istype(B, /obj/item/gun/ballistic/rifle))
+// B.rack(H) //Rack the bolt.
// addtimer(CALLBACK(G, PROC_REF(rack), H), 0.5 SECONDS, TIMER_UNIQUE)
//Call your friends to help :))
+ just_racked = FALSE
if(world.time >= HA.next_backup_call)
call_backup(HA)
@@ -1337,7 +1378,7 @@ This is to account for sec Ju-Jitsuing boarding commandos.
//I know kung-fu.
var/obj/item/card/id/their_id = target.get_idcard()
- if(their_id && !HA.stealing_id)
+ if(their_id && !HA.stealing_id && H.knpc_traits & KNPC_STEAL_ID)
H.visible_message("[H] starts to take [their_id] from [target]!")
HA.stealing_id = TRUE
addtimer(CALLBACK(HA, TYPE_PROC_REF(/datum/component/knpc, steal_id), their_id), 5 SECONDS)
@@ -1392,7 +1433,8 @@ This is to account for sec Ju-Jitsuing boarding commandos.
support_text += text
else
support_text += pick(H.call_lines)
- H.say(support_text)
+ if(!H.shut_up)
+ H.say(support_text)
// Call for other intelligent AIs
for(var/datum/component/knpc/HH as() in GLOB.knpcs - HA)
@@ -1401,6 +1443,8 @@ This is to account for sec Ju-Jitsuing boarding commandos.
if(other.z != H.z || !other.can_hear() || other.incapacitated())
continue //Yeah no. Radio is good, but not THAT good....yet
//They both have radios and can hear each other!
+ if(H.shut_up)
+ continue
if((radio?.on && other_radio?.on) || get_dist(other, H) <= HA.view_range || H.faction_check_mob(other, TRUE))
var/thetext = (other_radio) ? "; " : ""
thetext += pick(H.response_lines)
@@ -1438,6 +1482,42 @@ This is to account for sec Ju-Jitsuing boarding commandos.
var/previous_id = null //id of the node that precedes this one
var/obj/effect/landmark/patrol_node/previous //-- This isn't actually used anywhere.. - Delta
var/list/next_nodes = list() //List of possible followup nodes set by next_id. If multiple entities exist in the list, one will be chosen at random on every occasion.
+ invisibility = INVISIBILITY_OBSERVER
+
+/obj/effect/landmark/patrol_node/whitesands
+ id = "whitesands"
+ next_id = "whitesands"
+ icon_state = "x"
+
+/obj/effect/landmark/patrol_node/breach
+ name = "AI hold here node"
+ id = "hold"
+ next_id = "hold"
+ icon_state = "x4"
+
+/obj/effect/landmark/patrol_node/breach
+ name = "AI breach here node"
+ id = "breach1"
+ next_id = "breach2"
+ icon_state = "x"
+
+/obj/effect/landmark/patrol_node/breach/two
+ name = "AI after breach, go here node"
+ id = "breach2"
+ next_id = "breach3"
+ icon_state = "x2"
+
+/obj/effect/landmark/patrol_node/breach/three
+ name = "AI midway, go here node"
+ id = "breach3"
+ next_id = "breach4"
+ icon_state = "x3"
+
+/obj/effect/landmark/patrol_node/breach/four
+ name = "AI after sweeping, hold here node"
+ id = "breach4"
+ next_id = "breach4"
+ icon_state = "x4"
/obj/effect/landmark/patrol_node/Initialize(mapload)
. = ..()
From 3a75c22015926596b188c8dd587a826de5125f36 Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Sun, 31 Mar 2024 23:36:10 -0700
Subject: [PATCH 3/8] ough
---
code/modules/mob/living/carbon/human/knpc.dm | 238 +++++++++++++-----
.../living/simple_animal/hostile/survivors.dm | 32 ++-
2 files changed, 204 insertions(+), 66 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index 68bc313ac458..3db9651b9435 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -270,6 +270,8 @@ DEFINE_BITFIELD(knpc_traits, list(
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
+ /// 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/list/turfs_to_avoid = list(/turf/open/acid,/turf/open/lava)
/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, avoid_mobs)
src.caller = caller
@@ -369,6 +371,9 @@ DEFINE_BITFIELD(knpc_traits, list(
steps_taken++
if(!CAN_STEP(lag_turf, current_turf))
return
+ for(var/turf/checked_turf as anything in turfs_to_avoid)
+ if(istype(current_turf,checked_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)
@@ -430,6 +435,9 @@ DEFINE_BITFIELD(knpc_traits, list(
steps_taken++
if(!CAN_STEP(lag_turf, current_turf))
return
+ for(var/turf/checked_turf as anything in turfs_to_avoid)
+ if(istype(current_turf,checked_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)
@@ -531,6 +539,7 @@ GLOBAL_LIST_EMPTY(knpcs)
var/static/list/climbable = typecacheof(list(/obj/structure/table, /obj/structure/railing)) // climbable structures
var/pathfind_timeout = 0 //If pathfinding fails, it is püt in timeout for a while to avoid spamming the server with pathfinding calls.
var/timeout_stacks = 0 //Consecutive pathfind fails add additional delay stacks to further counteract the effects of knpcs in unreachable locations.
+ var/list/turfs_to_avoid = list(/turf/open/acid,/turf/open/lava)
/mob/living/carbon/human/ai_boarder
faction = list("neutral")
@@ -580,6 +589,9 @@ GLOBAL_LIST_EMPTY(knpcs)
/mob/living/carbon/human/ai_boarder/hermit/gunslinger
survivor_type = "gunslinger"
+/mob/living/carbon/human/ai_boarder/hermit/commando
+ survivor_type = "commando"
+
/mob/living/carbon/human/ai_boarder/hermit/Initialize(mapload)
if(!survivor_type)
survivor_type = pick("survivor","hunter","gunslinger")
@@ -600,7 +612,8 @@ GLOBAL_LIST_EMPTY(knpcs)
INVOKE_ASYNC(src, PROC_REF(set_species), mob_species)
var/obj/item/clothing/suit/hooded/survivor_hood = wear_suit
- survivor_hood.ToggleHood()
+ if(survivor_hood)
+ survivor_hood.ToggleHood()
/datum/outfit/whitesands/pre_equip(mob/living/carbon/human/H, visualsOnly)
var/mob/living/carbon/human/ai_boarder/hermit/hermit = H
@@ -627,7 +640,7 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/clothing/under/utility = 5
)
)
- else if (survivor_type == "gunslinger")
+ else if (survivor_type == "gunslinger" || survivor_type == "commando")
picked = pickweight(list(
/obj/item/clothing/under/rank/cargo/miner/lavaland = 35,
/obj/item/clothing/under/color/random = 25,
@@ -665,7 +678,7 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/storage/belt/mining/vendor = 3,
)
)
- else if(survivor_type == "gunslinger")
+ else if(survivor_type == "gunslinger" || survivor_type == "commando")
picked = pickweight(list(
/obj/item/storage/belt/mining = 30,
/obj/item/storage/belt/bandolier = 30,
@@ -683,7 +696,7 @@ GLOBAL_LIST_EMPTY(knpcs)
//everyone wears the same suit
suit = /obj/item/clothing/suit/hooded/survivor
- if (survivor_type == "gunslinger")
+ if (survivor_type == "gunslinger" || survivor_type == "commando")
if(prob(30))
picked = /obj/item/clothing/shoes/combat //but sometimes there are nicer shoes
else
@@ -774,6 +787,8 @@ GLOBAL_LIST_EMPTY(knpcs)
backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
if(prob(5))
backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
+ if(prob(10)) //carried over from previous
+ backpack_contents += /obj/item/crusher_trophy/shiny
//pockets
if(survivor_type == "survivor") //could also use fleshing out
@@ -787,9 +802,9 @@ GLOBAL_LIST_EMPTY(knpcs)
if (prob(20))
l_pocket = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
- if(survivor_type == "gunslinger")
+ if(survivor_type == "gunslinger" || survivor_type == "commando")
if(prob(50))
- l_pocket = /obj/item/ammo_box/magazine/aks74u
+ l_pocket = /obj/item/ammo_box/magazine/skm_545_39
r_pocket = /obj/item/tank/internals/emergency_oxygen/engi
else
@@ -802,6 +817,14 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/clothing/mask/gas/explorer = 20,
/obj/item/clothing/mask/gas/explorer/old = 20,
/obj/item/clothing/mask/gas/syndicate = 20,
+ /obj/item/clothing/mask/breath = 5,
+ /obj/item/clothing/mask/breath/medical = 5,
+ /obj/item/clothing/mask/breath/suns = 5,
+ /obj/item/clothing/mask/gas/sechailer = 10,
+ /obj/item/clothing/mask/gas/sechailer/balaclava = 10,
+ /obj/item/clothing/mask/gas/sechailer/balaclava/inteq = 10,
+ /obj/item/clothing/mask/gas/sechailer/swat = 1,
+ /obj/item/clothing/mask/gas/sechailer/swat/spacepol = 1,
)
)
@@ -841,10 +864,40 @@ GLOBAL_LIST_EMPTY(knpcs)
for(var/i in 1 to spare_ammo_count)
backpack_contents += /obj/item/ammo_box/aac_300blk_stripper
- if(survivor_type == "gunslinger")
- r_hand = /obj/item/gun/ballistic/automatic/smg/aks74u
- for(var/i in 1 to spare_ammo_count)
- backpack_contents += /obj/item/ammo_box/magazine/aks74u
+ if(survivor_type == "gunslinger" || survivor_type == "commando")
+ if(prob(7) || survivor_type == "commando") //cause fuck you, thats why
+ uniform = /obj/item/clothing/under/rank/security/officer/camo
+
+ suit = pickweight(list(
+ /obj/item/clothing/suit/armor/vest/bulletproof = 35,
+ /obj/item/clothing/suit/armor/vest/syndie = 20,
+ /obj/item/clothing/suit/armor/vest/marine = 1,
+ /obj/item/clothing/suit/armor/vest/marine/heavy = 1,
+ /obj/item/clothing/suit/armor/vest/marine = 1,
+ ))
+ head = pickweight(list(
+ /obj/item/clothing/head/helmet/bulletproof/x11 = 40,
+ /obj/item/clothing/head/helmet/bulletproof/m10 = 40,
+
+ /obj/item/clothing/head/helmet/swat = 20,
+ /obj/item/clothing/head/helmet/swat/nanotrasen = 20,
+
+ /obj/item/clothing/head/helmet/marine = 1,
+ /obj/item/clothing/head/helmet/marine/security = 1,
+ ))
+ spare_ammo_count = rand(1,3)
+ r_hand = /obj/item/gun/ballistic/automatic/assault/skm/pirate
+ for(var/i in 1 to spare_ammo_count)
+ if(prob(5))
+ backpack_contents += /obj/item/ammo_box/magazine/skm_762_40/drum //die.
+ else if(prob(20))
+ backpack_contents += /obj/item/ammo_box/magazine/skm_762_40/extended
+ else
+ backpack_contents += /obj/item/ammo_box/magazine/skm_762_40
+ else
+ r_hand = /obj/item/gun/ballistic/automatic/smg/skm_carbine
+ for(var/i in 1 to spare_ammo_count)
+ backpack_contents += /obj/item/ammo_box/magazine/skm_545_39
internals_slot = ITEM_SLOT_RPOCKET
@@ -916,13 +969,21 @@ GLOBAL_LIST_EMPTY(knpcs)
return FALSE
if(pathfind_timeout > 0) //Pathfinding in timeout, move around aimlessly
H.set_resting(FALSE, FALSE)
- H.Move(get_step(H,pick(GLOB.cardinals)))
+ var/turf/new_pos = get_step(H,pick(GLOB.cardinals))
+ for(var/turf/checked_turf as anything in turfs_to_avoid)
+ if(istype(new_pos,checked_turf))
+ return FALSE
+ H.Move(new_pos)
return FALSE
if(!path)
return FALSE
if(tries > 5)
//Add a bit of randomness to their movement to reduce "traffic jams"
- H.Move(get_step(H,pick(GLOB.cardinals)))
+ var/turf/new_pos = get_step(H,pick(GLOB.cardinals))
+ for(var/turf/checked_turf as anything in turfs_to_avoid)
+ if(istype(new_pos,checked_turf))
+ return FALSE
+ H.Move(new_pos)
if(prob(10))
H.toggle_resting()
return FALSE
@@ -1007,7 +1068,11 @@ GLOBAL_LIST_EMPTY(knpcs)
chosen_dir = pick(cardinal_sidestep_directions)
if(chosen_dir)
chosen_dir = turn(target_dir,chosen_dir)
- H.Move(get_step(H,chosen_dir))
+ var/turf/new_pos = get_step(H,chosen_dir)
+ for(var/turf/checked_turf as anything in turfs_to_avoid)
+ if(istype(new_pos,checked_turf))
+ return FALSE
+ H.Move(new_pos)
H.face_atom(target) //Looks better if they keep looking at you when dodging
///Allows the AI actor to be revived by a medic, and get straight back into the fight!
@@ -1160,6 +1225,7 @@ This is to account for sec Ju-Jitsuing boarding commandos.
return 0
var/obj/item/gun/G_New = locate(/obj/item/gun) in oview(HA.view_range, H)
if(G_New && gun_suitable(H, G_New))
+ G_New.safety = FALSE
return AI_SCORE_CRITICAL //There is a gun really obviously in the open....
return score
@@ -1177,6 +1243,7 @@ This is to account for sec Ju-Jitsuing boarding commandos.
var/mob/living/carbon/human/H = HA.parent
var/obj/item/storage/S = H.back
var/obj/item/gun/target_item = null
+ var/obj/item/A = H.get_active_held_item()
//Okay first off, is the gun already on our person?
if(S)
var/list/expanded_contents = S.contents + H.contents
@@ -1185,6 +1252,11 @@ This is to account for sec Ju-Jitsuing boarding commandos.
if(target_item)
H.visible_message("[H] rummages around in their backpack...")
target_item.forceMove(get_turf(H)) //Put it on the floor so they can grab it
+ if(A)
+ if(!S)
+ A.forceMove(get_turf(H))
+ else
+ A.forceMove(S)
if(H.put_in_hands(target_item))
return TRUE //We're done!
//Now we run the more expensive check to find a gun laying on the ground.
@@ -1211,6 +1283,11 @@ This is to account for sec Ju-Jitsuing boarding commandos.
var/obj/structure/closet/C = target_item.loc
if(C.open(H))
H.visible_message("[H] pops open [C]...")
+ if(A)
+ if(!S)
+ A.forceMove(get_turf(H))
+ else
+ A.forceMove(S)
if(istype(target_item, /obj/item/gun/ballistic))
var/obj/item/gun/ballistic/B = target_item
var/obj/item/ammo_box/magazine/M = locate(B.mag_type) in oview(3, target_item)
@@ -1324,12 +1401,17 @@ This is to account for sec Ju-Jitsuing boarding commandos.
var/obj/item/gun/G = null
if(istype(A, /obj/item/gun))
G = A
+ if(iscarbon(target) || !(G && get_dist(target, H) < 3))
+ HA.pathfind_to(target) //Walk up close and YEET SLAM
+ else
+ HA.dest = null
var/obj/item/gun/ballistic/B = null
if(istype(A, /obj/item/gun/ballistic))
B = A
H.a_intent = (prob(65)) ? INTENT_HARM : INTENT_DISARM
- G.safety = FALSE
- if(G && dist > 0)
+ if(G)
+ G.safety = FALSE
+ if(G && dist > 0 && !istype(target, /mob/living/simple_animal/hostile/asteroid/basilisk/whitesands))
if(!G.can_shoot() || !G?.chambered.BB)
if(istype(B, /obj/item/gun/ballistic/rifle) && check_ammo(B))
B.rack(H)
@@ -1350,7 +1432,7 @@ This is to account for sec Ju-Jitsuing boarding commandos.
if(istype(G, /obj/item/gun/energy/e_gun))
var/obj/item/gun/energy/e_gun/E = G
if(prob(20))
- E.select_fire(H)
+ E.unique_action(H)
if(isobj(target.loc))
if(!just_racked)
@@ -1368,57 +1450,91 @@ This is to account for sec Ju-Jitsuing boarding commandos.
call_backup(HA)
if(dist <= 1)
- if(A && !istype(A, /obj/item/gun) && A.force > 0)
- target.attackby(A, H)
- A.afterattack(target, H, TRUE)
-
- else
+ var/proc_fist = TRUE
+ if(!istype(A, /obj/item/gun) && H.a_intent == INTENT_HARM)
+ if(grab_melee(HA) )
+ A = H.get_active_held_item()
+ if(A)
+ target.attackby(A, H)
+ A.afterattack(target, H, TRUE)
+ proc_fist = FALSE
+
+ if(proc_fist && !isanimal(target))
H.dna.species.spec_attack_hand(H, target)
- if(target.incapacitated())
- //I know kung-fu.
-
- var/obj/item/card/id/their_id = target.get_idcard()
- if(their_id && !HA.stealing_id && H.knpc_traits & KNPC_STEAL_ID)
- H.visible_message("[H] starts to take [their_id] from [target]!")
- HA.stealing_id = TRUE
- addtimer(CALLBACK(HA, TYPE_PROC_REF(/datum/component/knpc, steal_id), their_id), 5 SECONDS)
-
- if(istype(H) && H.knpc_traits & KNPC_IS_MARTIAL_ARTIST)
- switch(rand(0, 2))
- //Throw!
- if(0)
- H.start_pulling(target, supress_message = FALSE)
- H.setGrabState(GRAB_AGGRESSIVE)
- H.visible_message("[H] judo throws [target]!")
+
+ if(target.incapacitated())
+ //I know kung-fu.
+
+ var/obj/item/card/id/their_id = target.get_idcard()
+ if(their_id && !HA.stealing_id && H.knpc_traits & KNPC_STEAL_ID)
+ H.visible_message("[H] starts to take [their_id] from [target]!")
+ HA.stealing_id = TRUE
+ addtimer(CALLBACK(HA, TYPE_PROC_REF(/datum/component/knpc, steal_id), their_id), 5 SECONDS)
+
+ if(istype(H) && H.knpc_traits & KNPC_IS_MARTIAL_ARTIST)
+ switch(rand(0, 2))
+ //Throw!
+ if(0)
+ H.start_pulling(target, supress_message = FALSE)
+ H.setGrabState(GRAB_AGGRESSIVE)
+ H.visible_message("[H] judo throws [target]!")
// playsound(get_turf(target), 'nsv13/sound/effects/judo_throw.ogg', 100, TRUE)
- target.shake_animation(10)
- target.throw_at(get_turf(get_step(H, pick(GLOB.cardinals))), 5, 5)
- if(1)
- H.do_attack_animation(target, ATTACK_EFFECT_PUNCH)
- target.visible_message("[H] grabs [target]'s wrist and wrenches it sideways!", \
- "[H] grabs your wrist and violently wrenches it to the side!")
- playsound(get_turf(H), 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
- target.emote("scream")
- target.dropItemToGround(target.get_active_held_item())
- target.apply_damage(5, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
- if(2)
- H.do_attack_animation(target, ATTACK_EFFECT_KICK)
- target.visible_message("[H] knees [target] in the stomach!", \
- "[H] winds you with a knee in the stomach!")
- target.audible_message("[target] gags!")
- target.losebreath += 3
+ target.shake_animation(10)
+ target.throw_at(get_turf(get_step(H, pick(GLOB.cardinals))), 5, 5)
+ if(1)
+ H.do_attack_animation(target, ATTACK_EFFECT_PUNCH)
+ target.visible_message("[H] grabs [target]'s wrist and wrenches it sideways!", \
+ "[H] grabs your wrist and violently wrenches it to the side!")
+ playsound(get_turf(H), 'sound/weapons/thudswoosh.ogg', 50, 1, -1)
+ target.emote("scream")
+ target.dropItemToGround(target.get_active_held_item())
+ target.apply_damage(5, BRUTE, pick(BODY_ZONE_L_ARM, BODY_ZONE_R_ARM))
+ if(2)
+ H.do_attack_animation(target, ATTACK_EFFECT_KICK)
+ target.visible_message("[H] knees [target] in the stomach!", \
+ "[H] winds you with a knee in the stomach!")
+ target.audible_message("[target] gags!")
+ target.losebreath += 3
- else
- //So they actually execute the curbstomp.
- if(dist <= 1)
- H.forceMove(get_turf(target))
- H.zone_selected = BODY_ZONE_HEAD
- //Curbstomp!
- H.MouseDrop(target)
- return
+ else
+ //So they actually execute the curbstomp.
+ if(dist <= 1)
+ H.forceMove(get_turf(target))
+ H.zone_selected = BODY_ZONE_HEAD
+ //Curbstomp!
+ H.MouseDrop(target)
+ return
if(H.knpc_traits & KNPC_IS_DODGER)
HA.kite(target)
+/datum/ai_goal/human/proc/grab_melee(datum/component/knpc/HA)
+ var/mob/living/carbon/human/ai_boarder/H = HA.parent
+ var/obj/item/A = H.get_active_held_item()
+ if(A && A.force > 10)
+ return TRUE
+
+ var/obj/item/storage/back_storage = H.back
+ //Okay first off, is the gun already on our person?
+ var/list/expanded_contents = H.contents
+ if(back_storage)
+ expanded_contents = back_storage.contents + H.contents
+ var/obj/item/highest_force_score
+ for(var/obj/item/item_to_check as anything in expanded_contents)
+ if(!isobj(item_to_check))
+ continue
+ if(item_to_check.force < 5)
+ continue
+ if(!highest_force_score)
+ highest_force_score = item_to_check
+ continue
+ if(item_to_check.force > highest_force_score.force)
+ highest_force_score = item_to_check
+ continue
+ if(H.put_in_hands(highest_force_score))
+ return TRUE
+ return FALSE
+
+
/datum/ai_goal/human/proc/call_backup(datum/component/knpc/HA)
HA.next_backup_call = world.time + 30 SECONDS //So it's not horribly spammy.
var/mob/living/carbon/human/ai_boarder/H = HA.parent
diff --git a/code/modules/mob/living/simple_animal/hostile/survivors.dm b/code/modules/mob/living/simple_animal/hostile/survivors.dm
index bc7573552bc5..2806a541794d 100644
--- a/code/modules/mob/living/simple_animal/hostile/survivors.dm
+++ b/code/modules/mob/living/simple_animal/hostile/survivors.dm
@@ -45,13 +45,23 @@
name = "Hermit Wanderer"
desc =" A wild-eyed figure, wearing tattered mining equipment and boasting a malformed body, twisted by the heavy metals and high background radiation of the sandworlds."
+/mob/living/simple_animal/hostile/asteroid/whitesands/survivor/Initialize(mapload)
+ . = ..()
+ var/mob/living/carbon/human/ai_boarder/hermit/survivor/newhermit = new(loc)
+ newhermit.faction = faction.Copy()
+ return INITIALIZE_HINT_QDEL
+
+
/mob/living/simple_animal/hostile/asteroid/whitesands/survivor/random/Initialize()
. = ..()
- if(prob(35))
- new /mob/living/simple_animal/hostile/asteroid/whitesands/ranged/hunter(loc)
- if(prob(10))
- new /mob/living/simple_animal/hostile/asteroid/whitesands/ranged/gunslinger(loc)
- return INITIALIZE_HINT_QDEL
+ if(prob(15))
+ new /mob/living/carbon/human/ai_boarder/hermit/hunter(loc)
+ else if(prob(5))
+ new /mob/living/carbon/human/ai_boarder/hermit/gunslinger(loc)
+ else if(prob(20))
+ new /mob/living/carbon/human/ai_boarder/hermit/survivor(loc)
+ return INITIALIZE_HINT_QDEL
+
/mob/living/simple_animal/hostile/asteroid/whitesands/ranged
icon_state = "survivor_hunter"
@@ -71,6 +81,12 @@
/obj/effect/mob_spawn/human/corpse/damaged/whitesands/hunter,
)
+/mob/living/simple_animal/hostile/asteroid/whitesands/ranged/hunter/Initialize(mapload)
+ . = ..()
+ var/mob/living/carbon/human/ai_boarder/hermit/hunter/newhermit = new(loc)
+ newhermit.faction = faction.Copy()
+ return INITIALIZE_HINT_QDEL
+
/mob/living/simple_animal/hostile/asteroid/whitesands/ranged/gunslinger
name = "Hermit Soldier"
desc = "The miner's rebellion, though mostly underground, recieved a few good weapon shipments from an off-sector source. You should probably start running."
@@ -85,6 +101,12 @@
/obj/effect/mob_spawn/human/corpse/damaged/whitesands/gunslinger,
)
+/mob/living/simple_animal/hostile/asteroid/whitesands/ranged/gunslinger/Initialize(mapload)
+ . = ..()
+ var/mob/living/carbon/human/ai_boarder/hermit/gunslinger/newhermit = new(loc)
+ newhermit.faction = faction.Copy()
+ return INITIALIZE_HINT_QDEL
+
//survivor corpses
/obj/effect/mob_spawn/human/corpse/damaged/whitesands
From 5d332daf76f741ff861a15c319bc3b0c8cf7fb9b Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Mon, 27 May 2024 15:15:57 -0700
Subject: [PATCH 4/8] adds a few wrinkles
---
code/modules/mob/living/carbon/human/knpc.dm | 55 +++++++++++++++-----
1 file changed, 42 insertions(+), 13 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index 3db9651b9435..4b71da59b1ce 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -603,7 +603,7 @@ GLOBAL_LIST_EMPTY(knpcs)
var/mob_species = pickweight(list(
/datum/species/human = 50,
/datum/species/lizard = 25,
- /datum/species/ethereal = 10,
+ /datum/species/elzuose = 10,
/datum/species/moth = 10,
/datum/species/spider = 3,
/datum/species/fly = 2
@@ -731,6 +731,14 @@ GLOBAL_LIST_EMPTY(knpcs)
//as are bag contents
backpack_contents = list()
+ backpack_contents += pickweight(list( //fallback in case of no weapons
+ /obj/item/wrench = 10,
+ /obj/item/crowbar = 15,
+ /obj/item/screwdriver = 5,
+ /obj/item/wirecutters = 10,
+ /obj/item/scalpel = 5,
+ /obj/item/flashlight/seclite = 10,
+ ))
if(prob(70))
backpack_contents += pickweight(list( //these could stand to be expanded, right now they're just mildly modified miner ones, and I don't know how to plus that up.
/obj/item/soap = 10,
@@ -798,14 +806,14 @@ GLOBAL_LIST_EMPTY(knpcs)
l_pocket = /obj/item/tank/internals/emergency_oxygen/engi
if(survivor_type == "hunter")
- l_pocket = /obj/item/tank/internals/emergency_oxygen/engi
+ l_pocket = /obj/item/tank/internals/emergency_oxygen/double
if (prob(20))
l_pocket = /obj/item/reagent_containers/food/snacks/meat/steak/goliath
if(survivor_type == "gunslinger" || survivor_type == "commando")
if(prob(50))
l_pocket = /obj/item/ammo_box/magazine/skm_545_39
- r_pocket = /obj/item/tank/internals/emergency_oxygen/engi
+ r_pocket = /obj/item/tank/internals/emergency_oxygen/double
else
r_pocket = /obj/item/tank/internals/emergency_oxygen/engi
@@ -885,7 +893,7 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/clothing/head/helmet/marine = 1,
/obj/item/clothing/head/helmet/marine/security = 1,
))
- spare_ammo_count = rand(1,3)
+ spare_ammo_count = rand(2,3)
r_hand = /obj/item/gun/ballistic/automatic/assault/skm/pirate
for(var/i in 1 to spare_ammo_count)
if(prob(5))
@@ -895,9 +903,22 @@ GLOBAL_LIST_EMPTY(knpcs)
else
backpack_contents += /obj/item/ammo_box/magazine/skm_762_40
else
- r_hand = /obj/item/gun/ballistic/automatic/smg/skm_carbine
+ picked = pickweight(list(
+ /obj/item/gun/ballistic/automatic/smg/skm_carbine = 70,
+ /obj/item/gun/ballistic/automatic/smg/mini_uzi = 20,
+ /obj/item/gun/ballistic/automatic/smg/firestorm = 4,
+ /obj/item/gun/ballistic/automatic/smg/vector = 4,
+ /obj/item/gun/ballistic/automatic/smg/cm5 = 4,
+ /obj/item/gun/ballistic/automatic/smg/skm_carbine/inteq = 4,
+ /obj/item/gun/ballistic/automatic/smg/c20r/cobra = 4,
+ /obj/item/gun/ballistic/automatic/smg/proto = 4,
+ /obj/item/gun/ballistic/automatic/pistol/APS = 4,
+ /obj/item/gun/ballistic/automatic/pistol/mauler = 4,
+ ))
+ r_hand = picked
+ var/obj/item/gun/ballistic/current_gun = picked
for(var/i in 1 to spare_ammo_count)
- backpack_contents += /obj/item/ammo_box/magazine/skm_545_39
+ backpack_contents += current_gun.mag_type
internals_slot = ITEM_SLOT_RPOCKET
@@ -1451,13 +1472,21 @@ This is to account for sec Ju-Jitsuing boarding commandos.
if(dist <= 1)
var/proc_fist = TRUE
- if(!istype(A, /obj/item/gun) && H.a_intent == INTENT_HARM)
- if(grab_melee(HA) )
- A = H.get_active_held_item()
- if(A)
- target.attackby(A, H)
- A.afterattack(target, H, TRUE)
- proc_fist = FALSE
+ var/override_gun_instincts = FALSE
+ if(istype(target, /mob/living/simple_animal/hostile/asteroid/basilisk/whitesands))
+ override_gun_instincts = TRUE
+ if((!istype(A, /obj/item/gun) || override_gun_instincts) && H.a_intent == INTENT_HARM)
+ if(!override_gun_instincts)
+ if(grab_melee(HA) )
+ A = H.get_active_held_item()
+ if(A)
+ target.attackby(A, H)
+ A.afterattack(target, H, TRUE)
+ proc_fist = FALSE
+ else
+ target.attackby(A, H)
+ A.afterattack(target, H, TRUE)
+ proc_fist = FALSE
if(proc_fist && !isanimal(target))
H.dna.species.spec_attack_hand(H, target)
From 0dbdf0602f9872c82dc2afad634dd17e32e52f77 Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Tue, 18 Jun 2024 19:15:45 -0700
Subject: [PATCH 5/8] compiles
---
code/modules/mob/living/carbon/human/knpc.dm | 2 --
1 file changed, 2 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index 4b71da59b1ce..de189898881d 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -795,8 +795,6 @@ GLOBAL_LIST_EMPTY(knpcs)
backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
if(prob(5))
backpack_contents += /obj/item/reagent_containers/hypospray/medipen/survival
- if(prob(10)) //carried over from previous
- backpack_contents += /obj/item/crusher_trophy/shiny
//pockets
if(survivor_type == "survivor") //could also use fleshing out
From d77a89d2c51fb5a1731bf916d8201d3283072e52 Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Mon, 9 Sep 2024 00:17:55 -0700
Subject: [PATCH 6/8] update with master
---
code/modules/mob/living/carbon/human/knpc.dm | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index de189898881d..3818b3101748 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -868,7 +868,7 @@ GLOBAL_LIST_EMPTY(knpcs)
r_hand = /obj/item/gun/ballistic/rifle/polymer
spare_ammo_count = rand(2,5)
for(var/i in 1 to spare_ammo_count)
- backpack_contents += /obj/item/ammo_box/aac_300blk_stripper
+ backpack_contents += /obj/item/ammo_box/a762_stripper
if(survivor_type == "gunslinger" || survivor_type == "commando")
if(prob(7) || survivor_type == "commando") //cause fuck you, thats why
@@ -877,6 +877,7 @@ GLOBAL_LIST_EMPTY(knpcs)
suit = pickweight(list(
/obj/item/clothing/suit/armor/vest/bulletproof = 35,
/obj/item/clothing/suit/armor/vest/syndie = 20,
+ /obj/item/clothing/suit/armor/gezena = 20,
/obj/item/clothing/suit/armor/vest/marine = 1,
/obj/item/clothing/suit/armor/vest/marine/heavy = 1,
/obj/item/clothing/suit/armor/vest/marine = 1,
@@ -887,12 +888,16 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/clothing/head/helmet/swat = 20,
/obj/item/clothing/head/helmet/swat/nanotrasen = 20,
+ /obj/item/clothing/head/helmet/gezena = 20,
/obj/item/clothing/head/helmet/marine = 1,
/obj/item/clothing/head/helmet/marine/security = 1,
))
spare_ammo_count = rand(2,3)
- r_hand = /obj/item/gun/ballistic/automatic/assault/skm/pirate
+ if(prob(10))
+ r_hand = /obj/item/gun/ballistic/automatic/hmg/skm_lmg/drum_mag //hell
+ else
+ r_hand = /obj/item/gun/ballistic/automatic/assault/skm/pirate
for(var/i in 1 to spare_ammo_count)
if(prob(5))
backpack_contents += /obj/item/ammo_box/magazine/skm_762_40/drum //die.
@@ -903,13 +908,14 @@ GLOBAL_LIST_EMPTY(knpcs)
else
picked = pickweight(list(
/obj/item/gun/ballistic/automatic/smg/skm_carbine = 70,
- /obj/item/gun/ballistic/automatic/smg/mini_uzi = 20,
+ /obj/item/gun/ballistic/automatic/pistol/spitter = 20,
+ /obj/item/gun/ballistic/automatic/smg/pounder = 4,
/obj/item/gun/ballistic/automatic/smg/firestorm = 4,
/obj/item/gun/ballistic/automatic/smg/vector = 4,
/obj/item/gun/ballistic/automatic/smg/cm5 = 4,
/obj/item/gun/ballistic/automatic/smg/skm_carbine/inteq = 4,
/obj/item/gun/ballistic/automatic/smg/c20r/cobra = 4,
- /obj/item/gun/ballistic/automatic/smg/proto = 4,
+ /obj/item/gun/ballistic/automatic/smg/skm_carbine/inteq/proto = 4,
/obj/item/gun/ballistic/automatic/pistol/APS = 4,
/obj/item/gun/ballistic/automatic/pistol/mauler = 4,
))
@@ -1362,7 +1368,7 @@ This is to account for sec Ju-Jitsuing boarding commandos.
expanded_contents = S.contents + H.contents
var/obj/item/ammo_box/target_mag = locate(B.mag_type) in expanded_contents
if(istype(B, /obj/item/gun/ballistic/rifle/polymer))
- target_mag = locate(/obj/item/ammo_box/aac_300blk_stripper) in expanded_contents
+ target_mag = locate(/obj/item/ammo_box/a762_stripper) in expanded_contents
if(istype(B, /obj/item/gun/ballistic/rifle))
if(!B.bolt_locked)
B.rack(H)
From fa54362268af306de74de7e65f85b0b9afde72ea Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Mon, 9 Sep 2024 00:28:34 -0700
Subject: [PATCH 7/8] compile error
---
code/modules/mob/living/carbon/human/knpc.dm | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index 3818b3101748..5b6672b5fb43 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -271,7 +271,7 @@ DEFINE_BITFIELD(knpc_traits, list(
/// 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
/// 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/list/turfs_to_avoid = list(/turf/open/acid,/turf/open/lava)
+ var/list/turfs_to_avoid = list(/turf/open/water/acid,/turf/open/lava)
/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid, avoid_mobs)
src.caller = caller
@@ -539,7 +539,7 @@ GLOBAL_LIST_EMPTY(knpcs)
var/static/list/climbable = typecacheof(list(/obj/structure/table, /obj/structure/railing)) // climbable structures
var/pathfind_timeout = 0 //If pathfinding fails, it is püt in timeout for a while to avoid spamming the server with pathfinding calls.
var/timeout_stacks = 0 //Consecutive pathfind fails add additional delay stacks to further counteract the effects of knpcs in unreachable locations.
- var/list/turfs_to_avoid = list(/turf/open/acid,/turf/open/lava)
+ var/list/turfs_to_avoid = list(/turf/open/water/acid,/turf/open/lava)
/mob/living/carbon/human/ai_boarder
faction = list("neutral")
From e7c914510ecba4a1c52760b93539a1ca5edebeee Mon Sep 17 00:00:00 2001
From: retlaw34 <58402542+retlaw34@users.noreply.github.com>
Date: Wed, 11 Sep 2024 03:21:36 -0700
Subject: [PATCH 8/8] tming purposes
---
code/modules/mob/living/carbon/human/knpc.dm | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/code/modules/mob/living/carbon/human/knpc.dm b/code/modules/mob/living/carbon/human/knpc.dm
index 5b6672b5fb43..c7bd37b0c98f 100644
--- a/code/modules/mob/living/carbon/human/knpc.dm
+++ b/code/modules/mob/living/carbon/human/knpc.dm
@@ -914,9 +914,9 @@ GLOBAL_LIST_EMPTY(knpcs)
/obj/item/gun/ballistic/automatic/smg/vector = 4,
/obj/item/gun/ballistic/automatic/smg/cm5 = 4,
/obj/item/gun/ballistic/automatic/smg/skm_carbine/inteq = 4,
- /obj/item/gun/ballistic/automatic/smg/c20r/cobra = 4,
+ /obj/item/gun/ballistic/automatic/smg/cobra = 4,
/obj/item/gun/ballistic/automatic/smg/skm_carbine/inteq/proto = 4,
- /obj/item/gun/ballistic/automatic/pistol/APS = 4,
+ /obj/item/gun/ballistic/automatic/pistol/rattlesnake = 4,
/obj/item/gun/ballistic/automatic/pistol/mauler = 4,
))
r_hand = picked