diff --git a/code/__DEFINES/DNA.dm b/code/__DEFINES/DNA.dm index 1d08e1ab4868..da2563e25464 100644 --- a/code/__DEFINES/DNA.dm +++ b/code/__DEFINES/DNA.dm @@ -99,6 +99,7 @@ #define TR_KEEPORGANS (1<<8) #define TR_KEEPSTUNS (1<<9) #define TR_KEEPREAGENTS (1<<10) +#define TR_KEEPAI (1<<11) //species traits for mutantraces #define MUTCOLORS 1 diff --git a/code/__DEFINES/ai/ai.dm b/code/__DEFINES/ai/ai.dm new file mode 100644 index 000000000000..4483119527dd --- /dev/null +++ b/code/__DEFINES/ai/ai.dm @@ -0,0 +1,81 @@ +#define GET_AI_BEHAVIOR(behavior_type) SSai_behaviors.ai_behaviors[behavior_type] +#define HAS_AI_CONTROLLER_TYPE(thing, type) istype(thing?.ai_controller, type) + +#define AI_STATUS_ON 1 +#define AI_STATUS_OFF 2 + + +///Monkey checks +#define SHOULD_RESIST(source) (source.on_fire || source.buckled || HAS_TRAIT(source, TRAIT_RESTRAINED) || (source.pulledby && source.pulledby.grab_state > GRAB_PASSIVE)) +#define IS_DEAD_OR_INCAP(source) (HAS_TRAIT(source, TRAIT_INCAPACITATED) || HAS_TRAIT(source, TRAIT_HANDS_BLOCKED) || IS_IN_STASIS(source)) + +///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on +#define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles + +///Cooldown on planning if planning failed last time +#define AI_FAILED_PLANNING_COOLDOWN 1.5 SECONDS + +///Flags for ai_behavior new() +#define AI_CONTROLLER_INCOMPATIBLE (1<<0) + +///Does this task require movement from the AI before it can be performed? +#define AI_BEHAVIOR_REQUIRE_MOVEMENT (1<<0) +///Does this task let you perform the action while you move closer? (Things like moving and shooting) +#define AI_BEHAVIOR_MOVE_AND_PERFORM (1<<1) + +///Subtree defines + +///This subtree should cancel any further planning, (Including from other subtrees) +#define SUBTREE_RETURN_FINISH_PLANNING 1 + +///Monkey AI controller blackboard keys + +#define BB_MONKEY_AGRESSIVE "BB_monkey_agressive" +#define BB_MONKEY_GUN_NEURONS_ACTIVATED "BB_monkey_gun_aware" +#define BB_MONKEY_GUN_WORKED "BB_monkey_gun_worked" +#define BB_MONKEY_BEST_FORCE_FOUND "BB_monkey_bestforcefound" +#define BB_MONKEY_ENEMIES "BB_monkey_enemies" +#define BB_MONKEY_BLACKLISTITEMS "BB_monkey_blacklistitems" +#define BB_MONKEY_PICKUPTARGET "BB_monkey_pickuptarget" +#define BB_MONKEY_PICKPOCKETING "BB_monkey_pickpocketing" +#define BB_MONKEY_CURRENT_ATTACK_TARGET "BB_monkey_current_attack_target" +#define BB_MONKEY_TARGET_DISPOSAL "BB_monkey_target_disposal" +#define BB_MONKEY_DISPOSING "BB_monkey_disposing" +#define BB_MONKEY_RECRUIT_COOLDOWN "BB_monkey_recruit_cooldown" +#define BB_MONKEY_NEXT_HUNGRY "BB_monkey_next_hungry" + +///Dog AI controller blackboard keys + +#define BB_SIMPLE_CARRY_ITEM "BB_SIMPLE_CARRY_ITEM" +#define BB_FETCH_TARGET "BB_FETCH_TARGET" +#define BB_FETCH_IGNORE_LIST "BB_FETCH_IGNORE_LISTlist" +#define BB_FETCH_DELIVER_TO "BB_FETCH_DELIVER_TO" +#define BB_DOG_FRIENDS "BB_DOG_FRIENDS" +#define BB_DOG_ORDER_MODE "BB_DOG_ORDER_MODE" +#define BB_DOG_PLAYING_DEAD "BB_DOG_PLAYING_DEAD" +#define BB_DOG_HARASS_TARGET "BB_DOG_HARASS_TARGET" + +/// Basically, what is our vision/hearing range for picking up on things to fetch/ +#define AI_DOG_VISION_RANGE 10 +/// What are the odds someone petting us will become our friend? +#define AI_DOG_PET_FRIEND_PROB 15 +/// After this long without having fetched something, we clear our ignore list +#define AI_FETCH_IGNORE_DURATION 30 SECONDS +/// After being ordered to heel, we spend this long chilling out +#define AI_DOG_HEEL_DURATION 20 SECONDS +/// After either being given a verbal order or a pointing order, ignore further of each for this duration +#define AI_DOG_COMMAND_COOLDOWN 2 SECONDS + +// dog command modes (what pointing at something/someone does depending on the last order the dog heard) +/// Don't do anything (will still react to stuff around them though) +#define DOG_COMMAND_NONE 0 +/// Will try to pick up and bring back whatever you point to +#define DOG_COMMAND_FETCH 1 +/// Will get within a few tiles of whatever you point at and continually growl/bark. If the target is a living mob who gets too close, the dog will attack them with bites +#define DOG_COMMAND_ATTACK 2 + +//enumerators for parsing dog command speech +#define COMMAND_HEEL "Heel" +#define COMMAND_FETCH "Fetch" +#define COMMAND_ATTACK "Attack" +#define COMMAND_DIE "Play Dead" diff --git a/code/__DEFINES/chat.dm b/code/__DEFINES/chat.dm index f816c0317f4c..14f0c8829dd9 100644 --- a/code/__DEFINES/chat.dm +++ b/code/__DEFINES/chat.dm @@ -36,4 +36,8 @@ /// Used for debug messages to the server #define debug2_world_log(msg) if (GLOB.Debug2) log_world("DEBUG: [msg]") /// Adds a generic box around whatever message you're sending in chat. Really makes things stand out. -#define examine_block(str) ("
" + str + "
") +#define boxed_message(str) ("
" + str + "
") +/// Adds a box around whatever message you're sending in chat. Can apply color and/or additional classes. Available colors: red, green, blue, purple. Use it like red_box +#define custom_boxed_message(classes, str) ("
" + str + "
") +/// Makes a fieldset with a neaty styled name. Can apply additional classes. +#define fieldset_block(title, content, classes) ("
" + title + "" + content + "
") diff --git a/code/__DEFINES/dcs/signals/signals.dm b/code/__DEFINES/dcs/signals/signals.dm index 630a5f08f4e2..1ee61507f8e1 100644 --- a/code/__DEFINES/dcs/signals/signals.dm +++ b/code/__DEFINES/dcs/signals/signals.dm @@ -26,6 +26,8 @@ #define COMSIG_GLOB_BUTTON_PRESSED "!button_pressed" /// a client (re)connected, after all /client/New() checks have passed : (client/connected_client) #define COMSIG_GLOB_CLIENT_CONNECT "!client_connect" +/// a person somewhere has thrown something : (mob/living/carbon/carbon_thrower, target) +#define COMSIG_GLOB_CARBON_THROW_THING "!throw_thing" // signals from globally accessible objects /// from SSsun when the sun changes position : (azimuth) @@ -215,6 +217,9 @@ ///from base of atom/set_opacity(): (new_opacity) #define COMSIG_ATOM_SET_OPACITY "atom_set_opacity" +///from base of atom/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) +#define COMSIG_ATOM_HITBY "atom_hitby" + /// from base of /atom/movable/proc/on_virtual_z_change(): (new_virtual_z, old_virtual_z) #define COMSIG_ATOM_VIRTUAL_Z_CHANGE "atom_virtual_z_change" @@ -261,6 +266,7 @@ #define COMSIG_CLICK_CTRL "ctrl_click" //from base of atom/AltClick(): (/mob) #define COMSIG_CLICK_ALT "alt_click" + #define COMPONENT_CANCEL_CLICK_ALT (1<<0) //from base of atom/CtrlShiftClick(/mob) #define COMSIG_CLICK_CTRL_SHIFT "ctrl_shift_click" ///from base of atom/CtrlShiftRightClick(/mob) @@ -320,6 +326,8 @@ #define COMPONENT_CANCEL_THROW (1<<0) ///from base of atom/movable/throw_at(): (datum/thrownthing, spin) #define COMSIG_MOVABLE_POST_THROW "movable_post_throw" +///from base of datum/thrownthing/finalize(): (obj/thrown_object, datum/thrownthing) used for when a throw is finished +#define COMSIG_MOVABLE_THROW_LANDED "movable_throw_landed" ///from base of atom/movable/onTransitZ(): (old_z, new_z) #define COMSIG_MOVABLE_Z_CHANGED "movable_ztransit" ///called when the movable is placed in an unaccessible area, used for shiploving: () @@ -425,6 +433,8 @@ #define COMSIG_MOB_ITEM_ATTACK_QDELETED "mob_item_attack_qdeleted" ///from base of mob/RangedAttack(): (atom/A, params) #define COMSIG_MOB_ATTACK_RANGED "mob_attack_ranged" +///From base of mob/update_movespeed():area +#define COMSIG_MOB_MOVESPEED_UPDATED "mob_update_movespeed" ///from base of /mob/throw_item(): (atom/target) #define COMSIG_MOB_THROW "mob_throw" ///from base of /mob/verb/examinate(): (atom/target) @@ -487,6 +497,8 @@ #define COMSIG_LIVING_DROP_LIMB "living_drop_limb" ///from base of mob/living/set_buckled(): (new_buckled) #define COMSIG_LIVING_SET_BUCKLED "living_set_buckled" +///From post-can inject check of syringe after attack (mob/user) +#define COMSIG_LIVING_TRY_SYRINGE "living_try_syringe" ///sent from borg recharge stations: (amount, repairs) #define COMSIG_PROCESS_BORGCHARGER_OCCUPANT "living_charge" diff --git a/code/__DEFINES/monkeys.dm b/code/__DEFINES/monkeys.dm index 8cc0bc11c0a0..dbc2ffb24f4c 100644 --- a/code/__DEFINES/monkeys.dm +++ b/code/__DEFINES/monkeys.dm @@ -1,37 +1,41 @@ //Monkey defines, placed here so they can be read by other things! -//Mode defines -#define MONKEY_IDLE 0 // idle -#define MONKEY_HUNT 1 // found target, hunting -#define MONKEY_FLEE 2 // free from enemies -#define MONKEY_DISPOSE 3 // dump body in disposals - -#define MONKEY_FLEE_HEALTH 50 // below this health value the monkey starts to flee from enemies -#define MONKEY_ENEMY_VISION 9 // how close an enemy must be to trigger aggression -#define MONKEY_FLEE_VISION 4 // how close an enemy must be before it triggers flee -#define MONKEY_ITEM_SNATCH_DELAY 25 // How long does it take the item to be taken from a mobs hand -#define MONKEY_CUFF_RETALIATION_PROB 20 // Probability monkey will aggro when cuffed -#define MONKEY_SYRINGE_RETALIATION_PROB 20 // Probability monkey will aggro when syringed +/// below this health value the monkey starts to flee from enemies +#define MONKEY_FLEE_HEALTH 50 +/// how close an enemy must be to trigger aggression +#define MONKEY_ENEMY_VISION 9 +/// how close an enemy must be before it triggers flee +#define MONKEY_FLEE_VISION 4 +/// How long does it take the item to be taken from a mobs hand +#define MONKEY_ITEM_SNATCH_DELAY 25 +/// Probability monkey will aggro when cuffed +#define MONKEY_CUFF_RETALIATION_PROB 20 +/// Probability monkey will aggro when syringed +#define MONKEY_SYRINGE_RETALIATION_PROB 20 // Probability per Life tick that the monkey will: -#define MONKEY_RESIST_PROB 50 // resist out of restraints -// when the monkey is idle -#define MONKEY_PULL_AGGRO_PROB 5 // aggro against the mob pulling it -#define MONKEY_SHENANIGAN_PROB 5 // chance of getting into mischief, i.e. finding/stealing items -// when the monkey is hunting -#define MONKEY_ATTACK_DISARM_PROB 50 // disarm an armed attacker -#define MONKEY_WEAPON_PROB 20 // if not currently getting an item, search for a weapon around it -#define MONKEY_RECRUIT_PROB 25 // recruit a monkey near it -#define MONKEY_SWITCH_TARGET_PROB 25 // switch targets if it sees another enemy - -#define MONKEY_RETALIATE_HARM_PROB 95 // probability for the monkey to aggro when attacked with harm intent -#define MONKEY_RETALIATE_DISARM_PROB 20 // probability for the monkey to aggro when attacked with disarm intent +/// probability that monkey resist out of restraints +#define MONKEY_RESIST_PROB 50 +/// probability that monkey aggro against the mob pulling it +#define MONKEY_PULL_AGGRO_PROB 5 +/// probability that monkey will get into mischief, i.e. finding/stealing items +#define MONKEY_SHENANIGAN_PROB 20 +/// probability that monkey will disarm an armed attacker +#define MONKEY_ATTACK_DISARM_PROB 50 +/// probability that monkey will get recruited when friend is attacked +#define MONKEY_RECRUIT_PROB 25 -#define MONKEY_HATRED_AMOUNT 4 // amount of aggro to add to an enemy when they attack user -#define MONKEY_HATRED_REDUCTION_PROB 25 // probability of reducing aggro by one when the monkey attacks +/// probability for the monkey to aggro when attacked with harm intent +#define MONKEY_RETALIATE_HARM_PROB 95 +/// probability for the monkey to aggro when attacked with disarm intent +#define MONKEY_RETALIATE_DISARM_PROB 20 -// how many Life ticks the monkey will fail to: -#define MONKEY_HUNT_FRUSTRATION_LIMIT 8 // Chase after an enemy before giving up -#define MONKEY_DISPOSE_FRUSTRATION_LIMIT 16 // Dispose of a body before giving up +/// amount of aggro to add to an enemy when they attack user +#define MONKEY_HATRED_AMOUNT 4 +/// amount of aggro to add to an enemy when a monkey is recruited +#define MONKEY_RECRUIT_HATED_AMOUNT 2 +/// probability of reducing aggro by one when the monkey attacks +#define MONKEY_HATRED_REDUCTION_PROB 20 -#define MONKEY_AGGRESSIVE_MVM_PROB 0 // If you mass edit monkies to be aggressive. there is a small chance of in-fighting +///Monkey recruit cooldown +#define MONKEY_RECRUIT_COOLDOWN 1 MINUTES diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 2a0d5e650387..7516c99313ee 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -120,6 +120,8 @@ #define INIT_ORDER_EVENTS 70 #define INIT_ORDER_JOBS 65 #define INIT_ORDER_QUIRKS 60 +#define INIT_ORDER_AI_MOVEMENT 57 //We need the movement setup +#define INIT_ORDER_AI_CONTROLLERS 56 //So the controller can get the ref #define INIT_ORDER_TICKER 55 #define INIT_ORDER_FACTION 53 #define INIT_ORDER_MAPPING 50 @@ -164,6 +166,8 @@ #define FIRE_PRIORITY_WET_FLOORS 20 #define FIRE_PRIORITY_AIR 20 #define FIRE_PRIORITY_NPC 20 +#define FIRE_PRIORITY_NPC_MOVEMENT 21 +#define FIRE_PRIORITY_NPC_ACTIONS 22 #define FIRE_PRIORITY_PROCESS 25 #define FIRE_PRIORITY_THROWING 25 #define FIRE_PRIORITY_SPACEDRIFT 30 diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm index 3dce892eb868..48691840688a 100644 --- a/code/__DEFINES/traits.dm +++ b/code/__DEFINES/traits.dm @@ -155,6 +155,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_PACIFISM "pacifism" #define TRAIT_IGNORESLOWDOWN "ignoreslow" #define TRAIT_IGNOREDAMAGESLOWDOWN "ignoredamageslowdown" +/// Makes it so the mob can use guns regardless of tool user status +#define TRAIT_GUN_NATURAL "gunnatural" #define TRAIT_DEATHCOMA "deathcoma" //Causes death-like unconsciousness #define TRAIT_FAKEDEATH "fakedeath" //Makes the owner appear as dead to most forms of medical examination #define TRAIT_DISFIGURED "disfigured" diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index e52d9af56251..3207ca3bdb58 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -22,6 +22,7 @@ #define VV_BITFIELD "Bitfield" #define VV_TEXT_LOCATE "Custom Reference Locate" #define VV_PROCCALL_RETVAL "Return Value of Proccall" +#define VV_HK_ADD_AI "add_ai" #define VV_MSG_MARKED "
Marked Object" #define VV_MSG_EDITED "
Var Edited" diff --git a/code/__HELPERS/AStar.dm b/code/__HELPERS/AStar.dm deleted file mode 100644 index 0e0de2a95326..000000000000 --- a/code/__HELPERS/AStar.dm +++ /dev/null @@ -1,212 +0,0 @@ -/* -A Star pathfinding algorithm -Returns a list of tiles forming a path from A to B, taking dense objects as well as walls, and the orientation of -windows along the route into account. -Use: -your_list = AStar(start location, end location, moving atom, distance proc, max nodes, maximum node depth, minimum distance to target, adjacent proc, atom id, turfs to exclude, check only simulated) - -Optional extras to add on (in order): -Distance proc : the distance used in every A* calculation (length of path and heuristic) -MaxNodes: The maximum number of nodes the returned path can be (0 = infinite) -Maxnodedepth: The maximum number of nodes to search (default: 30, 0 = infinite) -Mintargetdist: 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. -Adjacent proc : returns the turfs to consider around the actually processed node -Simulated only : whether to consider unsimulated turfs or not (used by some Adjacent proc) - -Also added 'exclude' turf to avoid travelling over; defaults to null - -Actual Adjacent procs : - - /turf/proc/reachableAdjacentTurfs : returns reachable turfs in cardinal directions (uses simulated_only) - - /turf/proc/reachableAdjacentAtmosTurfs : returns turfs in cardinal directions reachable via atmos - -*/ -#define PF_TIEBREAKER 0.005 -//tiebreker weight.To help to choose between equal paths -////////////////////// -//datum/PathNode object -////////////////////// -#define MASK_ODD 85 -#define MASK_EVEN 170 - - -//A* nodes variables -/datum/PathNode - var/turf/source //turf associated with the PathNode - var/datum/PathNode/prevNode //link to the parent PathNode - var/f //A* Node weight (f = g + h) - var/g //A* movement cost variable - var/h //A* heuristic variable - var/nt //count the number of Nodes traversed - var/bf //bitflag for dir to expand.Some sufficiently advanced motherfuckery - -/datum/PathNode/New(s,p,pg,ph,pnt,_bf) - source = s - prevNode = p - g = pg - h = ph - f = g + h*(1+ PF_TIEBREAKER) - nt = pnt - bf = _bf - -/datum/PathNode/proc/setp(p,pg,ph,pnt) - prevNode = p - g = pg - h = ph - f = g + h*(1+ PF_TIEBREAKER) - nt = pnt - -/datum/PathNode/proc/calc_f() - f = g + h - -////////////////////// -//A* procs -////////////////////// - -//the weighting function, used in the A* algorithm -/proc/PathWeightCompare(datum/PathNode/a, datum/PathNode/b) - return a.f - b.f - -//reversed so that the Heap is a MinHeap rather than a MaxHeap -/proc/HeapPathWeightCompare(datum/PathNode/a, datum/PathNode/b) - return b.f - a.f - -//wrapper that returns an empty list if A* failed to find a path -/proc/get_path_to(caller, end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableTurftest, id=null, turf/exclude=null, simulated_only = TRUE) - var/l = SSpathfinder.mobs.getfree(caller) - while(!l) - stoplag(3) - l = SSpathfinder.mobs.getfree(caller) - var/list/path = AStar(caller, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent,id, exclude, simulated_only) - - SSpathfinder.mobs.found(l) - if(!path) - path = list() - return path - -/proc/cir_get_path_to(caller, end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableTurftest, id=null, turf/exclude=null, simulated_only = TRUE) - var/l = SSpathfinder.circuits.getfree(caller) - while(!l) - stoplag(3) - l = SSpathfinder.circuits.getfree(caller) - var/list/path = AStar(caller, end, dist, maxnodes, maxnodedepth, mintargetdist, adjacent,id, exclude, simulated_only) - SSpathfinder.circuits.found(l) - if(!path) - path = list() - return path - -/proc/AStar(caller, _end, dist, maxnodes, maxnodedepth = 30, mintargetdist, adjacent = /turf/proc/reachableTurftest, id=null, turf/exclude=null, simulated_only = TRUE) - //sanitation - var/turf/end = get_turf(_end) - var/turf/start = get_turf(caller) - if(!start || !end) - stack_trace("Invalid A* start or destination") - return FALSE - if(start.virtual_z != end.virtual_z || start == end) //no pathfinding between z levels - return FALSE - if(maxnodes) - //if start turf is farther than maxnodes from end turf, no need to do anything - if(call(start, dist)(end) > maxnodes) - return FALSE - maxnodedepth = maxnodes //no need to consider path longer than maxnodes - var/datum/Heap/open = new /datum/Heap(/proc/HeapPathWeightCompare) //the open list - var/list/openc = new() //open list for node check - var/list/path = null //the returned path, if any - //initialization - var/datum/PathNode/cur = new /datum/PathNode(start,null,0,call(start,dist)(end),0,15,1)//current processed turf - open.Insert(cur) - openc[start] = cur - //then run the main loop - while(!open.IsEmpty() && !path) - cur = open.Pop() //get the lower f turf in the open list - //get the lower f node on the open list - //if we only want to get near the target, check if we're close enough - var/closeenough - if(mintargetdist) - closeenough = call(cur.source,dist)(end) <= mintargetdist - - - //found the target turf (or close enough), let's create the path to it - if(cur.source == end || closeenough) - path = new() - path.Add(cur.source) - while(cur.prevNode) - cur = cur.prevNode - path.Add(cur.source) - break - //get adjacents turfs using the adjacent proc, checking for access with id - if((!maxnodedepth)||(cur.nt <= maxnodedepth))//if too many steps, don't process that path - for(var/i = 0 to 3) - var/f= 1<>1) //getting reverse direction throught swapping even and odd bits.((f & 01010101)<<1)|((f & 10101010)>>1) - var/newg = cur.g + call(cur.source,dist)(T) - if(CN) - //is already in open list, check if it's a better way from the current turf - CN.bf &= 15^r //we have no closed, so just cut off exceed dir.00001111 ^ reverse_dir.We don't need to expand to checked turf. - if((newg < CN.g)) - if(call(cur.source,adjacent)(caller, T, id, simulated_only)) - CN.setp(cur,newg,CN.h,cur.nt+1) - open.ReSort(CN)//reorder the changed element in the list - else - //is not already in open list, so add it - if(call(cur.source,adjacent)(caller, T, id, simulated_only)) - CN = new(T,cur,newg,call(T,dist)(end),cur.nt+1,15^r) - open.Insert(CN) - openc[T] = CN - cur.bf = 0 - CHECK_TICK - //reverse the path to get it from start to finish - if(path) - for(var/i = 1 to round(0.5*path.len)) - path.Swap(i,path.len-i+1) - openc = null - //cleaning after us - return path - -//Returns adjacent turfs in cardinal directions that are reachable -//simulated_only controls whether only simulated turfs are considered or not - -/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only) - var/list/L = new() - var/turf/T - var/static/space_type_cache = typecacheof(/turf/open/space) - - for(var/k in 1 to GLOB.cardinals.len) - T = get_step(src,GLOB.cardinals[k]) - if(!T || (simulated_only && space_type_cache[T.type])) - continue - if(!T.density && !LinkBlockedWithAccess(T,caller, ID)) - L.Add(T) - return L - -/turf/proc/reachableTurftest(caller, turf/T, ID, simulated_only) - if(T && !T.density && !(simulated_only && SSpathfinder.space_type_cache[T.type]) && !LinkBlockedWithAccess(T,caller, ID)) - return TRUE - -//Returns adjacent turfs in cardinal directions that are reachable via atmos -/turf/proc/reachableAdjacentAtmosTurfs() - return atmos_adjacent_turfs - -/turf/proc/LinkBlockedWithAccess(turf/T, caller, ID) - var/adir = get_dir(src, T) - var/rdir = ((adir & MASK_ODD)<<1)|((adir & MASK_EVEN)>>1) - for(var/obj/structure/window/W in src) - if(!W.CanAStarPass(ID, adir)) - return TRUE - for(var/obj/machinery/door/window/W in src) - if(!W.CanAStarPass(ID, adir)) - return TRUE - for(var/obj/O in T) - if(!O.CanAStarPass(ID, rdir, caller)) - return TRUE - for(var/obj/machinery/door/firedoor/border_only/W in src) - if(!W.CanAStarPass(ID, adir, caller)) - return TRUE - - return FALSE diff --git a/code/__HELPERS/heap.dm b/code/__HELPERS/heap.dm index 1e369fd7e181..82ef9011bd09 100644 --- a/code/__HELPERS/heap.dm +++ b/code/__HELPERS/heap.dm @@ -1,39 +1,45 @@ ////////////////////// -//datum/Heap object +//datum/heap object ////////////////////// -/datum/Heap +/datum/heap var/list/L var/cmp -/datum/Heap/New(compare) +/datum/heap/New(compare) L = new() cmp = compare -/datum/Heap/proc/IsEmpty() - return !L.len +/datum/heap/Destroy(force, ...) + for(var/i in L) // because this is before the list helpers are loaded + qdel(i) + L = null + return ..() + +/datum/heap/proc/is_empty() + return !length(L) //Insert and place at its position a new node in the heap -/datum/Heap/proc/Insert(atom/A) +/datum/heap/proc/insert(atom/A) L.Add(A) - Swim(L.len) + swim(L.len) //removes and returns the first element of the heap //(i.e the max or the min dependant on the comparison function) -/datum/Heap/proc/Pop() - if(!L.len) +/datum/heap/proc/pop() + if(!length(L)) return 0 . = L[1] - L[1] = L[L.len] - L.Cut(L.len) - if(L.len) - Sink(1) + L[1] = L[length(L)] + L.Cut(length(L)) + if(length(L)) + sink(1) //Get a node up to its right position in the heap -/datum/Heap/proc/Swim(index) +/datum/heap/proc/swim(index) var/parent = round(index * 0.5) while(parent > 0 && (call(cmp)(L[index],L[parent]) > 0)) @@ -42,17 +48,17 @@ parent = round(index * 0.5) //Get a node down to its right position in the heap -/datum/Heap/proc/Sink(index) - var/g_child = GetGreaterChild(index) +/datum/heap/proc/sink(index) + var/g_child = get_greater_child(index) while(g_child > 0 && (call(cmp)(L[index],L[g_child]) < 0)) L.Swap(index,g_child) index = g_child - g_child = GetGreaterChild(index) + g_child = get_greater_child(index) //Returns the greater (relative to the comparison proc) of a node children //or 0 if there's no child -/datum/Heap/proc/GetGreaterChild(index) +/datum/heap/proc/get_greater_child(index) if(index * 2 > L.len) return 0 @@ -65,12 +71,12 @@ return index * 2 //Replaces a given node so it verify the heap condition -/datum/Heap/proc/ReSort(atom/A) +/datum/heap/proc/resort(atom/A) var/index = L.Find(A) - Swim(index) - Sink(index) + swim(index) + sink(index) -/datum/Heap/proc/List() +/datum/heap/proc/List() . = L.Copy() diff --git a/code/__HELPERS/path.dm b/code/__HELPERS/path.dm new file mode 100644 index 000000000000..dc9231c0c93b --- /dev/null +++ b/code/__HELPERS/path.dm @@ -0,0 +1,347 @@ +/** + * This file contains the stuff you need for using JPS (Jump Point Search) pathing, an alternative to A* that skips + * over large numbers of uninteresting tiles resulting in much quicker pathfinding solutions. Mind that diagonals + * cost the same as cardinal moves currently, so paths may look a bit strange, but should still be optimal. + */ + +/** + * This is the proc you use whenever you want to have pathfinding more complex than "try stepping towards the thing" + * + * Arguments: + * * caller: The movable atom that's trying to find the path + * * end: What we're trying to path to. It doesn't matter if this is a turf or some other atom, we're gonna just path to the turf it's on anyway + * * max_distance: The maximum number of steps we can take in a given path to search (default: 30, 0 = infinite) + * * mintargetdistance: Minimum distance to the target before path returns, could be used to get near a target, but not right to it - for an AI mob with a gun, for example. + * * id: An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant + * * simulated_only: Whether we consider turfs without atmos simulation (AKA do we want to ignore space) + * * exclude: If we want to avoid a specific turf, like if we're a mulebot who already got blocked by some turf + */ +/proc/get_path_to(caller, end, max_distance = 30, mintargetdist, id=null, simulated_only = TRUE, turf/exclude) + if(!caller || !get_turf(end)) + return + + var/l = SSpathfinder.mobs.getfree(caller) + while(!l) + stoplag(3) + l = SSpathfinder.mobs.getfree(caller) + + var/list/path + var/datum/pathfind/pathfind_datum = new(caller, end, id, max_distance, mintargetdist, simulated_only, exclude) + path = pathfind_datum.search() + qdel(pathfind_datum) + + SSpathfinder.mobs.found(l) + return path + +/** + * A helper macro to see if it's possible to step from the first turf into the second one, minding things like door access and directional windows. + * Note that this can only be used inside the [datum/pathfind][pathfind datum] since it uses variables from said datum + * If you really want to optimize things, optimize this, cuz this gets called a lot + */ +#define CAN_STEP(cur_turf, next) (next && !next.density && cur_turf.Adjacent(next) && !(simulated_only && SSpathfinder.space_type_cache[next.type]) && !cur_turf.LinkBlockedWithAccess(next,caller, id) && (next != avoid)) +/// Another helper macro for JPS, for telling when a node has forced neighbors that need expanding +#define STEP_NOT_HERE_BUT_THERE(cur_turf, dirA, dirB) ((!CAN_STEP(cur_turf, get_step(cur_turf, dirA)) && CAN_STEP(cur_turf, get_step(cur_turf, dirB)))) + +/// The JPS Node datum represents a turf that we find interesting enough to add to the open list and possibly search for new tiles from +/datum/jps_node + /// The turf associated with this node + var/turf/tile + /// The node we just came from + var/datum/jps_node/previous_node + /// The A* node weight (f_value = number_of_tiles + heuristic) + var/f_value + /// The A* node heuristic (a rough estimate of how far we are from the goal) + var/heuristic + /// How many steps it's taken to get here from the start (currently pulling double duty as steps taken & cost to get here, since all moves incl diagonals cost 1 rn) + var/number_tiles + /// How many steps it took to get here from the last node + var/jumps + /// Nodes store the endgoal so they can process their heuristic without a reference to the pathfind datum + var/turf/node_goal + +/datum/jps_node/New(turf/our_tile, datum/jps_node/incoming_previous_node, jumps_taken, turf/incoming_goal) + tile = our_tile + jumps = jumps_taken + if(incoming_goal) // if we have the goal argument, this must be the first/starting node + node_goal = incoming_goal + else if(incoming_previous_node) // if we have the parent, this is from a direct lateral/diagonal scan, we can fill it all out now + previous_node = incoming_previous_node + number_tiles = previous_node.number_tiles + jumps + node_goal = previous_node.node_goal + heuristic = get_dist(tile, node_goal) + f_value = number_tiles + heuristic + // otherwise, no parent node means this is from a subscan lateral scan, so we just need the tile for now until we call [datum/jps/proc/update_parent] on it + +/datum/jps_node/Destroy(force, ...) + previous_node = null + return ..() + +/datum/jps_node/proc/update_parent(datum/jps_node/new_parent) + previous_node = new_parent + node_goal = previous_node.node_goal + jumps = get_dist(tile, previous_node.tile) + number_tiles = previous_node.number_tiles + jumps + heuristic = get_dist(tile, node_goal) + f_value = number_tiles + heuristic + +/// TODO: Macro this to reduce proc overhead +/proc/HeapPathWeightCompare(datum/jps_node/a, datum/jps_node/b) + return b.f_value - a.f_value + +/// The datum used to handle the JPS pathfinding, completely self-contained +/datum/pathfind + /// The thing that we're actually trying to path for + var/atom/movable/caller + /// The turf where we started at + var/turf/start + /// The turf we're trying to path to (note that this won't track a moving target) + var/turf/end + /// The open list/stack we pop nodes out from (TODO: make this a normal list and macro-ize the heap operations to reduce proc overhead) + var/datum/heap/open + ///An assoc list that serves as the closed list & tracks what turfs came from where. Key is the turf, and the value is what turf it came from + var/list/sources + /// The list we compile at the end if successful to pass back + var/list/path + + // general pathfinding vars/args + /// An ID card representing what access we have and what doors we can open. Its location relative to the pathing atom is irrelevant + var/obj/item/card/id/id + /// How far away we have to get to the end target before we can call it quits + var/mintargetdist = 0 + /// I don't know what this does vs , but they limit how far we can search before giving up on a path + var/max_distance = 30 + /// Space is big and empty, if this is TRUE then we ignore pathing through unsimulated tiles + var/simulated_only + /// A specific turf we're avoiding, like if a mulebot is being blocked by someone t-posing in a doorway we're trying to get through + var/turf/avoid + +/datum/pathfind/New(atom/movable/caller, atom/goal, id, max_distance, mintargetdist, simulated_only, avoid) + src.caller = caller + end = get_turf(goal) + open = new /datum/heap(/proc/HeapPathWeightCompare) + sources = new() + src.id = id + src.max_distance = max_distance + src.mintargetdist = mintargetdist + src.simulated_only = simulated_only + src.avoid = avoid + +/// The proc you use to run the search, returns a list with the steps to the destination if one is available, or nothing if one couldn't be found +/datum/pathfind/proc/search() + start = get_turf(caller) + if(!start || !end) + stack_trace("Invalid A* start or destination") + return FALSE + if(start.z != end.z || start == end) //no pathfinding between z levels + return FALSE + if(max_distance && (max_distance < get_dist(start, end))) //if start turf is farther than max_distance from end turf, no need to do anything + return FALSE + + //initialization + var/datum/jps_node/current_processed_node = new (start, -1, 0, end) + open.insert(current_processed_node) + sources[start] = start // i'm sure this is fine + + //then run the main loop + while(!open.is_empty() && !path) + if(!caller) + return + current_processed_node = open.pop() //get the lower f_value turf in the open list + if(max_distance && (current_processed_node.number_tiles > max_distance))//if too many steps, don't process that path + continue + + var/turf/current_turf = current_processed_node.tile + for(var/scan_direction in list(EAST, WEST, NORTH, SOUTH)) + lateral_scan_spec(current_turf, scan_direction, current_processed_node) + + for(var/scan_direction in list(NORTHEAST, SOUTHEAST, NORTHWEST, SOUTHWEST)) + diag_scan_spec(current_turf, scan_direction, current_processed_node) + + CHECK_TICK + + //we're done! reverse the path to get it from start to finish + if(path) + for(var/i = 1 to round(0.5 * length(path))) + path.Swap(i, length(path) - i + 1) + sources = null + qdel(open) + return path + +/// Called when we've hit the goal with the node that represents the last tile, then sets the path var to that path so it can be returned by [datum/pathfind/proc/search] +/datum/pathfind/proc/unwind_path(datum/jps_node/unwind_node) + path = new() + var/turf/iter_turf = unwind_node.tile + path.Add(iter_turf) + + while(unwind_node.previous_node) + var/dir_goal = get_dir(iter_turf, unwind_node.previous_node.tile) + for(var/i = 1 to unwind_node.jumps) + iter_turf = get_step(iter_turf,dir_goal) + path.Add(iter_turf) + unwind_node = unwind_node.previous_node + +/** + * For performing lateral scans from a given starting turf. + * + * These scans are called from both the main search loop, as well as subscans for diagonal scans, and they treat finding interesting turfs slightly differently. + * If we're doing a normal lateral scan, we already have a parent node supplied, so we just create the new node and immediately insert it into the heap, ezpz. + * If we're part of a subscan, we still need for the diagonal scan to generate a parent node, so we return a node datum with just the turf and let the diag scan + * proc handle transferring the values and inserting them into the heap. + * + * Arguments: + * * original_turf: What turf did we start this scan at? + * * heading: What direction are we going in? Obviously, should be cardinal + * * parent_node: Only given for normal lateral scans, if we don't have one, we're a diagonal subscan. +*/ +/datum/pathfind/proc/lateral_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node) + var/steps_taken = 0 + + var/turf/current_turf = original_turf + var/turf/lag_turf = original_turf + + while(TRUE) + if(path) + return + lag_turf = current_turf + current_turf = get_step(current_turf, heading) + steps_taken++ + if(!CAN_STEP(lag_turf, current_turf)) + return + + if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist))) + var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken) + sources[current_turf] = original_turf + if(parent_node) // if this is a direct lateral scan we can wrap up, if it's a subscan from a diag, we need to let the diag make their node first, then finish + unwind_path(final_node) + return final_node + else if(sources[current_turf]) // already visited, essentially in the closed list + return + else + sources[current_turf] = original_turf + + if(parent_node && parent_node.number_tiles + steps_taken > max_distance) + return + + var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list? + + switch(heading) + if(NORTH) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST)) + interesting = TRUE + if(SOUTH) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST)) + interesting = TRUE + if(EAST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST)) + interesting = TRUE + if(WEST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST)) + interesting = TRUE + + if(interesting) + var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken) + if(parent_node) // if we're a diagonal subscan, we'll handle adding ourselves to the heap in the diag + open.insert(newnode) + return newnode + +/** + * For performing diagonal scans from a given starting turf. + * + * Unlike lateral scans, these only are called from the main search loop, so we don't need to worry about returning anything, + * though we do need to handle the return values of our lateral subscans of course. + * + * Arguments: + * * original_turf: What turf did we start this scan at? + * * heading: What direction are we going in? Obviously, should be diagonal + * * parent_node: We should always have a parent node for diagonals +*/ +/datum/pathfind/proc/diag_scan_spec(turf/original_turf, heading, datum/jps_node/parent_node) + var/steps_taken = 0 + var/turf/current_turf = original_turf + var/turf/lag_turf = original_turf + + while(TRUE) + if(path) + return + lag_turf = current_turf + current_turf = get_step(current_turf, heading) + steps_taken++ + if(!CAN_STEP(lag_turf, current_turf)) + return + + if(current_turf == end || (mintargetdist && (get_dist(current_turf, end) <= mintargetdist))) + var/datum/jps_node/final_node = new(current_turf, parent_node, steps_taken) + sources[current_turf] = original_turf + unwind_path(final_node) + return + else if(sources[current_turf]) // already visited, essentially in the closed list + return + else + sources[current_turf] = original_turf + + if(parent_node.number_tiles + steps_taken > max_distance) + return + + var/interesting = FALSE // have we found a forced neighbor that would make us add this turf to the open list? + var/datum/jps_node/possible_child_node // otherwise, did one of our lateral subscans turn up something? + + switch(heading) + if(NORTHWEST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, NORTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHWEST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, WEST) || lateral_scan_spec(current_turf, NORTH)) + if(NORTHEAST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, NORTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, SOUTH, SOUTHEAST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, EAST) || lateral_scan_spec(current_turf, NORTH)) + if(SOUTHWEST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, EAST, SOUTHEAST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHWEST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, WEST)) + if(SOUTHEAST) + if(STEP_NOT_HERE_BUT_THERE(current_turf, WEST, SOUTHWEST) || STEP_NOT_HERE_BUT_THERE(current_turf, NORTH, NORTHEAST)) + interesting = TRUE + else + possible_child_node = (lateral_scan_spec(current_turf, SOUTH) || lateral_scan_spec(current_turf, EAST)) + + if(interesting || possible_child_node) + var/datum/jps_node/newnode = new(current_turf, parent_node, steps_taken) + open.insert(newnode) + if(possible_child_node) + possible_child_node.update_parent(newnode) + open.insert(possible_child_node) + if(possible_child_node.tile == end || (mintargetdist && (get_dist(possible_child_node.tile, end) <= mintargetdist))) + unwind_path(possible_child_node) + return + +/** + * For seeing if we can actually move between 2 given turfs while accounting for our access and the caller's pass_flags + * + * Arguments: + * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach + * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf + * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? +*/ +/turf/proc/LinkBlockedWithAccess(turf/destination_turf, caller, ID) + var/actual_dir = get_dir(src, destination_turf) + + for(var/obj/structure/window/iter_window in src) + if(!iter_window.CanAStarPass(ID, actual_dir)) + return TRUE + + for(var/obj/machinery/door/window/iter_windoor in src) + if(!iter_windoor.CanAStarPass(ID, actual_dir)) + return TRUE + + var/reverse_dir = get_dir(destination_turf, src) + for(var/obj/iter_object in destination_turf) + if(!iter_object.CanAStarPass(ID, reverse_dir, caller)) + return TRUE + + return FALSE + +#undef CAN_STEP +#undef STEP_NOT_HERE_BUT_THERE diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm index 1e76dd756290..fce77475bde0 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -468,6 +468,8 @@ Turf and target are separate in case you want to teleport some distance from a t /proc/can_see(atom/source, atom/target, length=5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate. var/turf/current = get_turf(source) var/turf/target_turf = get_turf(target) + if(get_dist(source, target) > length) + return FALSE var/steps = 1 if(current != target_turf) current = get_step_towards(current, target_turf) diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index fb00d8bdf283..f8140f71e978 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -8,6 +8,12 @@ GLOBAL_PROTECT(mentors) GLOBAL_LIST_EMPTY_TYPED(directory, /client) //all ckeys with associated client GLOBAL_LIST_EMPTY(stealthminID) //reference list with IDs that store ckeys, for stealthmins +GLOBAL_LIST_INIT(dangerous_turfs, typecacheof(list( + /turf/open/lava, + /turf/open/chasm, + /turf/open/space, + /turf/open/openspace))) + //Since it didn't really belong in any other category, I'm putting this here //This is for procs to replace all the goddamn 'in world's that are chilling around the code diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index c387afaace70..475ec8a10c26 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -345,11 +345,12 @@ A.AltClick(src) /atom/proc/AltClick(mob/user) - var/result = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) + . = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) + if(. & COMPONENT_CANCEL_CLICK_ALT) + return var/turf/T = get_turf(src) if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T)) user.set_listed_turf(T) - return result /// Use this instead of [/mob/proc/AltClickOn] where you only want turf content listing without additional atom alt-click interaction /atom/proc/AltClickNoInteract(mob/user, atom/A) diff --git a/code/controllers/subsystem/ai_controllers.dm b/code/controllers/subsystem/ai_controllers.dm new file mode 100644 index 000000000000..5319d7316fb9 --- /dev/null +++ b/code/controllers/subsystem/ai_controllers.dm @@ -0,0 +1,33 @@ +/// The subsystem used to tick [/datum/ai_controllers] instances. Handling the re-checking of plans. +SUBSYSTEM_DEF(ai_controllers) + name = "AI Controller Ticker" + flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_NPC + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + init_order = INIT_ORDER_AI_CONTROLLERS + wait = 0.5 SECONDS //Plan every half second if required, not great not terrible. + + ///List of all ai_subtree singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_subtrees() + var/list/ai_subtrees = list() + ///List of all ai controllers currently running + var/list/active_ai_controllers = list() + +/datum/controller/subsystem/ai_controllers/Initialize(timeofday) + setup_subtrees() + return ..() + +/datum/controller/subsystem/ai_controllers/proc/setup_subtrees() + ai_subtrees = list() + for(var/subtree_type in subtypesof(/datum/ai_planning_subtree)) + var/datum/ai_planning_subtree/subtree = new subtree_type + ai_subtrees[subtree_type] = subtree + +/datum/controller/subsystem/ai_controllers/fire(resumed) + for(var/datum/ai_controller/ai_controller as anything in active_ai_controllers) + if(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown)) + continue + + if(!LAZYLEN(ai_controller.current_behaviors)) + ai_controller.SelectBehaviors(wait * 0.1) + if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan + COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN) diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm index 21ee7ea60b3c..12ed31d0af7f 100644 --- a/code/controllers/subsystem/pathfinder.dm +++ b/code/controllers/subsystem/pathfinder.dm @@ -3,13 +3,11 @@ SUBSYSTEM_DEF(pathfinder) init_order = INIT_ORDER_PATH flags = SS_NO_FIRE var/datum/flowcache/mobs - var/datum/flowcache/circuits var/static/space_type_cache /datum/controller/subsystem/pathfinder/Initialize() space_type_cache = typecacheof(/turf/open/space) mobs = new(10) - circuits = new(3) return ..() /datum/flowcache diff --git a/code/controllers/subsystem/processing/ai_behaviors.dm b/code/controllers/subsystem/processing/ai_behaviors.dm new file mode 100644 index 000000000000..4c98567405cc --- /dev/null +++ b/code/controllers/subsystem/processing/ai_behaviors.dm @@ -0,0 +1,20 @@ +/// The subsystem used to tick [/datum/ai_behavior] instances. Handling the individual actions an AI can take like punching someone in the fucking NUTS +PROCESSING_SUBSYSTEM_DEF(ai_behaviors) + name = "AI Behavior Ticker" + flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_NPC_ACTIONS + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + init_order = INIT_ORDER_AI_CONTROLLERS + wait = 1 + ///List of all ai_behavior singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors() + var/list/ai_behaviors + +/datum/controller/subsystem/processing/ai_behaviors/Initialize(timeofday) + SetupAIBehaviors() + return ..() + +/datum/controller/subsystem/processing/ai_behaviors/proc/SetupAIBehaviors() + ai_behaviors = list() + for(var/behavior_type in subtypesof(/datum/ai_behavior)) + var/datum/ai_behavior/ai_behavior = new behavior_type + ai_behaviors[behavior_type] = ai_behavior diff --git a/code/controllers/subsystem/processing/ai_movement.dm b/code/controllers/subsystem/processing/ai_movement.dm new file mode 100644 index 000000000000..6a6d64548ca7 --- /dev/null +++ b/code/controllers/subsystem/processing/ai_movement.dm @@ -0,0 +1,21 @@ +/// The subsystem used to tick [/datum/ai_movement] instances. Handling the movement of individual AI instances +PROCESSING_SUBSYSTEM_DEF(ai_movement) + name = "AI movement" + flags = SS_KEEP_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_NPC_MOVEMENT + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + init_order = INIT_ORDER_AI_MOVEMENT + wait = 1 + + ///an assoc list of all ai_movement types. Assoc type to instance + var/list/movement_types + +/datum/controller/subsystem/processing/ai_movement/Initialize(timeofday) + SetupAIMovementInstances() + return ..() + +/datum/controller/subsystem/processing/ai_movement/proc/SetupAIMovementInstances() + movement_types = list() + for(var/key as anything in subtypesof(/datum/ai_movement)) + var/datum/ai_movement/ai_movement = new key + movement_types[key] = ai_movement diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm index b4ad1d56df7e..c4dc415d0080 100644 --- a/code/controllers/subsystem/processing/processing.dm +++ b/code/controllers/subsystem/processing/processing.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(processing) name = "Processing" priority = FIRE_PRIORITY_PROCESS flags = SS_BACKGROUND|SS_POST_FIRE_TIMING|SS_NO_INIT - wait = 10 + wait = 1 SECONDS var/stat_tag = "P" //Used for logging var/list/processing = list() @@ -31,12 +31,12 @@ SUBSYSTEM_DEF(processing) current_run.len-- if(QDELETED(thing)) processing -= thing - else if(thing.process(wait) == PROCESS_KILL) + else if(thing.process(wait * 0.1) == PROCESS_KILL) // fully stop so that a future START_PROCESSING will work STOP_PROCESSING(src, thing) if (MC_TICK_CHECK) return -/datum/proc/process() +/datum/proc/process(delta_time) set waitfor = 0 return PROCESS_KILL diff --git a/code/controllers/subsystem/throwing.dm b/code/controllers/subsystem/throwing.dm index b64dab12d301..3d78d5871779 100644 --- a/code/controllers/subsystem/throwing.dm +++ b/code/controllers/subsystem/throwing.dm @@ -207,4 +207,7 @@ SUBSYSTEM_DEF(throwing) if(T && thrownthing.has_gravity(T)) T.zFall(thrownthing) + if(thrownthing) + SEND_SIGNAL(thrownthing, COMSIG_MOVABLE_THROW_LANDED, src) + qdel(src) diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 2b3ac0619c5a..3961a854cf15 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -351,7 +351,7 @@ SUBSYSTEM_DEF(ticker) m = pick(memetips) if(m) - to_chat(world, span_purple(examine_block("Tip of the round: [html_encode(m)]"))) + to_chat(world, span_purple(boxed_message("Tip of the round: [html_encode(m)]"))) /datum/controller/subsystem/ticker/proc/check_queue() if(!queued_players.len) diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index 03c244ae05d2..476d9fcc9dce 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -87,7 +87,7 @@ SUBSYSTEM_DEF(vote) var/log_string = replacetext(to_display, "\n", "\\n") // 'keep' the newlines, but dont actually print them as newlines log_vote(log_string) - to_chat(world, span_infoplain(vote_font("\n[to_display]"))) + to_chat(world, span_infoplain(vote_font("[to_display]"))) // Finally, doing any effects on vote completion if (final_winner) // if no one voted, or the vote cannot be won, final_winner will be null @@ -209,9 +209,9 @@ SUBSYSTEM_DEF(vote) var/to_display = current_vote.initiate_vote(vote_initiator_name, duration) log_vote(to_display) - to_chat(world, span_infoplain(vote_font("\n[span_bold(to_display)]\n\ + to_chat(world, custom_boxed_message("purple_box center", span_infoplain(vote_font("[span_bold(to_display)]
\ Type vote or click here to place your votes.\n\ - You have [DisplayTimeText(duration)] to vote."))) + You have [DisplayTimeText(duration)] to vote.")))) // And now that it's going, give everyone a voter action for(var/client/new_voter as anything in GLOB.clients) @@ -344,6 +344,7 @@ SUBSYSTEM_DEF(vote) /datum/action/vote name = "Vote!" button_icon_state = "vote" + background_icon_state = "bg_blink" /datum/action/vote/IsAvailable() return TRUE // Democracy is always available to the free people diff --git a/code/datums/ai/README.md b/code/datums/ai/README.md new file mode 100644 index 000000000000..f219b11bb247 --- /dev/null +++ b/code/datums/ai/README.md @@ -0,0 +1,21 @@ +# AI controllers + +## Introduction + +Our AI controller system is an attempt at making it possible to create modularized AI that stores its behavior in datums, while keeping state and decision making in a controller. This allows a more versatile way of creating AI that doesn't rely on OOP as much, and doesn't clutter up the Life() code in Mobs. + +## AI Controllers + +A datum that can be added to any atom in the game. Similarly to components, they might only support a given subtype (e.g. /mob/living), but the idea is that theoretically, you could apply a specific AI controller to a big a group of different types as possible and it would still work. + +These datums handle both the normal movement of mobs, but also their decision making, deciding which actions they will take based on the checks you put into their SelectBehaviors proc. + +If behaviors are selected, and the AI is in range, it will try to perform them. It runs all the behaviors it currently has in parallel; allowing for it to for example screech at someone while trying to attack them. Aslong as it has behaviors running, it will not try to generate new plans, making it not waste CPU when it already has an active goal. + +They also hold data for any of the actions they might need to use, such as cooldowns, whether or not they're currently fighting, etcetera this is stored in the blackboard, more information on that below. + +### Blackboard +The blackboard is an associated list keyed with strings and with values of whatever you want. These store information the mob has such as "Am I attacking someone", "Do I have a weapon". By using an associated list like this, no data needs to be stored on the actions themselves, and you could make actions that work on multiple ai controllers if you so pleased by making the key to use a variable. + +## AI Behavior +AI behaviors are the actions an AI can take. These can range from "Do an emote" to "Attack this target until he is dead". They are singletons and should contain nothing but static data. Any dynamic data should be stored in the blackboard, to allow different controllers to use the same behaviors. diff --git a/code/datums/ai/_ai_behavoir.dm b/code/datums/ai/_ai_behavoir.dm new file mode 100644 index 000000000000..fad64f6e97d6 --- /dev/null +++ b/code/datums/ai/_ai_behavoir.dm @@ -0,0 +1,25 @@ +///Abstract class for an action an AI can take, can range from movement to grabbing a nearby weapon. +/datum/ai_behavior + ///What distance you need to be from the target to perform the action + var/required_distance = 1 + ///Flags for extra behavior + var/behavior_flags = NONE + ///Cooldown between actions performances + var/action_cooldown = 0 + +/// Called by the ai controller when first being added. Additional arguments depend on the behavior type. +/// Return FALSE to cancel +/datum/ai_behavior/proc/setup(datum/ai_controller/controller, ...) + return TRUE + +///Called by the AI controller when this action is performed +/datum/ai_behavior/proc/perform(delta_time, datum/ai_controller/controller, ...) + controller.behavior_cooldowns[src] = world.time + action_cooldown + return + +///Called when the action is finished. +/datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded) + controller.current_behaviors.Remove(src) + controller.behavior_args -= type + if(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //If this was a movement task, reset our movement target. + controller.current_movement_target = null diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm new file mode 100644 index 000000000000..ce11df446aa4 --- /dev/null +++ b/code/datums/ai/_ai_controller.dm @@ -0,0 +1,254 @@ +/* +AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a atom. What this means is that these datums +have ways of interacting with a specific atom and control it. They posses a blackboard with the information the AI knows and has, and will plan behaviors it will try to execute through +multiple modular subtrees with behaviors +*/ + +/datum/ai_controller + ///The atom this controller is controlling + var/atom/pawn + ///Bitfield of traits for this AI to handle extra behavior + var/ai_traits + ///Current actions being performed by the AI. + var/list/current_behaviors + ///Current actions and their respective last time ran as an assoc list. + var/list/behavior_cooldowns = list() + ///Current status of AI (OFF/ON/IDLE) + var/ai_status + ///Current movement target of the AI, generally set by decision making. + var/atom/current_movement_target + ///Delay between atom movements, if this is not a multiplication of the delay in + var/move_delay + ///This is a list of variables the AI uses and can be mutated by actions. When an action is performed you pass this list and any relevant keys for the variables it can mutate. + var/list/blackboard = list() + ///Stored arguments for behaviors given during their initial creation + var/list/behavior_args = list() + ///Tracks recent pathing attempts, if we fail too many in a row we fail our current plans. + var/pathing_attempts + ///Can the AI remain in control if there is a client? + var/continue_processing_when_client = FALSE + ///distance to give up on target + var/max_target_distance = 14 + ///Cooldown for new plans, to prevent AI from going nuts if it can't think of new plans and looping on end + COOLDOWN_DECLARE(failed_planning_cooldown) + ///All subtrees this AI has available, will run them in order, so make sure they're in the order you want them to run. On initialization of this type, it will start as a typepath(s) and get converted to references of ai_subtrees found in SSai_controllers when init_subtrees() is called + var/list/planning_subtrees + + // Movement related things here + ///Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards. + var/datum/ai_movement/ai_movement = /datum/ai_movement/dumb + ///Cooldown until next movement + COOLDOWN_DECLARE(movement_cooldown) + ///Delay between movements. This is on the controller so we can keep the movement datum singleton + var/movement_delay = 0.1 SECONDS + + // The variables below are fucking stupid and should be put into the blackboard at some point. + ///A list for the path we're currently following, if we're using JPS pathing + var/list/movement_path + ///Cooldown for JPS movement, how often we're allowed to try making a new path + COOLDOWN_DECLARE(repath_cooldown) + ///AI paused time + var/paused_until = 0 + +/datum/ai_controller/New(atom/new_pawn) + change_ai_movement_type(ai_movement) + init_subtrees() + PossessPawn(new_pawn) + +/datum/ai_controller/Destroy(force, ...) + set_ai_status(AI_STATUS_OFF) + UnpossessPawn(FALSE) + return ..() + +///Overrides the current ai_movement of this controller with a new one +/datum/ai_controller/proc/change_ai_movement_type(datum/ai_movement/new_movement) + ai_movement = SSai_movement.movement_types[new_movement] + +///Completely replaces the planning_subtrees with a new set based on argument provided, list provided must contain specifically typepaths +/datum/ai_controller/proc/replace_planning_subtrees(list/typepaths_of_new_subtrees) + planning_subtrees = typepaths_of_new_subtrees + init_subtrees() + +///Loops over the subtrees in planning_subtrees and looks at the ai_controllers to grab a reference, ENSURE planning_subtrees ARE TYPEPATHS AND NOT INSTANCES/REFERENCES BEFORE EXECUTING THIS +/datum/ai_controller/proc/init_subtrees() + if(!LAZYLEN(planning_subtrees)) + return + var/list/temp_subtree_list = list() + for(var/subtree in planning_subtrees) + var/subtree_instance = SSai_controllers.ai_subtrees[subtree] + temp_subtree_list += subtree_instance + planning_subtrees = temp_subtree_list + +///Proc to move from one pawn to another, this will destroy the target's existing controller. +/datum/ai_controller/proc/PossessPawn(atom/new_pawn) + if(pawn) //Reset any old signals + UnpossessPawn(FALSE) + + if(istype(new_pawn.ai_controller)) //Existing AI, kill it. + QDEL_NULL(new_pawn.ai_controller) + + if(TryPossessPawn(new_pawn) & AI_CONTROLLER_INCOMPATIBLE) + qdel(src) + CRASH("[src] attached to [new_pawn] but these are not compatible!") + + pawn = new_pawn + pawn.ai_controller = src + + if(!continue_processing_when_client && istype(new_pawn, /mob)) + var/mob/possible_client_holder = new_pawn + if(possible_client_holder.client) + set_ai_status(AI_STATUS_OFF) + else + set_ai_status(AI_STATUS_ON) + else + set_ai_status(AI_STATUS_ON) + + RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) + +///Abstract proc for initializing the pawn to the new controller +/datum/ai_controller/proc/TryPossessPawn(atom/new_pawn) + return + +///Proc for deinitializing the pawn to the old controller +/datum/ai_controller/proc/UnpossessPawn(destroy) + UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT)) + pawn.ai_controller = null + pawn = null + if(destroy) + qdel(src) + return + +///Returns TRUE if the ai controller can actually run at the moment. +/datum/ai_controller/proc/able_to_run() + if(world.time < paused_until) + return FALSE + return TRUE + +/// Generates a plan and see if our existing one is still valid. +/datum/ai_controller/process(delta_time) + if(!able_to_run()) + walk(pawn, 0) //stop moving + return //this should remove them from processing in the future through event-based stuff. + if(!LAZYLEN(current_behaviors)) + PerformIdleBehavior(delta_time) //Do some stupid shit while we have nothing to do + return + + if(current_movement_target && get_dist(pawn, current_movement_target) > max_target_distance) //The distance is out of range + CancelActions() + return + + for(var/i in current_behaviors) + var/datum/ai_behavior/current_behavior = i + + if(behavior_cooldowns[current_behavior] > world.time) //Still on cooldown + continue + + if(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT && current_movement_target) //Might need to move closer + if(current_behavior.required_distance >= get_dist(pawn, current_movement_target)) ///Are we close enough to engage? + if(ai_movement.moving_controllers[src] == current_movement_target) //We are close enough, if we're moving stop.else + ai_movement.stop_moving_towards(src) + ProcessBehavior(delta_time, current_behavior) + return + + else if(ai_movement.moving_controllers[src] != current_movement_target) //We're too far, if we're not already moving start doing it. + ai_movement.start_moving_towards(src, current_movement_target) //Then start moving + + if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) //If we can move and perform then do so. + ProcessBehavior(delta_time, current_behavior) + return + else //No movement required + ProcessBehavior(delta_time, current_behavior) + return + + +///Move somewhere using dumb movement (byond base) +/datum/ai_controller/proc/MoveTo(delta_time) + var/current_loc = get_turf(pawn) + var/atom/movable/movable_pawn = pawn + + var/turf/target_turf = get_step_towards(movable_pawn, current_movement_target) + + if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs)) + movable_pawn.Move(target_turf, get_dir(current_loc, target_turf)) + if(current_loc == get_turf(movable_pawn)) + if(++pathing_attempts >= AI_MAX_PATH_LENGTH) + CancelActions() + pathing_attempts = 0 + + +///Perform some dumb idle behavior. +/datum/ai_controller/proc/PerformIdleBehavior(delta_time) + return + +///This is where you decide what actions are taken by the AI. +/datum/ai_controller/proc/SelectBehaviors(delta_time) + SHOULD_NOT_SLEEP(TRUE) //Fuck you don't sleep in procs like this. + if(!COOLDOWN_FINISHED(src, failed_planning_cooldown)) + return FALSE + + LAZYINITLIST(current_behaviors) + + if(LAZYLEN(planning_subtrees)) + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.SelectBehaviors(src, delta_time) == SUBTREE_RETURN_FINISH_PLANNING) + break + +///This proc handles changing ai status, and starts/stops processing if required. +/datum/ai_controller/proc/set_ai_status(new_ai_status) + if(ai_status == new_ai_status) + return FALSE //no change + + ai_status = new_ai_status + switch(ai_status) + if(AI_STATUS_ON) + SSai_controllers.active_ai_controllers += src + START_PROCESSING(SSai_behaviors, src) + if(AI_STATUS_OFF) + STOP_PROCESSING(SSai_behaviors, src) + SSai_controllers.active_ai_controllers -= src + CancelActions() + +/datum/ai_controller/proc/PauseAi(time) + paused_until = world.time + time + +/datum/ai_controller/proc/AddBehavior(behavior_type, ...) + var/datum/ai_behavior/behavior = GET_AI_BEHAVIOR(behavior_type) + if(!behavior) + CRASH("Behavior [behavior_type] not found.") + var/list/arguments = args.Copy() + arguments[1] = src + if(!behavior.setup(arglist(arguments))) + return + LAZYADD(current_behaviors, behavior) + arguments.Cut(1, 2) + if(length(arguments)) + behavior_args[behavior_type] = arguments + +/datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior) + var/list/arguments = list(delta_time, src) + var/list/stored_arguments = behavior_args[behavior.type] + if(stored_arguments) + arguments += stored_arguments + behavior.perform(arglist(arguments)) + +/datum/ai_controller/proc/CancelActions() + if(!LAZYLEN(current_behaviors)) + return + for(var/i in current_behaviors) + var/datum/ai_behavior/current_behavior = i + current_behavior.finish_action(src, FALSE) + +/datum/ai_controller/proc/on_sentience_gained() + UnregisterSignal(pawn, COMSIG_MOB_LOGIN) + if(!continue_processing_when_client) + set_ai_status(AI_STATUS_OFF) //Can't do anything while player is connected + RegisterSignal(pawn, COMSIG_MOB_LOGOUT, PROC_REF(on_sentience_lost)) + +/datum/ai_controller/proc/on_sentience_lost() + UnregisterSignal(pawn, COMSIG_MOB_LOGOUT) + set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected + RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) + +/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding, likely pointing to whatever ID slot is relevant +/datum/ai_controller/proc/get_access() + return diff --git a/code/datums/ai/_ai_planning_subtree.dm b/code/datums/ai/_ai_planning_subtree.dm new file mode 100644 index 000000000000..8f186d586a45 --- /dev/null +++ b/code/datums/ai/_ai_planning_subtree.dm @@ -0,0 +1,6 @@ +///A subtree is attached to a controller and is occasionally called by /ai_controller/SelectBehaviors(), this mainly exists to act as a way to subtype and modify SelectBehaviors() without needing to subtype the ai controller itself +/datum/ai_planning_subtree + +///Determines what behaviors should the controller try processing; if this returns SUBTREE_RETURN_FINISH_PLANNING then the controller won't go through the other subtrees should multiple exist in controller.planning_subtrees +/datum/ai_planning_subtree/proc/SelectBehaviors(datum/ai_controller/controller, delta_time) + return diff --git a/code/datums/ai/dog/dog_behaviors.dm b/code/datums/ai/dog/dog_behaviors.dm new file mode 100644 index 000000000000..3672b348118a --- /dev/null +++ b/code/datums/ai/dog/dog_behaviors.dm @@ -0,0 +1,208 @@ +/datum/ai_behavior/battle_screech/dog + screeches = list("barks","howls") + +/// Fetching makes the pawn chase after whatever it's targeting and pick it up when it's in range, with the dog_equip behavior +/datum/ai_behavior/fetch + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/fetch/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + var/obj/item/fetch_thing = controller.blackboard[BB_FETCH_TARGET] + + if(fetch_thing.anchored || !isturf(fetch_thing.loc) || IS_EDIBLE(fetch_thing)) //either we can't pick it up, or we'd rather eat it, so stop trying. + finish_action(controller, FALSE) + return + + if(in_range(living_pawn, fetch_thing)) + finish_action(controller, TRUE) + return + + finish_action(controller, FALSE) + +/datum/ai_behavior/fetch/finish_action(datum/ai_controller/controller, success) + . = ..() + + if(!success) //Don't try again on this item if we failed + var/obj/item/target = controller.blackboard[BB_FETCH_TARGET] + if(target) + controller.blackboard[BB_FETCH_IGNORE_LIST][target] = TRUE + controller.blackboard[BB_FETCH_TARGET] = null + controller.blackboard[BB_FETCH_DELIVER_TO] = null + + +/// This is simply a behaviour to pick up a fetch target +/datum/ai_behavior/simple_equip/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/obj/item/fetch_target = controller.blackboard[BB_FETCH_TARGET] + if(!isturf(fetch_target?.loc)) // someone picked it up or something happened to it + finish_action(controller, FALSE) + return + + if(in_range(controller.pawn, fetch_target)) + pickup_item(controller, fetch_target) + finish_action(controller, TRUE) + else + finish_action(controller, FALSE) + +/datum/ai_behavior/simple_equip/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.blackboard[BB_FETCH_TARGET] = null + +/datum/ai_behavior/simple_equip/proc/pickup_item(datum/ai_controller/controller, obj/item/target) + var/atom/pawn = controller.pawn + drop_item(controller) + pawn.visible_message("[pawn] picks up [target] in [pawn.p_their()] mouth.") + target.forceMove(pawn) + controller.blackboard[BB_SIMPLE_CARRY_ITEM] = target + return TRUE + +/datum/ai_behavior/simple_equip/proc/drop_item(datum/ai_controller/controller) + var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + if(!carried_item) + return + + var/atom/pawn = controller.pawn + pawn.visible_message("[pawn] drops [carried_item].") + carried_item.forceMove(get_turf(pawn)) + controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null + return TRUE + + + +/// This behavior involves dropping off a carried item to a specified person (or place) +/datum/ai_behavior/deliver_item + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/deliver_item/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] + if(!return_target) + finish_action(controller, FALSE) + if(in_range(controller.pawn, return_target)) + deliver_item(controller) + finish_action(controller, TRUE) + +/datum/ai_behavior/deliver_item/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.blackboard[BB_FETCH_DELIVER_TO] = null + +/// Actually drop the fetched item to the target +/datum/ai_behavior/deliver_item/proc/deliver_item(datum/ai_controller/controller) + var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + var/atom/movable/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] + if(!carried_item || !return_target) + finish_action(controller, FALSE) + return + + if(ismob(return_target)) + controller.pawn.visible_message("[controller.pawn] delivers [carried_item] at [return_target]'s feet.") + else // not sure how to best phrase this + controller.pawn.visible_message("[controller.pawn] delivers [carried_item] to [return_target].") + + carried_item.forceMove(get_turf(return_target)) + controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null + return TRUE + +/// This behavior involves either eating a snack we can reach, or begging someone holding a snack +/datum/ai_behavior/eat_snack + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/eat_snack/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/obj/item/snack = controller.current_movement_target + if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc))) + finish_action(controller, FALSE) + + var/mob/living/living_pawn = controller.pawn + if(!in_range(living_pawn, snack)) + return + + if(isturf(snack.loc)) + snack.attack_animal(living_pawn) // snack attack! + else if(iscarbon(snack.loc) && DT_PROB(10, delta_time)) + living_pawn.manual_emote("stares at [snack.loc]'s [snack.name] with a sad puppy-face.") + + if(QDELETED(snack)) // we ate it! + finish_action(controller, TRUE) + + +/// This behavior involves either eating a snack we can reach, or begging someone holding a snack +/datum/ai_behavior/play_dead + behavior_flags = NONE + +/datum/ai_behavior/play_dead/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/simple_animal/simple_pawn = controller.pawn + if(!istype(simple_pawn)) + return + + if(!controller.blackboard[BB_DOG_PLAYING_DEAD]) + controller.blackboard[BB_DOG_PLAYING_DEAD] = TRUE + simple_pawn.emote("deathgasp", intentional=FALSE) + simple_pawn.icon_state = simple_pawn.icon_dead + if(simple_pawn.flip_on_death) + simple_pawn.transform = simple_pawn.transform.Turn(180) + simple_pawn.density = FALSE + + if(DT_PROB(10, delta_time)) + finish_action(controller, TRUE) + +/datum/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + var/mob/living/simple_animal/simple_pawn = controller.pawn + if(!istype(simple_pawn) || simple_pawn.stat) // imagine actually dying while playing dead. hell, imagine being the kid waiting for your pup to get back up :( + return + controller.blackboard[BB_DOG_PLAYING_DEAD] = FALSE + simple_pawn.visible_message("[simple_pawn] springs to [simple_pawn.p_their()] feet, panting excitedly!") + simple_pawn.icon_state = simple_pawn.icon_living + if(simple_pawn.flip_on_death) + simple_pawn.transform = simple_pawn.transform.Turn(180) + simple_pawn.density = initial(simple_pawn.density) + +/// This behavior involves either eating a snack we can reach, or begging someone holding a snack +/datum/ai_behavior/harass + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + required_distance = 3 + +/datum/ai_behavior/harass/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + if(!istype(living_pawn)) + return + + var/atom/movable/harass_target = controller.blackboard[BB_DOG_HARASS_TARGET] + if(!harass_target || !can_see(living_pawn, harass_target, length=AI_DOG_VISION_RANGE)) + finish_action(controller, FALSE) + return + + if(controller.blackboard[BB_DOG_FRIENDS][harass_target]) + living_pawn.visible_message("[living_pawn] looks sideways at [harass_target] for a moment, then shakes [living_pawn.p_their()] head and ceases aggression.") + finish_action(controller, FALSE) + return + + var/mob/living/living_target = harass_target + if(istype(living_target) && (living_target.stat || HAS_TRAIT(living_target, TRAIT_FAKEDEATH))) + finish_action(controller, TRUE) + return + + // subtypes of this behavior can change behavior for how eager/averse the pawn is to attack the target as opposed to falling back/making noise/getting help + if(in_range(living_pawn, living_target)) + attack(controller, living_target) + else if(DT_PROB(50, delta_time)) + living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [harass_target]!") + +/datum/ai_behavior/harass/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.blackboard[BB_DOG_HARASS_TARGET] = null + +/// A proc representing when the mob is pushed to actually attack the target. Again, subtypes can be used to represent different attacks from different animals, or it can be some other generic behavior +/datum/ai_behavior/harass/proc/attack(datum/ai_controller/controller, mob/living/living_target) + var/mob/living/living_pawn = controller.pawn + if(!istype(living_pawn)) + return + living_pawn.do_attack_animation(living_target, ATTACK_EFFECT_BITE) + living_target.visible_message("[living_pawn] bites at [living_target]!", "[living_pawn] bites at you!", vision_distance = COMBAT_MESSAGE_RANGE) + if(istype(living_target)) + living_target.take_bodypart_damage(rand(5, 10)) + log_combat(living_pawn, living_target, "bit (AI)") diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm new file mode 100644 index 000000000000..5cd65654db7c --- /dev/null +++ b/code/datums/ai/dog/dog_controller.dm @@ -0,0 +1,271 @@ +/datum/ai_controller/dog + blackboard = list(\ + BB_SIMPLE_CARRY_ITEM = null,\ + BB_FETCH_TARGET = null,\ + BB_FETCH_DELIVER_TO = null,\ + BB_DOG_FRIENDS = list(),\ + BB_FETCH_IGNORE_LIST = list(),\ + BB_DOG_ORDER_MODE = DOG_COMMAND_NONE,\ + BB_DOG_PLAYING_DEAD = FALSE,\ + BB_DOG_HARASS_TARGET = null) + ai_movement = /datum/ai_movement/jps + planning_subtrees = list(/datum/ai_planning_subtree/dog) + + COOLDOWN_DECLARE(heel_cooldown) + COOLDOWN_DECLARE(command_cooldown) + +/datum/ai_controller/dog/process(delta_time) + if(ismob(pawn)) + var/mob/living/living_pawn = pawn + movement_delay = living_pawn.cached_multiplicative_slowdown + return ..() + +/datum/ai_controller/dog/TryPossessPawn(atom/new_pawn) + if(!isliving(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + + RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) + RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined)) + RegisterSignal(new_pawn, COMSIG_CLICK_ALT, PROC_REF(check_altclicked)) + RegisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING, PROC_REF(listened_throw)) + return ..() //Run parent at end + +/datum/ai_controller/dog/UnpossessPawn(destroy) + UnregisterSignal(pawn, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_PARENT_EXAMINE, COMSIG_GLOB_CARBON_THROW_THING, COMSIG_CLICK_ALT)) + return ..() //Run parent at end + +/datum/ai_controller/dog/able_to_run() + var/mob/living/living_pawn = pawn + + if(IS_DEAD_OR_INCAP(living_pawn)) + return FALSE + return ..() + +/datum/ai_controller/dog/get_access() + var/mob/living/simple_animal/simple_pawn = pawn + if(!istype(simple_pawn)) + return + + return simple_pawn.access_card + + +/datum/ai_controller/dog/PerformIdleBehavior(delta_time) + var/mob/living/living_pawn = pawn + if(!isturf(living_pawn.loc) || living_pawn.pulledby) + return + + // if we were just ordered to heel, chill out for a bit + if(!COOLDOWN_FINISHED(src, heel_cooldown)) + return + + // if we're just ditzing around carrying something, occasionally print a message so people know we have something + if(blackboard[BB_SIMPLE_CARRY_ITEM] && DT_PROB(5, delta_time)) + var/obj/item/carry_item = blackboard[BB_SIMPLE_CARRY_ITEM] + living_pawn.visible_message("[living_pawn] gently teethes on \the [carry_item] in [living_pawn.p_their()] mouth.", vision_distance=COMBAT_MESSAGE_RANGE) + + if(DT_PROB(5, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE)) + var/move_dir = pick(GLOB.alldirs) + living_pawn.Move(get_step(living_pawn, move_dir), move_dir) + else if(DT_PROB(10, delta_time)) + living_pawn.manual_emote(pick("dances around.","chases [living_pawn.p_their()] tail!")) + living_pawn.AddComponent(/datum/component/spinny) + +/// Someone has thrown something, see if it's someone we care about and start listening to the thrown item so we can see if we want to fetch it when it lands +/datum/ai_controller/dog/proc/listened_throw(datum/source, mob/living/carbon/carbon_thrower) + SIGNAL_HANDLER + if(blackboard[BB_FETCH_TARGET] || blackboard[BB_FETCH_DELIVER_TO] || blackboard[BB_DOG_PLAYING_DEAD]) // we're already busy + return + if(!COOLDOWN_FINISHED(src, heel_cooldown)) + return + if(!can_see(pawn, carbon_thrower, length=AI_DOG_VISION_RANGE)) + return + var/obj/item/thrown_thing = carbon_thrower.get_active_held_item() + if(!isitem(thrown_thing)) + return + if(blackboard[BB_FETCH_IGNORE_LIST][thrown_thing]) + return + + RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(listen_throw_land)) + +/// A throw we were listening to has finished, see if it's in range for us to try grabbing it +/datum/ai_controller/dog/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum) + SIGNAL_HANDLER + + UnregisterSignal(thrown_thing, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_THROW_LANDED)) + if(!istype(thrown_thing) || !isturf(thrown_thing.loc) || !can_see(pawn, thrown_thing, length=AI_DOG_VISION_RANGE)) + return + + current_movement_target = thrown_thing + blackboard[BB_FETCH_TARGET] = thrown_thing + blackboard[BB_FETCH_DELIVER_TO] = throwing_datum.thrower + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/fetch)) + +/// Someone's interacting with us by hand, see if they're being nice or mean +/datum/ai_controller/dog/proc/on_attack_hand(datum/source, mob/living/user) + SIGNAL_HANDLER + + if(user.a_intent == INTENT_HARM) + unfriend(user) + else + if(prob(AI_DOG_PET_FRIEND_PROB)) + befriend(user) + // if the dog has something in their mouth that they're not bringing to someone for whatever reason, have them drop it when pet by a friend + var/list/friends = blackboard[BB_DOG_FRIENDS] + if(blackboard[BB_SIMPLE_CARRY_ITEM] && !current_movement_target && friends[user]) + var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] + pawn.visible_message("[pawn] drops [carried_item] at [user]'s feet!") + // maybe have a dedicated proc for dropping things + carried_item.forceMove(get_turf(user)) + blackboard[BB_SIMPLE_CARRY_ITEM] = null + +/// Someone is being nice to us, let's make them a friend! +/datum/ai_controller/dog/proc/befriend(mob/living/new_friend) + var/list/friends = blackboard[BB_DOG_FRIENDS] + if(friends[new_friend]) + return + if(in_range(pawn, new_friend)) + new_friend.visible_message("[pawn] licks at [new_friend] in a friendly manner!", "[pawn] licks at you in a friendly manner!") + friends[new_friend] = TRUE + RegisterSignal(new_friend, COMSIG_MOB_POINTED, PROC_REF(check_point)) + RegisterSignal(new_friend, COMSIG_MOB_SAY, PROC_REF(check_verbal_command)) + +/// Someone is being mean to us, take them off our friends (add actual enemies behavior later) +/datum/ai_controller/dog/proc/unfriend(mob/living/ex_friend) + var/list/friends = blackboard[BB_DOG_FRIENDS] + friends[ex_friend] = null + UnregisterSignal(ex_friend, list(COMSIG_MOB_POINTED, COMSIG_MOB_SAY)) + +/// Someone is looking at us, if we're currently carrying something then show what it is, and include a message if they're our friend +/datum/ai_controller/dog/proc/on_examined(datum/source, mob/user, list/examine_text) + SIGNAL_HANDLER + + var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] + if(carried_item) + examine_text += "[pawn.p_they(TRUE)] [pawn.p_are()] carrying [carried_item.get_examine_string(user)] in [pawn.p_their()] mouth." + if(blackboard[BB_DOG_FRIENDS][user]) + examine_text += "[pawn.p_they(TRUE)] seem[pawn.p_s()] happy to see you!" + +/// If we died, drop anything we were carrying +/datum/ai_controller/dog/proc/on_death(mob/living/ol_yeller) + SIGNAL_HANDLER + + var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] + if(!carried_item) + return + + ol_yeller.visible_message("[ol_yeller] drops [carried_item] as [ol_yeller.p_they()] die[ol_yeller.p_s()].") + carried_item.forceMove(get_turf(ol_yeller)) + blackboard[BB_SIMPLE_CARRY_ITEM] = null + +// next section is regarding commands + +/// Someone alt clicked us, see if they're someone we should show the radial command menu to +/datum/ai_controller/dog/proc/check_altclicked(datum/source, mob/living/clicker) + SIGNAL_HANDLER + + if(!COOLDOWN_FINISHED(src, command_cooldown)) + return + if(!istype(clicker) || !blackboard[BB_DOG_FRIENDS][clicker]) + return + . = COMPONENT_CANCEL_CLICK_ALT + INVOKE_ASYNC(src, PROC_REF(command_radial), clicker) + +/// Show the command radial menu +/datum/ai_controller/dog/proc/command_radial(mob/living/clicker) + var/list/commands = list( + COMMAND_HEEL = image(icon = 'icons/Testing/turf_analysis.dmi', icon_state = "red_arrow"), + COMMAND_FETCH = image(icon = 'icons/mob/actions/actions_spells.dmi', icon_state = "summons"), + COMMAND_ATTACK = image(icon = 'icons/effects/effects.dmi', icon_state = "bite"), + COMMAND_DIE = image(icon = 'icons/mob/pets.dmi', icon_state = "puppy_dead") + ) + + var/choice = show_radial_menu(clicker, pawn, commands, custom_check = CALLBACK(src, PROC_REF(check_menu), clicker), tooltips = TRUE) + if(!choice || !check_menu(clicker)) + return + set_command_mode(clicker, choice) + +/datum/ai_controller/dog/proc/check_menu(mob/user) + if(!istype(user)) + CRASH("A non-mob is trying to issue an order to [pawn].") + if(user.incapacitated() || !can_see(user, pawn)) + return FALSE + return TRUE + +/// One of our friends said something, see if it's a valid command, and if so, take action +/datum/ai_controller/dog/proc/check_verbal_command(mob/speaker, speech_args) + SIGNAL_HANDLER + + if(!blackboard[BB_DOG_FRIENDS][speaker]) + return + + if(!COOLDOWN_FINISHED(src, command_cooldown)) + return + + var/spoken_text = speech_args[SPEECH_MESSAGE] // probably should check for full words + var/command + if(findtext(spoken_text, "heel") || findtext(spoken_text, "sit") || findtext(spoken_text, "stay")) + command = COMMAND_HEEL + else if(findtext(spoken_text, "fetch") || findtext(spoken_text, "get it")) + command = COMMAND_FETCH + else if(findtext(spoken_text, "attack") || findtext(spoken_text, "sic")) + command = COMMAND_ATTACK + else if(findtext(spoken_text, "play dead")) + command = COMMAND_DIE + else + return + + if(!can_see(pawn, speaker, length=AI_DOG_VISION_RANGE)) + return + set_command_mode(speaker, command) + +/// Whether we got here via radial menu or a verbal command, this is where we actually process what our new command will be +/datum/ai_controller/dog/proc/set_command_mode(mob/commander, command) + COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN) + + switch(command) + // heel: stop what you're doing, relax and try not to do anything for a little bit + if(COMMAND_HEEL) + pawn.visible_message("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] sit[pawn.p_s()] down obediently, awaiting further orders.") + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE + COOLDOWN_START(src, heel_cooldown, AI_DOG_HEEL_DURATION) + CancelActions() + // fetch: whatever the commander points to, try and bring it back + if(COMMAND_FETCH) + pawn.visible_message("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] bounce[pawn.p_s()] slightly in anticipation.") + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_FETCH + // attack: harass whoever the commander points to + if(COMMAND_ATTACK) + pawn.visible_message("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] growl[pawn.p_s()] intensely.") // imagine getting intimidated by a corgi + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_ATTACK + if(COMMAND_DIE) + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE + CancelActions() + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/play_dead)) + +/// Someone we like is pointing at something, see if it's something we might want to interact with (like if they might want us to fetch something for them) +/datum/ai_controller/dog/proc/check_point(mob/pointing_friend, atom/movable/pointed_movable) + SIGNAL_HANDLER + + if(!COOLDOWN_FINISHED(src, command_cooldown)) + return + if(pointed_movable == pawn || blackboard[BB_FETCH_TARGET] || !istype(pointed_movable) || blackboard[BB_DOG_ORDER_MODE] == DOG_COMMAND_NONE) // busy or no command + return + if(!can_see(pawn, pointing_friend, length=AI_DOG_VISION_RANGE) || !can_see(pawn, pointed_movable, length=AI_DOG_VISION_RANGE)) + return + + COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN) + + switch(blackboard[BB_DOG_ORDER_MODE]) + if(DOG_COMMAND_FETCH) + if(ismob(pointed_movable) || pointed_movable.anchored) + return + pawn.visible_message("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and barks excitedly!") + current_movement_target = pointed_movable + blackboard[BB_FETCH_TARGET] = pointed_movable + blackboard[BB_FETCH_DELIVER_TO] = pointing_friend + current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/fetch) + if(DOG_COMMAND_ATTACK) + pawn.visible_message("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and growls intensely!") + current_movement_target = pointed_movable + blackboard[BB_DOG_HARASS_TARGET] = pointed_movable + current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/harass) diff --git a/code/datums/ai/dog/dog_subtrees.dm b/code/datums/ai/dog/dog_subtrees.dm new file mode 100644 index 000000000000..1eab7b87251b --- /dev/null +++ b/code/datums/ai/dog/dog_subtrees.dm @@ -0,0 +1,40 @@ +/datum/ai_planning_subtree/dog + COOLDOWN_DECLARE(heel_cooldown) + COOLDOWN_DECLARE(reset_ignore_cooldown) + +/datum/ai_planning_subtree/dog/SelectBehaviors(datum/ai_controller/dog/controller, delta_time) + var/mob/living/living_pawn = controller.pawn + + // occasionally reset our ignore list + if(COOLDOWN_FINISHED(src, reset_ignore_cooldown) && length(controller.blackboard[BB_FETCH_IGNORE_LIST])) + COOLDOWN_START(src, reset_ignore_cooldown, AI_FETCH_IGNORE_DURATION) + controller.blackboard[BB_FETCH_IGNORE_LIST] = list() + + // if we were just ordered to heel, chill out for a bit + if(!COOLDOWN_FINISHED(src, heel_cooldown)) + return + + // if we're not already carrying something and we have a fetch target (and we're not already doing something with it), see if we can eat/equip it + if(!controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_TARGET]) + var/atom/movable/interact_target = controller.blackboard[BB_FETCH_TARGET] + if(in_range(living_pawn, interact_target) && (isturf(interact_target.loc))) + controller.current_movement_target = interact_target + if(IS_EDIBLE(interact_target)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/eat_snack)) + else if(isitem(interact_target)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/simple_equip)) + else + controller.blackboard[BB_FETCH_TARGET] = null + controller.blackboard[BB_FETCH_DELIVER_TO] = null + return + + // if we're carrying something and we have a destination to deliver it, do that + if(controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_DELIVER_TO]) + var/atom/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] + if(!can_see(controller.pawn, return_target, length=AI_DOG_VISION_RANGE)) + // if the return target isn't in sight, we'll just forget about it and carry the thing around + controller.blackboard[BB_FETCH_DELIVER_TO] = null + return + controller.current_movement_target = return_target + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/deliver_item)) + return diff --git a/code/datums/ai/generic_actions.dm b/code/datums/ai/generic_actions.dm new file mode 100644 index 000000000000..fdcc978857fd --- /dev/null +++ b/code/datums/ai/generic_actions.dm @@ -0,0 +1,111 @@ + +/datum/ai_behavior/resist/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + living_pawn.resist() + finish_action(controller, TRUE) + +/datum/ai_behavior/battle_screech + ///List of possible screeches the behavior has + var/list/screeches + +/datum/ai_behavior/battle_screech/perform(delta_time, datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick(screeches)) + finish_action(controller, TRUE) + +/// Use in hand the currently held item +/datum/ai_behavior/use_in_hand + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/use_in_hand/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held = pawn.get_item_by_slot(pawn.get_active_hand()) + if(!held) + finish_action(controller, FALSE) + return + pawn.activate_hand(pawn.get_active_hand()) + finish_action(controller, TRUE) + +/// Use the currently held item, or unarmed, on an object in the world +/datum/ai_behavior/use_on_object + required_distance = 1 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/use_on_object/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand()) + var/atom/target = controller.current_movement_target + + if(!target || !pawn.CanReach(target)) + finish_action(controller, FALSE) + return + + pawn.a_intent = INTENT_HELP + + if(held_item) + held_item.melee_attack_chain(pawn, target) + else + pawn.UnarmedAttack(target, TRUE) + + finish_action(controller, TRUE) + +/datum/ai_behavior/give + required_distance = 1 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/give/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand()) + var/atom/target = controller.current_movement_target + + if(!target || !pawn.CanReach(target) || !isliving(target)) + finish_action(controller, FALSE) + return + + var/mob/living/living_target = target + controller.PauseAi(1.5 SECONDS) + living_target.visible_message( + "[pawn] starts trying to give [held_item] to [living_target]!", + "[pawn] tries to give you [held_item]!" + ) + if(!do_after(pawn, 1 SECONDS, living_target)) + return + if(QDELETED(held_item) || QDELETED(living_target)) + finish_action(controller, FALSE) + return + var/pocket_choice = prob(50) ? ITEM_SLOT_RPOCKET : ITEM_SLOT_LPOCKET + if(prob(50) && living_target.can_put_in_hand(held_item)) + living_target.put_in_hand(held_item) + else if(held_item.mob_can_equip(living_target, pawn, pocket_choice, TRUE)) + living_target.equip_to_slot(held_item, pocket_choice) + + finish_action(controller, TRUE) + +/datum/ai_behavior/consume + required_distance = 1 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + action_cooldown = 2 SECONDS + +/datum/ai_behavior/consume/setup(datum/ai_controller/controller, obj/item/target) + . = ..() + controller.current_movement_target = target + +/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, obj/item/target) + . = ..() + var/mob/living/pawn = controller.pawn + + if(!(target in pawn.held_items)) + if(!pawn.put_in_hand_check(target)) + finish_action(controller, FALSE) + return + + pawn.put_in_hands(target) + + target.melee_attack_chain(pawn, pawn) + + if(QDELETED(target) || prob(10)) // Even if we don't finish it all we can randomly decide to be done + finish_action(controller, TRUE) diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm new file mode 100644 index 000000000000..822dae22eb23 --- /dev/null +++ b/code/datums/ai/monkey/monkey_behaviors.dm @@ -0,0 +1,279 @@ +/datum/ai_behavior/battle_screech/monkey + screeches = list("roar","screech") + +/datum/ai_behavior/monkey_equip + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/monkey_equip/finish_action(datum/ai_controller/controller, success) + . = ..() + + if(!success) //Don't try again on this item if we failed + var/list/item_blacklist = controller.blackboard[BB_MONKEY_BLACKLISTITEMS] + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + + item_blacklist[target] = TRUE + + controller.blackboard[BB_MONKEY_PICKUPTARGET] = null + +/datum/ai_behavior/monkey_equip/proc/equip_item(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + var/best_force = controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] + + if(!target) + finish_action(controller, FALSE) + return + + if(target.anchored) //Can't pick it up, so stop trying. + finish_action(controller, FALSE) + return + + // Strong weapon + else if(target.force > best_force) + living_pawn.drop_all_held_items() + living_pawn.put_in_hands(target) + controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] = target.force + finish_action(controller, TRUE) + return + + else if(target.slot_flags) //Clothing == top priority + living_pawn.dropItemToGround(target, TRUE) + living_pawn.update_icons() + if(!living_pawn.equip_to_appropriate_slot(target)) + finish_action(controller, FALSE) + return //Already wearing something, in the future this should probably replace the current item but the code didn't actually do that, and I dont want to support it right now. + finish_action(controller, TRUE) + return + + // EVERYTHING ELSE + else if(living_pawn.get_empty_held_index_for_side(LEFT_HANDS) || living_pawn.get_empty_held_index_for_side(RIGHT_HANDS)) + living_pawn.put_in_hands(target) + finish_action(controller, TRUE) + return + + finish_action(controller, FALSE) + +/datum/ai_behavior/monkey_equip/ground + required_distance = 0 + +/datum/ai_behavior/monkey_equip/ground/perform(delta_time, datum/ai_controller/controller) + equip_item(controller) + +/datum/ai_behavior/monkey_equip/pickpocket + +/datum/ai_behavior/monkey_equip/pickpocket/perform(delta_time, datum/ai_controller/controller) + + if(controller.blackboard[BB_MONKEY_PICKPOCKETING]) //We are pickpocketing, don't do ANYTHING!!!! + return + INVOKE_ASYNC(src, PROC_REF(attempt_pickpocket), controller) + +/datum/ai_behavior/monkey_equip/pickpocket/proc/attempt_pickpocket(datum/ai_controller/controller) + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + + var/mob/living/victim = target.loc + + var/mob/living/living_pawn = controller.pawn + + victim.visible_message("[living_pawn] starts trying to take [target] from [victim]!", "[living_pawn] tries to take [target]!") + + controller.blackboard[BB_MONKEY_PICKPOCKETING] = TRUE + + var/success = FALSE + + if(do_after(living_pawn, MONKEY_ITEM_SNATCH_DELAY, victim) && target) + + for(var/obj/item/I in victim.held_items) + if(I == target) + victim.visible_message("[living_pawn] snatches [target] from [victim].", "[living_pawn] snatched [target]!") + if(victim.temporarilyRemoveItemFromInventory(target)) + if(!QDELETED(target) && !equip_item(controller)) + target.forceMove(living_pawn.drop_location()) + success = TRUE + break + else + victim.visible_message("[living_pawn] tried to snatch [target] from [victim], but failed!", "[living_pawn] tried to grab [target]!") + + finish_action(controller, success) //We either fucked up or got the item. + +/datum/ai_behavior/monkey_equip/pickpocket/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.blackboard[BB_MONKEY_PICKPOCKETING] = FALSE + controller.blackboard[BB_MONKEY_PICKUPTARGET] = null + +/datum/ai_behavior/monkey_flee + +/datum/ai_behavior/monkey_flee/perform(delta_time, datum/ai_controller/controller) + . = ..() + + var/mob/living/living_pawn = controller.pawn + + if(living_pawn.health >= MONKEY_FLEE_HEALTH) + finish_action(controller, TRUE) //we're back in bussiness + + var/mob/living/target = null + + // flee from anyone who attacked us and we didn't beat down + for(var/mob/living/L in view(living_pawn, MONKEY_FLEE_VISION)) + if(controller.blackboard[BB_MONKEY_ENEMIES][L] && L.stat == CONSCIOUS) + target = L + break + + if(target) + walk_away(living_pawn, target, MONKEY_ENEMY_VISION, 5) + else + finish_action(controller, TRUE) + +/datum/ai_behavior/monkey_attack_mob + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration + +/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller) + . = ..() + + var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/mob/living/living_pawn = controller.pawn + + if(!target || target.stat != CONSCIOUS) + finish_action(controller, TRUE) //Target == owned + + if(isturf(target.loc) && !IS_DEAD_OR_INCAP(living_pawn)) // Check if they're a valid target + // check if target has a weapon + var/obj/item/W + for(var/obj/item/I in target.held_items) + if(!(I.item_flags & ABSTRACT)) + W = I + break + + // if the target has a weapon, chance to disarm them + if(W && DT_PROB(MONKEY_ATTACK_DISARM_PROB, delta_time)) + living_pawn.a_intent = INTENT_DISARM + monkey_attack(controller, target, delta_time) + else + living_pawn.a_intent = INTENT_HARM + monkey_attack(controller, target, delta_time) + + +/datum/ai_behavior/monkey_attack_mob/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = null + +/// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little +/datum/ai_behavior/monkey_attack_mob/proc/monkey_attack(datum/ai_controller/controller, mob/living/target, delta_time) + var/mob/living/living_pawn = controller.pawn + + if(living_pawn.next_move > world.time) + return + + living_pawn.changeNext_move(CLICK_CD_MELEE) //We play fair + + var/obj/item/weapon = locate(/obj/item) in living_pawn.held_items + + living_pawn.face_atom(target) + + if(isnull(controller.blackboard[BB_MONKEY_GUN_WORKED])) + controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE + + living_pawn.a_intent = INTENT_HARM + if(living_pawn.CanReach(target, weapon)) + if(weapon) + weapon.melee_attack_chain(living_pawn, target) + else + target.attack_paw(living_pawn) + controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // We reset their memory of the gun being 'broken' if they accomplish some other attack + else if(weapon) + var/atom/real_target = target + if(prob(10)) // Artificial miss + real_target = pick(oview(2, target)) + + var/obj/item/gun/gun = locate() in living_pawn.held_items + var/can_shoot = gun?.can_shoot() || FALSE + if(gun && controller.blackboard[BB_MONKEY_GUN_WORKED] && prob(95)) + // We attempt to attack even if we can't shoot so we get the effects of pulling the trigger + gun.afterattack(real_target, living_pawn, FALSE) + controller.blackboard[BB_MONKEY_GUN_WORKED] = can_shoot ? TRUE : prob(80) // Only 20% likely to notice it didn't work + if(can_shoot) + controller.blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] = TRUE + else + living_pawn.throw_item(real_target) + controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // 'worked' + + + + // no de-aggro + if(controller.blackboard[BB_MONKEY_AGRESSIVE]) + return + + if(DT_PROB(MONKEY_HATRED_REDUCTION_PROB, delta_time)) + controller.blackboard[BB_MONKEY_ENEMIES][target]-- + + // if we are not angry at our target, go back to idle + if(controller.blackboard[BB_MONKEY_ENEMIES][target] <= 0) + var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] + enemies.Remove(target) + if(controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] == target) + finish_action(controller, TRUE) + +/datum/ai_behavior/disposal_mob + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration + +/datum/ai_behavior/disposal_mob/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = null //Reset attack target + controller.blackboard[BB_MONKEY_DISPOSING] = FALSE //No longer disposing + controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = null //No target disposal + +/datum/ai_behavior/disposal_mob/perform(delta_time, datum/ai_controller/controller) + . = ..() + + if(controller.blackboard[BB_MONKEY_DISPOSING]) //We are disposing, don't do ANYTHING!!!! + return + + var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/mob/living/living_pawn = controller.pawn + + controller.current_movement_target = target + + if(target.pulledby != living_pawn && !HAS_AI_CONTROLLER_TYPE(target.pulledby, /datum/ai_controller/monkey)) //Dont steal from my fellow monkeys. + if(living_pawn.Adjacent(target) && isturf(target.loc)) + living_pawn.a_intent = INTENT_GRAB + target.grabbedby(living_pawn) + return //Do the rest next turn + + var/obj/machinery/disposal/disposal = controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] + controller.current_movement_target = disposal + + if(living_pawn.Adjacent(disposal)) + INVOKE_ASYNC(src, PROC_REF(try_disposal_mob), controller) //put him in! + else //This means we might be getting pissed! + return + +/datum/ai_behavior/disposal_mob/proc/try_disposal_mob(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/obj/machinery/disposal/disposal = controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] + + controller.blackboard[BB_MONKEY_DISPOSING] = TRUE + + if(target && disposal?.stuff_mob_in(target, living_pawn)) + disposal.flush() + finish_action(controller, TRUE) + + +/datum/ai_behavior/recruit_monkeys/perform(delta_time, datum/ai_controller/controller) + . = ..() + + controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN + var/mob/living/living_pawn = controller.pawn + + for(var/mob/living/L in view(living_pawn, MONKEY_ENEMY_VISION)) + if(!HAS_AI_CONTROLLER_TYPE(L, /datum/ai_controller/monkey)) + continue + + if(!DT_PROB(MONKEY_RECRUIT_PROB, delta_time)) + continue + var/datum/ai_controller/monkey/monkey_ai = L.ai_controller + var/atom/your_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/list/enemies = L.ai_controller.blackboard[BB_MONKEY_ENEMIES] + enemies[your_enemy] = MONKEY_RECRUIT_HATED_AMOUNT + monkey_ai.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN + finish_action(controller, TRUE) diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm new file mode 100644 index 000000000000..4cb8605d185f --- /dev/null +++ b/code/datums/ai/monkey/monkey_controller.dm @@ -0,0 +1,255 @@ +/* +AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a mob. What this means is that these datums +have ways of interacting with a specific mob and control it. +*/ +///OOK OOK OOK + +/datum/ai_controller/monkey + movement_delay = 0.4 SECONDS + planning_subtrees = list(/datum/ai_planning_subtree/monkey_tree) + blackboard = list( + BB_MONKEY_AGRESSIVE = FALSE, + BB_MONKEY_BEST_FORCE_FOUND = 0, + BB_MONKEY_ENEMIES = list(), + BB_MONKEY_BLACKLISTITEMS = list(), + BB_MONKEY_PICKUPTARGET = null, + BB_MONKEY_PICKPOCKETING = FALSE, + BB_MONKEY_DISPOSING = FALSE, + BB_MONKEY_TARGET_DISPOSAL = null, + BB_MONKEY_CURRENT_ATTACK_TARGET = null, + BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE, + BB_MONKEY_GUN_WORKED = TRUE, + BB_MONKEY_NEXT_HUNGRY = 0 + ) +/datum/ai_controller/monkey/angry + +/datum/ai_controller/monkey/angry/TryPossessPawn(atom/new_pawn) + . = ..() + if(. & AI_CONTROLLER_INCOMPATIBLE) + return + blackboard[BB_MONKEY_AGRESSIVE] = TRUE //Angry cunt + +/datum/ai_controller/monkey/TryPossessPawn(atom/new_pawn) + if(!isliving(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + + blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(0, 300) + + var/mob/living/living_pawn = new_pawn + RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby)) + RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) + RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_PAW, PROC_REF(on_attack_paw)) + RegisterSignal(new_pawn, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act)) + RegisterSignal(new_pawn, COMSIG_ATOM_HITBY, PROC_REF(on_hitby)) + RegisterSignal(new_pawn, COMSIG_LIVING_START_PULL, PROC_REF(on_startpulling)) + RegisterSignal(new_pawn, COMSIG_LIVING_TRY_SYRINGE, PROC_REF(on_try_syringe)) + RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk)) + RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, PROC_REF(on_attempt_cuff)) + RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, PROC_REF(update_movespeed)) + RegisterSignal(new_pawn, COMSIG_FOOD_EATEN, PROC_REF(on_eat)) + movement_delay = living_pawn.cached_multiplicative_slowdown + return ..() //Run parent at end + +/datum/ai_controller/monkey/UnpossessPawn(destroy) + UnregisterSignal(pawn, list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_LIVING_START_PULL,\ + COMSIG_LIVING_TRY_SYRINGE, COMSIG_ATOM_HULK_ATTACK, COMSIG_CARBON_CUFF_ATTEMPTED, COMSIG_MOB_MOVESPEED_UPDATED)) + return ..() //Run parent at end + +/datum/ai_controller/monkey/able_to_run() + . = ..() + var/mob/living/living_pawn = pawn + + if(IS_DEAD_OR_INCAP(living_pawn)) + return FALSE + +///re-used behavior pattern by monkeys for finding a weapon +/datum/ai_controller/monkey/proc/TryFindWeapon() + var/mob/living/living_pawn = pawn + + if(!locate(/obj/item) in living_pawn.held_items) + blackboard[BB_MONKEY_BEST_FORCE_FOUND] = 0 + + if(blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] && (locate(/obj/item/gun) in living_pawn.held_items)) + // We have a gun, what could we possibly want? + return FALSE + + var/obj/item/weapon + var/list/nearby_items = list() + for(var/obj/item/item in oview(2, living_pawn)) + nearby_items += item + + weapon = GetBestWeapon(nearby_items, living_pawn.held_items) + + var/pickpocket = FALSE + for(var/mob/living/carbon/human/human in oview(5, living_pawn)) + var/obj/item/held_weapon = GetBestWeapon(human.held_items + weapon, living_pawn.held_items) + if(held_weapon == weapon) // It's just the same one, not a held one + continue + pickpocket = TRUE + weapon = held_weapon + + if(!weapon || (weapon in living_pawn.held_items)) + return FALSE + + blackboard[BB_MONKEY_PICKUPTARGET] = weapon + current_movement_target = weapon + if(pickpocket) + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_equip/pickpocket)) + else + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_equip/ground)) + return TRUE + +/// Returns either the best weapon from the given choices or null if held weapons are better +/datum/ai_controller/monkey/proc/GetBestWeapon(list/choices, list/held_weapons) + var/gun_neurons_activated = blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] + var/top_force = 0 + var/obj/item/top_force_item + for(var/obj/item/item as anything in held_weapons) + if(!item) + continue + if(blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && istype(item, /obj/item/gun)) + // We have a gun, why bother looking for something inferior + // Also yes it is intentional that monkeys dont know how to pick the best gun + return item + if(item.force > top_force) + top_force = item.force + top_force_item = item + + for(var/obj/item/item as anything in choices) + if(!item) + continue + if(blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && istype(item, /obj/item/gun)) + return item + if(item.force <= top_force) + continue + top_force_item = item + top_force = item.force + + return top_force_item + +/datum/ai_controller/monkey/proc/TryFindFood() + . = FALSE + var/mob/living/living_pawn = pawn + + // Held items + + var/list/food_candidates = list() + for(var/obj/item as anything in living_pawn.held_items) + if(!item || !IsEdible(item)) + continue + food_candidates += item + + for(var/obj/item/candidate in oview(2, living_pawn)) + if(!IsEdible(candidate)) + continue + food_candidates += candidate + + if(length(food_candidates)) + var/obj/item/best_held = GetBestWeapon(null, living_pawn.held_items) + for(var/obj/item/held as anything in living_pawn.held_items) + if(!held || held == best_held) + continue + living_pawn.dropItemToGround(held) + + AddBehavior(/datum/ai_behavior/consume, pick(food_candidates)) + return TRUE + +/datum/ai_controller/monkey/proc/IsEdible(obj/item/thing) + if(IS_EDIBLE(thing)) + return TRUE + if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass)) + var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing + if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid! + return TRUE + return FALSE + +//When idle just kinda fuck around. +/datum/ai_controller/monkey/PerformIdleBehavior(delta_time) + var/mob/living/living_pawn = pawn + + if(DT_PROB(25, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) + step(living_pawn, pick(GLOB.cardinals)) + else if(DT_PROB(5, delta_time)) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick("screech")) + else if(DT_PROB(1, delta_time)) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick("scratch","jump","roll","tail")) + +///Reactive events to being hit +/datum/ai_controller/monkey/proc/retaliate(mob/living/L) + var/list/enemies = blackboard[BB_MONKEY_ENEMIES] + enemies[L] += MONKEY_HATRED_AMOUNT + +/datum/ai_controller/monkey/proc/on_attackby(datum/source, obj/item/I, mob/user) + SIGNAL_HANDLER + if(I.force && I.damtype != STAMINA) + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attack_hand(datum/source, mob/living/L) + SIGNAL_HANDLER + if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) + retaliate(L) + else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) + retaliate(L) + +/datum/ai_controller/monkey/proc/on_attack_paw(datum/source, mob/living/L) + SIGNAL_HANDLER + if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) + retaliate(L) + else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) + retaliate(L) + +/datum/ai_controller/monkey/proc/on_bullet_act(datum/source, obj/projectile/Proj) + SIGNAL_HANDLER + var/mob/living/living_pawn = pawn + if(istype(Proj , /obj/projectile/beam)||istype(Proj, /obj/projectile/bullet)) + if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE)) + if(!Proj.nodamage && Proj.damage < living_pawn.health && isliving(Proj.firer)) + retaliate(Proj.firer) + +/datum/ai_controller/monkey/proc/on_hitby(datum/source, atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + if(istype(AM, /obj/item)) + var/mob/living/living_pawn = pawn + var/obj/item/I = AM + if(I.throwforce < living_pawn.health && ishuman(I.thrownby)) + var/mob/living/carbon/human/H = I.thrownby + retaliate(H) + +/datum/ai_controller/monkey/proc/on_startpulling(datum/source, atom/movable/puller, state, force) + SIGNAL_HANDLER + var/mob/living/living_pawn = pawn + if(!IS_DEAD_OR_INCAP(living_pawn) && prob(MONKEY_PULL_AGGRO_PROB)) // nuh uh you don't pull me! + retaliate(living_pawn.pulledby) + return TRUE + +/datum/ai_controller/monkey/proc/on_try_syringe(datum/source, mob/user) + SIGNAL_HANDLER + // chance of monkey retaliation + if(prob(MONKEY_SYRINGE_RETALIATION_PROB)) + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attack_hulk(datum/source, mob/user) + SIGNAL_HANDLER + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attempt_cuff(datum/source, mob/user) + SIGNAL_HANDLER + // chance of monkey retaliation + if(prob(MONKEY_CUFF_RETALIATION_PROB)) + retaliate(user) + +/datum/ai_controller/monkey/proc/update_movespeed(mob/living/pawn) + SIGNAL_HANDLER + movement_delay = pawn.cached_multiplicative_slowdown + +/datum/ai_controller/monkey/proc/target_del(target) + SIGNAL_HANDLER + blackboard[BB_MONKEY_BLACKLISTITEMS] -= target + +/datum/ai_controller/monkey/proc/on_eat(mob/living/pawn) + SIGNAL_HANDLER + blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(120, 600) SECONDS diff --git a/code/datums/ai/monkey/monkey_subtrees.dm b/code/datums/ai/monkey/monkey_subtrees.dm new file mode 100644 index 000000000000..4e7317de5a56 --- /dev/null +++ b/code/datums/ai/monkey/monkey_subtrees.dm @@ -0,0 +1,84 @@ +/datum/ai_planning_subtree/monkey_tree/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time) + var/mob/living/living_pawn = controller.pawn + + if(SHOULD_RESIST(living_pawn) && DT_PROB(MONKEY_RESIST_PROB, delta_time)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/resist)) //BRO IM ON FUCKING FIRE BRO + return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY. + + var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] + + if(HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) //Not a pacifist? lets try some combat behavior. + return + + var/mob/living/selected_enemy + if(length(enemies) || controller.blackboard[BB_MONKEY_AGRESSIVE]) //We have enemies or are pissed + var/list/valids = list() + for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, living_pawn)) + if(possible_enemy == living_pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?) + continue + // Weighted list, so the closer they are the more likely they are to be chosen as the enemy + valids[possible_enemy] = CEILING(100 / (get_dist(living_pawn, possible_enemy) || 1), 1) + + selected_enemy = pick_weight(valids) + + if(selected_enemy) + if(!selected_enemy.stat) //He's up, get him! + if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_flee)) + return //I'm running fuck you guys + + if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing. + return SUBTREE_RETURN_FINISH_PLANNING + + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy + controller.current_movement_target = selected_enemy + if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/recruit_monkeys)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/battle_screech/monkey)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_attack_mob)) + return SUBTREE_RETURN_FINISH_PLANNING //Focus on this + + else //He's down, can we disposal him? + var/obj/machinery/disposal/bodyDisposal = locate(/obj/machinery/disposal/) in view(MONKEY_ENEMY_VISION, living_pawn) + if(bodyDisposal) + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy + controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = bodyDisposal + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/disposal_mob)) + return SUBTREE_RETURN_FINISH_PLANNING + + if(prob(5)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/use_in_hand)) + + if(selected_enemy || !DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time)) + return + + if(world.time >= controller.blackboard[BB_MONKEY_NEXT_HUNGRY] && controller.TryFindFood()) + return + + if(prob(50)) + var/list/possible_targets = list() + for(var/atom/thing in view(2, living_pawn)) + if(!thing.mouse_opacity) + continue + if(thing.IsObscured()) + continue + possible_targets += thing + var/atom/target = pick(possible_targets) + if(target) + controller.current_movement_target = target + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/use_on_object)) + return + + if(prob(5) && (locate(/obj/item) in living_pawn.held_items)) + var/list/possible_receivers = list() + for(var/mob/living/candidate in oview(2, controller.pawn)) + possible_receivers += candidate + + if(length(possible_receivers)) + var/mob/living/target = pick(possible_receivers) + controller.current_movement_target = target + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/give)) + return + + controller.TryFindWeapon() diff --git a/code/datums/ai/movement/_ai_movement.dm b/code/datums/ai/movement/_ai_movement.dm new file mode 100644 index 000000000000..c9d47bc6d66b --- /dev/null +++ b/code/datums/ai/movement/_ai_movement.dm @@ -0,0 +1,19 @@ +///This datum is an abstract class that can be overriden for different types of movement +/datum/ai_movement + ///Assoc list ist of controllers that are currently moving as key, and what they are moving to as value + var/list/moving_controllers = list() + ///How many times a given controller can fail on their route before they just give up + var/max_pathing_attempts + +/datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target) + controller.pathing_attempts = 0 + if(!moving_controllers.len) + START_PROCESSING(SSai_movement, src) + moving_controllers[controller] = current_movement_target + +/datum/ai_movement/proc/stop_moving_towards(datum/ai_controller/controller) + controller.pathing_attempts = 0 + moving_controllers -= controller + + if(!moving_controllers.len) + STOP_PROCESSING(SSai_movement, src) diff --git a/code/datums/ai/movement/ai_movement_dumb.dm b/code/datums/ai/movement/ai_movement_dumb.dm new file mode 100644 index 000000000000..0ce64669d373 --- /dev/null +++ b/code/datums/ai/movement/ai_movement_dumb.dm @@ -0,0 +1,27 @@ +///The most braindead type of movement, bee-line to the target with no concern of whats infront of us. +/datum/ai_movement/dumb + max_pathing_attempts = 16 + +///Put your movement behavior in here! +/datum/ai_movement/dumb/process(delta_time) + for(var/datum/ai_controller/controller as anything in moving_controllers) + if(!COOLDOWN_FINISHED(controller, movement_cooldown)) + continue + COOLDOWN_START(controller, movement_cooldown, controller.movement_delay) + + var/atom/movable/movable_pawn = controller.pawn + + if(!isturf(movable_pawn.loc)) //No moving if not on a turf + continue + + var/current_loc = get_turf(movable_pawn) + + var/turf/target_turf = get_step_towards(movable_pawn, controller.current_movement_target) + + if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs)) + movable_pawn.Move(target_turf, get_dir(current_loc, target_turf)) + + if(current_loc == get_turf(movable_pawn)) //Did we even move after trying to move? + controller.pathing_attempts++ + if(controller.pathing_attempts >= max_pathing_attempts) + controller.CancelActions() diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm new file mode 100644 index 000000000000..ea05b0fc899e --- /dev/null +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -0,0 +1,61 @@ +/** + * This movement datum represents smart-pathing + */ +/datum/ai_movement/jps + max_pathing_attempts = 4 + +///Put your movement behavior in here! +/datum/ai_movement/jps/process(delta_time) + for(var/datum/ai_controller/controller as anything in moving_controllers) + if(!COOLDOWN_FINISHED(controller, movement_cooldown)) + continue + COOLDOWN_START(controller, movement_cooldown, controller.movement_delay) + + var/atom/movable/movable_pawn = controller.pawn + if(!isturf(movable_pawn.loc)) //No moving if not on a turf + continue + + var/minimum_distance = controller.max_target_distance + // right now I'm just taking the shortest minimum distance of our current behaviors, at some point in the future + // we should let whatever sets the current_movement_target also set the min distance and max path length + // (or at least cache it on the controller) + if(LAZYLEN(controller.current_behaviors)) + for(var/datum/ai_behavior/iter_behavior as anything in controller.current_behaviors) + if(iter_behavior.required_distance < minimum_distance) + minimum_distance = iter_behavior.required_distance + + if(get_dist(movable_pawn, controller.current_movement_target) <= minimum_distance) + continue + + var/generate_path = FALSE // set to TRUE when we either have no path, or we failed a step + if(length(controller.movement_path)) + var/turf/next_step = controller.movement_path[1] + movable_pawn.Move(next_step) + + // this check if we're on exactly the next tile may be overly brittle for dense pawns who may get bumped slightly + // to the side while moving but could maybe still follow their path without needing a whole new path + if(get_turf(movable_pawn) == next_step) + controller.movement_path.Cut(1,2) + else + generate_path = TRUE + else + generate_path = TRUE + + if(generate_path) + if(!COOLDOWN_FINISHED(controller, repath_cooldown)) + continue + controller.pathing_attempts++ + if(controller.pathing_attempts >= max_pathing_attempts) + controller.CancelActions() + continue + + COOLDOWN_START(controller, repath_cooldown, 2 SECONDS) + controller.movement_path = get_path_to(movable_pawn, controller.current_movement_target, AI_MAX_PATH_LENGTH, minimum_distance, id=controller.get_access()) + +/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target) + controller.movement_path = null + return ..() + +/datum/ai_movement/jps/stop_moving_towards(datum/ai_controller/controller) + controller.movement_path = null + return ..() diff --git a/code/datums/components/mood.dm b/code/datums/components/mood.dm index a37fd00c3b5b..130cf3572ebc 100644 --- a/code/datums/components/mood.dm +++ b/code/datums/components/mood.dm @@ -92,7 +92,7 @@ msg += "[event.description]\n" // now we dont have to put \n in every moodlet description else msg += "I don't have much of a reaction to anything right now.\n" - to_chat(user, examine_block(msg)) + to_chat(user, boxed_message(msg)) ///Called after moodevent/s have been added/removed. /datum/component/mood/proc/update_mood() diff --git a/code/datums/components/spinny.dm b/code/datums/components/spinny.dm new file mode 100644 index 000000000000..cdf5262ab31b --- /dev/null +++ b/code/datums/components/spinny.dm @@ -0,0 +1,33 @@ +/** + * spinny.dm + * + * It's a component that spins things a whole bunch, like [proc/dance_rotate] but without the sleeps +*/ +/datum/component/spinny + dupe_mode = COMPONENT_DUPE_UNIQUE + /// How many turns are left? + var/steps_left + /// Turns clockwise by default, or counterclockwise if the reverse argument is TRUE + var/turn_degrees = 90 + +/datum/component/spinny/Initialize(steps = 12, reverse = FALSE) + if(!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + steps_left = steps + turn_degrees = (reverse ? -90 : 90) + START_PROCESSING(SSfastprocess, src) + +/datum/component/spinny/Destroy(force, silent) + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/component/spinny/process(delta_time) + steps_left-- + var/atom/spinny_boy = parent + if(!istype(spinny_boy) || steps_left <= 0) + qdel(src) + return + + // 25% chance to make 2 turns instead of 1 since the old dance_rotate wasn't strictly clockwise/counterclockwise + spinny_boy.setDir(turn(spinny_boy.dir, turn_degrees * (prob(25) ? 2 : 1))) diff --git a/code/datums/mind.dm b/code/datums/mind.dm index ea9f6918706b..1b0d456ebacd 100644 --- a/code/datums/mind.dm +++ b/code/datums/mind.dm @@ -247,7 +247,7 @@ var/datum/skill/the_skill = i msg += "[initial(the_skill.name)] - [get_skill_level_name(the_skill)]\n" msg += "" - to_chat(user, examine_block(msg)) + to_chat(user, boxed_message(msg)) /datum/mind/proc/set_death_time() SIGNAL_HANDLER diff --git a/code/datums/mutations/body.dm b/code/datums/mutations/body.dm index 0954c2a35bc8..d520c3bae5ed 100644 --- a/code/datums/mutations/body.dm +++ b/code/datums/mutations/body.dm @@ -179,11 +179,11 @@ /datum/mutation/human/race/on_acquiring(mob/living/carbon/human/owner) if(..()) return - . = owner.monkeyize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE) + . = owner.monkeyize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE | TR_KEEPAI) /datum/mutation/human/race/on_losing(mob/living/carbon/monkey/owner) if(owner && istype(owner) && owner.stat != DEAD && (owner.dna.mutations.Remove(src))) - . = owner.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE) + . = owner.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE | TR_KEEPAI) /datum/mutation/human/glow name = "Glowy" diff --git a/code/datums/votes/_vote_datum.dm b/code/datums/votes/_vote_datum.dm index 7118695bf1bb..e387e7fba48e 100644 --- a/code/datums/votes/_vote_datum.dm +++ b/code/datums/votes/_vote_datum.dm @@ -169,13 +169,14 @@ * Return a formatted string of text to be displayed to everyone. */ /datum/vote/proc/get_result_text(list/all_winners, real_winner, list/non_voters) + var/title_text = "" var/returned_text = "" if(override_question) - returned_text += span_bold(override_question) + title_text += span_bold(override_question) else - returned_text += span_bold("[capitalize(name)] Vote") + title_text += span_bold("[capitalize(name)] Vote") - returned_text += "\nWinner Selection: " + returned_text += "Winner Selection: " switch(winner_method) if(VOTE_WINNER_METHOD_NONE) returned_text += "None" @@ -215,7 +216,7 @@ returned_text += "\n" returned_text += get_winner_text(all_winners, real_winner, non_voters) - return returned_text + return fieldset_block(title_text, returned_text, "boxed_message purple_box") /** * Gets the text that displays the winning options within the result text. diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 6f1a0069e5c9..75a36e1aa677 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -136,6 +136,9 @@ ///List of smoothing groups this atom can smooth with. If this is null and atom is smooth, it smooths only with itself. var/list/canSmoothWith = null + ///AI controller that controls this atom. type on init, then turned into an instance during runtime + var/datum/ai_controller/ai_controller + /// The icon file of the connector to use when smoothing. /// Use of connectors requires the smoothing flags SMOOTH_BITMASK and SMOOTH_CONNECTORS. var/connector_icon = null @@ -265,6 +268,7 @@ set_custom_materials(temp_list) ComponentInitialize() + InitializeAIController() return INITIALIZE_HINT_NORMAL @@ -311,6 +315,7 @@ LAZYCLEARLIST(managed_overlays) QDEL_NULL(light) + QDEL_NULL(ai_controller) if(smoothing_flags & SMOOTH_QUEUED) SSicon_smooth.remove_from_queues(src) @@ -737,6 +742,7 @@ * throw lots of items around - singularity being a notable example) */ /atom/proc/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) + SEND_SIGNAL(src, COMSIG_ATOM_HITBY, AM, skipcatch, hitpush, blocked, throwingdatum) if(density && !has_gravity(AM)) //thrown stuff bounces off dense stuff in no grav, unless the thrown stuff ends up inside what it hit(embedding, bola, etc...). addtimer(CALLBACK(src, PROC_REF(hitby_react), AM), 2) @@ -1068,6 +1074,7 @@ VV_DROPDOWN_OPTION(VV_HK_RADIATE, "Radiate") VV_DROPDOWN_OPTION(VV_HK_EDIT_FILTERS, "Edit Filters") VV_DROPDOWN_OPTION(VV_HK_SELL, "Export Item") + VV_DROPDOWN_OPTION(VV_HK_ADD_AI, "Add AI controller") /atom/vv_do_topic(list/href_list) . = ..() @@ -1112,6 +1119,15 @@ var/strength = input(usr, "Choose the radiation strength.", "Choose the strength.") as num|null if(!isnull(strength)) AddComponent(/datum/component/radioactive, strength, src) + + if(href_list[VV_HK_ADD_AI]) + if(!check_rights(R_VAREDIT)) + return + var/result = input(usr, "Choose the AI controller to apply to this atom WARNING: Not all AI works on all atoms.", "AI controller") as null|anything in subtypesof(/datum/ai_controller) + if(!result) + return + ai_controller = new result(src) + if(href_list[VV_HK_MODIFY_TRANSFORM] && check_rights(R_VAREDIT)) var/result = input(usr, "Choose the transformation to apply","Transform Mod") as null|anything in list("Scale","Translate","Rotate") var/matrix/M = transform @@ -1310,6 +1326,8 @@ ///Deconstruct act /atom/proc/deconstruct_act(mob/living/user, obj/item/I) + if(flags_1 & NODECONSTRUCT_1) + return TRUE return SEND_SIGNAL(src, COMSIG_ATOM_DECONSTRUCT_ACT, user, I) ///Generate a tag for this atom @@ -1708,3 +1726,12 @@ */ /atom/proc/setClosed() return + +/** +* Instantiates the AI controller of this atom. Override this if you want to assign variables first. +* +* This will work fine without manually passing arguments. ++*/ +/atom/proc/InitializeAIController() + if(ai_controller) + ai_controller = new ai_controller(src) diff --git a/code/game/machinery/constructable_frame.dm b/code/game/machinery/constructable_frame.dm index a62780aad05a..d41117bdbb62 100644 --- a/code/game/machinery/constructable_frame.dm +++ b/code/game/machinery/constructable_frame.dm @@ -23,6 +23,8 @@ /obj/structure/frame/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if(I.use_tool(src, user, 3 SECONDS, volume=0)) diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm index 9bad37507a35..4b3c7a8eae08 100644 --- a/code/game/machinery/cryopod.dm +++ b/code/game/machinery/cryopod.dm @@ -450,7 +450,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/computer/cryopod/retro, 17) wakeupmessage += " A sickly feeling along with the pangs of hunger greet you upon your awakening." sleepyhead.set_nutrition(100) sleepyhead.apply_effect(rand(3,10), EFFECT_DROWSY) - to_chat(sleepyhead, span_danger(examine_block(wakeupmessage))) + to_chat(sleepyhead, span_danger(boxed_message(wakeupmessage))) /obj/machinery/cryopod/syndicate icon_state = "sleeper_s-open" diff --git a/code/game/machinery/deployable.dm b/code/game/machinery/deployable.dm index 3dedf5887d0c..24c417793668 100644 --- a/code/game/machinery/deployable.dm +++ b/code/game/machinery/deployable.dm @@ -81,6 +81,8 @@ /obj/structure/barricade/wooden/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if (I.use_tool(src, user, 2 SECONDS, volume=0)) diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm index 2239212c7440..7e157953e850 100644 --- a/code/game/machinery/doors/airlock.dm +++ b/code/game/machinery/doors/airlock.dm @@ -1234,6 +1234,8 @@ /obj/machinery/door/airlock/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE var/decon_time = 5 SECONDS diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm index ea51bac01b50..93225b2af9a9 100644 --- a/code/game/machinery/porta_turret/portable_turret.dm +++ b/code/game/machinery/porta_turret/portable_turret.dm @@ -513,9 +513,6 @@ if(!is_type_in_typecache(target_mob, dangerous_fauna)) return FALSE - if(ismonkey(target_mob)) - var/mob/living/carbon/monkey/monke = target_mob - return monke.mode == MONKEY_HUNT && target(target_mob) if(istype(target_mob, /mob/living/simple_animal/hostile/retaliate)) var/mob/living/simple_animal/hostile/retaliate/target_animal = target_mob return length(target_animal.enemies) && target(target_mob) diff --git a/code/game/objects/effects/temporary_visuals/projectiles/impact.dm b/code/game/objects/effects/temporary_visuals/projectiles/impact.dm index c00938077751..91459a55c772 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/impact.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/impact.dm @@ -52,3 +52,7 @@ /obj/effect/projectile/impact/pgf/rifle name = "beam impact" icon_state = "impact_pgf_rifle" + +/obj/effect/projectile/impact/gauss + name = "gauss impact" + icon_state = "hc_gauss" diff --git a/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm b/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm index b850681c713f..97646ae50e32 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/muzzle.dm @@ -41,3 +41,6 @@ /obj/effect/projectile/muzzle/pgf/rifle icon_state = "muzzle_pgf_rifle" + +/obj/effect/projectile/muzzle/gauss + icon_state = "muzzle_gauss_rifle" diff --git a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm index d7e867ed03ac..f0ab5580e2bc 100644 --- a/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm +++ b/code/game/objects/effects/temporary_visuals/projectiles/tracer.dm @@ -79,3 +79,6 @@ /obj/effect/projectile/tracer/pgf/rifle icon_state = "beam_pgf_rifle" + +/obj/effect/projectile/tracer/gauss + icon_state = "hc_gauss" diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 23de618975a8..17bcfd78d5c9 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -867,7 +867,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb . = "" /obj/item/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) - return + return SEND_SIGNAL(src, COMSIG_ATOM_HITBY, AM, skipcatch, hitpush, blocked, throwingdatum) /obj/item/attack_hulk(mob/living/carbon/human/user) return FALSE diff --git a/code/game/objects/items/devices/scanners.dm b/code/game/objects/items/devices/scanners.dm index 4df753123bba..a981d08fb05f 100644 --- a/code/game/objects/items/devices/scanners.dm +++ b/code/game/objects/items/devices/scanners.dm @@ -381,7 +381,7 @@ GENE SCANNER SEND_SIGNAL(M, COMSIG_NANITE_SCAN, user, FALSE) // we handled the last
so we don't need handholding - to_chat(user, examine_block(jointext(render_list, "")), trailing_newline = FALSE, type = MESSAGE_TYPE_INFO) + to_chat(user, boxed_message(jointext(render_list, "")), trailing_newline = FALSE, type = MESSAGE_TYPE_INFO) /proc/chemscan(mob/living/user, mob/living/M) if(istype(M) && M.reagents) @@ -399,7 +399,7 @@ GENE SCANNER else render_list += "Subject is not addicted to any reagents.\n" // we handled the last
so we don't need handholding - to_chat(user, examine_block(jointext(render_list, "")), type = MESSAGE_TYPE_INFO) + to_chat(user, boxed_message(jointext(render_list, "")), type = MESSAGE_TYPE_INFO) /obj/item/healthanalyzer/verb/toggle_mode() set name = "Switch Verbosity" @@ -612,7 +612,7 @@ GENE SCANNER \nInstability of the last fusion reaction: [round(cached_scan_results["fusion"], 0.01)]." // we let the join apply newlines so we do need handholding - to_chat(user, examine_block(jointext(render_list, "\n")), type = MESSAGE_TYPE_INFO) + to_chat(user, boxed_message(jointext(render_list, "\n")), type = MESSAGE_TYPE_INFO) return TRUE /obj/item/nanite_scanner diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm index f36c27bb244d..66d829baee25 100644 --- a/code/game/objects/items/handcuffs.dm +++ b/code/game/objects/items/handcuffs.dm @@ -40,17 +40,13 @@ if(!istype(C)) return + SEND_SIGNAL(C, COMSIG_CARBON_CUFF_ATTEMPTED, user) + if(iscarbon(user) && (HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50))) to_chat(user, "Uh... how do those things work?!") apply_cuffs(user,user) return - // chance of monkey retaliation - if(ismonkey(C) && prob(MONKEY_CUFF_RETALIATION_PROB)) - var/mob/living/carbon/monkey/M - M = C - M.retaliate(user) - if(!C.handcuffed) if(C.canBeHandcuffed()) C.visible_message("[user] is trying to put [src.name] on [C]!", \ diff --git a/code/game/objects/items/storage/belt.dm b/code/game/objects/items/storage/belt.dm index 966fc8899276..de4dcb5684cc 100644 --- a/code/game/objects/items/storage/belt.dm +++ b/code/game/objects/items/storage/belt.dm @@ -94,11 +94,11 @@ /obj/item/storage/belt/utility/chief/full/PopulateContents() new /obj/item/screwdriver/power(src) new /obj/item/crowbar/power(src) + new /obj/item/weldingtool/electric(src) new /obj/item/multitool(src) new /obj/item/stack/cable_coil(src,MAXCOIL,pick("red","yellow","orange")) new /obj/item/extinguisher/mini(src) new /obj/item/analyzer(src) - //much roomier now that we've managed to remove two tools /obj/item/storage/belt/utility/full/PopulateContents() new /obj/item/screwdriver(src) @@ -149,6 +149,7 @@ /obj/item/storage/belt/utility/full/ert/PopulateContents() new /obj/item/screwdriver/power(src) new /obj/item/crowbar/power(src) + new /obj/item/weldingtool/electric(src) new /obj/item/multitool(src) new /obj/item/construction/rcd/combat(src) new /obj/item/extinguisher/mini(src) diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index 0391de85eb33..aa63701ce0e9 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -243,11 +243,20 @@ /obj/get_dumping_location(datum/component/storage/source,mob/user) return get_turf(src) -/obj/proc/CanAStarPass(ID, dir, caller) - if(ismovable(caller)) - var/atom/movable/AM = caller - if(AM.pass_flags & pass_flags_self) - return TRUE +/** + * This proc is used for telling whether something can pass by this object in a given direction, for use by the pathfinding system. + * + * Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely + * multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible. + * + * Arguments: + * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference + * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions + * * caller- The movable we're checking pass flags for, if we're making any such checks + **/ +/obj/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) + if(istype(caller) && (caller.pass_flags & pass_flags_self)) + return TRUE . = !density /obj/proc/check_uplink_validity() @@ -387,6 +396,12 @@ return FALSE return TRUE +/obj/deconstruct_act(mob/living/user, obj/item/I) + if(resistance_flags & INDESTRUCTIBLE) + to_chat(user, span_warning("[src] cannot be deconstructed!")) + return FALSE + return ..() + /obj/analyzer_act(mob/living/user, obj/item/I) if(atmosanalyzer_scan(user, src)) return TRUE diff --git a/code/game/objects/structures/door_assembly.dm b/code/game/objects/structures/door_assembly.dm index 675a135c493b..bf5ea3b38913 100644 --- a/code/game/objects/structures/door_assembly.dm +++ b/code/game/objects/structures/door_assembly.dm @@ -347,6 +347,8 @@ /obj/structure/door_assembly/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if (I.use_tool(src, user, 3 SECONDS, volume=100)) diff --git a/code/game/objects/structures/dresser.dm b/code/game/objects/structures/dresser.dm index 61944f81198c..5cc59d28ead5 100644 --- a/code/game/objects/structures/dresser.dm +++ b/code/game/objects/structures/dresser.dm @@ -38,6 +38,8 @@ /obj/structure/dresser/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE to_chat(user, span_notice("You begin to disassemble [src].")) if(I.use_tool(src, user, 10, volume=50)) to_chat(user, span_notice("You successfully deconstruct [src].")) diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index 5f35e69d098e..85af7c9bb8eb 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -214,6 +214,8 @@ /obj/structure/girder/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if(I.use_tool(src, user, 3 SECONDS, volume=0)) @@ -302,11 +304,10 @@ if((mover.pass_flags & PASSGRILLE) || istype(mover, /obj/projectile)) return prob(girderpasschance) -/obj/structure/girder/CanAStarPass(ID, dir, caller) +/obj/structure/girder/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSGRILLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSGRILLE) /obj/structure/girder/deconstruct(disassembled = TRUE) if(!(flags_1 & NODECONSTRUCT_1)) diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm index bec5e5bf452b..7e2527c11dae 100644 --- a/code/game/objects/structures/grille.dm +++ b/code/game/objects/structures/grille.dm @@ -135,11 +135,10 @@ if(!. && istype(mover, /obj/projectile)) return prob(30) -/obj/structure/grille/CanAStarPass(ID, dir, caller) +/obj/structure/grille/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSGRILLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSGRILLE) /obj/structure/grille/attackby(obj/item/W, mob/user, params) user.changeNext_move(CLICK_CD_MELEE) @@ -233,6 +232,8 @@ /obj/structure/grille/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if (I.use_tool(src, user, 1 SECONDS, volume=100)) diff --git a/code/game/objects/structures/lattice.dm b/code/game/objects/structures/lattice.dm index 9aaefb8c014e..7755f94798a4 100644 --- a/code/game/objects/structures/lattice.dm +++ b/code/game/objects/structures/lattice.dm @@ -42,6 +42,8 @@ /obj/structure/lattice/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if(I.use_tool(src, user, 1 SECONDS, volume=0)) diff --git a/code/game/objects/structures/platforms.dm b/code/game/objects/structures/platforms.dm index a6431e8b7630..3d668f666c16 100644 --- a/code/game/objects/structures/platforms.dm +++ b/code/game/objects/structures/platforms.dm @@ -127,6 +127,8 @@ /obj/structure/platform/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if(I.use_tool(src, user, 3 SECONDS, volume=0)) diff --git a/code/game/objects/structures/railings.dm b/code/game/objects/structures/railings.dm index 3217f8274b32..b4bf99f17469 100644 --- a/code/game/objects/structures/railings.dm +++ b/code/game/objects/structures/railings.dm @@ -73,6 +73,8 @@ /obj/structure/railing/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if (I.use_tool(src, user, 3 SECONDS, volume=0)) diff --git a/code/game/objects/structures/salvaging.dm b/code/game/objects/structures/salvaging.dm index ccb76bd81c4a..e8c98cff4694 100644 --- a/code/game/objects/structures/salvaging.dm +++ b/code/game/objects/structures/salvaging.dm @@ -36,6 +36,8 @@ /obj/structure/salvageable/deconstruct_act(mob/living/user, obj/item/tool) . = ..() + if(.) + return FALSE user.visible_message("[user] starts slicing [src].", \ "You start salvaging anything useful from [src]...") if(tool.use_tool(src, user, 6 SECONDS)) diff --git a/code/game/objects/structures/tables_racks.dm b/code/game/objects/structures/tables_racks.dm index 6f1e59ebad5e..7d670cfc3cd4 100644 --- a/code/game/objects/structures/tables_racks.dm +++ b/code/game/objects/structures/tables_racks.dm @@ -229,6 +229,8 @@ /obj/structure/table/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if (I.use_tool(src, user, 1 SECONDS, volume=0)) diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 3f4ffc747ca9..e7a0fa946e23 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -296,6 +296,8 @@ /obj/structure/window/deconstruct_act(mob/living/user, obj/item/I) . = ..() + if(.) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE if (I.use_tool(src, user, decon_time, volume=100)) @@ -377,7 +379,7 @@ /obj/structure/window/get_dumping_location(obj/item/storage/source,mob/user) return null -/obj/structure/window/CanAStarPass(ID, to_dir) +/obj/structure/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) if(!density) return TRUE if(fulltile || (dir == to_dir)) diff --git a/code/game/turfs/closed/_closed.dm b/code/game/turfs/closed/_closed.dm index ea0c0d4ed4b1..a76d3715e414 100644 --- a/code/game/turfs/closed/_closed.dm +++ b/code/game/turfs/closed/_closed.dm @@ -271,6 +271,9 @@ /turf/closed/deconstruct_act(mob/living/user, obj/item/I) var/act_duration = breakdown_duration + if(breakdown_duration == -1) + to_chat(user, span_warning("[src] cannot be deconstructed!")) + return FALSE if(!I.tool_start_check(user, amount=0)) return FALSE to_chat(user, "You begin slicing through the outer plating...") diff --git a/code/game/turfs/closed/indestructible.dm b/code/game/turfs/closed/indestructible.dm index 9e55858970d2..3363424db56d 100644 --- a/code/game/turfs/closed/indestructible.dm +++ b/code/game/turfs/closed/indestructible.dm @@ -4,6 +4,7 @@ icon = 'icons/turf/walls.dmi' explosion_block = 50 max_integrity = 10000000 + breakdown_duration = -1 /turf/closed/indestructible/TerraformTurf(path, new_baseturf, flags, defer_change = FALSE, ignore_air = FALSE) return diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 4da6e25703bb..1f9dfc08f7da 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -676,3 +676,23 @@ GLOBAL_LIST_EMPTY(created_baseturf_lists) /turf/bullet_act(obj/projectile/hitting_projectile) . = ..() bullet_hit_sfx(hitting_projectile) + +/** + * Returns adjacent turfs to this turf that are reachable, in all cardinal directions + * + * Arguments: + * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach + * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf + * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? +*/ +/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only) + var/static/space_type_cache = typecacheof(/turf/open/space) + . = list() + + for(var/iter_dir in GLOB.cardinals) + var/turf/turf_to_check = get_step(src,iter_dir) + if(!turf_to_check || (simulated_only && space_type_cache[turf_to_check.type])) + continue + if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, caller, ID)) + continue + . += turf_to_check diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 61d8b2757836..2406d7d51564 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -692,7 +692,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that var/datum/asset/fuckywucky = get_asset_datum(/datum/asset/simple/fuckywucky) fuckywucky.send(m) SEND_SOUND(m, 'sound/misc/fuckywucky.ogg') - to_chat(m, span_purple(examine_block(""))) + to_chat(m, span_purple(boxed_message(""))) addtimer(CALLBACK(src, PROC_REF(restore_fucky_wucky)), 600) diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm index 033b71b6df5b..0ed035002f09 100644 --- a/code/modules/antagonists/changeling/powers/tiny_prick.dm +++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm @@ -47,7 +47,7 @@ return if(!isturf(user.loc)) return - if(!AStar(user, target.loc, /turf/proc/Distance, changeling.sting_range, simulated_only = FALSE)) + if(!get_path_to(user, target, max_distance = changeling.sting_range, simulated_only = FALSE)) return if(target.mind && target.mind.has_antag_datum(/datum/antagonist/changeling)) sting_feedback(user, target) @@ -106,7 +106,7 @@ C.real_name = NewDNA.real_name NewDNA.transfer_identity(C) if(ismonkey(C)) - C.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG) + C.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG | TR_KEEPAI) C.updateappearance(mutcolor_update=1) diff --git a/code/modules/buildmode/submodes/advanced.dm b/code/modules/buildmode/submodes/advanced.dm index 4fd6f30ca52b..f9eb00250c46 100644 --- a/code/modules/buildmode/submodes/advanced.dm +++ b/code/modules/buildmode/submodes/advanced.dm @@ -6,7 +6,7 @@ // of the currently selected path /datum/buildmode_mode/advanced/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Set object type")] -> Right Mouse Button on buildmode button\n\ [span_bold("Copy object type")] -> Left Mouse Button + Alt on turf/obj\n\ [span_bold("Place objects")] -> Left Mouse Button on turf/obj\n\ diff --git a/code/modules/buildmode/submodes/area_edit.dm b/code/modules/buildmode/submodes/area_edit.dm index b0d8925c0c85..c16744213e86 100644 --- a/code/modules/buildmode/submodes/area_edit.dm +++ b/code/modules/buildmode/submodes/area_edit.dm @@ -22,7 +22,7 @@ return ..() /datum/buildmode_mode/area_edit/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Select corner")] -> Left Mouse Button on obj/turf/mob\n\ [span_bold("Paint area")] -> Left Mouse Button + Alt on turf/obj/mob\n\ [span_bold("Select area to paint")] -> Right Mouse Button on obj/turf/mob\n\ diff --git a/code/modules/buildmode/submodes/basic.dm b/code/modules/buildmode/submodes/basic.dm index 180331e94ba8..b35a0b240ad1 100644 --- a/code/modules/buildmode/submodes/basic.dm +++ b/code/modules/buildmode/submodes/basic.dm @@ -2,7 +2,7 @@ key = "basic" /datum/buildmode_mode/basic/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Construct / Upgrade")] -> Left Mouse Button\n\ [span_bold("Deconstruct / Delete / Downgrade")] -> Right Mouse Button\n\ [span_bold("R-Window")] -> Left Mouse Button + Ctrl\n\ diff --git a/code/modules/buildmode/submodes/boom.dm b/code/modules/buildmode/submodes/boom.dm index f0837735c641..a2ba9cadeaf0 100644 --- a/code/modules/buildmode/submodes/boom.dm +++ b/code/modules/buildmode/submodes/boom.dm @@ -8,7 +8,7 @@ var/flames = -1 /datum/buildmode_mode/boom/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Set explosion destructiveness")] -> Right Mouse Button on buildmode button\n\ [span_bold("Kaboom")] -> Mouse Button on obj\n\n\ [span_warning("NOTE:")] Using the \"Config/Launch Supplypod\" verb allows you to do this in an IC way (i.e., making a cruise missile come down from the sky and explode wherever you click!)")) diff --git a/code/modules/buildmode/submodes/copy.dm b/code/modules/buildmode/submodes/copy.dm index 4ac7f9ec4796..6f555e9dc84c 100644 --- a/code/modules/buildmode/submodes/copy.dm +++ b/code/modules/buildmode/submodes/copy.dm @@ -7,7 +7,7 @@ return ..() /datum/buildmode_mode/copy/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Spawn a copy of selected target")] -> Left Mouse Button on obj/turf/mob\n\ [span_bold("Select target to copy")] -> Right Mouse Button on obj/mob")) ) diff --git a/code/modules/buildmode/submodes/delete.dm b/code/modules/buildmode/submodes/delete.dm index 4ef4fe37156c..9d35dba3d779 100644 --- a/code/modules/buildmode/submodes/delete.dm +++ b/code/modules/buildmode/submodes/delete.dm @@ -2,7 +2,7 @@ key = "delete" /datum/buildmode_mode/delete/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Delete an object")] -> Left Mouse Button on obj/turf/mob\n\ [span_bold("Delete all objects of a type")] -> Right Mouse Button on obj/turf/mob")) ) diff --git a/code/modules/buildmode/submodes/fill.dm b/code/modules/buildmode/submodes/fill.dm index 75f4f2d221b7..20428738d7d6 100644 --- a/code/modules/buildmode/submodes/fill.dm +++ b/code/modules/buildmode/submodes/fill.dm @@ -7,7 +7,7 @@ var/atom/objholder = null /datum/buildmode_mode/fill/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Select corner")] -> Left Mouse Button on turf/obj/mob\n\ [span_bold("Delete region")] -> Left Mouse Button + Alt on turf/obj/mob\n\ [span_bold("Select object type")] -> Right Mouse Button on buildmode button")) diff --git a/code/modules/buildmode/submodes/lightmaker.dm b/code/modules/buildmode/submodes/lightmaker.dm index 2b50343c641a..3b83ee64e4f9 100644 --- a/code/modules/buildmode/submodes/lightmaker.dm +++ b/code/modules/buildmode/submodes/lightmaker.dm @@ -6,7 +6,7 @@ var/light_color = COLOR_WHITE /datum/buildmode_mode/lightmaker/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Left Click")] -> Create light\n\ [span_bold("Right Click")] -> Delete light\n\ [span_bold("Right Click on Build Mode Button")] -> Change light properties")) diff --git a/code/modules/buildmode/submodes/outfit.dm b/code/modules/buildmode/submodes/outfit.dm index 56faf5d507cc..d1bb144357a3 100644 --- a/code/modules/buildmode/submodes/outfit.dm +++ b/code/modules/buildmode/submodes/outfit.dm @@ -7,7 +7,7 @@ return ..() /datum/buildmode_mode/outfit/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Select outfit to equip")] -> Right Mouse Button on buildmode button\n\ [span_bold("Equip the selected outfit")] -> Left Mouse Button on mob/living/carbon/human\n\ [span_bold("Strip and delete current outfit")] -> Right Mouse Button on mob/living/carbon/human")) diff --git a/code/modules/buildmode/submodes/proccall.dm b/code/modules/buildmode/submodes/proccall.dm index 47e7130aa386..996e5736aef0 100644 --- a/code/modules/buildmode/submodes/proccall.dm +++ b/code/modules/buildmode/submodes/proccall.dm @@ -6,7 +6,7 @@ var/list/proc_args = null /datum/buildmode_mode/proccall/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Choose procedure and arguments")] -> Right Mouse Button on buildmode button\n\ [span_bold("Apply procedure on object")] -> Left Mouse Button on machinery")) ) diff --git a/code/modules/buildmode/submodes/relocate_to.dm b/code/modules/buildmode/submodes/relocate_to.dm index 3a2d52bc5d17..4d9f94ba21a8 100644 --- a/code/modules/buildmode/submodes/relocate_to.dm +++ b/code/modules/buildmode/submodes/relocate_to.dm @@ -8,7 +8,7 @@ return ..() /datum/buildmode_mode/relocate_to/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Select")] -> Left Mouse Button on obj/mob\n\ [span_bold("Relocate")] -> Right Mouse Button on turf/obj/mob")) ) diff --git a/code/modules/buildmode/submodes/throwing.dm b/code/modules/buildmode/submodes/throwing.dm index 0539d2ec4f9f..14d843d1005b 100644 --- a/code/modules/buildmode/submodes/throwing.dm +++ b/code/modules/buildmode/submodes/throwing.dm @@ -8,7 +8,7 @@ return ..() /datum/buildmode_mode/throwing/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Select")] -> Left Mouse Button on turf/obj/mob\n\ [span_bold("Throw")] -> Right Mouse Button on turf/obj/mob")) ) diff --git a/code/modules/buildmode/submodes/tweakcomps.dm b/code/modules/buildmode/submodes/tweakcomps.dm index 4072f8dd8f2f..cab04143707a 100644 --- a/code/modules/buildmode/submodes/tweakcomps.dm +++ b/code/modules/buildmode/submodes/tweakcomps.dm @@ -4,7 +4,7 @@ var/rating = null /datum/buildmode_mode/tweakcomps/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Choose the rating of the components")] -> Right Mouse Button on buildmode button\n\ [span_bold("Sets the chosen rating of the components on the machinery")] -> Left Mouse Button on machinery")) ) diff --git a/code/modules/buildmode/submodes/variable_edit.dm b/code/modules/buildmode/submodes/variable_edit.dm index 728c909860b5..25c0ab8d394a 100644 --- a/code/modules/buildmode/submodes/variable_edit.dm +++ b/code/modules/buildmode/submodes/variable_edit.dm @@ -10,7 +10,7 @@ return ..() /datum/buildmode_mode/varedit/show_help(client/target_client) - to_chat(target_client, span_purple(examine_block( + to_chat(target_client, span_purple(boxed_message( "[span_bold("Select var(type) & value")] -> Right Mouse Button on buildmode button\n\ [span_bold("Set var(type) & value")] -> Left Mouse Button on turf/obj/mob\n\ [span_bold("Reset var's value")] -> Right Mouse Button on turf/obj/mob")) diff --git a/code/modules/cargo/packs/ammo.dm b/code/modules/cargo/packs/ammo.dm index 077d5f49a683..54a555997568 100644 --- a/code/modules/cargo/packs/ammo.dm +++ b/code/modules/cargo/packs/ammo.dm @@ -424,6 +424,12 @@ contains = list(/obj/item/storage/box/ammo/ferropellet) cost = 250 +/datum/supply_pack/ammo/hcpellets + name = "High Conductivity Pellet Box Crate" + desc = "Contains a 48-round high conductivity pellet ammo box for gauss guns such as the Claris." + contains = list(/obj/item/storage/box/ammo/ferropellet/hc) + cost = 500 + /* ferroslugs */ /datum/supply_pack/ammo/ferroslugboxcrate @@ -432,6 +438,12 @@ contains = list(/obj/item/storage/box/ammo/ferroslug) cost = 250 +/datum/supply_pack/ammo/hcslugs + name = "High Conductivity Slug Box Crate" + desc = "Contains a twenty-round high conductivity slug for gauss guns such as the Model-H." + contains = list(/obj/item/storage/box/ammo/ferroslug/hc) + cost = 500 + /* ferro lances */ /datum/supply_pack/ammo/ferrolanceboxcrate @@ -439,3 +451,9 @@ desc = "Contains a 48-round box for high-powered gauss guns such as the GAR assault rifle." contains = list(/obj/item/storage/box/ammo/ferrolance) cost = 250 + +/datum/supply_pack/ammo/ferrolanceboxcrate + name = "High Conductivity Lance Box Crate" + desc = "Contains a 48-round box for high-powered gauss guns such as the GAR assault rifle." + contains = list(/obj/item/storage/box/ammo/ferrolance/hc) + cost = 500 diff --git a/code/modules/cargo/packs/cybernetics.dm b/code/modules/cargo/packs/cybernetics.dm new file mode 100644 index 000000000000..37683060f8cc --- /dev/null +++ b/code/modules/cargo/packs/cybernetics.dm @@ -0,0 +1,79 @@ +/datum/supply_pack/cybernetic + group = "Cybernetics" + crate_type = /obj/structure/closet/crate/medical + + +// CYBERNETICS + +/datum/supply_pack/cybernetic/cyberarm_surgset + name = "Integrated Surgical Toolset Kit" + desc = "The latest in advanced medical cybernetics, the Surgical Toolset can be installed in the arms and act as a concealed kit to render surgical aid at striking efficiency." + cost = 4500 + contains = list(/obj/item/organ/cyberimp/arm/surgery) + crate_name = "implant crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 + +/datum/supply_pack/cybernetic/cyberarm_toolset + name = "Integrated Engineering Toolset Kit" + desc = "A recent innovation in engineering labor, this functions as a concealed toolkit for use in all manner of engineering operations. It is installed in the arms." + cost = 2000 + contains = list(/obj/item/organ/cyberimp/arm/toolset) + crate_name = "implant crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 + +/datum/supply_pack/cybernetic/cyberhud_sec + name = "Integrated Security HUD" + desc = "A HUD over the user's eyes that allows one to view security and IFF data on the field. Reports of recalls and blindness are merely disinformation by competitors." + cost = 2000 + contains = list(/obj/item/organ/cyberimp/eyes/hud/security) + crate_name = "implant crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 + +/datum/supply_pack/cybernetic/cyberhud_med + name = "Integrated Medical Analysis HUD" + desc = "A HUD over the user's eyes that allows one to view medical and heart-rate data on the field. Reports of recalls and blindness are merely disinformation by competitors." + cost = 2000 + contains = list(/obj/item/organ/cyberimp/eyes/hud/medical) + crate_name = "implant crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 + +/datum/supply_pack/cybernetic/cyberhud_diagnostic + name = "Integrated Exosuit Diagnostic HUD" + desc = "A HUD over the user's eyes that allows one to view an uplink of Powered Exoskeleton information. Reports of recalls and blindness are merely disinformation by competitors." + cost = 750 + contains = list(/obj/item/organ/cyberimp/eyes/hud/diagnostic) + crate_name = "implant crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 + +/datum/supply_pack/cybernetic/cyber_breathing + name = "Integrated Breathing Tube" + desc = "Commonly used for those with medical conditions relating to breathing, this implant provides a port to attach portable oxygen canisters to that pumps air directly into your lungs. Keep port sealed when not in use." + cost = 1000 + contains = list(/obj/item/organ/cyberimp/mouth/breathing_tube) + crate_name = "implant crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 + +/datum/supply_pack/cybernetic/cyberorgans + name = "Cybernetic Organs Replacement Pack" + desc = "Precision-manufactured replacement organs for those suffering catastrophic organ failure. Keep crate sealed until use, contaminants may cause rejection." + cost = 2000 + contains = list(/obj/item/organ/lungs/cybernetic/tier2, + /obj/item/organ/stomach/cybernetic/tier2, + /obj/item/organ/liver/cybernetic/tier2, + /obj/item/organ/heart/cybernetic/tier2) + crate_name = "organs crate" + crate_type = /obj/structure/closet/crate/freezer + faction = /datum/faction/syndicate/cybersun + faction_discount = 50 diff --git a/code/modules/hydroponics/grown.dm b/code/modules/hydroponics/grown.dm index 4f2b2420ee55..482a8aae3c89 100644 --- a/code/modules/hydroponics/grown.dm +++ b/code/modules/hydroponics/grown.dm @@ -81,7 +81,7 @@ if(reag_txt) msg += reag_txt - to_chat(user, examine_block(msg)) + to_chat(user, boxed_message(msg)) else if(seed) for(var/datum/plant_gene/trait/T in seed.genes) diff --git a/code/modules/hydroponics/growninedible.dm b/code/modules/hydroponics/growninedible.dm index 8ab59cc6f658..58a317319774 100644 --- a/code/modules/hydroponics/growninedible.dm +++ b/code/modules/hydroponics/growninedible.dm @@ -40,7 +40,7 @@ var/msg = "This is \a [src]\n" if(seed) msg += seed.get_analyzer_text() - to_chat(usr, examine_block(msg)) + to_chat(usr, boxed_message(msg)) return /obj/item/grown/proc/add_juice() diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm index 13faaf5f15a5..ca857e88dcca 100644 --- a/code/modules/hydroponics/hydroponics.dm +++ b/code/modules/hydroponics/hydroponics.dm @@ -570,7 +570,7 @@ msg += "Toxicity level: [span_notice("[toxic] / [HYDRO_MAX_TOXIC]")]\n" msg += "Water level: [span_notice("[waterlevel] / [maxwater]")]\n" msg += "Nutrition level: [span_notice("[reagents.total_volume] / [maxnutri]")]\n" - to_chat(user, examine_block(msg)) + to_chat(user, boxed_message(msg)) return else if(istype(O, /obj/item/cultivator)) diff --git a/code/modules/hydroponics/seeds.dm b/code/modules/hydroponics/seeds.dm index 65f47357899a..e02c3f197394 100644 --- a/code/modules/hydroponics/seeds.dm +++ b/code/modules/hydroponics/seeds.dm @@ -444,7 +444,7 @@ for(var/datum/plant_gene/reagent/Gene in genes) msg += "\n- [Gene.get_name()] -" msg += "\n*---------*" - to_chat(user, examine_block(msg)) + to_chat(user, boxed_message(msg)) return diff --git a/code/modules/mapping/mapping_helpers.dm b/code/modules/mapping/mapping_helpers.dm index 4912ccd0dba2..27fb53a7624d 100644 --- a/code/modules/mapping/mapping_helpers.dm +++ b/code/modules/mapping/mapping_helpers.dm @@ -503,6 +503,7 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava) log_mapping("[src] failed to find a crate at [AREACOORD(src)]") else shelve(crate) + qdel(src) /obj/effect/mapping_helpers/crate_shelve/proc/shelve(crate) var/obj/structure/crate_shelf/shelf = locate(/obj/structure/crate_shelf) in range(range, crate) diff --git a/code/modules/mob/living/brain/brain_item.dm b/code/modules/mob/living/brain/brain_item.dm index 5fe96021461b..967efa40d0bb 100644 --- a/code/modules/mob/living/brain/brain_item.dm +++ b/code/modules/mob/living/brain/brain_item.dm @@ -349,14 +349,14 @@ return TRUE //Proc to use when directly adding a trauma to the brain, so extra args can be given -/obj/item/organ/brain/proc/gain_trauma(datum/brain_trauma/trauma, resilience, ...) +/obj/item/organ/brain/proc/gain_trauma(datum/brain_trauma/trauma, resilience, natural_gain = FALSE, ...) var/list/arguments = list() - if(args.len > 2) - arguments = args.Copy(3) - . = brain_gain_trauma(trauma, resilience, arguments) + if(args.len > 3) + arguments = args.Copy(4) + . = brain_gain_trauma(trauma, resilience, natural_gain, arguments) //Direct trauma gaining proc. Necessary to assign a trauma to its brain. Avoid using directly. -/obj/item/organ/brain/proc/brain_gain_trauma(datum/brain_trauma/trauma, resilience, list/arguments) +/obj/item/organ/brain/proc/brain_gain_trauma(datum/brain_trauma/trauma, resilience, list/arguments, natural_gain = FALSE) if(!can_gain_trauma(trauma, resilience)) return @@ -378,7 +378,10 @@ if(owner) actual_trauma.owner = owner actual_trauma.on_gain() - if(resilience) + if(natural_gain) + if(actual_trauma.resilience >= TRAUMA_RESILIENCE_LOBOTOMY) + actual_trauma.resilience = TRAUMA_RESILIENCE_SURGERY + else if(resilience) actual_trauma.resilience = resilience . = actual_trauma if(owner?.client) @@ -396,7 +399,7 @@ return var/trauma_type = pick(possible_traumas) - gain_trauma(trauma_type, resilience) + gain_trauma(trauma_type, resilience, natural_gain) //Cure a random trauma of a certain resilience level /obj/item/organ/brain/proc/cure_trauma_type(brain_trauma_type = /datum/brain_trauma, resilience = TRAUMA_RESILIENCE_BASIC) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 5276cf514d65..bcf78e60c8d1 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -108,6 +108,7 @@ /mob/proc/throw_item(atom/target) SEND_SIGNAL(src, COMSIG_MOB_THROW, target) + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CARBON_THROW_THING, src, target) return /mob/living/carbon/throw_item(atom/target) diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 51815282406d..48747f0106f8 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -138,6 +138,9 @@ //ATTACK HAND IGNORING PARENT RETURN VALUE /mob/living/carbon/attack_hand(mob/living/carbon/human/user) + if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_HAND, user) & COMPONENT_CANCEL_ATTACK_CHAIN) + . = TRUE + for(var/datum/surgery/S in surgeries) if(body_position != LYING_DOWN && S.lying_required) continue diff --git a/code/modules/mob/living/carbon/emote.dm b/code/modules/mob/living/carbon/emote.dm index 358fa0626092..d96bbd72531a 100644 --- a/code/modules/mob/living/carbon/emote.dm +++ b/code/modules/mob/living/carbon/emote.dm @@ -82,7 +82,22 @@ key = "screech" key_third_person = "screeches" message = "screeches." - mob_type_allowed_typecache = list(/mob/living/carbon/monkey, /mob/living/carbon/alien) + mob_type_allowed_typecache = list(/mob/living/carbon/monkey) + emote_type = EMOTE_AUDIBLE + +/datum/emote/living/carbon/screech/get_sound(mob/living/user) + return pick('sound/creatures/monkey/monkey_screech_1.ogg', + 'sound/creatures/monkey/monkey_screech_2.ogg', + 'sound/creatures/monkey/monkey_screech_3.ogg', + 'sound/creatures/monkey/monkey_screech_4.ogg', + 'sound/creatures/monkey/monkey_screech_5.ogg', + 'sound/creatures/monkey/monkey_screech_6.ogg', + 'sound/creatures/monkey/monkey_screech_7.ogg') + +/datum/emote/living/carbon/screech/roar + key = "roar" + key_third_person = "roars" + message = "roars." /datum/emote/living/carbon/sign key = "sign" diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 0c8782129698..c4a447b59d5b 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -7,6 +7,7 @@ var/t_him = p_them() var/t_has = p_have() var/t_is = p_are() + var/t_es = p_es() var/obscure_name var/list/obscured = check_obscured_slots() var/skipface = ((wear_mask?.flags_inv & HIDEFACE) || (head?.flags_inv & HIDEFACE)) @@ -330,6 +331,8 @@ if(HAS_TRAIT(src, TRAIT_DUMB)) msg += "[t_He] [t_has] a stupid expression on [t_his] face.\n" if(getorgan(/obj/item/organ/brain)) + if(ai_controller?.ai_status == AI_STATUS_ON) + msg += "[t_He] do[t_es]n't appear to be [t_him]self.\n" if(!key) msg += "[t_He] [t_is] totally catatonic. The stresses of life in deep-space must have been too much for [t_him]. Any recovery is unlikely.\n" else if(!client) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index a4d89a53b548..b33a751df628 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1289,6 +1289,9 @@ return known_name return . +/mob/living/carbon/human/monkeybrain + ai_controller = /datum/ai_controller/monkey + /mob/living/carbon/human/species var/race = null diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 081c94f4a8f9..4f3857841d7a 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -861,7 +861,7 @@ if(roundstart_quirks.len) combined_msg += "You have these quirks: [get_trait_string()]." - to_chat(src, examine_block(combined_msg.Join("\n"))) + to_chat(src, boxed_message(combined_msg.Join("\n"))) /mob/living/carbon/human/damage_clothes(damage_amount, damage_type = BRUTE, damage_flag = 0, def_zone) if(damage_type != BRUTE && damage_type != BURN) diff --git a/code/modules/mob/living/carbon/monkey/combat.dm b/code/modules/mob/living/carbon/monkey/combat.dm deleted file mode 100644 index 8fd4e89566c7..000000000000 --- a/code/modules/mob/living/carbon/monkey/combat.dm +++ /dev/null @@ -1,426 +0,0 @@ -#define MAX_RANGE_FIND 32 - -/mob/living/carbon/monkey - var/aggressive=0 // set to 1 using VV for an angry monkey - var/frustration=0 - var/pickupTimer=0 - var/list/enemies = list() - var/mob/living/target - var/obj/item/pickupTarget - var/mode = MONKEY_IDLE - var/list/myPath = list() - var/list/blacklistItems = list() - var/maxStepsTick = 6 - var/best_force = 0 - var/martial_art = new/datum/martial_art - var/resisting = FALSE - var/pickpocketing = FALSE - var/disposing_body = FALSE - var/obj/machinery/disposal/bodyDisposal = null - var/next_battle_screech = 0 - var/battle_screech_cooldown = 50 - -/mob/living/carbon/monkey/proc/IsStandingStill() - return resisting || pickpocketing || disposing_body - -// blocks -// taken from /mob/living/carbon/human/interactive/ -/mob/living/carbon/monkey/proc/walk2derpless(target) - if(!target || IsStandingStill()) - return 0 - - if(myPath.len <= 0) - myPath = get_path_to(src, get_turf(target), /turf/proc/Distance, MAX_RANGE_FIND + 1, 250,1) - - if(myPath) - if(myPath.len > 0) - for(var/i = 0; i < maxStepsTick; ++i) - if(!IsDeadOrIncap()) - if(myPath.len >= 1) - walk_to(src,myPath[1],0,5) - myPath -= myPath[1] - return 1 - - // failed to path correctly so just try to head straight for a bit - walk_to(src,get_turf(target),0,5) - sleep(1) - walk_to(src,0) - - return 0 - - -// taken from /mob/living/carbon/human/interactive/ -/mob/living/carbon/monkey/proc/IsDeadOrIncap() - return HAS_TRAIT(src, TRAIT_INCAPACITATED) || HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) - - -/mob/living/carbon/monkey/proc/battle_screech() - if(next_battle_screech < world.time) - emote(pick("roar","screech")) - for(var/mob/living/carbon/monkey/M in view(7,src)) - M.next_battle_screech = world.time + battle_screech_cooldown - -/mob/living/carbon/monkey/proc/equip_item(obj/item/I) - if(I.loc == src) - return TRUE - - if(I.anchored) - blacklistItems[I] ++ - return FALSE - - // WEAPONS - if(istype(I, /obj/item)) - var/obj/item/W = I - if(W.force >= best_force) - put_in_hands(W) - best_force = W.force - return TRUE - - // CLOTHING - else if(istype(I, /obj/item/clothing)) - var/obj/item/clothing/C = I - monkeyDrop(C) - addtimer(CALLBACK(src, PROC_REF(pickup_and_wear), C), 5) - return TRUE - - // EVERYTHING ELSE - else - if(!get_item_for_held_index(1) || !get_item_for_held_index(2)) - put_in_hands(I) - return TRUE - - blacklistItems[I] ++ - return FALSE - -/mob/living/carbon/monkey/proc/pickup_and_wear(obj/item/clothing/C) - if(!equip_to_appropriate_slot(C)) - monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn - addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5) - -/mob/living/carbon/monkey/resist_restraints() - var/obj/item/I = null - if(handcuffed) - I = handcuffed - else if(legcuffed) - I = legcuffed - if(I) - changeNext_move(CLICK_CD_BREAKOUT) - last_special = world.time + CLICK_CD_BREAKOUT - cuff_resist(I) - -/mob/living/carbon/monkey/proc/should_target(mob/living/L) - if(HAS_TRAIT(src, TRAIT_PACIFISM)) - return FALSE - - if(enemies[L]) - return TRUE - - // target non-monkey mobs when aggressive, with a small probability of monkey v monkey - if(aggressive && (!istype(L, /mob/living/carbon/monkey/) || prob(MONKEY_AGGRESSIVE_MVM_PROB))) - return TRUE - - return FALSE - -/mob/living/carbon/monkey/proc/handle_combat() - if(pickupTarget) - if(IsDeadOrIncap() || blacklistItems[pickupTarget] || HAS_TRAIT(pickupTarget, TRAIT_NODROP)) - pickupTarget = null - else - pickupTimer++ - if(pickupTimer >= 4) - blacklistItems[pickupTarget] ++ - pickupTarget = null - pickupTimer = 0 - else - INVOKE_ASYNC(src, PROC_REF(walk2derpless), pickupTarget.loc) - if(Adjacent(pickupTarget) || Adjacent(pickupTarget.loc)) // next to target - drop_all_held_items() // who cares about these items, i want that one! - if(isturf(pickupTarget.loc)) // on floor - equip_item(pickupTarget) - pickupTarget = null - pickupTimer = 0 - else if(ismob(pickupTarget.loc)) // in someones hand - var/mob/M = pickupTarget.loc - if(!pickpocketing) - pickpocketing = TRUE - M.visible_message("[src] starts trying to take [pickupTarget] from [M]!", "[src] tries to take [pickupTarget]!") - INVOKE_ASYNC(src, PROC_REF(pickpocket), M) - return TRUE - - switch(mode) - if(MONKEY_IDLE) // idle - if(enemies.len) - var/list/around = view(src, MONKEY_ENEMY_VISION) // scan for enemies - for(var/mob/living/L in around) - if(should_target(L)) - if(L.stat == CONSCIOUS) - battle_screech() - retaliate(L) - return TRUE - else - bodyDisposal = locate(/obj/machinery/disposal/) in around - if(bodyDisposal) - target = L - mode = MONKEY_DISPOSE - return TRUE - - // pickup any nearby objects - if(!pickupTarget) - var/obj/item/I = locate(/obj/item/) in oview(2,src) - if(I && !blacklistItems[I]) - pickupTarget = I - else - var/mob/living/carbon/human/H = locate(/mob/living/carbon/human/) in oview(2,src) - if(H) - pickupTarget = pick(H.held_items) - - if(MONKEY_HUNT) // hunting for attacker - if(health < MONKEY_FLEE_HEALTH) - mode = MONKEY_FLEE - return TRUE - - if(target != null) - INVOKE_ASYNC(src, PROC_REF(walk2derpless), target) - - // pickup any nearby weapon - if(!pickupTarget && prob(MONKEY_WEAPON_PROB)) - var/obj/item/W = locate(/obj/item/) in oview(2,src) - if(!locate(/obj/item) in held_items) - best_force = 0 - if(W && !blacklistItems[W] && W.force > best_force) - pickupTarget = W - - // recruit other monkies - var/list/around = view(src, MONKEY_ENEMY_VISION) - for(var/mob/living/carbon/monkey/M in around) - if(M.mode == MONKEY_IDLE && prob(MONKEY_RECRUIT_PROB)) - M.battle_screech() - M.target = target - M.mode = MONKEY_HUNT - - // switch targets - for(var/mob/living/L in around) - if(L != target && should_target(L) && L.stat == CONSCIOUS && prob(MONKEY_SWITCH_TARGET_PROB)) - target = L - return TRUE - - // if can't reach target for long enough, go idle - if(frustration >= MONKEY_HUNT_FRUSTRATION_LIMIT) - back_to_idle() - return TRUE - - if(target && target.stat == CONSCIOUS) // make sure target exists - if(Adjacent(target) && isturf(target.loc) && !IsDeadOrIncap()) // if right next to perp - - // check if target has a weapon - var/obj/item/W - for(var/obj/item/I in target.held_items) - if(!(I.item_flags & ABSTRACT)) - W = I - break - - // if the target has a weapon, chance to disarm them - if(W && prob(MONKEY_ATTACK_DISARM_PROB)) - pickupTarget = W - a_intent = INTENT_DISARM - monkey_attack(target) - - else - a_intent = INTENT_HARM - monkey_attack(target) - - return TRUE - - else // not next to perp - var/turf/olddist = get_dist(src, target) - if((get_dist(src, target)) >= (olddist)) - frustration++ - else - frustration = 0 - else - back_to_idle() - - if(MONKEY_FLEE) - var/list/around = view(src, MONKEY_FLEE_VISION) - target = null - - // flee from anyone who attacked us and we didn't beat down - for(var/mob/living/L in around) - if(enemies[L] && L.stat == CONSCIOUS) - target = L - - if(target != null) - walk_away(src, target, MONKEY_ENEMY_VISION, 5) - else - back_to_idle() - - return TRUE - - if(MONKEY_DISPOSE) - - // if can't dispose of body go back to idle - if(!target || !bodyDisposal || frustration >= MONKEY_DISPOSE_FRUSTRATION_LIMIT) - back_to_idle() - return TRUE - - if(target.pulledby != src && !istype(target.pulledby, /mob/living/carbon/monkey/)) - - INVOKE_ASYNC(src, PROC_REF(walk2derpless), target.loc) - - if(Adjacent(target) && isturf(target.loc)) - a_intent = INTENT_GRAB - target.grabbedby(src) - else - var/turf/olddist = get_dist(src, target) - if((get_dist(src, target)) >= (olddist)) - frustration++ - else - frustration = 0 - - else if(!disposing_body) - INVOKE_ASYNC(src, PROC_REF(walk2derpless), bodyDisposal.loc) - - if(Adjacent(bodyDisposal)) - disposing_body = TRUE - addtimer(CALLBACK(src, PROC_REF(stuff_mob_in)), 5) - - else - var/turf/olddist = get_dist(src, bodyDisposal) - if((get_dist(src, bodyDisposal)) >= (olddist)) - frustration++ - else - frustration = 0 - - return TRUE - - return IsStandingStill() - -/mob/living/carbon/monkey/proc/pickpocket(mob/M) - if(do_after(src, MONKEY_ITEM_SNATCH_DELAY, M) && pickupTarget) - for(var/obj/item/I in M.held_items) - if(I == pickupTarget) - M.visible_message("[src] snatches [pickupTarget] from [M].", "[src] snatched [pickupTarget]!") - if(M.temporarilyRemoveItemFromInventory(pickupTarget)) - if(!QDELETED(pickupTarget) && !equip_item(pickupTarget)) - pickupTarget.forceMove(drop_location()) - else - M.visible_message("[src] tried to snatch [pickupTarget] from [M], but failed!", "[src] tried to grab [pickupTarget]!") - pickpocketing = FALSE - pickupTarget = null - pickupTimer = 0 - -/mob/living/carbon/monkey/proc/stuff_mob_in() - if(bodyDisposal && target && Adjacent(bodyDisposal)) - bodyDisposal.stuff_mob_in(target, src) - disposing_body = FALSE - back_to_idle() - -/mob/living/carbon/monkey/proc/back_to_idle() - - if(pulling) - stop_pulling() - - mode = MONKEY_IDLE - target = null - a_intent = INTENT_HELP - frustration = 0 - walk_to(src,0) - -// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little -/mob/living/carbon/monkey/proc/monkey_attack(mob/living/L) - var/obj/item/Weapon = locate(/obj/item) in held_items - - // attack with weapon if we have one - if(Weapon) - Weapon.melee_attack_chain(src, L) - else - L.attack_paw(src) - - // no de-aggro - if(aggressive) - return - - // if we arn't enemies, we were likely recruited to attack this target, jobs done if we calm down so go back to idle - if(!enemies[L]) - if(target == L && prob(MONKEY_HATRED_REDUCTION_PROB)) - back_to_idle() - return // already de-aggroed - - if(prob(MONKEY_HATRED_REDUCTION_PROB)) - enemies[L] -- - - // if we are not angry at our target, go back to idle - if(enemies[L] <= 0) - enemies.Remove(L) - if(target == L) - back_to_idle() - -// get angry are a mob -/mob/living/carbon/monkey/proc/retaliate(mob/living/L) - mode = MONKEY_HUNT - target = L - if(L != src) - enemies[L] += MONKEY_HATRED_AMOUNT - - if(a_intent != INTENT_HARM) - battle_screech() - a_intent = INTENT_HARM - -/mob/living/carbon/monkey/attack_hand(mob/living/L) - if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) - retaliate(L) - else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) - retaliate(L) - return ..() - -/mob/living/carbon/monkey/attack_paw(mob/living/L) - if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) - retaliate(L) - else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) - retaliate(L) - return ..() - -/mob/living/carbon/monkey/attackby(obj/item/W, mob/user, params) - ..() - if((W.force) && (!target) && (W.damtype != STAMINA)) - retaliate(user) - -/mob/living/carbon/monkey/bullet_act(obj/projectile/Proj) - if(istype(Proj , /obj/projectile/beam)||istype(Proj, /obj/projectile/bullet)) - if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE)) - if(!Proj.nodamage && Proj.damage < src.health && isliving(Proj.firer)) - retaliate(Proj.firer) - . = ..() - -/mob/living/carbon/monkey/hitby(atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum) - if(istype(AM, /obj/item)) - var/obj/item/I = AM - if(I.throwforce < src.health && I.thrownby && ishuman(I.thrownby)) - var/mob/living/carbon/human/H = I.thrownby - retaliate(H) - ..() - -/mob/living/carbon/monkey/on_entered(datum/source, atom/movable/AM) - . = ..() - if(!IsDeadOrIncap() && ismob(AM) && target) - var/mob/living/carbon/monkey/M = AM - if(!istype(M) || !M) - return - knockOver(M) - return - -/mob/living/carbon/monkey/proc/monkeyDrop(obj/item/A) - if(A) - dropItemToGround(A, TRUE) - update_icons() - -/mob/living/carbon/monkey/grabbedby(mob/living/carbon/user) - . = ..() - if(!IsDeadOrIncap() && pulledby && (mode != MONKEY_IDLE || prob(MONKEY_PULL_AGGRO_PROB))) // nuh uh you don't pull me! - if(Adjacent(pulledby)) - a_intent = INTENT_DISARM - monkey_attack(pulledby) - retaliate(pulledby) - return TRUE - -#undef MAX_RANGE_FIND diff --git a/code/modules/mob/living/carbon/monkey/life.dm b/code/modules/mob/living/carbon/monkey/life.dm index b4469ea5b63c..01423b1aa2ee 100644 --- a/code/modules/mob/living/carbon/monkey/life.dm +++ b/code/modules/mob/living/carbon/monkey/life.dm @@ -1,33 +1,5 @@ - - /mob/living/carbon/monkey - -/mob/living/carbon/monkey/Life() - set invisibility = 0 - - if (notransform) - return - - if(..() && !IS_IN_STASIS(src)) - - if(!client) - if(stat == CONSCIOUS) - if(on_fire || buckled || HAS_TRAIT(src, TRAIT_RESTRAINED) || (pulledby && pulledby.grab_state > GRAB_PASSIVE)) - if(!resisting && prob(MONKEY_RESIST_PROB)) - resisting = TRUE - walk_to(src,0) - execute_resist() - else if(resisting) - resisting = FALSE - else if((mode == MONKEY_IDLE && !pickupTarget && !prob(MONKEY_SHENANIGAN_PROB)) || !handle_combat()) - if(prob(25) && (mobility_flags & MOBILITY_MOVE) && isturf(loc) && !pulledby) - step(src, pick(GLOB.cardinals)) - else if(prob(1)) - emote(pick("scratch","jump","roll","tail")) - else - walk_to(src,0) - /mob/living/carbon/monkey/handle_mutations_and_radiation() if(radiation) if(radiation > RAD_MOB_KNOCKDOWN && prob(RAD_MOB_KNOCKDOWN_PROB)) diff --git a/code/modules/mob/living/carbon/monkey/monkey.dm b/code/modules/mob/living/carbon/monkey/monkey.dm index 755c674a107d..6056ac83fa7d 100644 --- a/code/modules/mob/living/carbon/monkey/monkey.dm +++ b/code/modules/mob/living/carbon/monkey/monkey.dm @@ -26,6 +26,8 @@ hud_type = /datum/hud/monkey melee_damage_lower = 1 melee_damage_upper = 3 + ai_controller = /datum/ai_controller/monkey + faction = list("neutral", "monkey") /mob/living/carbon/monkey/Initialize(mapload, cubespawned=FALSE, mob/spawner) add_verb(src, /mob/living/proc/mob_sleep) @@ -169,10 +171,10 @@ return 1 /mob/living/carbon/monkey/angry - aggressive = TRUE /mob/living/carbon/monkey/angry/Initialize() . = ..() + ai_controller.blackboard[BB_MONKEY_AGRESSIVE] = TRUE if(prob(10)) var/obj/item/clothing/head/helmet/justice/escape/helmet = new(src) equip_to_slot_or_del(helmet,ITEM_SLOT_HEAD) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 99db31b26c0f..ba9b99822600 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1125,7 +1125,7 @@ if(G.trigger_guard == TRIGGER_GUARD_NONE) to_chat(src, "You are unable to fire this!") return FALSE - if(G.trigger_guard != TRIGGER_GUARD_ALLOW_ALL && !IsAdvancedToolUser()) + if(G.trigger_guard != TRIGGER_GUARD_ALLOW_ALL && (!IsAdvancedToolUser(src) && !HAS_TRAIT(src, TRAIT_GUN_NATURAL))) to_chat(src, "You try to fire [G], but can't use the trigger!") return FALSE return TRUE diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 3c7736c06230..1e81bd48b63f 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -564,7 +564,7 @@ Pass a positive integer as an argument to override a bot's default speed. var/datum/job/captain/All = new/datum/job/captain all_access.access = All.get_access() - set_path(get_path_to(src, waypoint, /turf/proc/Distance_cardinal, 0, 200, id=all_access)) + set_path(get_path_to(src, waypoint, 200, id=all_access)) calling_ai = caller //Link the AI to the bot! ai_waypoint = waypoint @@ -782,16 +782,16 @@ Pass a positive integer as an argument to override a bot's default speed. // given an optional turf to avoid /mob/living/simple_animal/bot/proc/calc_path(turf/avoid) check_bot_access() - set_path(get_path_to(src, patrol_target, /turf/proc/Distance_cardinal, 0, 120, id=access_card, exclude=avoid)) + set_path(get_path_to(src, patrol_target, 120, id=access_card, exclude=avoid)) /mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid) check_bot_access() INVOKE_ASYNC(src, PROC_REF(do_calc_summon_path), avoid) /mob/living/simple_animal/bot/proc/do_calc_summon_path(turf/avoid) - set_path(get_path_to(src, summon_target, /turf/proc/Distance_cardinal, 0, 150, id=access_card, exclude=avoid)) + set_path(get_path_to(src, summon_target, 150, id=access_card, exclude=avoid)) if(!length(path)) //Cannot reach target. Give up and announce the issue. - speak("Summon command failed, destination unreachable.",radio_channel) + speak("Summon command failed, destination unreachable.", radio_channel) bot_reset() /mob/living/simple_animal/bot/proc/summon_step() @@ -816,7 +816,9 @@ Pass a positive integer as an argument to override a bot's default speed. calc_summon_path() /mob/living/simple_animal/bot/proc/summon_step_not_moved() - calc_summon_path() + //calc_summon_path() + speak("Summon command failed, destination unreachable.",radio_channel) + bot_reset() tries = 0 /mob/living/simple_animal/bot/Bump(atom/A) //Leave no door unopened! diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm index e53b675c95bc..aad4a7a63f86 100644 --- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm +++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm @@ -261,11 +261,11 @@ mode = BOT_IDLE return - if(target && path.len == 0 && (get_dist(src,target) > 1)) - path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 30, id=access_card) + if(target && (!path || path.len == 0) && (get_dist(src,target) > 1)) + path = get_path_to(src, target, 30, id=access_card) mode = BOT_MOVING if(!path.len) //try to get closer if you can't reach the target directly - path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 30, 1, id=access_card) + path = get_path_to(src, target, 30, id=access_card) if(!path.len) //Do not chase a target we cannot reach. add_to_ignore(target) target = null diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm index 0fabc6c7fb53..1b04fbb51669 100644 --- a/code/modules/mob/living/simple_animal/bot/firebot.dm +++ b/code/modules/mob/living/simple_animal/bot/firebot.dm @@ -269,7 +269,7 @@ if(get_dist(src, target_fire) > 2) - path = get_path_to(src, get_turf(target_fire), /turf/proc/Distance_cardinal, 0, 30, 1, id=access_card) + path = get_path_to(src, target_fire, 30, 1, id=access_card) mode = BOT_MOVING if(!length(path)) soft_reset() diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm index 980f12897e70..662386649186 100644 --- a/code/modules/mob/living/simple_animal/bot/floorbot.dm +++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm @@ -255,9 +255,9 @@ if(path.len == 0) if(!isturf(target)) var/turf/TL = get_turf(target) - path = get_path_to(src, TL, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, TL, 30, id=access_card,simulated_only = FALSE) else - path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, target, 30, id=access_card,simulated_only = FALSE) if(!bot_move(target)) add_to_ignore(target) diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm index 22d68c8a6190..6bcd39abb6a6 100644 --- a/code/modules/mob/living/simple_animal/bot/medbot.dm +++ b/code/modules/mob/living/simple_animal/bot/medbot.dm @@ -413,10 +413,10 @@ return if(patient && path.len == 0 && (get_dist(src,patient) > 1)) - path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,id=access_card) + path = get_path_to(src, patient, 30,id=access_card) mode = BOT_MOVING if(!path.len) //try to get closer if you can't reach the patient directly - path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,1,id=access_card) + path = get_path_to(src, patient, 30,1,id=access_card) if(!path.len) //Do not chase a patient we cannot reach. soft_reset() diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index 1c10311f7b3c..59a6125b5fbd 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -615,7 +615,7 @@ // calculates a path to the current destination // given an optional turf to avoid /mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null) - path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 250, id=access_card, exclude=avoid) + path = get_path_to(src, target, 250, id=access_card, exclude=avoid) // sets the current destination // signals all beacons matching the delivery code diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm index 2a47d4f0c220..6dc3bbf55048 100644 --- a/code/modules/mob/living/simple_animal/friendly/dog.dm +++ b/code/modules/mob/living/simple_animal/friendly/dog.dm @@ -16,65 +16,11 @@ see_in_dark = 5 speak_chance = 1 turns_per_move = 10 - var/turns_since_scan = 0 - var/obj/movement_target + ai_controller = /datum/ai_controller/dog + stop_automated_movement = TRUE footstep_type = FOOTSTEP_MOB_CLAW -/mob/living/simple_animal/pet/dog/Life() - ..() - - //Feeding, chasing food, FOOOOODDDD - if(!stat && !resting && !buckled) - turns_since_scan++ - if(turns_since_scan > 5) - turns_since_scan = 0 - if((movement_target) && !(isturf(movement_target.loc) || ishuman(movement_target.loc))) - movement_target = null - stop_automated_movement = 0 - if(!movement_target || !(movement_target.loc in oview(src, 3))) - movement_target = null - stop_automated_movement = 0 - for(var/obj/item/reagent_containers/food/snacks/S in oview(src,3)) - if(isturf(S.loc) || ishuman(S.loc)) - movement_target = S - break - if(movement_target) - stop_automated_movement = 1 - step_to(src,movement_target,1) - sleep(3) - step_to(src,movement_target,1) - sleep(3) - step_to(src,movement_target,1) - - if(movement_target) //Not redundant due to sleeps, Item can be gone in 6 decisecomds - var/turf/T = get_turf(movement_target) - if(!T) - return - if (T.x < src.x) - setDir(WEST) - else if (T.x > src.x) - setDir(EAST) - else if (T.y < src.y) - setDir(SOUTH) - else if (T.y > src.y) - setDir(NORTH) - else - setDir(SOUTH) - - if(!Adjacent(movement_target)) //can't reach food through windows. - return - - if(isturf(movement_target.loc)) - movement_target.attack_animal(src) - else if(ishuman(movement_target.loc)) - if(prob(20)) - manual_emote("stares at [movement_target.loc]'s [movement_target] with a sad puppy-face") - - if(prob(1)) - manual_emote(pick("dances around.","chases its tail!")) - INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(dance_rotate), src) - //Corgis and pugs are now under one dog subtype /mob/living/simple_animal/pet/dog/corgi @@ -165,6 +111,7 @@ dat += "Head:[inventory_head]" : "add_inv=head'>Empty"]" dat += "Back:[inventory_back]" : "add_inv=back'>Empty"]" dat += "Collar:[pcollar]" : "add_inv=collar'>Empty"]" + dat += "ID Card:[access_card]" : "add_inv=card'>Empty"]" dat += {" Close "} @@ -248,6 +195,10 @@ pcollar = null update_corgi_fluff() regenerate_icons() + if("card") + if(access_card) + usr.put_in_hands(access_card) + access_card = null show_inv(usr) @@ -300,9 +251,22 @@ return item_to_add.forceMove(src) - src.inventory_back = item_to_add + inventory_back = item_to_add update_corgi_fluff() regenerate_icons() + if("card") + if(access_card) + to_chat(usr, "[src] already has \an [access_card] pinned to [p_them()]!") + return + var/obj/item/item_to_add = usr.get_active_held_item() + if(!usr.temporarilyRemoveItemFromInventory(item_to_add)) + to_chat(usr, "\The [item_to_add] is stuck to your hand, you cannot pin it to [src]!") + return + if(!istype(item_to_add, /obj/item/card/id)) + to_chat(usr, "You can't pin [item_to_add] to [src]!") + return + item_to_add.forceMove(src) + access_card = item_to_add show_inv(usr) else diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index 92b955d3a841..5a900e64e199 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -648,7 +648,7 @@ item = I break if(item) - if(!AStar(src, get_turf(item), /turf/proc/Distance_cardinal)) + if(!get_path_to(src, item)) item = null continue return item diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index a1a0886a2362..738428592d9a 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -208,7 +208,8 @@ . = ..() if(stat == DEAD) . += "Upon closer examination, [p_they()] appear[p_s()] to be dead." - + if(access_card) + . += "There appears to be [icon2html(access_card, user)] \a [access_card] pinned to [p_them()]." /mob/living/simple_animal/update_stat() if(status_flags & GODMODE) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index eb3ee07a322b..c0af36bc9f33 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -512,7 +512,7 @@ for(var/i in 1 to (length(result) - 1)) result[i] += "\n" - to_chat(src, examine_block("[result.Join()]")) + to_chat(src, boxed_message("[result.Join()]")) SEND_SIGNAL(src, COMSIG_MOB_EXAMINATE, examinify) diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index ef21915e1fca..1be945b3d8f4 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -1,6 +1,6 @@ #define TRANSFORMATION_DURATION 22 -/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG)) +/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG| TR_KEEPAI)) if (notransform || transformation_timer) return @@ -150,6 +150,8 @@ changeling.purchasedpowers += hf changeling.regain_powers() + if(tr_flags & TR_KEEPAI) + ai_controller.PossessPawn(O) if (tr_flags & TR_DEFAULTMSG) to_chat(O, "You are now a monkey.") @@ -167,7 +169,7 @@ ////////////////////////// Humanize ////////////////////////////// //Could probably be merged with monkeyize but other transformations got their own procs, too -/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG)) +/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG | TR_KEEPAI)) if (notransform || transformation_timer) return @@ -329,6 +331,9 @@ else O.set_species(/datum/species/human) + if(tr_flags & TR_KEEPAI) + ai_controller.PossessPawn(O) + O.a_intent = INTENT_HELP if (tr_flags & TR_DEFAULTMSG) to_chat(O, "You are now a human.") diff --git a/code/modules/movespeed/_movespeed_modifier.dm b/code/modules/movespeed/_movespeed_modifier.dm index 06cbaf0b99cd..4befe2458faa 100644 --- a/code/modules/movespeed/_movespeed_modifier.dm +++ b/code/modules/movespeed/_movespeed_modifier.dm @@ -200,6 +200,7 @@ GLOBAL_LIST_EMPTY(movespeed_modification_cache) continue . += amt cached_multiplicative_slowdown = . + SEND_SIGNAL(src, COMSIG_MOB_MOVESPEED_UPDATED) /// Get the move speed modifiers list of the mob /mob/proc/get_movespeed_modifiers() diff --git a/code/modules/projectiles/ammunition/special/gauss.dm b/code/modules/projectiles/ammunition/special/gauss.dm index 0370d95051c4..04eb57fd4b73 100644 --- a/code/modules/projectiles/ammunition/special/gauss.dm +++ b/code/modules/projectiles/ammunition/special/gauss.dm @@ -8,6 +8,16 @@ firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/gauss var/energy_cost = 100 +/obj/item/ammo_casing/caseless/gauss/hc + name = "high conductivity pellet" + desc = "A small crystal-metal pellet." + caliber = "pellet" + icon_state = "hc-pellet" + projectile_type = /obj/projectile/bullet/gauss/hc + auto_rotate = FALSE + firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/gauss + energy_cost = 200 + /obj/item/ammo_casing/caseless/gauss/lance name = "ferromagnetic lance" desc = "A sharp metal rod." @@ -17,6 +27,16 @@ auto_rotate = TRUE energy_cost = 166 +/obj/item/ammo_casing/caseless/gauss/lance/hc + name = "high conductivity lance" + desc = "A sharp crystal-metal lance." + caliber = "lance" + icon_state = "hc-lance" + projectile_type = /obj/projectile/bullet/gauss/lance/hc + auto_rotate = FALSE + firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/gauss + energy_cost = 332 + /obj/item/ammo_casing/caseless/gauss/slug name = "ferromagnetic slug" desc = "A large metal slug." @@ -25,3 +45,13 @@ projectile_type = /obj/projectile/bullet/gauss/slug auto_rotate = TRUE energy_cost = 700 + +/obj/item/ammo_casing/caseless/gauss/slug/hc + name = "high conductivity lance" + desc = "A large crystal-metal slug." + caliber = "slug" + icon_state = "hc-slug" + projectile_type = /obj/projectile/bullet/gauss/slug/hc + auto_rotate = FALSE + firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect/gauss + energy_cost = 1400 diff --git a/code/modules/projectiles/boxes_magazines/ammo_stacks/prefab_stacks/premade_gauss_stacks.dm b/code/modules/projectiles/boxes_magazines/ammo_stacks/prefab_stacks/premade_gauss_stacks.dm index a64e075dfa84..b7a2dfc86299 100644 --- a/code/modules/projectiles/boxes_magazines/ammo_stacks/prefab_stacks/premade_gauss_stacks.dm +++ b/code/modules/projectiles/boxes_magazines/ammo_stacks/prefab_stacks/premade_gauss_stacks.dm @@ -11,6 +11,19 @@ /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferropellet = 4) generate_items_inside(items_inside,src) +/obj/item/ammo_box/magazine/ammo_stack/prefilled/ferropellet/hc + ammo_type = /obj/item/ammo_casing/caseless/gauss/hc + +/obj/item/storage/box/ammo/ferropellet/hc + name = "box of high conductivity pellets" + desc = "A box of high conductivity pellets for gauss firearms." + icon_state = "ferropelletsbox" + +/obj/item/storage/box/ammo/ferropellet/hc/PopulateContents() + var/static/items_inside = list( + /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferropellet/hc = 4) + generate_items_inside(items_inside,src) + /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferroslug ammo_type = /obj/item/ammo_casing/caseless/gauss/slug @@ -24,6 +37,19 @@ /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferroslug = 4) generate_items_inside(items_inside,src) +/obj/item/ammo_box/magazine/ammo_stack/prefilled/ferroslug/hc + ammo_type = /obj/item/ammo_casing/caseless/gauss/slug/hc + +/obj/item/storage/box/ammo/ferroslug/hc + name = "box of high conductivity slugs" + desc = "A box of high conductivity slugs for gauss firearms." + icon_state = "ferroslugsbox" + +/obj/item/storage/box/ammo/ferroslug/hc/PopulateContents() + var/static/items_inside = list( + /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferroslug/hc = 4) + generate_items_inside(items_inside,src) + /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferrolance ammo_type = /obj/item/ammo_casing/caseless/gauss/lance @@ -36,3 +62,16 @@ var/static/items_inside = list( /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferrolance = 4) generate_items_inside(items_inside,src) + +/obj/item/ammo_box/magazine/ammo_stack/prefilled/ferrolance/hc + ammo_type = /obj/item/ammo_casing/caseless/gauss/lance/hc + +/obj/item/storage/box/ammo/ferrolance/hc + name = "box of high conductivity lances" + desc = "A box of high conductivity lances for gauss firearms." + icon_state = "ferrolancesbox" + +/obj/item/storage/box/ammo/ferrolance/hc/PopulateContents() + var/static/items_inside = list( + /obj/item/ammo_box/magazine/ammo_stack/prefilled/ferrolance/hc = 4) + generate_items_inside(items_inside,src) diff --git a/code/modules/projectiles/projectile/bullets/gauss.dm b/code/modules/projectiles/projectile/bullets/gauss.dm index 6011adb61c88..4d17e6256cfb 100644 --- a/code/modules/projectiles/projectile/bullets/gauss.dm +++ b/code/modules/projectiles/projectile/bullets/gauss.dm @@ -9,6 +9,16 @@ light_color = MOVABLE_LIGHT light_range = 3 +/obj/projectile/bullet/gauss/hc + name = "ferromagnetic pellet" + damage = 10 + armour_penetration = 60 + range = 35 + hitscan = TRUE + muzzle_type = /obj/effect/projectile/muzzle/gauss + tracer_type = /obj/effect/projectile/tracer/gauss + impact_type = /obj/effect/projectile/impact/gauss + // Ferromagnetic Lance (GAR AR) /obj/projectile/bullet/gauss/lance @@ -17,6 +27,16 @@ damage = 30 armour_penetration = 20 +/obj/projectile/bullet/gauss/lance/hc + name = "ferromagnetic lance" + damage = 15 + armour_penetration = 80 + range = 35 + hitscan = TRUE + muzzle_type = /obj/effect/projectile/muzzle/gauss + tracer_type = /obj/effect/projectile/tracer/gauss + impact_type = /obj/effect/projectile/impact/gauss + // Ferromagnetic Slug (Model H) /obj/projectile/bullet/gauss/slug @@ -25,3 +45,13 @@ damage = 50 armour_penetration = -60 speed = 0.8 + +/obj/projectile/bullet/gauss/slug/hc + name = "ferromagnetic lance" + damage = 25 + armour_penetration = 0 + range = 35 + hitscan = TRUE + muzzle_type = /obj/effect/projectile/muzzle/gauss + tracer_type = /obj/effect/projectile/tracer/gauss + impact_type = /obj/effect/projectile/impact/gauss diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm index 5d11dcb720ee..3241695c7e78 100644 --- a/code/modules/reagents/reagent_containers/syringes.dm +++ b/code/modules/reagents/reagent_containers/syringes.dm @@ -67,11 +67,7 @@ if(!L.can_inject(user, 1)) return - // chance of monkey retaliation - if(ismonkey(target) && prob(MONKEY_SYRINGE_RETALIATION_PROB)) - var/mob/living/carbon/monkey/M - M = target - M.retaliate(user) + SEND_SIGNAL(target, COMSIG_LIVING_TRY_SYRINGE, user) switch(mode) if(SYRINGE_DRAW) diff --git a/code/modules/surgery/bodyparts/bodyparts.dm b/code/modules/surgery/bodyparts/bodyparts.dm index 86735b49a9fb..5464c5279795 100644 --- a/code/modules/surgery/bodyparts/bodyparts.dm +++ b/code/modules/surgery/bodyparts/bodyparts.dm @@ -837,7 +837,7 @@ if(bone_status != BONE_FLAG_BROKEN || !owner || istype(owner?.buckled, /obj/structure/bed/roller)) return - if(prob(5)) + if(prob(owner.m_intent == MOVE_INTENT_RUN ? 5 : 1)) if(HAS_TRAIT(owner, TRAIT_ANALGESIA)) to_chat(owner, span_notice("[pick("You feel something shifting inside your [name].", "There is something moving inside [name].", "Something inside your [name] slips.")]")) else diff --git a/code/modules/surgery/brain_surgery.dm b/code/modules/surgery/brain_surgery.dm index 0b1d8610889f..7544f683b74a 100644 --- a/code/modules/surgery/brain_surgery.dm +++ b/code/modules/surgery/brain_surgery.dm @@ -55,7 +55,7 @@ "[user] screws up, causing brain damage!", "[user] completes the surgery on [target]'s brain.") target.adjustOrganLoss(ORGAN_SLOT_BRAIN, 60) - target.gain_trauma_type(BRAIN_TRAUMA_SEVERE, TRAUMA_RESILIENCE_LOBOTOMY) + target.gain_trauma_type(BRAIN_TRAUMA_SEVERE, TRAUMA_RESILIENCE_SURGERY) else user.visible_message("[user] suddenly notices that the brain [user.p_they()] [user.p_were()] working on is not there anymore.", "You suddenly notice that the brain you were working on is not there anymore.") return FALSE diff --git a/html/changelogs/AutoChangeLog-pr-3918.yml b/html/changelogs/AutoChangeLog-pr-3918.yml deleted file mode 100644 index 5f46880de0aa..000000000000 --- a/html/changelogs/AutoChangeLog-pr-3918.yml +++ /dev/null @@ -1,7 +0,0 @@ -author: Gristle, Rye-Rice -changes: - - {rscadd: MP Gygax} - - {rscadd: Mech conversion kits to cargo} - - {balance: MK-2 ripley upgrade now costs 500.} - - {bugfix: Mech charging sanity.} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-3944.yml b/html/changelogs/AutoChangeLog-pr-3944.yml deleted file mode 100644 index a084ea8bda58..000000000000 --- a/html/changelogs/AutoChangeLog-pr-3944.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: rye-rice -changes: - - {rscadd: Resprites everything eoehoma related!} - - {rscadd: E-11 is more accurate. This isn't saying much.} - - {bugfix: E-60's charge overlay should now work} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-3988.yml b/html/changelogs/AutoChangeLog-pr-3988.yml deleted file mode 100644 index d3e8a31d338d..000000000000 --- a/html/changelogs/AutoChangeLog-pr-3988.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: Gristlebee -changes: - - {rscadd: You can use a multitool for mechanical brain surgery on the fix brain - step.} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-3994.yml b/html/changelogs/AutoChangeLog-pr-3994.yml new file mode 100644 index 000000000000..f0a6e3cdf5b5 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-3994.yml @@ -0,0 +1,7 @@ +author: Aylong +changes: + - {rscadd: Added a blinking background icon to vote action buttons} + - {rscadd: Added boxed message blocks and fieldset message blocks} + - {rscadd: Added a boxed message block for votes and a fieldset message block for + vote results} +delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-3996.yml b/html/changelogs/AutoChangeLog-pr-3996.yml new file mode 100644 index 000000000000..f86240097673 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-3996.yml @@ -0,0 +1,5 @@ +author: zimon9 +changes: + - {balance: The bone breakage damage proc now depends on the movement intent of + a carbon. Walking reduces the proc call probability} +delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4002.yml b/html/changelogs/AutoChangeLog-pr-4002.yml deleted file mode 100644 index 9886d37a65e2..000000000000 --- a/html/changelogs/AutoChangeLog-pr-4002.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: Ical92 -changes: - - {bugfix: gathering no longer has infinite reach} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4008.yml b/html/changelogs/AutoChangeLog-pr-4008.yml new file mode 100644 index 000000000000..e5ff599e7a7c --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-4008.yml @@ -0,0 +1,4 @@ +author: Erikafox +changes: + - {bugfix: you can no longer grind down the outpost} +delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4016.yml b/html/changelogs/AutoChangeLog-pr-4016.yml deleted file mode 100644 index 60f3bd339ac6..000000000000 --- a/html/changelogs/AutoChangeLog-pr-4016.yml +++ /dev/null @@ -1,6 +0,0 @@ -author: generalthrax -changes: - - {rscadd: Glass bottles of the large and small variety added to the autolathe} - - {rscadd: Distiller added to the black market} - - {balance: Beekeeping crates pricing reduced} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4019.yml b/html/changelogs/AutoChangeLog-pr-4019.yml deleted file mode 100644 index cf0abcdc26fe..000000000000 --- a/html/changelogs/AutoChangeLog-pr-4019.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: generalthrax -changes: - - {bugfix: Fixes plating under some catwalks that aren't supposed to be there on - the Mudskipper} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4028.yml b/html/changelogs/AutoChangeLog-pr-4028.yml new file mode 100644 index 000000000000..897a2f04de12 --- /dev/null +++ b/html/changelogs/AutoChangeLog-pr-4028.yml @@ -0,0 +1,4 @@ +author: Erikafox +changes: + - {rscadd: You can now purchase high conductivity gauss rounds from the outpost.} +delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4036.yml b/html/changelogs/AutoChangeLog-pr-4036.yml deleted file mode 100644 index 1ed66d369091..000000000000 --- a/html/changelogs/AutoChangeLog-pr-4036.yml +++ /dev/null @@ -1,5 +0,0 @@ -author: firebudgy -changes: - - {rscdel: RnD retrieval missions} - - {balance: 'Dogtag missions take any kind of dogtag, both Frontiersman and Ramzi'} -delete-after: true diff --git a/html/changelogs/AutoChangeLog-pr-4037.yml b/html/changelogs/AutoChangeLog-pr-4037.yml deleted file mode 100644 index 9a858987a24f..000000000000 --- a/html/changelogs/AutoChangeLog-pr-4037.yml +++ /dev/null @@ -1,4 +0,0 @@ -author: Thera-Pissed -changes: - - {rscadd: Replaced Jump To Mob on ghost HUD with a respawn option.} -delete-after: true diff --git a/html/changelogs/archive/2025-01.yml b/html/changelogs/archive/2025-01.yml index 905111cda91e..695358a3bfea 100644 --- a/html/changelogs/archive/2025-01.yml +++ b/html/changelogs/archive/2025-01.yml @@ -157,3 +157,32 @@ and Firestorm Pan Magazine to factional cargo. - rscadd: N+S has deployed a new batch of Shaft Miners to Harrier-class vessels. - rscdel: Janitors have been reassigned from the Harrier for retraining. +2025-01-20: + Gristle, Rye-Rice: + - rscadd: MP Gygax + - rscadd: Mech conversion kits to cargo + - balance: MK-2 ripley upgrade now costs 500. + - bugfix: Mech charging sanity. + Gristlebee: + - rscadd: You can use a multitool for mechanical brain surgery on the fix brain + step. + - balance: You can no longer gain deep-rooted traumas from failed brain surgery + or natural damage. + Ical92: + - bugfix: gathering no longer has infinite reach + - bugfix: the outpost's crate shelvers have been trained to leave no evidence + Thera-Pissed: + - rscadd: Replaced Jump To Mob on ghost HUD with a respawn option. + firebudgy: + - rscdel: RnD retrieval missions + - balance: Dogtag missions take any kind of dogtag, both Frontiersman and Ramzi + generalthrax: + - rscadd: Glass bottles of the large and small variety added to the autolathe + - rscadd: Distiller added to the black market + - balance: Beekeeping crates pricing reduced + - bugfix: Fixes plating under some catwalks that aren't supposed to be there on + the Mudskipper + rye-rice: + - rscadd: Resprites everything eoehoma related! + - rscadd: E-11 is more accurate. This isn't saying much. + - bugfix: E-60's charge overlay should now work diff --git a/icons/obj/ammunition/ammo_bullets.dmi b/icons/obj/ammunition/ammo_bullets.dmi index 6ac65ff5bb66..9a35253de3bc 100644 Binary files a/icons/obj/ammunition/ammo_bullets.dmi and b/icons/obj/ammunition/ammo_bullets.dmi differ diff --git a/icons/obj/projectiles_impact.dmi b/icons/obj/projectiles_impact.dmi index 543aea109b41..575a5e1c5699 100644 Binary files a/icons/obj/projectiles_impact.dmi and b/icons/obj/projectiles_impact.dmi differ diff --git a/icons/obj/projectiles_muzzle.dmi b/icons/obj/projectiles_muzzle.dmi index b8f311e2dea4..afce0784edf2 100644 Binary files a/icons/obj/projectiles_muzzle.dmi and b/icons/obj/projectiles_muzzle.dmi differ diff --git a/icons/obj/projectiles_tracer.dmi b/icons/obj/projectiles_tracer.dmi index 167e456e1573..d7d0147747f8 100644 Binary files a/icons/obj/projectiles_tracer.dmi and b/icons/obj/projectiles_tracer.dmi differ diff --git a/shiptest.dme b/shiptest.dme index 551cceb11d37..f57ba4140c75 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -161,6 +161,7 @@ #include "code\__DEFINES\vv.dm" #include "code\__DEFINES\wall_dents.dm" #include "code\__DEFINES\wires.dm" +#include "code\__DEFINES\ai\ai.dm" #include "code\__DEFINES\dcs\flags.dm" #include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals\signals.dm" @@ -186,7 +187,6 @@ #include "code\__HELPERS\_planes.dm" #include "code\__HELPERS\_string_lists.dm" #include "code\__HELPERS\areas.dm" -#include "code\__HELPERS\AStar.dm" #include "code\__HELPERS\atoms.dm" #include "code\__HELPERS\bindings.dm" #include "code\__HELPERS\bitflag_lists.dm" @@ -213,6 +213,7 @@ #include "code\__HELPERS\mouse_control.dm" #include "code\__HELPERS\nameof.dm" #include "code\__HELPERS\names.dm" +#include "code\__HELPERS\path.dm" #include "code\__HELPERS\priority_announce.dm" #include "code\__HELPERS\pronouns.dm" #include "code\__HELPERS\qdel.dm" @@ -326,6 +327,7 @@ #include "code\controllers\configuration\entries\resources.dm" #include "code\controllers\subsystem\achievements.dm" #include "code\controllers\subsystem\acid.dm" +#include "code\controllers\subsystem\ai_controllers.dm" #include "code\controllers\subsystem\air.dm" #include "code\controllers\subsystem\ambience.dm" #include "code\controllers\subsystem\assets.dm" @@ -403,6 +405,8 @@ #include "code\controllers\subsystem\vis_overlays.dm" #include "code\controllers\subsystem\vote.dm" #include "code\controllers\subsystem\weather.dm" +#include "code\controllers\subsystem\processing\ai_behaviors.dm" +#include "code\controllers\subsystem\processing\ai_movement.dm" #include "code\controllers\subsystem\processing\fastprocess.dm" #include "code\controllers\subsystem\processing\fluids.dm" #include "code\controllers\subsystem\processing\instruments.dm" @@ -470,6 +474,19 @@ #include "code\datums\achievements\skill_achievements.dm" #include "code\datums\actions\beam_rifle.dm" #include "code\datums\actions\ninja.dm" +#include "code\datums\ai\_ai_behavoir.dm" +#include "code\datums\ai\_ai_controller.dm" +#include "code\datums\ai\_ai_planning_subtree.dm" +#include "code\datums\ai\generic_actions.dm" +#include "code\datums\ai\dog\dog_behaviors.dm" +#include "code\datums\ai\dog\dog_controller.dm" +#include "code\datums\ai\dog\dog_subtrees.dm" +#include "code\datums\ai\monkey\monkey_behaviors.dm" +#include "code\datums\ai\monkey\monkey_controller.dm" +#include "code\datums\ai\monkey\monkey_subtrees.dm" +#include "code\datums\ai\movement\_ai_movement.dm" +#include "code\datums\ai\movement\ai_movement_dumb.dm" +#include "code\datums\ai\movement\ai_movement_jps.dm" #include "code\datums\atmosphere\_atmosphere.dm" #include "code\datums\atmosphere\planetary.dm" #include "code\datums\brain_damage\brain_trauma.dm" @@ -556,6 +573,7 @@ #include "code\datums\components\sizzle.dm" #include "code\datums\components\slippery.dm" #include "code\datums\components\spill.dm" +#include "code\datums\components\spinny.dm" #include "code\datums\components\spooky.dm" #include "code\datums\components\squeak.dm" #include "code\datums\components\stationstuck.dm" @@ -1953,6 +1971,7 @@ #include "code\modules\cargo\packs\chemistry.dm" #include "code\modules\cargo\packs\civilian.dm" #include "code\modules\cargo\packs\costumes_toys.dm" +#include "code\modules\cargo\packs\cybernetics.dm" #include "code\modules\cargo\packs\emergency.dm" #include "code\modules\cargo\packs\exploration.dm" #include "code\modules\cargo\packs\fishing.dm" @@ -2627,7 +2646,6 @@ #include "code\modules\mob\living\carbon\human\species_types\vampire.dm" #include "code\modules\mob\living\carbon\human\species_types\vox.dm" #include "code\modules\mob\living\carbon\human\species_types\zombies.dm" -#include "code\modules\mob\living\carbon\monkey\combat.dm" #include "code\modules\mob\living\carbon\monkey\death.dm" #include "code\modules\mob\living\carbon\monkey\inventory.dm" #include "code\modules\mob\living\carbon\monkey\life.dm" diff --git a/sound/creatures/monkey/monkey_screech_1.ogg b/sound/creatures/monkey/monkey_screech_1.ogg new file mode 100644 index 000000000000..a4d5bc45429a Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_1.ogg differ diff --git a/sound/creatures/monkey/monkey_screech_2.ogg b/sound/creatures/monkey/monkey_screech_2.ogg new file mode 100644 index 000000000000..ea44bcbcd814 Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_2.ogg differ diff --git a/sound/creatures/monkey/monkey_screech_3.ogg b/sound/creatures/monkey/monkey_screech_3.ogg new file mode 100644 index 000000000000..eeb33057d0ae Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_3.ogg differ diff --git a/sound/creatures/monkey/monkey_screech_4.ogg b/sound/creatures/monkey/monkey_screech_4.ogg new file mode 100644 index 000000000000..5a60b9466fa0 Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_4.ogg differ diff --git a/sound/creatures/monkey/monkey_screech_5.ogg b/sound/creatures/monkey/monkey_screech_5.ogg new file mode 100644 index 000000000000..04b4be87f842 Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_5.ogg differ diff --git a/sound/creatures/monkey/monkey_screech_6.ogg b/sound/creatures/monkey/monkey_screech_6.ogg new file mode 100644 index 000000000000..d73c3e9bb225 Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_6.ogg differ diff --git a/sound/creatures/monkey/monkey_screech_7.ogg b/sound/creatures/monkey/monkey_screech_7.ogg new file mode 100644 index 000000000000..291a61d75421 Binary files /dev/null and b/sound/creatures/monkey/monkey_screech_7.ogg differ diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss index 1acd0cf0094c..3c94228d5757 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-dark.scss @@ -39,6 +39,10 @@ a.popt { text-decoration: none; } +.center { + text-align: center; +} + /* POPUPS */ .popup { @@ -703,6 +707,10 @@ em { font-size: 60%; } +.smallish { + font-size: 80%; +} + .big { font-size: 185%; } @@ -895,14 +903,68 @@ em { margin-left: 3em; } -.examine_block { - background: #1b1c1e; - border: 1px solid #a4bad6; - margin: 0.5em; - padding: 0.5em 0.75em; -} - .tooltip { font-style: italic; border-bottom: 1px dashed #fff; } + +.fieldset_legend { + position: relative; + max-width: 95%; + font-size: 120%; + padding: 0.2em 0.5em; + background: #151515; // Chat background color + border: 1px solid; + border-color: inherit; + border-radius: 0.33em; + z-index: 1; + + // "Mask" a half of the border + // It very rough but it only possible way i see with IE compat + // Replace it with normal mask-image when 516 got stable + &:before { + content: ''; + position: absolute; + left: 0; + height: 1.15em; + width: 100%; + background: #151515; // Chat background color + transform: translateY(-50%) scaleX(1.05); + z-index: -1; + } +} + +.boxed_message { + background: hsl(220, 10%, 10%); + border: 2px solid; + border-left: 5px solid; + border-color: hsla(220, 40%, 75%, 0.25); + margin: 0.5em 0; + padding: 0.5em 0.75em; + border-radius: 0.33em; + + &.red_box { + background: hsl(0, 20%, 10%); + border-color: hsla(0, 100%, 50%, 0.5); + } + + &.green_box { + background: hsl(140, 20%, 10%); + border-color: hsla(120, 100%, 50%, 0.5); + } + + &.blue_box { + background: hsl(220, 20%, 10%); + border-color: hsla(225, 90%, 65%, 0.5); + } + + &.purple_box { + background: hsl(260, 25%, 12.5%); + border-color: hsla(260, 100%, 75%, 0.5); + } + + hr { + margin: 0.5em -0.75em; + border-color: inherit; + } +} diff --git a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss index 49ccc958d5cf..219a0ebc853a 100644 --- a/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss +++ b/tgui/packages/tgui-panel/styles/tgchat/chat-light.scss @@ -942,14 +942,40 @@ h2.alert { margin-left: 3em; } -.examine_block { - background: #f2f7fa; - border: 1px solid #111a27; - margin: 0.5em; - padding: 0.5em 0.75em; -} - .tooltip { font-style: italic; border-bottom: 1px dashed #000; } + +.fieldset_legend { + background: #ffffff; // Chat background color + + &:before { + background: #ffffff; // Chat background color + } +} + +.boxed_message { + background: hsl(220, 100%, 97.5%); + border-color: hsla(220, 75%, 25%, 0.5); + + &.red_box { + background: hsl(0, 100%, 97.5%); + border-color: hsla(0, 100%, 50%, 0.5); + } + + &.green_box { + background: hsl(140, 100%, 97.5%); + border-color: hsl(120, 100%, 33%, 0.5); + } + + &.blue_box { + background: hsl(220, 100%, 97.5%); + border-color: hsla(225, 100%, 50%, 0.5); + } + + &.purple_box { + background: hsl(260, 100%, 97.5%); + border-color: hsla(260, 100%, 50%, 0.5); + } +}