From efc8adb6ddeccc22cc0b35b9a13db25599f5e6bc Mon Sep 17 00:00:00 2001 From: warriorstar-orion Date: Tue, 15 Apr 2025 10:45:56 -0400 Subject: [PATCH] Basic mobs targeting, attacks, and pig migration. (#28987) * Basic mobs targeting, attacks, and pig migration. * run updatepaths * fix duplicate macro def * Update code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> Signed-off-by: warriorstar-orion --------- Signed-off-by: warriorstar-orion Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com> --- .../SpaceRuins/syndie_space_base.dmm | 2 +- _maps/map_files/stations/boxstation.dmm | 2 +- _maps/map_files/stations/cerestation.dmm | 2 +- _maps/map_files/stations/deltastation.dmm | 2 +- _maps/map_files/stations/emeraldstation.dmm | 2 +- _maps/map_files/stations/metastation.dmm | 2 +- code/__DEFINES/ai/blackboard_defines.dm | 26 +++ code/__DEFINES/dcs/ai_signals.dm | 1 + code/__DEFINES/dcs/atom_signals.dm | 9 + code/__DEFINES/dcs/attack_chain_signals.dm | 9 + code/__DEFINES/dcs/mob_signals.dm | 6 - code/__HELPERS/trait_helpers.dm | 10 ++ code/_globalvars/traits.dm | 4 +- code/_onclick/other_mobs.dm | 19 ++- code/datums/ai/ai_planning_subtree.dm | 9 + .../basic_ai_behaviors/basic_attacking.dm | 147 ++++++++++++++++ .../basic_ai_behaviors/nearest_targeting.dm | 13 ++ .../run_away_from_target.dm | 75 +++++++++ .../basic_ai_behaviors/targeting.dm | 158 ++++++++++++++++++ .../basic_mobs/basic_subtrees/flee_target.dm | 25 +++ .../basic_subtrees/random_speech.dm | 8 - .../basic_subtrees/simple_attack_target.dm | 13 ++ .../simple_find_nearest_target_to_flee.dm | 25 +++ .../basic_subtrees/target_retaliate.dm | 80 +++++++++ .../components/ai_retaliate_advanced.dm | 37 ++++ code/datums/elements/ai_flee_while_injured.dm | 46 +++++ code/datums/elements/ai_retaliate.dm | 27 +++ code/datums/elements/relay_attackers.dm | 95 +++++++++++ code/datums/proximity/ai_target_tracking.dm | 113 +++++++++++++ .../structures/crates_lockers/crittercrate.dm | 4 +- code/modules/mob/living/basic/basic_mob.dm | 37 ++++ .../modules/mob/living/basic/{cow => }/cow.dm | 25 ++- code/modules/mob/living/basic/cow/cow_ai.dm | 15 -- code/modules/mob/living/basic/pig.dm | 50 ++++++ .../mob/living/carbon/human/human_defense.dm | 15 ++ code/modules/mob/living/living_defense.dm | 20 +++ .../simple_animal/friendly/farm_animals.dm | 26 --- code/modules/projectiles/projectile_base.dm | 17 ++ code/tests/game_tests.dm | 1 + .../test_ensure_subtree_operational_datum.dm | 64 +++++++ paradise.dme | 17 +- sound/creatures/pig/pig1.ogg | Bin 0 -> 5690 bytes sound/creatures/pig/pig2.ogg | Bin 0 -> 7953 bytes tools/UpdatePaths/Scripts/28987_pig.txt | 1 + 44 files changed, 1190 insertions(+), 69 deletions(-) create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm create mode 100644 code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm create mode 100644 code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm create mode 100644 code/datums/components/ai_retaliate_advanced.dm create mode 100644 code/datums/elements/ai_flee_while_injured.dm create mode 100644 code/datums/elements/ai_retaliate.dm create mode 100644 code/datums/elements/relay_attackers.dm create mode 100644 code/datums/proximity/ai_target_tracking.dm rename code/modules/mob/living/basic/{cow => }/cow.dm (78%) delete mode 100644 code/modules/mob/living/basic/cow/cow_ai.dm create mode 100644 code/modules/mob/living/basic/pig.dm create mode 100644 code/tests/test_ensure_subtree_operational_datum.dm create mode 100644 sound/creatures/pig/pig1.ogg create mode 100644 sound/creatures/pig/pig2.ogg create mode 100644 tools/UpdatePaths/Scripts/28987_pig.txt diff --git a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm index c27e2caa9bf..8280c2b1f60 100644 --- a/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm +++ b/_maps/map_files/RandomRuins/SpaceRuins/syndie_space_base.dmm @@ -6851,7 +6851,7 @@ /turf/simulated/floor/plating, /area/ruin/unpowered/syndicate_space_base/engineering) "OI" = ( -/mob/living/simple_animal/pig{ +/mob/living/basic/pig{ faction = list("neutral","syndicate") }, /turf/simulated/floor/grass, diff --git a/_maps/map_files/stations/boxstation.dmm b/_maps/map_files/stations/boxstation.dmm index 4c083a45157..3e6dc065932 100644 --- a/_maps/map_files/stations/boxstation.dmm +++ b/_maps/map_files/stations/boxstation.dmm @@ -25807,7 +25807,7 @@ /obj/machinery/atmospherics/pipe/simple/hidden/scrubbers{ dir = 4 }, -/mob/living/simple_animal/pig, +/mob/living/basic/pig, /turf/simulated/floor/grass, /area/station/service/hydroponics) "bLo" = ( diff --git a/_maps/map_files/stations/cerestation.dmm b/_maps/map_files/stations/cerestation.dmm index 996de425ca5..ed8eaf0206f 100644 --- a/_maps/map_files/stations/cerestation.dmm +++ b/_maps/map_files/stations/cerestation.dmm @@ -34931,7 +34931,7 @@ /turf/simulated/floor/plating, /area/station/maintenance/fore) "fPp" = ( -/mob/living/simple_animal/pig, +/mob/living/basic/pig, /turf/simulated/floor/grass, /area/station/service/hydroponics) "fPv" = ( diff --git a/_maps/map_files/stations/deltastation.dmm b/_maps/map_files/stations/deltastation.dmm index 4c22039fbe1..777cef7316f 100644 --- a/_maps/map_files/stations/deltastation.dmm +++ b/_maps/map_files/stations/deltastation.dmm @@ -78439,7 +78439,7 @@ }, /area/station/legal/courtroom/gallery) "qjc" = ( -/mob/living/simple_animal/pig, +/mob/living/basic/pig, /turf/simulated/floor/grass, /area/station/service/kitchen) "qjH" = ( diff --git a/_maps/map_files/stations/emeraldstation.dmm b/_maps/map_files/stations/emeraldstation.dmm index 5262ce150fa..39ce57ef80e 100644 --- a/_maps/map_files/stations/emeraldstation.dmm +++ b/_maps/map_files/stations/emeraldstation.dmm @@ -120291,7 +120291,7 @@ /turf/simulated/floor/plating, /area/station/maintenance/dorms/starboard) "xYw" = ( -/mob/living/simple_animal/pig, +/mob/living/basic/pig, /turf/simulated/floor/grass, /area/station/service/kitchen) "xYF" = ( diff --git a/_maps/map_files/stations/metastation.dmm b/_maps/map_files/stations/metastation.dmm index 2df5e986f53..58b6f5420f6 100644 --- a/_maps/map_files/stations/metastation.dmm +++ b/_maps/map_files/stations/metastation.dmm @@ -80496,7 +80496,7 @@ }, /area/station/science/xenobiology) "uXs" = ( -/mob/living/simple_animal/pig, +/mob/living/basic/pig, /turf/simulated/floor/grass, /area/station/service/hydroponics) "uXA" = ( diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm index 6d48c601acc..ce749d8c5a5 100644 --- a/code/__DEFINES/ai/blackboard_defines.dm +++ b/code/__DEFINES/ai/blackboard_defines.dm @@ -6,6 +6,17 @@ #define BB_CURRENT_MIN_MOVE_DISTANCE "BB_min_move_distance" /// How likely is this mob to move when idle per tick? #define BB_BASIC_MOB_IDLE_WALK_CHANCE "BB_basic_idle_walk_chance" +/// Flag to set on if you want your mob to STOP running away +#define BB_BASIC_MOB_STOP_FLEEING "BB_basic_stop_fleeing" +/// Key of something we're running away from +#define BB_BASIC_MOB_FLEE_TARGET "BB_basic_flee_target" +/// Key for the current hidden location of something we're running away from +#define BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION "BB_basic_flee_target_hiding_location" +/// Key defining the targeting strategy for things to flee from +#define BB_FLEE_TARGETING_STRATEGY "BB_flee_targeting_strategy" +/// Key defining how far we attempt to get away from something we're fleeing from +#define BB_BASIC_MOB_FLEE_DISTANCE "BB_basic_flee_distance" +#define DEFAULT_BASIC_FLEE_DISTANCE 9 // Searching @@ -14,12 +25,22 @@ // Targeting +/// How close a mob must be for us to select it as a target, if that is less than how far we can maintain it as a target +#define BB_AGGRO_RANGE "BB_aggro_range" +/// Key for our current target. +#define BB_BASIC_MOB_CURRENT_TARGET "BB_basic_current_target" +/// Key for the current hidden location of our target if applicable. +#define BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION "BB_basic_current_target_hiding_location" +/// Key for the type of targeting strategy we will apply. +#define BB_TARGETING_STRATEGY "targeting_strategy" /// Key for the minimum status at which we want to target mobs (does not need to be specified if CONSCIOUS) #define BB_TARGET_MINIMUM_STAT "BB_target_minimum_stat" /// Should we skip the faction check for the targeting strategy? #define BB_ALWAYS_IGNORE_FACTION "BB_always_ignore_factions" /// Are we in some kind of temporary state of ignoring factions when targeting? can result in volatile results if multiple behaviours touch this #define BB_TEMPORARILY_IGNORE_FACTION "BB_temporarily_ignore_factions" +///List of mobs who have damaged us +#define BB_BASIC_MOB_RETALIATE_LIST "BB_basic_mob_shitlist" // Food and eating @@ -40,3 +61,8 @@ #define BB_BASIC_MOB_TIP_REACTING "BB_basic_tip_reacting" /// the motherfucker who tipped us #define BB_BASIC_MOB_TIPPER "BB_basic_tip_tipper" + +// Misc + +/// For /datum/ai_behavior/find_potential_targets, what if any field are we using currently +#define BB_FIND_TARGETS_FIELD(type) "bb_find_targets_field_[type]" diff --git a/code/__DEFINES/dcs/ai_signals.dm b/code/__DEFINES/dcs/ai_signals.dm index b6a3ce5e399..b9d52316d7c 100644 --- a/code/__DEFINES/dcs/ai_signals.dm +++ b/code/__DEFINES/dcs/ai_signals.dm @@ -12,6 +12,7 @@ //from base of atom/attack_basic_mob(): (/mob/user) #define COMSIG_ATOM_ATTACK_BASIC_MOB "attack_basic_mob" + #define COMPONENT_BASIC_ATTACK_CANCEL_CHAIN (1<<0) ///sent from ai controllers when they possess a pawn: (datum/ai_controller/source_controller) #define COMSIG_AI_CONTROLLER_POSSESSED_PAWN "ai_controller_possessed_pawn" diff --git a/code/__DEFINES/dcs/atom_signals.dm b/code/__DEFINES/dcs/atom_signals.dm index a1fc6449a75..39bca29e114 100644 --- a/code/__DEFINES/dcs/atom_signals.dm +++ b/code/__DEFINES/dcs/atom_signals.dm @@ -98,6 +98,15 @@ #define COMSIG_ATOM_PREHIT "atom_prehit" #define ATOM_PREHIT_SUCCESS (1<<0) #define ATOM_PREHIT_FAILURE (1<<1) +///from relay_attackers element: (atom/attacker, attack_flags) +#define COMSIG_ATOM_ATTACK_ANIMAL "atom_attack_animal" +#define COMSIG_ATOM_WAS_ATTACKED "atom_was_attacked" + ///The damage type of the weapon projectile is non-lethal stamina + #define ATTACKER_STAMINA_ATTACK (1<<0) + ///the attacker is shoving the source + #define ATTACKER_SHOVING (1<<1) + /// The attack is a damaging-type attack + #define ATTACKER_DAMAGING_ATTACK (1<<2) /// Called from atom/Initialize() of target: (atom/target) #define COMSIG_ATOM_INITIALIZED_ON "atom_initialized_on" diff --git a/code/__DEFINES/dcs/attack_chain_signals.dm b/code/__DEFINES/dcs/attack_chain_signals.dm index 64cbef3243a..51210b9074d 100644 --- a/code/__DEFINES/dcs/attack_chain_signals.dm +++ b/code/__DEFINES/dcs/attack_chain_signals.dm @@ -47,6 +47,15 @@ #define COMSIG_AFTER_ATTACK "item_after_attack" //! [/obj/item/proc/afterattack] -> atom/target, mob/user, params #define COMSIG_AFTER_ATTACKED_BY "after_attacked_by" //! [/obj/item/proc/afterattack] -> obj/item/weapon, mob/user, proximity_flag, params +/// Before attackingtarget has happened, source is the attacker and target is the attacked +#define COMSIG_HOSTILE_PRE_ATTACKINGTARGET "hostile_pre_attackingtarget" + #define COMPONENT_HOSTILE_NO_ATTACK COMPONENT_CANCEL_ATTACK_CHAIN //cancel the attack, only works before attack happens +#define COMSIG_HOSTILE_ATTACKINGTARGET "hostile_attackingtarget" +/// Called when a /mob/living/simple_animal/hostile fines a new target: (atom/source, give_target) +#define COMSIG_HOSTILE_FOUND_TARGET "comsig_hostile_found_target" +/// After attackingtarget has happened, source is the attacker and target is the attacked, extra argument for if the attackingtarget was successful +#define COMSIG_HOSTILE_POST_ATTACKINGTARGET "hostile_post_attackingtarget" + // Return values for directing the control of the attack chain. Distinct from // signal interceptors because they're not meant to be combined, and to mesh better with // historical use of return values in attack chain procs. diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm index b978c2dcde8..a92233a371c 100644 --- a/code/__DEFINES/dcs/mob_signals.dm +++ b/code/__DEFINES/dcs/mob_signals.dm @@ -188,12 +188,6 @@ ///from /mob/living/simple_animal/handle_environment() #define COMSIG_SIMPLEANIMAL_HANDLE_ENVIRONMENT "simpleanimal_handle_environment" -// /mob/living/simple_animal/hostile signals -#define COMSIG_HOSTILE_ATTACKINGTARGET "hostile_attackingtarget" - #define COMPONENT_HOSTILE_NO_ATTACK (1<<0) -//Called when a /mob/living/simple_animal/hostile fines a new target: (atom/source, give_target) -#define COMSIG_HOSTILE_FOUND_TARGET "comsig_hostile_found_target" - ///from of mob/MouseDrop(): (/atom/over, /mob/user) #define COMSIG_DO_MOB_STRIP "do_mob_strip" diff --git a/code/__HELPERS/trait_helpers.dm b/code/__HELPERS/trait_helpers.dm index 054eccfbc72..b28856ea2f3 100644 --- a/code/__HELPERS/trait_helpers.dm +++ b/code/__HELPERS/trait_helpers.dm @@ -130,6 +130,9 @@ #define HAS_MIND_TRAIT(target, trait) (istype(target, /datum/mind) ? HAS_TRAIT(target, trait) : (target.mind ? HAS_TRAIT(target.mind, trait) : FALSE)) /// Gives a unique trait source for any given datum #define UNIQUE_TRAIT_SOURCE(target) "unique_source_[target.UID()]" +/// Returns a list of trait sources for this trait. Only useful for wacko cases and internal futzing +/// You should not be using this +#define GET_TRAIT_SOURCES(target, trait) (target.status_traits?[trait] || list()) /* Remember to update _globalvars/traits.dm if you're adding/removing/renaming traits. @@ -326,6 +329,13 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// A general trait for tracking whether a zombie owned the organ or limb #define TRAIT_I_WANT_BRAINS_ORGAN "zombie_organ" + +/// Trait given by /datum/element/relay_attacker +#define TRAIT_RELAYING_ATTACKER "relaying_attacker" + +/// Trait applied to a mob when it gets a required "operational datum" (components/elements). Sends out the source as the type of the element. +#define TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM "element-required" + //****** OBJ TRAITS *****// ///An /obj that should not increase the "depth" of the search for adjacency, diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm index 397cda59005..9f41c3c43c3 100644 --- a/code/_globalvars/traits.dm +++ b/code/_globalvars/traits.dm @@ -113,7 +113,9 @@ GLOBAL_LIST_INIT(traits_by_type, list( "TRAIT_ANTIMAGIC_NO_SELFBLOCK" = TRAIT_ANTIMAGIC_NO_SELFBLOCK, "TRAIT_RECENTLY_BLOCKED_MAGIC" = TRAIT_RECENTLY_BLOCKED_MAGIC, "TRAIT_UNKNOWN" = TRAIT_UNKNOWN, - "TRAIT_EXAMINE_HALLUCINATING" = TRAIT_EXAMINE_HALLUCINATING + "TRAIT_EXAMINE_HALLUCINATING" = TRAIT_EXAMINE_HALLUCINATING, + "TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM" = TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, + "TRAIT_RELAYING_ATTACKER" = TRAIT_RELAYING_ATTACKER, ), /datum/mind = list( diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index 364cdee730d..552c4353513 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -66,14 +66,29 @@ if(sigreturn & COMPONENT_SKIP_ATTACK) return FALSE - target.attack_animal(src) + resolve_unarmed_attack(target, modifiers) /mob/living/simple_animal/hostile/UnarmedAttack(atom/A) target = A AttackingTarget() +/mob/living/proc/resolve_unarmed_attack(atom/attack_target, list/modifiers) + attack_target.attack_animal(src, modifiers) + /atom/proc/attack_animal(mob/user) - return + SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_ANIMAL, user) + +///When a basic mob attacks something, either by AI or user. +/atom/proc/attack_basic_mob(mob/user, list/modifiers) + SHOULD_CALL_PARENT(TRUE) + if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_BASIC_MOB, user) & COMPONENT_BASIC_ATTACK_CANCEL_CHAIN) + return + return handle_basic_attack(user, modifiers) //return value of attack animal, this is how much damage was dealt to the attacked thing + +///This exists so stuff can override the default call of attack_animal for attack_basic_mob +///Remove this when simple animals are removed and everything can be handled on attack basic mob. +/atom/proc/handle_basic_attack(user, modifiers) + return attack_animal(user, modifiers) /mob/living/RestrainedClickOn(atom/A) return diff --git a/code/datums/ai/ai_planning_subtree.dm b/code/datums/ai/ai_planning_subtree.dm index 4ab224cd8d7..10e45a49257 100644 --- a/code/datums/ai/ai_planning_subtree.dm +++ b/code/datums/ai/ai_planning_subtree.dm @@ -3,6 +3,15 @@ /// subtype and modify select_behaviors() without needing to subtype the AI /// controller itself. /datum/ai_planning_subtree + /// A list of typepaths of "operational datums" (elements/components) we + /// absolutely NEED to run. Checked in unit tests, as well as be a nice + /// reminder to developers that such a thing might be needed. Note that in + /// the Attach/Inititalize/New (or any future equivalent for these procs), + /// you will need to add the trait TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM + /// to the mob in question in order for unit tests to succeed. This will + /// break obviously enough if you don't do this and declare the required + /// datum here however. + var/list/operational_datums = null /// Determines what behaviors should the controller try processing; if this /// returns SUBTREE_RETURN_FINISH_PLANNING then the controller won't go through diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm new file mode 100644 index 00000000000..e32c8135a23 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm @@ -0,0 +1,147 @@ +/datum/ai_behavior/basic_melee_attack + action_cooldown = 0.2 SECONDS // We gotta check unfortunately often because we're in a race condition with nextmove + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + ///do we finish this action after hitting once? + var/terminate_after_action = FALSE + +/datum/ai_behavior/basic_melee_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + if(!controller.blackboard[targeting_strategy_key]) + CRASH("No targeting strategy was supplied in the blackboard for [controller.pawn]") + //Hiding location is priority + var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + + set_movement_target(controller, target) + +/datum/ai_behavior/basic_melee_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + if(isliving(controller.pawn)) + var/mob/living/pawn = controller.pawn + if(world.time < pawn.next_move) + return AI_BEHAVIOR_INSTANT + + var/mob/living/basic/basic_mob = controller.pawn + //targeting strategy will kill the action if not real anymore + var/atom/target = controller.blackboard[target_key] + var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key]) + + if(!targeting_strategy.can_attack(basic_mob, target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something! + + controller.set_blackboard_key(hiding_location_key, hiding_target) + + var/atom/final_target = hiding_target || target + controller.ai_interact(target = final_target, intent = INTENT_HARM) + if(terminate_after_action) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_DELAY + +/datum/ai_behavior/basic_melee_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + if(!succeeded) + controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/basic_melee_attack/interact_once + terminate_after_action = TRUE + +/datum/ai_behavior/basic_melee_attack/interact_once/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/basic_ranged_attack + action_cooldown = 0.6 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + required_distance = 3 + /// range we will try chasing the target before giving up + var/chase_range = 9 + ///do we care about avoiding friendly fire? + var/avoid_friendly_fire = FALSE + +/datum/ai_behavior/basic_ranged_attack/setup(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + if(HAS_TRAIT(controller.pawn, TRAIT_HANDS_BLOCKED)) + return FALSE + var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/basic_ranged_attack/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + var/mob/living/basic/basic_mob = controller.pawn + //targeting strategy will kill the action if not real anymore + var/atom/target = controller.blackboard[target_key] + var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key]) + + if(!targeting_strategy.can_attack(basic_mob, target, chase_range)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + + var/atom/hiding_target = targeting_strategy.find_hidden_mobs(basic_mob, target) //If this is valid, theyre hidden in something! + var/atom/final_target = hiding_target ? hiding_target : target + + if(!can_see(basic_mob, final_target, required_distance)) + return AI_BEHAVIOR_INSTANT + + if(avoid_friendly_fire && check_friendly_in_path(basic_mob, target, targeting_strategy)) + adjust_position(basic_mob, target) + return AI_BEHAVIOR_DELAY + + controller.set_blackboard_key(hiding_location_key, hiding_target) + basic_mob.RangedAttack(final_target) + return AI_BEHAVIOR_DELAY //only start the cooldown when the shot is shot + +/datum/ai_behavior/basic_ranged_attack/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + if(!succeeded) + controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/basic_ranged_attack/proc/check_friendly_in_path(mob/living/source, atom/target, datum/targeting_strategy/targeting_strategy) + var/list/turfs_list = calculate_trajectory(source, target) + for(var/turf/possible_turf as anything in turfs_list) + + for(var/mob/living/potential_friend in possible_turf) + if(!targeting_strategy.can_attack(source, potential_friend)) + return TRUE + + return FALSE + +/datum/ai_behavior/basic_ranged_attack/proc/adjust_position(mob/living/living_pawn, atom/target) + var/turf/our_turf = get_turf(living_pawn) + var/list/possible_turfs = list() + + for(var/direction in GLOB.alldirs) + var/turf/target_turf = get_step(our_turf, direction) + if(isnull(target_turf)) + continue + if(is_blocked_turf(target_turf) || get_dist(target_turf, target) > get_dist(living_pawn, target)) + continue + possible_turfs += target_turf + + if(!length(possible_turfs)) + return + var/turf/picked_turf = get_closest_atom(/turf, possible_turfs, target) + step(living_pawn, get_dir(living_pawn, picked_turf)) + +/datum/ai_behavior/basic_ranged_attack/proc/calculate_trajectory(mob/living/source , atom/target) + var/list/turf_list = get_line(source, target) + var/list_length = length(turf_list) - 1 + for(var/i in 1 to list_length) + var/turf/current_turf = turf_list[i] + var/turf/next_turf = turf_list[i + 1] + var/direction_to_turf = get_dir(current_turf, next_turf) + if(!IS_DIR_DIAGONAL(direction_to_turf)) + continue + + for(var/cardinal_direction in GLOB.cardinal) + if(cardinal_direction & direction_to_turf) + turf_list += get_step(current_turf, cardinal_direction) + + turf_list -= get_turf(source) + turf_list -= get_turf(target) + + return turf_list + +/datum/ai_behavior/basic_ranged_attack/avoid_friendly_fire + avoid_friendly_fire = TRUE diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm new file mode 100644 index 00000000000..8a570375cba --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/nearest_targeting.dm @@ -0,0 +1,13 @@ +/// Picks targets based on which one is closest to you, choice between targets at equal distance is arbitrary +/datum/ai_behavior/find_potential_targets/nearest + +/datum/ai_behavior/find_potential_targets/nearest/pick_final_target(datum/ai_controller/controller, list/filtered_targets) + var/turf/our_position = get_turf(controller.pawn) + return get_closest_atom(/atom/, filtered_targets, our_position) + +/// As above but targets have been filtered from the 'retaliate' blackboard list +/datum/ai_behavior/target_from_retaliate_list/nearest + +/datum/ai_behavior/target_from_retaliate_list/nearest/pick_final_target(datum/ai_controller/controller, list/enemies_list) + var/turf/our_position = get_turf(controller.pawn) + return get_closest_atom(/atom/, enemies_list, our_position) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm new file mode 100644 index 00000000000..09958cd987c --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/run_away_from_target.dm @@ -0,0 +1,75 @@ +/// Move to a position further away from your current target +/datum/ai_behavior/run_away_from_target + required_distance = 0 + action_cooldown = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + /// How far do we try to run? Further makes for smoother running, but potentially weirder pathfinding + var/run_distance = DEFAULT_BASIC_FLEE_DISTANCE + /// Clear target if we finish the action unsuccessfully + var/clear_failed_targets = TRUE + +/datum/ai_behavior/run_away_from_target/setup(datum/ai_controller/controller, target_key, hiding_location_key) + var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + run_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || initial(run_distance) + if(!plot_path_away_from(controller, target)) + return FALSE + return ..() + +/datum/ai_behavior/run_away_from_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key) + if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING]) + return AI_BEHAVIOR_DELAY + var/atom/target = controller.blackboard[hiding_location_key] || controller.blackboard[target_key] + if(QDELETED(target) || !can_see(controller.pawn, target, run_distance)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + if(get_dist(controller.pawn, controller.current_movement_target) > required_distance) + return AI_BEHAVIOR_DELAY // Still heading over + if(plot_path_away_from(controller, target)) + return AI_BEHAVIOR_DELAY + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + +/datum/ai_behavior/run_away_from_target/proc/plot_path_away_from(datum/ai_controller/controller, atom/target) + var/turf/target_destination = get_turf(controller.pawn) + var/static/list/offset_angles = list(45, 90, 135, 180, 225, 270) + for(var/angle in offset_angles) + var/turf/test_turf = get_furthest_turf(controller.pawn, angle, target) + if(isnull(test_turf)) + continue + var/distance_from_target = get_dist(target, test_turf) + if(distance_from_target <= get_dist(target, target_destination)) + continue + target_destination = test_turf + if(distance_from_target == run_distance) //we already got the max running distance + break + + if(target_destination == get_turf(controller.pawn)) + return FALSE + set_movement_target(controller, target_destination) + return TRUE + +/datum/ai_behavior/run_away_from_target/proc/get_furthest_turf(atom/source, angle, atom/target) + var/turf/return_turf + for(var/i in 1 to run_distance) + var/turf/test_destination = get_ranged_target_turf_direct(source, target, range = i, offset = angle) + if(is_blocked_turf(test_destination, excluded_objs = GLOB.airlocks + src)) + break + return_turf = test_destination + return return_turf + +/datum/ai_behavior/run_away_from_target/finish_action(datum/ai_controller/controller, succeeded, target_key, hiding_location_key) + . = ..() + if(clear_failed_targets) + controller.clear_blackboard_key(target_key) + +/datum/ai_behavior/run_away_from_target/run_and_shoot + clear_failed_targets = FALSE + +/datum/ai_behavior/run_away_from_target/run_and_shoot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, hiding_location_key) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/mob/living/living_pawn = controller.pawn + living_pawn.RangedAttack(target) + return ..() + diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm new file mode 100644 index 00000000000..637f15c1334 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/targeting.dm @@ -0,0 +1,158 @@ +/// List of objects that AIs will treat as targets +GLOBAL_LIST_EMPTY_TYPED(hostile_machines, /atom) +/// Static typecache list of things we are interested in +/// Consider this a union of the for loop and the hearers call from below +/// Must be kept up to date with the contents of hostile_machines +GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list( + /mob, + /obj/machinery/porta_turret, + /obj/mecha, +))) + +/datum/ai_behavior/find_potential_targets + action_cooldown = 2 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + /// How far can we see stuff? + var/vision_range = 9 + /// Blackboard key for aggro range, uses vision range if not specified + var/aggro_range_key = BB_AGGRO_RANGE + +/datum/ai_behavior/find_potential_targets/get_cooldown(datum/ai_controller/cooldown_for) + if(cooldown_for.blackboard[BB_FIND_TARGETS_FIELD(type)]) + return 60 SECONDS + return ..() + +/datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + var/mob/living/living_mob = controller.pawn + var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key]) + + if(!targeting_strategy) + CRASH("No target datum was supplied in the blackboard for [controller.pawn]") + + var/atom/current_target = controller.blackboard[target_key] + if(targeting_strategy.can_attack(living_mob, current_target, vision_range)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/aggro_range = controller.blackboard[aggro_range_key] || vision_range + + controller.clear_blackboard_key(target_key) + + // If we're using a field rn, just don't do anything yeah? + if(controller.blackboard[BB_FIND_TARGETS_FIELD(type)]) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/list/potential_targets = hearers(aggro_range, get_turf(controller.pawn)) - living_mob //Remove self, so we don't suicide + + for(var/atom/hostile_machine as anything in GLOB.hostile_machines) + if(can_see(living_mob, hostile_machine, aggro_range)) + potential_targets += hostile_machine + + if(!length(potential_targets)) + failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/list/filtered_targets = list() + + for(var/atom/pot_target in potential_targets) + if(targeting_strategy.can_attack(living_mob, pot_target))//Can we attack it? + filtered_targets += pot_target + continue + + if(!length(filtered_targets)) + failed_to_find_anyone(controller, target_key, targeting_strategy_key, hiding_location_key) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/atom/target = pick_final_target(controller, filtered_targets) + controller.set_blackboard_key(target_key, target) + + var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, target) + + if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially. + controller.set_blackboard_key(hiding_location_key, potential_hiding_location) + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/find_potential_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + var/aggro_range = controller.blackboard[aggro_range_key] || vision_range + // takes the larger between our range() input and our implicit hearers() input (world.view) + aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2)) + // Alright, here's the interesting bit + // We're gonna use this max range to hook into a proximity field so we can just await someone interesting to come along + // Rather then trying to check every few seconds + var/datum/proximity_monitor/advanced/ai_target_tracking/detection_field = new( + controller.pawn, + aggro_range, + TRUE, + src, + controller, + target_key, + targeting_strategy_key, + hiding_location_key, + ) + // We're gonna store this field in our blackboard, so we can clear it away if we end up finishing successsfully + controller.set_blackboard_key(BB_FIND_TARGETS_FIELD(type), detection_field) + +/datum/ai_behavior/find_potential_targets/proc/new_turf_found(turf/found, datum/ai_controller/controller, datum/targeting_strategy/strategy) + var/valid_found = FALSE + var/mob/pawn = controller.pawn + for(var/maybe_target as anything in found) + if(maybe_target == pawn) + continue + if(!is_type_in_typecache(maybe_target, GLOB.target_interested_atoms)) + continue + if(!strategy.can_attack(pawn, maybe_target)) + continue + valid_found = TRUE + break + if(!valid_found) + return + // If we found any one thing we "could" attack, then run the full search again so we can select from the best possible canidate + var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] + qdel(field) // autoclears so it's fine + // Fire instantly, you should find something I hope + controller.modify_cooldown(src, world.time) + +/datum/ai_behavior/find_potential_targets/proc/atom_allowed(atom/movable/checking, datum/targeting_strategy/strategy, mob/pawn) + if(checking == pawn) + return FALSE + if(!ismob(checking) && !is_type_in_typecache(checking, GLOB.target_interested_atoms)) + return FALSE + if(!strategy.can_attack(pawn, checking)) + return FALSE + return TRUE + +/datum/ai_behavior/find_potential_targets/proc/new_atoms_found(list/atom/movable/found, datum/ai_controller/controller, target_key, datum/targeting_strategy/strategy, hiding_location_key) + var/mob/pawn = controller.pawn + var/list/accepted_targets = list() + for(var/maybe_target as anything in found) + if(maybe_target == pawn) + continue + // Need to better handle viewers here + if(!ismob(maybe_target) && !is_type_in_typecache(maybe_target, GLOB.target_interested_atoms)) + continue + if(!strategy.can_attack(pawn, maybe_target)) + continue + accepted_targets += maybe_target + + // Alright, we found something acceptable, let's use it yeah? + var/atom/target = pick_final_target(controller, accepted_targets) + controller.set_blackboard_key(target_key, target) + + var/atom/potential_hiding_location = strategy.find_hidden_mobs(pawn, target) + + if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially. + controller.set_blackboard_key(hiding_location_key, potential_hiding_location) + + finish_action(controller, succeeded = TRUE) + +/datum/ai_behavior/find_potential_targets/finish_action(datum/ai_controller/controller, succeeded, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + if(succeeded) + var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] + qdel(field) // autoclears so it's fine + controller.cancel_actions() // On retarget cancel any further queued actions so that they will setup again with new target + controller.modify_cooldown(src, get_cooldown(controller)) + +/// Returns the desired final target from the filtered list of targets +/datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets) + return pick(filtered_targets) diff --git a/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm new file mode 100644 index 00000000000..e0762fccde5 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/flee_target.dm @@ -0,0 +1,25 @@ +/// Try to escape from your current target, without performing any other actions. +/datum/ai_planning_subtree/flee_target + /// Behaviour to execute in order to flee + var/flee_behaviour = /datum/ai_behavior/run_away_from_target + /// Blackboard key in which to store selected target + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// Blackboard key in which to store selected target's hiding place + var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION + +/datum/ai_planning_subtree/flee_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/atom/flee_from = controller.blackboard[target_key] + if(!should_flee(controller, flee_from)) + return + var/flee_distance = controller.blackboard[BB_BASIC_MOB_FLEE_DISTANCE] || DEFAULT_BASIC_FLEE_DISTANCE + if(get_dist(controller.pawn, flee_from) >= flee_distance) + return + + controller.queue_behavior(flee_behaviour, target_key, hiding_place_key) + return SUBTREE_RETURN_FINISH_PLANNING //we gotta get out of here. + +/datum/ai_planning_subtree/flee_target/proc/should_flee(datum/ai_controller/controller, atom/flee_from) + if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING] || QDELETED(flee_from)) + return FALSE + return TRUE diff --git a/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm b/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm index b2084f56002..faaf5d458db 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/random_speech.dm @@ -53,11 +53,3 @@ if(length(speak_verbs)) speak_verb = pick(speak_verbs) controller.queue_behavior(/datum/ai_behavior/perform_speech, pick(speak), sound_to_play, speak_verb) - -/datum/ai_planning_subtree/random_speech/cow - speech_chance = 2 - speak = list("Moo?", "Moo", "MOOOOOO") - speak_verbs = list("moos", "moos hauntingly") - sound = list('sound/creatures/cow.ogg') - emote_hear = list("brays.") - emote_see = list("shakes her head.") diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm new file mode 100644 index 00000000000..e5901af13c9 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm @@ -0,0 +1,13 @@ +/datum/ai_planning_subtree/basic_melee_attack_subtree + /// What do we do in order to attack someone? + var/datum/ai_behavior/basic_melee_attack/melee_attack_behavior = /datum/ai_behavior/basic_melee_attack + /// Is this the last thing we do? (if we set a movement target, this will usually be yes) + var/end_planning = TRUE + +/datum/ai_planning_subtree/basic_melee_attack_subtree/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + if(!controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET)) + return + controller.queue_behavior(melee_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + if(end_planning) + return SUBTREE_RETURN_FINISH_PLANNING // we are going into battle...no distractions. diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm new file mode 100644 index 00000000000..f698ac7838e --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_nearest_target_to_flee.dm @@ -0,0 +1,25 @@ +/// Find the nearest thing which we assume is hostile and set it as the flee target +/datum/ai_planning_subtree/simple_find_nearest_target_to_flee + +/datum/ai_planning_subtree/simple_find_nearest_target_to_flee/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING]) + return + controller.queue_behavior(/datum/ai_behavior/find_potential_targets/nearest, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + +/// Find the nearest thing on our list of 'things which have done damage to me' and set it as the flee target +/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee + ///the targeting strategy we use + var/targeting_key = BB_TARGETING_STRATEGY + ///what key should we set the target as + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + +/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + if(controller.blackboard[BB_BASIC_MOB_STOP_FLEEING]) + return + controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list/nearest, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_key, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + +/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee/from_flee_key + target_key = BB_BASIC_MOB_FLEE_TARGET + targeting_key = BB_FLEE_TARGETING_STRATEGY diff --git a/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm new file mode 100644 index 00000000000..42a4e70081c --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/target_retaliate.dm @@ -0,0 +1,80 @@ +/// Sets the BB target to a mob which you can see and who has recently attacked you +/datum/ai_planning_subtree/target_retaliate + operational_datums = list(/datum/element/ai_retaliate, /datum/component/ai_retaliate_advanced) + /// Blackboard key which tells us how to select valid targets + var/targeting_strategy_key = BB_TARGETING_STRATEGY + /// Blackboard key in which to store selected target + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + /// Blackboard key in which to store selected target's hiding place + var/hiding_place_key = BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION + /// do we check for faction? + var/check_faction = FALSE + +/datum/ai_planning_subtree/target_retaliate/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + controller.queue_behavior(/datum/ai_behavior/target_from_retaliate_list, BB_BASIC_MOB_RETALIATE_LIST, target_key, targeting_strategy_key, hiding_place_key, check_faction) + +/datum/ai_planning_subtree/target_retaliate/check_faction + check_faction = TRUE + +/// Places a mob which you can see and who has recently attacked you into some 'run away from this' AI keys +/// Can use a different targeting strategy than you use to select attack targets +/// Not required if fleeing is the only target behaviour or uses the same target datum +/datum/ai_planning_subtree/target_retaliate/to_flee + targeting_strategy_key = BB_FLEE_TARGETING_STRATEGY + target_key = BB_BASIC_MOB_FLEE_TARGET + hiding_place_key = BB_BASIC_MOB_FLEE_TARGET_HIDING_LOCATION + +/** + * Picks a target from a provided list of atoms who have been pissing you off + * You will probably need /datum/element/ai_retaliate to take advantage of this unless you're populating the blackboard yourself + */ +/datum/ai_behavior/target_from_retaliate_list + action_cooldown = 2 SECONDS + /// How far can we see stuff? + var/vision_range = 9 + +/datum/ai_behavior/target_from_retaliate_list/perform(seconds_per_tick, datum/ai_controller/controller, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction) + var/mob/living/living_mob = controller.pawn + var/datum/targeting_strategy/targeting_strategy = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key]) + if(!targeting_strategy) + . = AI_BEHAVIOR_DELAY + CRASH("No target datum was supplied in the blackboard for [controller.pawn]") + + var/list/shitlist = controller.blackboard[shitlist_key] + var/atom/existing_target = controller.blackboard[target_key] + + if(!check_faction) + controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, TRUE) + + if(!QDELETED(existing_target) && (locate(existing_target) in shitlist) && targeting_strategy.can_attack(living_mob, existing_target, vision_range)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + var/list/enemies_list = list() + for(var/mob/living/potential_target as anything in shitlist) + if(!targeting_strategy.can_attack(living_mob, potential_target, vision_range)) + continue + enemies_list += potential_target + + if(!length(enemies_list)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/atom/new_target = pick_final_target(controller, enemies_list) + controller.set_blackboard_key(target_key, new_target) + + var/atom/potential_hiding_location = targeting_strategy.find_hidden_mobs(living_mob, new_target) + + if(potential_hiding_location) //If they're hiding inside of something, we need to know so we can go for that instead initially. + controller.set_blackboard_key(hiding_location_key, potential_hiding_location) + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/// Returns the desired final target from the filtered list of enemies +/datum/ai_behavior/target_from_retaliate_list/proc/pick_final_target(datum/ai_controller/controller, list/enemies_list) + return pick(enemies_list) + +/datum/ai_behavior/target_from_retaliate_list/finish_action(datum/ai_controller/controller, succeeded, shitlist_key, target_key, targeting_strategy_key, hiding_location_key, check_faction) + . = ..() + if(succeeded || check_faction) + return + var/usually_ignores_faction = controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || FALSE + controller.set_blackboard_key(BB_TEMPORARILY_IGNORE_FACTION, usually_ignores_faction) diff --git a/code/datums/components/ai_retaliate_advanced.dm b/code/datums/components/ai_retaliate_advanced.dm new file mode 100644 index 00000000000..c06617b938f --- /dev/null +++ b/code/datums/components/ai_retaliate_advanced.dm @@ -0,0 +1,37 @@ +/** + * Attached to a mob with an AI controller, passes things which have damaged it to a blackboard. + * The AI controller is responsible for doing anything with that information. + * Differs from the element as it passes new entries through a callback. + */ +/datum/component/ai_retaliate_advanced + /// Callback to a mob for custom behaviour + var/datum/callback/post_retaliate_callback + +/datum/component/ai_retaliate_advanced/Initialize(datum/callback/post_retaliate_callback) + if(!ismob(parent)) + return ELEMENT_INCOMPATIBLE + + src.post_retaliate_callback = post_retaliate_callback + parent.AddElement(/datum/element/relay_attackers) + + ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + +/datum/component/ai_retaliate_advanced/Destroy(force) + post_retaliate_callback = null + return ..() + +/datum/component/ai_retaliate_advanced/RegisterWithParent() + RegisterSignal(parent, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + +/datum/component/ai_retaliate_advanced/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_ATOM_WAS_ATTACKED) + +/// Add an attacking atom to a blackboard list of things which attacked us +/datum/component/ai_retaliate_advanced/proc/on_attacked(mob/victim, atom/attacker) + SIGNAL_HANDLER + + if(!victim.ai_controller) + return + + victim.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) + post_retaliate_callback?.InvokeAsync(attacker) diff --git a/code/datums/elements/ai_flee_while_injured.dm b/code/datums/elements/ai_flee_while_injured.dm new file mode 100644 index 00000000000..75fc47f7f14 --- /dev/null +++ b/code/datums/elements/ai_flee_while_injured.dm @@ -0,0 +1,46 @@ +/** + * Attached to a mob with an AI controller, simply sets a flag on whether or not to run away based on current health values. + */ +/datum/element/ai_flee_while_injured + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// Health value to end fleeing if at or above + var/stop_fleeing_at + /// Health value to start fleeing if at or below + var/start_fleeing_below + +/datum/element/ai_flee_while_injured/Attach(datum/target, stop_fleeing_at = 1, start_fleeing_below = 0.5) + . = ..() + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + var/mob/living/living_target = target + if(!living_target.ai_controller) + return ELEMENT_INCOMPATIBLE + src.stop_fleeing_at = stop_fleeing_at + src.start_fleeing_below = start_fleeing_below + RegisterSignal(target, COMSIG_LIVING_HEALTH_UPDATE, PROC_REF(on_health_changed)) + on_health_changed(target) + +/datum/element/ai_flee_while_injured/Detach(datum/source) + . = ..() + UnregisterSignal(source, COMSIG_LIVING_HEALTH_UPDATE) + +/// When the mob's health changes, check what the blackboard state should be +/datum/element/ai_flee_while_injured/proc/on_health_changed(mob/living/source) + SIGNAL_HANDLER + + if(isnull(source.ai_controller)) + return + + var/current_health_percentage = source.health / source.maxHealth + if(source.ai_controller.blackboard[BB_BASIC_MOB_STOP_FLEEING]) + if(current_health_percentage > start_fleeing_below) + return + source.ai_controller.cancel_actions() + source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, FALSE) + return + + if(current_health_percentage < stop_fleeing_at) + return + source.ai_controller.cancel_actions() // Stop fleeing go back to whatever you were doing + source.ai_controller.set_blackboard_key(BB_BASIC_MOB_STOP_FLEEING, TRUE) diff --git a/code/datums/elements/ai_retaliate.dm b/code/datums/elements/ai_retaliate.dm new file mode 100644 index 00000000000..837b9747ebc --- /dev/null +++ b/code/datums/elements/ai_retaliate.dm @@ -0,0 +1,27 @@ +/** + * Attached to a mob with an AI controller, passes things which have damaged it to a blackboard. + * The AI controller is responsible for doing anything with that information. + */ +/datum/element/ai_retaliate + +/datum/element/ai_retaliate/Attach(datum/target) + . = ..() + if(!ismob(target)) + return ELEMENT_INCOMPATIBLE + + target.AddElement(/datum/element/relay_attackers) + RegisterSignal(target, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked)) + + ADD_TRAIT(target, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + +/datum/element/ai_retaliate/Detach(datum/source, ...) + . = ..() + UnregisterSignal(source, COMSIG_ATOM_WAS_ATTACKED) + +/// Add an attacking atom to a blackboard list of things which attacked us +/datum/element/ai_retaliate/proc/on_attacked(mob/victim, atom/attacker) + SIGNAL_HANDLER + + if(victim == attacker) + return + victim.ai_controller?.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, attacker) diff --git a/code/datums/elements/relay_attackers.dm b/code/datums/elements/relay_attackers.dm new file mode 100644 index 00000000000..524bb619b8f --- /dev/null +++ b/code/datums/elements/relay_attackers.dm @@ -0,0 +1,95 @@ +/** + * This element registers to a shitload of signals which can signify "someone attacked me". + * If anyone does it sends a single "someone attacked me" signal containing details about who done it. + * This prevents other components and elements from having to register to the same list of a million signals, should be more maintainable in one place. + */ +/datum/element/relay_attackers + +/datum/element/relay_attackers/Attach(datum/target) + . = ..() + if(!HAS_TRAIT(target, TRAIT_RELAYING_ATTACKER)) // Little bit gross but we want to just apply this shit from a bunch of places + // Boy this sure is a lot of ways to tell us that someone tried to attack us + RegisterSignal(target, COMSIG_AFTER_ATTACKED_BY, PROC_REF(on_after_attacked_by)) + RegisterSignals(target, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_ATTACK_PAW), PROC_REF(on_attack_generic)) + RegisterSignals(target, list(COMSIG_ATOM_ATTACK_BASIC_MOB, COMSIG_ATOM_ATTACK_ANIMAL), PROC_REF(on_attack_npc)) + RegisterSignal(target, COMSIG_ATOM_BULLET_ACT, PROC_REF(on_bullet_act)) + RegisterSignal(target, COMSIG_ATOM_HITBY, PROC_REF(on_hitby)) + RegisterSignal(target, COMSIG_ATOM_HULK_ATTACK, PROC_REF(on_attack_hulk)) + ADD_TRAIT(target, TRAIT_RELAYING_ATTACKER, UID()) + +/datum/element/relay_attackers/Detach(datum/source, ...) + . = ..() + UnregisterSignal(source, list( + COMSIG_AFTER_ATTACKED_BY, + COMSIG_ATOM_ATTACK_HAND, + COMSIG_ATOM_ATTACK_PAW, + COMSIG_ATOM_ATTACK_BASIC_MOB, + COMSIG_ATOM_BULLET_ACT, + COMSIG_ATOM_HITBY, + COMSIG_ATOM_HULK_ATTACK, + )) + REMOVE_TRAIT(source, TRAIT_RELAYING_ATTACKER, UID()) + +/datum/element/relay_attackers/proc/on_after_attacked_by(atom/target, obj/item/weapon, mob/attacker, proximity_flag, click_parameters) + SIGNAL_HANDLER // COMSIG_AFTER_ATTACKED_BY + if(!proximity_flag) // we don't care about someone clicking us with a piece of metal from across the room + return + if(weapon.force) + relay_attacker(target, attacker, weapon.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK) + +/datum/element/relay_attackers/proc/on_attack_generic(atom/target, mob/living/attacker, list/modifiers) + SIGNAL_HANDLER + + // Check for a shove. + if(attacker.a_intent == INTENT_DISARM) + relay_attacker(target, attacker, ATTACKER_SHOVING) + return + + // Else check for harm intent. + if(attacker.a_intent == INTENT_HARM) + relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK) + return + +/datum/element/relay_attackers/proc/on_attack_npc(atom/target, mob/living/attacker) + SIGNAL_HANDLER // COMSIG_ATOM_ATTACK_BASIC_MOB + COMSIG_ATOM_ATTACK_ANIMAL + // both simple animals and basic mobs have the same var name here but we have to check both types + if(isanimal(attacker)) + var/mob/living/simple_animal/SA = attacker + if(SA.melee_damage_upper > 0) + relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK) + return + if(isbasicmob(attacker)) + var/mob/living/basic/B = attacker + if(B.melee_damage_upper > 0) + relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK) + return + +/// Even if another component blocked this hit, someone still shot at us +/datum/element/relay_attackers/proc/on_bullet_act(atom/target, obj/item/projectile/hit_projectile) + SIGNAL_HANDLER // COMSIG_ATOM_BULLET_ACT + if(!hit_projectile.is_hostile_projectile()) + return + if(!ismob(hit_projectile.firer)) + return + relay_attacker(target, hit_projectile.firer, hit_projectile.damage_type == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK) + +/// Even if another component blocked this hit, someone still threw something +/datum/element/relay_attackers/proc/on_hitby(atom/target, atom/movable/hit_atom, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER // COMSIG_ATOM_HITBY + if(!isitem(hit_atom)) + return + var/obj/item/hit_item = hit_atom + if(!hit_item.throwforce) + return + var/mob/thrown_by = locateUID(hit_item.thrownby) + if(!ismob(thrown_by)) + return + relay_attacker(target, thrown_by, hit_item.damtype == STAMINA ? ATTACKER_STAMINA_ATTACK : ATTACKER_DAMAGING_ATTACK) + +/datum/element/relay_attackers/proc/on_attack_hulk(atom/target, mob/attacker) + SIGNAL_HANDLER + relay_attacker(target, attacker, ATTACKER_DAMAGING_ATTACK) + +/// Send out a signal identifying whoever just attacked us (usually a mob but sometimes a mech or turret) +/datum/element/relay_attackers/proc/relay_attacker(atom/victim, atom/attacker, attack_flags) + SEND_SIGNAL(victim, COMSIG_ATOM_WAS_ATTACKED, attacker, attack_flags) diff --git a/code/datums/proximity/ai_target_tracking.dm b/code/datums/proximity/ai_target_tracking.dm new file mode 100644 index 00000000000..bb47dfec62f --- /dev/null +++ b/code/datums/proximity/ai_target_tracking.dm @@ -0,0 +1,113 @@ +// Proximity monitor that checks to see if anything interesting enters our bounds +/datum/proximity_monitor/advanced/ai_target_tracking + edge_is_a_field = TRUE + /// The ai behavior who owns us + var/datum/ai_behavior/find_potential_targets/owning_behavior + /// The ai controller we're using + var/datum/ai_controller/controller + /// The target key we're trying to fill + var/target_key + /// The targeting strategy KEY we're using + var/targeting_strategy_key + /// The hiding location key we're using + var/hiding_location_key + + /// The targeting strategy we're using + var/datum/targeting_strategy/filter + /// If we've built our field yet + /// Prevents wasted work on the first build (since the behavior did it) + var/first_build = TRUE + +// Initially, run the check manually +// If that fails, set up a field and have it manage the behavior fully +/datum/proximity_monitor/advanced/ai_target_tracking/New(atom/_host, range, _ignore_if_not_on_turf = TRUE, datum/ai_behavior/find_potential_targets/owning_behavior, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + src.owning_behavior = owning_behavior + src.controller = controller + src.target_key = target_key + src.targeting_strategy_key = targeting_strategy_key + src.hiding_location_key = hiding_location_key + src.filter = GET_TARGETING_STRATEGY(controller.blackboard[targeting_strategy_key]) + RegisterSignal(controller, COMSIG_PARENT_QDELETING, PROC_REF(controller_deleted)) + RegisterSignal(controller, COMSIG_AI_CONTROLLER_PICKED_BEHAVIORS, PROC_REF(controller_think)) + RegisterSignal(controller, COMSIG_AI_CONTROLLER_POSSESSED_PAWN, PROC_REF(pawn_changed)) + RegisterSignal(controller, AI_CONTROLLER_BEHAVIOR_QUEUED(owning_behavior.type), PROC_REF(behavior_requeued)) + RegisterSignal(controller, COMSIG_AI_BLACKBOARD_KEY_SET(targeting_strategy_key), PROC_REF(targeting_datum_changed)) + RegisterSignal(controller, COMSIG_AI_BLACKBOARD_KEY_CLEARED(targeting_strategy_key), PROC_REF(targeting_datum_cleared)) + recalculate_field(full_recalc = TRUE) + +/datum/proximity_monitor/advanced/ai_target_tracking/Destroy() + . = ..() + if(!QDELETED(controller) && owning_behavior) + controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller)) + owning_behavior = null + controller = null + target_key = null + targeting_strategy_key = null + hiding_location_key = null + filter = null + +/datum/proximity_monitor/advanced/ai_target_tracking/recalculate_field(full_recalc = FALSE) + . = ..() + first_build = FALSE + +/datum/proximity_monitor/advanced/ai_target_tracking/setup_field_turf(turf/target) + . = ..() + if(first_build) + return + owning_behavior.new_turf_found(target, controller, filter) + +/datum/proximity_monitor/advanced/ai_target_tracking/field_turf_crossed(atom/movable/movable, turf/location, turf/old_location) + . = ..() + + if(!owning_behavior.atom_allowed(movable, filter, controller.pawn)) + return + + owning_behavior.new_atoms_found(list(movable), controller, target_key, filter, hiding_location_key) + +/// React to controller planning +/datum/proximity_monitor/advanced/ai_target_tracking/proc/controller_deleted(datum/source) + SIGNAL_HANDLER + qdel(src) + +/// React to the pawn goin byebye +/datum/proximity_monitor/advanced/ai_target_tracking/proc/pawn_changed(datum/source) + SIGNAL_HANDLER + qdel(src) + +/// React to controller planning +/datum/proximity_monitor/advanced/ai_target_tracking/proc/controller_think(datum/ai_controller/source, list/datum/ai_behavior/old_behaviors, list/datum/ai_behavior/new_behaviors) + SIGNAL_HANDLER + // If our parent was forgotten, nuke ourselves + if(!new_behaviors[owning_behavior]) + qdel(src) + +/datum/proximity_monitor/advanced/ai_target_tracking/proc/behavior_requeued(datum/source, list/new_arguments) + SIGNAL_HANDLER + check_new_args(arglist(new_arguments)) + +/// Ensure our args and locals are up to date +/datum/proximity_monitor/advanced/ai_target_tracking/proc/check_new_args(target_key, targeting_strategy_key, hiding_location_key) + var/update_filter = FALSE + if(src.target_key != target_key) + src.target_key = target_key + if(src.targeting_strategy_key != targeting_strategy_key) + src.targeting_strategy_key = targeting_strategy_key + update_filter = TRUE + if(src.hiding_location_key != hiding_location_key) + src.hiding_location_key = hiding_location_key + if(update_filter) + targeting_datum_changed(null) + +/datum/proximity_monitor/advanced/ai_target_tracking/proc/targeting_datum_changed(datum/source) + SIGNAL_HANDLER + filter = controller.blackboard[targeting_strategy_key] + // Filter changed, need to do a full reparse + // Fucking 9 * 9 out here I stg + for(var/turf/in_field as anything in field_turfs + edge_turfs) + owning_behavior.new_turf_found(in_field, controller, filter) + +/datum/proximity_monitor/advanced/ai_target_tracking/proc/targeting_datum_cleared(datum/source) + SIGNAL_HANDLER + // Go fuckin home bros + qdel(src) diff --git a/code/game/objects/structures/crates_lockers/crittercrate.dm b/code/game/objects/structures/crates_lockers/crittercrate.dm index 702614172d0..4f22cfa549d 100644 --- a/code/game/objects/structures/crates_lockers/crittercrate.dm +++ b/code/game/objects/structures/crates_lockers/crittercrate.dm @@ -48,7 +48,7 @@ content_mob = pick(/mob/living/simple_animal/pet/dog/corgi, /mob/living/simple_animal/pet/dog/corgi/lisa, /mob/living/basic/cow, - /mob/living/simple_animal/pig, + /mob/living/basic/pig, /mob/living/simple_animal/hostile/retaliate/goat, /mob/living/simple_animal/turkey, /mob/living/simple_animal/chick, @@ -72,7 +72,7 @@ /obj/structure/closet/critter/pig name = "pig crate" - content_mob = /mob/living/simple_animal/pig + content_mob = /mob/living/basic/pig /obj/structure/closet/critter/goat name = "goat crate" diff --git a/code/modules/mob/living/basic/basic_mob.dm b/code/modules/mob/living/basic/basic_mob.dm index 5a929e8d349..8ebf272d365 100644 --- a/code/modules/mob/living/basic/basic_mob.dm +++ b/code/modules/mob/living/basic/basic_mob.dm @@ -19,6 +19,7 @@ RESTRICT_TYPE(/mob/living/basic) name = "basic mob" desc = "If you can see this, make an issue report on GitHub." healable = TRUE + icon = 'icons/mob/animal.dmi' var/basic_mob_flags @@ -110,6 +111,9 @@ RESTRICT_TYPE(/mob/living/basic) // Attack related vars. // Unclear how much of this should either be componentized or pulled to /mob/living + /// Sound played when the critter attacks. + var/attack_sound = 'sound/weapons/punch1.ogg' + /// Sound played when the critter is attacked. var/attacked_sound = 'sound/weapons/punch1.ogg' /// The amount of damage done to the mob when hand-attacked on harm intent. var/harm_intent_damage = 3 @@ -117,6 +121,20 @@ RESTRICT_TYPE(/mob/living/basic) var/list/damage_coeff = list(BRUTE = 1, BURN = 1, TOX = 1, STAMINA = 0, OXY = 1) /// Minimum force required to deal any damage var/force_threshold = 0 + /// Lower bound of damage done by unarmed melee attacks. + var/melee_damage_lower = 0 + /// Upper bound of damage done by unarmed melee attacks. + var/melee_damage_upper = 0 + /// How much damage this simple animal does to objects, if any + var/obj_damage = 0 + /// Flat armour reduction, occurs after percentage armour penetration. + var/armour_penetration_flat = 0 + /// Percentage armour reduction, happens before flat armour reduction. + var/armour_penetration_percentage = 0 + /// Damage type of a simple mob's melee attack, should it do damage. + var/melee_damage_type = BRUTE + /// How often can you melee attack? + var/melee_attack_cooldown = 2 SECONDS /mob/living/basic/Initialize(mapload) . = ..() @@ -164,6 +182,25 @@ RESTRICT_TYPE(/mob/living/basic) /mob/living/basic/proc/valid_respawn_target_for(mob/user) return FALSE +/mob/living/basic/resolve_unarmed_attack(atom/attack_target, list/modifiers) + melee_attack(attack_target, modifiers) + +/mob/living/basic/proc/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) + if(!early_melee_attack(target, modifiers, ignore_cooldown)) + return FALSE + + var/result = target.attack_basic_mob(src, modifiers) + SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, result) + return result + +/mob/living/basic/proc/early_melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) + face_atom(target) + if(!ignore_cooldown) + changeNext_move(melee_attack_cooldown) + if(SEND_SIGNAL(src, COMSIG_HOSTILE_PRE_ATTACKINGTARGET, target, Adjacent(target), modifiers) & COMPONENT_HOSTILE_NO_ATTACK) + return FALSE + return TRUE + /mob/living/basic/handle_environment(datum/gas_mixture/readonly_environment) SEND_SIGNAL(src, COMSIG_SIMPLEANIMAL_HANDLE_ENVIRONMENT, readonly_environment) diff --git a/code/modules/mob/living/basic/cow/cow.dm b/code/modules/mob/living/basic/cow.dm similarity index 78% rename from code/modules/mob/living/basic/cow/cow.dm rename to code/modules/mob/living/basic/cow.dm index 7715db88b53..1ba8bc4049c 100644 --- a/code/modules/mob/living/basic/cow/cow.dm +++ b/code/modules/mob/living/basic/cow.dm @@ -1,7 +1,30 @@ +/datum/ai_planning_subtree/random_speech/cow + speech_chance = 2 + speak = list("Moo?", "Moo", "MOOOOOO") + speak_verbs = list("moos", "moos hauntingly") + sound = list('sound/creatures/cow.ogg') + emote_hear = list("brays.") + emote_see = list("shakes her head.") + +/datum/ai_controller/basic_controller/cow + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + BB_BASIC_MOB_TIP_REACTING = FALSE, + BB_BASIC_MOB_TIPPER = null, + ) + + ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/tip_reaction, + /datum/ai_planning_subtree/find_food, + /datum/ai_planning_subtree/random_speech/cow, + ) + /mob/living/basic/cow name = "cow" desc = "Known for their milk, just don't tip them over." - icon = 'icons/mob/animal.dmi' icon_state = "cow" icon_living = "cow" icon_dead = "cow_dead" diff --git a/code/modules/mob/living/basic/cow/cow_ai.dm b/code/modules/mob/living/basic/cow/cow_ai.dm deleted file mode 100644 index 34e26e57c9a..00000000000 --- a/code/modules/mob/living/basic/cow/cow_ai.dm +++ /dev/null @@ -1,15 +0,0 @@ -/datum/ai_controller/basic_controller/cow - blackboard = list( - BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, - BB_BASIC_MOB_TIP_REACTING = FALSE, - BB_BASIC_MOB_TIPPER = null, - ) - - ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED - ai_movement = /datum/ai_movement/basic_avoidance - idle_behavior = /datum/idle_behavior/idle_random_walk - planning_subtrees = list( - /datum/ai_planning_subtree/tip_reaction, - /datum/ai_planning_subtree/find_food, - /datum/ai_planning_subtree/random_speech/cow, - ) diff --git a/code/modules/mob/living/basic/pig.dm b/code/modules/mob/living/basic/pig.dm new file mode 100644 index 00000000000..09b3f378c32 --- /dev/null +++ b/code/modules/mob/living/basic/pig.dm @@ -0,0 +1,50 @@ +/datum/ai_planning_subtree/random_speech/pig + speech_chance = 2 + speak = list("oink?", "oink", "OINK") + sound = list( + 'sound/creatures/pig/pig1.ogg', + 'sound/creatures/pig/pig2.ogg' + ) + emote_hear = list("oinks.") + emote_see = list("rolls around.") + +/datum/ai_controller/basic_controller/pig + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic, + ) + + ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED + ai_movement = /datum/ai_movement/basic_avoidance + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee, + /datum/ai_planning_subtree/flee_target, + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/random_speech/pig, + ) + +/mob/living/basic/pig + name = "pig" + desc = "Oink oink." + icon_state = "pig" + icon_living = "pig" + icon_dead = "pig_dead" + ai_controller = /datum/ai_controller/basic_controller/pig + see_in_dark = 6 + butcher_results = list(/obj/item/food/meat/ham = 6) + health = 50 + maxHealth = 50 + melee_damage_lower = 1 + melee_damage_upper = 2 + mob_biotypes = MOB_ORGANIC | MOB_BEAST + gold_core_spawnable = FRIENDLY_SPAWN + blood_volume = BLOOD_VOLUME_NORMAL + unintelligble_phrases = list("Oink?", "Oink", "OINK") + unintelligble_speak_verbs = list("oinks") + +/mob/living/basic/pig/Initialize(mapload) + . = ..() + AddElement(/datum/element/ai_retaliate) + AddElement(/datum/element/ai_flee_while_injured) + AddComponent(/datum/component/footstep, FOOTSTEP_MOB_SHOE) diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm index 128ecc4c037..2dbea5e84da 100644 --- a/code/modules/mob/living/carbon/human/human_defense.dm +++ b/code/modules/mob/living/carbon/human/human_defense.dm @@ -717,6 +717,21 @@ emp_act apply_damage(damage, M.melee_damage_type, affecting, armor) updatehealth("animal attack") +/mob/living/carbon/human/handle_basic_attack(mob/living/basic/attacker, modifiers) + . = ..() + if(.) + var/damage = rand(attacker.melee_damage_lower, attacker.melee_damage_upper) + if(check_shields(attacker, damage, "[attacker.name]", MELEE_ATTACK, attacker.armour_penetration_flat, attacker.armour_penetration_percentage)) + return FALSE + var/dam_zone = pick("head", "chest", "groin", "l_arm", "l_hand", "r_arm", "r_hand", "l_leg", "l_foot", "r_leg", "r_foot") + var/obj/item/organ/external/affecting = get_organ(ran_zone(dam_zone)) + if(!affecting) + affecting = get_organ("chest") + affecting.add_autopsy_data(attacker.name, damage) // Add the mob's name to the autopsy data + var/armor = run_armor_check(affecting, MELEE, armour_penetration_flat = attacker.armour_penetration_flat, armour_penetration_percentage = attacker.armour_penetration_percentage) + apply_damage(damage, attacker.melee_damage_type, affecting, armor) + updatehealth("basicmob attack") + /mob/living/carbon/human/attack_slime(mob/living/simple_animal/slime/M) if(..()) //successful slime attack var/damage = rand(5, 25) diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm index e12a1b36fb9..f1566e0a230 100644 --- a/code/modules/mob/living/living_defense.dm +++ b/code/modules/mob/living/living_defense.dm @@ -339,6 +339,8 @@ return TRUE /mob/living/attack_animal(mob/living/simple_animal/M) + . = ..() + M.face_atom(src) if((M.a_intent == INTENT_HELP && M.ckey) || M.melee_damage_upper == 0) M.custom_emote(EMOTE_VISIBLE, "[M.friendly] [src].") @@ -355,6 +357,24 @@ add_attack_logs(M, src, "Animal attacked") return TRUE +// TODO: Probably a bunch of this shit doesn't need to be here but I don't feel +// like sorting it out right now +/mob/living/handle_basic_attack(mob/living/basic/attacker, modifiers) + if((attacker.a_intent == INTENT_HELP && attacker.ckey) || attacker.melee_damage_upper == 0) + attacker.custom_emote(EMOTE_VISIBLE, "[attacker.friendly_verb_continuous] [src].") + return FALSE + if(HAS_TRAIT(attacker, TRAIT_PACIFISM)) + to_chat(attacker, "You don't want to hurt anyone!") + return FALSE + + if(attacker.attack_sound) + playsound(loc, attacker.attack_sound, 50, TRUE, 1) + attacker.do_attack_animation(src) + visible_message("[attacker] [attacker.attack_verb_continuous] [src]!", \ + "[attacker] [attacker.attack_verb_continuous] [src]!") + add_attack_logs(attacker, src, "Basicmob attacked") + return TRUE + /mob/living/attack_larva(mob/living/carbon/alien/larva/L) switch(L.a_intent) if(INTENT_HELP) diff --git a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm index 3690c0c2618..54837024ea3 100644 --- a/code/modules/mob/living/simple_animal/friendly/farm_animals.dm +++ b/code/modules/mob/living/simple_animal/friendly/farm_animals.dm @@ -305,32 +305,6 @@ GLOBAL_VAR_INIT(chicken_count, 0) /mob/living/simple_animal/chicken/featherbottom/npc_safe(mob/user) return FALSE -/mob/living/simple_animal/pig - name = "pig" - desc = "Oink oink." - icon_state = "pig" - icon_living = "pig" - icon_dead = "pig_dead" - speak = list("oink?","oink","OINK") - speak_emote = list("oinks") -// emote_hear = list("brays") - emote_see = list("rolls around") - speak_chance = 1 - turns_per_move = 5 - see_in_dark = 6 - butcher_results = list(/obj/item/food/meat/ham = 6) - response_help = "pets" - response_disarm = "gently pushes aside" - response_harm = "kicks" - attacktext = "kicks" - health = 50 - maxHealth = 50 - can_collar = TRUE - mob_biotypes = MOB_ORGANIC | MOB_BEAST - gold_core_spawnable = FRIENDLY_SPAWN - blood_volume = BLOOD_VOLUME_NORMAL - footstep_type = FOOTSTEP_MOB_SHOE - /mob/living/simple_animal/turkey name = "turkey" desc = "Benjamin Franklin would be proud." diff --git a/code/modules/projectiles/projectile_base.dm b/code/modules/projectiles/projectile_base.dm index 49754de3ac1..fa6f81aac3b 100644 --- a/code/modules/projectiles/projectile_base.dm +++ b/code/modules/projectiles/projectile_base.dm @@ -593,5 +593,22 @@ /obj/item/projectile/proc/cleanup_beam_segments() QDEL_LIST_ASSOC(beam_segments) +/** + * Is this projectile considered "hostile"? + * + * By default all projectiles which deal damage or impart crowd control effects (including stamina) are hostile + * + * This is NOT used for pacifist checks, that's handled by [/obj/item/ammo_casing/var/harmful] + * This is used in places such as AI responses to determine if they're being threatened or not (among other places) + */ +/obj/item/projectile/proc/is_hostile_projectile() + if(damage > 0 || stamina > 0) + return TRUE + + if(stun + weaken + paralyze + knockdown > 0 SECONDS) + return TRUE + + return FALSE + #undef MOVES_HITSCAN #undef MUZZLE_EFFECT_PIXEL_INCREMENT diff --git a/code/tests/game_tests.dm b/code/tests/game_tests.dm index e7bd5315b61..03f191b9e13 100644 --- a/code/tests/game_tests.dm +++ b/code/tests/game_tests.dm @@ -29,6 +29,7 @@ #include "test_dynamic_budget.dm" #include "test_elements.dm" #include "test_emotes.dm" +#include "test_ensure_subtree_operational_datum.dm" #include "test_init_sanity.dm" #include "test_log_format.dm" #include "test_map_templates.dm" diff --git a/code/tests/test_ensure_subtree_operational_datum.dm b/code/tests/test_ensure_subtree_operational_datum.dm new file mode 100644 index 00000000000..3b6cbf6b836 --- /dev/null +++ b/code/tests/test_ensure_subtree_operational_datum.dm @@ -0,0 +1,64 @@ +/// The subtree that requires the operational datum. +#define REQUIRED_SUBTREE "required_subtree" +/// The list of typepaths of applicable operational datums that would satisfy the requirement. +#define REQUIRED_OPERATIONAL_DATUMS "required_operational_datums" + +/// Unit Test that ensure that if we add a specific planning subtree to a basic mob's planning tree, that we also have the operational datum needed for it (component/element). +/// This can be extended to other "mandatory" operational datums for certain subtrees to work. +/datum/game_test/ensure_subtree_operational_datum + /// Associated list of mobs that we need to test this on. Key is the typepath of the mob, value is a list of the planning subtree and the operational datums that are required for it. + var/list/testable_mobs = list() + +/datum/game_test/ensure_subtree_operational_datum/Run() + gather_testable_mobs() + test_applicable_mobs() + +/// First, look for all mobs that have a planning subtree that requires an element, then add it to the list for stuff to test afterwards. Done like this to not have one mumbo proc that's hard to read. +/datum/game_test/ensure_subtree_operational_datum/proc/gather_testable_mobs() + for(var/mob/living/basic/checkable_mob as anything in subtypesof(/mob/living/basic)) + var/datum/ai_controller/testable_controller = initial(checkable_mob.ai_controller) + if(isnull(testable_controller)) + continue + + // we can't do inital() memes on lists so it's allocation time + testable_controller = allocate(testable_controller) + var/list/ai_planning_subtrees = testable_controller.planning_subtrees // list of instantiated datums. easy money + if(!length(ai_planning_subtrees)) + continue + + for(var/datum/ai_planning_subtree/testable_subtree as anything in ai_planning_subtrees) + var/list/necessary_datums = testable_subtree.operational_datums + if(isnull(necessary_datums)) + continue + + testable_mobs[checkable_mob] = list( + REQUIRED_OPERATIONAL_DATUMS = necessary_datums, + REQUIRED_SUBTREE = testable_subtree.type, + ) + +/// Then, test the mobs that we've found +/datum/game_test/ensure_subtree_operational_datum/proc/test_applicable_mobs() + for(var/mob/living/basic/checkable_mob as anything in testable_mobs) + var/list/checkable_mob_data = testable_mobs[checkable_mob] + checkable_mob = allocate(checkable_mob) + + var/datum/ai_planning_subtree/test_subtree = checkable_mob_data[REQUIRED_SUBTREE] + var/list/trait_sources = GET_TRAIT_SOURCES(checkable_mob, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM) + if(!length(trait_sources)) // yes yes we could use `COUNT_TRAIT_SOURCES` but why invoke the same macro twice + TEST_FAIL("The mob [checkable_mob] ([checkable_mob.type]) does not have ANY instances of TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, but has a planning subtree ([test_subtree]) that requires it!") + continue + + var/has_element = FALSE + var/list/testable_operational_datums = checkable_mob_data[REQUIRED_OPERATIONAL_DATUMS] + for(var/iterable in trait_sources) + if(iterable in testable_operational_datums) + has_element = TRUE + break + + if(!has_element) + var/list/message_list = list("The mob [checkable_mob] ([checkable_mob.type]) has a planning subtree ([test_subtree]) that requires a component/element, but does not have any!") + message_list += "Needs one of the following to satisfy the requirement: ([testable_operational_datums.Join(", ")])" + TEST_FAIL(message_list.Join(" ")) + +#undef REQUIRED_SUBTREE +#undef REQUIRED_OPERATIONAL_DATUMS diff --git a/paradise.dme b/paradise.dme index 96d6995744a..627db15476a 100644 --- a/paradise.dme +++ b/paradise.dme @@ -445,10 +445,18 @@ #include "code\datums\ai\ai_controller.dm" #include "code\datums\ai\ai_planning_subtree.dm" #include "code\datums\ai\basic_mobs\basic_controller.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\interact_with_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targeting.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\targeting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\find_food.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\flee_target.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\random_speech.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\simple_attack_target.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_nearest_target_to_flee.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\tip_reaction.dm" #include "code\datums\ai\generic\find_and_set.dm" #include "code\datums\ai\generic\generic_behaviors.dm" @@ -464,6 +472,7 @@ #include "code\datums\cache\crew.dm" #include "code\datums\cache\powermonitor.dm" #include "code\datums\components\_component.dm" +#include "code\datums\components\ai_retaliate_advanced.dm" #include "code\datums\components\anti_magic.dm" #include "code\datums\components\boomerang.dm" #include "code\datums\components\boss_music.dm" @@ -574,6 +583,8 @@ #include "code\datums\diseases\advance\symptoms\weight.dm" #include "code\datums\diseases\advance\symptoms\youth.dm" #include "code\datums\elements\_element.dm" +#include "code\datums\elements\ai_flee_while_injured.dm" +#include "code\datums\elements\ai_retaliate.dm" #include "code\datums\elements\atmos_requirements.dm" #include "code\datums\elements\basic_eating.dm" #include "code\datums\elements\body_temperature.dm" @@ -584,6 +595,7 @@ #include "code\datums\elements\earhealing.dm" #include "code\datums\elements\high_value_item.dm" #include "code\datums\elements\rad_insulation.dm" +#include "code\datums\elements\relay_attackers.dm" #include "code\datums\elements\ridable.dm" #include "code\datums\elements\rust_element.dm" #include "code\datums\elements\shatters_when_thrown.dm" @@ -619,6 +631,7 @@ #include "code\datums\outfits\plasmamen_outfits.dm" #include "code\datums\outfits\vv_outfit.dm" #include "code\datums\proximity\advanced_proximity_monitor.dm" +#include "code\datums\proximity\ai_target_tracking.dm" #include "code\datums\proximity\proximity_monitor.dm" #include "code\datums\proximity\singulo_proximity_monitor.dm" #include "code\datums\ruins\lavaland.dm" @@ -2327,8 +2340,8 @@ #include "code\modules\mob\living\stat_states.dm" #include "code\modules\mob\living\taste.dm" #include "code\modules\mob\living\basic\basic_mob.dm" -#include "code\modules\mob\living\basic\cow\cow.dm" -#include "code\modules\mob\living\basic\cow\cow_ai.dm" +#include "code\modules\mob\living\basic\pig.dm" +#include "code\modules\mob\living\basic\cow.dm" #include "code\modules\mob\living\brain\brain_death.dm" #include "code\modules\mob\living\brain\brain_emote.dm" #include "code\modules\mob\living\brain\brain_life.dm" diff --git a/sound/creatures/pig/pig1.ogg b/sound/creatures/pig/pig1.ogg new file mode 100644 index 0000000000000000000000000000000000000000..eb63bd5c6ebf5f34bdd84c0a72f821899be35492 GIT binary patch literal 5690 zcmZu!3pkY9*WWW{803-}PHxj+3LdxFXfSfky@?S=WJKj! zNUA|XZn-7bMA5}HgoN+q^qv3rf1YnW&))Amd$09dYyI|bX3uNo;b9Bl;Fn2PON?%P zoC%Vbhhbs<=Y3pg7q=E*M)$WK0C?|&Z9l7ER$Gz(+E(P&j@+1A4$5hB+ZbH`0GLDT z4m591RVyD4++Q@8W81JeQ=B?cO`WJt)W9iPo%5o3`?z0p_wrRbfV1-UrTud9C#tHe z5^;)x{8{b(pF9w@*ikf1H)r5Kmpq=2n%m*ZLa{>f127_+%g0mn*OhCd-4Jx zf&+|e_u)Vt@&RGB0Nj?j2LKMr^2?jz*|Z1XpggGmIO4!A;{WdmWv0*%(qSk8N8}dD zF3b)jj^ri^fH0sY0)Pa712Hbe&kvYLKu7RzXAuLOQ2Ad8p;Q2DheIW3VIn@5n230r zXo=u~i8r((EK$a{RjRi((qdMBzr*%BgiWfjo6O< zMF_-hq`YIiJS|zylqqj2VqT`)SmwZVqlx@)L68TaD#nL}8^8p4euYA?TgmANK#w2S zigz`MzhjcjKx`B4w$;U`mSCfe6pvtJE{I3){zDXMi?=O|vG-5Pirx0{PFhOXD8QhlH z3#ThW+>D#BPCGI`&{z|+EDuL$X0f%I>_BCk9&$≀+(0sZ%bQWZ6wJUxe0-Y!=9E z3q~CaBr5>vg$o)H*4w2V1{|DejzV;nq8W=2!;y0d%radx>Rz_cTFa)(AB%cJgvhKwWA^GsGlk43je zAjgeGw8Rq7XadoArwv9e9Z+d3&!8O7AQn5FO^X3--Dz2$Zb!=!2;Gd+G^4gfVcl-b z&gdv$1VD~r3>hnngDZE4IOe#say|tbb-zHEFF?SSDIV!*1dDExBcU5(Db&_uAxo#J z*D|7M@SXH#yh!Z}L=uX?rn$h;Y#v!7G>9UQsWJaFWs3k5lA1BWA_|ZR7HMo=D!kJz zj0dDy$UX{AEEEN;Yl9aFMaklVq5vHg1$J(+4`d;w11oO;_LQ9RW2EGia-iQZI1mSbp~ff9 z5LQPiMF=0^Q;J9hfDYqg??fdmTM5-8U;#MVRypn#VL6!1urHjI$vz;!&v@L>Kx>sLw-004j~iO0K+8G!_i&= zV**xHwY)mUd4k)*`r06%H94Mi&BfLqt@Rr=XmnYj|$` zI2V4aoqr64g8G3B0Sy#t`|d+fowt|3*4?)SpWJo(v7`c_AS<24%4gqJQdg39!nJRP3jrOeubI+ua>(g&0hb{R8P zd%_ST3kz7aKXy_z?2BiHa!=h3o)=bUTol+J^X_QF)s5LRe*uG<4w~PCa@lXp`wPc& z5UcV$mtUsk_{>d9oI2S&)jJ&KX(N&8XK*p~LK=%YnA-R3u%XlAeoPJjo;;h78*45H zQh1nHZvPMauepO2^d-rXx5Ud=cJCWye|-8*+hW2@MN`A{@}|TiZ|-&r#UyOPzPc#! z5QT>Oj`-DxX1lXq4uRFLR&Hibt~w&X@Pm5u>cqM$Mg5H!1hK+0|2NAUgXSfvUyHO< zE8p8hIwqKPcdoutEt*P{mw9{67bf;SXPGvbv2p54Tk6YObtY3iPK-&36N7Ix*V8X^ zsp{D3TTp`cXrdh);G{dV87(lgurJj>u_RyHUt6I)wd#jqCRGR59z^Uk%4d4CfoZe*xalKW32)5yz!h*)6*LM`kcI zlvkQ(;h?_yleaQ%`z}h$y*sMd&ogbuCtFm)wBL~v{`fDuSrp%s4;1e3Zyt0bY3$jE zndmS>+uxhZ12o*MZXLf*+BLFI8Tc&?N{zXVCV5P)&mRn{*ga|Q?D$fKRM0TnyB@yc zy=3)Kmu#swisL=|T#sz=my07d#w+^L(5HL+-#)7@eP`TYaMrwK`27*Yk7=51!H+-G zgmg}Kd8zg=>kWZsXZc&@}@GUNockLzPMZNT;;ozFORN|SD@2bqV zVm*4);kH$~xZ9kTYa3^H046~tc(J%|S|!KOqMSs3R<8DNv-(X)@p&a@^WkT1EpEBX z%Laujy%;59+OloSvmVdO_$duP{EBL;9&<#`+#=+Bjb0brl?TzSQdEBKXNE;;&yhpH z&AiyA(KP5T%@7UD+imP*{&J{&d|zWgYL*j9{C-_yITJ>GeEVigWzgvTi+Gr|m<%^B zRch_}Iw$|{`6A7vzvZ%odTM7MdNny&9>fMY4 z@(14EdCi`d!57-x3eD3oJC=>zn^U!>mazOlS`K@@NgB17zkK9qU)E8>r_No~1KEIh zaZ~KfWI}@^QiNvcttN&uUdcS(s4{Z4O!oJILjUQ+6EMk%E|kRZ*`-g%#lTpJ-8%k>ibsy6|pgC_W@!8Fj0Ca1_r4ZH6Ehn~= zdf%%{>R6ICYPqVE&E?&Gw(H5yeCLH%POzR2lQbTnsJ~>NK$Eb^tf_B(5j@BKrkRSB zR_q;^x9fdKsXqicB15#w5wqOO&EUIwWTOCh2Lf^~JO~X*v{SJNl%z)?Pkwb@J-FWY zleOvmY()mIm&~7m3gM!M`oO;NjW+euxmOVsF8)6##QSSP;c``}e?I-Q+z;uwDKwU? zr8Xf1e|f=9**cB;E=e()CF2ZZ==WiAM;4AX6cG0Y6u;3&oPSN}&w52u%?&ka9(ek8 zC*8MI@A*r8o9oEo%t%uWwUd66dp+wTx+>t6t2@%D92-1HhK>B#KO^P(wQKEj#fqK3 zddIg*C0q`Z(cBI{z)DDCXu1%PA|g$rs^34{Ouu$6`;Us>D?fv#7;^Ju0cK-Ei7vLk2^UV zOI>pJ>Gx#9Z92+$02}x^RQTidh`w{1tqT-5_p>icACC|8R#g5ZiFBNj&wA()c$4?_ zSqnHrkAAEi^VVHWw3-W`G8n=JKDDp`*BV291wfRtR8lJL6!ba+3LjKL|Je`HQYPM# zIFcY_U)Vv-SC%fD}XcjL0cghlNUeQBUI4f$_uA)<0T{w6Y!Jy$<1)v=Uauo>q%tqWTjSItzW{ zWyO{YPDue`i(Y2qaM7z41sjJ&RP6vFSsMpz z0p>Lq?(i^H!w?s)^sre`Sa}IGbiqS1h8xR2k!xSwk#$*`mUQMs(b$CJMmcUQM1kH= z1wPr9P6KOq%6^Q5lGk*5>x>C`WK;J5rFdyf!U$0B)_@xC==ndJJB5cfyPiDIMCZI1 zb{_82ZIAjYIy&iU5WDuSIblur;dSvVbZhz78;l3!At1W;WX?Whu`Uul_@%!-xl<2^ zK-*WtMnky*_jr5YIo$j!hrmh6KL(TkHfMT?KRR2RAYa%qu$%Xj_iri`>+kVglv%zw zgXC{h9Yy!xHrF5BIR}JF1#-aMv)!0C^myaJQl8o9W}X&CIJvOca2Hju&v0k&{CcBl z0RKBc%;zHhXuf{4{j*8f0H5;A6#2wbHa9t2E6!g#RA4A%*tHj?wPEn_;V#~6)e<4F zv3C7#eNEs`M&KW`5he_1|nH$?4L6W(6mVo@>Rr2#hX^BjF`F!=@ufmZxq# z*&R$G@BQ*#fXBaNT9ZaF57IkwzVoLYzsaXJ>vPPZCDC|?9uKDmLCaN-HjnuIYA-d- z?dk-_IwK#s`|Lch)9}g(mGPsZzJ6)GqQ-d^4ZAK)-0g~~J>w?#9zS5qlR*zZHTFy% zIaLwxTuMJeSXE|FY_UJ;QDUewz$zl$?N0CD+jqxHIP>_Z-wfGS)wi<;l=%DE#JT0v z#vM|(>^GI#-+t5UiP@jTUBd}wMJaoQrc%8vNgC# z3@ky2oEf_Y16YLy5~qq6z{rvpicdJSK`)7X5K2#Oq z8O%Pg)2XIIf%QO8GG%XFT;F5%BeZJ~?DUmA`y;xbC?^0HJ)a>NNrl47^g}a}vKB`HR zPpZ!Mfjcgm6z&m1gledlA){MZNTMLYPa#ut0*`3bzI~a~t3x#TIU10n5vR6zwxu;c z~f7 zh~`KqMN*;MrRw3ptePFCpA9aDU?f|R3E`;K_JiG@J)b)-pMWL$&3^MXYrU}1QD-Ns zvNUeZzZe!_<8m7wLU{C)F$1rCb>dokc`2;FpnC@;Y=)$=>om-D&nBtYJ#J+0hqcy> zvI5#85zk;jkIOym-cL^x4wRM-!w#}xf2~iw@30%8)gp41bGY*I2w)Z(vH@Z%2)Gw`1@siw^TT3g zfHe#}xW4r|+cPo5gC4k%KUwTF(bG{~dG*MPA=fqzFn80xC_VN-=a$6r>jm zf>H$pl%gVnG^I-s6%i?a@caII&t1=xy=Sjk^R7B;vbV9jyBUCj|I#gUl~4Oa@-4-m z5HU#51%D@Mz&-<_Q?`Et!1n;;@BInHcz@;p(EiH(nQ5AOFPrV&Uz%b14}c+qZ$ zkTdpoM|xA8EdIhG4UmcoM->$m6_k*Y#^-&gzW%NOu09u~)R4x$7peah3Q~|$lv6-T zGUSwGk1EO@Ju0=Y6TkiSSm3|Ywp?Gyz2)mF$~0UFphhF6dQ3IMG?>CFB! z4MetpztAKA6xjYV1Z+S-63}2EXjt;!AmtE>w!l+hURL;NcQf0z90f+V;|?AwC`hC`$POkA|v z0L|He53lc!Ny6)z(A=mDV-7+{2taE|7BNc2o2*k3=i>@7A@qgd!iCF)ac=9B18X{k zRIPPF-bVhv6TLQA#i?w(HSlY@iSTkpzr&x{GznJ?lab>)gB6^f_uSE;xNA zn10{7a4xub{!Tj1Ggp$1c~e6i7}V+cPJHu&NS|k@xRc8;AFwih90h+($p$pd6vCTz zV&Odo1|2n|WQ2c%4xtRaCGulaaq(n2mCtAh6H;sd)4@`jVf-|AMF^mM3C2=*`up0irK`HsZpJ<%=|lr-AraMg%*Ml z1%S&AP%Q8O6f6p08IPH?xWCzKZf61t-!fydpn2bO1zj-2xPj(tjI1C5aA<}AWb-w} z(;i*%0C@^DqYda-YC6>>eKt7P;0MD|9m65wz`BC65kWUK)}4;(8KTgO;|lM^p;9{$ zxvbk7$emC_=9s}JmNhHJ=S&j-w+(>G%A(e z{he4m+XB;}nCZayDZDL&qeesRFbeH1BcySKQl9kdY$Sf&Vttnox`z`TYl0ahCNJC( zqr0zDuviL;vUVBe5WnU?ClMgn%Q~4@EI+Lqp_@sA1Jn>kF%s^eXrusffHGpi4``i) zIGth-I)$IcYC>>ya#FnGKo>*BQotHz07hYi_YMu7$tYgH#Leg<*xQke#sCE-0C0C! zMGOZCsj+c$)IuDch^1rWbIAlFUft9< zIBEc4WaiR}P;b8G5sxaSLb3de8rgS@u{)djMF5T6+e&omt_QxVKPe56SO9S1)M633 z#;L_7evMa)O^UH${*}{{0Lc!T|949v0m-u!LRxHLF^cPeuya|%!Cfd2#OV8zX`jh| z`xv7b|5u(df&$;eB7ns6fAmTNT1Sm)6$YQ)hO!pG>!)Bi-~t$jWCv?T*P}7hn5`J| zRr`&LgK@dRjAwLl4n_}#{u_nSqiI~EWRbpgUy<&?Npku<%@mQ&C`MzNnfgZTgIs1T z=Y;__HaJiqomOB1oZP(suxjE+zI715rsWb%Cg5Q)tZCrD(SrbkmHYxFUGLES*ui{J zXfjG}11-=cA2ldzmZQ&vW<8Bqd&2Y^my-U9<7deJXg^kX_% z$>jYRM!=d`L9;kiM~4uTTu|LIF!c?>x8JP)LonF>4%&|lql>`)9J%Fm6sYkE5@^eGq2$&_eDdE{|s z#a7fwuuCH%JCq;T;6!~!f;qt$BW>Jd2feQj%%!Lx4Xwq-T4_{ad}}<)dR#1EV|d6i zaQlXR_O?mx(P39VEoG^V{Lk1wWl+r5CFUsV!xKzB&nI?kqI#AEr$j>o4ul;)Hakr? zkv@F%fqB{mp1^ma?}tl|w|jT^BzJyQ{qZC7BcWP@{Z~VO{6o(7wF~dBYrqye^h8hE zzV1n=JkKqr+!$aREOi;dwZ>wBbNwRzqR-j*70%-3>WP^p`MiJf0=P#5jaiX?M(Wr; z$~Ui5=|S`FIpfY<8*!3$=qfwW>zi=&tZ4Frr!9&!?sr~-_Gqy))d8I{H4<4wO8c7}lrM{>Wg%#{Gks=(hRX04FJxaXOepMgQ8XfRmsZ ztAE;0m8H)I3sxShLd(-W>RsSC3OEPWsar#4{j;j%iR3--y#0ub4vjBIG1wupQ3HJG zhrZnyS06fEy^+PU84k`&Oxqhqo@(p4A3Q|r$3VyW!{}q9*6+%wX%Q!A4nhQJDV_VjMa_rc>f{HtDhy_e!@9dzaK(;S1Y00P|kEQ z9@aklxqIumpCd4RR-%0}vSZ;bA2)s=-)j7q1fSLR)>UR~x8Ur1(Z?#? zlk;1#v z?36|I?)yN4PtBJiZvWG~OrB?%RsdW@P2~ygGP#D>_RY|k=otucbc}bW>o<({z7OYn z<>@8e8*tK9bFNrGv~ay!za`wNLU>o({^z)-CwNr!9)`bOj5~m4qIrK{hV?u`QJvPG zFw4a>{eeOIOmQ3`!yEv}OUDGNqQIk9E_gInp69+{-c4#&Z+t|u@0_1%FWhA5~LQKV%mXL=z z&3tFIC38HTmf%oWgS}DjLRHe+O_d?4#z4(y&RzO(VI10RabxASZnGTM6R4dPj`nNE z1exlJ;k61J7O|e~gn2vvlwH)w%OBxuo^`{1h~QtJ9;-Zs$w5Db>Ye;uc9xFp_uAWef@ z7$bv0tiH8A@bu~6_cc+3+O8_)Q$9Q>?+6EH#E6nauU{dKy>f5PXZh_9J%QEmO`<2j z54=_#kJV>-ogKgn&(&|JpRUNbe>1gi!QE$sb&W9{V7fZJ026UrAu#KCS0$!<*W$R& z=&8a2lita&>I|0*gy|GOAwji5lNra|gtlL8+}g@Elfn` z3fUlme4Y;p@(??w2d|v;qH=bO9{-11aNw~b zF1$Im{IQdzku|INl=ZF0DYa}(zrCOh6O*QXE0N~ey~cBu>^xs{;CwT0a!~V56l-yp ztx+RW9?NDX&E6EKJ^=|GuroAKtZQ6`mLAqB5F!qD@otUkGimleV+DL+WQ3}82r^qD z88&k&gX66p=a279I@X5NKT|AgRx$SuCHd%$Eg!K&vlK|oG)J{q6D7Z%svwcKZly4{ z`mZe*=oY^4mM%Uyo}qnghcqU*7Rr=?H%`3U4dJZ>J~B~4y8*jhg7jzgz5JCS?Z*v1 zs}RnxRg3S$9uoH*gs_`HU6^3^eXr;7^w#pd|Lote0SE)9J3dCNz*SWn6WeH8K02AZ zTto*BZ_DEqYd?U_h{w`qhmvnX^l{eyR;9h@GCLdGY1?P2VDjy?&KvHVLHE5pfy+7B zw(Xy2wnx3{{byi^Hsi5=`7XJn1(X7UX8#+fUhaC|iVhmAbr-pB_#Kn_CSYwNGF9lO$)faaj_P@_h z)*YsM^)6Y+95v^&p;vQfua;+CrG{NB9t)QK$-FH~7D&ufJ#~I0Xf`XcRC@As#k#c3YEm{pC@v({Y_79-zWd)oP#QZ5cWzBn)u#>Gt;jeks4(&J;^P_gUl=dQx6Lb1`@sN}27B|el-VW-BQy|4RK zVSZVQ?&?Ln%srCF52-Y3YLf{uA-2pnOx)R2AM4l&nl@|u9$SJjUMwPgl@%Ga^BOQD z&3|TTxSN9Hd?CHE0B!j2O)$6;Eod4O_vk?G^tP<$)GuIt%t@;dA`VUq^tYsTomab| zuBD{yIN9dA4G*qk5|cc$8r^U8angEbvP24@`~i6tWPVc9=D{5C>5C8uscGMH+;#I6 zcZq`shWt3}?b(0-6Ew4YqN-$0@?yta$>>?HbC)k}L}$U}4{W@`H@`D@uvNn&k^8Eg zJb&`+tLv(`J|Clew4O@v!SM=f%~X6DKKg9#I}7-Xx zJbJa`&o`wLy#s^3Plr}(qpn;Ma-c+Jsg+r^?O{WiPQKuzNq+nDQ{=F$j&nWLT1oR- zweahxXr4lkH6zG#+;X4nLAX;&PDLZ##O&_ZlJk~Xu;x2*vUH*D<+{#iY<5R<4T^%T zF6N2(#~`mfwGfs4$Zy=E^i9&+RMqW7>E-JyRLBDiCsI zwuVKWCF^G6dB?+cbs(i$GhoUy|9rAxX~mJ^po~P7;FzJ3#+s0&`b+3n1#T~Fo_+~` z8a;p_B=*e-qEfKklG9t9zx z$=_GP&p1>$2A{p>7yRy8k7{Is&(i%@+~ni2(hs)ym5)9awlM8YRbN8e6mi;L_Qxi^ zo}TD4_cHqUSWnFArnG(flc#P5;&BE?$}?%O=eWM|tW>|G8H3N~M z$pAD6b#v`OYsKn+UBITOh6?7kY1QYXJb?GX5Fp`NLU)&jkx>&%t;vAD)upPWZ)!Zf zN#0+M-jx*>sQR6ADDd0iEkiGK0wG;0p8V8rK)U+HFP)?-0tzRnMiHkD?G-ykPu3lL zkpJ@QwYO$oJI5b|HoJ**xXvjMItAsXIC=D-jQqY9KPdRN^idM=B2|Iw)YAD8sDh}e ziV|ALkovgQCum!5J8(MQ_OP|eR9VCMDnwa2SZ;?V4;L4y7%hak z5-lu2L(oG{E8)Ro4I|RX3QbMrpUbPfwU4V8xrQE=T>2trw*2~pf3cj*C)pbY-&OWl zj{YYZBAnYWUJ+AFN$+MJBA6D)?7<#tX0w zwx1A0SV;H`5X>|SdOr3eZhgPzBy>(7Y4^q@T4l_fv*fFOpCq}F?_VBWTO7Z^AFHdX zZOhiY6@Oh2d2&__pJhBHa+NI7|5g6g`H@hM;6Pfv*go8&VqI+8i)e=`f0i*h!zMiToxwEOd@%ES{*` z;M@PfY05l*z=juuu=+i(B0q>c9qwv)?*(6RRe*p>jIuFo;xdpd5X?rN3ZJlp%sZJr z67H(25L{rg>`URHV}uq{h)!IA;m3FRw)5^*aBJ1E4n8#6iwG=OM3|ZL7GmTE??l^o zSg^Da#A3L?%>2@$vTE4}Cf{3rR5^~ad+1rUr|x+MpZA@4pfvIJx*+{*(PBJ7%p(x% zraTPNkJoQTjpXgH)%nzw{ur*@-IBWWC5oX-tshn^ zcP>iHT~$M$u;IJ5*Iw51W_yV~5CVa)t z_D{p3%;?IJgt<|VFxqKEu~w9PaAvv+zT1>DBcK2;oNQr}^`779}Mmv89PTp*Hc9`EPx z&0nP*#vEXL(+7Wjw&hx)~!7(8B4trbt?C1MOUZ`Z@Wu#d0 zf6jQb&gL+Ov*ihKrJbO_l=n_5L_Yb4$J;T#azrCDC6db_k@;ph_DuBcij(HcLF??> zX|N7^(Q)>%mkEGqo4v^;M6-i@;M)|D)q|eN?h;yzYZz8KSl|)!;L)Q17Q(sPJv|WE zk*Z=bBluT(q_*j&uN9NW7S&3Trgl=q;VJ2)Tl%bVS7oQD_^lxi@dTCnb z^bK3mC~m|amgIJf7IfWNTSNSwaZ}3`d+=GzUx_uuX0x| zrLVkx@p(d?UGtZ3PVMCecX!x<_S|U@j$_?v^-T(o+R&d zc~vkCx~KzU7@NEM+)WG%CarL z9@?QOab)~;!3Idr75M;H{k2SnpPJ)?exOeEy zg;^2FnQQA*U-VWEC*;<+C*owL+dmgs`VS#bU0O>?=zH+V=}d)T-XDeEwblpi&Pwx7 z?ukqh&oxeVLBd(nga)Nqb_?IJy*XXhU#+}UH3Wl44plD%WsHTbaJ|As8Qs{sM~!k* z;oP)eZ2D@#Q*+1?Bt1UrA+rM+I{$4e?0WvN5QAhu!GByG^ELeFvKi6LR=`Mz8^ znFeFm8KlbBx9CUbl7LZ0$KL_kQ|vuWP{dNfAzcJ##D;e9nHp z6s#79?fYI|C;BZU@jiTj&7qtZ$v2;+qmPC{eWxy0`t$QyDI)^4hnw8XS}&`6OmVCm z1>7*;PnvRF>dcQ-&lXiLeVZZ%n!amWKmU^`gWOGOPZ*$$-XRSmD5C|QjQ=Bj&fXS9 z)=uI0rHQzex*s{YK7cpLPFdbrLeXhQ;!gDoIrrp~30;8uDkOaG$(tR!LqV(O%s)Zg za?73s>L+BLe)^0h7gcxi#NtDehNgI*g%*wF^t@z0w$|bj23(X-xbto9qYO9mlTIz| z^AIunuydC|)%@B^pDDkvSgYFMm1r&62X$W_FtKVg2(?=#s0~i$BbfieHj6Ne;dazU z@Nv@hC7bfE?!4_KLxG``{q#TqRw=i!_Y4*~w^2k6d94KT+(eh( XfEIe(MuRlEp*=ovb#$)r0}J?n96v8N literal 0 HcmV?d00001 diff --git a/tools/UpdatePaths/Scripts/28987_pig.txt b/tools/UpdatePaths/Scripts/28987_pig.txt new file mode 100644 index 00000000000..77a3c75aaab --- /dev/null +++ b/tools/UpdatePaths/Scripts/28987_pig.txt @@ -0,0 +1 @@ +/mob/living/simple_animal/pig/@SUBTYPES : /mob/living/basic/pig/@SUBTYPES{@OLD}