diff --git a/code/__DEFINES/ai/ai_defines.dm b/code/__DEFINES/ai/ai_defines.dm index 8ba7fec642c..6edf0e98531 100644 --- a/code/__DEFINES/ai/ai_defines.dm +++ b/code/__DEFINES/ai/ai_defines.dm @@ -58,3 +58,7 @@ /// This subtree should cancel any further planning, (Including from other subtrees) #define SUBTREE_RETURN_FINISH_PLANNING 1 + +///For JPS pathing, the maximum length of a path we'll try to generate. Should be modularized depending on what we're doing later on +#define AI_MAX_PATH_LENGTH 30 // 30 is possibly overkill since by default we lose interest after 14 tiles of distance, but this gives wiggle room for weaving around obstacles +#define AI_BOT_PATH_LENGTH 60 diff --git a/code/__DEFINES/ai/blackboard_defines.dm b/code/__DEFINES/ai/blackboard_defines.dm index b317d977c87..696c2bcc70d 100644 --- a/code/__DEFINES/ai/blackboard_defines.dm +++ b/code/__DEFINES/ai/blackboard_defines.dm @@ -3,86 +3,138 @@ // Movement /// The closest we're allowed to / required to get to a target. -#define BB_CURRENT_MIN_MOVE_DISTANCE "BB_min_move_distance" +#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" +#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" +#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" +#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" +#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" +#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 BB_BASIC_MOB_FLEE_DISTANCE "BB_BASIC_FLEE_DISTANCE" #define DEFAULT_BASIC_FLEE_DISTANCE 9 // Searching /// key holding a range to look for stuff in -#define BB_SEARCH_RANGE "BB_search_range" +#define BB_SEARCH_RANGE "BB_SEARCH_RANGE" // Targeting +/// Generic key for a non-specific targeted action +#define BB_TARGETED_ACTION "BB_TARGETED_ACTION" +/// Generic key for a non-specific action +#define BB_GENERIC_ACTION "BB_GENERIC_ACTION" /// 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" +#define BB_AGGRO_RANGE "BB_AGGRO_RANGE" /// Key for our current target. -#define BB_BASIC_MOB_CURRENT_TARGET "BB_basic_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" +#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" +#define BB_TARGETING_STRATEGY "BB_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" +#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" +#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" +#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" +#define BB_BASIC_MOB_RETALIATE_LIST "BB_BASIC_MOB_SHITLIST" //Hunting BB keys /// Key that holds our current hunting target -#define BB_CURRENT_HUNTING_TARGET "BB_current_hunting_target" +#define BB_CURRENT_HUNTING_TARGET "BB_CURRENT_HUNTING_TARGET" /// Key that holds our less priority hunting target -#define BB_LOW_PRIORITY_HUNTING_TARGET "BB_low_priority_hunting_target" +#define BB_LOW_PRIORITY_HUNTING_TARGET "BB_LOW_PRIORITY_HUNTING_TARGET" /// Key that holds the cooldown for our hunting subtree #define BB_HUNTING_COOLDOWN(type) "BB_HUNTING_COOLDOWN_[type]" // Food and eating /// list of foods this mob likes -#define BB_BASIC_FOODS "BB_basic_foods" +#define BB_BASIC_FOODS "BB_BASIC_FOODS" /// key holding any food we've found -#define BB_TARGET_FOOD "BB_target_food" +#define BB_TARGET_FOOD "BB_TARGET_FOOD" /// key holding emotes we play after eating -#define BB_EAT_EMOTES "BB_eat_emotes" +#define BB_EAT_EMOTES "BB_EAT_EMOTES" /// key holding the next time we eat -#define BB_NEXT_FOOD_EAT "BB_next_food_eat" +#define BB_NEXT_FOOD_EAT "BB_NEXT_FOOD_EAT" /// key holding our eating cooldown -#define BB_EAT_FOOD_COOLDOWN "BB_eat_food_cooldown" +#define BB_EAT_FOOD_COOLDOWN "BB_EAT_FOOD_COOLDOWN" // Tipped blackboards /// Bool that means a basic mob will start reacting to being tipped in its planning -#define BB_BASIC_MOB_TIP_REACTING "BB_basic_tip_reacting" +#define BB_BASIC_MOB_TIP_REACTING "BB_BASIC_TIP_REACTING" /// the motherfucker who tipped us -#define BB_BASIC_MOB_TIPPER "BB_basic_tip_tipper" +#define BB_BASIC_MOB_TIPPER "BB_BASIC_TIP_TIPPER" /// Is there something that scared us into being stationary? If so, hold the reference here -#define BB_STATIONARY_CAUSE "BB_thing_that_made_us_stationary" +#define BB_STATIONARY_CAUSE "BB_THING_THAT_MADE_US_STATIONARY" ///How long should we remain stationary for? -#define BB_STATIONARY_SECONDS "BB_stationary_time_in_seconds" +#define BB_STATIONARY_SECONDS "BB_STATIONARY_TIME_IN_SECONDS" ///Should we move towards the target that triggered us to be stationary? -#define BB_STATIONARY_MOVE_TO_TARGET "BB_stationary_move_to_target" +#define BB_STATIONARY_MOVE_TO_TARGET "BB_STATIONARY_MOVE_TO_TARGET" /// What targets will trigger us to be stationary? Must be a list. -#define BB_STATIONARY_TARGETS "BB_stationary_targets" +#define BB_STATIONARY_TARGETS "BB_STATIONARY_TARGETS" /// How often can we get spooked by a target? -#define BB_STATIONARY_COOLDOWN "BB_stationary_cooldown" +#define BB_STATIONARY_COOLDOWN "BB_STATIONARY_COOLDOWN" + +// minebot keys +/// key that stores our toggle light ability +#define BB_MINEBOT_LIGHT_ABILITY "BB_MINEBOT_LIGHT_ABILITY" +/// key that stores our dump ore ability +#define BB_MINEBOT_DUMP_ABILITY "BB_MINEBOT_DUMP_ABILITY" +/// key that stores our target turf +#define BB_TARGET_MINERAL_TURF "BB_TARGET_MINERAL_TURF" +/// key that stores list of the turfs we ignore +#define BB_BLACKLIST_MINERAL_TURFS "BLACKLIST_MINERAL_TURFS" +/// key that stores the previous blocked wall +#define BB_PREVIOUS_UNREACHABLE_WALL "BB_PREVIOUS_UNREACHABLE_WALL" +/// key that stores our mining mode +#define BB_AUTOMATED_MINING "BB_AUTOMATED_MINING" +/// key that stores the nearest dead human +#define BB_NEARBY_DEAD_MINER "BB_NEARBY_DEAD_MINER" +/// key that holds the drone we defend +#define BB_DRONE_DEFEND "BB_DEFEND_DRONE" +/// key that holds the minimum distance before we flee +#define BB_MINIMUM_SHOOTING_DISTANCE "BB_MINIMUM_SHOOTING_DISTANCE" +/// key that holds the miner we must befriend +#define BB_MINER_FRIEND "BB_MINER_FRIEND" +/// should we auto protect? +#define BB_MINEBOT_AUTO_DEFEND "BB_MINEBOT_AUTO_DEFEND" +/// gibtonite wall we need to run away from +#define BB_MINEBOT_GIBTONITE_RUN "BB_MINEBOT_GIBTONITE_RUN" + +/// Blackboard field for the most recent command the pet was given +#define BB_ACTIVE_PET_COMMAND "BB_ACTIVE_PET_COMMAND" + +/// Blackboard field for what we actually want the pet to target +#define BB_CURRENT_PET_TARGET "BB_CURRENT_PET_TARGET" +/// Blackboard field for how we target things, as usually we want to be more permissive than normal +#define BB_PET_TARGETING_STRATEGY "BB_PET_TARGETING" +/// List of UIDs to mobs this mob is friends with, will follow their instructions and won't attack them +#define BB_FRIENDS_LIST "BB_FRIENDS_LIST" +/// List of strings we might say to encourage someone to make better choices. +#define BB_OWNER_SELF_HARM_RESPONSES "BB_SELF_HARM_RESPONSES" +/// List of strings used to apologize for attacking friends. +#define BB_OWNER_FRIENDLY_FIRE_APOLOGIES "BB_FF_APOLOGIES" + // 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]" + +/// key that tells the wall we will mine +#define BB_TARGET_MINERAL_WALL "BB_TARGET_MINERAL_WALL" +/// key that holds the ore we will eat +#define BB_ORE_TARGET "BB_ORE_TARGET" +/// which ore types we will not eat +#define BB_ORE_IGNORE_TYPES "BB_ORE_IGNORE_TYPES" diff --git a/code/__DEFINES/dcs/atom_signals.dm b/code/__DEFINES/dcs/atom_signals.dm index 17b7ce1200e..54934917ece 100644 --- a/code/__DEFINES/dcs/atom_signals.dm +++ b/code/__DEFINES/dcs/atom_signals.dm @@ -162,3 +162,4 @@ #define COMSIG_LENS_ATTACH "lens_attach" /// When using an insert on an item that can accept an insert #define COMSIG_INSERT_ATTACH "insert_attach" +#define COMSIG_MINE_EXPOSE_GIBTONITE "mine_expose_gibtonite" diff --git a/code/__DEFINES/dcs/mob_signals.dm b/code/__DEFINES/dcs/mob_signals.dm index fa194f0ef62..9207e195dc2 100644 --- a/code/__DEFINES/dcs/mob_signals.dm +++ b/code/__DEFINES/dcs/mob_signals.dm @@ -242,3 +242,19 @@ #define MOVE_ARG_DIRECTION 2 #define COMSIG_LIVING_RESTING "living_resting" + +/// From /mob/living/befriend() : (mob/living/new_friend) +#define COMSIG_LIVING_BEFRIENDED "living_befriended" + +/// From /mob/living/unfriend() : (mob/living/old_friend) +#define COMSIG_LIVING_UNFRIENDED "living_unfriended" + +/// From the base of /datum/component/callouts/proc/callout_picker(mob/user, atom/clicked_atom): (datum/callout_option/callout, atom/target) +#define COMSIG_MOB_CREATED_CALLOUT "mob_created_callout" + +///from the ranged_attacks component for basic mobs: (mob/living/basic/firer, atom/target, modifiers) +#define COMSIG_BASICMOB_PRE_ATTACK_RANGED "basicmob_pre_attack_ranged" + #define COMPONENT_CANCEL_RANGED_ATTACK COMPONENT_CANCEL_ATTACK_CHAIN //! Cancel to prevent the attack from happening + +///from the ranged_attacks component for basic mobs: (mob/living/basic/firer, atom/target, modifiers) +#define COMSIG_BASICMOB_POST_ATTACK_RANGED "basicmob_post_attack_ranged" diff --git a/code/__DEFINES/dcs/movable_signals.dm b/code/__DEFINES/dcs/movable_signals.dm index 55df1faf213..d4b07c5e9b1 100644 --- a/code/__DEFINES/dcs/movable_signals.dm +++ b/code/__DEFINES/dcs/movable_signals.dm @@ -86,3 +86,6 @@ ///signal sent out by an atom when it is no longer pulling something : (atom/pulling) #define COMSIG_ATOM_NO_LONGER_PULLING "movable_no_longer_pulling" + +///from base of /atom/movable/point_at: (atom/A, obj/effect/temp_visual/point/point) +#define COMSIG_MOVABLE_POINTED "movable_pointed" diff --git a/code/__DEFINES/tg_cooldowns.dm b/code/__DEFINES/tg_cooldowns.dm index 81f43e3c134..874210929a7 100644 --- a/code/__DEFINES/tg_cooldowns.dm +++ b/code/__DEFINES/tg_cooldowns.dm @@ -74,3 +74,11 @@ #define COOLDOWN_RESET(cd_source, cd_index) cd_source.cd_index = 0 #define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_index - world.time)) + +///adds to existing cooldown timer if its started, otherwise starts anew +#define COOLDOWN_INCREMENT(cd_source, cd_index, cd_increment) \ + if(COOLDOWN_FINISHED(cd_source, cd_index)) { \ + COOLDOWN_START(cd_source, cd_index, cd_increment); \ + return; \ + } \ + cd_source.cd_index += (cd_increment); diff --git a/code/__HELPERS/mob_helpers.dm b/code/__HELPERS/mob_helpers.dm index b8022082d3a..66b81649377 100644 --- a/code/__HELPERS/mob_helpers.dm +++ b/code/__HELPERS/mob_helpers.dm @@ -760,3 +760,74 @@ GLOBAL_LIST_EMPTY(do_after_once_tracker) /// rounds value to limited symbols after the period for organ damage and other values /proc/round_health(health) return round(health, 0.01) + +/// Takes in an associated list (key `/datum/action` typepaths, value is the AI +/// blackboard key) and handles granting the action and adding it to the mob's +/// AI controller blackboard. This is only useful in instances where you don't +/// want to store the reference to the action on a variable on the mob. You can +/// set the value to null if you don't want to add it to the blackboard (like in +/// player controlled instances). Is also safe with null AI controllers. Assumes +/// that the action will be initialized and held in the mob itself, which is +/// typically standard. +/mob/proc/grant_actions_by_list(list/input) + if(length(input) <= 0) + return + + for(var/action in input) + var/datum/action/ability = new action(src) + ability.Grant(src) + + var/blackboard_key = input[action] + if(isnull(blackboard_key)) + continue + + ai_controller?.set_blackboard_key(blackboard_key, ability) + +/** + * [/proc/ran_zone] but only returns bodyzones that the mob actually has. + * + * * `blacklisted_parts` allows you to specify zones that will not be chosen. + * e.g.: list(`BODY_ZONE_CHEST`, `BODY_ZONE_R_LEG`). **Blacklisting + * `BODY_ZONE_CHEST` is really risky since it's the only bodypart guaranteed + * to always exist. Only do that if you're certain they have limbs, otherwise + * we'll crash!** + * + * * [/proc/ran_zone] has a base `prob(80)` to return the `base_zone` (or if null, + * `BODY_ZONE_CHEST`) vs something in our generated list of limbs. This + * probability is overriden when either blacklisted_parts contains + * BODY_ZONE_CHEST and we aren't passed a base_zone (since the default + * fallback for ran_zone would be the chest in that scenario), or if + * even_weights is enabled. You can also manually adjust this probability by + * altering `base_probability`. + * + * * even_weights - ran_zone has a 40% chance (after the prob(80) mentioned + * above) of picking a limb, vs the torso & head which have an additional 10% + * chance. Setting even_weights to TRUE will make it just a straight up pick() + * between all possible bodyparts. + */ +/mob/proc/get_random_valid_zone(base_zone, base_probability = 80, list/blacklisted_parts, even_weights, bypass_warning) + return BODY_ZONE_CHEST // Pass the default of check_zone to be safe. + +/mob/living/carbon/human/get_random_valid_zone(base_zone, base_probability = 80, list/blacklisted_parts, even_weights, bypass_warning) + var/list/limbs = list() + for(var/obj/item/organ/limb as anything in bodyparts) + var/limb_zone = limb.parent_organ // cache the parent organ since we're gonna check it a ton. + if(limb_zone in blacklisted_parts) + continue + if(even_weights) + limbs[limb_zone] = 1 + continue + if(limb_zone == BODY_ZONE_CHEST || limb_zone == BODY_ZONE_HEAD) + limbs[limb_zone] = 1 + else + limbs[limb_zone] = 4 + + if(base_zone && !(check_zone(base_zone) in limbs)) + base_zone = null // check if the passed zone is infact valid + + var/chest_blacklisted + if(BODY_ZONE_CHEST in blacklisted_parts) + chest_blacklisted = TRUE + if(bypass_warning && length(limbs)) + CRASH("limbs is empty and the chest is blacklisted. this may not be intended!") + return (((chest_blacklisted && !base_zone) || even_weights) ? pickweight(limbs) : ran_zone(base_zone, base_probability, limbs)) diff --git a/code/_onclick/other_mobs.dm b/code/_onclick/other_mobs.dm index 552c4353513..cc40581db96 100644 --- a/code/_onclick/other_mobs.dm +++ b/code/_onclick/other_mobs.dm @@ -67,6 +67,7 @@ return FALSE resolve_unarmed_attack(target, modifiers) + return TRUE /mob/living/simple_animal/hostile/UnarmedAttack(atom/A) target = A 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 index bead4a4eac7..010a7b1c431 100644 --- a/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/basic_attacking.dm @@ -124,7 +124,7 @@ 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) +/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) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/befriend_target.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/befriend_target.dm new file mode 100644 index 00000000000..db09d11dd8e --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/befriend_target.dm @@ -0,0 +1,19 @@ +///behavior to befriend any targets +/datum/ai_behavior/befriend_target + +/datum/ai_behavior/befriend_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key, befriend_message) + var/mob/living/living_pawn = controller.pawn + var/mob/living/living_target = controller.blackboard[target_key] + if(QDELETED(living_target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + living_pawn.befriend(living_target) + var/befriend_text = controller.blackboard[befriend_message] + if(befriend_text) + to_chat(living_target, "[living_pawn] [befriend_text]") + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/befriend_target/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.clear_blackboard_key(target_key) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm new file mode 100644 index 00000000000..9067df3e63f --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/travel_towards.dm @@ -0,0 +1,53 @@ +/** + * # Travel Towards + * Moves towards the atom in the passed blackboard key. + * Planning continues during this action so it can be interrupted by higher priority actions. + */ +/datum/ai_behavior/travel_towards + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + /// If true we will get rid of our target on completion + var/clear_target = FALSE + /// If desired, an alternate [/datum/ai_movement] to use for movement/pathfinding. + var/new_movement_type + +/datum/ai_behavior/travel_towards/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target, new_movement_type) + +/datum/ai_behavior/travel_towards/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/travel_towards/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + if(clear_target) + controller.clear_blackboard_key(target_key) + if(new_movement_type) + controller.change_ai_movement_type(initial(controller.ai_movement)) + +/datum/ai_behavior/travel_towards/stop_on_arrival + clear_target = TRUE + +/datum/ai_behavior/travel_towards/adjacent + required_distance = 1 + +/** + * # Travel Towards Atom + * Travel towards an atom you pass directly from the controller rather than a blackboard key. + * You might need to do this to avoid repeating some checks in both a controller and an action. + */ +/datum/ai_behavior/travel_towards_atom + required_distance = 0 + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/travel_towards_atom/setup(datum/ai_controller/controller, atom/target_atom) + . = ..() + if(isnull(target_atom)) + return FALSE + set_movement_target(controller, target_atom) + +/datum/ai_behavior/travel_towards_atom/perform(seconds_per_tick, datum/ai_controller/controller, atom/target_atom) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED diff --git a/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm new file mode 100644 index 00000000000..0744d33acea --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/mine_walls.dm @@ -0,0 +1,31 @@ +/datum/ai_behavior/find_mineral_wall + +/datum/ai_behavior/find_mineral_wall/perform(seconds_per_tick, datum/ai_controller/controller, found_wall_key) + var/mob/living_pawn = controller.pawn + + for(var/turf/simulated/mineral/potential_wall in oview(9, living_pawn)) + if(!check_if_mineable(controller, potential_wall)) // check if its surrounded by walls + continue + controller.set_blackboard_key(found_wall_key, potential_wall) // closest wall first! + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + +/// Check to see if we can get to and mine the turf the mineral is in +/datum/ai_behavior/find_mineral_wall/proc/check_if_mineable(datum/ai_controller/controller, turf/target_wall) + var/mob/living/source = controller.pawn + var/direction_to_turf = get_dir(target_wall, source) + if(!IS_DIR_DIAGONAL(direction_to_turf)) + return TRUE + var/list/directions_to_check = list() + for(var/direction_check in GLOB.cardinal) + if(direction_check & direction_to_turf) + directions_to_check += direction_check + + for(var/direction in directions_to_check) + var/turf/test_turf = get_step(target_wall, direction) + if(isnull(test_turf)) + continue + if(!test_turf.is_blocked_turf(source_atom = source)) + return TRUE + return FALSE 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 index e5901af13c9..e7266d86363 100644 --- a/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_attack_target.dm @@ -11,3 +11,14 @@ 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. + +/datum/ai_planning_subtree/basic_ranged_attack_subtree + operational_datums = list(/datum/component/ranged_attacks) + var/datum/ai_behavior/basic_ranged_attack/ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack + +/datum/ai_planning_subtree/basic_ranged_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(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + return SUBTREE_RETURN_FINISH_PLANNING //we are going into battle...no distractions. diff --git a/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm new file mode 100644 index 00000000000..d7d2323621c --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/simple_find_target.dm @@ -0,0 +1,19 @@ +/datum/ai_planning_subtree/simple_find_target + /// Variable to store target in + var/target_key = BB_BASIC_MOB_CURRENT_TARGET + +/datum/ai_planning_subtree/simple_find_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + controller.queue_behavior(/datum/ai_behavior/find_potential_targets, target_key, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + +// Prevents finding a target if a human is nearby +/datum/ai_planning_subtree/simple_find_target/not_while_observed + +/datum/ai_planning_subtree/simple_find_target/not_while_observed/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + for(var/mob/living/carbon/human/watcher in hearers(11, controller.pawn)) + if(watcher.stat != DEAD) + return + return ..() + +/datum/ai_planning_subtree/simple_find_target/to_flee + target_key = BB_BASIC_MOB_FLEE_TARGET diff --git a/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm b/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm new file mode 100644 index 00000000000..f16bc68ca60 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_subtrees/use_mob_ability.dm @@ -0,0 +1,33 @@ +/** + * Simple behaviours which simply try to use an ability whenever it is available. + * For something which wants a target try `targeted_mob_ability`. + */ +/datum/ai_planning_subtree/use_mob_ability + /// Blackboard key for the ability + var/ability_key = BB_GENERIC_ACTION + /// Behaviour to perform using ability + var/use_ability_behavior = /datum/ai_behavior/use_mob_ability + /// If true we terminate planning after trying to use the ability. + var/finish_planning = FALSE + +/datum/ai_planning_subtree/use_mob_ability/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!ability_key) + CRASH("You forgot to tell this mob where to find its ability") + + var/datum/action/using_action = controller.blackboard[ability_key] + if(!using_action?.IsAvailable()) + return + + controller.queue_behavior(use_ability_behavior, ability_key) + if(finish_planning) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/use_mob_ability + +/datum/ai_behavior/use_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key) + var/datum/action/using_action = controller.blackboard[ability_key] + if(QDELETED(using_action)) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED + if(using_action.Trigger()) + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED diff --git a/code/datums/ai/basic_mobs/hunting_behavior/find_and_hunt_target.dm b/code/datums/ai/basic_mobs/hunting_behavior/find_and_hunt_target.dm new file mode 100644 index 00000000000..6aae13f7b4b --- /dev/null +++ b/code/datums/ai/basic_mobs/hunting_behavior/find_and_hunt_target.dm @@ -0,0 +1,52 @@ +/** + * Tells the AI to find a certain target nearby to hunt. + * If a target has been found, we will start to move towards it, and eventually attack it. + */ +/datum/ai_planning_subtree/find_and_hunt_target + /// What key in the blacbkboard do we store our hunting target? + /// If you want to have multiple hunting behaviors on a controller be sure that this is unique + var/target_key = BB_CURRENT_HUNTING_TARGET + /// What behavior to execute if we have no target + var/finding_behavior = /datum/ai_behavior/find_hunt_target + /// What behavior to execute if we do have a target + var/hunting_behavior = /datum/ai_behavior/hunt_target + /// What targets we're hunting for + var/list/hunt_targets + /// In what radius will we hunt + var/hunt_range = 2 + /// What are the chances we hunt something at any given moment + var/hunt_chance = 100 + ///do we finish planning subtree + var/finish_planning = TRUE + +/datum/ai_planning_subtree/find_and_hunt_target/New() + . = ..() + hunt_targets = typecacheof(hunt_targets) + +/datum/ai_planning_subtree/find_and_hunt_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!SPT_PROB(hunt_chance, seconds_per_tick)) + return + + if(controller.blackboard[BB_HUNTING_COOLDOWN(type)] >= world.time) + return + + if(!controller.blackboard_key_exists(target_key)) + controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range) + return + + // We ARE hunting something, execute the hunt. + // Note that if our AI controller has multiple hunting subtrees set, + // we may accidentally be executing another tree's hunt - not ideal, + // try to set a unique target key if you have multiple + + controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN(type)) + if(finish_planning) + return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else + +/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores + target_key = BB_ORE_TARGET + hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/hunt_ores + finding_behavior = /datum/ai_behavior/find_hunt_target/hunt_ores + hunt_targets = list(/obj/item/stack/ore) + hunt_chance = 90 + hunt_range = 9 diff --git a/code/datums/ai/basic_mobs/hunting_behavior/find_hunt_target.dm b/code/datums/ai/basic_mobs/hunting_behavior/find_hunt_target.dm new file mode 100644 index 00000000000..4d3ac43ae88 --- /dev/null +++ b/code/datums/ai/basic_mobs/hunting_behavior/find_hunt_target.dm @@ -0,0 +1,39 @@ +/// Finds a specific atom type to hunt. +/datum/ai_behavior/find_hunt_target + /// is this only meant to search for turf types? + var/search_turf_types = FALSE + +/datum/ai_behavior/find_hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range) + var/mob/living/living_mob = controller.pawn + var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob) + for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt)) + if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick)) + continue + controller.set_blackboard_key(hunting_target_key, possible_dinner) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + +/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius, datum/ai_controller/controller, seconds_per_tick) + if(isliving(dinner)) + var/mob/living/living_target = dinner + if(living_target.stat == DEAD) + return FALSE + + return can_see(source, dinner, radius) + +/datum/ai_behavior/find_hunt_target/hunt_ores + +/datum/ai_behavior/find_hunt_target/hunt_ores/valid_dinner(mob/living/basic/source, obj/item/stack/ore/target, radius, datum/ai_controller/controller) + var/list/forbidden_ore = controller.blackboard[BB_ORE_IGNORE_TYPES] + + if(is_type_in_list(target, forbidden_ore)) + return FALSE + + if(!isturf(target.loc)) + return FALSE + + var/obj/item/pet_target = controller.blackboard[BB_CURRENT_PET_TARGET] + if(target == pet_target) // we are currently fetching this ore for master, dont eat it! + return FALSE + + return can_see(source, target, radius) diff --git a/code/datums/ai/basic_mobs/hunting_behavior/hunt_target.dm b/code/datums/ai/basic_mobs/hunting_behavior/hunt_target.dm new file mode 100644 index 00000000000..4de9964d17b --- /dev/null +++ b/code/datums/ai/basic_mobs/hunting_behavior/hunt_target.dm @@ -0,0 +1,63 @@ +/// Hunts down a specific atom type. +/datum/ai_behavior/hunt_target + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH + /// How long do we have to wait after a successful hunt? + var/hunt_cooldown = 5 SECONDS + /// Do we reset the target after attacking something, so we can check for status changes. + var/always_reset_target = FALSE + +/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) + . = ..() + var/atom/hunt_target = controller.blackboard[hunting_target_key] + if(isnull(hunt_target)) + return FALSE + set_movement_target(controller, hunt_target) + +/datum/ai_behavior/hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) + var/mob/living/hunter = controller.pawn + var/atom/hunted = controller.blackboard[hunting_target_key] + + if(QDELETED(hunted)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + target_caught(hunter, hunted) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/hunt_target/proc/target_caught(mob/living/hunter, atom/hunted) + if(isliving(hunted)) // Are we hunting a living mob? + var/mob/living/living_target = hunted + hunter.manual_emote("chomps [living_target]!") + living_target.investigate_log("has been killed by [key_name(hunter)].", INVESTIGATE_DEATHS) + add_attack_logs(hunter, hunted, "AI controller killed", ATKLOG_ALL) + living_target.death() + + else if(isfood(hunted)) + hunted.attack_animal(hunter) + + else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs. + hunter.manual_emote("chomps [hunted]!") + qdel(hunted) + +/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key) + . = ..() + if(succeeded && hunting_cooldown_key) + controller.set_blackboard_key(hunting_cooldown_key, world.time + hunt_cooldown) + else if(hunting_target_key) + controller.clear_blackboard_key(hunting_target_key) + if(always_reset_target && hunting_target_key) + controller.clear_blackboard_key(hunting_target_key) + +/datum/ai_behavior/hunt_target/interact_with_target + /// Which intent should we use to interact with + var/behavior_intent = INTENT_HARM + +/datum/ai_behavior/hunt_target/interact_with_target/target_caught(mob/living/hunter, atom/hunted) + var/datum/ai_controller/controller = hunter.ai_controller + controller.ai_interact(target = hunted, intent = behavior_intent) + +/datum/ai_behavior/hunt_target/interact_with_target/hunt_ores + always_reset_target = TRUE + +/datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot + always_reset_target = TRUE + hunt_cooldown = 2 SECONDS + behavior_intent = INTENT_HELP diff --git a/code/datums/ai/basic_mobs/hunting_behavior/hunting_behaviors.dm b/code/datums/ai/basic_mobs/hunting_behavior/hunting_behaviors.dm deleted file mode 100644 index cc4a8bc607e..00000000000 --- a/code/datums/ai/basic_mobs/hunting_behavior/hunting_behaviors.dm +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Tells the AI to find a certain target nearby to hunt. - * If a target has been found, we will start to move towards it, and eventually attack it. - */ -/datum/ai_planning_subtree/find_and_hunt_target - /// What key in the blacbkboard do we store our hunting target? - /// If you want to have multiple hunting behaviors on a controller be sure that this is unique - var/target_key = BB_CURRENT_HUNTING_TARGET - /// What behavior to execute if we have no target - var/finding_behavior = /datum/ai_behavior/find_hunt_target - /// What behavior to execute if we do have a target - var/hunting_behavior = /datum/ai_behavior/hunt_target - /// What targets we're hunting for - var/list/hunt_targets - /// In what radius will we hunt - var/hunt_range = 2 - /// What are the chances we hunt something at any given moment - var/hunt_chance = 100 - ///do we finish planning subtree - var/finish_planning = TRUE - -/datum/ai_planning_subtree/find_and_hunt_target/New() - . = ..() - hunt_targets = typecacheof(hunt_targets) - -/datum/ai_planning_subtree/find_and_hunt_target/select_behaviors(datum/ai_controller/controller, seconds_per_tick) - if(!SPT_PROB(hunt_chance, seconds_per_tick)) - return - - if(controller.blackboard[BB_HUNTING_COOLDOWN(type)] >= world.time) - return - - if(!controller.blackboard_key_exists(target_key)) - controller.queue_behavior(finding_behavior, target_key, hunt_targets, hunt_range) - return - - // We ARE hunting something, execute the hunt. - // Note that if our AI controller has multiple hunting subtrees set, - // we may accidentally be executing another tree's hunt - not ideal, - // try to set a unique target key if you have multiple - - controller.queue_behavior(hunting_behavior, target_key, BB_HUNTING_COOLDOWN(type)) - if(finish_planning) - return SUBTREE_RETURN_FINISH_PLANNING //If we're hunting we're too busy for anything else - -/// Finds a specific atom type to hunt. -/datum/ai_behavior/find_hunt_target - ///is this only meant to search for turf types? - var/search_turf_types = FALSE - -/datum/ai_behavior/find_hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, types_to_hunt, hunt_range) - var/mob/living/living_mob = controller.pawn - var/list/interesting_objects = search_turf_types ? RANGE_TURFS(hunt_range, living_mob) : oview(hunt_range, living_mob) - for(var/atom/possible_dinner as anything in typecache_filter_list(interesting_objects, types_to_hunt)) - if(!valid_dinner(living_mob, possible_dinner, hunt_range, controller, seconds_per_tick)) - continue - controller.set_blackboard_key(hunting_target_key, possible_dinner) - return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED - return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED - -/datum/ai_behavior/find_hunt_target/proc/valid_dinner(mob/living/source, atom/dinner, radius, datum/ai_controller/controller, seconds_per_tick) - if(isliving(dinner)) - var/mob/living/living_target = dinner - if(living_target.stat == DEAD) - return FALSE - - return can_see(source, dinner, radius) - -/datum/ai_behavior/find_hunt_target/search_turf_types - search_turf_types = TRUE - -/// Hunts down a specific atom type. -/datum/ai_behavior/hunt_target - behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH - /// How long do we have to wait after a successful hunt? - var/hunt_cooldown = 5 SECONDS - /// Do we reset the target after attacking something, so we can check for status changes. - var/always_reset_target = FALSE - -/datum/ai_behavior/hunt_target/setup(datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) - . = ..() - var/atom/hunt_target = controller.blackboard[hunting_target_key] - if(isnull(hunt_target)) - return FALSE - set_movement_target(controller, hunt_target) - -/datum/ai_behavior/hunt_target/perform(seconds_per_tick, datum/ai_controller/controller, hunting_target_key, hunting_cooldown_key) - var/mob/living/hunter = controller.pawn - var/atom/hunted = controller.blackboard[hunting_target_key] - - if(QDELETED(hunted)) - return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED - target_caught(hunter, hunted) - return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED - -/datum/ai_behavior/hunt_target/proc/target_caught(mob/living/hunter, atom/hunted) - if(isliving(hunted)) // Are we hunting a living mob? - var/mob/living/living_target = hunted - hunter.manual_emote("chomps [living_target]!") - living_target.investigate_log("has been killed by [key_name(hunter)].", INVESTIGATE_DEATHS) - living_target.death() - - else if(isfood(hunted)) - hunted.attack_animal(hunter) - - else // We're hunting an object, and should delete it instead of killing it. Mostly useful for decal bugs like ants or spider webs. - hunter.manual_emote("chomps [hunted]!") - qdel(hunted) - -/datum/ai_behavior/hunt_target/finish_action(datum/ai_controller/controller, succeeded, hunting_target_key, hunting_cooldown_key) - . = ..() - if(succeeded && hunting_cooldown_key) - controller.set_blackboard_key(hunting_cooldown_key, world.time + hunt_cooldown) - else if(hunting_target_key) - controller.clear_blackboard_key(hunting_target_key) - if(always_reset_target && hunting_target_key) - controller.clear_blackboard_key(hunting_target_key) - -/datum/ai_behavior/hunt_target/interact_with_target - /// Which intent should we use to interact with - var/behavior_intent = INTENT_HARM - -/datum/ai_behavior/hunt_target/interact_with_target/target_caught(mob/living/hunter, obj/structure/cable/hunted) - var/datum/ai_controller/controller = hunter.ai_controller - controller.ai_interact(target = hunted, intent = behavior_intent) diff --git a/code/datums/ai/movement/ai_movement_jps.dm b/code/datums/ai/movement/ai_movement_jps.dm new file mode 100644 index 00000000000..12608f8a7cd --- /dev/null +++ b/code/datums/ai/movement/ai_movement_jps.dm @@ -0,0 +1,44 @@ +/** + * This movement datum represents smart-pathing + */ +/datum/ai_movement/jps + max_pathing_attempts = 20 + var/maximum_length = AI_MAX_PATH_LENGTH + /// how we deal with diagonal movement, whether we try to avoid them or follow through with them + var/diagonal_flags = DIAGONAL_REMOVE_CLUNKY + +/datum/ai_movement/jps/start_moving_towards(datum/ai_controller/controller, atom/current_movement_target, min_distance) + . = ..() + var/atom/movable/moving = controller.pawn + var/delay = controller.movement_delay + + var/datum/move_loop/has_target/jps/loop = GLOB.move_manager.jps_move(moving, + current_movement_target, + delay, + repath_delay = 0.5 SECONDS, + // TODO: "simulated_only" refers to only considering atmos-simulated tiles in pathfinding. + // At some point we need to make it easy to tell, here, if a given atom controlled + // by an AI should consider itself "spaceproof" enough to traverse turfs + // without air, but should be definintely keyed to every mob's own survival + // requirements. + simulated_only = FALSE, + max_path_length = maximum_length, + minimum_distance = controller.get_minimum_distance(), + access = controller.get_access(), + subsystem = SSai_movement, + diagonal_handling = diagonal_flags, + extra_info = controller, + ) + + RegisterSignal(loop, COMSIG_MOVELOOP_PREPROCESS_CHECK, PROC_REF(pre_move)) + RegisterSignal(loop, COMSIG_MOVELOOP_POSTPROCESS, PROC_REF(post_move)) + RegisterSignal(loop, COMSIG_MOVELOOP_JPS_REPATH, PROC_REF(repath_incoming)) + + return loop + +/datum/ai_movement/jps/proc/repath_incoming(datum/move_loop/has_target/jps/source) + SIGNAL_HANDLER // COMSIG_MOVELOOP_JPS_REPATH + var/datum/ai_controller/controller = source.extra_info + + source.access = controller.get_access() + source.minimum_distance = controller.get_minimum_distance() diff --git a/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm b/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm index 33bba4749e2..9ff1c493ef2 100644 --- a/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm +++ b/code/datums/ai/targeting_strategy/basic_targeting_strategy.dm @@ -80,3 +80,6 @@ if(controller.blackboard[BB_ALWAYS_IGNORE_FACTION] || controller.blackboard[BB_TEMPORARILY_IGNORE_FACTION]) return FALSE return living_mob.faction_check_mob(the_target, exact_match = check_factions_exactly) + +/datum/targeting_strategy/basic/always_check_factions + check_factions_exactly = TRUE diff --git a/code/datums/ai/targeting_strategy/dont_target_friends.dm b/code/datums/ai/targeting_strategy/dont_target_friends.dm new file mode 100644 index 00000000000..44910ca5217 --- /dev/null +++ b/code/datums/ai/targeting_strategy/dont_target_friends.dm @@ -0,0 +1,46 @@ +/// Don't target an atom in our friends list (or turfs), anything else is fair game +/datum/targeting_strategy/basic/not_friends + /// If we can try to wall/mineral turfs or not + var/attack_closed_turf = FALSE + /// If we want it to attack ALL OBJECTS + var/attack_obj = FALSE + +/// Returns true or false depending on if the target can be attacked by the mob +/datum/targeting_strategy/basic/not_friends/can_attack(mob/living/living_mob, atom/target, vision_range) + if(attack_closed_turf && (iswallturf(target) || ismineralturf(target))) + return TRUE + + if(attack_obj && isobj(target)) + var/obj/target_obj = target + if(!(target_obj.resistance_flags & INDESTRUCTIBLE)) + return TRUE + + if(living_mob?.ai_controller?.blackboard[BB_FRIENDS_LIST] && (target in living_mob.ai_controller.blackboard[BB_FRIENDS_LIST])) + return FALSE + + return ..() + +/// friends dont care about factions +/datum/targeting_strategy/basic/not_friends/faction_check(mob/living/living_mob, mob/living/the_target) + return FALSE + +/datum/targeting_strategy/basic/not_friends/attack_closed_turfs + attack_closed_turf = TRUE + +// Allows mobs to target objs. In case you want your mob to target these without going after walls +/datum/targeting_strategy/basic/not_friends/attack_objs + attack_obj = TRUE + +// Let us target everything that needs targeting, just like they used to. +/datum/targeting_strategy/basic/not_friends/attack_everything + attack_closed_turf = TRUE + attack_obj = TRUE + +/// Subtype that allows us to target items while deftly avoiding attacking our allies. Be careful when it comes to targeting items as an AI could get trapped targeting something it can't destroy. +/datum/targeting_strategy/basic/not_friends/allow_items + +/datum/targeting_strategy/basic/not_friends/allow_items/can_attack(mob/living/living_mob, atom/the_target, vision_range) + . = ..() + if(isitem(the_target)) + // trust fall exercise + return TRUE diff --git a/code/datums/components/pet_commands/obeys_commands.dm b/code/datums/components/pet_commands/obeys_commands.dm new file mode 100644 index 00000000000..b2f927a5992 --- /dev/null +++ b/code/datums/components/pet_commands/obeys_commands.dm @@ -0,0 +1,60 @@ +/** + * # Obeys Commands Component + * Manages a list of pet command datums, allowing you to boss it around + * Creates a radial menu of pet commands when this creature is alt-clicked, if it has any + */ + +/datum/component/obeys_commands + /// List of commands you can give to the owner of this component + var/list/available_commands = list() + +/// The available_commands parameter should be passed as a list of typepaths +/datum/component/obeys_commands/Initialize(list/command_typepaths = list()) + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + var/mob/living/living_parent = parent + if(!living_parent.ai_controller) + return COMPONENT_INCOMPATIBLE + if(!length(command_typepaths)) + CRASH("Initialised obedience component with no commands.") + + for(var/command_path in command_typepaths) + var/datum/pet_command/new_command = new command_path(parent) + available_commands[new_command.command_name] = new_command + +/datum/component/obeys_commands/Destroy(force) + QDEL_LIST_ASSOC_VAL(available_commands) + return ..() + +/datum/component/obeys_commands/RegisterWithParent() + RegisterSignal(parent, COMSIG_LIVING_BEFRIENDED, PROC_REF(add_friend)) + RegisterSignal(parent, COMSIG_LIVING_UNFRIENDED, PROC_REF(remove_friend)) + RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + +/datum/component/obeys_commands/UnregisterFromParent() + UnregisterSignal(parent, list(COMSIG_LIVING_BEFRIENDED, COMSIG_LIVING_UNFRIENDED, COMSIG_PARENT_EXAMINE)) + +/// Add someone to our friends list +/datum/component/obeys_commands/proc/add_friend(datum/source, mob/living/new_friend) + SIGNAL_HANDLER + for(var/command_name as anything in available_commands) + var/datum/pet_command/command = available_commands[command_name] + INVOKE_ASYNC(command, TYPE_PROC_REF(/datum/pet_command, add_new_friend), new_friend) + +/// Remove someone from our friends list +/datum/component/obeys_commands/proc/remove_friend(datum/source, mob/living/old_friend) + SIGNAL_HANDLER + for(var/command_name as anything in available_commands) + var/datum/pet_command/command = available_commands[command_name] + INVOKE_ASYNC(command, TYPE_PROC_REF(/datum/pet_command, remove_friend), old_friend) + +/// Add a note about whether they will follow the instructions of the inspecting mob +/datum/component/obeys_commands/proc/on_examine(mob/living/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + if(source.stat == DEAD || source.stat == UNCONSCIOUS) + return + if(!(user in source.ai_controller?.blackboard[BB_FRIENDS_LIST])) + return + examine_list += "[source.p_they(TRUE)] seem[source.p_s()] happy to see you!" diff --git a/code/datums/components/pet_commands/pet_command.dm b/code/datums/components/pet_commands/pet_command.dm new file mode 100644 index 00000000000..aa8ff403c95 --- /dev/null +++ b/code/datums/components/pet_commands/pet_command.dm @@ -0,0 +1,179 @@ +// TODO: port /datum/callout_option and all the associated shenanigans + +/** + * # Pet Command + * Set some AI blackboard commands in response to receiving instructions + * This is abstract and should be extended for actual behaviour + */ +/datum/pet_command + /// UID of who follows this command + var/parent_uid + /// Unique name used for radial selection, should not be shared with other commands on one mob + var/command_name + /// Description to display in radial menu + var/command_desc + /// If true, command will not appear in radial menu and can only be accessed through speech + var/hidden = FALSE + /// Speech strings to listen out for + var/list/speech_commands = list() + /// Shown above the mob's head when it hears you + var/command_feedback + /// How close a mob needs to be to a target to respond to a command + var/sense_radius = 7 + /// does this pet command need a point to activate? + var/requires_pointing = FALSE + /// Blackboard key for targeting strategy, this is likely going to need it + var/targeting_strategy_key = BB_PET_TARGETING_STRATEGY + /// our pointed reaction we play + var/pointed_reaction + +/datum/pet_command/New(mob/living/parent) + . = ..() + parent_uid = parent.UID() + +/// Register a new guy we want to listen to +/datum/pet_command/proc/add_new_friend(mob/living/tamer) + RegisterSignal(tamer, COMSIG_MOB_SAY, PROC_REF(respond_to_command)) + RegisterSignal(tamer, COMSIG_MOB_AUTOMUTE_CHECK, PROC_REF(waive_automute)) + if(requires_pointing) + RegisterSignal(tamer, COMSIG_MOVABLE_POINTED, PROC_REF(point_on_target)) + +/// Stop listening to a guy +/datum/pet_command/proc/remove_friend(mob/living/unfriended) + UnregisterSignal(unfriended, list( + COMSIG_MOB_SAY, + COMSIG_MOB_AUTOMUTE_CHECK, + COMSIG_MOVABLE_POINTED, + )) + +/// Stop the automute from triggering for commands (unless the spoken text is suspiciously longer than the command) +/datum/pet_command/proc/waive_automute(mob/living/speaker, client/client, last_message, mute_type) + SIGNAL_HANDLER // COMSIG_MOB_AUTOMUTE_CHECK + if(mute_type == MUTE_IC && find_command_in_text(last_message, check_verbosity = TRUE)) + return WAIVE_AUTOMUTE_CHECK + return NONE + +/// Respond to something that one of our friends has asked us to do +/datum/pet_command/proc/respond_to_command(mob/living/speaker, speech_args) + SIGNAL_HANDLER // COMSIG_MOB_SAY + var/mob/living/parent = locateUID(parent_uid) + if(!parent) + return + if(!can_see(parent, speaker, sense_radius)) // Basically the same rules as hearing + return + + var/spoken_text = speech_args[SPEECH_MESSAGE] + if(!find_command_in_text(spoken_text)) + return + + try_activate_command(commander = speaker, radial_command = FALSE) + +/** + * Returns true if we find any of our spoken commands in the text. + * if check_verbosity is true, skip the match if there spoken_text is way longer than the match + */ +/datum/pet_command/proc/find_command_in_text(spoken_text, check_verbosity = FALSE) + for(var/command as anything in speech_commands) + if(!findtext(spoken_text, command)) + continue + if(check_verbosity && length(spoken_text) > length(command) + MAX_NAME_LEN) + continue + return TRUE + return FALSE + +/datum/pet_command/proc/pet_able_to_respond() + var/mob/living/parent = locateUID(parent_uid) + if(isnull(parent) || isnull(parent.ai_controller)) + return FALSE + if(parent.stat == DEAD || parent.stat == UNCONSCIOUS) // Probably can't hear them if we're dead + return FALSE + return TRUE + +/// Apply a command state if conditions are right, return command if successful +/datum/pet_command/proc/try_activate_command(mob/living/commander, radial_command) + if(!pet_able_to_respond()) + return FALSE + var/mob/living/parent = locateUID(parent_uid) + set_command_active(parent, commander, radial_command) + return TRUE + +/datum/pet_command/proc/generate_emote_command(atom/target) + var/mob/living/living_pet = locateUID(parent_uid) + return isnull(living_pet) ? null : retrieve_command_text(living_pet, target) + +/datum/pet_command/proc/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to spring into action!" + +/// Target the pointed atom for actions +/datum/pet_command/proc/look_for_target(mob/living/friend, atom/potential_target) + var/mob/living/parent = locateUID(parent_uid) + if(!pet_able_to_respond()) + return FALSE + if(parent.ai_controller.blackboard[BB_CURRENT_PET_TARGET] == potential_target) // That's already our target + return FALSE + if(!can_see(parent, potential_target, sense_radius)) + return FALSE + + parent.ai_controller.cancel_actions() + set_command_target(parent, potential_target) + return TRUE + +/// Activate the command, extend to add visible messages and the like +/datum/pet_command/proc/set_command_active(mob/living/parent, mob/living/commander, radial_command = FALSE) + parent.ai_controller.clear_blackboard_key(BB_CURRENT_PET_TARGET) + + parent.ai_controller.cancel_actions() // Stop whatever you're doing and do this instead + parent.ai_controller.set_blackboard_key(BB_ACTIVE_PET_COMMAND, src) + if(command_feedback) + parent.emote("me", EMOTE_VISIBLE, "[command_feedback]") + if(!radial_command) + return + if(!requires_pointing) + var/manual_emote_text = generate_emote_command() + commander.emote("me", EMOTE_VISIBLE, manual_emote_text) + return + RegisterSignal(commander, COMSIG_MOB_CLICKON, PROC_REF(click_on_target)) + +/datum/pet_command/proc/click_on_target(mob/living/source, atom/target, list/modifiers) + SIGNAL_HANDLER // COMSIG_MOB_CLICKON + if(!can_see(source, target, 9)) + return COMSIG_MOB_CANCEL_CLICKON + var/manual_emote_text = generate_emote_command(target) + if(on_target_set(source, target) && !isnull(manual_emote_text)) + INVOKE_ASYNC(source, TYPE_PROC_REF(/mob, custom_emote), EMOTE_VISIBLE, manual_emote_text) + UnregisterSignal(source, COMSIG_MOB_CLICKON) + return COMSIG_MOB_CANCEL_CLICKON + +/datum/pet_command/proc/point_on_target(mob/living/friend, atom/potential_target) + SIGNAL_HANDLER // COMSIG_MOVABLE_POINTED + on_target_set(friend, potential_target) + +/// Store the target for the AI blackboard +/datum/pet_command/proc/set_command_target(mob/living/parent, atom/target) + parent.ai_controller.set_blackboard_key(BB_CURRENT_PET_TARGET, target) + return TRUE + +// TODO: Port /datum/pet_command/proc/provide_radial_data + +/** + * Execute an AI action on the provided controller, what we should actually do when this command is active. + * This should basically always be called from a planning subtree which passes its own controller. + * Return SUBTREE_RETURN_FINISH_PLANNING to pass that instruction on to the controller, or don't if you don't want that. + */ +/datum/pet_command/proc/execute_action(datum/ai_controller/controller) + SHOULD_CALL_PARENT(FALSE) + CRASH("Pet command execute action not implemented.") + +/// Target the pointed atom for actions +/datum/pet_command/proc/on_target_set(mob/living/friend, atom/potential_target) + var/mob/living/parent = locateUID(parent_uid) + if(!parent) + return FALSE + + parent.ai_controller.cancel_actions() + if(!look_for_target(friend, potential_target) || !set_command_target(parent, potential_target)) + return FALSE + parent.visible_message("[parent] follows [friend]'s gesture towards [potential_target] [pointed_reaction]!") + return TRUE + + diff --git a/code/datums/components/pet_commands/pet_command_planning.dm b/code/datums/components/pet_commands/pet_command_planning.dm new file mode 100644 index 00000000000..a4ead42ac80 --- /dev/null +++ b/code/datums/components/pet_commands/pet_command_planning.dm @@ -0,0 +1,14 @@ +/** + * # Pet Planning + * Perform behaviour based on what pet commands you have received. This is delegated to the pet command datum. + * When a command is set, we blackboard a key to our currently active command. + * The blackboard also has a weak reference to every command datum available to us. + * We use the key to figure out which datum to run, then ask it to figure out how to execute its action. + */ +/datum/ai_planning_subtree/pet_planning + +/datum/ai_planning_subtree/pet_planning/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/datum/pet_command/command = controller.blackboard[BB_ACTIVE_PET_COMMAND] + if(!command) + return // Do something else + return command.execute_action(controller) diff --git a/code/datums/components/pet_commands/pet_commands_basic.dm b/code/datums/components/pet_commands/pet_commands_basic.dm new file mode 100644 index 00000000000..1003deb3c5e --- /dev/null +++ b/code/datums/components/pet_commands/pet_commands_basic.dm @@ -0,0 +1,210 @@ +// None of these are really complex enough to merit their own file + +/** + * # Pet Command: Idle + * Tells a pet to resume its idle behaviour, usually staying put where you leave it + */ +/datum/pet_command/idle + command_name = "Stay" + command_desc = "Command your pet to stay idle in this location." + speech_commands = list("sit", "stay", "stop") + command_feedback = "sits" + +/datum/pet_command/idle/execute_action(datum/ai_controller/controller) + return SUBTREE_RETURN_FINISH_PLANNING // This cancels further AI planning + +/datum/pet_command/idle/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to stay idle!" + +/** + * # Pet Command: Stop + * Tells a pet to exit command mode and resume its normal behaviour, which includes regular target-seeking and what have you + */ +/datum/pet_command/free + command_name = "Loose" + command_desc = "Allow your pet to resume its natural behaviours." + speech_commands = list("free", "loose") + command_feedback = "relaxes." + +/datum/pet_command/free/execute_action(datum/ai_controller/controller) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + return // Just move on to the next planning subtree. + +/datum/pet_command/free/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to go free!" + +/** + * # Pet Command: Follow + * Tells a pet to follow you until you tell it to do something else + */ +/datum/pet_command/follow + command_name = "Follow" + command_desc = "Command your pet to accompany you." + speech_commands = list("heel", "follow") + ///the behavior we use to follow + var/follow_behavior = /datum/ai_behavior/pet_follow_friend + +/datum/pet_command/follow/set_command_active(mob/living/parent, mob/living/commander) + . = ..() + set_command_target(parent, commander) + +/datum/pet_command/follow/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to follow!" + +/datum/pet_command/follow/execute_action(datum/ai_controller/controller) + controller.queue_behavior(follow_behavior, BB_CURRENT_PET_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/** + * # Pet Command: Use ability + * Use an an ability that does not require any targets + */ +/datum/pet_command/untargeted_ability + ///untargeted ability we will use + var/ability_key + +/datum/pet_command/untargeted_ability/execute_action(datum/ai_controller/controller) + var/datum/action/ability = controller.blackboard[ability_key] + if(!ability?.IsAvailable()) + return + controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/pet_command/untargeted_ability/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to use an ability!" + +/** + * # Pet Command: Attack + * Tells a pet to chase and bite the next thing you point at + */ +/datum/pet_command/attack + command_name = "Attack" + command_desc = "Command your pet to attack things that you point out to it." + requires_pointing = TRUE + speech_commands = list("attack", "sic", "kill") + command_feedback = "growls." + pointed_reaction = "and growls" + /// Balloon alert to display if providing an invalid target + var/refuse_reaction = "shakes head" + /// Attack behaviour to use + var/attack_behaviour = /datum/ai_behavior/basic_melee_attack + +// Refuse to target things we can't target, chiefly other friends +/datum/pet_command/attack/set_command_target(mob/living/parent, atom/target) + if(!target) + return FALSE + var/mob/living/living_parent = parent + if(!living_parent.ai_controller) + return FALSE + var/datum/targeting_strategy/targeter = GET_TARGETING_STRATEGY(living_parent.ai_controller.blackboard[targeting_strategy_key]) + if(!targeter) + return FALSE + if(!targeter.can_attack(living_parent, target)) + refuse_target(parent, target) + return FALSE + return ..() + +/datum/pet_command/attack/retrieve_command_text(atom/living_pet, atom/target) + return isnull(target) ? null : "signals [living_pet] to attack [target]!" + +/// Display feedback about not targeting something +/datum/pet_command/attack/proc/refuse_target(mob/living/parent, atom/target) + var/mob/living/living_parent = parent + living_parent.custom_emote(EMOTE_VISIBLE, refuse_reaction) + living_parent.visible_message("[living_parent] refuses to attack [target].") + +/datum/pet_command/attack/execute_action(datum/ai_controller/controller) + controller.queue_behavior(attack_behaviour, BB_CURRENT_PET_TARGET, targeting_strategy_key) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/pet_command/protect_owner + command_name = "Protect owner" + command_desc = "Your pet will run to your aid." + hidden = TRUE + /// The range our owner needs to be in for us to protect him + var/protect_range = 9 + /// The behavior we will use when he is attacked + var/protect_behavior = /datum/ai_behavior/basic_melee_attack + /// Message cooldown to prevent too many people from telling you not to commit suicide + COOLDOWN_DECLARE(self_harm_message_cooldown) + /// Message cooldown to prevent spamming apologies + COOLDOWN_DECLARE(friendly_fire_message_cooldown) + +/datum/pet_command/protect_owner/add_new_friend(mob/living/tamer) + RegisterSignal(tamer, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(set_attacking_target)) + if(!HAS_TRAIT(tamer, TRAIT_RELAYING_ATTACKER)) + tamer.AddElement(/datum/element/relay_attackers) + +/datum/pet_command/protect_owner/remove_friend(mob/living/unfriended) + UnregisterSignal(unfriended, COMSIG_ATOM_WAS_ATTACKED) + +/datum/pet_command/protect_owner/execute_action(datum/ai_controller/controller) + var/mob/living/victim = controller.blackboard[BB_CURRENT_PET_TARGET] + if(QDELETED(victim)) + controller.clear_blackboard_key(BB_CURRENT_PET_TARGET) + return + // cancel the action if they're below our given crit stat, OR if we're trying to attack ourselves (this can happen on tamed mobs w/ protect subtree rarely) + if(victim.stat > controller.blackboard[BB_TARGET_MINIMUM_STAT] || victim == controller.pawn) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + return + controller.queue_behavior(protect_behavior, BB_CURRENT_PET_TARGET, BB_PET_TARGETING_STRATEGY) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/pet_command/protect_owner/set_command_active(mob/living/parent, mob/living/victim) + . = ..() + set_command_target(parent, victim) + +/datum/pet_command/protect_owner/proc/set_attacking_target(atom/source, mob/living/attacker) + SIGNAL_HANDLER // COMSIG_ATOM_WAS_ATTACKED + + var/mob/living/basic/owner = locateUID(parent_uid) + if(isnull(owner)) + return + + // TODO: Be smarter about handling signals when our AI controller isn't active + // This should definitely be handled somewhere higher up + if(owner.ai_controller.ai_status != AI_STATUS_ON) + return + + if(source == attacker) + var/list/interventions = owner.ai_controller?.blackboard[BB_OWNER_SELF_HARM_RESPONSES] || list() + if(length(interventions) && COOLDOWN_FINISHED(src, self_harm_message_cooldown) && prob(30)) + COOLDOWN_START(src, self_harm_message_cooldown, 5 SECONDS) + var/chosen_statement = pick(interventions) + INVOKE_ASYNC(owner, TYPE_PROC_REF(/atom, atom_say), chosen_statement) + return + if(owner == attacker) + var/list/apologies = owner.ai_controller?.blackboard[BB_OWNER_FRIENDLY_FIRE_APOLOGIES] || list() + if(length(apologies) && COOLDOWN_FINISHED(src, friendly_fire_message_cooldown)) + COOLDOWN_START(src, friendly_fire_message_cooldown, 5 SECONDS) + var/chosen_statement = pick(apologies) + INVOKE_ASYNC(owner, TYPE_PROC_REF(/atom, atom_say), chosen_statement) + return + + var/mob/living/current_target = owner.ai_controller?.blackboard[BB_CURRENT_PET_TARGET] + if(attacker == current_target) // we are already dealing with this target + return + if(isliving(attacker) && can_see(owner, attacker, protect_range)) + set_command_active(owner, attacker) + +/datum/pet_command/move + command_name = "Move" + command_desc = "Command your pet to move to a location!" + requires_pointing = TRUE + speech_commands = list("move", "walk") + /// the behavior we use to walk towards targets + var/datum/ai_behavior/walk_behavior = /datum/ai_behavior/travel_towards + +/datum/pet_command/move/set_command_target(mob/living/parent, atom/target) + if(isnull(target) || !can_see(parent, target, 9)) + return FALSE + return ..() + +/datum/pet_command/move/execute_action(datum/ai_controller/controller) + if(controller.blackboard_key_exists(BB_CURRENT_PET_TARGET)) + controller.queue_behavior(walk_behavior, BB_CURRENT_PET_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/pet_command/move/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to move!" diff --git a/code/datums/components/pet_commands/pet_follow_friend.dm b/code/datums/components/pet_commands/pet_follow_friend.dm new file mode 100644 index 00000000000..4ba01febdde --- /dev/null +++ b/code/datums/components/pet_commands/pet_follow_friend.dm @@ -0,0 +1,16 @@ +/// Just keep following the target until the command is interrupted +/datum/ai_behavior/pet_follow_friend + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/pet_follow_friend/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/pet_follow_friend/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + return AI_BEHAVIOR_DELAY diff --git a/code/datums/components/ranged_attacks.dm b/code/datums/components/ranged_attacks.dm new file mode 100644 index 00000000000..c882a8fdf3c --- /dev/null +++ b/code/datums/components/ranged_attacks.dm @@ -0,0 +1,90 @@ +/** + * Configurable ranged attack for basic mobs. + */ +/datum/component/ranged_attacks + /// What kind of casing do we use to fire? + var/casing_type + /// What kind of projectile to we fire? Use only one of this or casing_type + var/projectile_type + /// Sound to play when we fire our projectile + var/projectile_sound + /// how many shots we will fire + var/burst_shots + /// intervals between shots + var/burst_intervals + /// Time to wait between shots + var/cooldown_time + /// Tracks time between shots + COOLDOWN_DECLARE(fire_cooldown) + +/datum/component/ranged_attacks/Initialize( + casing_type, + projectile_type, + projectile_sound = 'sound/weapons/gunshots/gunshot_pistol.ogg', + burst_shots, + burst_intervals = 0.2 SECONDS, + cooldown_time = 3 SECONDS, +) + . = ..() + if(!isbasicmob(parent)) + return COMPONENT_INCOMPATIBLE + + src.casing_type = casing_type + src.projectile_sound = projectile_sound + src.projectile_type = projectile_type + src.cooldown_time = cooldown_time + + if(casing_type && projectile_type) + CRASH("Set both casing type and projectile type in [parent]'s ranged attacks component! uhoh! stinky!") + if(!casing_type && !projectile_type) + CRASH("Set neither casing type nor projectile type in [parent]'s ranged attacks component! What are they supposed to be attacking with, air?") + if(burst_shots <= 1) + return + src.burst_shots = burst_shots + src.burst_intervals = burst_intervals + +/datum/component/ranged_attacks/RegisterWithParent() + . = ..() + RegisterSignal(parent, COMSIG_MOB_ATTACK_RANGED, PROC_REF(fire_ranged_attack)) + ADD_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + +/datum/component/ranged_attacks/UnregisterFromParent() + . = ..() + UnregisterSignal(parent, COMSIG_MOB_ATTACK_RANGED) + REMOVE_TRAIT(parent, TRAIT_SUBTREE_REQUIRED_OPERATIONAL_DATUM, type) + +/datum/component/ranged_attacks/proc/fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers) + SIGNAL_HANDLER + if(!COOLDOWN_FINISHED(src, fire_cooldown)) + return + if(SEND_SIGNAL(firer, COMSIG_BASICMOB_PRE_ATTACK_RANGED, target, modifiers) & COMPONENT_CANCEL_RANGED_ATTACK) + return + COOLDOWN_START(src, fire_cooldown, cooldown_time) + INVOKE_ASYNC(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers) + if(isnull(burst_shots)) + return + for(var/i in 1 to (burst_shots - 1)) + addtimer(CALLBACK(src, PROC_REF(async_fire_ranged_attack), firer, target, modifiers), i * burst_intervals) + +/// Actually fire the damn thing +/datum/component/ranged_attacks/proc/async_fire_ranged_attack(mob/living/basic/firer, atom/target, modifiers) + if(QDELETED(firer)) + return + + firer.face_atom(target) + if(projectile_type) + firer.fire_projectile(projectile_type, target, projectile_sound) + SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers) + return + playsound(firer, projectile_sound, 100, TRUE) + var/turf/startloc = get_turf(firer) + var/obj/item/ammo_casing/casing = new casing_type(startloc) + var/target_zone + if(ismob(target)) + var/mob/target_mob = target + target_zone = target_mob.get_random_valid_zone() + else + target_zone = ran_zone() + casing.fire(target, firer, null, null, null, target_zone, 0, firer) + casing.update_appearance() + SEND_SIGNAL(parent, COMSIG_BASICMOB_POST_ATTACK_RANGED, target, modifiers) diff --git a/code/datums/ores.dm b/code/datums/ores.dm index 2f866c1c2f4..b779d6592e8 100644 --- a/code/datums/ores.dm +++ b/code/datums/ores.dm @@ -158,6 +158,7 @@ if(GIBTONITE_UNSTRUCK) playsound(src,'sound/effects/hit_on_shattered_glass.ogg', 50, TRUE) explosive_reaction(source, user, triggered_by_explosion) + SEND_SIGNAL(source, COMSIG_MINE_EXPOSE_GIBTONITE, user) return MINERAL_PREVENT_DIG if(GIBTONITE_ACTIVE) detonate(source) diff --git a/code/game/turfs/simulated/floor/chasm.dm b/code/game/turfs/simulated/floor/chasm.dm index 1f34b58e429..83157e84c9f 100644 --- a/code/game/turfs/simulated/floor/chasm.dm +++ b/code/game/turfs/simulated/floor/chasm.dm @@ -67,7 +67,7 @@ if(!pass_info.is_living) return TRUE - return pass_info.is_flying || pass_info.is_megafauna + return pass_info.is_flying || pass_info.is_megafauna || (locate(/obj/structure/bridge_walkway) in src) /turf/simulated/floor/chasm/get_smooth_underlay_icon(mutable_appearance/underlay_appearance, turf/asking_turf, adjacency_dir) underlay_appearance.icon = 'icons/turf/floors.dmi' @@ -180,6 +180,9 @@ L.adjustBruteLoss(30) falling_atoms -= AM +/turf/simulated/floor/chasm/can_cross_safely(atom/movable/crossing) + return locate(/obj/structure/bridge_walkway) in src + /turf/simulated/floor/chasm/straight_down var/obj/effect/abstract/chasm_storage/storage diff --git a/code/game/turfs/simulated/floor/lava.dm b/code/game/turfs/simulated/floor/lava.dm index bb75c9f9147..b26f9db09db 100644 --- a/code/game/turfs/simulated/floor/lava.dm +++ b/code/game/turfs/simulated/floor/lava.dm @@ -67,6 +67,12 @@ return TRUE return FALSE +/turf/simulated/floor/lava/CanPathfindPass(to_dir, datum/can_pass_info/pass_info) + if(!pass_info.is_living) + return TRUE + + return pass_info.is_flying || pass_info.is_megafauna || (locate(/obj/structure/bridge_walkway) in src) + /turf/simulated/floor/lava/proc/burn_stuff(AM) . = FALSE @@ -165,6 +171,9 @@ /turf/simulated/floor/lava/burn_tile() return +/turf/simulated/floor/lava/can_cross_safely(atom/movable/crossing) + return locate(/obj/structure/bridge_walkway) in src + /turf/simulated/floor/lava/lava_land_surface oxygen = LAVALAND_OXYGEN nitrogen = LAVALAND_NITROGEN diff --git a/code/modules/mining/minebot.dm b/code/modules/mining/minebot.dm deleted file mode 100644 index 69de3618b60..00000000000 --- a/code/modules/mining/minebot.dm +++ /dev/null @@ -1,361 +0,0 @@ -/**********************Mining drone**********************/ -#define MINEDRONE_COLLECT 1 -#define MINEDRONE_ATTACK 2 - -/mob/living/simple_animal/hostile/mining_drone - name = "nanotrasen minebot" - desc = "The instructions printed on the side read: This is a small robot used to support miners, can be set to search and collect loose ore, or to help fend off wildlife. A mining scanner can instruct it to drop loose ore. Field repairs can be done with a welder." - gender = NEUTER - icon = 'icons/obj/aibots.dmi' - icon_state = "mining_drone" - icon_living = "mining_drone" - status_flags = CANSTUN|CANWEAKEN|CANPUSH - mouse_opacity = MOUSE_OPACITY_ICON - faction = list("neutral") - weather_immunities = list("ash") - a_intent = INTENT_HARM - atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) - minbodytemp = 0 - move_to_delay = 10 - health = 125 - maxHealth = 125 - melee_damage_lower = 15 - melee_damage_upper = 15 - maxbodytemp = INFINITY - obj_damage = 10 - environment_smash = 0 - check_friendly_fire = TRUE - stop_automated_movement_when_pulled = TRUE - attacktext = "drills" - attack_sound = 'sound/weapons/circsawhit.ogg' - sentience_type = SENTIENCE_MINEBOT - speak_emote = list("states") - wanted_objects = list(/obj/item/stack/ore/diamond, /obj/item/stack/ore/gold, /obj/item/stack/ore/silver, - /obj/item/stack/ore/plasma, /obj/item/stack/ore/uranium, /obj/item/stack/ore/iron, - /obj/item/stack/ore/bananium, /obj/item/stack/ore/tranquillite, /obj/item/stack/ore/glass, - /obj/item/stack/ore/titanium) - healable = FALSE - del_on_death = TRUE - var/mode = MINEDRONE_COLLECT - var/light_on = FALSE - var/mesons_active - var/obj/item/gun/energy/kinetic_accelerator/minebot/stored_gun - - -/mob/living/simple_animal/hostile/mining_drone/Initialize(mapload) - . = ..() - stored_gun = new(src) - var/static/list/action_paths = list( - /datum/action/innate/minedrone/toggle_light, - /datum/action/innate/minedrone/toggle_meson_vision, - /datum/action/innate/minedrone/toggle_mode, - /datum/action/innate/minedrone/dump_ore, - ) - for(var/action_path in action_paths) - var/datum/action/act = new action_path - act.Grant(src) - - SetCollectBehavior() - -/mob/living/simple_animal/hostile/mining_drone/Destroy() - QDEL_NULL(stored_gun) - return ..() - -/mob/living/simple_animal/hostile/mining_drone/emp_act(severity) - adjustHealth(100 / severity) - to_chat(src, "NOTICE: EMP detected, systems damaged!") - visible_message("[src] crackles and buzzes violently!") - -/mob/living/simple_animal/hostile/mining_drone/sentience_act() - ..() - check_friendly_fire = FALSE - -/mob/living/simple_animal/hostile/mining_drone/examine(mob/user) - . = ..() - var/t_He = p_they(TRUE) - var/t_him = p_them() - var/t_s = p_s() - if(health < maxHealth) - if(health >= maxHealth * 0.5) - . += "[t_He] look[t_s] slightly dented." - else - . += "[t_He] look[t_s] severely dented!" - . += {"Using a mining scanner on [t_him] will instruct [t_him] to drop stored ore. [max(0, LAZYLEN(contents) - 1)] Stored Ore\n - Field repairs can be done with a welder."} - if(stored_gun && stored_gun.max_mod_capacity) - . += "[stored_gun.get_remaining_mod_capacity()]% mod capacity remaining." - for(var/A in stored_gun.get_modkits()) - var/obj/item/borg/upgrade/modkit/M = A - . += "There is \a [M] installed, using [M.cost]% capacity." - -/mob/living/simple_animal/hostile/mining_drone/item_interaction(mob/living/user, obj/item/I, list/modifiers) - if(istype(I, /obj/item/mining_scanner) || istype(I, /obj/item/t_scanner/adv_mining_scanner)) - to_chat(user, "You instruct [src] to drop any collected ore.") - DropOre() - return ITEM_INTERACT_COMPLETE - if(istype(I, /obj/item/borg/upgrade/modkit)) - I.melee_attack_chain(user, stored_gun, list2params(modifiers)) - return ITEM_INTERACT_COMPLETE - - return ..() - -/mob/living/simple_animal/hostile/mining_drone/crowbar_act(mob/user, obj/item/I) - if(user.a_intent != INTENT_HELP) - return - . = TRUE - if(!I.tool_start_check(src, user, 0)) - return - I.melee_attack_chain(user, stored_gun) - -/mob/living/simple_animal/hostile/mining_drone/welder_act(mob/user, obj/item/I) - if(user.a_intent != INTENT_HELP) - return - . = TRUE - if(health == maxHealth) - to_chat(user, "[src] doesn't need repairing!") - return - if(!I.tool_use_check(user, 1)) - return - if(AIStatus != AI_OFF && AIStatus != AI_IDLE) - to_chat(user, "[src] is moving around too much to repair!") - return - WELDER_ATTEMPT_REPAIR_MESSAGE - if(I.use_tool(src, user, 15, 1, volume = I.tool_volume) && health != maxHealth) - adjustBruteLoss(-20) - WELDER_REPAIR_SUCCESS_MESSAGE - return - -/mob/living/simple_animal/hostile/mining_drone/death() - DropOre(0) - if(stored_gun) - for(var/obj/item/borg/upgrade/modkit/M in stored_gun.modkits) - M.uninstall(stored_gun) - deathmessage = "blows apart!" - . = ..() - -/mob/living/simple_animal/hostile/mining_drone/attack_hand(mob/living/carbon/human/M) - if(M.a_intent == INTENT_HELP) - toggle_mode() - switch(mode) - if(MINEDRONE_COLLECT) - to_chat(M, "[src] has been set to search and store loose ore.") - if(MINEDRONE_ATTACK) - to_chat(M, "[src] has been set to attack hostile wildlife.") - return - ..() - -/mob/living/simple_animal/hostile/mining_drone/CanPass(atom/movable/O) - if(istype(O, /obj/item/projectile/kinetic)) - var/obj/item/projectile/kinetic/K = O - if(K.kinetic_gun) - for(var/A in K.kinetic_gun.get_modkits()) - var/obj/item/borg/upgrade/modkit/M = A - if(istype(M, /obj/item/borg/upgrade/modkit/minebot_passthrough)) - return TRUE - if(istype(O, /obj/item/projectile/destabilizer)) - return TRUE - return ..() - -/mob/living/simple_animal/hostile/mining_drone/proc/SetCollectBehavior() - mode = MINEDRONE_COLLECT - vision_range = 9 - search_objects = 2 - wander = TRUE - ranged = FALSE - minimum_distance = 1 - retreat_distance = null - icon_state = "mining_drone" - to_chat(src, "You are set to collect mode. You can now collect loose ore.") - -/mob/living/simple_animal/hostile/mining_drone/proc/SetOffenseBehavior() - mode = MINEDRONE_ATTACK - vision_range = 7 - search_objects = 0 - wander = FALSE - ranged = TRUE - retreat_distance = 2 - minimum_distance = 1 - icon_state = "mining_drone_offense" - to_chat(src, "You are set to attack mode. You can now attack from range.") - -/mob/living/simple_animal/hostile/mining_drone/AttackingTarget() - if(istype(target, /obj/item/stack/ore) && mode == MINEDRONE_COLLECT) - CollectOre() - return - if(isliving(target)) - SetOffenseBehavior() - return ..() - -/mob/living/simple_animal/hostile/mining_drone/OpenFire(atom/A) - if(CheckFriendlyFire(A)) - return - stored_gun.afterattack__legacy__attackchain(A, src) //of the possible options to allow minebots to have KA mods, would you believe this is the best? - -/mob/living/simple_animal/hostile/mining_drone/proc/CollectOre() - for(var/obj/item/stack/ore/O in range(1, src)) - O.forceMove(src) - -/mob/living/simple_animal/hostile/mining_drone/proc/DropOre(message = 1) - if(!length(contents)) - if(message) - to_chat(src, "You attempt to dump your stored ore, but you have none.") - return - if(message) - to_chat(src, "You dump your stored ore.") - for(var/obj/item/stack/ore/O in contents) - O.forceMove(drop_location()) - -/mob/living/simple_animal/hostile/mining_drone/adjustHealth(amount, updating_health = TRUE) - if(mode != MINEDRONE_ATTACK && amount > 0) - SetOffenseBehavior() - . = ..() - -/mob/living/simple_animal/hostile/mining_drone/proc/toggle_mode() - switch(mode) - if(MINEDRONE_ATTACK) - SetCollectBehavior() - else - SetOffenseBehavior() - -//Actions for sentient minebots - -/datum/action/innate/minedrone - check_flags = AB_CHECK_CONSCIOUS - button_background_icon_state = "bg_default" - -/datum/action/innate/minedrone/toggle_light - name = "Toggle Light" - button_overlay_icon_state = "mech_lights_off" - -/datum/action/innate/minedrone/toggle_light/Activate() - var/mob/living/simple_animal/hostile/mining_drone/user = owner - - if(user.light_on) - user.set_light(0) - else - user.set_light(6) - user.light_on = !user.light_on - to_chat(user, "You toggle your light [user.light_on ? "on" : "off"].") - -/datum/action/innate/minedrone/toggle_meson_vision - name = "Toggle Meson Vision" - button_overlay_icon_state = "meson" - var/sight_flags = SEE_TURFS - var/lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE - -/datum/action/innate/minedrone/toggle_meson_vision/Activate() - var/mob/living/user = owner - var/is_active = user.sight & SEE_TURFS - - if(is_active) - UnregisterSignal(user, COMSIG_MOB_UPDATE_SIGHT) - user.update_sight() - else - RegisterSignal(user, COMSIG_MOB_UPDATE_SIGHT, PROC_REF(update_user_sight)) - user.update_sight() - - to_chat(user, "You toggle your meson vision [!is_active ? "on" : "off"].") - -/datum/action/innate/minedrone/toggle_meson_vision/proc/update_user_sight(mob/living/user) - user.sight |= sight_flags - if(!isnull(lighting_alpha)) - user.lighting_alpha = min(user.lighting_alpha, lighting_alpha) - -/datum/action/innate/minedrone/toggle_mode - name = "Toggle Mode" - button_overlay_icon_state = "mech_cycle_equip_off" - -/datum/action/innate/minedrone/toggle_mode/Activate() - var/mob/living/simple_animal/hostile/mining_drone/user = owner - user.toggle_mode() - -/datum/action/innate/minedrone/dump_ore - name = "Dump Ore" - button_overlay_icon_state = "mech_eject" - -/datum/action/innate/minedrone/dump_ore/Activate() - var/mob/living/simple_animal/hostile/mining_drone/user = owner - user.DropOre() - - -/**********************Minebot Upgrades**********************/ - -//Melee - -/obj/item/mine_bot_upgrade - name = "minebot melee upgrade" - desc = "A minebot upgrade." - icon_state = "door_electronics" - icon = 'icons/obj/doors/door_assembly.dmi' - -/obj/item/mine_bot_upgrade/afterattack__legacy__attackchain(mob/living/simple_animal/hostile/mining_drone/M, mob/user, proximity) - if(!istype(M) || !proximity) - return - upgrade_bot(M, user) - -/obj/item/mine_bot_upgrade/proc/upgrade_bot(mob/living/simple_animal/hostile/mining_drone/M, mob/user) - if(M.melee_damage_upper != initial(M.melee_damage_upper)) - to_chat(user, "[M] already has a combat upgrade installed!") - return - M.melee_damage_lower += 7 - M.melee_damage_upper += 7 - to_chat(user, "You upgrade [M]'s combat module.") - qdel(src) - -//Health - -/obj/item/mine_bot_upgrade/health - name = "minebot armor upgrade" - -/obj/item/mine_bot_upgrade/health/upgrade_bot(mob/living/simple_animal/hostile/mining_drone/M, mob/user) - if(M.maxHealth != initial(M.maxHealth)) - to_chat(user, "[M] already has a reinforced chassis!") - return - M.maxHealth += 45 - M.updatehealth() - qdel(src) - -//AI - -/obj/item/slimepotion/sentience/mining - name = "minebot AI upgrade" - desc = "Can be used to grant sentience to minebots." - icon_state = "door_electronics" - icon = 'icons/obj/doors/door_assembly.dmi' - sentience_type = SENTIENCE_MINEBOT - origin_tech = "programming=6" - var/base_health_add = 5 //sentient minebots are penalized for beign sentient; they have their stats reset to normal plus these values - var/base_damage_add = 1 //this thus disables other minebot upgrades - var/base_speed_add = 1 - var/base_cooldown_add = 10 //base cooldown isn't reset to normal, it's just added on, since it's not practical to disable the cooldown module - -/obj/item/slimepotion/sentience/mining/after_success(mob/living/user, mob/living/simple_animal/SM) - if(istype(SM, /mob/living/simple_animal/hostile/mining_drone)) - var/mob/living/simple_animal/hostile/mining_drone/M = SM - M.maxHealth = initial(M.maxHealth) + base_health_add - M.melee_damage_lower = initial(M.melee_damage_lower) + base_damage_add - M.melee_damage_upper = initial(M.melee_damage_upper) + base_damage_add - M.move_to_delay = initial(M.move_to_delay) + base_speed_add - if(M.stored_gun) - M.stored_gun.overheat_time += base_cooldown_add - if(M.mind) - M.mind.offstation_role = TRUE - -/**********************Mining drone cube**********************/ - -/obj/item/mining_drone_cube - name = "mining drone cube" - desc = "Compressed mining drone, ready for deployment. Just press the button to activate!" - w_class = WEIGHT_CLASS_SMALL - icon = 'icons/obj/aibots.dmi' - icon_state = "minedronecube" - item_state = "electronic" - -/obj/item/mining_drone_cube/attack_self__legacy__attackchain(mob/user) - user.visible_message("\The [src] suddenly expands into a fully functional mining drone!", \ - "You press center button on \the [src]. The device suddenly expands into a fully functional mining drone!") - new /mob/living/simple_animal/hostile/mining_drone(get_turf(src)) - qdel(src) - -#undef MINEDRONE_COLLECT -#undef MINEDRONE_ATTACK diff --git a/code/modules/mob/living/basic/basic_mob.dm b/code/modules/mob/living/basic/basic_mob.dm index c4bf65bafc4..a9c2a7ea8f3 100644 --- a/code/modules/mob/living/basic/basic_mob.dm +++ b/code/modules/mob/living/basic/basic_mob.dm @@ -48,6 +48,8 @@ RESTRICT_TYPE(/mob/living/basic) var/gold_core_spawnable = NO_SPAWN /// Holding var for determining who own/controls a sentient simple animal (for sentience potions). var/mob/living/carbon/human/master_commander = null + /// Sentience type, for slime potions + var/sentience_type = SENTIENCE_ORGANIC /// Higher speed is slower, negative speed is faster var/speed = 1 @@ -299,3 +301,35 @@ RESTRICT_TYPE(/mob/living/basic) else apply_damage(damage, damagetype, null, getarmor(null, armorcheck)) return TRUE + +// Health/Damage adjustment, cribbed straight from simplemobs + +/mob/living/basic/adjustHealth(amount, updating_health = TRUE) + . = ..() + if(!ckey && stat == CONSCIOUS) + if(ai_controller?.ai_status == AI_STATUS_IDLE) + ai_controller.set_ai_status(AI_STATUS_ON) + +/mob/living/basic/adjustBruteLoss(amount, updating_health = TRUE) + if(damage_coeff[BRUTE]) + return adjustHealth(amount * damage_coeff[BRUTE], updating_health) + +/mob/living/basic/adjustFireLoss(amount, updating_health = TRUE) + if(damage_coeff[BURN]) + return adjustHealth(amount * damage_coeff[BURN], updating_health) + +/mob/living/basic/adjustOxyLoss(amount, updating_health = TRUE) + if(damage_coeff[OXY]) + return adjustHealth(amount * damage_coeff[OXY], updating_health) + +/mob/living/basic/adjustToxLoss(amount, updating_health = TRUE) + if(damage_coeff[TOX]) + return adjustHealth(amount * damage_coeff[TOX], updating_health) + +/mob/living/basic/adjustCloneLoss(amount, updating_health = TRUE) + if(damage_coeff[CLONE]) + return adjustHealth(amount * damage_coeff[CLONE], updating_health) + +/mob/living/basic/adjustStaminaLoss(amount, updating_health = TRUE) + if(damage_coeff[STAMINA]) + return ..(amount * damage_coeff[STAMINA], updating_health) diff --git a/code/modules/mob/living/basic/minebots/minebot.dm b/code/modules/mob/living/basic/minebots/minebot.dm new file mode 100644 index 00000000000..f3074164a95 --- /dev/null +++ b/code/modules/mob/living/basic/minebots/minebot.dm @@ -0,0 +1,187 @@ +/mob/living/basic/mining_drone + name = "nanotrasen minebot" + desc = "The instructions printed on the side read: This is a small robot used to support miners, can be set to search and collect loose ore, or to help fend off wildlife. A mining scanner can instruct it to drop loose ore. Field repairs can be done with a welder." + gender = NEUTER + icon = 'icons/obj/aibots.dmi' + icon_state = "mining_drone" + icon_living = "mining_drone" + status_flags = CANSTUN|CANWEAKEN|CANPUSH + mouse_opacity = MOUSE_OPACITY_ICON + basic_mob_flags = DEL_ON_DEATH + sentience_type = SENTIENCE_MINEBOT + faction = list("neutral", "goldgrub") // goldgrubs are invulnerable to PKA fire + weather_immunities = list("ash") + atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0) + minimum_survivable_temperature = 0 + health = 125 + maxHealth = 125 + melee_damage_lower = 15 + melee_damage_upper = 15 + obj_damage = 10 + maximum_survivable_temperature = INFINITY + attack_sound = 'sound/weapons/circsawhit.ogg' + speak_emote = list("states") + healable = FALSE + ai_controller = /datum/ai_controller/basic_controller/minebot + hud_type = /datum/hud/simple_animal + var/light_on = FALSE + var/obj/item/gun/energy/kinetic_accelerator/minebot/stored_gun + var/obj/item/radio/radio + /// The commands our owner can give us + var/static/list/pet_commands = list( + /datum/pet_command/idle/minebot, + /datum/pet_command/move, + /datum/pet_command/protect_owner/minebot, + /datum/pet_command/minebot_ability/light, + /datum/pet_command/minebot_ability/dump, + /datum/pet_command/automate_mining, + /datum/pet_command/free/minebot, + /datum/pet_command/follow, + /datum/pet_command/attack/minebot, + ) + +/mob/living/basic/mining_drone/Initialize(mapload) + . = ..() + stored_gun = new(src) + var/static/list/innate_actions = list( + /datum/action/innate/minedrone/toggle_light = BB_MINEBOT_LIGHT_ABILITY, + /datum/action/innate/minedrone/toggle_meson_vision = null, + /datum/action/innate/minedrone/dump_ore = BB_MINEBOT_DUMP_ABILITY, + ) + + grant_actions_by_list(innate_actions) + + radio = new(src) + radio.listening = FALSE + radio.follow_target = src + radio.config(list("Supply" = TRUE)) + + AddComponent(/datum/component/footstep, FOOTSTEP_OBJ_ROBOT) + AddComponent(/datum/component/obeys_commands, pet_commands) + +/mob/living/basic/mining_drone/Destroy() + QDEL_NULL(stored_gun) + drop_ore(message = FALSE) + return ..() + +/mob/living/basic/mining_drone/emp_act(severity) + adjustHealth(100 / severity) + to_chat(src, "NOTICE: EMP detected, systems damaged!") + visible_message("[src] crackles and buzzes violently!") + +/mob/living/basic/mining_drone/examine(mob/user) + . = ..() + var/t_He = p_they(TRUE) + var/t_s = p_s() + if(health < maxHealth) + if(health >= maxHealth * 0.5) + . += "[t_He] look[t_s] slightly dented." + else + . += "[t_He] look[t_s] severely dented!" + var/ore_count = 0 + for(var/obj/item/stack/ore/ore_stack in contents) + ore_count += ore_stack.amount + + . += "[t_He] is currently storing [ore_count] ore." + + if(stored_gun && stored_gun.max_mod_capacity) + . += "[stored_gun.get_remaining_mod_capacity()]% mod capacity remaining." + for(var/A in stored_gun.get_modkits()) + var/obj/item/borg/upgrade/modkit/M = A + . += "There is \a [M] installed, using [M.cost]% capacity." + +/mob/living/basic/mining_drone/item_interaction(mob/living/user, obj/item/I, list/modifiers) + if(istype(I, /obj/item/mining_scanner) || istype(I, /obj/item/t_scanner/adv_mining_scanner)) + to_chat(user, "You instruct [src] to drop any collected ore.") + drop_ore() + return ITEM_INTERACT_COMPLETE + if(istype(I, /obj/item/borg/upgrade/modkit)) + I.melee_attack_chain(user, stored_gun, list2params(modifiers)) + return ITEM_INTERACT_COMPLETE + + return ..() + +/mob/living/basic/mining_drone/crowbar_act(mob/user, obj/item/I) + if(user.a_intent != INTENT_HELP) + return + . = TRUE + if(!I.tool_start_check(src, user, 0)) + return + I.melee_attack_chain(user, stored_gun) + +/mob/living/basic/mining_drone/welder_act(mob/user, obj/item/I) + if(user.a_intent != INTENT_HELP) + return + . = TRUE + if(health == maxHealth) + to_chat(user, "[src] doesn't need repairing!") + return + if(!I.tool_use_check(user, 1)) + return + WELDER_ATTEMPT_REPAIR_MESSAGE + if(I.use_tool(src, user, 15, 1, volume = I.tool_volume) && health != maxHealth) + adjustBruteLoss(-20) + WELDER_REPAIR_SUCCESS_MESSAGE + +/mob/living/basic/mining_drone/death() + drop_ore(message = FALSE) + if(stored_gun) + for(var/obj/item/borg/upgrade/modkit/M in stored_gun.modkits) + M.uninstall(stored_gun) + robogibs(loc) + deathmessage = "blows apart!" + return ..() + +/mob/living/basic/mining_drone/CanPass(atom/movable/O) + if(istype(O, /obj/item/projectile/kinetic)) + var/obj/item/projectile/kinetic/K = O + if(K.kinetic_gun) + for(var/A in K.kinetic_gun.get_modkits()) + var/obj/item/borg/upgrade/modkit/M = A + if(istype(M, /obj/item/borg/upgrade/modkit/minebot_passthrough)) + return TRUE + if(istype(O, /obj/item/projectile/destabilizer)) + return TRUE + return ..() + +/mob/living/basic/mining_drone/RangedAttack(atom/target, list/modifiers) + if(a_intent != INTENT_HARM) + return + stored_gun.afterattack__legacy__attackchain(target, src) + +/mob/living/basic/mining_drone/UnarmedAttack(atom/attack_target, proximity_flag, list/modifiers) + . = ..() + + if(!. || !proximity_flag || (a_intent == INTENT_HARM)) + return + + if(istype(attack_target, /obj/item/stack/ore)) + var/obj/item/target_ore = attack_target + target_ore.forceMove(src) + +/mob/living/basic/mining_drone/proc/drop_ore(message = TRUE) + if(!length(contents)) + if(message) + to_chat(src, "You attempt to dump your stored ore, but you have none.") + return + if(message) + to_chat(src, "You dump your stored ore.") + for(var/obj/item/stack/ore/O in contents) + O.forceMove(drop_location()) + +// MARK: Minebot Cube + +/obj/item/mining_drone_cube + name = "mining drone cube" + desc = "Compressed mining drone, ready for deployment. Just press the button to activate!" + w_class = WEIGHT_CLASS_SMALL + icon = 'icons/obj/aibots.dmi' + icon_state = "minedronecube" + item_state = "electronic" + +/obj/item/mining_drone_cube/attack_self__legacy__attackchain(mob/user) + user.visible_message("\The [src] suddenly expands into a fully functional mining drone!", \ + "You press center button on [src]. The device suddenly expands into a fully functional mining drone!") + var/mob/living/basic/mining_drone/drone = new(get_turf(src)) + drone.befriend(user) + qdel(src) diff --git a/code/modules/mob/living/basic/minebots/minebot_abilities.dm b/code/modules/mob/living/basic/minebots/minebot_abilities.dm new file mode 100644 index 00000000000..65ff261a172 --- /dev/null +++ b/code/modules/mob/living/basic/minebots/minebot_abilities.dm @@ -0,0 +1,48 @@ +/datum/action/innate/minedrone + check_flags = AB_CHECK_CONSCIOUS + button_background_icon_state = "bg_default" + +/datum/action/innate/minedrone/toggle_light + name = "Toggle Light" + button_overlay_icon_state = "mech_lights_off" + +/datum/action/innate/minedrone/toggle_light/Activate() + var/mob/living/basic/mining_drone/user = owner + + if(user.light_on) + user.set_light(0) + else + user.set_light(6) + user.light_on = !user.light_on + to_chat(user, "You toggle your light [user.light_on ? "on" : "off"].") + +/datum/action/innate/minedrone/toggle_meson_vision + name = "Toggle Meson Vision" + button_overlay_icon_state = "meson" + +/datum/action/innate/minedrone/toggle_meson_vision/Activate() + var/mob/living/user = owner + var/active = TRUE + if(HAS_TRAIT_FROM(user, TRAIT_MESON_VISION, "minebot")) + active = FALSE + REMOVE_TRAIT(user, TRAIT_MESON_VISION, "minebot") + else + ADD_TRAIT(user, TRAIT_MESON_VISION, "minebot") + + update_user_sight(user) + to_chat(user, "You toggle your meson vision [!active ? "on" : "off"].") + +/datum/action/innate/minedrone/toggle_meson_vision/proc/update_user_sight(mob/living/user) + user.sight = initial(user.sight) + user.lighting_alpha = initial(user.lighting_alpha) + if(HAS_TRAIT(user, TRAIT_MESON_VISION)) + user.sight |= SEE_TURFS + user.lighting_alpha = min(user.lighting_alpha, LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE) + +/datum/action/innate/minedrone/dump_ore + name = "Dump Ore" + button_overlay_icon_state = "mech_eject" + +/datum/action/innate/minedrone/dump_ore/Activate() + var/mob/living/basic/mining_drone/user = owner + user.drop_ore() diff --git a/code/modules/mob/living/basic/minebots/minebot_ai.dm b/code/modules/mob/living/basic/minebots/minebot_ai.dm new file mode 100644 index 00000000000..31666b704c8 --- /dev/null +++ b/code/modules/mob/living/basic/minebots/minebot_ai.dm @@ -0,0 +1,306 @@ +/datum/ai_controller/basic_controller/minebot + blackboard = list( + BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/always_check_factions, + BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends, + BB_MINIMUM_SHOOTING_DISTANCE = 3, + BB_MINEBOT_PLANT_MINES = TRUE, + BB_MINEBOT_REPAIR_DRONE = TRUE, + BB_MINEBOT_AUTO_DEFEND = TRUE, + BB_BLACKLIST_MINERAL_TURFS = list(), + BB_AUTOMATED_MINING = FALSE, + BB_OWNER_SELF_HARM_RESPONSES = list( + "Please stop hurting yourself.", + "There is no need to do that.", + "Your actions are illogical.", + "Please make better choices.", + "Remember, you have beaten your worst days before.", + ), + BB_OWNER_FRIENDLY_FIRE_APOLOGIES = list( + "Sorry.", + "My fault.", + "Oops.", + ), + ) + + ai_movement = /datum/ai_movement/jps + idle_behavior = /datum/idle_behavior/idle_random_walk + planning_subtrees = list( + /datum/ai_planning_subtree/simple_find_target, + /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/befriend_miners, + /datum/ai_planning_subtree/minebot_maintain_distance, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot, + /datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot, + /datum/ai_planning_subtree/minebot_mining, + /datum/ai_planning_subtree/flee_target/minebot_gibtonite, + /datum/ai_planning_subtree/locate_dead_humans, + ) + ai_traits = AI_FLAG_PAUSE_DURING_DO_AFTER + +/datum/ai_planning_subtree/befriend_miners/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard_key_exists(BB_MINER_FRIEND)) + return + controller.queue_behavior(/datum/ai_behavior/befriend_target, BB_MINER_FRIEND) + +/// Find dead humans and report their location on the radio +/datum/ai_planning_subtree/locate_dead_humans/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(controller.blackboard_key_exists(BB_NEARBY_DEAD_MINER)) + controller.queue_behavior(/datum/ai_behavior/send_sos_message, BB_NEARBY_DEAD_MINER) + return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/find_and_set/unconscious_human, BB_NEARBY_DEAD_MINER, /mob/living/carbon/human) + +/datum/ai_behavior/find_and_set/unconscious_human/search_tactic(datum/ai_controller/controller, locate_path, search_range) + // For now, we only look for friends, and we only befriend the person + // who created us, so we're not screaming on comms about every single + // dead player that we find + if(!controller.blackboard_key_exists(BB_FRIENDS_LIST)) + return null + for(var/mob/living/carbon/human/target in oview(search_range, controller.pawn)) + if(!(target in controller.blackboard[BB_FRIENDS_LIST])) + continue + if(target.stat >= UNCONSCIOUS && target.mind) + return target + return null + +/datum/ai_behavior/send_sos_message + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 2 MINUTES + +/datum/ai_behavior/send_sos_message/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/living/carbon/target = controller.blackboard[target_key] + var/mob/living/living_pawn = controller.pawn + if(QDELETED(target) || is_station_level(target.z)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/turf/target_turf = get_turf(target) + var/obj/item/radio/radio = locate(/obj/item/radio) in living_pawn.contents + if(!radio) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/message = message_to_multilingual("Miner in need of help at coordinates: [target_turf.x], [target_turf.y], [target_turf.z]!") + radio.talk_into(living_pawn, message, "Supply") + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/send_sos_message/finish_action(datum/ai_controller/controller, success, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + +/// operational datums is null because we dont use a ranged component, we use a gun in our contents +/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot + operational_datums = null + ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack/minebot + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/minebot/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return + var/mob/living/living_pawn = controller.pawn + if(living_pawn.a_intent != INTENT_HARM) + return + controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETING_STRATEGY, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/minebot_maintain_distance/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return + var/mob/living/living_pawn = controller.pawn + if(get_dist(living_pawn, target) <= controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE]) + controller.queue_behavior(/datum/ai_behavior/run_away_from_target/run_and_shoot, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/basic_ranged_attack/minebot + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + avoid_friendly_fire = TRUE + /// if our target is closer than this distance, finish action + var/minimum_distance = 3 + +/datum/ai_behavior/basic_ranged_attack/minebot/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + . = ..() + minimum_distance = controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] ? controller.blackboard[BB_MINIMUM_SHOOTING_DISTANCE] : initial(minimum_distance) + var/atom/target = controller.blackboard[target_key] + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + var/mob/living/living_pawn = controller.pawn + if(get_dist(living_pawn, target) <= minimum_distance) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/// mine walls if we are on automated mining mode +/datum/ai_planning_subtree/minebot_mining/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + if(!controller.blackboard[BB_AUTOMATED_MINING]) + return + if(controller.blackboard_key_exists(BB_MINEBOT_GIBTONITE_RUN)) + return + if(controller.blackboard_key_exists(BB_TARGET_MINERAL_TURF)) + controller.queue_behavior(/datum/ai_behavior/minebot_mine_turf, BB_TARGET_MINERAL_TURF) + return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/find_mineral_wall/minebot, BB_TARGET_MINERAL_TURF) + +/datum/ai_behavior/find_mineral_wall/minebot + +/datum/ai_behavior/find_mineral_wall/minebot/check_if_mineable(datum/ai_controller/controller, turf/target_wall) + var/list/forbidden_turfs = controller.blackboard[BB_BLACKLIST_MINERAL_TURFS] + var/turf/previous_unreachable_wall = controller.blackboard[BB_PREVIOUS_UNREACHABLE_WALL] + if(is_type_in_list(target_wall, forbidden_turfs) || target_wall == previous_unreachable_wall) + return FALSE + controller.clear_blackboard_key(BB_PREVIOUS_UNREACHABLE_WALL) + return ..() + +/datum/ai_behavior/minebot_mine_turf + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + required_distance = 2 + action_cooldown = 3 SECONDS + +/datum/ai_behavior/minebot_mine_turf/setup(datum/ai_controller/controller, target_key) + . = ..() + var/turf/target = controller.blackboard[target_key] + if(isnull(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/minebot_mine_turf/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/living/basic/living_pawn = controller.pawn + var/turf/target = controller.blackboard[target_key] + + if(QDELETED(target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + if(check_obstacles_in_path(controller, target)) + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED + + var/old_intent = living_pawn.a_intent + if(living_pawn.a_intent != INTENT_HARM) + living_pawn.a_intent = INTENT_HARM + + living_pawn.face_atom(target) + + RegisterSignal(target, COMSIG_MINE_EXPOSE_GIBTONITE, PROC_REF(on_mine_expose_gibtonite)) + + living_pawn.RangedAttack(target) + living_pawn.a_intent = old_intent + + return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED + +/datum/ai_behavior/minebot_mine_turf/proc/on_mine_expose_gibtonite(datum/source, mob/living/instigator) + SIGNAL_HANDLER // COMSIG_MINE_EXPOSE_GIBTONITE + + instigator.emote("me", EMOTE_VISIBLE, "yelps!") + instigator.ai_controller.set_blackboard_key(BB_MINEBOT_GIBTONITE_RUN, source) + +/datum/ai_behavior/minebot_mine_turf/proc/check_obstacles_in_path(datum/ai_controller/controller, turf/target) + var/mob/living/source = controller.pawn + var/list/turfs_in_path = get_line(source, target) - target + for(var/turf/turf as anything in turfs_in_path) + if(turf.is_blocked_turf(exclude_mobs = TRUE)) + controller.set_blackboard_key(BB_PREVIOUS_UNREACHABLE_WALL, target) + return TRUE + for(var/mob/living/potential_friend in turf) + if(source == potential_friend) + continue + if(controller.blackboard_key_exists(BB_FRIENDS_LIST) && (potential_friend in controller.blackboard[BB_FRIENDS_LIST])) + return TRUE + if(source.faction_check_mob(potential_friend)) + return TRUE + return FALSE + +/datum/ai_behavior/minebot_mine_turf/finish_action(datum/ai_controller/controller, success, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + +/// store ores in our body +/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot + hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/consume_ores/minebot + hunt_chance = 100 + hunt_range = 4 + +/datum/ai_planning_subtree/find_and_hunt_target/hunt_ores/minebot/select_behaviors(datum/ai_controller/controller, seconds_per_tick) + // If we're not on automated mining mode, don't pick up ore + // This is so we don't constantly pick up ore we were just told to drop + // while in another mode + if(!controller.blackboard[BB_AUTOMATED_MINING]) + return + + return ..() + +/datum/ai_planning_subtree/flee_target/minebot_gibtonite + target_key = BB_MINEBOT_GIBTONITE_RUN + +/// pet commands +/datum/pet_command/free/minebot + +/datum/pet_command/free/minebot/execute_action(datum/ai_controller/controller) + controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE) + return ..() + +/datum/pet_command/automate_mining + command_name = "Automate mining" + command_desc = "Make your minebot automatically mine!" + speech_commands = list("mine") + +/datum/pet_command/automate_mining/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to start mining!" + +/datum/pet_command/automate_mining/execute_action(datum/ai_controller/controller) + controller.set_blackboard_key(BB_AUTOMATED_MINING, TRUE) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + +/datum/pet_command/minebot_ability + command_name = "Minebot ability" + command_desc = "Make your minebot use one of its abilities." + /// the ability we will use + var/ability_key + +/datum/pet_command/minebot_ability/execute_action(datum/ai_controller/controller) + var/datum/action/ability = controller.blackboard[ability_key] + if(!ability?.IsAvailable()) + return + controller.queue_behavior(/datum/ai_behavior/use_mob_ability, ability_key) + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/pet_command/minebot_ability/light + command_name = "Toggle lights" + command_desc = "Make your minebot toggle its lights." + speech_commands = list("light") + ability_key = BB_MINEBOT_LIGHT_ABILITY + +/datum/pet_command/minebot_ability/light/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to toggle its lights!" + +/datum/pet_command/minebot_ability/dump + command_name = "Dump ore" + command_desc = "Make your minebot dump all its ore!" + speech_commands = list("dump", "ore") + ability_key = BB_MINEBOT_DUMP_ABILITY + +/datum/pet_command/minebot_ability/dump/retrieve_command_text(atom/living_pet, atom/target) + return "signals [living_pet] to dump its ore!" + +/datum/pet_command/attack/minebot + attack_behaviour = /datum/ai_behavior/basic_ranged_attack/minebot + +/datum/pet_command/attack/minebot/execute_action(datum/ai_controller/controller) + controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE) + var/mob/living/living_pawn = controller.pawn + if(living_pawn.a_intent != INTENT_HARM) + living_pawn.a_intent = INTENT_HARM + return ..() + +/datum/pet_command/idle/minebot + +/datum/pet_command/idle/minebot/execute_action(datum/ai_controller/controller) + controller.set_blackboard_key(BB_AUTOMATED_MINING, FALSE) + return ..() + +/datum/pet_command/protect_owner/minebot + +/datum/pet_command/protect_owner/minebot/set_command_target(mob/living/parent, atom/target) + if(!parent.ai_controller.blackboard[BB_MINEBOT_AUTO_DEFEND]) + return FALSE + if(!parent.ai_controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET) && !QDELETED(target)) //we are already dealing with something, + parent.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target) + return TRUE + +/datum/pet_command/protect_owner/minebot/execute_action(datum/ai_controller/controller) + if(controller.blackboard[BB_MINEBOT_AUTO_DEFEND]) + var/mob/living/living_pawn = controller.pawn + living_pawn.a_intent = INTENT_HARM + controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND) diff --git a/code/modules/mob/living/basic/minebots/minebot_upgrades.dm b/code/modules/mob/living/basic/minebots/minebot_upgrades.dm new file mode 100644 index 00000000000..6fb3e2c3564 --- /dev/null +++ b/code/modules/mob/living/basic/minebots/minebot_upgrades.dm @@ -0,0 +1,61 @@ +/// Minebot Melee Damage Upgrade +/obj/item/mine_bot_upgrade + name = "minebot melee upgrade" + desc = "A minebot upgrade." + icon_state = "door_electronics" + icon = 'icons/obj/doors/door_assembly.dmi' + new_attack_chain = TRUE + +/obj/item/mine_bot_upgrade/interact_with_atom(atom/target, mob/living/user, list/modifiers) + var/mob/living/basic/mining_drone/minebot = target + if(!istype(minebot)) + return ..() + + upgrade_bot(minebot, user) + return ITEM_INTERACT_COMPLETE + +/obj/item/mine_bot_upgrade/proc/upgrade_bot(mob/living/basic/mining_drone/M, mob/user) + if(M.melee_damage_upper != initial(M.melee_damage_upper)) + to_chat(user, "[M] already has a combat upgrade installed!") + return + M.melee_damage_lower += 7 + M.melee_damage_upper += 7 + to_chat(user, "You upgrade [M]'s combat module.") + qdel(src) + +/// Minebot Health Upgrade +/obj/item/mine_bot_upgrade/health + name = "minebot armor upgrade" + +/obj/item/mine_bot_upgrade/health/upgrade_bot(mob/living/basic/mining_drone/M, mob/user) + if(M.maxHealth != initial(M.maxHealth)) + to_chat(user, "[M] already has a reinforced chassis!") + return + M.maxHealth += 45 + M.updatehealth() + to_chat(user, "You upgrade [M]'s chassis.") + qdel(src) + +/// Minebot AI upgrade/sentience potion +/obj/item/slimepotion/sentience/mining + name = "minebot AI upgrade" + desc = "Can be used to grant sentience to minebots." + icon_state = "door_electronics" + icon = 'icons/obj/doors/door_assembly.dmi' + sentience_type = SENTIENCE_MINEBOT + origin_tech = "programming=6" + var/base_health_add = 5 // sentient minebots are penalized for beign sentient; they have their stats reset to normal plus these values + var/base_damage_add = 1 // this thus disables other minebot upgrades + var/base_speed_add = 1 + var/base_cooldown_add = 10 // base cooldown isn't reset to normal, it's just added on, since it's not practical to disable the cooldown module + +/obj/item/slimepotion/sentience/mining/after_success(mob/living/user, mob/living/simple_animal/SM) + if(istype(SM, /mob/living/basic/mining_drone)) + var/mob/living/basic/mining_drone/M = SM + M.maxHealth = initial(M.maxHealth) + base_health_add + M.melee_damage_lower = initial(M.melee_damage_lower) + base_damage_add + M.melee_damage_upper = initial(M.melee_damage_upper) + base_damage_add + if(M.stored_gun) + M.stored_gun.overheat_time += base_cooldown_add + if(M.mind) + M.mind.offstation_role = TRUE diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 2e1283bad08..fed9832c338 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -40,7 +40,6 @@ med_hud_set_health() med_hud_set_status() - /mob/living/Destroy() if(ranged_ability) ranged_ability.remove_ranged_ability(src) @@ -67,6 +66,12 @@ ranged_ability.remove_mousepointer(prev_client) SEND_SIGNAL(src, COMSIG_LIVING_GHOSTIZED) +/// Legacy method for simplemobs to handle turning off their AI. +/// Unrelated to and unused for AI controllers, which handle their +/// AI cooperation with signals. +/mob/living/proc/sentience_act() + return + /mob/living/proc/OpenCraftingMenu() return @@ -1266,3 +1271,18 @@ /mob/living/proc/sec_hud_set_ID() return + +/// Proc for giving a mob a new 'friend', generally used for AI control and +/// targeting. Returns false if already friends or null if qdeleted. +/mob/living/proc/befriend(mob/living/new_friend) + SHOULD_CALL_PARENT(TRUE) + if(QDELETED(new_friend)) + return + var/friend_ref = new_friend.UID() + if(faction.Find(friend_ref)) + return FALSE + faction |= friend_ref + ai_controller?.insert_blackboard_key_lazylist(BB_FRIENDS_LIST, new_friend) + + SEND_SIGNAL(src, COMSIG_LIVING_BEFRIENDED, new_friend) + return TRUE diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm index f9357358420..3da93373ced 100644 --- a/code/modules/mob/living/living_defines.dm +++ b/code/modules/mob/living/living_defines.dm @@ -15,6 +15,8 @@ var/health = 100 //A mob's health /// Healable by medical stacks? var/healable = FALSE + /// The action emote which is displayed after the mob's name upon death. + var/deathmessage = "" //Damage related vars, NOTE: THESE SHOULD ONLY BE MODIFIED BY PROCS VAR_PROTECTED/bruteloss = 0 //Brutal damage caused by brute force (punching, being clubbed by a toolbox ect... this also accounts for pressure damage) diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm index 8db87c4fc1b..45169dfef6b 100644 --- a/code/modules/mob/living/living_say.dm +++ b/code/modules/mob/living/living_say.dm @@ -289,6 +289,8 @@ GLOBAL_LIST_EMPTY(channel_to_radio_key) if(get_turf(M) in hearturfs) listening |= M + SEND_SIGNAL(src, COMSIG_MOB_SAY, args) + var/list/speech_bubble_recipients = list() var/speech_bubble_test = say_test(message) diff --git a/code/modules/mob/living/simple_animal/hostile/mining/goldgrub.dm b/code/modules/mob/living/simple_animal/hostile/mining/goldgrub.dm index 1b6a0675acb..416b8d19d18 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining/goldgrub.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining/goldgrub.dm @@ -36,6 +36,8 @@ /mob/living/simple_animal/hostile/asteroid/goldgrub/Initialize(mapload) . = ..() + faction |= "goldgrub" + var/i = rand(1,3) while(i) loot += pick(/obj/item/stack/ore/silver, /obj/item/stack/ore/gold, /obj/item/stack/ore/uranium, /obj/item/stack/ore/diamond) diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 9bbc0cf3e72..2c07eb67fef 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -110,7 +110,6 @@ var/list/loot = list() /// Causes mob to be deleted on death, useful for mobs that spawn lootable corpses var/del_on_death = FALSE - var/deathmessage = "" /// The sound played on death var/death_sound = null @@ -482,7 +481,7 @@ if(changed) animate(src, transform = ntransform, time = 2, easing = EASE_IN|EASE_OUT) -/mob/living/simple_animal/proc/sentience_act() //Called when a simple animal gains sentience via gold slime potion +/mob/living/simple_animal/sentience_act() // Called when a simple animal gains sentience via gold slime potion toggle_ai(AI_OFF) can_have_ai = FALSE diff --git a/code/modules/mob/mob_misc_procs.dm b/code/modules/mob/mob_misc_procs.dm index 3474fadcefa..7d14dc55c0a 100644 --- a/code/modules/mob/mob_misc_procs.dm +++ b/code/modules/mob/mob_misc_procs.dm @@ -480,7 +480,7 @@ if(hud_used && hud_used.action_intent) hud_used.action_intent.icon_state = "[a_intent]" - else if(isrobot(src) || islarva(src) || isanimal(src) || is_ai(src)) + else if(isrobot(src) || islarva(src) || isanimal_or_basicmob(src) || is_ai(src)) switch(input) if(INTENT_HELP) a_intent = INTENT_HELP diff --git a/code/modules/point/point.dm b/code/modules/point/point.dm index c25dc793610..40992ed2a32 100644 --- a/code/modules/point/point.dm +++ b/code/modules/point/point.dm @@ -22,6 +22,8 @@ var/turf/our_tile = get_turf(src) var/obj/visual = new /obj/effect/temp_visual/point(our_tile, invisibility) + SEND_SIGNAL(src, COMSIG_MOVABLE_POINTED, pointed_atom, visual) + animate(visual, pixel_x = (tile.x - our_tile.x) * world.icon_size + pointed_atom.pixel_x, pixel_y = (tile.y - our_tile.y) * world.icon_size + pointed_atom.pixel_y, time = 1.7, easing = EASE_OUT) /// Create a bubble pointing at a particular icon and icon state. diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm index aa02b7910ad..a293c6af720 100644 --- a/code/modules/projectiles/gun.dm +++ b/code/modules/projectiles/gun.dm @@ -161,7 +161,7 @@ "You fire [src]!", "You hear \a [fire_sound_text]!" ) - if(chambered.muzzle_flash_effect) + if(chambered?.muzzle_flash_effect) var/obj/effect/temp_visual/target_angled/muzzle_flash/effect = new chambered.muzzle_flash_effect(get_turf(src), target, muzzle_flash_time) effect.alpha = min(255, muzzle_strength * 255) if(chambered.muzzle_flash_color) diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm index 157f049155f..29903d8bdf7 100644 --- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm +++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm @@ -328,10 +328,10 @@ message_admins("User attempted to install a modkit into a kinetic accelerator while it is installed in an accelerator. Could be a double click, or an exploit attempt.") return FALSE if(minebot_upgrade) - if(minebot_exclusive && !istype(KA.loc, /mob/living/simple_animal/hostile/mining_drone)) + if(minebot_exclusive && !istype(KA.loc, /mob/living/basic/mining_drone)) to_chat(user, "The modkit you're trying to install is only rated for minebot use.") return FALSE - else if(istype(KA.loc, /mob/living/simple_animal/hostile/mining_drone)) + else if(istype(KA.loc, /mob/living/basic/mining_drone)) to_chat(user, "The modkit you're trying to install is not rated for minebot use.") return FALSE if(denied_type) diff --git a/code/modules/projectiles/projectile_base.dm b/code/modules/projectiles/projectile_base.dm index fa6f81aac3b..81be0ad0f38 100644 --- a/code/modules/projectiles/projectile_base.dm +++ b/code/modules/projectiles/projectile_base.dm @@ -610,5 +610,22 @@ return FALSE +/// Fire a projectile from this atom at another atom +/atom/proc/fire_projectile(projectile_type, atom/target, sound, firer, list/ignore_targets = list()) + if(!isnull(sound)) + playsound(src, sound, vol = 100, vary = TRUE) + + var/turf/startloc = get_turf(src) + var/obj/item/projectile/bullet = new projectile_type(startloc) + bullet.starting = startloc + bullet.firer = firer || src + bullet.firer_source_atom = src + bullet.yo = target.y - startloc.y + bullet.xo = target.x - startloc.x + bullet.original = target + bullet.preparePixelProjectile(target, src) + bullet.fire() + return bullet + #undef MOVES_HITSCAN #undef MUZZLE_EFFECT_PIXEL_INCREMENT diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm index 60a5b196fa8..4deb8970408 100644 --- a/code/modules/research/xenobiology/xenobiology.dm +++ b/code/modules/research/xenobiology/xenobiology.dm @@ -233,7 +233,7 @@ if(being_used || !ismob(target)) return FALSE var/mob/M = target - if(!isanimal(M) || M.mind) //only works on animals that aren't player controlled + if(!isanimal_or_basicmob(M) || M.mind) // only works on animals that aren't player controlled to_chat(user, "[M] is already too intelligent for this to work!") return FALSE if(M.stat) diff --git a/code/tests/attack_chain/test_attack_chain_mobs.dm b/code/tests/attack_chain/test_attack_chain_mobs.dm index fd69cfd3bb0..3544f73f64b 100644 --- a/code/tests/attack_chain/test_attack_chain_mobs.dm +++ b/code/tests/attack_chain/test_attack_chain_mobs.dm @@ -42,7 +42,7 @@ cqc.remove(player.puppet) player.set_intent(INTENT_HELP) - var/mob/mining_drone = player.spawn_mob_nearby(/mob/living/simple_animal/hostile/mining_drone) + var/mob/mining_drone = player.spawn_mob_nearby(/mob/living/basic/mining_drone) var/obj/scanner = player.spawn_obj_in_hand(/obj/item/mining_scanner) player.click_on(mining_drone) TEST_ASSERT_LAST_CHATLOG(player, "drop any collected ore.") diff --git a/paradise.dme b/paradise.dme index 10b8bd55dab..4cec028e1b4 100644 --- a/paradise.dme +++ b/paradise.dme @@ -452,29 +452,38 @@ #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\emote_with_target.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\befriend_target.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\stop_and_stare.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_ai_behaviors\travel_towards.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\mine_walls.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\stare_at_thing.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\simple_find_target.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\basic_mobs\hunting_behavior\hunting_behaviors.dm" +#include "code\datums\ai\basic_mobs\basic_subtrees\use_mob_ability.dm" +#include "code\datums\ai\basic_mobs\hunting_behavior\find_and_hunt_target.dm" +#include "code\datums\ai\basic_mobs\hunting_behavior\find_hunt_target.dm" +#include "code\datums\ai\basic_mobs\hunting_behavior\hunt_target.dm" #include "code\datums\ai\generic\find_and_set.dm" #include "code\datums\ai\generic\generic_behaviors.dm" #include "code\datums\ai\idle_behaviors\idle_behavior.dm" #include "code\datums\ai\idle_behaviors\idle_random_walk.dm" #include "code\datums\ai\movement\ai_movement.dm" +#include "code\datums\ai\movement\ai_movement_jps.dm" #include "code\datums\ai\movement\basic_avoidance.dm" #include "code\datums\ai\movement\complete_stop.dm" #include "code\datums\ai\targeting_strategy\basic_targeting_strategy.dm" +#include "code\datums\ai\targeting_strategy\dont_target_friends.dm" #include "code\datums\ai\targeting_strategy\targeting_strategy.dm" #include "code\datums\cache\air_alarm.dm" #include "code\datums\cache\apc_cache.dm" @@ -517,6 +526,7 @@ #include "code\datums\components\parry.dm" #include "code\datums\components\persistent_overlay.dm" #include "code\datums\components\radioactive.dm" +#include "code\datums\components\ranged_attacks.dm" #include "code\datums\components\scope.dm" #include "code\datums\components\shelved.dm" #include "code\datums\components\shielded.dm" @@ -533,6 +543,11 @@ #include "code\datums\components\tippable.dm" #include "code\datums\components\two_handed.dm" #include "code\datums\components\zombie_regen.dm" +#include "code\datums\components\pet_commands\obeys_commands.dm" +#include "code\datums\components\pet_commands\pet_command.dm" +#include "code\datums\components\pet_commands\pet_command_planning.dm" +#include "code\datums\components\pet_commands\pet_commands_basic.dm" +#include "code\datums\components\pet_commands\pet_follow_friend.dm" #include "code\datums\components\riding\riding.dm" #include "code\datums\components\riding\riding_vehicle.dm" #include "code\datums\discord\discord_manager.dm" @@ -2285,7 +2300,6 @@ #include "code\modules\mining\machine_unloading.dm" #include "code\modules\mining\machine_vending.dm" #include "code\modules\mining\mine_items.dm" -#include "code\modules\mining\minebot.dm" #include "code\modules\mining\mint.dm" #include "code\modules\mining\money_bag.dm" #include "code\modules\mining\ores_coins.dm" @@ -2373,6 +2387,10 @@ #include "code\modules\mob\living\basic\farm_animals\deer.dm" #include "code\modules\mob\living\basic\farm_animals\deer_ai.dm" #include "code\modules\mob\living\basic\farm_animals\pig.dm" +#include "code\modules\mob\living\basic\minebots\minebot.dm" +#include "code\modules\mob\living\basic\minebots\minebot_abilities.dm" +#include "code\modules\mob\living\basic\minebots\minebot_ai.dm" +#include "code\modules\mob\living\basic\minebots\minebot_upgrades.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"