From 5e573e2db25402aa761b37992c4c7e2c90760a40 Mon Sep 17 00:00:00 2001 From: FalloutFalcon <86381784+FalloutFalcon@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:47:44 -0600 Subject: [PATCH] Port: Datumized AI and JPS (#3950) ## About The Pull Request https://github.com/tgstation/tgstation/pull/55238 https://github.com/tgstation/tgstation/pull/56780 https://github.com/tgstation/tgstation/pull/55515 https://github.com/tgstation/tgstation/pull/57111 https://github.com/tgstation/tgstation/pull/57186 https://github.com/tgstation/tgstation/pull/58631 https://github.com/tgstation/tgstation/pull/60249 minor backend stuff from https://github.com/tgstation/tgstation/pull/55778 https://github.com/tgstation/tgstation/pull/55728 ## Why It's Good For The Game prereqs for basic mobs. performance! advanced ai! ## Changelog :cl: add: monkey smart c: add: dog smart c: add: jps, a cheap pathfinding /:cl: --- code/__DEFINES/DNA.dm | 1 + code/__DEFINES/ai/ai.dm | 81 ++++ code/__DEFINES/dcs/signals/signals.dm | 12 + code/__DEFINES/monkeys.dm | 64 +-- code/__DEFINES/subsystems.dm | 4 + code/__DEFINES/traits.dm | 2 + code/__DEFINES/vv.dm | 1 + code/__HELPERS/AStar.dm | 212 --------- code/__HELPERS/heap.dm | 50 +- code/__HELPERS/path.dm | 347 ++++++++++++++ code/__HELPERS/unsorted.dm | 2 + code/_globalvars/lists/mobs.dm | 6 + code/_onclick/click.dm | 5 +- code/controllers/subsystem/ai_controllers.dm | 33 ++ code/controllers/subsystem/pathfinder.dm | 2 - .../subsystem/processing/ai_behaviors.dm | 20 + .../subsystem/processing/ai_movement.dm | 21 + .../subsystem/processing/processing.dm | 6 +- code/controllers/subsystem/throwing.dm | 3 + code/datums/ai/README.md | 21 + code/datums/ai/_ai_behavoir.dm | 25 + code/datums/ai/_ai_controller.dm | 254 +++++++++++ code/datums/ai/_ai_planning_subtree.dm | 6 + code/datums/ai/dog/dog_behaviors.dm | 208 +++++++++ code/datums/ai/dog/dog_controller.dm | 271 +++++++++++ code/datums/ai/dog/dog_subtrees.dm | 40 ++ code/datums/ai/generic_actions.dm | 111 +++++ code/datums/ai/monkey/monkey_behaviors.dm | 279 ++++++++++++ code/datums/ai/monkey/monkey_controller.dm | 255 +++++++++++ code/datums/ai/monkey/monkey_subtrees.dm | 84 ++++ code/datums/ai/movement/_ai_movement.dm | 19 + code/datums/ai/movement/ai_movement_dumb.dm | 27 ++ code/datums/ai/movement/ai_movement_jps.dm | 61 +++ code/datums/components/spinny.dm | 33 ++ code/datums/mutations/body.dm | 4 +- code/game/atoms.dm | 25 + .../machinery/porta_turret/portable_turret.dm | 3 - code/game/objects/items.dm | 2 +- code/game/objects/items/handcuffs.dm | 8 +- code/game/objects/objs.dm | 19 +- code/game/objects/structures/girders.dm | 7 +- code/game/objects/structures/grille.dm | 7 +- code/game/objects/structures/window.dm | 2 +- code/game/turfs/turf.dm | 20 + .../changeling/powers/tiny_prick.dm | 4 +- code/modules/mob/living/carbon/carbon.dm | 1 + .../mob/living/carbon/carbon_defense.dm | 3 + code/modules/mob/living/carbon/emote.dm | 17 +- .../mob/living/carbon/human/examine.dm | 3 + code/modules/mob/living/carbon/human/human.dm | 3 + .../mob/living/carbon/monkey/combat.dm | 426 ------------------ code/modules/mob/living/carbon/monkey/life.dm | 28 -- .../mob/living/carbon/monkey/monkey.dm | 4 +- code/modules/mob/living/living.dm | 2 +- .../mob/living/simple_animal/bot/bot.dm | 12 +- .../mob/living/simple_animal/bot/cleanbot.dm | 6 +- .../mob/living/simple_animal/bot/firebot.dm | 2 +- .../mob/living/simple_animal/bot/floorbot.dm | 4 +- .../mob/living/simple_animal/bot/medbot.dm | 4 +- .../mob/living/simple_animal/bot/mulebot.dm | 2 +- .../mob/living/simple_animal/friendly/dog.dm | 78 +--- .../mob/living/simple_animal/parrot.dm | 2 +- .../mob/living/simple_animal/simple_animal.dm | 3 +- code/modules/mob/transform_procs.dm | 9 +- code/modules/movespeed/_movespeed_modifier.dm | 1 + .../reagents/reagent_containers/syringes.dm | 6 +- shiptest.dme | 21 +- sound/creatures/monkey/monkey_screech_1.ogg | Bin 0 -> 15300 bytes sound/creatures/monkey/monkey_screech_2.ogg | Bin 0 -> 15649 bytes sound/creatures/monkey/monkey_screech_3.ogg | Bin 0 -> 17832 bytes sound/creatures/monkey/monkey_screech_4.ogg | Bin 0 -> 16704 bytes sound/creatures/monkey/monkey_screech_5.ogg | Bin 0 -> 20239 bytes sound/creatures/monkey/monkey_screech_6.ogg | Bin 0 -> 19189 bytes sound/creatures/monkey/monkey_screech_7.ogg | Bin 0 -> 18851 bytes 74 files changed, 2466 insertions(+), 838 deletions(-) create mode 100644 code/__DEFINES/ai/ai.dm delete mode 100644 code/__HELPERS/AStar.dm create mode 100644 code/__HELPERS/path.dm create mode 100644 code/controllers/subsystem/ai_controllers.dm create mode 100644 code/controllers/subsystem/processing/ai_behaviors.dm create mode 100644 code/controllers/subsystem/processing/ai_movement.dm create mode 100644 code/datums/ai/README.md create mode 100644 code/datums/ai/_ai_behavoir.dm create mode 100644 code/datums/ai/_ai_controller.dm create mode 100644 code/datums/ai/_ai_planning_subtree.dm create mode 100644 code/datums/ai/dog/dog_behaviors.dm create mode 100644 code/datums/ai/dog/dog_controller.dm create mode 100644 code/datums/ai/dog/dog_subtrees.dm create mode 100644 code/datums/ai/generic_actions.dm create mode 100644 code/datums/ai/monkey/monkey_behaviors.dm create mode 100644 code/datums/ai/monkey/monkey_controller.dm create mode 100644 code/datums/ai/monkey/monkey_subtrees.dm create mode 100644 code/datums/ai/movement/_ai_movement.dm create mode 100644 code/datums/ai/movement/ai_movement_dumb.dm create mode 100644 code/datums/ai/movement/ai_movement_jps.dm create mode 100644 code/datums/components/spinny.dm delete mode 100644 code/modules/mob/living/carbon/monkey/combat.dm create mode 100644 sound/creatures/monkey/monkey_screech_1.ogg create mode 100644 sound/creatures/monkey/monkey_screech_2.ogg create mode 100644 sound/creatures/monkey/monkey_screech_3.ogg create mode 100644 sound/creatures/monkey/monkey_screech_4.ogg create mode 100644 sound/creatures/monkey/monkey_screech_5.ogg create mode 100644 sound/creatures/monkey/monkey_screech_6.ogg create mode 100644 sound/creatures/monkey/monkey_screech_7.ogg 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/dcs/signals/signals.dm b/code/__DEFINES/dcs/signals/signals.dm index 2cd723567e17..eb5a0e59a7fe 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 1155ea88bed9..b702358a9977 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 a21147e7860e..12bf5edd6ff6 100644 --- a/code/__HELPERS/unsorted.dm +++ b/code/__HELPERS/unsorted.dm @@ -468,6 +468,8 @@ Turf and target are separate in case you want to teleport some distance from a t /proc/can_see(atom/source, atom/target, length=5) // I couldnt be arsed to do actual raycasting :I This is horribly inaccurate. var/turf/current = get_turf(source) var/turf/target_turf = get_turf(target) + if(get_dist(source, target) > length) + return FALSE var/steps = 1 if(current != target_turf) current = get_step_towards(current, target_turf) diff --git a/code/_globalvars/lists/mobs.dm b/code/_globalvars/lists/mobs.dm index fb00d8bdf283..f8140f71e978 100644 --- a/code/_globalvars/lists/mobs.dm +++ b/code/_globalvars/lists/mobs.dm @@ -8,6 +8,12 @@ GLOBAL_PROTECT(mentors) GLOBAL_LIST_EMPTY_TYPED(directory, /client) //all ckeys with associated client GLOBAL_LIST_EMPTY(stealthminID) //reference list with IDs that store ckeys, for stealthmins +GLOBAL_LIST_INIT(dangerous_turfs, typecacheof(list( + /turf/open/lava, + /turf/open/chasm, + /turf/open/space, + /turf/open/openspace))) + //Since it didn't really belong in any other category, I'm putting this here //This is for procs to replace all the goddamn 'in world's that are chilling around the code diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index c387afaace70..475ec8a10c26 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -345,11 +345,12 @@ A.AltClick(src) /atom/proc/AltClick(mob/user) - var/result = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) + . = SEND_SIGNAL(src, COMSIG_CLICK_ALT, user) + if(. & COMPONENT_CANCEL_CLICK_ALT) + return var/turf/T = get_turf(src) if(T && (isturf(loc) || isturf(src)) && user.TurfAdjacent(T)) user.set_listed_turf(T) - return result /// Use this instead of [/mob/proc/AltClickOn] where you only want turf content listing without additional atom alt-click interaction /atom/proc/AltClickNoInteract(mob/user, atom/A) diff --git a/code/controllers/subsystem/ai_controllers.dm b/code/controllers/subsystem/ai_controllers.dm new file mode 100644 index 000000000000..5319d7316fb9 --- /dev/null +++ b/code/controllers/subsystem/ai_controllers.dm @@ -0,0 +1,33 @@ +/// The subsystem used to tick [/datum/ai_controllers] instances. Handling the re-checking of plans. +SUBSYSTEM_DEF(ai_controllers) + name = "AI Controller Ticker" + flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_NPC + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + init_order = INIT_ORDER_AI_CONTROLLERS + wait = 0.5 SECONDS //Plan every half second if required, not great not terrible. + + ///List of all ai_subtree singletons, key is the typepath while assigned value is a newly created instance of the typepath. See setup_subtrees() + var/list/ai_subtrees = list() + ///List of all ai controllers currently running + var/list/active_ai_controllers = list() + +/datum/controller/subsystem/ai_controllers/Initialize(timeofday) + setup_subtrees() + return ..() + +/datum/controller/subsystem/ai_controllers/proc/setup_subtrees() + ai_subtrees = list() + for(var/subtree_type in subtypesof(/datum/ai_planning_subtree)) + var/datum/ai_planning_subtree/subtree = new subtree_type + ai_subtrees[subtree_type] = subtree + +/datum/controller/subsystem/ai_controllers/fire(resumed) + for(var/datum/ai_controller/ai_controller as anything in active_ai_controllers) + if(!COOLDOWN_FINISHED(ai_controller, failed_planning_cooldown)) + continue + + if(!LAZYLEN(ai_controller.current_behaviors)) + ai_controller.SelectBehaviors(wait * 0.1) + if(!LAZYLEN(ai_controller.current_behaviors)) //Still no plan + COOLDOWN_START(ai_controller, failed_planning_cooldown, AI_FAILED_PLANNING_COOLDOWN) diff --git a/code/controllers/subsystem/pathfinder.dm b/code/controllers/subsystem/pathfinder.dm index 21ee7ea60b3c..12ed31d0af7f 100644 --- a/code/controllers/subsystem/pathfinder.dm +++ b/code/controllers/subsystem/pathfinder.dm @@ -3,13 +3,11 @@ SUBSYSTEM_DEF(pathfinder) init_order = INIT_ORDER_PATH flags = SS_NO_FIRE var/datum/flowcache/mobs - var/datum/flowcache/circuits var/static/space_type_cache /datum/controller/subsystem/pathfinder/Initialize() space_type_cache = typecacheof(/turf/open/space) mobs = new(10) - circuits = new(3) return ..() /datum/flowcache diff --git a/code/controllers/subsystem/processing/ai_behaviors.dm b/code/controllers/subsystem/processing/ai_behaviors.dm new file mode 100644 index 000000000000..4c98567405cc --- /dev/null +++ b/code/controllers/subsystem/processing/ai_behaviors.dm @@ -0,0 +1,20 @@ +/// The subsystem used to tick [/datum/ai_behavior] instances. Handling the individual actions an AI can take like punching someone in the fucking NUTS +PROCESSING_SUBSYSTEM_DEF(ai_behaviors) + name = "AI Behavior Ticker" + flags = SS_POST_FIRE_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_NPC_ACTIONS + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + init_order = INIT_ORDER_AI_CONTROLLERS + wait = 1 + ///List of all ai_behavior singletons, key is the typepath while assigned value is a newly created instance of the typepath. See SetupAIBehaviors() + var/list/ai_behaviors + +/datum/controller/subsystem/processing/ai_behaviors/Initialize(timeofday) + SetupAIBehaviors() + return ..() + +/datum/controller/subsystem/processing/ai_behaviors/proc/SetupAIBehaviors() + ai_behaviors = list() + for(var/behavior_type in subtypesof(/datum/ai_behavior)) + var/datum/ai_behavior/ai_behavior = new behavior_type + ai_behaviors[behavior_type] = ai_behavior diff --git a/code/controllers/subsystem/processing/ai_movement.dm b/code/controllers/subsystem/processing/ai_movement.dm new file mode 100644 index 000000000000..6a6d64548ca7 --- /dev/null +++ b/code/controllers/subsystem/processing/ai_movement.dm @@ -0,0 +1,21 @@ +/// The subsystem used to tick [/datum/ai_movement] instances. Handling the movement of individual AI instances +PROCESSING_SUBSYSTEM_DEF(ai_movement) + name = "AI movement" + flags = SS_KEEP_TIMING|SS_BACKGROUND + priority = FIRE_PRIORITY_NPC_MOVEMENT + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + init_order = INIT_ORDER_AI_MOVEMENT + wait = 1 + + ///an assoc list of all ai_movement types. Assoc type to instance + var/list/movement_types + +/datum/controller/subsystem/processing/ai_movement/Initialize(timeofday) + SetupAIMovementInstances() + return ..() + +/datum/controller/subsystem/processing/ai_movement/proc/SetupAIMovementInstances() + movement_types = list() + for(var/key as anything in subtypesof(/datum/ai_movement)) + var/datum/ai_movement/ai_movement = new key + movement_types[key] = ai_movement diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm index b4ad1d56df7e..c4dc415d0080 100644 --- a/code/controllers/subsystem/processing/processing.dm +++ b/code/controllers/subsystem/processing/processing.dm @@ -4,7 +4,7 @@ SUBSYSTEM_DEF(processing) name = "Processing" priority = FIRE_PRIORITY_PROCESS flags = SS_BACKGROUND|SS_POST_FIRE_TIMING|SS_NO_INIT - wait = 10 + wait = 1 SECONDS var/stat_tag = "P" //Used for logging var/list/processing = list() @@ -31,12 +31,12 @@ SUBSYSTEM_DEF(processing) current_run.len-- if(QDELETED(thing)) processing -= thing - else if(thing.process(wait) == PROCESS_KILL) + else if(thing.process(wait * 0.1) == PROCESS_KILL) // fully stop so that a future START_PROCESSING will work STOP_PROCESSING(src, thing) if (MC_TICK_CHECK) return -/datum/proc/process() +/datum/proc/process(delta_time) set waitfor = 0 return PROCESS_KILL diff --git a/code/controllers/subsystem/throwing.dm b/code/controllers/subsystem/throwing.dm index b64dab12d301..3d78d5871779 100644 --- a/code/controllers/subsystem/throwing.dm +++ b/code/controllers/subsystem/throwing.dm @@ -207,4 +207,7 @@ SUBSYSTEM_DEF(throwing) if(T && thrownthing.has_gravity(T)) T.zFall(thrownthing) + if(thrownthing) + SEND_SIGNAL(thrownthing, COMSIG_MOVABLE_THROW_LANDED, src) + qdel(src) diff --git a/code/datums/ai/README.md b/code/datums/ai/README.md new file mode 100644 index 000000000000..f219b11bb247 --- /dev/null +++ b/code/datums/ai/README.md @@ -0,0 +1,21 @@ +# AI controllers + +## Introduction + +Our AI controller system is an attempt at making it possible to create modularized AI that stores its behavior in datums, while keeping state and decision making in a controller. This allows a more versatile way of creating AI that doesn't rely on OOP as much, and doesn't clutter up the Life() code in Mobs. + +## AI Controllers + +A datum that can be added to any atom in the game. Similarly to components, they might only support a given subtype (e.g. /mob/living), but the idea is that theoretically, you could apply a specific AI controller to a big a group of different types as possible and it would still work. + +These datums handle both the normal movement of mobs, but also their decision making, deciding which actions they will take based on the checks you put into their SelectBehaviors proc. + +If behaviors are selected, and the AI is in range, it will try to perform them. It runs all the behaviors it currently has in parallel; allowing for it to for example screech at someone while trying to attack them. Aslong as it has behaviors running, it will not try to generate new plans, making it not waste CPU when it already has an active goal. + +They also hold data for any of the actions they might need to use, such as cooldowns, whether or not they're currently fighting, etcetera this is stored in the blackboard, more information on that below. + +### Blackboard +The blackboard is an associated list keyed with strings and with values of whatever you want. These store information the mob has such as "Am I attacking someone", "Do I have a weapon". By using an associated list like this, no data needs to be stored on the actions themselves, and you could make actions that work on multiple ai controllers if you so pleased by making the key to use a variable. + +## AI Behavior +AI behaviors are the actions an AI can take. These can range from "Do an emote" to "Attack this target until he is dead". They are singletons and should contain nothing but static data. Any dynamic data should be stored in the blackboard, to allow different controllers to use the same behaviors. diff --git a/code/datums/ai/_ai_behavoir.dm b/code/datums/ai/_ai_behavoir.dm new file mode 100644 index 000000000000..fad64f6e97d6 --- /dev/null +++ b/code/datums/ai/_ai_behavoir.dm @@ -0,0 +1,25 @@ +///Abstract class for an action an AI can take, can range from movement to grabbing a nearby weapon. +/datum/ai_behavior + ///What distance you need to be from the target to perform the action + var/required_distance = 1 + ///Flags for extra behavior + var/behavior_flags = NONE + ///Cooldown between actions performances + var/action_cooldown = 0 + +/// Called by the ai controller when first being added. Additional arguments depend on the behavior type. +/// Return FALSE to cancel +/datum/ai_behavior/proc/setup(datum/ai_controller/controller, ...) + return TRUE + +///Called by the AI controller when this action is performed +/datum/ai_behavior/proc/perform(delta_time, datum/ai_controller/controller, ...) + controller.behavior_cooldowns[src] = world.time + action_cooldown + return + +///Called when the action is finished. +/datum/ai_behavior/proc/finish_action(datum/ai_controller/controller, succeeded) + controller.current_behaviors.Remove(src) + controller.behavior_args -= type + if(behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT) //If this was a movement task, reset our movement target. + controller.current_movement_target = null diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm new file mode 100644 index 000000000000..ce11df446aa4 --- /dev/null +++ b/code/datums/ai/_ai_controller.dm @@ -0,0 +1,254 @@ +/* +AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a atom. What this means is that these datums +have ways of interacting with a specific atom and control it. They posses a blackboard with the information the AI knows and has, and will plan behaviors it will try to execute through +multiple modular subtrees with behaviors +*/ + +/datum/ai_controller + ///The atom this controller is controlling + var/atom/pawn + ///Bitfield of traits for this AI to handle extra behavior + var/ai_traits + ///Current actions being performed by the AI. + var/list/current_behaviors + ///Current actions and their respective last time ran as an assoc list. + var/list/behavior_cooldowns = list() + ///Current status of AI (OFF/ON/IDLE) + var/ai_status + ///Current movement target of the AI, generally set by decision making. + var/atom/current_movement_target + ///Delay between atom movements, if this is not a multiplication of the delay in + var/move_delay + ///This is a list of variables the AI uses and can be mutated by actions. When an action is performed you pass this list and any relevant keys for the variables it can mutate. + var/list/blackboard = list() + ///Stored arguments for behaviors given during their initial creation + var/list/behavior_args = list() + ///Tracks recent pathing attempts, if we fail too many in a row we fail our current plans. + var/pathing_attempts + ///Can the AI remain in control if there is a client? + var/continue_processing_when_client = FALSE + ///distance to give up on target + var/max_target_distance = 14 + ///Cooldown for new plans, to prevent AI from going nuts if it can't think of new plans and looping on end + COOLDOWN_DECLARE(failed_planning_cooldown) + ///All subtrees this AI has available, will run them in order, so make sure they're in the order you want them to run. On initialization of this type, it will start as a typepath(s) and get converted to references of ai_subtrees found in SSai_controllers when init_subtrees() is called + var/list/planning_subtrees + + // Movement related things here + ///Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards. + var/datum/ai_movement/ai_movement = /datum/ai_movement/dumb + ///Cooldown until next movement + COOLDOWN_DECLARE(movement_cooldown) + ///Delay between movements. This is on the controller so we can keep the movement datum singleton + var/movement_delay = 0.1 SECONDS + + // The variables below are fucking stupid and should be put into the blackboard at some point. + ///A list for the path we're currently following, if we're using JPS pathing + var/list/movement_path + ///Cooldown for JPS movement, how often we're allowed to try making a new path + COOLDOWN_DECLARE(repath_cooldown) + ///AI paused time + var/paused_until = 0 + +/datum/ai_controller/New(atom/new_pawn) + change_ai_movement_type(ai_movement) + init_subtrees() + PossessPawn(new_pawn) + +/datum/ai_controller/Destroy(force, ...) + set_ai_status(AI_STATUS_OFF) + UnpossessPawn(FALSE) + return ..() + +///Overrides the current ai_movement of this controller with a new one +/datum/ai_controller/proc/change_ai_movement_type(datum/ai_movement/new_movement) + ai_movement = SSai_movement.movement_types[new_movement] + +///Completely replaces the planning_subtrees with a new set based on argument provided, list provided must contain specifically typepaths +/datum/ai_controller/proc/replace_planning_subtrees(list/typepaths_of_new_subtrees) + planning_subtrees = typepaths_of_new_subtrees + init_subtrees() + +///Loops over the subtrees in planning_subtrees and looks at the ai_controllers to grab a reference, ENSURE planning_subtrees ARE TYPEPATHS AND NOT INSTANCES/REFERENCES BEFORE EXECUTING THIS +/datum/ai_controller/proc/init_subtrees() + if(!LAZYLEN(planning_subtrees)) + return + var/list/temp_subtree_list = list() + for(var/subtree in planning_subtrees) + var/subtree_instance = SSai_controllers.ai_subtrees[subtree] + temp_subtree_list += subtree_instance + planning_subtrees = temp_subtree_list + +///Proc to move from one pawn to another, this will destroy the target's existing controller. +/datum/ai_controller/proc/PossessPawn(atom/new_pawn) + if(pawn) //Reset any old signals + UnpossessPawn(FALSE) + + if(istype(new_pawn.ai_controller)) //Existing AI, kill it. + QDEL_NULL(new_pawn.ai_controller) + + if(TryPossessPawn(new_pawn) & AI_CONTROLLER_INCOMPATIBLE) + qdel(src) + CRASH("[src] attached to [new_pawn] but these are not compatible!") + + pawn = new_pawn + pawn.ai_controller = src + + if(!continue_processing_when_client && istype(new_pawn, /mob)) + var/mob/possible_client_holder = new_pawn + if(possible_client_holder.client) + set_ai_status(AI_STATUS_OFF) + else + set_ai_status(AI_STATUS_ON) + else + set_ai_status(AI_STATUS_ON) + + RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) + +///Abstract proc for initializing the pawn to the new controller +/datum/ai_controller/proc/TryPossessPawn(atom/new_pawn) + return + +///Proc for deinitializing the pawn to the old controller +/datum/ai_controller/proc/UnpossessPawn(destroy) + UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT)) + pawn.ai_controller = null + pawn = null + if(destroy) + qdel(src) + return + +///Returns TRUE if the ai controller can actually run at the moment. +/datum/ai_controller/proc/able_to_run() + if(world.time < paused_until) + return FALSE + return TRUE + +/// Generates a plan and see if our existing one is still valid. +/datum/ai_controller/process(delta_time) + if(!able_to_run()) + walk(pawn, 0) //stop moving + return //this should remove them from processing in the future through event-based stuff. + if(!LAZYLEN(current_behaviors)) + PerformIdleBehavior(delta_time) //Do some stupid shit while we have nothing to do + return + + if(current_movement_target && get_dist(pawn, current_movement_target) > max_target_distance) //The distance is out of range + CancelActions() + return + + for(var/i in current_behaviors) + var/datum/ai_behavior/current_behavior = i + + if(behavior_cooldowns[current_behavior] > world.time) //Still on cooldown + continue + + if(current_behavior.behavior_flags & AI_BEHAVIOR_REQUIRE_MOVEMENT && current_movement_target) //Might need to move closer + if(current_behavior.required_distance >= get_dist(pawn, current_movement_target)) ///Are we close enough to engage? + if(ai_movement.moving_controllers[src] == current_movement_target) //We are close enough, if we're moving stop.else + ai_movement.stop_moving_towards(src) + ProcessBehavior(delta_time, current_behavior) + return + + else if(ai_movement.moving_controllers[src] != current_movement_target) //We're too far, if we're not already moving start doing it. + ai_movement.start_moving_towards(src, current_movement_target) //Then start moving + + if(current_behavior.behavior_flags & AI_BEHAVIOR_MOVE_AND_PERFORM) //If we can move and perform then do so. + ProcessBehavior(delta_time, current_behavior) + return + else //No movement required + ProcessBehavior(delta_time, current_behavior) + return + + +///Move somewhere using dumb movement (byond base) +/datum/ai_controller/proc/MoveTo(delta_time) + var/current_loc = get_turf(pawn) + var/atom/movable/movable_pawn = pawn + + var/turf/target_turf = get_step_towards(movable_pawn, current_movement_target) + + if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs)) + movable_pawn.Move(target_turf, get_dir(current_loc, target_turf)) + if(current_loc == get_turf(movable_pawn)) + if(++pathing_attempts >= AI_MAX_PATH_LENGTH) + CancelActions() + pathing_attempts = 0 + + +///Perform some dumb idle behavior. +/datum/ai_controller/proc/PerformIdleBehavior(delta_time) + return + +///This is where you decide what actions are taken by the AI. +/datum/ai_controller/proc/SelectBehaviors(delta_time) + SHOULD_NOT_SLEEP(TRUE) //Fuck you don't sleep in procs like this. + if(!COOLDOWN_FINISHED(src, failed_planning_cooldown)) + return FALSE + + LAZYINITLIST(current_behaviors) + + if(LAZYLEN(planning_subtrees)) + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.SelectBehaviors(src, delta_time) == SUBTREE_RETURN_FINISH_PLANNING) + break + +///This proc handles changing ai status, and starts/stops processing if required. +/datum/ai_controller/proc/set_ai_status(new_ai_status) + if(ai_status == new_ai_status) + return FALSE //no change + + ai_status = new_ai_status + switch(ai_status) + if(AI_STATUS_ON) + SSai_controllers.active_ai_controllers += src + START_PROCESSING(SSai_behaviors, src) + if(AI_STATUS_OFF) + STOP_PROCESSING(SSai_behaviors, src) + SSai_controllers.active_ai_controllers -= src + CancelActions() + +/datum/ai_controller/proc/PauseAi(time) + paused_until = world.time + time + +/datum/ai_controller/proc/AddBehavior(behavior_type, ...) + var/datum/ai_behavior/behavior = GET_AI_BEHAVIOR(behavior_type) + if(!behavior) + CRASH("Behavior [behavior_type] not found.") + var/list/arguments = args.Copy() + arguments[1] = src + if(!behavior.setup(arglist(arguments))) + return + LAZYADD(current_behaviors, behavior) + arguments.Cut(1, 2) + if(length(arguments)) + behavior_args[behavior_type] = arguments + +/datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior) + var/list/arguments = list(delta_time, src) + var/list/stored_arguments = behavior_args[behavior.type] + if(stored_arguments) + arguments += stored_arguments + behavior.perform(arglist(arguments)) + +/datum/ai_controller/proc/CancelActions() + if(!LAZYLEN(current_behaviors)) + return + for(var/i in current_behaviors) + var/datum/ai_behavior/current_behavior = i + current_behavior.finish_action(src, FALSE) + +/datum/ai_controller/proc/on_sentience_gained() + UnregisterSignal(pawn, COMSIG_MOB_LOGIN) + if(!continue_processing_when_client) + set_ai_status(AI_STATUS_OFF) //Can't do anything while player is connected + RegisterSignal(pawn, COMSIG_MOB_LOGOUT, PROC_REF(on_sentience_lost)) + +/datum/ai_controller/proc/on_sentience_lost() + UnregisterSignal(pawn, COMSIG_MOB_LOGOUT) + set_ai_status(AI_STATUS_ON) //Can't do anything while player is connected + RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) + +/// Use this proc to define how your controller defines what access the pawn has for the sake of pathfinding, likely pointing to whatever ID slot is relevant +/datum/ai_controller/proc/get_access() + return diff --git a/code/datums/ai/_ai_planning_subtree.dm b/code/datums/ai/_ai_planning_subtree.dm new file mode 100644 index 000000000000..8f186d586a45 --- /dev/null +++ b/code/datums/ai/_ai_planning_subtree.dm @@ -0,0 +1,6 @@ +///A subtree is attached to a controller and is occasionally called by /ai_controller/SelectBehaviors(), this mainly exists to act as a way to subtype and modify SelectBehaviors() without needing to subtype the ai controller itself +/datum/ai_planning_subtree + +///Determines what behaviors should the controller try processing; if this returns SUBTREE_RETURN_FINISH_PLANNING then the controller won't go through the other subtrees should multiple exist in controller.planning_subtrees +/datum/ai_planning_subtree/proc/SelectBehaviors(datum/ai_controller/controller, delta_time) + return diff --git a/code/datums/ai/dog/dog_behaviors.dm b/code/datums/ai/dog/dog_behaviors.dm new file mode 100644 index 000000000000..3672b348118a --- /dev/null +++ b/code/datums/ai/dog/dog_behaviors.dm @@ -0,0 +1,208 @@ +/datum/ai_behavior/battle_screech/dog + screeches = list("barks","howls") + +/// Fetching makes the pawn chase after whatever it's targeting and pick it up when it's in range, with the dog_equip behavior +/datum/ai_behavior/fetch + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/fetch/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + var/obj/item/fetch_thing = controller.blackboard[BB_FETCH_TARGET] + + if(fetch_thing.anchored || !isturf(fetch_thing.loc) || IS_EDIBLE(fetch_thing)) //either we can't pick it up, or we'd rather eat it, so stop trying. + finish_action(controller, FALSE) + return + + if(in_range(living_pawn, fetch_thing)) + finish_action(controller, TRUE) + return + + finish_action(controller, FALSE) + +/datum/ai_behavior/fetch/finish_action(datum/ai_controller/controller, success) + . = ..() + + if(!success) //Don't try again on this item if we failed + var/obj/item/target = controller.blackboard[BB_FETCH_TARGET] + if(target) + controller.blackboard[BB_FETCH_IGNORE_LIST][target] = TRUE + controller.blackboard[BB_FETCH_TARGET] = null + controller.blackboard[BB_FETCH_DELIVER_TO] = null + + +/// This is simply a behaviour to pick up a fetch target +/datum/ai_behavior/simple_equip/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/obj/item/fetch_target = controller.blackboard[BB_FETCH_TARGET] + if(!isturf(fetch_target?.loc)) // someone picked it up or something happened to it + finish_action(controller, FALSE) + return + + if(in_range(controller.pawn, fetch_target)) + pickup_item(controller, fetch_target) + finish_action(controller, TRUE) + else + finish_action(controller, FALSE) + +/datum/ai_behavior/simple_equip/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.blackboard[BB_FETCH_TARGET] = null + +/datum/ai_behavior/simple_equip/proc/pickup_item(datum/ai_controller/controller, obj/item/target) + var/atom/pawn = controller.pawn + drop_item(controller) + pawn.visible_message("[pawn] picks up [target] in [pawn.p_their()] mouth.") + target.forceMove(pawn) + controller.blackboard[BB_SIMPLE_CARRY_ITEM] = target + return TRUE + +/datum/ai_behavior/simple_equip/proc/drop_item(datum/ai_controller/controller) + var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + if(!carried_item) + return + + var/atom/pawn = controller.pawn + pawn.visible_message("[pawn] drops [carried_item].") + carried_item.forceMove(get_turf(pawn)) + controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null + return TRUE + + + +/// This behavior involves dropping off a carried item to a specified person (or place) +/datum/ai_behavior/deliver_item + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/deliver_item/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] + if(!return_target) + finish_action(controller, FALSE) + if(in_range(controller.pawn, return_target)) + deliver_item(controller) + finish_action(controller, TRUE) + +/datum/ai_behavior/deliver_item/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.blackboard[BB_FETCH_DELIVER_TO] = null + +/// Actually drop the fetched item to the target +/datum/ai_behavior/deliver_item/proc/deliver_item(datum/ai_controller/controller) + var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + var/atom/movable/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] + if(!carried_item || !return_target) + finish_action(controller, FALSE) + return + + if(ismob(return_target)) + controller.pawn.visible_message("[controller.pawn] delivers [carried_item] at [return_target]'s feet.") + else // not sure how to best phrase this + controller.pawn.visible_message("[controller.pawn] delivers [carried_item] to [return_target].") + + carried_item.forceMove(get_turf(return_target)) + controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null + return TRUE + +/// This behavior involves either eating a snack we can reach, or begging someone holding a snack +/datum/ai_behavior/eat_snack + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/eat_snack/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/obj/item/snack = controller.current_movement_target + if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc))) + finish_action(controller, FALSE) + + var/mob/living/living_pawn = controller.pawn + if(!in_range(living_pawn, snack)) + return + + if(isturf(snack.loc)) + snack.attack_animal(living_pawn) // snack attack! + else if(iscarbon(snack.loc) && DT_PROB(10, delta_time)) + living_pawn.manual_emote("stares at [snack.loc]'s [snack.name] with a sad puppy-face.") + + if(QDELETED(snack)) // we ate it! + finish_action(controller, TRUE) + + +/// This behavior involves either eating a snack we can reach, or begging someone holding a snack +/datum/ai_behavior/play_dead + behavior_flags = NONE + +/datum/ai_behavior/play_dead/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/simple_animal/simple_pawn = controller.pawn + if(!istype(simple_pawn)) + return + + if(!controller.blackboard[BB_DOG_PLAYING_DEAD]) + controller.blackboard[BB_DOG_PLAYING_DEAD] = TRUE + simple_pawn.emote("deathgasp", intentional=FALSE) + simple_pawn.icon_state = simple_pawn.icon_dead + if(simple_pawn.flip_on_death) + simple_pawn.transform = simple_pawn.transform.Turn(180) + simple_pawn.density = FALSE + + if(DT_PROB(10, delta_time)) + finish_action(controller, TRUE) + +/datum/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + var/mob/living/simple_animal/simple_pawn = controller.pawn + if(!istype(simple_pawn) || simple_pawn.stat) // imagine actually dying while playing dead. hell, imagine being the kid waiting for your pup to get back up :( + return + controller.blackboard[BB_DOG_PLAYING_DEAD] = FALSE + simple_pawn.visible_message("[simple_pawn] springs to [simple_pawn.p_their()] feet, panting excitedly!") + simple_pawn.icon_state = simple_pawn.icon_living + if(simple_pawn.flip_on_death) + simple_pawn.transform = simple_pawn.transform.Turn(180) + simple_pawn.density = initial(simple_pawn.density) + +/// This behavior involves either eating a snack we can reach, or begging someone holding a snack +/datum/ai_behavior/harass + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + required_distance = 3 + +/datum/ai_behavior/harass/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + if(!istype(living_pawn)) + return + + var/atom/movable/harass_target = controller.blackboard[BB_DOG_HARASS_TARGET] + if(!harass_target || !can_see(living_pawn, harass_target, length=AI_DOG_VISION_RANGE)) + finish_action(controller, FALSE) + return + + if(controller.blackboard[BB_DOG_FRIENDS][harass_target]) + living_pawn.visible_message("[living_pawn] looks sideways at [harass_target] for a moment, then shakes [living_pawn.p_their()] head and ceases aggression.") + finish_action(controller, FALSE) + return + + var/mob/living/living_target = harass_target + if(istype(living_target) && (living_target.stat || HAS_TRAIT(living_target, TRAIT_FAKEDEATH))) + finish_action(controller, TRUE) + return + + // subtypes of this behavior can change behavior for how eager/averse the pawn is to attack the target as opposed to falling back/making noise/getting help + if(in_range(living_pawn, living_target)) + attack(controller, living_target) + else if(DT_PROB(50, delta_time)) + living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [harass_target]!") + +/datum/ai_behavior/harass/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.blackboard[BB_DOG_HARASS_TARGET] = null + +/// A proc representing when the mob is pushed to actually attack the target. Again, subtypes can be used to represent different attacks from different animals, or it can be some other generic behavior +/datum/ai_behavior/harass/proc/attack(datum/ai_controller/controller, mob/living/living_target) + var/mob/living/living_pawn = controller.pawn + if(!istype(living_pawn)) + return + living_pawn.do_attack_animation(living_target, ATTACK_EFFECT_BITE) + living_target.visible_message("[living_pawn] bites at [living_target]!", "[living_pawn] bites at you!", vision_distance = COMBAT_MESSAGE_RANGE) + if(istype(living_target)) + living_target.take_bodypart_damage(rand(5, 10)) + log_combat(living_pawn, living_target, "bit (AI)") diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm new file mode 100644 index 000000000000..5cd65654db7c --- /dev/null +++ b/code/datums/ai/dog/dog_controller.dm @@ -0,0 +1,271 @@ +/datum/ai_controller/dog + blackboard = list(\ + BB_SIMPLE_CARRY_ITEM = null,\ + BB_FETCH_TARGET = null,\ + BB_FETCH_DELIVER_TO = null,\ + BB_DOG_FRIENDS = list(),\ + BB_FETCH_IGNORE_LIST = list(),\ + BB_DOG_ORDER_MODE = DOG_COMMAND_NONE,\ + BB_DOG_PLAYING_DEAD = FALSE,\ + BB_DOG_HARASS_TARGET = null) + ai_movement = /datum/ai_movement/jps + planning_subtrees = list(/datum/ai_planning_subtree/dog) + + COOLDOWN_DECLARE(heel_cooldown) + COOLDOWN_DECLARE(command_cooldown) + +/datum/ai_controller/dog/process(delta_time) + if(ismob(pawn)) + var/mob/living/living_pawn = pawn + movement_delay = living_pawn.cached_multiplicative_slowdown + return ..() + +/datum/ai_controller/dog/TryPossessPawn(atom/new_pawn) + if(!isliving(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + + RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) + RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined)) + RegisterSignal(new_pawn, COMSIG_CLICK_ALT, PROC_REF(check_altclicked)) + RegisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING, PROC_REF(listened_throw)) + return ..() //Run parent at end + +/datum/ai_controller/dog/UnpossessPawn(destroy) + UnregisterSignal(pawn, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_PARENT_EXAMINE, COMSIG_GLOB_CARBON_THROW_THING, COMSIG_CLICK_ALT)) + return ..() //Run parent at end + +/datum/ai_controller/dog/able_to_run() + var/mob/living/living_pawn = pawn + + if(IS_DEAD_OR_INCAP(living_pawn)) + return FALSE + return ..() + +/datum/ai_controller/dog/get_access() + var/mob/living/simple_animal/simple_pawn = pawn + if(!istype(simple_pawn)) + return + + return simple_pawn.access_card + + +/datum/ai_controller/dog/PerformIdleBehavior(delta_time) + var/mob/living/living_pawn = pawn + if(!isturf(living_pawn.loc) || living_pawn.pulledby) + return + + // if we were just ordered to heel, chill out for a bit + if(!COOLDOWN_FINISHED(src, heel_cooldown)) + return + + // if we're just ditzing around carrying something, occasionally print a message so people know we have something + if(blackboard[BB_SIMPLE_CARRY_ITEM] && DT_PROB(5, delta_time)) + var/obj/item/carry_item = blackboard[BB_SIMPLE_CARRY_ITEM] + living_pawn.visible_message("[living_pawn] gently teethes on \the [carry_item] in [living_pawn.p_their()] mouth.", vision_distance=COMBAT_MESSAGE_RANGE) + + if(DT_PROB(5, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE)) + var/move_dir = pick(GLOB.alldirs) + living_pawn.Move(get_step(living_pawn, move_dir), move_dir) + else if(DT_PROB(10, delta_time)) + living_pawn.manual_emote(pick("dances around.","chases [living_pawn.p_their()] tail!")) + living_pawn.AddComponent(/datum/component/spinny) + +/// Someone has thrown something, see if it's someone we care about and start listening to the thrown item so we can see if we want to fetch it when it lands +/datum/ai_controller/dog/proc/listened_throw(datum/source, mob/living/carbon/carbon_thrower) + SIGNAL_HANDLER + if(blackboard[BB_FETCH_TARGET] || blackboard[BB_FETCH_DELIVER_TO] || blackboard[BB_DOG_PLAYING_DEAD]) // we're already busy + return + if(!COOLDOWN_FINISHED(src, heel_cooldown)) + return + if(!can_see(pawn, carbon_thrower, length=AI_DOG_VISION_RANGE)) + return + var/obj/item/thrown_thing = carbon_thrower.get_active_held_item() + if(!isitem(thrown_thing)) + return + if(blackboard[BB_FETCH_IGNORE_LIST][thrown_thing]) + return + + RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(listen_throw_land)) + +/// A throw we were listening to has finished, see if it's in range for us to try grabbing it +/datum/ai_controller/dog/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum) + SIGNAL_HANDLER + + UnregisterSignal(thrown_thing, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_THROW_LANDED)) + if(!istype(thrown_thing) || !isturf(thrown_thing.loc) || !can_see(pawn, thrown_thing, length=AI_DOG_VISION_RANGE)) + return + + current_movement_target = thrown_thing + blackboard[BB_FETCH_TARGET] = thrown_thing + blackboard[BB_FETCH_DELIVER_TO] = throwing_datum.thrower + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/fetch)) + +/// Someone's interacting with us by hand, see if they're being nice or mean +/datum/ai_controller/dog/proc/on_attack_hand(datum/source, mob/living/user) + SIGNAL_HANDLER + + if(user.a_intent == INTENT_HARM) + unfriend(user) + else + if(prob(AI_DOG_PET_FRIEND_PROB)) + befriend(user) + // if the dog has something in their mouth that they're not bringing to someone for whatever reason, have them drop it when pet by a friend + var/list/friends = blackboard[BB_DOG_FRIENDS] + if(blackboard[BB_SIMPLE_CARRY_ITEM] && !current_movement_target && friends[user]) + var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] + pawn.visible_message("[pawn] drops [carried_item] at [user]'s feet!") + // maybe have a dedicated proc for dropping things + carried_item.forceMove(get_turf(user)) + blackboard[BB_SIMPLE_CARRY_ITEM] = null + +/// Someone is being nice to us, let's make them a friend! +/datum/ai_controller/dog/proc/befriend(mob/living/new_friend) + var/list/friends = blackboard[BB_DOG_FRIENDS] + if(friends[new_friend]) + return + if(in_range(pawn, new_friend)) + new_friend.visible_message("[pawn] licks at [new_friend] in a friendly manner!", "[pawn] licks at you in a friendly manner!") + friends[new_friend] = TRUE + RegisterSignal(new_friend, COMSIG_MOB_POINTED, PROC_REF(check_point)) + RegisterSignal(new_friend, COMSIG_MOB_SAY, PROC_REF(check_verbal_command)) + +/// Someone is being mean to us, take them off our friends (add actual enemies behavior later) +/datum/ai_controller/dog/proc/unfriend(mob/living/ex_friend) + var/list/friends = blackboard[BB_DOG_FRIENDS] + friends[ex_friend] = null + UnregisterSignal(ex_friend, list(COMSIG_MOB_POINTED, COMSIG_MOB_SAY)) + +/// Someone is looking at us, if we're currently carrying something then show what it is, and include a message if they're our friend +/datum/ai_controller/dog/proc/on_examined(datum/source, mob/user, list/examine_text) + SIGNAL_HANDLER + + var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] + if(carried_item) + examine_text += "[pawn.p_they(TRUE)] [pawn.p_are()] carrying [carried_item.get_examine_string(user)] in [pawn.p_their()] mouth." + if(blackboard[BB_DOG_FRIENDS][user]) + examine_text += "[pawn.p_they(TRUE)] seem[pawn.p_s()] happy to see you!" + +/// If we died, drop anything we were carrying +/datum/ai_controller/dog/proc/on_death(mob/living/ol_yeller) + SIGNAL_HANDLER + + var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] + if(!carried_item) + return + + ol_yeller.visible_message("[ol_yeller] drops [carried_item] as [ol_yeller.p_they()] die[ol_yeller.p_s()].") + carried_item.forceMove(get_turf(ol_yeller)) + blackboard[BB_SIMPLE_CARRY_ITEM] = null + +// next section is regarding commands + +/// Someone alt clicked us, see if they're someone we should show the radial command menu to +/datum/ai_controller/dog/proc/check_altclicked(datum/source, mob/living/clicker) + SIGNAL_HANDLER + + if(!COOLDOWN_FINISHED(src, command_cooldown)) + return + if(!istype(clicker) || !blackboard[BB_DOG_FRIENDS][clicker]) + return + . = COMPONENT_CANCEL_CLICK_ALT + INVOKE_ASYNC(src, PROC_REF(command_radial), clicker) + +/// Show the command radial menu +/datum/ai_controller/dog/proc/command_radial(mob/living/clicker) + var/list/commands = list( + COMMAND_HEEL = image(icon = 'icons/Testing/turf_analysis.dmi', icon_state = "red_arrow"), + COMMAND_FETCH = image(icon = 'icons/mob/actions/actions_spells.dmi', icon_state = "summons"), + COMMAND_ATTACK = image(icon = 'icons/effects/effects.dmi', icon_state = "bite"), + COMMAND_DIE = image(icon = 'icons/mob/pets.dmi', icon_state = "puppy_dead") + ) + + var/choice = show_radial_menu(clicker, pawn, commands, custom_check = CALLBACK(src, PROC_REF(check_menu), clicker), tooltips = TRUE) + if(!choice || !check_menu(clicker)) + return + set_command_mode(clicker, choice) + +/datum/ai_controller/dog/proc/check_menu(mob/user) + if(!istype(user)) + CRASH("A non-mob is trying to issue an order to [pawn].") + if(user.incapacitated() || !can_see(user, pawn)) + return FALSE + return TRUE + +/// One of our friends said something, see if it's a valid command, and if so, take action +/datum/ai_controller/dog/proc/check_verbal_command(mob/speaker, speech_args) + SIGNAL_HANDLER + + if(!blackboard[BB_DOG_FRIENDS][speaker]) + return + + if(!COOLDOWN_FINISHED(src, command_cooldown)) + return + + var/spoken_text = speech_args[SPEECH_MESSAGE] // probably should check for full words + var/command + if(findtext(spoken_text, "heel") || findtext(spoken_text, "sit") || findtext(spoken_text, "stay")) + command = COMMAND_HEEL + else if(findtext(spoken_text, "fetch") || findtext(spoken_text, "get it")) + command = COMMAND_FETCH + else if(findtext(spoken_text, "attack") || findtext(spoken_text, "sic")) + command = COMMAND_ATTACK + else if(findtext(spoken_text, "play dead")) + command = COMMAND_DIE + else + return + + if(!can_see(pawn, speaker, length=AI_DOG_VISION_RANGE)) + return + set_command_mode(speaker, command) + +/// Whether we got here via radial menu or a verbal command, this is where we actually process what our new command will be +/datum/ai_controller/dog/proc/set_command_mode(mob/commander, command) + COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN) + + switch(command) + // heel: stop what you're doing, relax and try not to do anything for a little bit + if(COMMAND_HEEL) + pawn.visible_message("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] sit[pawn.p_s()] down obediently, awaiting further orders.") + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE + COOLDOWN_START(src, heel_cooldown, AI_DOG_HEEL_DURATION) + CancelActions() + // fetch: whatever the commander points to, try and bring it back + if(COMMAND_FETCH) + pawn.visible_message("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] bounce[pawn.p_s()] slightly in anticipation.") + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_FETCH + // attack: harass whoever the commander points to + if(COMMAND_ATTACK) + pawn.visible_message("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] growl[pawn.p_s()] intensely.") // imagine getting intimidated by a corgi + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_ATTACK + if(COMMAND_DIE) + blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE + CancelActions() + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/play_dead)) + +/// Someone we like is pointing at something, see if it's something we might want to interact with (like if they might want us to fetch something for them) +/datum/ai_controller/dog/proc/check_point(mob/pointing_friend, atom/movable/pointed_movable) + SIGNAL_HANDLER + + if(!COOLDOWN_FINISHED(src, command_cooldown)) + return + if(pointed_movable == pawn || blackboard[BB_FETCH_TARGET] || !istype(pointed_movable) || blackboard[BB_DOG_ORDER_MODE] == DOG_COMMAND_NONE) // busy or no command + return + if(!can_see(pawn, pointing_friend, length=AI_DOG_VISION_RANGE) || !can_see(pawn, pointed_movable, length=AI_DOG_VISION_RANGE)) + return + + COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN) + + switch(blackboard[BB_DOG_ORDER_MODE]) + if(DOG_COMMAND_FETCH) + if(ismob(pointed_movable) || pointed_movable.anchored) + return + pawn.visible_message("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and barks excitedly!") + current_movement_target = pointed_movable + blackboard[BB_FETCH_TARGET] = pointed_movable + blackboard[BB_FETCH_DELIVER_TO] = pointing_friend + current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/fetch) + if(DOG_COMMAND_ATTACK) + pawn.visible_message("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and growls intensely!") + current_movement_target = pointed_movable + blackboard[BB_DOG_HARASS_TARGET] = pointed_movable + current_behaviors += GET_AI_BEHAVIOR(/datum/ai_behavior/harass) diff --git a/code/datums/ai/dog/dog_subtrees.dm b/code/datums/ai/dog/dog_subtrees.dm new file mode 100644 index 000000000000..1eab7b87251b --- /dev/null +++ b/code/datums/ai/dog/dog_subtrees.dm @@ -0,0 +1,40 @@ +/datum/ai_planning_subtree/dog + COOLDOWN_DECLARE(heel_cooldown) + COOLDOWN_DECLARE(reset_ignore_cooldown) + +/datum/ai_planning_subtree/dog/SelectBehaviors(datum/ai_controller/dog/controller, delta_time) + var/mob/living/living_pawn = controller.pawn + + // occasionally reset our ignore list + if(COOLDOWN_FINISHED(src, reset_ignore_cooldown) && length(controller.blackboard[BB_FETCH_IGNORE_LIST])) + COOLDOWN_START(src, reset_ignore_cooldown, AI_FETCH_IGNORE_DURATION) + controller.blackboard[BB_FETCH_IGNORE_LIST] = list() + + // if we were just ordered to heel, chill out for a bit + if(!COOLDOWN_FINISHED(src, heel_cooldown)) + return + + // if we're not already carrying something and we have a fetch target (and we're not already doing something with it), see if we can eat/equip it + if(!controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_TARGET]) + var/atom/movable/interact_target = controller.blackboard[BB_FETCH_TARGET] + if(in_range(living_pawn, interact_target) && (isturf(interact_target.loc))) + controller.current_movement_target = interact_target + if(IS_EDIBLE(interact_target)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/eat_snack)) + else if(isitem(interact_target)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/simple_equip)) + else + controller.blackboard[BB_FETCH_TARGET] = null + controller.blackboard[BB_FETCH_DELIVER_TO] = null + return + + // if we're carrying something and we have a destination to deliver it, do that + if(controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_DELIVER_TO]) + var/atom/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] + if(!can_see(controller.pawn, return_target, length=AI_DOG_VISION_RANGE)) + // if the return target isn't in sight, we'll just forget about it and carry the thing around + controller.blackboard[BB_FETCH_DELIVER_TO] = null + return + controller.current_movement_target = return_target + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/deliver_item)) + return diff --git a/code/datums/ai/generic_actions.dm b/code/datums/ai/generic_actions.dm new file mode 100644 index 000000000000..fdcc978857fd --- /dev/null +++ b/code/datums/ai/generic_actions.dm @@ -0,0 +1,111 @@ + +/datum/ai_behavior/resist/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/living_pawn = controller.pawn + living_pawn.resist() + finish_action(controller, TRUE) + +/datum/ai_behavior/battle_screech + ///List of possible screeches the behavior has + var/list/screeches + +/datum/ai_behavior/battle_screech/perform(delta_time, datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick(screeches)) + finish_action(controller, TRUE) + +/// Use in hand the currently held item +/datum/ai_behavior/use_in_hand + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/use_in_hand/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held = pawn.get_item_by_slot(pawn.get_active_hand()) + if(!held) + finish_action(controller, FALSE) + return + pawn.activate_hand(pawn.get_active_hand()) + finish_action(controller, TRUE) + +/// Use the currently held item, or unarmed, on an object in the world +/datum/ai_behavior/use_on_object + required_distance = 1 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/use_on_object/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand()) + var/atom/target = controller.current_movement_target + + if(!target || !pawn.CanReach(target)) + finish_action(controller, FALSE) + return + + pawn.a_intent = INTENT_HELP + + if(held_item) + held_item.melee_attack_chain(pawn, target) + else + pawn.UnarmedAttack(target, TRUE) + + finish_action(controller, TRUE) + +/datum/ai_behavior/give + required_distance = 1 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/give/perform(delta_time, datum/ai_controller/controller) + . = ..() + var/mob/living/pawn = controller.pawn + var/obj/item/held_item = pawn.get_item_by_slot(pawn.get_active_hand()) + var/atom/target = controller.current_movement_target + + if(!target || !pawn.CanReach(target) || !isliving(target)) + finish_action(controller, FALSE) + return + + var/mob/living/living_target = target + controller.PauseAi(1.5 SECONDS) + living_target.visible_message( + "[pawn] starts trying to give [held_item] to [living_target]!", + "[pawn] tries to give you [held_item]!" + ) + if(!do_after(pawn, 1 SECONDS, living_target)) + return + if(QDELETED(held_item) || QDELETED(living_target)) + finish_action(controller, FALSE) + return + var/pocket_choice = prob(50) ? ITEM_SLOT_RPOCKET : ITEM_SLOT_LPOCKET + if(prob(50) && living_target.can_put_in_hand(held_item)) + living_target.put_in_hand(held_item) + else if(held_item.mob_can_equip(living_target, pawn, pocket_choice, TRUE)) + living_target.equip_to_slot(held_item, pocket_choice) + + finish_action(controller, TRUE) + +/datum/ai_behavior/consume + required_distance = 1 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + action_cooldown = 2 SECONDS + +/datum/ai_behavior/consume/setup(datum/ai_controller/controller, obj/item/target) + . = ..() + controller.current_movement_target = target + +/datum/ai_behavior/consume/perform(delta_time, datum/ai_controller/controller, obj/item/target) + . = ..() + var/mob/living/pawn = controller.pawn + + if(!(target in pawn.held_items)) + if(!pawn.put_in_hand_check(target)) + finish_action(controller, FALSE) + return + + pawn.put_in_hands(target) + + target.melee_attack_chain(pawn, pawn) + + if(QDELETED(target) || prob(10)) // Even if we don't finish it all we can randomly decide to be done + finish_action(controller, TRUE) diff --git a/code/datums/ai/monkey/monkey_behaviors.dm b/code/datums/ai/monkey/monkey_behaviors.dm new file mode 100644 index 000000000000..822dae22eb23 --- /dev/null +++ b/code/datums/ai/monkey/monkey_behaviors.dm @@ -0,0 +1,279 @@ +/datum/ai_behavior/battle_screech/monkey + screeches = list("roar","screech") + +/datum/ai_behavior/monkey_equip + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/monkey_equip/finish_action(datum/ai_controller/controller, success) + . = ..() + + if(!success) //Don't try again on this item if we failed + var/list/item_blacklist = controller.blackboard[BB_MONKEY_BLACKLISTITEMS] + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + + item_blacklist[target] = TRUE + + controller.blackboard[BB_MONKEY_PICKUPTARGET] = null + +/datum/ai_behavior/monkey_equip/proc/equip_item(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + var/best_force = controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] + + if(!target) + finish_action(controller, FALSE) + return + + if(target.anchored) //Can't pick it up, so stop trying. + finish_action(controller, FALSE) + return + + // Strong weapon + else if(target.force > best_force) + living_pawn.drop_all_held_items() + living_pawn.put_in_hands(target) + controller.blackboard[BB_MONKEY_BEST_FORCE_FOUND] = target.force + finish_action(controller, TRUE) + return + + else if(target.slot_flags) //Clothing == top priority + living_pawn.dropItemToGround(target, TRUE) + living_pawn.update_icons() + if(!living_pawn.equip_to_appropriate_slot(target)) + finish_action(controller, FALSE) + return //Already wearing something, in the future this should probably replace the current item but the code didn't actually do that, and I dont want to support it right now. + finish_action(controller, TRUE) + return + + // EVERYTHING ELSE + else if(living_pawn.get_empty_held_index_for_side(LEFT_HANDS) || living_pawn.get_empty_held_index_for_side(RIGHT_HANDS)) + living_pawn.put_in_hands(target) + finish_action(controller, TRUE) + return + + finish_action(controller, FALSE) + +/datum/ai_behavior/monkey_equip/ground + required_distance = 0 + +/datum/ai_behavior/monkey_equip/ground/perform(delta_time, datum/ai_controller/controller) + equip_item(controller) + +/datum/ai_behavior/monkey_equip/pickpocket + +/datum/ai_behavior/monkey_equip/pickpocket/perform(delta_time, datum/ai_controller/controller) + + if(controller.blackboard[BB_MONKEY_PICKPOCKETING]) //We are pickpocketing, don't do ANYTHING!!!! + return + INVOKE_ASYNC(src, PROC_REF(attempt_pickpocket), controller) + +/datum/ai_behavior/monkey_equip/pickpocket/proc/attempt_pickpocket(datum/ai_controller/controller) + var/obj/item/target = controller.blackboard[BB_MONKEY_PICKUPTARGET] + + var/mob/living/victim = target.loc + + var/mob/living/living_pawn = controller.pawn + + victim.visible_message("[living_pawn] starts trying to take [target] from [victim]!", "[living_pawn] tries to take [target]!") + + controller.blackboard[BB_MONKEY_PICKPOCKETING] = TRUE + + var/success = FALSE + + if(do_after(living_pawn, MONKEY_ITEM_SNATCH_DELAY, victim) && target) + + for(var/obj/item/I in victim.held_items) + if(I == target) + victim.visible_message("[living_pawn] snatches [target] from [victim].", "[living_pawn] snatched [target]!") + if(victim.temporarilyRemoveItemFromInventory(target)) + if(!QDELETED(target) && !equip_item(controller)) + target.forceMove(living_pawn.drop_location()) + success = TRUE + break + else + victim.visible_message("[living_pawn] tried to snatch [target] from [victim], but failed!", "[living_pawn] tried to grab [target]!") + + finish_action(controller, success) //We either fucked up or got the item. + +/datum/ai_behavior/monkey_equip/pickpocket/finish_action(datum/ai_controller/controller, success) + . = ..() + controller.blackboard[BB_MONKEY_PICKPOCKETING] = FALSE + controller.blackboard[BB_MONKEY_PICKUPTARGET] = null + +/datum/ai_behavior/monkey_flee + +/datum/ai_behavior/monkey_flee/perform(delta_time, datum/ai_controller/controller) + . = ..() + + var/mob/living/living_pawn = controller.pawn + + if(living_pawn.health >= MONKEY_FLEE_HEALTH) + finish_action(controller, TRUE) //we're back in bussiness + + var/mob/living/target = null + + // flee from anyone who attacked us and we didn't beat down + for(var/mob/living/L in view(living_pawn, MONKEY_FLEE_VISION)) + if(controller.blackboard[BB_MONKEY_ENEMIES][L] && L.stat == CONSCIOUS) + target = L + break + + if(target) + walk_away(living_pawn, target, MONKEY_ENEMY_VISION, 5) + else + finish_action(controller, TRUE) + +/datum/ai_behavior/monkey_attack_mob + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration + +/datum/ai_behavior/monkey_attack_mob/perform(delta_time, datum/ai_controller/controller) + . = ..() + + var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/mob/living/living_pawn = controller.pawn + + if(!target || target.stat != CONSCIOUS) + finish_action(controller, TRUE) //Target == owned + + if(isturf(target.loc) && !IS_DEAD_OR_INCAP(living_pawn)) // Check if they're a valid target + // check if target has a weapon + var/obj/item/W + for(var/obj/item/I in target.held_items) + if(!(I.item_flags & ABSTRACT)) + W = I + break + + // if the target has a weapon, chance to disarm them + if(W && DT_PROB(MONKEY_ATTACK_DISARM_PROB, delta_time)) + living_pawn.a_intent = INTENT_DISARM + monkey_attack(controller, target, delta_time) + else + living_pawn.a_intent = INTENT_HARM + monkey_attack(controller, target, delta_time) + + +/datum/ai_behavior/monkey_attack_mob/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = null + +/// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little +/datum/ai_behavior/monkey_attack_mob/proc/monkey_attack(datum/ai_controller/controller, mob/living/target, delta_time) + var/mob/living/living_pawn = controller.pawn + + if(living_pawn.next_move > world.time) + return + + living_pawn.changeNext_move(CLICK_CD_MELEE) //We play fair + + var/obj/item/weapon = locate(/obj/item) in living_pawn.held_items + + living_pawn.face_atom(target) + + if(isnull(controller.blackboard[BB_MONKEY_GUN_WORKED])) + controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE + + living_pawn.a_intent = INTENT_HARM + if(living_pawn.CanReach(target, weapon)) + if(weapon) + weapon.melee_attack_chain(living_pawn, target) + else + target.attack_paw(living_pawn) + controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // We reset their memory of the gun being 'broken' if they accomplish some other attack + else if(weapon) + var/atom/real_target = target + if(prob(10)) // Artificial miss + real_target = pick(oview(2, target)) + + var/obj/item/gun/gun = locate() in living_pawn.held_items + var/can_shoot = gun?.can_shoot() || FALSE + if(gun && controller.blackboard[BB_MONKEY_GUN_WORKED] && prob(95)) + // We attempt to attack even if we can't shoot so we get the effects of pulling the trigger + gun.afterattack(real_target, living_pawn, FALSE) + controller.blackboard[BB_MONKEY_GUN_WORKED] = can_shoot ? TRUE : prob(80) // Only 20% likely to notice it didn't work + if(can_shoot) + controller.blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] = TRUE + else + living_pawn.throw_item(real_target) + controller.blackboard[BB_MONKEY_GUN_WORKED] = TRUE // 'worked' + + + + // no de-aggro + if(controller.blackboard[BB_MONKEY_AGRESSIVE]) + return + + if(DT_PROB(MONKEY_HATRED_REDUCTION_PROB, delta_time)) + controller.blackboard[BB_MONKEY_ENEMIES][target]-- + + // if we are not angry at our target, go back to idle + if(controller.blackboard[BB_MONKEY_ENEMIES][target] <= 0) + var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] + enemies.Remove(target) + if(controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] == target) + finish_action(controller, TRUE) + +/datum/ai_behavior/disposal_mob + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM //performs to increase frustration + +/datum/ai_behavior/disposal_mob/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = null //Reset attack target + controller.blackboard[BB_MONKEY_DISPOSING] = FALSE //No longer disposing + controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = null //No target disposal + +/datum/ai_behavior/disposal_mob/perform(delta_time, datum/ai_controller/controller) + . = ..() + + if(controller.blackboard[BB_MONKEY_DISPOSING]) //We are disposing, don't do ANYTHING!!!! + return + + var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/mob/living/living_pawn = controller.pawn + + controller.current_movement_target = target + + if(target.pulledby != living_pawn && !HAS_AI_CONTROLLER_TYPE(target.pulledby, /datum/ai_controller/monkey)) //Dont steal from my fellow monkeys. + if(living_pawn.Adjacent(target) && isturf(target.loc)) + living_pawn.a_intent = INTENT_GRAB + target.grabbedby(living_pawn) + return //Do the rest next turn + + var/obj/machinery/disposal/disposal = controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] + controller.current_movement_target = disposal + + if(living_pawn.Adjacent(disposal)) + INVOKE_ASYNC(src, PROC_REF(try_disposal_mob), controller) //put him in! + else //This means we might be getting pissed! + return + +/datum/ai_behavior/disposal_mob/proc/try_disposal_mob(datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + var/mob/living/target = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/obj/machinery/disposal/disposal = controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] + + controller.blackboard[BB_MONKEY_DISPOSING] = TRUE + + if(target && disposal?.stuff_mob_in(target, living_pawn)) + disposal.flush() + finish_action(controller, TRUE) + + +/datum/ai_behavior/recruit_monkeys/perform(delta_time, datum/ai_controller/controller) + . = ..() + + controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN + var/mob/living/living_pawn = controller.pawn + + for(var/mob/living/L in view(living_pawn, MONKEY_ENEMY_VISION)) + if(!HAS_AI_CONTROLLER_TYPE(L, /datum/ai_controller/monkey)) + continue + + if(!DT_PROB(MONKEY_RECRUIT_PROB, delta_time)) + continue + var/datum/ai_controller/monkey/monkey_ai = L.ai_controller + var/atom/your_enemy = controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] + var/list/enemies = L.ai_controller.blackboard[BB_MONKEY_ENEMIES] + enemies[your_enemy] = MONKEY_RECRUIT_HATED_AMOUNT + monkey_ai.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] = world.time + MONKEY_RECRUIT_COOLDOWN + finish_action(controller, TRUE) diff --git a/code/datums/ai/monkey/monkey_controller.dm b/code/datums/ai/monkey/monkey_controller.dm new file mode 100644 index 000000000000..4cb8605d185f --- /dev/null +++ b/code/datums/ai/monkey/monkey_controller.dm @@ -0,0 +1,255 @@ +/* +AI controllers are a datumized form of AI that simulates the input a player would otherwise give to a mob. What this means is that these datums +have ways of interacting with a specific mob and control it. +*/ +///OOK OOK OOK + +/datum/ai_controller/monkey + movement_delay = 0.4 SECONDS + planning_subtrees = list(/datum/ai_planning_subtree/monkey_tree) + blackboard = list( + BB_MONKEY_AGRESSIVE = FALSE, + BB_MONKEY_BEST_FORCE_FOUND = 0, + BB_MONKEY_ENEMIES = list(), + BB_MONKEY_BLACKLISTITEMS = list(), + BB_MONKEY_PICKUPTARGET = null, + BB_MONKEY_PICKPOCKETING = FALSE, + BB_MONKEY_DISPOSING = FALSE, + BB_MONKEY_TARGET_DISPOSAL = null, + BB_MONKEY_CURRENT_ATTACK_TARGET = null, + BB_MONKEY_GUN_NEURONS_ACTIVATED = FALSE, + BB_MONKEY_GUN_WORKED = TRUE, + BB_MONKEY_NEXT_HUNGRY = 0 + ) +/datum/ai_controller/monkey/angry + +/datum/ai_controller/monkey/angry/TryPossessPawn(atom/new_pawn) + . = ..() + if(. & AI_CONTROLLER_INCOMPATIBLE) + return + blackboard[BB_MONKEY_AGRESSIVE] = TRUE //Angry cunt + +/datum/ai_controller/monkey/TryPossessPawn(atom/new_pawn) + if(!isliving(new_pawn)) + return AI_CONTROLLER_INCOMPATIBLE + + blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(0, 300) + + var/mob/living/living_pawn = new_pawn + RegisterSignal(new_pawn, COMSIG_PARENT_ATTACKBY, PROC_REF(on_attackby)) + RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) + RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_PAW, PROC_REF(on_attack_paw)) + RegisterSignal(new_pawn, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act)) + RegisterSignal(new_pawn, COMSIG_ATOM_HITBY, PROC_REF(on_hitby)) + RegisterSignal(new_pawn, COMSIG_LIVING_START_PULL, PROC_REF(on_startpulling)) + RegisterSignal(new_pawn, COMSIG_LIVING_TRY_SYRINGE, PROC_REF(on_try_syringe)) + RegisterSignal(new_pawn, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk)) + RegisterSignal(new_pawn, COMSIG_CARBON_CUFF_ATTEMPTED, PROC_REF(on_attempt_cuff)) + RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, PROC_REF(update_movespeed)) + RegisterSignal(new_pawn, COMSIG_FOOD_EATEN, PROC_REF(on_eat)) + movement_delay = living_pawn.cached_multiplicative_slowdown + return ..() //Run parent at end + +/datum/ai_controller/monkey/UnpossessPawn(destroy) + UnregisterSignal(pawn, list(COMSIG_PARENT_ATTACKBY, COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW, COMSIG_ATOM_BULLET_ACT, COMSIG_ATOM_HITBY, COMSIG_LIVING_START_PULL,\ + COMSIG_LIVING_TRY_SYRINGE, COMSIG_ATOM_HULK_ATTACK, COMSIG_CARBON_CUFF_ATTEMPTED, COMSIG_MOB_MOVESPEED_UPDATED)) + return ..() //Run parent at end + +/datum/ai_controller/monkey/able_to_run() + . = ..() + var/mob/living/living_pawn = pawn + + if(IS_DEAD_OR_INCAP(living_pawn)) + return FALSE + +///re-used behavior pattern by monkeys for finding a weapon +/datum/ai_controller/monkey/proc/TryFindWeapon() + var/mob/living/living_pawn = pawn + + if(!locate(/obj/item) in living_pawn.held_items) + blackboard[BB_MONKEY_BEST_FORCE_FOUND] = 0 + + if(blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] && (locate(/obj/item/gun) in living_pawn.held_items)) + // We have a gun, what could we possibly want? + return FALSE + + var/obj/item/weapon + var/list/nearby_items = list() + for(var/obj/item/item in oview(2, living_pawn)) + nearby_items += item + + weapon = GetBestWeapon(nearby_items, living_pawn.held_items) + + var/pickpocket = FALSE + for(var/mob/living/carbon/human/human in oview(5, living_pawn)) + var/obj/item/held_weapon = GetBestWeapon(human.held_items + weapon, living_pawn.held_items) + if(held_weapon == weapon) // It's just the same one, not a held one + continue + pickpocket = TRUE + weapon = held_weapon + + if(!weapon || (weapon in living_pawn.held_items)) + return FALSE + + blackboard[BB_MONKEY_PICKUPTARGET] = weapon + current_movement_target = weapon + if(pickpocket) + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_equip/pickpocket)) + else + LAZYADD(current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_equip/ground)) + return TRUE + +/// Returns either the best weapon from the given choices or null if held weapons are better +/datum/ai_controller/monkey/proc/GetBestWeapon(list/choices, list/held_weapons) + var/gun_neurons_activated = blackboard[BB_MONKEY_GUN_NEURONS_ACTIVATED] + var/top_force = 0 + var/obj/item/top_force_item + for(var/obj/item/item as anything in held_weapons) + if(!item) + continue + if(blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && istype(item, /obj/item/gun)) + // We have a gun, why bother looking for something inferior + // Also yes it is intentional that monkeys dont know how to pick the best gun + return item + if(item.force > top_force) + top_force = item.force + top_force_item = item + + for(var/obj/item/item as anything in choices) + if(!item) + continue + if(blackboard[BB_MONKEY_BLACKLISTITEMS][item]) + continue + if(gun_neurons_activated && istype(item, /obj/item/gun)) + return item + if(item.force <= top_force) + continue + top_force_item = item + top_force = item.force + + return top_force_item + +/datum/ai_controller/monkey/proc/TryFindFood() + . = FALSE + var/mob/living/living_pawn = pawn + + // Held items + + var/list/food_candidates = list() + for(var/obj/item as anything in living_pawn.held_items) + if(!item || !IsEdible(item)) + continue + food_candidates += item + + for(var/obj/item/candidate in oview(2, living_pawn)) + if(!IsEdible(candidate)) + continue + food_candidates += candidate + + if(length(food_candidates)) + var/obj/item/best_held = GetBestWeapon(null, living_pawn.held_items) + for(var/obj/item/held as anything in living_pawn.held_items) + if(!held || held == best_held) + continue + living_pawn.dropItemToGround(held) + + AddBehavior(/datum/ai_behavior/consume, pick(food_candidates)) + return TRUE + +/datum/ai_controller/monkey/proc/IsEdible(obj/item/thing) + if(IS_EDIBLE(thing)) + return TRUE + if(istype(thing, /obj/item/reagent_containers/food/drinks/drinkingglass)) + var/obj/item/reagent_containers/food/drinks/drinkingglass/glass = thing + if(glass.reagents.total_volume) // The glass has something in it, time to drink the mystery liquid! + return TRUE + return FALSE + +//When idle just kinda fuck around. +/datum/ai_controller/monkey/PerformIdleBehavior(delta_time) + var/mob/living/living_pawn = pawn + + if(DT_PROB(25, delta_time) && (living_pawn.mobility_flags & MOBILITY_MOVE) && isturf(living_pawn.loc) && !living_pawn.pulledby) + step(living_pawn, pick(GLOB.cardinals)) + else if(DT_PROB(5, delta_time)) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick("screech")) + else if(DT_PROB(1, delta_time)) + INVOKE_ASYNC(living_pawn, TYPE_PROC_REF(/mob, emote), pick("scratch","jump","roll","tail")) + +///Reactive events to being hit +/datum/ai_controller/monkey/proc/retaliate(mob/living/L) + var/list/enemies = blackboard[BB_MONKEY_ENEMIES] + enemies[L] += MONKEY_HATRED_AMOUNT + +/datum/ai_controller/monkey/proc/on_attackby(datum/source, obj/item/I, mob/user) + SIGNAL_HANDLER + if(I.force && I.damtype != STAMINA) + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attack_hand(datum/source, mob/living/L) + SIGNAL_HANDLER + if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) + retaliate(L) + else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) + retaliate(L) + +/datum/ai_controller/monkey/proc/on_attack_paw(datum/source, mob/living/L) + SIGNAL_HANDLER + if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) + retaliate(L) + else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) + retaliate(L) + +/datum/ai_controller/monkey/proc/on_bullet_act(datum/source, obj/projectile/Proj) + SIGNAL_HANDLER + var/mob/living/living_pawn = pawn + if(istype(Proj , /obj/projectile/beam)||istype(Proj, /obj/projectile/bullet)) + if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE)) + if(!Proj.nodamage && Proj.damage < living_pawn.health && isliving(Proj.firer)) + retaliate(Proj.firer) + +/datum/ai_controller/monkey/proc/on_hitby(datum/source, atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + if(istype(AM, /obj/item)) + var/mob/living/living_pawn = pawn + var/obj/item/I = AM + if(I.throwforce < living_pawn.health && ishuman(I.thrownby)) + var/mob/living/carbon/human/H = I.thrownby + retaliate(H) + +/datum/ai_controller/monkey/proc/on_startpulling(datum/source, atom/movable/puller, state, force) + SIGNAL_HANDLER + var/mob/living/living_pawn = pawn + if(!IS_DEAD_OR_INCAP(living_pawn) && prob(MONKEY_PULL_AGGRO_PROB)) // nuh uh you don't pull me! + retaliate(living_pawn.pulledby) + return TRUE + +/datum/ai_controller/monkey/proc/on_try_syringe(datum/source, mob/user) + SIGNAL_HANDLER + // chance of monkey retaliation + if(prob(MONKEY_SYRINGE_RETALIATION_PROB)) + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attack_hulk(datum/source, mob/user) + SIGNAL_HANDLER + retaliate(user) + +/datum/ai_controller/monkey/proc/on_attempt_cuff(datum/source, mob/user) + SIGNAL_HANDLER + // chance of monkey retaliation + if(prob(MONKEY_CUFF_RETALIATION_PROB)) + retaliate(user) + +/datum/ai_controller/monkey/proc/update_movespeed(mob/living/pawn) + SIGNAL_HANDLER + movement_delay = pawn.cached_multiplicative_slowdown + +/datum/ai_controller/monkey/proc/target_del(target) + SIGNAL_HANDLER + blackboard[BB_MONKEY_BLACKLISTITEMS] -= target + +/datum/ai_controller/monkey/proc/on_eat(mob/living/pawn) + SIGNAL_HANDLER + blackboard[BB_MONKEY_NEXT_HUNGRY] = world.time + rand(120, 600) SECONDS diff --git a/code/datums/ai/monkey/monkey_subtrees.dm b/code/datums/ai/monkey/monkey_subtrees.dm new file mode 100644 index 000000000000..4e7317de5a56 --- /dev/null +++ b/code/datums/ai/monkey/monkey_subtrees.dm @@ -0,0 +1,84 @@ +/datum/ai_planning_subtree/monkey_tree/SelectBehaviors(datum/ai_controller/monkey/controller, delta_time) + var/mob/living/living_pawn = controller.pawn + + if(SHOULD_RESIST(living_pawn) && DT_PROB(MONKEY_RESIST_PROB, delta_time)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/resist)) //BRO IM ON FUCKING FIRE BRO + return SUBTREE_RETURN_FINISH_PLANNING //IM NOT DOING ANYTHING ELSE BUT EXTUINGISH MYSELF, GOOD GOD HAVE MERCY. + + var/list/enemies = controller.blackboard[BB_MONKEY_ENEMIES] + + if(HAS_TRAIT(controller.pawn, TRAIT_PACIFISM)) //Not a pacifist? lets try some combat behavior. + return + + var/mob/living/selected_enemy + if(length(enemies) || controller.blackboard[BB_MONKEY_AGRESSIVE]) //We have enemies or are pissed + var/list/valids = list() + for(var/mob/living/possible_enemy in view(MONKEY_ENEMY_VISION, living_pawn)) + if(possible_enemy == living_pawn || (!enemies[possible_enemy] && (!controller.blackboard[BB_MONKEY_AGRESSIVE] || HAS_AI_CONTROLLER_TYPE(possible_enemy, /datum/ai_controller/monkey)))) //Are they an enemy? (And do we even care?) + continue + // Weighted list, so the closer they are the more likely they are to be chosen as the enemy + valids[possible_enemy] = CEILING(100 / (get_dist(living_pawn, possible_enemy) || 1), 1) + + selected_enemy = pick_weight(valids) + + if(selected_enemy) + if(!selected_enemy.stat) //He's up, get him! + if(living_pawn.health < MONKEY_FLEE_HEALTH) //Time to skeddadle + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_flee)) + return //I'm running fuck you guys + + if(controller.TryFindWeapon()) //Getting a weapon is higher priority if im not fleeing. + return SUBTREE_RETURN_FINISH_PLANNING + + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy + controller.current_movement_target = selected_enemy + if(controller.blackboard[BB_MONKEY_RECRUIT_COOLDOWN] < world.time) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/recruit_monkeys)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/battle_screech/monkey)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/monkey_attack_mob)) + return SUBTREE_RETURN_FINISH_PLANNING //Focus on this + + else //He's down, can we disposal him? + var/obj/machinery/disposal/bodyDisposal = locate(/obj/machinery/disposal/) in view(MONKEY_ENEMY_VISION, living_pawn) + if(bodyDisposal) + controller.blackboard[BB_MONKEY_CURRENT_ATTACK_TARGET] = selected_enemy + controller.blackboard[BB_MONKEY_TARGET_DISPOSAL] = bodyDisposal + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/disposal_mob)) + return SUBTREE_RETURN_FINISH_PLANNING + + if(prob(5)) + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/use_in_hand)) + + if(selected_enemy || !DT_PROB(MONKEY_SHENANIGAN_PROB, delta_time)) + return + + if(world.time >= controller.blackboard[BB_MONKEY_NEXT_HUNGRY] && controller.TryFindFood()) + return + + if(prob(50)) + var/list/possible_targets = list() + for(var/atom/thing in view(2, living_pawn)) + if(!thing.mouse_opacity) + continue + if(thing.IsObscured()) + continue + possible_targets += thing + var/atom/target = pick(possible_targets) + if(target) + controller.current_movement_target = target + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/use_on_object)) + return + + if(prob(5) && (locate(/obj/item) in living_pawn.held_items)) + var/list/possible_receivers = list() + for(var/mob/living/candidate in oview(2, controller.pawn)) + possible_receivers += candidate + + if(length(possible_receivers)) + var/mob/living/target = pick(possible_receivers) + controller.current_movement_target = target + LAZYADD(controller.current_behaviors, GET_AI_BEHAVIOR(/datum/ai_behavior/give)) + return + + controller.TryFindWeapon() diff --git a/code/datums/ai/movement/_ai_movement.dm b/code/datums/ai/movement/_ai_movement.dm new file mode 100644 index 000000000000..c9d47bc6d66b --- /dev/null +++ b/code/datums/ai/movement/_ai_movement.dm @@ -0,0 +1,19 @@ +///This datum is an abstract class that can be overriden for different types of movement +/datum/ai_movement + ///Assoc list ist of controllers that are currently moving as key, and what they are moving to as value + var/list/moving_controllers = list() + ///How many times a given controller can fail on their route before they just give up + var/max_pathing_attempts + +/datum/ai_movement/proc/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target) + controller.pathing_attempts = 0 + if(!moving_controllers.len) + START_PROCESSING(SSai_movement, src) + moving_controllers[controller] = current_movement_target + +/datum/ai_movement/proc/stop_moving_towards(datum/ai_controller/controller) + controller.pathing_attempts = 0 + moving_controllers -= controller + + if(!moving_controllers.len) + STOP_PROCESSING(SSai_movement, src) diff --git a/code/datums/ai/movement/ai_movement_dumb.dm b/code/datums/ai/movement/ai_movement_dumb.dm new file mode 100644 index 000000000000..0ce64669d373 --- /dev/null +++ b/code/datums/ai/movement/ai_movement_dumb.dm @@ -0,0 +1,27 @@ +///The most braindead type of movement, bee-line to the target with no concern of whats infront of us. +/datum/ai_movement/dumb + max_pathing_attempts = 16 + +///Put your movement behavior in here! +/datum/ai_movement/dumb/process(delta_time) + for(var/datum/ai_controller/controller as anything in moving_controllers) + if(!COOLDOWN_FINISHED(controller, movement_cooldown)) + continue + COOLDOWN_START(controller, movement_cooldown, controller.movement_delay) + + var/atom/movable/movable_pawn = controller.pawn + + if(!isturf(movable_pawn.loc)) //No moving if not on a turf + continue + + var/current_loc = get_turf(movable_pawn) + + var/turf/target_turf = get_step_towards(movable_pawn, controller.current_movement_target) + + if(!is_type_in_typecache(target_turf, GLOB.dangerous_turfs)) + movable_pawn.Move(target_turf, get_dir(current_loc, target_turf)) + + if(current_loc == get_turf(movable_pawn)) //Did we even move after trying to move? + controller.pathing_attempts++ + if(controller.pathing_attempts >= max_pathing_attempts) + controller.CancelActions() diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm new file mode 100644 index 000000000000..ea05b0fc899e --- /dev/null +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -0,0 +1,61 @@ +/** + * This movement datum represents smart-pathing + */ +/datum/ai_movement/jps + max_pathing_attempts = 4 + +///Put your movement behavior in here! +/datum/ai_movement/jps/process(delta_time) + for(var/datum/ai_controller/controller as anything in moving_controllers) + if(!COOLDOWN_FINISHED(controller, movement_cooldown)) + continue + COOLDOWN_START(controller, movement_cooldown, controller.movement_delay) + + var/atom/movable/movable_pawn = controller.pawn + if(!isturf(movable_pawn.loc)) //No moving if not on a turf + continue + + var/minimum_distance = controller.max_target_distance + // right now I'm just taking the shortest minimum distance of our current behaviors, at some point in the future + // we should let whatever sets the current_movement_target also set the min distance and max path length + // (or at least cache it on the controller) + if(LAZYLEN(controller.current_behaviors)) + for(var/datum/ai_behavior/iter_behavior as anything in controller.current_behaviors) + if(iter_behavior.required_distance < minimum_distance) + minimum_distance = iter_behavior.required_distance + + if(get_dist(movable_pawn, controller.current_movement_target) <= minimum_distance) + continue + + var/generate_path = FALSE // set to TRUE when we either have no path, or we failed a step + if(length(controller.movement_path)) + var/turf/next_step = controller.movement_path[1] + movable_pawn.Move(next_step) + + // this check if we're on exactly the next tile may be overly brittle for dense pawns who may get bumped slightly + // to the side while moving but could maybe still follow their path without needing a whole new path + if(get_turf(movable_pawn) == next_step) + controller.movement_path.Cut(1,2) + else + generate_path = TRUE + else + generate_path = TRUE + + if(generate_path) + if(!COOLDOWN_FINISHED(controller, repath_cooldown)) + continue + controller.pathing_attempts++ + if(controller.pathing_attempts >= max_pathing_attempts) + controller.CancelActions() + continue + + COOLDOWN_START(controller, repath_cooldown, 2 SECONDS) + controller.movement_path = get_path_to(movable_pawn, controller.current_movement_target, AI_MAX_PATH_LENGTH, minimum_distance, id=controller.get_access()) + +/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target) + controller.movement_path = null + return ..() + +/datum/ai_movement/jps/stop_moving_towards(datum/ai_controller/controller) + controller.movement_path = null + return ..() diff --git a/code/datums/components/spinny.dm b/code/datums/components/spinny.dm new file mode 100644 index 000000000000..cdf5262ab31b --- /dev/null +++ b/code/datums/components/spinny.dm @@ -0,0 +1,33 @@ +/** + * spinny.dm + * + * It's a component that spins things a whole bunch, like [proc/dance_rotate] but without the sleeps +*/ +/datum/component/spinny + dupe_mode = COMPONENT_DUPE_UNIQUE + /// How many turns are left? + var/steps_left + /// Turns clockwise by default, or counterclockwise if the reverse argument is TRUE + var/turn_degrees = 90 + +/datum/component/spinny/Initialize(steps = 12, reverse = FALSE) + if(!isatom(parent)) + return COMPONENT_INCOMPATIBLE + + steps_left = steps + turn_degrees = (reverse ? -90 : 90) + START_PROCESSING(SSfastprocess, src) + +/datum/component/spinny/Destroy(force, silent) + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/component/spinny/process(delta_time) + steps_left-- + var/atom/spinny_boy = parent + if(!istype(spinny_boy) || steps_left <= 0) + qdel(src) + return + + // 25% chance to make 2 turns instead of 1 since the old dance_rotate wasn't strictly clockwise/counterclockwise + spinny_boy.setDir(turn(spinny_boy.dir, turn_degrees * (prob(25) ? 2 : 1))) diff --git a/code/datums/mutations/body.dm b/code/datums/mutations/body.dm index 0954c2a35bc8..d520c3bae5ed 100644 --- a/code/datums/mutations/body.dm +++ b/code/datums/mutations/body.dm @@ -179,11 +179,11 @@ /datum/mutation/human/race/on_acquiring(mob/living/carbon/human/owner) if(..()) return - . = owner.monkeyize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE) + . = owner.monkeyize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE | TR_KEEPAI) /datum/mutation/human/race/on_losing(mob/living/carbon/monkey/owner) if(owner && istype(owner) && owner.stat != DEAD && (owner.dna.mutations.Remove(src))) - . = owner.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE) + . = owner.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_KEEPSE | TR_KEEPAI) /datum/mutation/human/glow name = "Glowy" diff --git a/code/game/atoms.dm b/code/game/atoms.dm index 46b08169f829..75a36e1aa677 100644 --- a/code/game/atoms.dm +++ b/code/game/atoms.dm @@ -136,6 +136,9 @@ ///List of smoothing groups this atom can smooth with. If this is null and atom is smooth, it smooths only with itself. var/list/canSmoothWith = null + ///AI controller that controls this atom. type on init, then turned into an instance during runtime + var/datum/ai_controller/ai_controller + /// The icon file of the connector to use when smoothing. /// Use of connectors requires the smoothing flags SMOOTH_BITMASK and SMOOTH_CONNECTORS. var/connector_icon = null @@ -265,6 +268,7 @@ set_custom_materials(temp_list) ComponentInitialize() + InitializeAIController() return INITIALIZE_HINT_NORMAL @@ -311,6 +315,7 @@ LAZYCLEARLIST(managed_overlays) QDEL_NULL(light) + QDEL_NULL(ai_controller) if(smoothing_flags & SMOOTH_QUEUED) SSicon_smooth.remove_from_queues(src) @@ -737,6 +742,7 @@ * throw lots of items around - singularity being a notable example) */ /atom/proc/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) + SEND_SIGNAL(src, COMSIG_ATOM_HITBY, AM, skipcatch, hitpush, blocked, throwingdatum) if(density && !has_gravity(AM)) //thrown stuff bounces off dense stuff in no grav, unless the thrown stuff ends up inside what it hit(embedding, bola, etc...). addtimer(CALLBACK(src, PROC_REF(hitby_react), AM), 2) @@ -1068,6 +1074,7 @@ VV_DROPDOWN_OPTION(VV_HK_RADIATE, "Radiate") VV_DROPDOWN_OPTION(VV_HK_EDIT_FILTERS, "Edit Filters") VV_DROPDOWN_OPTION(VV_HK_SELL, "Export Item") + VV_DROPDOWN_OPTION(VV_HK_ADD_AI, "Add AI controller") /atom/vv_do_topic(list/href_list) . = ..() @@ -1112,6 +1119,15 @@ var/strength = input(usr, "Choose the radiation strength.", "Choose the strength.") as num|null if(!isnull(strength)) AddComponent(/datum/component/radioactive, strength, src) + + if(href_list[VV_HK_ADD_AI]) + if(!check_rights(R_VAREDIT)) + return + var/result = input(usr, "Choose the AI controller to apply to this atom WARNING: Not all AI works on all atoms.", "AI controller") as null|anything in subtypesof(/datum/ai_controller) + if(!result) + return + ai_controller = new result(src) + if(href_list[VV_HK_MODIFY_TRANSFORM] && check_rights(R_VAREDIT)) var/result = input(usr, "Choose the transformation to apply","Transform Mod") as null|anything in list("Scale","Translate","Rotate") var/matrix/M = transform @@ -1710,3 +1726,12 @@ */ /atom/proc/setClosed() return + +/** +* Instantiates the AI controller of this atom. Override this if you want to assign variables first. +* +* This will work fine without manually passing arguments. ++*/ +/atom/proc/InitializeAIController() + if(ai_controller) + ai_controller = new ai_controller(src) diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm index ea51bac01b50..93225b2af9a9 100644 --- a/code/game/machinery/porta_turret/portable_turret.dm +++ b/code/game/machinery/porta_turret/portable_turret.dm @@ -513,9 +513,6 @@ if(!is_type_in_typecache(target_mob, dangerous_fauna)) return FALSE - if(ismonkey(target_mob)) - var/mob/living/carbon/monkey/monke = target_mob - return monke.mode == MONKEY_HUNT && target(target_mob) if(istype(target_mob, /mob/living/simple_animal/hostile/retaliate)) var/mob/living/simple_animal/hostile/retaliate/target_animal = target_mob return length(target_animal.enemies) && target(target_mob) diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 23de618975a8..17bcfd78d5c9 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -867,7 +867,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb . = "" /obj/item/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum) - return + return SEND_SIGNAL(src, COMSIG_ATOM_HITBY, AM, skipcatch, hitpush, blocked, throwingdatum) /obj/item/attack_hulk(mob/living/carbon/human/user) return FALSE diff --git a/code/game/objects/items/handcuffs.dm b/code/game/objects/items/handcuffs.dm index f36c27bb244d..66d829baee25 100644 --- a/code/game/objects/items/handcuffs.dm +++ b/code/game/objects/items/handcuffs.dm @@ -40,17 +40,13 @@ if(!istype(C)) return + SEND_SIGNAL(C, COMSIG_CARBON_CUFF_ATTEMPTED, user) + if(iscarbon(user) && (HAS_TRAIT(user, TRAIT_CLUMSY) && prob(50))) to_chat(user, "Uh... how do those things work?!") apply_cuffs(user,user) return - // chance of monkey retaliation - if(ismonkey(C) && prob(MONKEY_CUFF_RETALIATION_PROB)) - var/mob/living/carbon/monkey/M - M = C - M.retaliate(user) - if(!C.handcuffed) if(C.canBeHandcuffed()) C.visible_message("[user] is trying to put [src.name] on [C]!", \ diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm index bbcaa94f0867..aa63701ce0e9 100644 --- a/code/game/objects/objs.dm +++ b/code/game/objects/objs.dm @@ -243,11 +243,20 @@ /obj/get_dumping_location(datum/component/storage/source,mob/user) return get_turf(src) -/obj/proc/CanAStarPass(ID, dir, caller) - if(ismovable(caller)) - var/atom/movable/AM = caller - if(AM.pass_flags & pass_flags_self) - return TRUE +/** + * This proc is used for telling whether something can pass by this object in a given direction, for use by the pathfinding system. + * + * Trying to generate one long path across the station will call this proc on every single object on every single tile that we're seeing if we can move through, likely + * multiple times per tile since we're likely checking if we can access said tile from multiple directions, so keep these as lightweight as possible. + * + * Arguments: + * * ID- An ID card representing what access we have (and thus if we can open things like airlocks or windows to pass through them). The ID card's physical location does not matter, just the reference + * * to_dir- What direction we're trying to move in, relevant for things like directional windows that only block movement in certain directions + * * caller- The movable we're checking pass flags for, if we're making any such checks + **/ +/obj/proc/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) + if(istype(caller) && (caller.pass_flags & pass_flags_self)) + return TRUE . = !density /obj/proc/check_uplink_validity() diff --git a/code/game/objects/structures/girders.dm b/code/game/objects/structures/girders.dm index d30f28801f76..85af7c9bb8eb 100644 --- a/code/game/objects/structures/girders.dm +++ b/code/game/objects/structures/girders.dm @@ -304,11 +304,10 @@ if((mover.pass_flags & PASSGRILLE) || istype(mover, /obj/projectile)) return prob(girderpasschance) -/obj/structure/girder/CanAStarPass(ID, dir, caller) +/obj/structure/girder/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSGRILLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSGRILLE) /obj/structure/girder/deconstruct(disassembled = TRUE) if(!(flags_1 & NODECONSTRUCT_1)) diff --git a/code/game/objects/structures/grille.dm b/code/game/objects/structures/grille.dm index 10a4413f442f..7e2527c11dae 100644 --- a/code/game/objects/structures/grille.dm +++ b/code/game/objects/structures/grille.dm @@ -135,11 +135,10 @@ if(!. && istype(mover, /obj/projectile)) return prob(30) -/obj/structure/grille/CanAStarPass(ID, dir, caller) +/obj/structure/grille/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) . = !density - if(ismovable(caller)) - var/atom/movable/mover = caller - . = . || (mover.pass_flags & PASSGRILLE) + if(istype(caller)) + . = . || (caller.pass_flags & PASSGRILLE) /obj/structure/grille/attackby(obj/item/W, mob/user, params) user.changeNext_move(CLICK_CD_MELEE) diff --git a/code/game/objects/structures/window.dm b/code/game/objects/structures/window.dm index 35cc9fba1aae..e7a0fa946e23 100644 --- a/code/game/objects/structures/window.dm +++ b/code/game/objects/structures/window.dm @@ -379,7 +379,7 @@ /obj/structure/window/get_dumping_location(obj/item/storage/source,mob/user) return null -/obj/structure/window/CanAStarPass(ID, to_dir) +/obj/structure/window/CanAStarPass(obj/item/card/id/ID, to_dir, atom/movable/caller) if(!density) return TRUE if(fulltile || (dir == to_dir)) diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm index 4da6e25703bb..1f9dfc08f7da 100644 --- a/code/game/turfs/turf.dm +++ b/code/game/turfs/turf.dm @@ -676,3 +676,23 @@ GLOBAL_LIST_EMPTY(created_baseturf_lists) /turf/bullet_act(obj/projectile/hitting_projectile) . = ..() bullet_hit_sfx(hitting_projectile) + +/** + * Returns adjacent turfs to this turf that are reachable, in all cardinal directions + * + * Arguments: + * * caller: The movable, if one exists, being used for mobility checks to see what tiles it can reach + * * ID: An ID card that decides if we can gain access to doors that would otherwise block a turf + * * simulated_only: Do we only worry about turfs with simulated atmos, most notably things that aren't space? +*/ +/turf/proc/reachableAdjacentTurfs(caller, ID, simulated_only) + var/static/space_type_cache = typecacheof(/turf/open/space) + . = list() + + for(var/iter_dir in GLOB.cardinals) + var/turf/turf_to_check = get_step(src,iter_dir) + if(!turf_to_check || (simulated_only && space_type_cache[turf_to_check.type])) + continue + if(turf_to_check.density || LinkBlockedWithAccess(turf_to_check, caller, ID)) + continue + . += turf_to_check diff --git a/code/modules/antagonists/changeling/powers/tiny_prick.dm b/code/modules/antagonists/changeling/powers/tiny_prick.dm index 033b71b6df5b..0ed035002f09 100644 --- a/code/modules/antagonists/changeling/powers/tiny_prick.dm +++ b/code/modules/antagonists/changeling/powers/tiny_prick.dm @@ -47,7 +47,7 @@ return if(!isturf(user.loc)) return - if(!AStar(user, target.loc, /turf/proc/Distance, changeling.sting_range, simulated_only = FALSE)) + if(!get_path_to(user, target, max_distance = changeling.sting_range, simulated_only = FALSE)) return if(target.mind && target.mind.has_antag_datum(/datum/antagonist/changeling)) sting_feedback(user, target) @@ -106,7 +106,7 @@ C.real_name = NewDNA.real_name NewDNA.transfer_identity(C) if(ismonkey(C)) - C.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG) + C.humanize(TR_KEEPITEMS | TR_KEEPIMPLANTS | TR_KEEPORGANS | TR_KEEPDAMAGE | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG | TR_KEEPAI) C.updateappearance(mutcolor_update=1) diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 5276cf514d65..bcf78e60c8d1 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -108,6 +108,7 @@ /mob/proc/throw_item(atom/target) SEND_SIGNAL(src, COMSIG_MOB_THROW, target) + SEND_GLOBAL_SIGNAL(COMSIG_GLOB_CARBON_THROW_THING, src, target) return /mob/living/carbon/throw_item(atom/target) diff --git a/code/modules/mob/living/carbon/carbon_defense.dm b/code/modules/mob/living/carbon/carbon_defense.dm index 51815282406d..48747f0106f8 100644 --- a/code/modules/mob/living/carbon/carbon_defense.dm +++ b/code/modules/mob/living/carbon/carbon_defense.dm @@ -138,6 +138,9 @@ //ATTACK HAND IGNORING PARENT RETURN VALUE /mob/living/carbon/attack_hand(mob/living/carbon/human/user) + if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_HAND, user) & COMPONENT_CANCEL_ATTACK_CHAIN) + . = TRUE + for(var/datum/surgery/S in surgeries) if(body_position != LYING_DOWN && S.lying_required) continue diff --git a/code/modules/mob/living/carbon/emote.dm b/code/modules/mob/living/carbon/emote.dm index 358fa0626092..d96bbd72531a 100644 --- a/code/modules/mob/living/carbon/emote.dm +++ b/code/modules/mob/living/carbon/emote.dm @@ -82,7 +82,22 @@ key = "screech" key_third_person = "screeches" message = "screeches." - mob_type_allowed_typecache = list(/mob/living/carbon/monkey, /mob/living/carbon/alien) + mob_type_allowed_typecache = list(/mob/living/carbon/monkey) + emote_type = EMOTE_AUDIBLE + +/datum/emote/living/carbon/screech/get_sound(mob/living/user) + return pick('sound/creatures/monkey/monkey_screech_1.ogg', + 'sound/creatures/monkey/monkey_screech_2.ogg', + 'sound/creatures/monkey/monkey_screech_3.ogg', + 'sound/creatures/monkey/monkey_screech_4.ogg', + 'sound/creatures/monkey/monkey_screech_5.ogg', + 'sound/creatures/monkey/monkey_screech_6.ogg', + 'sound/creatures/monkey/monkey_screech_7.ogg') + +/datum/emote/living/carbon/screech/roar + key = "roar" + key_third_person = "roars" + message = "roars." /datum/emote/living/carbon/sign key = "sign" diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm index 0c8782129698..c4a447b59d5b 100644 --- a/code/modules/mob/living/carbon/human/examine.dm +++ b/code/modules/mob/living/carbon/human/examine.dm @@ -7,6 +7,7 @@ var/t_him = p_them() var/t_has = p_have() var/t_is = p_are() + var/t_es = p_es() var/obscure_name var/list/obscured = check_obscured_slots() var/skipface = ((wear_mask?.flags_inv & HIDEFACE) || (head?.flags_inv & HIDEFACE)) @@ -330,6 +331,8 @@ if(HAS_TRAIT(src, TRAIT_DUMB)) msg += "[t_He] [t_has] a stupid expression on [t_his] face.\n" if(getorgan(/obj/item/organ/brain)) + if(ai_controller?.ai_status == AI_STATUS_ON) + msg += "[t_He] do[t_es]n't appear to be [t_him]self.\n" if(!key) msg += "[t_He] [t_is] totally catatonic. The stresses of life in deep-space must have been too much for [t_him]. Any recovery is unlikely.\n" else if(!client) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index a4d89a53b548..b33a751df628 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -1289,6 +1289,9 @@ return known_name return . +/mob/living/carbon/human/monkeybrain + ai_controller = /datum/ai_controller/monkey + /mob/living/carbon/human/species var/race = null diff --git a/code/modules/mob/living/carbon/monkey/combat.dm b/code/modules/mob/living/carbon/monkey/combat.dm deleted file mode 100644 index 8fd4e89566c7..000000000000 --- a/code/modules/mob/living/carbon/monkey/combat.dm +++ /dev/null @@ -1,426 +0,0 @@ -#define MAX_RANGE_FIND 32 - -/mob/living/carbon/monkey - var/aggressive=0 // set to 1 using VV for an angry monkey - var/frustration=0 - var/pickupTimer=0 - var/list/enemies = list() - var/mob/living/target - var/obj/item/pickupTarget - var/mode = MONKEY_IDLE - var/list/myPath = list() - var/list/blacklistItems = list() - var/maxStepsTick = 6 - var/best_force = 0 - var/martial_art = new/datum/martial_art - var/resisting = FALSE - var/pickpocketing = FALSE - var/disposing_body = FALSE - var/obj/machinery/disposal/bodyDisposal = null - var/next_battle_screech = 0 - var/battle_screech_cooldown = 50 - -/mob/living/carbon/monkey/proc/IsStandingStill() - return resisting || pickpocketing || disposing_body - -// blocks -// taken from /mob/living/carbon/human/interactive/ -/mob/living/carbon/monkey/proc/walk2derpless(target) - if(!target || IsStandingStill()) - return 0 - - if(myPath.len <= 0) - myPath = get_path_to(src, get_turf(target), /turf/proc/Distance, MAX_RANGE_FIND + 1, 250,1) - - if(myPath) - if(myPath.len > 0) - for(var/i = 0; i < maxStepsTick; ++i) - if(!IsDeadOrIncap()) - if(myPath.len >= 1) - walk_to(src,myPath[1],0,5) - myPath -= myPath[1] - return 1 - - // failed to path correctly so just try to head straight for a bit - walk_to(src,get_turf(target),0,5) - sleep(1) - walk_to(src,0) - - return 0 - - -// taken from /mob/living/carbon/human/interactive/ -/mob/living/carbon/monkey/proc/IsDeadOrIncap() - return HAS_TRAIT(src, TRAIT_INCAPACITATED) || HAS_TRAIT(src, TRAIT_HANDS_BLOCKED) - - -/mob/living/carbon/monkey/proc/battle_screech() - if(next_battle_screech < world.time) - emote(pick("roar","screech")) - for(var/mob/living/carbon/monkey/M in view(7,src)) - M.next_battle_screech = world.time + battle_screech_cooldown - -/mob/living/carbon/monkey/proc/equip_item(obj/item/I) - if(I.loc == src) - return TRUE - - if(I.anchored) - blacklistItems[I] ++ - return FALSE - - // WEAPONS - if(istype(I, /obj/item)) - var/obj/item/W = I - if(W.force >= best_force) - put_in_hands(W) - best_force = W.force - return TRUE - - // CLOTHING - else if(istype(I, /obj/item/clothing)) - var/obj/item/clothing/C = I - monkeyDrop(C) - addtimer(CALLBACK(src, PROC_REF(pickup_and_wear), C), 5) - return TRUE - - // EVERYTHING ELSE - else - if(!get_item_for_held_index(1) || !get_item_for_held_index(2)) - put_in_hands(I) - return TRUE - - blacklistItems[I] ++ - return FALSE - -/mob/living/carbon/monkey/proc/pickup_and_wear(obj/item/clothing/C) - if(!equip_to_appropriate_slot(C)) - monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn - addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5) - -/mob/living/carbon/monkey/resist_restraints() - var/obj/item/I = null - if(handcuffed) - I = handcuffed - else if(legcuffed) - I = legcuffed - if(I) - changeNext_move(CLICK_CD_BREAKOUT) - last_special = world.time + CLICK_CD_BREAKOUT - cuff_resist(I) - -/mob/living/carbon/monkey/proc/should_target(mob/living/L) - if(HAS_TRAIT(src, TRAIT_PACIFISM)) - return FALSE - - if(enemies[L]) - return TRUE - - // target non-monkey mobs when aggressive, with a small probability of monkey v monkey - if(aggressive && (!istype(L, /mob/living/carbon/monkey/) || prob(MONKEY_AGGRESSIVE_MVM_PROB))) - return TRUE - - return FALSE - -/mob/living/carbon/monkey/proc/handle_combat() - if(pickupTarget) - if(IsDeadOrIncap() || blacklistItems[pickupTarget] || HAS_TRAIT(pickupTarget, TRAIT_NODROP)) - pickupTarget = null - else - pickupTimer++ - if(pickupTimer >= 4) - blacklistItems[pickupTarget] ++ - pickupTarget = null - pickupTimer = 0 - else - INVOKE_ASYNC(src, PROC_REF(walk2derpless), pickupTarget.loc) - if(Adjacent(pickupTarget) || Adjacent(pickupTarget.loc)) // next to target - drop_all_held_items() // who cares about these items, i want that one! - if(isturf(pickupTarget.loc)) // on floor - equip_item(pickupTarget) - pickupTarget = null - pickupTimer = 0 - else if(ismob(pickupTarget.loc)) // in someones hand - var/mob/M = pickupTarget.loc - if(!pickpocketing) - pickpocketing = TRUE - M.visible_message("[src] starts trying to take [pickupTarget] from [M]!", "[src] tries to take [pickupTarget]!") - INVOKE_ASYNC(src, PROC_REF(pickpocket), M) - return TRUE - - switch(mode) - if(MONKEY_IDLE) // idle - if(enemies.len) - var/list/around = view(src, MONKEY_ENEMY_VISION) // scan for enemies - for(var/mob/living/L in around) - if(should_target(L)) - if(L.stat == CONSCIOUS) - battle_screech() - retaliate(L) - return TRUE - else - bodyDisposal = locate(/obj/machinery/disposal/) in around - if(bodyDisposal) - target = L - mode = MONKEY_DISPOSE - return TRUE - - // pickup any nearby objects - if(!pickupTarget) - var/obj/item/I = locate(/obj/item/) in oview(2,src) - if(I && !blacklistItems[I]) - pickupTarget = I - else - var/mob/living/carbon/human/H = locate(/mob/living/carbon/human/) in oview(2,src) - if(H) - pickupTarget = pick(H.held_items) - - if(MONKEY_HUNT) // hunting for attacker - if(health < MONKEY_FLEE_HEALTH) - mode = MONKEY_FLEE - return TRUE - - if(target != null) - INVOKE_ASYNC(src, PROC_REF(walk2derpless), target) - - // pickup any nearby weapon - if(!pickupTarget && prob(MONKEY_WEAPON_PROB)) - var/obj/item/W = locate(/obj/item/) in oview(2,src) - if(!locate(/obj/item) in held_items) - best_force = 0 - if(W && !blacklistItems[W] && W.force > best_force) - pickupTarget = W - - // recruit other monkies - var/list/around = view(src, MONKEY_ENEMY_VISION) - for(var/mob/living/carbon/monkey/M in around) - if(M.mode == MONKEY_IDLE && prob(MONKEY_RECRUIT_PROB)) - M.battle_screech() - M.target = target - M.mode = MONKEY_HUNT - - // switch targets - for(var/mob/living/L in around) - if(L != target && should_target(L) && L.stat == CONSCIOUS && prob(MONKEY_SWITCH_TARGET_PROB)) - target = L - return TRUE - - // if can't reach target for long enough, go idle - if(frustration >= MONKEY_HUNT_FRUSTRATION_LIMIT) - back_to_idle() - return TRUE - - if(target && target.stat == CONSCIOUS) // make sure target exists - if(Adjacent(target) && isturf(target.loc) && !IsDeadOrIncap()) // if right next to perp - - // check if target has a weapon - var/obj/item/W - for(var/obj/item/I in target.held_items) - if(!(I.item_flags & ABSTRACT)) - W = I - break - - // if the target has a weapon, chance to disarm them - if(W && prob(MONKEY_ATTACK_DISARM_PROB)) - pickupTarget = W - a_intent = INTENT_DISARM - monkey_attack(target) - - else - a_intent = INTENT_HARM - monkey_attack(target) - - return TRUE - - else // not next to perp - var/turf/olddist = get_dist(src, target) - if((get_dist(src, target)) >= (olddist)) - frustration++ - else - frustration = 0 - else - back_to_idle() - - if(MONKEY_FLEE) - var/list/around = view(src, MONKEY_FLEE_VISION) - target = null - - // flee from anyone who attacked us and we didn't beat down - for(var/mob/living/L in around) - if(enemies[L] && L.stat == CONSCIOUS) - target = L - - if(target != null) - walk_away(src, target, MONKEY_ENEMY_VISION, 5) - else - back_to_idle() - - return TRUE - - if(MONKEY_DISPOSE) - - // if can't dispose of body go back to idle - if(!target || !bodyDisposal || frustration >= MONKEY_DISPOSE_FRUSTRATION_LIMIT) - back_to_idle() - return TRUE - - if(target.pulledby != src && !istype(target.pulledby, /mob/living/carbon/monkey/)) - - INVOKE_ASYNC(src, PROC_REF(walk2derpless), target.loc) - - if(Adjacent(target) && isturf(target.loc)) - a_intent = INTENT_GRAB - target.grabbedby(src) - else - var/turf/olddist = get_dist(src, target) - if((get_dist(src, target)) >= (olddist)) - frustration++ - else - frustration = 0 - - else if(!disposing_body) - INVOKE_ASYNC(src, PROC_REF(walk2derpless), bodyDisposal.loc) - - if(Adjacent(bodyDisposal)) - disposing_body = TRUE - addtimer(CALLBACK(src, PROC_REF(stuff_mob_in)), 5) - - else - var/turf/olddist = get_dist(src, bodyDisposal) - if((get_dist(src, bodyDisposal)) >= (olddist)) - frustration++ - else - frustration = 0 - - return TRUE - - return IsStandingStill() - -/mob/living/carbon/monkey/proc/pickpocket(mob/M) - if(do_after(src, MONKEY_ITEM_SNATCH_DELAY, M) && pickupTarget) - for(var/obj/item/I in M.held_items) - if(I == pickupTarget) - M.visible_message("[src] snatches [pickupTarget] from [M].", "[src] snatched [pickupTarget]!") - if(M.temporarilyRemoveItemFromInventory(pickupTarget)) - if(!QDELETED(pickupTarget) && !equip_item(pickupTarget)) - pickupTarget.forceMove(drop_location()) - else - M.visible_message("[src] tried to snatch [pickupTarget] from [M], but failed!", "[src] tried to grab [pickupTarget]!") - pickpocketing = FALSE - pickupTarget = null - pickupTimer = 0 - -/mob/living/carbon/monkey/proc/stuff_mob_in() - if(bodyDisposal && target && Adjacent(bodyDisposal)) - bodyDisposal.stuff_mob_in(target, src) - disposing_body = FALSE - back_to_idle() - -/mob/living/carbon/monkey/proc/back_to_idle() - - if(pulling) - stop_pulling() - - mode = MONKEY_IDLE - target = null - a_intent = INTENT_HELP - frustration = 0 - walk_to(src,0) - -// attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little -/mob/living/carbon/monkey/proc/monkey_attack(mob/living/L) - var/obj/item/Weapon = locate(/obj/item) in held_items - - // attack with weapon if we have one - if(Weapon) - Weapon.melee_attack_chain(src, L) - else - L.attack_paw(src) - - // no de-aggro - if(aggressive) - return - - // if we arn't enemies, we were likely recruited to attack this target, jobs done if we calm down so go back to idle - if(!enemies[L]) - if(target == L && prob(MONKEY_HATRED_REDUCTION_PROB)) - back_to_idle() - return // already de-aggroed - - if(prob(MONKEY_HATRED_REDUCTION_PROB)) - enemies[L] -- - - // if we are not angry at our target, go back to idle - if(enemies[L] <= 0) - enemies.Remove(L) - if(target == L) - back_to_idle() - -// get angry are a mob -/mob/living/carbon/monkey/proc/retaliate(mob/living/L) - mode = MONKEY_HUNT - target = L - if(L != src) - enemies[L] += MONKEY_HATRED_AMOUNT - - if(a_intent != INTENT_HARM) - battle_screech() - a_intent = INTENT_HARM - -/mob/living/carbon/monkey/attack_hand(mob/living/L) - if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) - retaliate(L) - else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) - retaliate(L) - return ..() - -/mob/living/carbon/monkey/attack_paw(mob/living/L) - if(L.a_intent == INTENT_HARM && prob(MONKEY_RETALIATE_HARM_PROB)) - retaliate(L) - else if(L.a_intent == INTENT_DISARM && prob(MONKEY_RETALIATE_DISARM_PROB)) - retaliate(L) - return ..() - -/mob/living/carbon/monkey/attackby(obj/item/W, mob/user, params) - ..() - if((W.force) && (!target) && (W.damtype != STAMINA)) - retaliate(user) - -/mob/living/carbon/monkey/bullet_act(obj/projectile/Proj) - if(istype(Proj , /obj/projectile/beam)||istype(Proj, /obj/projectile/bullet)) - if((Proj.damage_type == BURN) || (Proj.damage_type == BRUTE)) - if(!Proj.nodamage && Proj.damage < src.health && isliving(Proj.firer)) - retaliate(Proj.firer) - . = ..() - -/mob/living/carbon/monkey/hitby(atom/movable/AM, skipcatch = FALSE, hitpush = TRUE, blocked = FALSE, datum/thrownthing/throwingdatum) - if(istype(AM, /obj/item)) - var/obj/item/I = AM - if(I.throwforce < src.health && I.thrownby && ishuman(I.thrownby)) - var/mob/living/carbon/human/H = I.thrownby - retaliate(H) - ..() - -/mob/living/carbon/monkey/on_entered(datum/source, atom/movable/AM) - . = ..() - if(!IsDeadOrIncap() && ismob(AM) && target) - var/mob/living/carbon/monkey/M = AM - if(!istype(M) || !M) - return - knockOver(M) - return - -/mob/living/carbon/monkey/proc/monkeyDrop(obj/item/A) - if(A) - dropItemToGround(A, TRUE) - update_icons() - -/mob/living/carbon/monkey/grabbedby(mob/living/carbon/user) - . = ..() - if(!IsDeadOrIncap() && pulledby && (mode != MONKEY_IDLE || prob(MONKEY_PULL_AGGRO_PROB))) // nuh uh you don't pull me! - if(Adjacent(pulledby)) - a_intent = INTENT_DISARM - monkey_attack(pulledby) - retaliate(pulledby) - return TRUE - -#undef MAX_RANGE_FIND diff --git a/code/modules/mob/living/carbon/monkey/life.dm b/code/modules/mob/living/carbon/monkey/life.dm index b4469ea5b63c..01423b1aa2ee 100644 --- a/code/modules/mob/living/carbon/monkey/life.dm +++ b/code/modules/mob/living/carbon/monkey/life.dm @@ -1,33 +1,5 @@ - - /mob/living/carbon/monkey - -/mob/living/carbon/monkey/Life() - set invisibility = 0 - - if (notransform) - return - - if(..() && !IS_IN_STASIS(src)) - - if(!client) - if(stat == CONSCIOUS) - if(on_fire || buckled || HAS_TRAIT(src, TRAIT_RESTRAINED) || (pulledby && pulledby.grab_state > GRAB_PASSIVE)) - if(!resisting && prob(MONKEY_RESIST_PROB)) - resisting = TRUE - walk_to(src,0) - execute_resist() - else if(resisting) - resisting = FALSE - else if((mode == MONKEY_IDLE && !pickupTarget && !prob(MONKEY_SHENANIGAN_PROB)) || !handle_combat()) - if(prob(25) && (mobility_flags & MOBILITY_MOVE) && isturf(loc) && !pulledby) - step(src, pick(GLOB.cardinals)) - else if(prob(1)) - emote(pick("scratch","jump","roll","tail")) - else - walk_to(src,0) - /mob/living/carbon/monkey/handle_mutations_and_radiation() if(radiation) if(radiation > RAD_MOB_KNOCKDOWN && prob(RAD_MOB_KNOCKDOWN_PROB)) diff --git a/code/modules/mob/living/carbon/monkey/monkey.dm b/code/modules/mob/living/carbon/monkey/monkey.dm index 755c674a107d..6056ac83fa7d 100644 --- a/code/modules/mob/living/carbon/monkey/monkey.dm +++ b/code/modules/mob/living/carbon/monkey/monkey.dm @@ -26,6 +26,8 @@ hud_type = /datum/hud/monkey melee_damage_lower = 1 melee_damage_upper = 3 + ai_controller = /datum/ai_controller/monkey + faction = list("neutral", "monkey") /mob/living/carbon/monkey/Initialize(mapload, cubespawned=FALSE, mob/spawner) add_verb(src, /mob/living/proc/mob_sleep) @@ -169,10 +171,10 @@ return 1 /mob/living/carbon/monkey/angry - aggressive = TRUE /mob/living/carbon/monkey/angry/Initialize() . = ..() + ai_controller.blackboard[BB_MONKEY_AGRESSIVE] = TRUE if(prob(10)) var/obj/item/clothing/head/helmet/justice/escape/helmet = new(src) equip_to_slot_or_del(helmet,ITEM_SLOT_HEAD) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 99db31b26c0f..ba9b99822600 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1125,7 +1125,7 @@ if(G.trigger_guard == TRIGGER_GUARD_NONE) to_chat(src, "You are unable to fire this!") return FALSE - if(G.trigger_guard != TRIGGER_GUARD_ALLOW_ALL && !IsAdvancedToolUser()) + if(G.trigger_guard != TRIGGER_GUARD_ALLOW_ALL && (!IsAdvancedToolUser(src) && !HAS_TRAIT(src, TRAIT_GUN_NATURAL))) to_chat(src, "You try to fire [G], but can't use the trigger!") return FALSE return TRUE diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm index 3c7736c06230..1e81bd48b63f 100644 --- a/code/modules/mob/living/simple_animal/bot/bot.dm +++ b/code/modules/mob/living/simple_animal/bot/bot.dm @@ -564,7 +564,7 @@ Pass a positive integer as an argument to override a bot's default speed. var/datum/job/captain/All = new/datum/job/captain all_access.access = All.get_access() - set_path(get_path_to(src, waypoint, /turf/proc/Distance_cardinal, 0, 200, id=all_access)) + set_path(get_path_to(src, waypoint, 200, id=all_access)) calling_ai = caller //Link the AI to the bot! ai_waypoint = waypoint @@ -782,16 +782,16 @@ Pass a positive integer as an argument to override a bot's default speed. // given an optional turf to avoid /mob/living/simple_animal/bot/proc/calc_path(turf/avoid) check_bot_access() - set_path(get_path_to(src, patrol_target, /turf/proc/Distance_cardinal, 0, 120, id=access_card, exclude=avoid)) + set_path(get_path_to(src, patrol_target, 120, id=access_card, exclude=avoid)) /mob/living/simple_animal/bot/proc/calc_summon_path(turf/avoid) check_bot_access() INVOKE_ASYNC(src, PROC_REF(do_calc_summon_path), avoid) /mob/living/simple_animal/bot/proc/do_calc_summon_path(turf/avoid) - set_path(get_path_to(src, summon_target, /turf/proc/Distance_cardinal, 0, 150, id=access_card, exclude=avoid)) + set_path(get_path_to(src, summon_target, 150, id=access_card, exclude=avoid)) if(!length(path)) //Cannot reach target. Give up and announce the issue. - speak("Summon command failed, destination unreachable.",radio_channel) + speak("Summon command failed, destination unreachable.", radio_channel) bot_reset() /mob/living/simple_animal/bot/proc/summon_step() @@ -816,7 +816,9 @@ Pass a positive integer as an argument to override a bot's default speed. calc_summon_path() /mob/living/simple_animal/bot/proc/summon_step_not_moved() - calc_summon_path() + //calc_summon_path() + speak("Summon command failed, destination unreachable.",radio_channel) + bot_reset() tries = 0 /mob/living/simple_animal/bot/Bump(atom/A) //Leave no door unopened! diff --git a/code/modules/mob/living/simple_animal/bot/cleanbot.dm b/code/modules/mob/living/simple_animal/bot/cleanbot.dm index e53b675c95bc..aad4a7a63f86 100644 --- a/code/modules/mob/living/simple_animal/bot/cleanbot.dm +++ b/code/modules/mob/living/simple_animal/bot/cleanbot.dm @@ -261,11 +261,11 @@ mode = BOT_IDLE return - if(target && path.len == 0 && (get_dist(src,target) > 1)) - path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 30, id=access_card) + if(target && (!path || path.len == 0) && (get_dist(src,target) > 1)) + path = get_path_to(src, target, 30, id=access_card) mode = BOT_MOVING if(!path.len) //try to get closer if you can't reach the target directly - path = get_path_to(src, target.loc, /turf/proc/Distance_cardinal, 30, 1, id=access_card) + path = get_path_to(src, target, 30, id=access_card) if(!path.len) //Do not chase a target we cannot reach. add_to_ignore(target) target = null diff --git a/code/modules/mob/living/simple_animal/bot/firebot.dm b/code/modules/mob/living/simple_animal/bot/firebot.dm index 0fabc6c7fb53..1b04fbb51669 100644 --- a/code/modules/mob/living/simple_animal/bot/firebot.dm +++ b/code/modules/mob/living/simple_animal/bot/firebot.dm @@ -269,7 +269,7 @@ if(get_dist(src, target_fire) > 2) - path = get_path_to(src, get_turf(target_fire), /turf/proc/Distance_cardinal, 0, 30, 1, id=access_card) + path = get_path_to(src, target_fire, 30, 1, id=access_card) mode = BOT_MOVING if(!length(path)) soft_reset() diff --git a/code/modules/mob/living/simple_animal/bot/floorbot.dm b/code/modules/mob/living/simple_animal/bot/floorbot.dm index 980f12897e70..662386649186 100644 --- a/code/modules/mob/living/simple_animal/bot/floorbot.dm +++ b/code/modules/mob/living/simple_animal/bot/floorbot.dm @@ -255,9 +255,9 @@ if(path.len == 0) if(!isturf(target)) var/turf/TL = get_turf(target) - path = get_path_to(src, TL, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, TL, 30, id=access_card,simulated_only = FALSE) else - path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 30, id=access_card,simulated_only = FALSE) + path = get_path_to(src, target, 30, id=access_card,simulated_only = FALSE) if(!bot_move(target)) add_to_ignore(target) diff --git a/code/modules/mob/living/simple_animal/bot/medbot.dm b/code/modules/mob/living/simple_animal/bot/medbot.dm index 22d68c8a6190..6bcd39abb6a6 100644 --- a/code/modules/mob/living/simple_animal/bot/medbot.dm +++ b/code/modules/mob/living/simple_animal/bot/medbot.dm @@ -413,10 +413,10 @@ return if(patient && path.len == 0 && (get_dist(src,patient) > 1)) - path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,id=access_card) + path = get_path_to(src, patient, 30,id=access_card) mode = BOT_MOVING if(!path.len) //try to get closer if you can't reach the patient directly - path = get_path_to(src, get_turf(patient), /turf/proc/Distance_cardinal, 0, 30,1,id=access_card) + path = get_path_to(src, patient, 30,1,id=access_card) if(!path.len) //Do not chase a patient we cannot reach. soft_reset() diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm index 1c10311f7b3c..59a6125b5fbd 100644 --- a/code/modules/mob/living/simple_animal/bot/mulebot.dm +++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm @@ -615,7 +615,7 @@ // calculates a path to the current destination // given an optional turf to avoid /mob/living/simple_animal/bot/mulebot/calc_path(turf/avoid = null) - path = get_path_to(src, target, /turf/proc/Distance_cardinal, 0, 250, id=access_card, exclude=avoid) + path = get_path_to(src, target, 250, id=access_card, exclude=avoid) // sets the current destination // signals all beacons matching the delivery code diff --git a/code/modules/mob/living/simple_animal/friendly/dog.dm b/code/modules/mob/living/simple_animal/friendly/dog.dm index 2a47d4f0c220..6dc3bbf55048 100644 --- a/code/modules/mob/living/simple_animal/friendly/dog.dm +++ b/code/modules/mob/living/simple_animal/friendly/dog.dm @@ -16,65 +16,11 @@ see_in_dark = 5 speak_chance = 1 turns_per_move = 10 - var/turns_since_scan = 0 - var/obj/movement_target + ai_controller = /datum/ai_controller/dog + stop_automated_movement = TRUE footstep_type = FOOTSTEP_MOB_CLAW -/mob/living/simple_animal/pet/dog/Life() - ..() - - //Feeding, chasing food, FOOOOODDDD - if(!stat && !resting && !buckled) - turns_since_scan++ - if(turns_since_scan > 5) - turns_since_scan = 0 - if((movement_target) && !(isturf(movement_target.loc) || ishuman(movement_target.loc))) - movement_target = null - stop_automated_movement = 0 - if(!movement_target || !(movement_target.loc in oview(src, 3))) - movement_target = null - stop_automated_movement = 0 - for(var/obj/item/reagent_containers/food/snacks/S in oview(src,3)) - if(isturf(S.loc) || ishuman(S.loc)) - movement_target = S - break - if(movement_target) - stop_automated_movement = 1 - step_to(src,movement_target,1) - sleep(3) - step_to(src,movement_target,1) - sleep(3) - step_to(src,movement_target,1) - - if(movement_target) //Not redundant due to sleeps, Item can be gone in 6 decisecomds - var/turf/T = get_turf(movement_target) - if(!T) - return - if (T.x < src.x) - setDir(WEST) - else if (T.x > src.x) - setDir(EAST) - else if (T.y < src.y) - setDir(SOUTH) - else if (T.y > src.y) - setDir(NORTH) - else - setDir(SOUTH) - - if(!Adjacent(movement_target)) //can't reach food through windows. - return - - if(isturf(movement_target.loc)) - movement_target.attack_animal(src) - else if(ishuman(movement_target.loc)) - if(prob(20)) - manual_emote("stares at [movement_target.loc]'s [movement_target] with a sad puppy-face") - - if(prob(1)) - manual_emote(pick("dances around.","chases its tail!")) - INVOKE_ASYNC(GLOBAL_PROC, GLOBAL_PROC_REF(dance_rotate), src) - //Corgis and pugs are now under one dog subtype /mob/living/simple_animal/pet/dog/corgi @@ -165,6 +111,7 @@ dat += "Head:[inventory_head]" : "add_inv=head'>Empty"]" dat += "Back:[inventory_back]" : "add_inv=back'>Empty"]" dat += "Collar:[pcollar]" : "add_inv=collar'>Empty"]" + dat += "ID Card:[access_card]" : "add_inv=card'>Empty"]" dat += {" Close "} @@ -248,6 +195,10 @@ pcollar = null update_corgi_fluff() regenerate_icons() + if("card") + if(access_card) + usr.put_in_hands(access_card) + access_card = null show_inv(usr) @@ -300,9 +251,22 @@ return item_to_add.forceMove(src) - src.inventory_back = item_to_add + inventory_back = item_to_add update_corgi_fluff() regenerate_icons() + if("card") + if(access_card) + to_chat(usr, "[src] already has \an [access_card] pinned to [p_them()]!") + return + var/obj/item/item_to_add = usr.get_active_held_item() + if(!usr.temporarilyRemoveItemFromInventory(item_to_add)) + to_chat(usr, "\The [item_to_add] is stuck to your hand, you cannot pin it to [src]!") + return + if(!istype(item_to_add, /obj/item/card/id)) + to_chat(usr, "You can't pin [item_to_add] to [src]!") + return + item_to_add.forceMove(src) + access_card = item_to_add show_inv(usr) else diff --git a/code/modules/mob/living/simple_animal/parrot.dm b/code/modules/mob/living/simple_animal/parrot.dm index 92b955d3a841..5a900e64e199 100644 --- a/code/modules/mob/living/simple_animal/parrot.dm +++ b/code/modules/mob/living/simple_animal/parrot.dm @@ -648,7 +648,7 @@ item = I break if(item) - if(!AStar(src, get_turf(item), /turf/proc/Distance_cardinal)) + if(!get_path_to(src, item)) item = null continue return item diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index a1a0886a2362..738428592d9a 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -208,7 +208,8 @@ . = ..() if(stat == DEAD) . += "Upon closer examination, [p_they()] appear[p_s()] to be dead." - + if(access_card) + . += "There appears to be [icon2html(access_card, user)] \a [access_card] pinned to [p_them()]." /mob/living/simple_animal/update_stat() if(status_flags & GODMODE) diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm index ef21915e1fca..1be945b3d8f4 100644 --- a/code/modules/mob/transform_procs.dm +++ b/code/modules/mob/transform_procs.dm @@ -1,6 +1,6 @@ #define TRANSFORMATION_DURATION 22 -/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG)) +/mob/living/carbon/proc/monkeyize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG| TR_KEEPAI)) if (notransform || transformation_timer) return @@ -150,6 +150,8 @@ changeling.purchasedpowers += hf changeling.regain_powers() + if(tr_flags & TR_KEEPAI) + ai_controller.PossessPawn(O) if (tr_flags & TR_DEFAULTMSG) to_chat(O, "You are now a monkey.") @@ -167,7 +169,7 @@ ////////////////////////// Humanize ////////////////////////////// //Could probably be merged with monkeyize but other transformations got their own procs, too -/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG)) +/mob/living/carbon/proc/humanize(tr_flags = (TR_KEEPITEMS | TR_KEEPVIRUS | TR_KEEPSTUNS | TR_KEEPREAGENTS | TR_DEFAULTMSG | TR_KEEPAI)) if (notransform || transformation_timer) return @@ -329,6 +331,9 @@ else O.set_species(/datum/species/human) + if(tr_flags & TR_KEEPAI) + ai_controller.PossessPawn(O) + O.a_intent = INTENT_HELP if (tr_flags & TR_DEFAULTMSG) to_chat(O, "You are now a human.") diff --git a/code/modules/movespeed/_movespeed_modifier.dm b/code/modules/movespeed/_movespeed_modifier.dm index 06cbaf0b99cd..4befe2458faa 100644 --- a/code/modules/movespeed/_movespeed_modifier.dm +++ b/code/modules/movespeed/_movespeed_modifier.dm @@ -200,6 +200,7 @@ GLOBAL_LIST_EMPTY(movespeed_modification_cache) continue . += amt cached_multiplicative_slowdown = . + SEND_SIGNAL(src, COMSIG_MOB_MOVESPEED_UPDATED) /// Get the move speed modifiers list of the mob /mob/proc/get_movespeed_modifiers() diff --git a/code/modules/reagents/reagent_containers/syringes.dm b/code/modules/reagents/reagent_containers/syringes.dm index 5d11dcb720ee..3241695c7e78 100644 --- a/code/modules/reagents/reagent_containers/syringes.dm +++ b/code/modules/reagents/reagent_containers/syringes.dm @@ -67,11 +67,7 @@ if(!L.can_inject(user, 1)) return - // chance of monkey retaliation - if(ismonkey(target) && prob(MONKEY_SYRINGE_RETALIATION_PROB)) - var/mob/living/carbon/monkey/M - M = target - M.retaliate(user) + SEND_SIGNAL(target, COMSIG_LIVING_TRY_SYRINGE, user) switch(mode) if(SYRINGE_DRAW) diff --git a/shiptest.dme b/shiptest.dme index ef4500858dca..d7a9f1aa48d9 100644 --- a/shiptest.dme +++ b/shiptest.dme @@ -160,6 +160,7 @@ #include "code\__DEFINES\vv.dm" #include "code\__DEFINES\wall_dents.dm" #include "code\__DEFINES\wires.dm" +#include "code\__DEFINES\ai\ai.dm" #include "code\__DEFINES\dcs\flags.dm" #include "code\__DEFINES\dcs\helpers.dm" #include "code\__DEFINES\dcs\signals\signals.dm" @@ -185,7 +186,6 @@ #include "code\__HELPERS\_planes.dm" #include "code\__HELPERS\_string_lists.dm" #include "code\__HELPERS\areas.dm" -#include "code\__HELPERS\AStar.dm" #include "code\__HELPERS\atoms.dm" #include "code\__HELPERS\bindings.dm" #include "code\__HELPERS\bitflag_lists.dm" @@ -212,6 +212,7 @@ #include "code\__HELPERS\mouse_control.dm" #include "code\__HELPERS\nameof.dm" #include "code\__HELPERS\names.dm" +#include "code\__HELPERS\path.dm" #include "code\__HELPERS\priority_announce.dm" #include "code\__HELPERS\pronouns.dm" #include "code\__HELPERS\qdel.dm" @@ -325,6 +326,7 @@ #include "code\controllers\configuration\entries\resources.dm" #include "code\controllers\subsystem\achievements.dm" #include "code\controllers\subsystem\acid.dm" +#include "code\controllers\subsystem\ai_controllers.dm" #include "code\controllers\subsystem\air.dm" #include "code\controllers\subsystem\ambience.dm" #include "code\controllers\subsystem\assets.dm" @@ -401,6 +403,8 @@ #include "code\controllers\subsystem\vis_overlays.dm" #include "code\controllers\subsystem\vote.dm" #include "code\controllers\subsystem\weather.dm" +#include "code\controllers\subsystem\processing\ai_behaviors.dm" +#include "code\controllers\subsystem\processing\ai_movement.dm" #include "code\controllers\subsystem\processing\fastprocess.dm" #include "code\controllers\subsystem\processing\fluids.dm" #include "code\controllers\subsystem\processing\instruments.dm" @@ -468,6 +472,19 @@ #include "code\datums\achievements\skill_achievements.dm" #include "code\datums\actions\beam_rifle.dm" #include "code\datums\actions\ninja.dm" +#include "code\datums\ai\_ai_behavoir.dm" +#include "code\datums\ai\_ai_controller.dm" +#include "code\datums\ai\_ai_planning_subtree.dm" +#include "code\datums\ai\generic_actions.dm" +#include "code\datums\ai\dog\dog_behaviors.dm" +#include "code\datums\ai\dog\dog_controller.dm" +#include "code\datums\ai\dog\dog_subtrees.dm" +#include "code\datums\ai\monkey\monkey_behaviors.dm" +#include "code\datums\ai\monkey\monkey_controller.dm" +#include "code\datums\ai\monkey\monkey_subtrees.dm" +#include "code\datums\ai\movement\_ai_movement.dm" +#include "code\datums\ai\movement\ai_movement_dumb.dm" +#include "code\datums\ai\movement\ai_movement_jps.dm" #include "code\datums\atmosphere\_atmosphere.dm" #include "code\datums\atmosphere\planetary.dm" #include "code\datums\brain_damage\brain_trauma.dm" @@ -554,6 +571,7 @@ #include "code\datums\components\sizzle.dm" #include "code\datums\components\slippery.dm" #include "code\datums\components\spill.dm" +#include "code\datums\components\spinny.dm" #include "code\datums\components\spooky.dm" #include "code\datums\components\squeak.dm" #include "code\datums\components\stationstuck.dm" @@ -2634,7 +2652,6 @@ #include "code\modules\mob\living\carbon\human\species_types\vampire.dm" #include "code\modules\mob\living\carbon\human\species_types\vox.dm" #include "code\modules\mob\living\carbon\human\species_types\zombies.dm" -#include "code\modules\mob\living\carbon\monkey\combat.dm" #include "code\modules\mob\living\carbon\monkey\death.dm" #include "code\modules\mob\living\carbon\monkey\inventory.dm" #include "code\modules\mob\living\carbon\monkey\life.dm" diff --git a/sound/creatures/monkey/monkey_screech_1.ogg b/sound/creatures/monkey/monkey_screech_1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..a4d5bc45429aa0416a1a189c0aaefca1ab8af5bd GIT binary patch literal 15300 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 literal 0 HcmV?d00001 diff --git a/sound/creatures/monkey/monkey_screech_2.ogg b/sound/creatures/monkey/monkey_screech_2.ogg new file mode 100644 index 0000000000000000000000000000000000000000..ea44bcbcd814b7dd2a59bfc0f3fc8193135335a6 GIT binary patch literal 15649 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 literal 0 HcmV?d00001 diff --git a/sound/creatures/monkey/monkey_screech_4.ogg b/sound/creatures/monkey/monkey_screech_4.ogg new file mode 100644 index 0000000000000000000000000000000000000000..5a60b9466fa047efb2a6ff1083c5063d2e5f1dad GIT binary patch literal 16704 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⋙P=*j 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 G@xK7+rh>`< literal 0 HcmV?d00001 diff --git a/sound/creatures/monkey/monkey_screech_5.ogg b/sound/creatures/monkey/monkey_screech_5.ogg new file mode 100644 index 0000000000000000000000000000000000000000..04b4be87f842bff31efa962c06d4fa1211ca18b9 GIT binary patch literal 20239 zcmeFYbyytD(ALrgbPs7gibXV1MS5;S6SNEve*k}S^;6EwXB?aei_NTLf2#5mY|lE5W>G?6yddcP#0TvRW}(Ek8=ak6u>b5b$E;dt1% zxY@Y4n33S*@ZdiQ4J`#tEio-yD=HIXMvMy%E@Z;R!!N|k#m~jZ z|}4{>kh}Yg*v*x?-DNXawZ+9lZTm&6_tjSg|nL_)X9b! z>u*yuB%~Bow3Q`gG{pFb;RSLsDjG8X53dIy{mUq3Nhuu=fC>P}AmE=Y1fK1OBmR>h zw!{o6~}e~A6X`%n6}wTJ*LbTGt}nVj{X_rd^RO$~?O-#Q2YlK=yB|G>i#umC(d z0EPtT`WgTb$PBBV_{8zky8o`h&xk3XuwnCCT zD_*3fJ}Y4QcMQSPKWu_?)I~4LN+E4iK5j!Wtym$iL_duYN)(O!zffT!7%fy2B>zi* z4*0A5|D=CJ2^ae7Gz+058lj|OrnpVPf4T@k1|R?sz9|^Br6!nUp+Gd+2XD{+)GjzH z{-yEv`Y#F?fQ-13kGQf9YOu;_@*@2MKKzd+Nz@ZyznA)zphri00uXF01EtvQ8<-A2!iAQu+K@R`gN?f zM38CT3>qceBGfuwtRfWEu)Y6QdeclAQLHj#RV2M4w8>Bg$*ccvR)Q#g7OH9}BbZe^ ztl~4incsg`=@p@T3*D2r1k#xL@P9?(i2Zlf|D1yWKrajdfWXkK3jRl%T*(2TpcmeP z$bYAv1o(XO|1rU>EU^pRN^urq5|B3F0TE!pg(HvT}RL?r8DHCU*pJg2dN8lajRYKKZ&A_U7p*5qWoQlCZexamE zX!PAvdx{sHP2bZnf)FUrXld7A{L=tKVBB41#?wgG(}>Ei$To@vs3@yyXJLm`R58qX z8UZs}M&AdKIF-M9v;k{XTI}!{8yn7Xa7;mR3`=y~w=j%nkV5L0U>Fksgcif+9cWgu zzNk+ULzXlH2F*7DHc^J6aK?~uM!ZPUg zvntQTjYRP=_=;i^P^0b~4~rYr$~YM3f4K?0T90!I>+It&LcLjd_NyOfv^ z{t;>)2*CV*(@t=&3p+XRmBt~*jDD}$h4<4`LMi}}hmSp1R)9(@{bI3U?K2pDaGR=)YR!h5(4Bl|Zw?!;xddtrfD$ptb-N zTuGq?AXA03(r>(^Kpn3#0)PcCvdxm6nF3g`cw>=CAv3(T1yF#O7mG9td1`8k7fTrd zo(Hg|c&7kh3V_$y@&csr8w?pfAVKgVShy?#{06TENT=Y#MEIXERWvPm$h+~C)inzp=hA)-Pj>QsS)VeZQ_(2K6f%Ok~AXZBl~kL_DLN zv}z--=H{7n)(D+B9(>qCZ~#*(No#O0n28haC!nCBX#fzj;6jT}sHnsAmITBEacDaw zGsplHY9a{@Zapy_gBF6!pa)R*sW;&nKtf6eeg1sP3Wm}C=>UQ%fhvg;jle_&LSg)Q zIzX1^5{mB;_4)<8^>Et*XB`00kwK)TdELohP*74))6mkhc zC95DSr>k#l^px|I{WSLU_%#2t_q6cT_*DK>|J3wU2fu!NN_vWW3VP~!Dtt;^BW;PZ3X<|I}r`^Honln}2dZS|O*^<>@itthExw&t#DJsgzSkD%T26 zMAvj{lfdLZ>u#<_ZZY(ZTc-|#ovJp%%UgX}qHg{I4lcP~Lm$4Aya4^V)!HSRR>J-~ zEhdW8M?!(D*OmR2V;gUglm0eCcbpGJdt;9xeXFrmM6_$h_0lWHgzh`xzErB#pxK}I zJkU?`8Uuftv@NoLpSO0VglbX;lW~){!ooOHwNG2N$}RI_!8A+x#5F@-zv5oYw)l6U z><@bxc&C)FMk$Hjf4SSS5hd?AI&-$2KLsm&sRLhLwtvIb{k@+T==aW{0wX1=D~_ki zk@#|AkcHrU_Qw&*_NC)S<;NL=7qVD8w~mE=Iwu|4gA$=yWUfgz>b(nxWfb-{!%c@k z`PGdc-=;Vs`A1pZh6SPDlzMxL1oA7p;bK0f&}~1xub&UJ%9cz{*gHDJ$X1=xhq5o; zJ+$(_nmnsRIS>#|K$zFO9CI#t8zrq=EM}=cyTH24hQ-0#uv5l&LaS|PhLXFvj#>STOj05*S$UlrpO&+gL ztOLPKIB7FoX_P@}6+zz5dTo$%9*A^?t^E}F+5@r=FOn;V$K8c3sA9R?7fp}RJ}lFk zEi(@q?DcM+94z=I(cK9(-v9o+?nv6>n*hL21+c)w`)R)U$@CO+~a z*B^tfYMoV9gb~r)-!vSWMqi$=)1iK}*+2d`?CGZoBpdaB0EQvg9P*)KEf&)JqzEPx zP2^1Y*ig_(|FydukQ5NDF(#a?(^y=ihPrmIyYzjsHa`$eV6|%puk_MWR!OVYjjTD#P+G_?*$L1{acEErjKe*e4+oH6|uOi)DMu}((uTK%zU5_bEY&oI4il!4Cvca zu#XgFc5eLWjZgS}mI1#RD%M)nGB4cG5yUx#aGkg?yTm2Bj6eFu9dXere}b+Ku;+G< z-FRDXdmmkzZbPUtwqbRxVsUNsJJ9G}KL}G%;Z3sG6y=nZ!O-@$G@GiN=cmOqPvm{@ zX@=WA->v~n4EEN1$Hs(jyu$ZTL*NBL4dYi-EhHc|zVn|*p(4VXaz>K4)b&kvb5*64kOWsID>at(r3u!v zKB;o8tHU4Hcrr1c2M1-+A~$}4rnvbuy+o4v@r-r0qD%aXS|7L_5!Pb-Z3h4KzyfUM}DU+nf>YR zsNjtSzo6N6&NT6XX9t%BdFyn39Rm60dKcMx;v{^2F+%}*`?nrbha^6tbR2E;U4LEK zUx0cb57;L-Vi^yew5D{!c~;11Tq`tEyeQ2$oGs^#3_$^0nEe~OZ>lup-g}%o zlN{|#xt^c&%-Tk_bGah|)&Ny;1i<~2ZS<(LYR2v~pPSuKSbO}APM1r@*sHTUs5NN7 zKtF2Ctlnr-#tzDz`_nac`BR0)x(dzX#)G(X1^D#Rw@v1x*`np9{YgZ>r)V=lNfdy= zkfee+tHfU&sQDH@7cRLp1g0E&4SOw1<|LU zC|@DH`s2%&p1$RnO#=Kd%sJE(q|n zKHRO)jc2BR-pe!D#^GeBdK09XUsnT4c$ZkZdVmkCApvXRAX9`XWdDBpp&^qU6V)ai zgt}k?eQv`p@mzlK?gz*3mJ9$IWTN!ymfP>?0t5bjQb)C}z@PbEH|6)-v z*X$HSw)^{u*Z^`6UPkf%WZT3eB1n+}fz{zTbQo|6<3hq@JmMysH5Jy{Yp8X2? z-E&Xgk?*cE;`Ze+uqi5E>QgkYR7~WX?W8`thkp%#V~FHKt9VuWC07v}+Uw9$XCx&vb_+h5@s0O+=P_wqF7S~XDF#P8iIi$kuK%?@S|&xZ^wHr1)ji=EGIUgTi(tjptrTSxG53PNuFm}=xW{bcCV0>S0IpkVdNpz&WqC%BF zyMTiyLTo!TYGxxQ8e*A|S@yXvS27D%UYTMJ1Hh?AtU-64$#nWztJkS=lh6KQ{Ja25 z;KJJUbf=+oCnq)3iU3dpi)F`9wg`Hjwe1H;TIqSql1v-CdszvhKd0Im5GlCbsD7+LO+#I-1 ze__eAN(9{J7D?@}S4jQgH~)O!EOc&s`r^iL(LAOQ?d-v9`tio~e)WY+r|Ly_ApM5o!~MmEcMr;P&#hX5HWuiY zIo`U!XWnWMnxYKWGW5I--91Qf$=L#DE)4{X2&t(VjQNWOP9CI|2!a-*9KT3_YZZh4 z)LSnZk;Qwyb*uE$!>et+#+Ydw#%p|DHVR$Vo6cb%ea#ETTeR|Uvo-!s0SCVP;gG?>@m9>v^`8;=DCkB5;{4`1Uwse* zmqr188C{DpJY?Ee;{7`MDVB$3j{qsKtZ}?(V{{Da^)ufL34nJAQx#5<84w!W`FqzC>ANgKa@Vx`ib&Or201T?TioQO| z(w8(z>vv8+nO9@dq4yzZVRNZ@d-=XA%wu*K_eVSi%>u5p(6UlMvkT!=>UPuA9`9?} zsC`Q%ME8XBoGt6GEM_lf*s>aXm*mON<|2fK?U9QJ|L!`lxeKMzN9Vk|5TwO3K%yET z&oF+C3=~+|#a#&6@BtoOPbg&G=?V6l1 zehuGE0RY-CEi`g2h1b`6?agn502g55m4&XN0A?n{YGK@1fV@5yB^8)`<2< zv-qGL3wQrVfaUwoxFTYCM=RGJj=xpoC+&lqSo(w>a(bkDlEDCsgld~NY*NSgh@w|T zH-6!0lH+Ek0Vl`MA8qkNWsEg^L810N$%`Dw+a7zV9C)}`%@SetMOC1I$6DVz>t=4L zN@L{U+1J@*z5)F`<4pbc235klWdlu{BVsok0?@xqO~^lx%l)2~oI129Nt%95^ecQ@ z!I@~XeDG-K9OcyeFWW_}_s^A>rhR7nl$IMVC6umLEb!w>*yv<_(#sAwp@g&|a=qq! z8L2QfY)|DgqM86PRRJ-v<^DqAfvJ@>lUMOdVZSfu?BCnC)O2NLDGR0X5P+aSB<>e; zv(xr0m%y!>%xJ~jGm_y$l+XGqtCOMMOGt4Ou;5`MY%`VeE9oO^$r=2bzOBJ}r(CD~ zJ@q=)Jq=ETA^in)&cJdP3YjW~X$teR^{uu+&}Y?xUNrAZX2SSlVWv zDM!W|bN-UW;YUe{s zWK*~$OLr+~>b-zPj|Fpd>)Y|gmC~EmkXMzvW)HW56pXda09hEOn5nv{9Z>9#pbrL~ zbYFM}^FX@p8PZVLxZxvksuq3$;1^WP+3RT}h@m7x&d$rW?i*lb`w)MT$m1hf8(HGB z4@biG{9$%PlA{7=nJz~;jX|II!z6cUL!=pO_`mB}9E>#a8i-=v>G8v<%^g3Ic=B+_ zCnvIy+~*QWI81M+N>WCCwincBA9EG80dq7vVs!{v_#kXS0WCL>_Dx4dbcWO;<{I*a~u07UaB&Qdqk0fG7VGg7Q=4Cz6G% zQBTD^&LSS`f$0=rTZc1dYN6cyQm`swJPy&6c?l?)wOGi7X0G zXf!T|(otyFZ%)^BCY1=Sh))U8QW9v$(^?WUix}Lw?C*wzn)>Alar+UIby{o0_-yN= zlM2G%(ZNb=S4=Tbt92+xG=eBbG=timj?(8|8nq|6SKjx2lOm9ieefDm37&lRv?Wkr zArXpS2U;UfB(bU}9gSe9OuoT1bJ<;wnD@Os3xS)f3i$R0dZinsiE2=G85=I9@Xw6< zT6Gd~IV(CG<3N+;y!rqO%qe`{)LBI_#%!V5rR@`M(cCi+IerMg=~vHhRhLtzyVp%1 za}bPS_Pw2!bVBFCSwsDjB?{;;?<_|*_qwCXU-v+$M83h zdVoZM5{o|nZc-@K;6^^9t$SRbVICQ`kwpESoQUMjXD_zIgG(wI6L#`C`$H~dd;i+o zG#AXLbu=eSRa1B*T8YDAKUS~e?LOM_m$0I#OGm;y@p7a-=?9Q1Q%aMCK6eXyTdj6qRN~hxWtF)HLfs)b zW0!q$T-8rh&ul9t8%;m>dV6!N|FYm9;_FBxU7i!EwVB(-@qXH1HPc<(V6BFY?lv#l zC2W?pSzFlsHf=6Y4ks0Ed9m)5Em2UXI<)wl*=1B5T#UDl&1g{HmmZSauuL@}8H@KTvSOW#Oi?@iBq`=w=F#r*B<0;zH0Z59) z4)cLi0vIbA@G5R$-U0x|1OSQ~O!Vf#FTbT51Y0H-zUTN>@UI`*)6sSqJ)Y;Od49v! zvru-VNP>JBc{5^}5Y826bqzNxs{RwHRU#kRH==RWSyXX6O+;Z<2#bJxGr4>{ckc^* z6Ad;x?MRLEaK`Tgv_yVFFX(n2FEs{eKMfy@#mJu)8grXRQkzy;5CVO`t8uD4x8fKG zCs_M4Q=>K(?!ojFdT=hPDL4sS0S1^v2s&)};#$0+C*>$xL&SmkxJ-rM$ciz=Tc1!> zVkB6#xxEWXPP&V4GV*3~a|Q2m0Mk*@K#@?>B|qM^JY#&lMAP6DV{Vji>o;OZ=sWZD zePxk(lb@YUCeGNnLAApoDO!nZ;t@@Ikw^fHxhT)lfakduo6}UV)a~KcA0jd;pg>>m zLUG2e6|k8O{E6EESn}5e2;hVAY>Fm~-Lm2wtfd_u9|QboRuFq_sWRdc5uaogUl3!Cm(|NQV7q zE$SqM+k`dK5GCSaSwsnzcY`>gYiVzgN?;qsN*c@POjH_)$dzlH&7-^I()!uM4FnoU z1WZ(K!RHbEncv97*vLD`(Qv2$Dz{I+U&EI)udDBf?4LK+Mn=hdsTR`#A(n1}Nogkt zrWh45;9=u|AYD467bvE%wRc2sC5%(6@CDWqx0RAVyBA5RikqQ+`rYOequ*?V4y?dK z4L|kt&8`BGnYbIE#g2_FPev?(*sI;03=&Jx=?m9BQqDSfb?+Q@o?i~PKg}n}sWJY+ zYRD#Miny&3q8L3$_xWwU4FdtTw{h0ltmF@o!2U}?e*25?;ffua?Bj2(3iQ@n68+pc z_GdQzR~u2_OZI+$vr01%dUN< zustU(&5~3osdoTJG@iM*UiI*ld;$15gap-ZF<~N?6;w#)jhKcs#WJxySl_Cu*@S_K zj!p$s_2qO;MtngX;XPDE_LCnWtdcFNU^CV!Xpj=h%VvrPP=^ngh`%(iAtT<7Z;^1` z+4*?AmS%tVfh)i}%ROxR!9vDkj@yq#1;+Ii5mH$mDYY>a+39Be2xJcym7(Fn=0*&o3w6}Te?sTeF#qoXgP5c_6+DpJz zn6W4xoaBRl-fEd3M)7Ll{7o0>GmdwqCqF!})CLhmlkNN6Wp1=CBv90r4hdGkB#w{N zyaqccFP7X!Q~`$!;Gx=h;uvyJDQw(>>AsV=C=&vO3fK z4Bt&^!AYJ^KDW22UH2#_HVrmmQJaK{$;)dwsTa+7adj9G@I2=X>9%Q+{=wSkq*bY_c zqZxkJ>@~>xVjofRzqj0F(3aRCoZJY}RPZ{S$wur2V@9u%(b{?{ffQ$b4`-k+qzhh` zQR{bAyET3W_Yd@b;{SRt?HJhkB3lX{(dRjO#O3L7Ghwg*0vb;!-}*>%R&e=DmGB)1 zzJ)!|&m}Lrya9i-U{|)b-1;XmsZF z*+d4d?ycN0O33Hnj^`(XdwYdzeSL<-8XrfmON|2585s`Y(L%ZM%|$fYH^x!|`UEc% zY!}Cbn|T?`uT7zh1B)}Mqo8pDCgUHoh&-=#IDAxSp3}}F4EUuZE7V11bxq^t+J+a` zogxwqE4dkPYLN)HvGoyBYrNjV5?X^vDH>KSk{qk~%)f9F(7|SH*d5a-K3mcL#kYT= zJ70p^l%#f7^?J3&0-?2lslB?iwfY{_?wJ;Zo0F(>+BlwPQOr$bX}AtK0~wlyiP}JV zg80)?y)HP|X^cEQ&0Cl946i2Y^$p?Ae1m2Ro^4AVP=Cs3AJ;5pL+GEXo()I7KZm81 z)Y(1r4)ZzqfDn-S<3_={rC)JQF$ovGdeZH5C9l;6(QP8WNHvj==+}~SpQZky zXdDDuJ?b^LtuZYEzBQdjY}#Jq*cT%Ep!C8%A5tSBa3fd>c$go2qtV^yu%-Yok&oXa z)%D9pr0B(n(DecUr!T;RuwRB0*h0>Ru~ggLmoM1dy^K_nN}GQ71>L2QWOCnkAYsQnEgv zLhgOV2M@UI)Y9bxs@8M}PYF-X;~y9Kea*9Z!`*pix+zaZM-lSo4gFB}nCd9gKc}i) zA7?p+WRxJ?xia^$W8Gc_2i_~!za~=mRi`Cu9Ld^-cW_!&`s{q5gmiG02W4~{a|5Q z_uWjY4nexvd<4#F2v7f?k{-QQFg9c?;+e=g?YUMIl^X0bqq==LsDxfLV?jB;Pg4VC_J_g z8Sqd+f?b`TSR^c_!zaVM`9o!*fTuX-CsVxKKa&p$2CJay*`G2tb8F4t7i zh#Y0@VlB~IDwKgBp9Ev^_am2M-nR4iD^6fIfYs}k>dI?cM2W{M>kOCjNt7r)&?v-Y z>ZA{PXZ~!<{(aS-EGdCt!ig`~{W~yjkGFjCQ9>YK^d^`SlX`6n2@?O7)ie~&Z%`eu zd-tUEr|16l*_Him@H}D=@tP1;0>EQm_4aOUuTrpP4apaj9>d6LMGvdzt-cMvg1cn^ z@HOwl&W=fy)WF{dik(9?jr|&~i7v%adHv(2?cqB8XD3HLb@l6L=9xd_>BYKp|EZ2s zZE1~Yr!zc*n>E0gB*ER5m3^vf7rs7Q*;6@gB@;h}21B$cXP6sbVFqik@H zoy+n(wqB6soq-Q`=WPMW7&#viBjTCcCo3HZFlg<#M%I7(gwG?LBdldW>=F z+sgB5d#hvQI_X@OHDp7G7^bPC^w?v=!Cnc40uo*@;1IWffzg6V*3gfE!;FC4;8 zHZ~A`PoARm1I4y#*;W=vaNpbj>2%Ta*nGY9m6`X0IVH8|8pEfc&sI0?)Y#-8a)cE? zd|p(Y(`OR1O{KCbMI*qWO_Md^bIGS`&f%s9T20vuE1HaWggGOCuZWOji5rr*E3#ASi_VBJsUzGebAj)G zDa}q9XMlxvt8+v(%~|>r74MW4RdW`Fx}j+xtd77g;u(R%@R1}T3;f7j#cBB45_|=F z=#!0txc$mKg}C<`p087AEmo&M_7q{~@B(ZJqGfXFjk?34Y04yp@q3z@V;c;-p75dG0@}ch?-H%Dx5io4|-kJQy=$}d-pjNJ)c`9CD zHNOthU7%?m54kY_ga1*V%^ibS!!ddWkD}R%hqpSl@_kFC!aI-vCdd>FgsoZM0ia37 zyf!n$SEi3KV&23BPuTGDfxR#5KEu`g@ZHPGA+6(K0foG%(Tah^D#wpCOP~n2%hVEt+q(HZ^vwzJ`K6Gs0L*{m=Gh4-)`iez(h0G>@ zZK2u>mc4Q;v~Rx?A6IIbkQVoZWk|WNQ&kB)B7{!P9nBms?2}3dF7N&pjT}1lG^_-Z zB%{JnKoiGXz_=gBDx3o`uF5T2zhYVbCw(i@OhJuDO6=E7j|jVP=`{}%JJP*w+oP|H zD|NXoc8h45G&5cs?n0E^dn-*x4hQ1OZq=>bCH-%>Sos%z?^m#WAQQmfp z9!7O(zTqE?7VHTn_C);U<|ubX2&iR0;#3!PbMKmdZ8uO+XVmteLt>0K93>T{ABZ4*?LE4Tg^{7uZ@!rMPg?J9j0;h%O-9Q9oFOVclOl;a4 zSV@h?W?^mpW|VS@i1hmTFkUps7M0`1A@G#A^O=Nk5eN|fB8pm(V7VQ(C{@UqidoW^ zfP#a3yHc&;zl#>K)&4xDLe16B(xd7!!Y@TN&b<~x_j--m>UuC%1Oz{La~y>$7#g8V zOfPSakQ;HJRII43+OWrRu8T@fy_fgG? zBPIARBfz3%9$#Tf5n!;qXtrXBK{Loxp7i=dS(Giph>c0mfyGJHp}&fKJI|TrqV_It z^}ApRQ(cKZQk=x&3Dkp8zCIM^G=D?(wuS7r@e=JdASRxK>!2XEQpBwU205qhWd z{ia8sc{Fq5e@}Yg-Yx68cMoi1{(UR^f>QkLaS!noT`l2o6mFrJM?V4hn9_Pg>Votw zs|`wG;){A!6y?pICo}umE(E6_mG)L?>&yi`{U*Ppoh9Z%P~ClsZG`^_8#gz zDs&hHc^qJhOu3xqDpck*rgvx~oU%RF$sS<(&LRT_e~NwJnDy(kJDNVVHzGdRN_V{W0_epG=bR0?lRmyRuv3_n z2Y0R@P?QC5gFV`zMVSe4sbPwm6q3KXc}OfjkXco6AICHAlbKbN8^)OLDa;WI6OHsf zJ&0f2@IB>?0+3pK^~mh{0{Eesg;k#=F>0)a;H@e=_814URy9ZD8bR;wi4u2J{VmCr zj|*~qzB>6sgx*8WCG6FWCqBZ8@m9plOd}SU+=DV=Z$*m4*VniZZt|AbiQFZ%6i%_q zISjE|t7n|$Mw9zu^rE^iwmmRE}@Dc59gaV41DmG%%Nz>Bbo{>Dt*D<4F3CvtLV zwou&lpmB2lQsCKF>a|m?t@Ot0=kK|1AE5SFKrfW-A&-(8zK4m7yM_s`OI8vsbHFNtA)= zA!my7#%xWMYXw-bJ^0xqMRW1~>6Ir_sgtTl+$bz#ltb94oes0|+*s?nNxjF3CF*^O zzFM-*XH8kl)Cl3XIWNK?bL=2Q2xkaJ6qeaMyQNc(P397=Pl|TzJ5-b{1r{=4R?gM> z?n+E>1Fh&n#_O#wzHKf(eLcxM=k*h_rpbikCmzR%N^O~&Zd zBA#y+^P)iJ89DF!r9?$0gyNw7o6~Lzf%iSEH@<#{dH{fF9p#5vb^ADRKk@6!HO)a? zT89WW=EL^nV6cUmBUSZo>u?~>Ti=h^4t)H^>j^jeo>BC? zck!NgiGtD|iVYa~KIm|78)4#b_S(m%=^3xVkL%;S;ryX{%7a-q=uvx%@s^)axuC*_ z1_Lk^1-%bO>xGH4e=K;=M6nd7_}yL}K+o}zsz)gechc5exrcG#cZ#w<(o|YYmfy&B z@87bmwqdB*C5L!S)yOGG$XKpUaqAMRe6`T8PCDyDK7buN8Th*@vb4x{ZJn0E;|ntm zr#d@Z9jRwXc6t)4YV{aRTw!JNvE6gm!y?=Z#BN(VuYIgj}>$29Tz+}WN;4h_%)Y_OvThaVASxG=XHc0Zp;|zerWoy2Up2LzB2lP9F2(l28-vrdQv7KhS}(mZRbS$SehF z0GJAfIc^zljfK7Hp0oKm>RS>=%=pFjqVRLT%$-0%h}QA-%U1)Wo3w~pK~^m8Xz@#M zzU#>pAhX$$g~o%7Sd6U60=3CCk*LSKVyd6aE`PcI>`{_Y1E~}I2?WH448sudx%|_- zQDxT-Gy=oGqzrowF|6D?xBjlASk#FAF%#!+{5EX|6mkzq;eMkUC1E{5m+Po`Q5B19 z`#7!fo6k&NK97xKAxQmB5fNbh*>RQb)?fquc%a#g;#8q%ZqAbB{d$~-XL2I^C`q7* z-~|sslBjHS!T=HDM`3IB)Py|S@>8R5RGQM1KGF=;nY?}MlR?4?O#4jHO5WTdkek%SfVLo)*!7r28JM_Rh$`j0$DJA!ppI9)?I z1(V&=<7%DHD-@-A2+zqetB?$t%$hUMEBXczPtt!9HE}bblwX%mzL&^g4M*UVfTs;Z zsa{KQCWSkaQW6;oQ<5Wuk-*GuO;JB7!%v%t>SI4^wP0a&A-av_%3lXzC1KXSC=Xf1 zm?za1DxNm*Iv9O<#|E-I;Jp~!lZ1c6ddk9HgGt+$ ztK$ClEOPt&AETv*34BAyUT|t~C+~X)`hDT{ccWWQRHDlqCQ0Vi#oX*b)9%wCE=iPF zH^)7eosuoaV=`qE`sw&_VA&KymYy)Jo8C85CQBwOUf*IqbzVkY2&F{ikaa3&*T7IF zW8_Fqe>kL1zG>y{Dm$;>^JPa7$<>Vy_EFIcgz6<;CZSbT!KAvc*4F#;SR0OncXM6! zdN9uOhWrnY>_-{%{kC|nqp2bA&+j$qPfgq&IJR$q-osqD76X>#se0WjQ)U5nRao5J zv7*dpv!d(92hk^^X2eBler3OoZ>?*-`;7`D^hez+y$Kvxte|cNU8081wwtOiN;RI| zkg%lr2)E26HgWM~!)=YTnjT|*r)MZFZ{))8*4xP zW2!rX^zr^~0T^jn^9X6&aBvCvb3Al-oflxtZ%C1&myl^~j53F;jfV`?@6No_Pn`!t zWTf9kwbiuV_*yHcJn07&3fPaEs$geMG!oPfKHW#60+22eeVi+$h9&Qe8;oH_$E(Yp zJPZ4MA?FYg!RRc;j1G8^kqQqbD!g6qqoQu>%|i12VUlCBCJ%;`f6d=7pip~ko{PZCFT))}j&%j7sQu(Nu2mfJa%3#2!3YcasT z?)LGmpRtGxY8TwYQYaSKQ&{#esbcs|61(G0HXDbu_^Q~aDMv-VUzE#dSfeUKm8%zW z7W~zi-HEQm;1?Gr^#P_;=IT7VmX@$Oep1eBg6QXV(L+-r7=1_tT2p|$poZ>mOXuHS z3wg&x0+2x({xJ$>lTNK#6&$yGn%%DPWZdDRejlF4qIq&Vz6?{g9AsX^6HS6qd z$?Fz5$20T24!Jktrx^A0OcPT9fgxfcA0IgKYuoZq^4?R<-1sk!J^jjlC31+wZ1ZG* zPG!FS>o#jNNa%#+26Oa2IyQ_wOvKJM%^PT+SGfuJWMr_NWwG=DFxU00*q0J@VTB` zT?lOo;fmrD8=GHFO-#%i|1_5ibHVa@_#s;WC+fN8PUFYt>WsN-V;KQS{l~Qgn&P)6 zC}xD;k!u^Q?zR+-&*8}kMWvcGi|osPPl zC(~cw>q$Q29~tfXBlDng3qSe0A{UD+yY97kkB&UYBGD*^)xN23Lfbfc8)3y-v_k`>mQx)2s`nBS?H?I?p&xH=-qp8hBkNWRT{nMc zVt+VCR*tN;X`w;Y%b{ds?1mvxX)~{AEgWTY5kiX4yyqadSvm~h1BI(rIwe?xWhQsM zv^m{F%T|d@aJfgkezH{^Vjl_Dk&hdVIkJK34{iBU;zmv6)qMZf;zeWuKh3mdt`W@= z(y`vD|LwpYmy&VtL_fkp8EH=vvEEBd1w19=C6ZQCT5LS9{4#f4I(m^oA*y(I?ExJ| zZX5xI>4%phH0;MsAhNzOm~p8=ZFG*_{u75ZypfL>l+DNp0P@Z&3bL~53(bgwLcwx6 zAyw-sA?+dXZ(FxG>2gt}vg zH}{GK_+k+t!4I(Am!|;xZ{@w>b!v1v)2p6gH^w^)0FVIfcnJUi%|r0#mWP~d^G8ty zet!^oJhy=XfUwr35d(||5C{edP*JupZT+ z$a7#lF(!kVlhbAoA0A1>OtwK*Aq@Zk@M(DC{>N`x?mxz^58rQOb3MeK`bZ!CQ+||x zao^~Cln6+sw1`)&yrC4yw!Chp}C8TW?6)nJn7-eSVakv!G-5nu^!%^vW zwo7Rt25}N+EKVWrwUGBPQvm<~Kvh+l)^oU&PPH#qerKDG6;1#!K@zF_A~80d%CfX3 zLaIFWs@wb^OUI#J>RHkkNz2~-oX_vm=mh!UOy3=m+z zy${C7d`SY3?*jk;03JsCz3ZAir@XgRl^(X;V+6b;S9^m8 zI&9FXT1k|(VvEEO(kP!I003*9Bo%%UN=#e(1i^1Y+n=Bl{& zxAmh(Umwr!w~3Y(!$F5dQlmCXqDi9dsX-;C8)3S~jmIfkCX>0`E~_w??;*hy=OtlB z`5|}68Rg4jTPc&%WuB`##!a-wJzLkk+UqMJAp-ztsFqxEiE;1R5emFcn~ZxIpNbPQ z%oF`~SwR5Q8Gxzfd%MixRP0o^)kLZ@vcl~mT}~xQ0DCE+?06#Us@~qIy1XXbE>nKh zMQlUH<{Utrkpo0lxA!W3LNjM-u37#1Kf+;tcgT=f9;b5+(BlPkH332OaxVj~$$|}2 z(lXio&^sF$dlqxe0BHJeCX7a-WOO)WuF}7m>#qvi9^OVn@|FMw5c^vQdb?&v0zfI) z?|&~Q00F?Q*!+o40`4NabzgTWUnmxxfcLHfEIRyK?zA+D33D?K**56 z92gh?zD2CrEvIcE#RVWf(1a1=Y;F*yQ2HGV!D67OKWz*`=Hd5@H-E=TUNDE|$16OSa(g6(&Ar-vGgYsU|$ z=Pe1!ombU%Oi;QSI?&dQO*1iC=9Z5S%Ov#Z{U#A3@iW0p8Odfp}*-CvU4(hW^RT{Syv5Kt6d^ZZhqUiM;q zNK*DC)jRRrkPwka*0AQ9Xh3~^-@iXAV9>l*eQ#_uIgB=%Ceaz$-pkv?LeX`3o23#6 zfDVA2{Hk-3h0vXAX>g|eag~wuGc}3~jGdc~R^0^XP`2cWZ7XQAN++o8IA4QDjmvb` zt?fNzZSxE@12sl%i~@V*0YWCR0WqQ{fQ;tYI4O&Jn>A|7tTnqT>fwPigvy@oI=)2& z-DX+Afk!o>fcAUjRgbH>)5vrHK==CmccxVB z-v4dwaJVh(%Jr>3k=08}I$^lU8PaaT2PU|cZOk@s(f+8XHt9~=^j#J0R*x1_Tl3a~ z%_jJdFk%l23bMEJ@LY8cMY}!YEU#CceVo&WTXtsmOPX=kS=kX`l~tV{n$F?Liu*|W z*WH##7fWEJLyC2&-MQ59r6*nb7e2b80!w|1Yb#NafstNyca|gZ4|0Ppx~2=>YNCC* zR`I$Q+gq>a>Lc+fW#47FRpWKrgF1VoeC6QDtK|6M_BU@7O?$CO{9|276YWjETur=C zlVoIps}s1JS_?NZI6AYfD5kK9lR>?~88GJtVFeCm_uaHKGgdT{&{uBjb=mV)VVu=J zn$G7@4@W0hB&p~{!`{T%%b-Q{wMEM+D+4tF6z-PgcR<5P1R_5P%0edw$P!E#0Er9P znvi5c?BvlDwMF;bzzZ_NeK@G!83d&O$Pkbx2!Wa46ov>>b%L5e7z2hVE(pF@#hB?W z8lb@l3;@1mtZ(bt=mb8tU;rS?*x81q$P4`Er2r&w;b45|nSxY&NyR1Ns?L`PMF#-D zQ1UT>?8UfLQW92Tw-;-jo)vMG%~+$ZsgeVMea^{|DUr4sMA^_HnMW?$Jj~SIc=!(Y z{a(u1t1yo*Oy^YW0*+S}~Bt@9v{ ztjpWTDl*6^=`LMn)B|#V8@i{*R_G~PZB_2$>}rTJXxZ%H(zWV!6&$26+9v0{WU-gW z7iyTt`!V+8XT|l?$az@JLI%h@hRkIFkzqA?San7Q#Cc>!&KgFV!J*ek!=u;la=moT zav1My^d9Gt$nDDZ`pT&Gc0rldtgUI+l&)dKmR?7U)h?{79T9l!pK z`n+9qbu~b8IArHdSChKDzUYF;CL;qPBg1O)a33wr9P7yJkzfJ14ebc3g}F?WEdl72Ota=1G}UqdHiLte literal 0 HcmV?d00001 diff --git a/sound/creatures/monkey/monkey_screech_6.ogg b/sound/creatures/monkey/monkey_screech_6.ogg new file mode 100644 index 0000000000000000000000000000000000000000..d73c3e9bb22500ae5217eb07895fa7fa842a6ce9 GIT binary patch literal 19189 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%0AF zYYx9xjk9Zh7+y_)8tdP#(9taHoalOkXzcuk46MS5>MJ4cT=k||!W zDVcoK!q2${)CMDWb;3+3-pW$w{Tgd$Ssyt~vGv@aGrl=Sr~;?U`-WBdTA%@{%e^sh zgM@~j3ueo7P15mGv7}2hartU&64HgOLxE<{czCBTCs_AY%X%tt8Ua=s8qq={xdK@h zgGb`6P<{|2SMrT~J4>=pep^z+n+O!mE>gORrPiD{_sXKYD4yeSSe{7Mjt4CTR$8P@ z0y9b`*WiGsRHja8ElvA5=P&1I^*d>t0AZdNt;BFyA@JXlHhpfXu|FLsPPJ_~EYy$y zIjV_8HqL08onr2bWR5F8H3u%LtbIMe$ap|Xtd?0D&-HYsXI4x3*jv8@hF2J$`0g=O zbDHSeuu>2m;klA=M(4ZA6?2QWi0H~&s(Z`#s%FLA`^AC5Wq3^DXg0sf53`${fewh8 z?zPdkWL33}-0(n6X5J@Z90<0rmdMupOQz{hn&l$Y4y#vcK#ck9X31t6CQ8Kf2=2{} z?Qk!#-B!sk9dj){KrD`S)tT+tD%oLO@`+>DzRzxnJ*)00yFJQ=3{{rsBXhQzgbp~u z+>`LNUBARU##EpSu>u(n#Cd^|bV?g;7L`D^Q1Pps6DJ;Z@Yc@UyG*8QWe}Sw8ZJPK z4r%>~%1~2~vr_zg{{<24)guA#z2cdt&y8_Cpmop!cuzW*!)=3 zEw(W2?+lf8-<9l`MEx_0ba*^W_ZXL}1M<@G;W;+vC%9X0f92Nmk!*aoV82;Tm{hT( z2(qKb{^fKfb<~M5cA``q!Dn5tDRU_Jip#)v_)q0oVcNUSah}Wdh|kfQZc1}f^N2uK z3V9jovIBi_Bgr$ney2n%RaW4`NrAS3Pu(YR&ITrDuFp%FI z)V3-ERyHK6YSBt_kTMLIWjReDxWq@OEvpiCEmxK)w8`x&HT36lin`SB-HB0|0YGRV z-cAL*n>!zr#Rn}5`1ZXX z>pC8DGIaV_8x%-WhF=!%hzxGA;!jN4PjNxpl<%|VtaDsemcIx(hy(B_^b}RWiZa0p-Sk@GZK3OW zBs*V`l0S99F%NZDGS#`PIq?Rn39_NHfrwO^Px8I?@MK#fY#|E(7lrp;97Lkj3iM*t z%l^(zHZH?}UBIqO1OQ4Q5031tiL{S6v15Uj{5T?=B1YyJ8 zC${1G?Hg+X4qX^CdGYhGR;!dSGujkAs8zZtc zj{x8y07S~)my#^E#M~>+?_KH7t>DKhu3*fNHg@XsY zp-}?l7lu!14wIs`=nw$Y82$;qLkw{A*$Un@D|r95@rrz5#>e#U*vN_uQow(1Cfy7V zGD9n6?HNeQZ4#IMF~?=yd8Qh?QS2T5VLa%;X??mNQ>f5C{?|g1D{shqR-P;j0Qml% zxqM;oIV&#f;`%*Gdrw%a1D)KPSKeINx|m~t49IwKA4#^whz6O{AP7xmH~nWbVc;r3 za=s;E9_wN*g&79E1OhseRIox}HSRqb-eIG$Gzg!TpbqESKY3gu6z-UbS%gGVtY^8# zs4CCl;5}1865CjLt}T2G|EWPF&P1S6ciF)@))Ch=O9a>V;A7(MRI<%GFGsI}CBF)! z-TK5^-Avl`FY01M?rm=FAKHtl=4L6Xe|qfRQyJ9%iRc-+UlC$ELod^DZs|RcUwEQw z$0qLj>XmDA0kf{70fxUb!+(mV)h3#=>j^M#*yh~jrZYG!pLMYVE5g3l*@&Fw+76-} z8(+z+AriDCac3~gG83fh68Q;py$`D&0Nx6&dw^Icp$JG| zx^tLMV;nNIz%Oq1$x>#Wz_XacU(^Udr`xJVMnFVegAJ?G>ZGilt(qx%dZutyoYth% z!{VPm;(`z3L5}0fw{lSjmPPYQ)qmGA!r}aDsrP^EuUk{mV$@st-t~AC#qqpg3T51y zfe1>6OO%E+S{W-0x2W5ZSN5k(2IJ8~{2e7Y*hJ+JXUypJ@e61n7yg0Y@IK8*&l&?V zMRLhd8p#%PbY0b%SVkNB2~KzVZFtORsdPIe6&=~%kbG%?Fyd`6k(zk(QkyPAUW(Cj zUu8wex|h~>!xWGZmu6K?(KK~k*tDm@eP>K6MS21RSzW{fUO$yH$g|9Au({R25`JcG zS8-4L+V%83y9!n@^;$x4Z%MGJfBNcx&CuINNen;psD~S6WE4lZj5!%WDG3UppRYvq zt5kYA56=yJ$9Cb+QROw^F!`YlG93IRpaYGk15UvY`P1-y9K}d}XgcUa5mZl5eNH=ar(e!G&{Rs42(D94)U>_*yen_0@?i`-O^ zlPpMduwcKv0ZPd@DWgj?;|owg0Q7{cx={ChM(Wm|9I|5i1I4oZJJX0!d>U(2(6ZylW|jPrQC3t#rXnj zaOEou(}KHzvH%TME)*#h+b+}~Ts!aHWe@liN~8Bzab|PyPO`W`?kBmN1u$>kp4o^@ z15R`yc!|Lo5Drr7nQ=W?d%QyW3;VYktecC6Im)Xc%rPYN=_h7;I#|&ovMV2Mb$Z@> zG0@}XSRDQ>bk!0+c%=Ol4Fc9HIuw3I#z5d8fDR{v{<)PtS#Ne-vy$$~Hv!t8EbQd5 zTv)T^TIx5H0F>Kb(0e}ZY3pS+wsEVq=Z}^Po;k;2bKUgs=GeehCnjHoL>(x!28A9Q za1?P5N$N6-#N5F7<d|IEx!t%LZ_S8Cq zd3uED5xKo$G9|N_LHe$$PCy3HADI0uVU89<#e))UXIaWx5}vEWMexZVZ4(2WRO=4_ zdO->mB`SE?kxEi43&EGNm_J8oO?_P_O0!sC?z_;Tk~p6R>M)tCT;V2Dhv90w7#aZe zEDZP?gCf6=E)@J4TO@|)8CZ(9eZ3!A*M3b0uXPBZ;ldx0crpp@{m+F#(ad-oT z;b}DrgJbEy^II-t(0xXnF>NSkI6aW_71@9Uo#j9?vRQav+z#A`YHgICHbV~eXQ&6! za)3(q3yboZ@>pH;s`J7yPXvBvw@*6^pV}QCHRZkti;&}KssHluw518pB`qQ^%jQ#v z)!;nu&itpp(&1xInR>LkQ=wa4U|EK#x~33YdqP$6G)w|RH{aeaGP9pfoGXgFju8+4 zy+@?5a`rBYw1mIJxs@%0A~c~Wg2}>7Ur6#wS(OvW$z@o?l!P0Bc!!!7zNE!P=4FzD z1CTzeNnAEgZT=Lx8{0NnE|A5-22eb9sW0Anv(R#I{N98H3KN9J=SfIY3Zee8d`)qC z?6dnWj#QN2=SNU5!Y{IN9^Be`wBE1iNHvq_?p^MQ&({05Q-F0&7$s9)>kN+)wU`+(x~$6r23=X7IqFZTrUV zdNRQ+Gxe$Jq#<94vGcDxUtI`eZgD+`j^NecoWu@Z2N)F`fDroVuDZ4oG>aQg+=GWj z25`thR=|cw+ayKA*nnx+t7@g7=!t}&UNst;q9L-w0ZZHbYfS)4pQ_M-A zuzczQZHU(t!ONI4Wzz;WKGvMLpThr*3wqz9hu&PVL_Q&bMB-Sa*u^-na05 zsz#NtjdFMqTq1!4$vWid(`d9MoRc+Oh_Oi|Ldd<@xO+&Ndr#qJC38 zwYvvNd<(OMV_gme^Q`Bu3kIX{_;-PD;wIBUZB(MmY5`xn6PmA0_rLQ89ew0SgVgvC z;so)Al63W1!d&W{WapL=23G8^EVW0B6JjV$X`}se+3-&+xa6AK>M`H$FTJYG_XT5^g zN4qKs3UW9h4gLu0S5>V;Z? zF7B>oc`(LW7ZPQGqIf!MMnK@s*|{14662rWQ+~R56(7DL3MdA2`1!G`kjj;UnbCl( zUA-R9sU8EH^Nr~^{x7XN^vN`8as#+R!Z!&@6DpB}#i`zQ{6Zr3_N%H&%=N)AjhQCj zKdxj{ji$Fu8677fFf>pp+(uE25VO&Rv)vITEVJBd$>ch3pH1->9pr8Nn?Zy)Ost*l zfRciB0UtUUE0P8K<^+LQ^@b1Td)Js23-n zq2Fs>{X&TpDnL=2Ii*y`F6f?AxOCgJ#Zdw?!{$|n$8W^h^Im3d$Lq9>w2JcyT6HdP z7RI)?7qQLpg~z+{X?|Cslp#Vq3i~pvMX&DzW$HoptSbki#i!Iv*v7!BL|vZqNyDWl z#k-VphH`)*A;10NtC%|<-=~GD-k3v=$1x9kF-Z|rZOvKdabas-9aRy%L(#4xXDn!t za`9SZFfMOd&W|d}uNiqd=ujfM(THYFZe}wM95HnHMgqKtUG?}J8KqZgRkdFc?`m6) zea6`z0-9&}QiA$Nva);7EQqOrhneR|05Bl33$&F-G@1ME2=8k6jM-|KtUz-mU^;Tg zdl^*J3)As z9ox&zT6SXXNfWRO6Ed_;rOIkz(WH^2}SvRD*p7oZ0w z%DEU-*xdE{T;z2_#vuJ^{XrGA1%4o}vi!&@%$Nl&@xDMR}e^#HDH-jKk z-z?Un>{4BI?;CcW`lx*#_8r0Bfwf2CQZhkFI)t^Nk`SyHX=>6&~A30hYf}2PTxj3sFQvv-5hPkCpWn6NutM z4bjm5f=`YIJ@+x8twI=g?nlKY!4P;8_+U+j&pX__8;;6Io4v-}&?4C+Uifsg6kH#0&?ot%N!ct5zq$8&H4CNn0FL#IavzU z%f`)FV!Rzggn1u>q9f5P^9yk&hW=z_gT=OVF zqBsHS#q94uKXbUh$3INCxuZ2h6^Q^S&y1+{vn}{IKXdG647TLJPRlY=Y~y+xu}`eZ zhO)IXLo>g>F!Qbn^WC2%s<(K?xna4r`g=!HhT0T`)#N1j7NMNT{Vg zZ?%mn`dFFj+w$-;?Q~T`<&)Z9W4V4eoKn7L8my7}YNF^wj@;EB=KlVOP)W|cWq&E% zNC{AlHAPi?#fSq!FXgTQGEfN7~UkfZ0| znF2&;@C2`)?*DPahKNT`)cRRPfvJ;?E6!}NLlXl|Pvy@2W~&7ckwF+ucMN&7(rpa>rU#q%eR z3sqT~po(l)TmK|p7XrZPLd^I9-N5&#@^!-b5JNowV5ls{z@JH_M&#uiDJF5)za9WU z*>LTSovp%JQa(WOB--STnPs8hC-IiLlYbH5d0KgkOxP?O>l;={BK5+pv*4{Rx+B3`6|dA=pZU=_7OFf_!m}O!O|sfz-3htL^FfSJMwH8E4-E zPMPV9K3;+g-k=r8_zMJwh6jO->4W{5N!ZhHwhhB^f~k#%8WGXOxz4nc{EO&jk5yMB z!AO7=E#vVP{AWI9lxwM4E7xTvlbywmEkFlstDQqaJY@gUXfdH_jLS@K@Ux-rWSksM zg`7-dvb@@O>Qn(UY8}f+Yc}4X4N}#X0ha@RNT#NI7!5McL}mka;fM96kPLh$-P$2$ z^}Q!X0XHSe!8{{*r1U_}F`Et$we=KTd{$P~(yerJ{;C5uJwdP5D#7#AU&)!OW06%->O0sG*b|CL)wYWRvUS)N;dI z#2ZBZ6H)PId}v$w*sm|}NR2e8;fb;FL2(X5aDevaqwD?MU5gS3Xg)#m)#@Pd{QUX1 zJx1(Ux&gLFm$c83Ks;tLyF$fWP6D%Y^a4LWrfcyPZFjACLoEN185IC+Go@6Yedc!) zD}7lg#`%TJeios6DDU#;V?_0N@4=N(vLu+h?efwEwRXik7QZDiyju;AN4}}}dTUEZl69h2E!DfF>TrMgxV}!|!8}GN1OeShr%QdlCLc8xAi$c}w zrsLpM8?M|4Bb-rS?9g$v)}|HngPVo?^XMzVbP)j9VlWebs&FlKqv$v>447ev9eSU^ zTbE+JH?Z%)^MO%A;YPzV+r^3gFjjG&R$Npv@r;_`-2m%YltC~;kW_F$BiabocVlWC zlQUGNGNcLwkm)m+GUAOVg>aT^$MrqNdJtu3F9a1g z*cA+ve!^QQ!sz7FXLQmJs_onzmiePFc!O`!uIu~Fy4|mT!Rb`@@n$qWjPaqGG~I+4 z&;tO!pr~0E<0G%b4I8?4kcLcY#hAH6SJ#u%Q^C>$kU4GX84XJ5P47d(h4oLpyH#tHQ&E#(>DXCtCD=kg`{9K)Z+Y?(ov-E1! z`AN=e>AhA}-QGS32t1q97;md*$^HGyBDx}*5h2g2R`e6E)oZf2Xt8g|pV-`2zD>uH z=_oD=)bTFk{Xj*37d@6LeC9?FS}Ygf2IbPgdBunMY0lC(0KXsr>Z=5^?K)ztpvwGLXkrL9Tq3M!V9y#dg?sOjTB>Pqz7MS zA!eIn9w0olJ!6JcFYFD}or4#**Ge6}ZR1AW9$^~T8c}6SAQ|#? z?XlEOK%Oc7tzO@ijI779=PU8BUaCBj*8vklOS#dOhr@_O?*`JHWjbdbJrO!=t+m*` zm@dG)e!wUp7STk_5H0dGm(dsH*##wH zXjnc3gl8$45m4KJMX@RQW-g47K!ak;PV-PX*;NHqP=XfBI$62dT^k}8<*JpqE&cY2 zF}zNelx7_$@MISFzBo2qwswzzpn5Sn)N`NZ$%EPTPVt7Cdak|=I#h-HsRlAJOxOla<5QhbJP7i^^2k7Tyjv|Lt^uvm;H||D&f5jc)Qx zy%xVasc}k}O}vi~VixXrP=Hy9R$u+W>_ounr9280rRZx+#K0PYD%hqQW=G+#)LH8( zS9d1bB>gzp1Xa=hs$&p_S`<0gi2FyqYLB16X#6IvReq*&<=Yz9J#B+ti}uW={Q>>H5VCq*n;+9;CVBrloN+c z(ikr@4J8^iq0mNCcw{CYBle9Tw6_hy;Z4Jt)02uFWGam7M1M|>!2g=7<5@92I?iV~ zMQPi+I>i<{mg{)p?k1T3LK9x%jqIrOF<((M)EZ&Sjz4eU+*{aZG1V|bouDw%HjwGPpEvd~)ovVY^A;9>jkP68jDTbB!K9aTD+_}D zTFoW_VE}MGe$U@VjDfx-zEd@up!73fjrT!`ZTWdG(X9T<2}H|eV(I+me)k^z8nHxs zDlE2dci?d5zSXOwqvudD)3y2hs$#w`mpTa>VmL$MFQ=t>wZ8nB>tB>)8g9u2d0 z_YEg2E32R0=l5->goLcD)aCJ|0^R@s^zWN9xc!9#Ps5XB8L)o;1_+Et15yC&fuM~6 z5U{@C!m{9!AA|dSOTRB=hB0JIb@oP4@0^U?>JzuK2k#(iN&CL6!@_>;GHM z*JCD8cj?|7bXVQ(k6OA1xOUeU%53IN!er-q$Yjn*<&D#3nl(Y`rO zh7)I)!^tBGHUxj@s~gnE;|#wj&=m5x=wkN3Bupn1g|p|*BfH2S9f&;+l0NexIwrzk)Yy)Oh82}2v7MKYMo<#)Z${UX4in74~aNHt- zaupz|b2ZDLyCzb{5BS^pxO&@K2dh;z)IznbNCp6i(UCP*pQWnIuGGHg#vy~OUH9@v zW$cABXRf~c&HAYq5B%ldaas6-+c!8JI(U9Ri!R-t2HP^wwyZNV?alS$nxUDtrb6y? z1GSzfj`9q!-!+P#vtG&cu718<%*hfJ3W@>K&pptF$!o3td-Lb^e)W^ed8vNd>$P6% z`}e=ApL;B4KPMeqm7EP^<$iZh&oxVHe!u^I9_njWD)bgADcLo3|-|$Ibppvt1=b4^PU~0WorNZVsX`qKo z_J-a@Dtp7Gv+JtTxA$30JE`#ZJ85+lvE`+=?9KeEU)(>N)%3C}V%1w<BCEvBw1r=cmVX=6#LXlv$X>gIzFE!C9MRFo2ChgRn|X6NML=VIq!=Vo_= zLL^j_l%$k3HH5{K<&?w}StuoB<&;##l=Y;fC|UoNq12F2ml9LbV4)OKm13b(Qc+fr z(xp^&vvoA}uylvkQgU{(xAbv`;@a3cx|F3@s@ceYL*rq-5}>XzotZWgvq){N+X zyP_^8p`fg#Brc^c%u5I@kdabWm-^o{4}$*}qZq{{-hlul06+i%|0KcCBmgmmqCx}M zW{3%ZLc2hKDlPz4gGK-VnT*#007?J?0I@Ktq|vYpr0}GOf2;p1U=r7$000PJlZ)-! z;EUaojop?dm7|u!jt6q7%ArL60-*s0fcqE1-}oO0G87Wj{jUZfXnz~{8~$5+|0?)T zOvz<|2T+25oZvh*23!o%f8}AJT?1gkVBn#h_*efi|2Fx*hJX490RT)4xbT91_Y2ya zkRZ@ML;y^{#D$9(pn>vq5{ikz0_YGE1CXMh&`c}>Dj*|K^9{%mgZ+o9%|G$K5aEDz zHJTJ^G1zcRHBwZFIhE~S;D6pbW6w5wFZa6+4Mx@T;VOke-TGX9H3A(2MLjRxBKXd;W!FX%<{-+2Y; zSy$yF#fa#mN|5~*_J0WcWw$wFY)=Bspd0C+8qI&>xdaD8qBylKHVZbs4uFKN3tcoY ze`nU;@LwbSPcvW`04$UE7hlSarr(VP@t^8Lf2SIlgC?ZM6_*c+^dGMMhZX-&f<{mh z@&I&xbWww#)sO*7H8f;OIgG9Z09c217fOo?2(}`#-qx-VkXxuK#q` zI~&P64=M8BKnNX7BsJu}Ey4dF{lDk`|Yq6&fW% zQxF=c5Q+cqIRBs0NB}_S0+ldqxoEh=HP}ET4B)~LT?B&4-gUkdc$(oFAO-##7o-Rz z^y;tZWB6P7o0x`~D+DUXVE{F#DuLFW;8cn=*G;!dCml=_8poAOwDifg>8TN#z|FNy zw@#nrn$S@4&f804I+vV+YFyQS-Yul4ZxaX$uYd!@VE~rD_`5!NCFxZqP%1TC<5xlx zT!tFla*4FI>3dOmjOj&(i7cbPgjBes=w>`kb<>M{@~op3HNuK~vKi8=B9B@oxsY8e0k9+bO?7*OpkJt-%hEH!V_6NZ>G8IK17;{uRwSC&yT+t@-M zNxqa_F$<6m<)u#ckOVZ5jidv{g(`hPD4PVoVWObxA_LnXKnKc4E~P21@y|30P}OwB zm1wD(QfQk#P{RT(Rk>1BtNWW6H}_31vdu2Gl~kMJnwWyVsS-3EzmiO_NzdN3&9+X@ zV~Eb)^U1dL&D-}$FY$o^ASk3zrR0FSkZD3wG8vx?fSCY@m@A9il+8KUXbo5T9!TyN zSN1Nbs~47h;!248{2cHmCx;+T{71S&WB|ZB7#2_sLN9-viTep2|L;5)f=;u)YPkTD z1A!_OlLLh+6sqyipxO!Uiz>oD?-dFQ5dNR$92{iBfDcHLn&<14|E0TOuM!iB=X05BZ_3up#oc_gY?V)3NAY=EF^ffk4-0ZgEyLplrsx~Qn+{_0EU zLZAU;G2&$bS*ZC$1I4C+E)nRmhNhv=|4o2`!7!Kr;IaO9l}Q$c&D#zn%dAI=ha>;P zMt}%_NZ_V{r&u&_lx$KYYPhDh(}46HRZ<1XYnE6GAMZSCy_AA9#mH>SG(Zg(BnfQ< z6AmDZj86mS04Qh{P!m%;7#C30G1(`40|VoROazY*X5PbpPzSY6#Vx{RC0P)nkY~VZ z>|_AT8@6p&aQJ}qVn4hG(R%QkQvpnI%wSCD`aqHG>PF@jx(KIVUWd!dQ%C$q5uXD9 z)Cd?xz{J8DCzXj9M3jvjLXwLbMvngHUFShvh)^*BBPl7#Irwwq0)$P?A+B!b77R7{ zktqME0)qqwga1kW&4a=Je2>7fCjCiMguaABb2%?b&^z@d>m~c8_~k3~PJyONUoxPjJ};3q zXz-m@xL#%-l-bZ4VeF~eUyMbEOLmiy;kU(VM}PkA()AyFvi0xpZMZpm_;cbO5csqb z*kZk$tz<77clTjz#ji@gcJA58`B~lT<9l045&QwK{+A0QceFvA*&DgTBy$5>wH5n|fDeI1uM^7h{KdRqC7;OL9$oFOD(rEbl(XDM<8-Fpka!+4 z1u@K7I&ivD<_vj0jg<&}ZtOxt1I;k?+x9U@fcf9k2{Q$-*Gkw_^gREAD#A}D{PpN}q9?PmcAqEMF6r0FGi<|S<__v~)=#an{V*{?L9x5JwUSjc9UJp9 z7wh%H@13KZ<@*!`0~#EGJH@d`ysvwPbb*m*YU!XwsmC_T%MH2#bYkKqat6b`W`>?d zfiXY%_4&pB^ZiSHBL!m)ryGC&1AfNsln$lL(j^1KmC7(%URo&{Y>>-S zT+J6F^?(9{)Wkibd)occ6>35%X7$xURDsY>I)pwtreR?lc1+X^%F1Y3xau9F;sQM` zTDxc{eTLe2iJPZPLj`ytDJyOYVRGSbQX;rx<|W|C3`j{48LhR$;pnx4q0=>10JYma=>J(ZL$xk@hG0A6u2%HcU;WbX1iN2O;C0suCbK-vQGv zS6^Qy<{wsc6HaPu;rp&eMT#Gf**eZdb}YTDidr5%D5KVcAeWc=Gn&cPo%U8%>InxL zp8hCHvg!)WesNo)D~1+Q9~0)wAuvoF9$G#Fv3Yi7HjKGd49Zcj>uanWa!DWy_Fglx z*UEKdTU%!DRz9O}AwGR)4WIMS$sWAr2O2_n1ol4Y3s`QY%Ko8~O@xE6_I!voMIFgs z*_G#(@{179mp0zbP7A5z2qC!17rI9x~JyLlu&x^RW&CxQ?^k;P$ZSuC=Nh;Nm)fd&%}n2`K_5BFDXyj1#43Jwp9dv zU5m6S+b>{?rXo`peS&m_9mz8>E5yfs%|7brplGo5 zpdJpAocB0k4N|fW~@xuXNkD+hDhyyNgQ95PfZWx4m5;#N?;VdXo%3IcR<_>jn@u$0>-snIg?ccE3ZZ z4A=iT#goY*YPeQ!5B${0a9Sk?FcoTXayX3WM0SoB{Y4~qEhN5<&sjZuS?ydi`GPJ1 zyL8~)(1f8VRc#8kwW(Gn2SC1XNR+$_$&?moy^}qs1x6GB4|G;};pE1Tsrr5+5ic+8 zo(5%eszqHD2mmY?vBN8LZ2g_z3-WwLoPT{btfgcW+R~QjHRlqJtAHErGqU!D?nTY& zH{m6IKIN&FnY?$NAOJJ&bTkC1db$g$qED;M=GN8jKesFc7Qd1ME7 zr0St5r_I4k8ht-36G2-gipGb+i7=adF1}1_opf|*&^Gbo(Swl%wSI*uZ&B|eo6kYU zzShmh?L#U84Ehz8%TFzA@Z-aJ-9Ach=k_@JeGMYEwqjHDf$*~A$F-e5t??oNglePe zlW}gg&??`Ot|vpfQ19|jVRl}}L{R*GI7ioDlr1s>1mu!;l|3Ju&I|$Kofg;(TAE1+ z$7uVM@-P9HtND&j>FF|(`?G!n>Y<-3_s7(;%>5{=D16*N$86H;+ltGcQ$BXmXr^s8 zOZChBKdo}~Uyp1F1CrnakzD#5=f@Yctcy!$*+zxdQJ@w&sV|?*>Ta*ox`XvsUAFe} z@TkS4jAL(CgzNPb`BNDgJ~9z8ed$bsk62FOZw?QlcKQz|`mUr5mDKX64L+LY1i{E@ zKAAvvJ&|x=8j-OnJ3i6Vph>+&NMw;o%1cs>6~A~jf3>o7C@J*O=PKaPi}xXvtAUvQJ=30YV%}fxdYJA;wNj6|oBKqo` z7BAybVK1qE6G|DHDwv>T6l?qCbh(Cd;`AJ?MZ+RKOI zy(NQm;!^QE4a7X@(KxM}tq+YCwXl>wE#ys^MHw(KSmYBynOFUQfF>wi6c1F25KmNk zTZr8;k&ej@P>x+J(1?PJiX5UHv-4-!7rpO12pXwAe;(K9JNTI@7nwGf#}!%+LUF%jz=&_S($H`i$=0F4 z!lUgrlY5ypkf8nf@zScFf8bpuhXQ7<^p2Mc8=wkI^F6BqlB`-R_)@mKbzg2pZ`Ra*(UhCCMQ&l7*q*(h_=}R^D;mKo~z%#DrG!Ror7c~#TYtHz}@thB5J_P^_WIm3NT zgy<*DfLQ{`L9`iCwc~RXONih&HltM@$&sGYmxKSfuA!2Mo%fB3PxVz5oJNA1xA>RB zsTNDm5*4{9y+QHu>2#lnNtfn2Wj&#}c2~OEER(q>Z|2Td+}n$WJC~e3@=F7{)*i=J zaSZYz6DdOzSF3Zd2|V6&5c9a~=R=L}`0*5EOENE54H5UxjuR~DA)~#&BjeVLdY3xe zuAbKhul&8BRwx_JsnT4LD`HDD*Xf!K^YVT6&(g!S=L@d3iNok{9j-3^746po?v;1S zIUh%0=tJw`c*p%OW0C^KT6!9l7c^=aWg)Pzgz`FjuW!{SUt_Gxx7J{~#HxOEZq!5i z_EYAm!nz#0VoMc}Il{t6`c^&jBm%gItVZSJ(F3c=nYof0O~syiygdcwrS}3}NIjEM zDv3VC(jt}8H>N9>e3YJtL`&1f2F|bGnbd#3gYKWtxOQhZtPJ}JVDFeC7pHa|Tqf*? z#pmakh{|q%40aIxPO|_upl&dIZI7E0FCI8a1O%I@Hd~=o zA?D~ds}U|qllG|*jS!xD%%#VcGKFip9VEXcj50H-%AgE4K_@a#E@cY#jI}i-YuK;A z;cgdrL4$=)#3#B7^*o8iHzZ*3NaInR%8-n&R5y|`aQ0+jB&SABo}CW#@HZ=H#ta(muQ6- z#_;4*QmIEp*_ROC0rpgW_f0LLT!Cxa86*zb`dvB>fiL_>fvjBmryZ(aIbtT@wFF!a zY~I`)0L+5}f#>K!$+66}ZXQ?OMjsO=NlLmTfo(Z&rr%kYLS(wI z|2>~zn|SOenvN|W3WgzU4mq}(1a31GF&q#qq`{1tqZD$Vo2J7x-heop{bWp&T-2%lkrf8mBsacZi!#o&1D1&G;s4ZJM!nDXU$1MzU;v3K> zM2CCMz6n1He!4okG7%xg*t?3kLjb^k>>PFd_wIC}0EiF_c_UiVci%20k}?#JUAC8N z`W{ZJp{N8MUM*+-*B@G1hF&R+W^gtLzP;h+)N_QX)4~8$jbIM$yLb&+-G!+{=XF-d;B3V{I~AjM0uC9`wWhSDVdkZP$mq&RG&Ly7tv8SFa~NJ(V!u8Me;S z5T`4>qZ=QDb>M@Kehca%1SSK;rk5gn&+RzPCK|E0GENekEA(MPXBskS?5=$~XYea4 zd(U1Wrz{Tck&b<6Jia+yYXbnIfn%hitDFqsOj<$aaRTnaB-W0{xN`V8>RWWY%h15t z>SX8IrCp7;xpCNR`%EDgmC!9mAtj!ri82OzcF!z2s_*B7yx%i^WrdXqql|pk)Zzmp z!;cho5Lo4j=O+VU;;-QFSs;A^y$(j{nUpir@c^n?xJgo;*HH8~u5lU<&o95s5!CS$ zm=WrK=?@1*)}xW*B_nM)0dPij7N2PnzvN4AwqnSYGqqGOAr>rJ$Y3B$v!uQEswNhD}J6tf@GUQSgATFr^vM0Pe9+GW=e$rY{k(LuzTH7X5G?3 zz1%oljPMct{gEEUtFCtPuobPq+_)02;=`d$r4K7Jl5sPo2g2{>(y0`uT>z1KjqR=e z6N^Pxj=kuJ7v;C`pO+dRpV@vaE?;GQ&2=6!K2aJx62AvKx4g-DGjH-yu+)E<%New= zR)X49l)c+Qj}*y0KoR<-gTrA3J|yviP|ReEJ#$p~pvwqkd1-ph2$o?+VEYw#ak!@H z-hBtv-vP6k{7aXEN(#mUG^*t>GDw3d%*aUS#5WmtBdTvNFQ1VNdx=SjLaZ9US2r%V z$*!hF;Fm=my#gR~m7bHwbzJNloSf8?y6O5^RXFU$%lY#6JPj(v-veZ1mp*TAP-or# zvU+3__ANmzppmuoM8%X$>&jV|oL3^WslP}MjlzEjYF3{LCj7>E$?#JoFZNRnMHyja zcP@=NJtM<67sW~L_Ougj#cIh$JjB;4-I>62B950QTN(g-q(nzJy=0qR--WyCgXd6- z?l9ugQjnYIP|pL(nI!I!*?3|;R$Oc^7T4`7R32N0bu6sq@aKf1`^7Te0`9`LYMwkL zde(EHQ4umc71`QZ%)z=!=oJeyp%a-7kbQW%fi0qHFpR5k); z(h&43JKa+2NC0*Hl?~-;E{5RsZ2fma%yX*Kma7n z5FsNMYr$Lb>!B?xyY?39Rwx}YGhT5De2T3CXDvqrP0)0Y(Vz4to1_D`I>C*MmYxJ*u>K zTD&{C76O`rK{q3WP+GMWc%3=;!O7_Zcdy6vx^IF7OK}x#mKO>l2tLV;IcIBTq_g6R zd#*?eV}j1V?#{{bE%$kz-4{4uapJV?Ro4zZ{baqP4~ODNHmt~lV_f0a^X>)s1CBS#YLmxT@2;CK<+Gg0o|`B7$`2UHJ{s=4U*5YLpgZ;J zW6gCwDE5|9o6&e;1M@--^pij>)EvpQ-Kz;+uz-&BX75Xz*3O&IMba|>_;Q8-rZCb7 z`;t+g@Z7ZbF-Qc?Aoz+t3ij;yMvCk2ICSiO7kbdve=Au!Lh@_ znW4!N(aJE79u+Bi!NM0WU)}^!H>Fp$?^u<)I{={Km)Xs2hYZ(V8U3$@comQ2YrGpG zV`QuArX7pMlds=%vGe<-@qBDGErV85Vyuzk2-!`7uB|b)7=u1CQuDhm@`HhaeQG2@ zu#6&*SBMS=R{NGu>6KDUrYKa?tz4Y2JEXYZpu4*x#empb4<8jT$*3Tkr1pIvZ>X6y zwc@q)=m@3Nr-l=4HrHzKoP&`XW(bJL9J)yj0jx{~oQP+RT{wWey`l$5Zn^T)Zi}A+ zeEy!O?$yOSIq_vb?~U-C;34=Al=!jJH zpW~hB+Rs@vsrHU`Ay;L_DVzftBC9QLjklC35(j^6J*X%bB<;*x8cM5Wux2QkNF$&L z1zx@i+92M}+m>8bhUP+6c#G7Ap*hLdb@!>p zf)Al0^`~}HD25ke0C4`j=L<_KCbx&tIcHx=)s(aP=FG4>V&xu3a~ZMam<}p)|8MU+ zAEAD&`U7NBj@(nYKe~;$n7t|4LRIDN=q`62(r*>2KxGJT_!DI#{~#BsTW!IBzr;E+ zG^?c|N$2eQWZNTUVR4s2)H-o4qPqmud`Y1R>60liyrO&UL}|FJ{wIC{%(Au zsCLjt4=eofmp<--&@t@hcQk8;GmSVa>nE!OnkNl{hgQLEf0MN-61yU6dY4_7TU_A! z=5#OQi??^q-Hdi(a$%3A+r*od_dNmSIV$Le(-*DJg5NZpb8zK%s=OqHGeX#r$E{-O z9}s$me>=OtuIKAHppzH}e71mF%Ddhnq=3zT1%!V0_$}O($S*pH4Kmq41Xg$3DhN~5 zTTx!S&^&$ctFn71Ew_CK?~uD^i0kPl#PE^Sto>koL1C?LMN>_iw~ldUXJ63L*B&gL&NdiL4IMGazRwBl%nMfe@WBE2x>%cN4#`8 zdbdPxta-7F(Nc?DQ!xx4RV;n1_`F9b0e8?+4+vjEEmLKzA6#(Jn?#i(pTfv$k^fP? zu_X-P0&Qmvxwz%NO+afycWaoyk%f`C*luVNsRyL&@=+W8ZJcayHnX&q?M*e$lm_MG@q)0Y( zdf1Fm?NtVi?%cLNJPGAz&<`Nq_2#%BoZo2{Ose9W;nHc+dpk8Y$vXg{q&xWf>(*a{ zr_lKg-i#*+Jw=7o(}sR;Gy>sk(+32EB!!nA6CIMd88Gof5kf2g`Oat4tDDelf`<_8eZO=d-m0Fg1%3&WD(u~Kdtn6 z@KmRJ{bW8%CGdlkjMh(UcUBSs-xhHINFypKwBB=V9SIe4C?$N2TPtANPB<4L0lQK8 zC$(Su>&Bsgc%~S8GHx?>7fMj5;6hW@XmO}X!p|lgrukeuM%qF=+HdiIm81Y(dW~EB zvJJ92Jj`H0S`4v!BxV~D9<3Or!gBYqng#saBrV_)oevMD|DhbIpe zyyn%UO1CxSCB}O^C4D=liZQ29=(pTGZ1ttLHjlp}TtEM3?#Ztd?t;u8gq4iilv<9v z9U50xE#8N*bK0Ez+3e><0G>`$9jv=86+tT~87&dNR3x)V(XQPZo%(KMW6n{Ynvy;< z1@B==YjRoj^i+xs29i|+U#1cQw}fsw=LskRv2KKy4-tSIS=XgOwe7aRnQhVHlFo@8 z_zoqWQr(YDcfpM@Ih%b+?T=Qq zB-4~&$c_L31qH8J2^!~524B%&FD0OBX)~peKm1J@;Nm79%Uf6+@KBNv7)l-de08|R zokRn0CKG6h0^|c9*uplaSZb4dZgjdLfTjNR^k+{pU-hJ-X3|2aL+P`SCZGd{-YY%G*H zoulh-3Ait-uWe#AINm)TZ4xwY^HLRFGwzjJkFPOHA5ffn@E~HHC4Ax|WU>Oev@dW00 zdP?XtTJK!7ncvV%ttEt9`gI{oTa|vg<#I=TJuY{Yct06@)@_F#7ogBO7 zNZYYj4R=-{W{Fdp8}GM^ex-`hs0%9zN03EJkgw?4!RcGkFQ#7&Bba^&wd&Mstff8R z_=~d9et&C3M>9G$Mcvv30M@OdlT#sAJ@@U^;sMd=qVs@6{mb#%ujP~c!w;ciClA$A zY|}`{hlankTK3Kw&2SCZ8XnOI9rzFfX%vIL1wz9V1ry;v4`|CQgnLNVnmN) zZ*XKZ0%js*QKpm&Or3_E^}rWe%o4Cg_?H;w3=>}>B2Re&?;;RhR!~nGAP#$-Mw#o2 zblmn~{O6A(#YosJ_zN6f8W|Z_yajt!aRz(saCk_xqrW{y3Dcx^c80>ye%=%7)aF|@ zYt_N2&GLCRM&BU~;u%7By|?^iN^i>^g$<{7ehvj>$&m)K>mN7GorR|*2vN*!RRL_7 z$nz`8Mbmk^e@>lus-FU0&%C?tV;A@g!`!v&OG4Pil@>@LXy&yVcx&*T!yNW?&^jE0 z-R*lwf9QgpWIk2~u^j3*nxqf#K!7L-YP{G4IwVMs*clru=;9T~7&)~*s=`03<2 z)PV>Bp~rK13J4MjIlY@GhOUE-+qOm{n#HLLEU^^c{g19R0`!|v2>x{E<}FAWQReam z_CM@fx7M5OhK3FXa#|8SJ)jP!OrDwtfl)IuOEY#1XuK(!BGdA$s8XUB#wq;=hb50JzhHZelcW386EeK7z_}BOCIX;^m{r zv8NH>;y#pXRy1ftTWXWQ9U$mW)?8aL_uz=4z7aUOqv_fh&J?!=l&sSWsT~AgkC~I` zBhe_MO^t7*W%*aN*zA*1ex8(sgWL+_-JFghJ_BCaQ?w6pIbh=g)Pie$@QG? zx;We$keRS7b~-)bVZjqMo^$y4*tjhVG@KqEUa<>E~eR329A&efw*;GWmJ~Q3E085uUXt z34yxJ89;^^o%ECVpn?A07gc1D>ItPSEbKR_2S(t?)#<9GT(IW~NDB9-Xh$wP5C9KA z*sbp+p-!J2w~tW`Bz1K2q!0(!pW2&mxRh#8lPH(`0Z7$1K05I)wO0<_#fm0pgMWml zvWr8v3TLbE1!tiIsOFgyx$)w>^Jy&{y{T?qv-g^)V-rItsS!qTE#|ExUm)o{j>m+o zW325cU{zF|FV}qKne0kCAYWOA@(6P&>4FH#w7ke*Y6NwZl$8A(0F!_lty>;d%LS_} z$RRREqw5v$w%griG5r<^>{Cb|( z1JU7a*~hV8-e-XLl=afQ<{q!>2VZTQa%^0@dw9AgH#vN+p5p!0s#X78+J)F0wcNi5 z9&pdAe0o?lL^VyLLxuXLefC&%=Xmk(0oSmeFkPejxH8|ERieNPYg$n`GVs!Hk@6>z zp$78{Ld?7T2dD#doH39?3-xx>X}qSbpP2T;+!_mh_LLCHN}02g_Cnp0Je<|ZJ}O>m*jqkD;cZy+}0Rg+13jex;N z52&ksT4CDG&xkmmMmMsw>b!<2J3)g+#@M7iJk_`Td;DX8M!U@KSefh#OOg;R3Y5tb zVK94>r|Sd6@!jJ&vH96FiSS{XRT)X(5GGA2u}gMJeg=amY3w)?J0dd&l+zP>nwH;y zsMO25%}}e=@W^@j%P6Mp=M*HXidZZg~`Ghb!8s4c(9w?uOq^0 zuvNoH29NeFQIP=95^>;U1S#qSfcn;D!-6fAX8-}3G@(szsd_NzO3HvNdcmAl-Mmr-&6t|$d@?I^H5fAKlJ01B%2Rn zq06__@9ljjI>a&W?e%z{n>iC^Td&ZJ&=GI)V|?LY;p6K!^UQS`iRsTZ&>vLD&kOsMYb2hChJ$N26`dpsdZG4wEDg1_(&qV zZI+Q*+^$rLvJpR7lvn%vO}cbXwi-KQHP|%{QW7$1C;%wFxLOI;`-l6=+&*I~xx=e+ zPh#1z=~HB?(Q8aK3{=9I1jT*3wt(@^ItVm0$`U8F!vV?b^JI(ytXf?rqo7Yuel2#L zpQ&FiDlaL@CM$hsKSS-EPzfpk4ISVsZt2?)ePE}UxKd*s?~TQDEV(cbR$Egl_UZMs zO~ArE&r_?OkFF6lgYi&Tx&3AetUuGZ(u*S!b|V5TE{B}^Ypdt8Gvz$!8r4mm?pAVR zpG1a20uUi^V8A#h7N~nMaR#|G>_14}!or#O8O?r1fA3F>0|M58>|F!a!nl(PV_S^; z;+0*YSe}eytuv&zNf~8dmsO*<&(TemGWnlkK-@bw%2Ue2z9W`U(X#S3?P578wX^YE zh6qnh936^;s|d`m^@u6Ox($5!L89LCNP2(w1N*|mUgG9ZX8NOFF{u7nxH4Fp)b5Jq z@_EtlX;HHHeQ-7soFOPG47g}klfjnVZ@l$TV1t{|*ew0ba0+W)ODm2GD7yF#OB%vY z*7E}QeImaDjdVGtZ!NZcW&>{+Q{kvUyVB1@WSG)W&)u$>(8A5Km^-V!EQyD8R!4Ft zG9zM7k~|(wzeoXb&{L<|(_5K)w?aF_y4($)*{${m^Qx7(ON|qyab(eqw$rtx;m7j_ zwz%wgRn01GL*_x}Y~4H8npZS?FF|K%pWokfjN)|3-qegg(++hkol?uhTWbg6rX*9@ zGucCQ>1;DX#}VRB8LsbHPTnAdtP_I96oOR4h5%LFeQ(72chU8@`+7X)ap0y$l90~F z0E#mLW$KXM#1})QrIjJblH%&s7{b7GniX0T2oT{VOMb6^zum81y**#&^hQ9t;iqm) zJ1!c+T;O*22m$h#1J8R!(N*{A^y>FZz_w}Vkgq@(04OUF6$M`JvFz#`r=H%!gOOG= zzYyl>NKai~`0NEf(?`)He=>=mBPuBY&&EEL=yE>AfcrfHDco@Stxi9`4f$!MwPFB5}T|H)_O57JS-j?KldccP`Lm;2S}lXweI(s z(PA?+0K@xhj*f{wSP+J*&5fUHXqBp?LONos-ys*Gqvz=!Y!vf+`gqS-$b(_Z3|3od zBwSKK&;*sS3v7LL#dU8`n-GP$4tSm)VzqPN)Bw1|iqMo9%3=3LGkSBWe?ZoD==FuE z7+&?~&s7*g&)zFLZrAId3*@+O9uV{~*kOLLA}$G0?99Y-jn}U6)tHQ+zIacM#7|i3 z{#MOTDU;$2!C8VHO96^zD?NHz7qcoK=8=J<*CtJ7bNk({Cpi?~VN5WXr)Pn4=IeW@ z-;j421Aoe40Wg;+Uf8FsLH)gtK$n+2V8n}V?}0`|Fjrn^6Z@**)bL}~NJziHkv2?X z$nWR*FDz2*x5AIR$M)ML#gD%?2l<`IF^?B1;Q-)sLcwO^L-kvqKfA~V3;TiOya2}?=?@b_o{U~w+{pZ{12+TN0` zMyt5jNvFnniD+FL4MY(4k4wClL9K5{_-u+6_uzOfx!8_A0RYVkoPYSTDdf!f(QG|x zF`|E9B>LDF<;GPye*V@VpvL`nE67YLc!-PcVhXoO4gARJZj3m{e)(hYS*p5V)w6H~ z9l*|mEwQ~X#kh<3;`C0z9UX`dAdP-%&6#9&YGngHN?=rWNW`f$?Uw< z1_5Z3MZ@kh8-mVsCU(SN@OG-MAeG$S{dY3xPjSE&8kWn8k6JKH&ZDuQFajDOZG#}n zB-$e>+~izmL<#hR<*U?V;1f4&hjy~xbL$`XO>J?t!i1(7rOzAV=NYT@tYwd-N*7Qo z&4A5$@W-do+Txhpcb>HVe>SRJVJV>Bx2TFFGXhAS_I>OjlTe8aiA7xsf=~{q%ikck z>snDJsRELlT)ORR)$ikZ$wkzvibhNldtWc8I#*RBC$lK_lcoUT8YYW*cF|^*h^ms| zIT&b|dIb?SB(1`U=%KsF zdZjm&3v(q*3S{twUyu(N2zhXkH7#fS7>DC1VO%WdZIPXpQ%wltwp!HVMprqwuMB#X z^GH=#rPAHL99str;+UJ_^P|UN;%3*cPRvgE>%P@=Hzr%uA13c!QL;O1_acAsI()va zYyYLBnGC^(33k9QdP=+brqt%3HyO8si7<1;+_hF{R1G3#D99wcQm7O>(~dB23qI?N z$j1TmIc1M!eMSOcg(5_rvXn*uOxoua)Ru*O`V|1evh4K@j&m)IT>BB2E5ba=_d#?~ zjJ%2Iq(T@vi~AY@+cjt?0hW>iCdc%E0D8xd+&Hq%zIdfoBj4)|ornb}GQQNlE5P%F zU*3E*#lN_z-{re9$M5A~Wx*wh7gwbM%lgc!nJ*icT^Pwgi;$Q zl4UR*mM2*#hQAw)2U2`%wD5_z@+x&utD9tY&~KA4!Nldm7+e3HW!_f|#PlclrU*2j@CQeDer+EWRAN7fDWzC{;#Pc$yQSP* zCwp};&kI}8xOIB(gAa4KKPHe@h1*gE&;unb8RVoL&RM7n3N8e@gUBR-%NPXt7u&b? zcN_s*4VLYMj)!0YX*b)e?M5ZxCO^6~4es{V<96+913_tVF2*1W?mP*OI`q0Ln_y(M zkPzbVI;!x6;Pqd+IX38m*EH)ih-T&D%s@KMc^&hY`5YvNwqHfzFfd&J&}WVQqc6L| zxo^lS&{gZs_*FOHr_oJIGk*39Zmh-BkW3GVp8$h^`N`Y#KUEADQ*&BJ_u3_0wh!Y{ zFaG2{V_VrR%$RC2xJM4vF*Vc2TD+qoCjK;GeQ;|pJM-&OebKCx$15;!fX8gdM5&q< z>D=H+ea&FOY%S(RI!wtb_fshh_?wD93ZETWHYY6mK;@a4`_~kGn)U6$OjE1%tx+1l zp=P)uQV0UZL}Trx3tt=m3Yegj+~>6T&02@aTwzLM0+#L_pTw0@!i2A5Q1Cer}Kx?h^j>dxO<1{rC;b z_DPbxk}mHtJ%pKakL~F(P~y}5wjfEg1}T3F07zD%E^&_sC@N?9(OE_hR}@7$xSYt( z%$3$*qMAPPpwn(=DFnM;E_Jp@sKC?mj!h{Y58jBE407lg!MJ0B<0aA$z%VofY(Nsv zI2#jUWuoFwuj8cTyr|%Jyl9#Z19>j5NkK9=ci!7EA@ezKVkL)mmxg^J=agc9uG6o| z4lv+CcVmBOEB}a-%88Cu%k4c8#E0YszcIXZ+8{a!lG#3(BcFLQp~gkA_w`2(PZ%5+ zRLK77Ve_8Xerc7xl3l2dJ)21N?)joc-ndnA*g$?%3vvTO(nN+MwF*cj9>sI_J9${_Nd{7oD#7w!z!xOwmMnfogos@Ev*VT zkY*!zI{>iDaq}=o_E;A4teD-e;5Y_t!tRWG!#+&H5R?T|UNOrQRu7)l!%3!zIjk@p zVThL3h0ghjBLm+MRuS5xPssbYNXPAI)81eRs0>)`mQ(8UdA@tWS(+q|1$Xjz+eQ-5u}jEz=iAMP3`C z)h@ozqozXApC`*LuH)AJ=!yS8kvNJH85TWR!WbpMf~z-zB^C6F!BbKU!`+)FNv;5! zS`9nTH_i@;B6`~rOhpuIP#+jz-rG~ag+>MPuA{Iip$cj*6L0--aN)5JD3u%|Jbh>L z9gD(2m5rR6@t@E|g}M;62R#MO|GATcSST#t0bDcc#2V;Xy}MgG$QH|FVCDK3YSsD5 z4}70QT^|~We?9LOS-d>|;S;#Q`%3aHXf9+fbL{bWrui;#>+lw5X_b~`M%g&v^0i=% zOdlvN+i#v3S?-+~iD2lkd-;}5WYMkB{kKl;;Vv|YXkvjg4_{2rt2HaKK5`F!^t^#- zK|s0t83;!OP3qG)yZv~1)HgsjfipYnJf`P5|Lxz^x?10P%JhXA#w745@Zk*qg(m?H z@~=O^>X4oh&{1YFX_Y*}YqzvqCpABV80m(%8A;{>Dvo)9KnluGg_?YkB{H&!fp$B( zD8c&&Siqt%3OIAES5dx9u}>38Q`*zcHFmI_*6=7_fta$IB+zTQFJjg{1YRMO=98E#S&2y$J%jK9JsAN`hduXRgfZ2yqURSSoMcuz2avA6d8 z+c6+2YCUzCdIu~oON(Gs&S4y{PD9m6WO^Jv6fw3QaE#o50Q9;AXcswRd3Vz`f zk~6qU-n>&k%U0@PYO`=J+#Psd0+G|OHM?GSnd)^hA94tRU_~g_e~7Z)Pp$DWSeu_8 z*{rdUyh9&ep9vDBZ{>o)wU?h}%y{Zs5XAR2S8MOb|5|B9(&xd)-plJ;yvhSyN4!e* zv1HdH?$m1kWL;i0#dZ0|-DbHk$LCKb+PS6*-pq8WyD|tkf4sW;kj-U#+Y6*e5AV?+a4?O$U)SD;4ozem;1Gbc3UL3_Sev-H{NaTC z7xKco`k5LrvKVG+me2X{xSL@=_~B+phmaEbkD!q+?`fuJUEIt!r9}>W(aH$dQ>eipx5=T?GR5J}$1{=Qhqc60 zbNRX)Zsr^u}5ip<3+EU#|dcafN(Uzqw@__4TkhBj{|MT0!QD;u&?iS0y zwvM=bGBaTN_ObSNz_>=aWs_(Nh_Bw~V|c&CFPtG|uQxa_$FDL7b^>RA-R02G!jL409x2CE zGnt5^18~53h~wkspWnh1z%pqy=h?&_HM?G8H=sEF&(7lF=P$;OWlf>$CS21;44t^)nmVR+@!Ph8tV)+p4|guQUUMga<;X*Go0Nl2lh( zVw5rSRFY$A;zRw%C`8uGUv{LexgUP)raP`JlM2rpNJryybs`+gvfJb>|0&$1m?#^| z;Wa$AS370oof1 zmc=qhr!*?ohY>bz6!Vp|g#L02_;_F*UV- z%XyoU6VkQ8ru0yS;)~n%KzOitU-R7tLz-I9off7BlC7p~mEgnu%DM)r{N=vZ?@LgV z)o|PBlwp1q{npFy!_^eKVW(BPe@+d8_m7CtCu=>N@Ka-dZ5*RH{IS8u8zKzw&alRf z`kcZYSxSQz*S36-zBft#{xJ5`Dnoh34i_T>q0lnCH!7|K@T|`$1Xtf70JqII zInIy|K!B8xf91$fuswNxoY^~)*A1l&C^hT!@OVq*s<=oZPcR+r7uZmx zn5$&5VTQO^eLubDO+6={=U>z-E?jEe*zOGIqyys)rv4O-Vi zGUnUmz~X0-1E+{^Ghbf<9fi0Fd)Jr$5d;JK{5pM&RN6~rE@xbtmYWK#IdApvTsWGj z{mCa@?q8FZWicAkfhhZl2eQbL*J66IhAcU2Eda8T-rwK%*TZ>F_Hz%tf$8V$%F56B zS*__2aCm)(xh)Ja^)`o`ShKo+%yBxboe?vJ3zWYHJum_^)ACon~6rE`tFI`gdD^ zs`~nNG4cO)KL7i|mZ7*D+FjW?T?AIgZLdxN3VmEst!|mxGQCn=_D`c4N}Q8;BZC#Ti|sCAn(=W=?Feb^-N z@^9Yg?0WU%oc+0!qx7D$f4P^h%pE(@nRTZ-own0@rRK}pm+t3@KG1~pN;4WXhoj0> z45nr$wwtnQ&?dEvj)e`vh=m%}m|*#~1b`9#4geq{Fc{R_Yt12sNFQW1&1O&87!Uy7 zWt5U$E;tEbXw?8+#%>rUyJi>w00000005H3Y)6GOPbvmxsgnAbQWMLu z$HL2DN2!`@XIINoFP)jTwb@2`2coTO!sF~{tCp~%t#a7W*5$Agm!z|}xr*^ehl&Vopc0rMnY+B_c YuM>Iyy%y!u_qQmYzE>H*aVwsO6iwuUWdHyG literal 0 HcmV?d00001