diff --git a/code/__DEFINES/ai.dm b/code/__DEFINES/ai.dm index d9a52bbb3b2..fa482dc6103 100644 --- a/code/__DEFINES/ai.dm +++ b/code/__DEFINES/ai.dm @@ -26,7 +26,10 @@ #define AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION (1<<4) ///AI flags +/// Don't move if being pulled #define STOP_MOVING_WHEN_PULLED (1<<0) +/// Don't act if you're dead +#define STOP_ACTING_WHILE_DEAD (1<<1) //Base Subtree defines @@ -157,14 +160,9 @@ ///Dog AI controller blackboard keys #define BB_SIMPLE_CARRY_ITEM "BB_SIMPLE_CARRY_ITEM" -#define BB_FETCH_TARGET "BB_FETCH_TARGET" #define BB_FETCH_IGNORE_LIST "BB_FETCH_IGNORE_LISTlist" #define BB_FETCH_DELIVER_TO "BB_FETCH_DELIVER_TO" -#define BB_DOG_FRIENDS "BB_DOG_FRIENDS" -#define BB_DOG_ORDER_MODE "BB_DOG_ORDER_MODE" -#define BB_DOG_PLAYING_DEAD "BB_DOG_PLAYING_DEAD" #define BB_DOG_HARASS_TARGET "BB_DOG_HARASS_TARGET" -#define BB_DOG_HARASS_FRUSTRATION "BB_DOG_HARASS_FRUSTRATION" #define BB_DOG_HARASS_HARM "BB_DOG_HARASS_HARM" #define BB_DOG_IS_SLOW "BB_DOG_IS_SLOW" diff --git a/code/__DEFINES/ai_pet_commands.dm b/code/__DEFINES/ai_pet_commands.dm index 8887a3875dd..5894aedff14 100644 --- a/code/__DEFINES/ai_pet_commands.dm +++ b/code/__DEFINES/ai_pet_commands.dm @@ -1,15 +1,5 @@ /// Blackboard field for the most recent command the pet was given #define BB_ACTIVE_PET_COMMAND "BB_active_pet_command" -/// Follow your normal behaviour -#define PET_COMMAND_NONE "pet_command_none" -/// Don't take any actions at all -#define PET_COMMAND_IDLE "pet_command_idle" -/// Pursue and attack the pointed target -#define PET_COMMAND_ATTACK "pet_command_attack" -/// Pursue the person who made this command -#define PET_COMMAND_FOLLOW "pet_commmand_follow" -/// Use a targetted mob ability -#define PET_COMMAND_USE_ABILITY "pet_command_use_ability" /// Blackboard field for what we actually want the pet to target #define BB_CURRENT_PET_TARGET "BB_current_pet_target" diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm index aed55ce70af..b878fb21823 100644 --- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm +++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm @@ -133,3 +133,5 @@ /// 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" diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 6fcb5baf892..b7f255dee2c 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -8,7 +8,7 @@ multiple modular subtrees with behaviors ///The atom this controller is controlling var/atom/pawn ///Bitfield of traits for this AI to handle extra behavior - var/ai_traits + var/ai_traits = STOP_ACTING_WHILE_DEAD ///Current actions planned to be performed by the AI in the upcoming plan var/list/planned_behaviors ///Current actions being performed by the AI. @@ -106,24 +106,34 @@ multiple modular subtrees with behaviors pawn = new_pawn pawn.ai_controller = src - if(!continue_processing_when_client && istype(new_pawn, /mob)) - var/mob/possible_client_holder = new_pawn - if(possible_client_holder.client) - set_ai_status(AI_STATUS_OFF) - else - set_ai_status(AI_STATUS_ON) - else + if (!ismob(new_pawn)) set_ai_status(AI_STATUS_ON) + else + set_ai_status(get_setup_mob_ai_status(new_pawn)) RegisterSignal(pawn, COMSIG_MOB_LOGIN, PROC_REF(on_sentience_gained)) +/// Mobs have more complicated factors about whether their AI should be on or not +/datum/ai_controller/proc/get_setup_mob_ai_status(mob/mob_pawn) + var/final_status = AI_STATUS_ON + + if(!continue_processing_when_client && mob_pawn.client) + final_status = AI_STATUS_OFF + + if(ai_traits & STOP_ACTING_WHILE_DEAD) + RegisterSignal(pawn, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed)) + if(mob_pawn.stat == DEAD) + final_status = AI_STATUS_OFF + + return final_status + ///Abstract proc for initializing the pawn to the new controller /datum/ai_controller/proc/TryPossessPawn(atom/new_pawn) return ///Proc for deinitializing the pawn to the old controller /datum/ai_controller/proc/UnpossessPawn(destroy) - UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT)) + UnregisterSignal(pawn, list(COMSIG_MOB_LOGIN, COMSIG_MOB_LOGOUT, COMSIG_MOB_STATCHANGE)) if(ai_movement.moving_controllers[src]) ai_movement.stop_moving_towards(src) pawn.ai_controller = null @@ -161,6 +171,7 @@ multiple modular subtrees with behaviors CancelActions() return + for(var/datum/ai_behavior/current_behavior as anything in current_behaviors) // Convert the current behaviour action cooldown to realtime seconds from deciseconds.current_behavior @@ -284,6 +295,12 @@ multiple modular subtrees with behaviors arguments += stored_arguments current_behavior.finish_action(arglist(arguments)) +/// Turn the controller on or off based on if you're alive, we only register to this if the flag is present so don't need to check again +/datum/ai_controller/proc/on_stat_changed(mob/living/source, new_stat) + SIGNAL_HANDLER + var/new_ai_status = (new_stat == DEAD) ? AI_STATUS_OFF : AI_STATUS_ON + set_ai_status(new_ai_status) + /datum/ai_controller/proc/on_sentience_gained() SIGNAL_HANDLER UnregisterSignal(pawn, COMSIG_MOB_LOGIN) diff --git a/code/datums/ai/basic_mobs/basic_ai_behaviors/pick_up_item.dm b/code/datums/ai/basic_mobs/basic_ai_behaviors/pick_up_item.dm new file mode 100644 index 00000000000..c4b1f1dd9b7 --- /dev/null +++ b/code/datums/ai/basic_mobs/basic_ai_behaviors/pick_up_item.dm @@ -0,0 +1,50 @@ +/** + * Simple behaviour for picking up an item we are already in range of. + * The blackboard storage key isn't very safe because it doesn't make sense to register signals in here. + * Use the AI held item component to manage this. + */ +/datum/ai_behavior/pick_up_item + +/datum/ai_behavior/pick_up_item/setup(datum/ai_controller/controller, target_key, storage_key) + . = ..() + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/obj/item/target = weak_target?.resolve() + return isitem(target) && isturf(target.loc) && !target.anchored + +/datum/ai_behavior/pick_up_item/perform(delta_time, datum/ai_controller/controller, target_key, storage_key) + . = ..() + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/obj/item/target = weak_target?.resolve() + if(!isturf(target?.loc)) // Someone picked it up or it got deleted + finish_action(controller, FALSE, target_key) + return + if(!in_range(controller.pawn, target)) // It teleported + finish_action(controller, FALSE, target_key) + return + pickup_item(controller, target, storage_key) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/pick_up_item/finish_action(datum/ai_controller/controller, success, target_key, storage_key) + . = ..() + controller.blackboard[target_key] = null + +/datum/ai_behavior/pick_up_item/proc/pickup_item(datum/ai_controller/controller, obj/item/target, storage_key) + var/atom/pawn = controller.pawn + drop_existing_item(controller, storage_key) + pawn.visible_message(span_notice("[pawn] picks up [target].")) + target.forceMove(pawn) + controller.blackboard[storage_key] = WEAKREF(target) + return TRUE + +/datum/ai_behavior/pick_up_item/proc/drop_existing_item(datum/ai_controller/controller, storage_key) + var/datum/weakref/carried_ref = controller.blackboard[storage_key] + var/obj/item/carried_item = carried_ref?.resolve() + if(!carried_item) + return + controller.blackboard[storage_key] = null + var/atom/pawn = controller.pawn + if(carried_item.loc != pawn) + return + pawn.visible_message(span_notice("[pawn] drops [carried_item].")) + carried_item.forceMove(get_turf(pawn)) + return TRUE diff --git a/code/datums/ai/basic_mobs/pet_commands/fetch.dm b/code/datums/ai/basic_mobs/pet_commands/fetch.dm new file mode 100644 index 00000000000..e0e87e6e1a6 --- /dev/null +++ b/code/datums/ai/basic_mobs/pet_commands/fetch.dm @@ -0,0 +1,157 @@ +/** + * Traverse to a target with the intention of picking it up. + * If we can't do that, add it to a list of ignored items. + */ +/datum/ai_behavior/fetch_seek + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/fetch_seek/setup(datum/ai_controller/controller, target_key, delivery_key) + . = ..() + var/datum/weakref/thing_ref = controller.blackboard[target_key] + var/obj/item/fetch_thing = thing_ref?.resolve() + + // It stopped existing + if (!fetch_thing) + return FALSE + set_movement_target(fetch_thing) + +/datum/ai_behavior/fetch_seek/perform(delta_time, datum/ai_controller/controller, target_key, delivery_key) + . = ..() + var/datum/weakref/thing_ref = controller.blackboard[target_key] + var/obj/item/fetch_thing = thing_ref?.resolve() + + // It stopped existing + if (!fetch_thing) + finish_action(controller, FALSE, target_key, delivery_key) + return + var/mob/living/living_pawn = controller.pawn + // We can't pick this up + if (fetch_thing.anchored || !isturf(fetch_thing.loc) || !living_pawn.CanReach(fetch_thing)) + finish_action(controller, FALSE, target_key, delivery_key) + return + + finish_action(controller, TRUE, target_key, delivery_key) + +/datum/ai_behavior/fetch_seek/finish_action(datum/ai_controller/controller, success, target_key, delivery_key) + . = ..() + if (success) + return + // Blacklist item if we failed + var/datum/weakref/thing_ref = controller.blackboard[target_key] + var/obj/item/target = thing_ref?.resolve() + if (target) + controller.blackboard[BB_FETCH_IGNORE_LIST][thing_ref] = TRUE + controller.blackboard[target_key] = null + controller.blackboard[delivery_key] = null + +/** + * The second half of fetching, deliver the item to a target. + */ +/datum/ai_behavior/deliver_fetched_item + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/deliver_fetched_item/setup(datum/ai_controller/controller, delivery_key, storage_key) + . = ..() + var/datum/weakref/return_ref = controller.blackboard[delivery_key] + var/mob/living/return_target = return_ref?.resolve() + if(!return_target) // Guess it's mine now + return FALSE + set_movement_target(return_target) + +/datum/ai_behavior/deliver_fetched_item/perform(delta_time, datum/ai_controller/controller, delivery_key, storage_key) + . = ..() + var/datum/weakref/return_ref = controller.blackboard[delivery_key] + var/mob/living/return_target = return_ref?.resolve() + if(!return_target) + finish_action(controller, FALSE, delivery_key) + return + + deliver_item(controller, return_target, storage_key) + finish_action(controller, TRUE, delivery_key) + +/datum/ai_behavior/deliver_fetched_item/finish_action(datum/ai_controller/controller, success, delivery_key) + . = ..() + controller.blackboard[delivery_key] = null + controller.blackboard[BB_ACTIVE_PET_COMMAND] = null + +/// Actually deliver the fetched item to the target, if we still have it +/datum/ai_behavior/deliver_fetched_item/proc/deliver_item(datum/ai_controller/controller, return_target, storage_key) + var/mob/pawn = controller.pawn + var/datum/weakref/carried_ref = controller.blackboard[storage_key] + var/obj/item/carried_item = carried_ref?.resolve() + if(!carried_item || carried_item.loc != pawn) + pawn.visible_message(span_notice("[pawn] looks around as if [pawn.p_they()] [pawn.p_have()] lost something.")) + finish_action(controller, FALSE) + return + + pawn.visible_message(span_notice("[pawn] delivers [carried_item] to [return_target].")) + carried_item.forceMove(get_turf(return_target)) + controller.blackboard[storage_key] = null + return TRUE + +/** + * The alternate second half of fetching, attack the item if we can eat it. + * Or make pleading eyes at someone who has picked it up. + * + * Unfortunately this doesn't work because food can't currently be eaten by mobs. + */ +/datum/ai_behavior/eat_fetched_snack + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + action_cooldown = 0.8 SECONDS + +/datum/ai_behavior/eat_fetched_snack/setup(datum/ai_controller/controller, target_key, delivery_key) + . = ..() + var/datum/weakref/thing_ref = controller.blackboard[target_key] + var/obj/item/snack = thing_ref?.resolve() + + if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc))) + return FALSE // This isn't food at all! + set_movement_target(snack) + +/datum/ai_behavior/eat_fetched_snack/perform(delta_time, datum/ai_controller/controller, target_key, delivery_key) + . = ..() + var/datum/weakref/thing_ref = controller.blackboard[target_key] + var/obj/item/snack = thing_ref?.resolve() + + if(!(isturf(snack.loc) || ishuman(snack.loc))) + finish_action(controller, FALSE) // Where did it go? + + var/mob/living/basic/basic_pawn = controller.pawn + if(!in_range(basic_pawn, snack)) + return + + if(isturf(snack.loc)) + basic_pawn.melee_attack(snack) // snack attack! + else if(iscarbon(snack.loc) && DT_PROB(10, delta_time)) + basic_pawn.manual_emote("Stares at [snack.loc]'s [snack.name] intently.") + + if(QDELETED(snack)) // we ate it! + finish_action(controller, TRUE, target_key, delivery_key) + +/datum/ai_behavior/eat_fetched_snack/finish_action(datum/ai_controller/controller, succeeded, target_key, delivery_key) + . = ..() + controller.blackboard[target_key] = null + controller.blackboard[delivery_key] = null + controller.blackboard[BB_ACTIVE_PET_COMMAND] = null + +/** + * Clear our failed fetch list every so often + */ +/datum/ai_behavior/forget_failed_fetches + /// How long to wait between resetting the list + var/cooldown_duration = AI_FETCH_IGNORE_DURATION + /// Time until we should forget things we failed to pick up + COOLDOWN_DECLARE(reset_ignore_cooldown) + +/datum/ai_behavior/forget_failed_fetches/setup(datum/ai_controller/controller, ...) + . = ..() + if (!COOLDOWN_FINISHED(src, reset_ignore_cooldown)) + return FALSE + if (!length(controller.blackboard[BB_FETCH_IGNORE_LIST])) + return + +/datum/ai_behavior/forget_failed_fetches/perform(delta_time, datum/ai_controller/controller) + . = ..() + COOLDOWN_START(src, reset_ignore_cooldown, cooldown_duration) + controller.blackboard[BB_FETCH_IGNORE_LIST] = list() + finish_action(controller, TRUE) diff --git a/code/datums/ai/basic_mobs/pet_commands/pet_command_planning.dm b/code/datums/ai/basic_mobs/pet_commands/pet_command_planning.dm index bf1b13b7853..8489bb55ebd 100644 --- a/code/datums/ai/basic_mobs/pet_commands/pet_command_planning.dm +++ b/code/datums/ai/basic_mobs/pet_commands/pet_command_planning.dm @@ -8,11 +8,8 @@ /datum/ai_planning_subtree/pet_planning /datum/ai_planning_subtree/pet_planning/SelectBehaviors(datum/ai_controller/controller, delta_time) - var/active_command_key = controller.blackboard[BB_ACTIVE_PET_COMMAND] - if (!active_command_key) - return // Do something else - var/datum/weakref/weak_command = controller.blackboard[active_command_key] + var/datum/weakref/weak_command = controller.blackboard[BB_ACTIVE_PET_COMMAND] var/datum/pet_command/command = weak_command?.resolve() if (!command) - return // We forgot this command at some point + return // Do something else return command.execute_action(controller) diff --git a/code/datums/ai/basic_mobs/pet_commands/pet_use_targetted_ability.dm b/code/datums/ai/basic_mobs/pet_commands/pet_use_targetted_ability.dm index 940993cb393..3e8f1246e1c 100644 --- a/code/datums/ai/basic_mobs/pet_commands/pet_use_targetted_ability.dm +++ b/code/datums/ai/basic_mobs/pet_commands/pet_use_targetted_ability.dm @@ -23,5 +23,4 @@ /datum/ai_behavior/pet_use_ability/finish_action(datum/ai_controller/controller, succeeded, ability_key, target_key) . = ..() - controller.blackboard[BB_ACTIVE_PET_COMMAND] = PET_COMMAND_IDLE // Wait for further instruction controller.blackboard[target_key] = null diff --git a/code/datums/ai/basic_mobs/pet_commands/play_dead.dm b/code/datums/ai/basic_mobs/pet_commands/play_dead.dm new file mode 100644 index 00000000000..5eb86c60309 --- /dev/null +++ b/code/datums/ai/basic_mobs/pet_commands/play_dead.dm @@ -0,0 +1,24 @@ +/// Pretend to be dead +/datum/ai_behavior/play_dead + +/datum/ai_behavior/play_dead/setup(datum/ai_controller/controller) + . = ..() + var/mob/living/basic/basic_pawn = controller.pawn + if(!istype(basic_pawn) || basic_pawn.stat) // Can't act dead if you're dead + return + basic_pawn.emote("deathgasp", intentional=FALSE) + basic_pawn.look_dead() + +/datum/ai_behavior/play_dead/perform(delta_time, datum/ai_controller/controller) + . = ..() + if(DT_PROB(10, delta_time)) + finish_action(controller, TRUE) + +/datum/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded) + . = ..() + var/mob/living/basic/basic_pawn = controller.pawn + if(!istype(basic_pawn) || basic_pawn.stat) // imagine actually dying while playing dead. hell, imagine being the kid waiting for your pup to get back up :( + return + basic_pawn.visible_message(span_notice("[basic_pawn] miraculously springs back to life!")) + basic_pawn.look_alive() + controller.blackboard[BB_ACTIVE_PET_COMMAND] = null diff --git a/code/datums/ai/dog/dog_behaviors.dm b/code/datums/ai/dog/dog_behaviors.dm index 2424299510a..36640e260b5 100644 --- a/code/datums/ai/dog/dog_behaviors.dm +++ b/code/datums/ai/dog/dog_behaviors.dm @@ -1,233 +1,60 @@ -/datum/ai_behavior/battle_screech/dog - screeches = list("barks","howls") - -/// Fetching makes the pawn chase after whatever it's targeting and pick it up when it's in range, with the dog_equip behavior -/datum/ai_behavior/fetch - behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT - -/datum/ai_behavior/fetch/perform(delta_time, datum/ai_controller/controller) - . = ..() - var/mob/living/living_pawn = controller.pawn - var/obj/item/fetch_thing = controller.blackboard[BB_FETCH_TARGET] - - //either we can't pick it up, or we'd rather eat it, so stop trying. - if(fetch_thing.anchored || !isturf(fetch_thing.loc) || IS_EDIBLE(fetch_thing) || !living_pawn.CanReach(fetch_thing)) - finish_action(controller, FALSE) - return - - finish_action(controller, TRUE) - -/datum/ai_behavior/fetch/finish_action(datum/ai_controller/controller, success) - . = ..() - - if(!success) //Don't try again on this item if we failed - var/obj/item/target = controller.blackboard[BB_FETCH_TARGET] - if(target) - controller.blackboard[BB_FETCH_IGNORE_LIST][WEAKREF(target)] = TRUE - controller.blackboard[BB_FETCH_TARGET] = null - controller.blackboard[BB_FETCH_DELIVER_TO] = null - - -/// This is simply a behaviour to pick up a fetch target -/datum/ai_behavior/simple_equip/perform(delta_time, datum/ai_controller/controller) - . = ..() - var/obj/item/fetch_target = controller.blackboard[BB_FETCH_TARGET] - if(!isturf(fetch_target?.loc) || !isitem(fetch_target)) // someone picked it up, something happened to it, or it wasn't an item anyway - finish_action(controller, FALSE) - return - - if(in_range(controller.pawn, fetch_target)) - pickup_item(controller, fetch_target) - finish_action(controller, TRUE) - else - finish_action(controller, FALSE) - -/datum/ai_behavior/simple_equip/finish_action(datum/ai_controller/controller, success) - . = ..() - controller.blackboard[BB_FETCH_TARGET] = null - if(!success) - controller.blackboard[BB_FETCH_DELIVER_TO] = null - -/datum/ai_behavior/simple_equip/proc/pickup_item(datum/ai_controller/controller, obj/item/target) - var/atom/pawn = controller.pawn - drop_item(controller) - pawn.visible_message(span_notice("[pawn] picks up [target] in [pawn.p_their()] mouth.")) - target.forceMove(pawn) - controller.blackboard[BB_SIMPLE_CARRY_ITEM] = target - return TRUE - -/datum/ai_behavior/simple_equip/proc/drop_item(datum/ai_controller/controller) - var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] - if(!carried_item) - return - - var/atom/pawn = controller.pawn - pawn.visible_message(span_notice("[pawn] drops [carried_item].")) - carried_item.forceMove(get_turf(pawn)) - controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null - return TRUE - - - -/// This behavior involves dropping off a carried item to a specified person (or place) -/datum/ai_behavior/deliver_item - behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT - -/datum/ai_behavior/deliver_item/perform(delta_time, datum/ai_controller/controller) - . = ..() - var/mob/living/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] - if(!return_target) - finish_action(controller, FALSE) - if(in_range(controller.pawn, return_target)) - deliver_item(controller) - finish_action(controller, TRUE) - -/datum/ai_behavior/deliver_item/finish_action(datum/ai_controller/controller, success) - . = ..() - controller.blackboard[BB_FETCH_TARGET] = null - controller.blackboard[BB_FETCH_DELIVER_TO] = null - -/// Actually drop the fetched item to the target -/datum/ai_behavior/deliver_item/proc/deliver_item(datum/ai_controller/controller) - var/obj/item/carried_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] - var/atom/movable/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] - if(!carried_item || !return_target) - finish_action(controller, FALSE) - return - - if(ismob(return_target)) - controller.pawn.visible_message(span_notice("[controller.pawn] delivers [carried_item] at [return_target]'s feet.")) - else // not sure how to best phrase this - controller.pawn.visible_message(span_notice("[controller.pawn] delivers [carried_item] to [return_target].")) - - carried_item.forceMove(get_turf(return_target)) - controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null - return TRUE - -/// This behavior involves either eating a snack we can reach, or begging someone holding a snack -/datum/ai_behavior/eat_snack - behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT - -/datum/ai_behavior/eat_snack/perform(delta_time, datum/ai_controller/controller) - . = ..() - var/obj/item/snack = controller.current_movement_target - if(!istype(snack) || !IS_EDIBLE(snack) || !(isturf(snack.loc) || ishuman(snack.loc))) - finish_action(controller, FALSE) - - var/mob/living/living_pawn = controller.pawn - if(!in_range(living_pawn, snack)) - return - - if(isturf(snack.loc)) - snack.attack_animal(living_pawn) // snack attack! - else if(iscarbon(snack.loc) && DT_PROB(10, delta_time)) - living_pawn.manual_emote("stares at [snack.loc]'s [snack.name] with a sad puppy-face.") - - if(QDELETED(snack)) // we ate it! - finish_action(controller, TRUE) - - -/// This behavior involves either eating a snack we can reach, or begging someone holding a snack -/datum/ai_behavior/play_dead - behavior_flags = NONE - -/datum/ai_behavior/play_dead/perform(delta_time, datum/ai_controller/controller) - . = ..() - var/mob/living/basic/simple_pawn = controller.pawn - if(!istype(simple_pawn)) - return - - if(!controller.blackboard[BB_DOG_PLAYING_DEAD]) - controller.blackboard[BB_DOG_PLAYING_DEAD] = TRUE - simple_pawn.emote("deathgasp", intentional=FALSE) - simple_pawn.icon_state = simple_pawn.icon_dead - if(simple_pawn.basic_mob_flags & FLIP_ON_DEATH) - simple_pawn.transform = simple_pawn.transform.Turn(180) - simple_pawn.set_density(FALSE) - - if(DT_PROB(10, delta_time)) - finish_action(controller, TRUE) - -/datum/ai_behavior/play_dead/finish_action(datum/ai_controller/controller, succeeded) - . = ..() - var/mob/living/basic/simple_pawn = controller.pawn - if(!istype(simple_pawn) || simple_pawn.stat) // imagine actually dying while playing dead. hell, imagine being the kid waiting for your pup to get back up :( - return - controller.blackboard[BB_DOG_PLAYING_DEAD] = FALSE - simple_pawn.visible_message(span_notice("[simple_pawn] springs to [simple_pawn.p_their()] feet, panting excitedly!")) - simple_pawn.icon_state = simple_pawn.icon_living - if(simple_pawn.basic_mob_flags & FLIP_ON_DEATH) - simple_pawn.transform = simple_pawn.transform.Turn(180) - simple_pawn.set_density(initial(simple_pawn.density)) - -/// This behavior involves either eating a snack we can reach, or begging someone holding a snack -/datum/ai_behavior/harass +/** + * Pursue the target, growl if we're close, and bite if we're adjacent + * Dogs are actually not very aggressive and won't attack unless you approach them + * Adds a floor to the melee damage of the dog, as most pet dogs don't actually have any melee strength + */ +/datum/ai_behavior/basic_melee_attack/dog + action_cooldown = 0.8 SECONDS behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM required_distance = 3 -/datum/ai_behavior/harass/perform(delta_time, datum/ai_controller/controller) - . = ..() +/datum/ai_behavior/basic_melee_attack/dog/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + controller.behavior_cooldowns[src] = world.time + action_cooldown var/mob/living/living_pawn = controller.pawn - if(!istype(living_pawn) || !(isturf(living_pawn.loc) || HAS_TRAIT(living_pawn, TRAIT_AI_BAGATTACK))) + if(!(isturf(living_pawn.loc) || HAS_TRAIT(living_pawn, TRAIT_AI_BAGATTACK))) // Void puppies can attack from inside bags + finish_action(controller, FALSE, target_key, targetting_datum_key, hiding_location_key) return - var/datum/weakref/harass_ref = controller.blackboard[BB_DOG_HARASS_TARGET] - var/atom/movable/harass_target = harass_ref.resolve() - if(!harass_target || !can_see(living_pawn, harass_target, length=AI_DOG_VISION_RANGE)) - finish_action(controller, FALSE) + // Unfortunately going to repeat this check in parent call but what can you do + var/datum/weakref/weak_target = controller.blackboard[target_key] + var/atom/target = weak_target?.resolve() + var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key] + if (!targetting_datum.can_attack(living_pawn, target)) + finish_action(controller, FALSE, target_key, targetting_datum_key, hiding_location_key) return - if(controller.blackboard[BB_DOG_FRIENDS][harass_ref]) - living_pawn.visible_message(span_danger("[living_pawn] looks sideways at [harass_target] for a moment, then shakes [living_pawn.p_their()] head and ceases aggression.")) - finish_action(controller, FALSE) + if (!in_range(living_pawn, target)) + growl_at(living_pawn, target, delta_time) return - var/mob/living/living_target = harass_target - if(istype(living_target) && (living_target.stat || HAS_TRAIT(living_target, TRAIT_FAKEDEATH))) - finish_action(controller, TRUE) + if(!controller.blackboard[BB_DOG_HARASS_HARM]) + paw_harmlessly(living_pawn, target, delta_time) return - if(!controller.blackboard[BB_DOG_HARASS_FRUSTRATION]) - controller.blackboard[BB_DOG_HARASS_FRUSTRATION] = world.time - else if(controller.blackboard[BB_DOG_HARASS_FRUSTRATION] + AI_DOG_HARASS_FRUSTRATE_TIME < world.time) // if we haven't actually bit them in a while, give up - living_pawn.visible_message(span_danger("[living_pawn] yawns and seems to lose interest in harassing [harass_target].")) - finish_action(controller, FALSE) + // Give Ian some teeth + var/old_melee_lower = living_pawn.melee_damage_lower + var/old_melee_upper = living_pawn.melee_damage_upper + living_pawn.melee_damage_lower = max(5, old_melee_lower) + living_pawn.melee_damage_upper = max(10, old_melee_upper) + + . = ..() // Bite time + + living_pawn.melee_damage_lower = old_melee_lower + living_pawn.melee_damage_upper = old_melee_upper + +/// Swat at someone we don't like but won't hurt +/datum/ai_behavior/basic_melee_attack/dog/proc/paw_harmlessly(mob/living/living_pawn, atom/target, delta_time) + if(!DT_PROB(20, delta_time)) return + living_pawn.do_attack_animation(target, ATTACK_EFFECT_DISARM) + playsound(target, 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1) + target.visible_message(span_danger("[living_pawn] paws ineffectually at [target]!"), span_danger("[living_pawn] paws ineffectually at you!")) - // subtypes of this behavior can change behavior for how eager/averse the pawn is to attack the target as opposed to falling back/making noise/getting help - if(in_range(living_pawn, living_target)) - attack(controller, living_target) - else if(DT_PROB(15, delta_time)) - living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [harass_target]!") - if(DT_PROB(40, delta_time)) - playsound(living_pawn, pick('sound/creatures/dog/growl1.ogg', 'sound/creatures/dog/growl2.ogg'), 50, TRUE, -1) - -/datum/ai_behavior/harass/finish_action(datum/ai_controller/controller, succeeded) - . = ..() - controller.blackboard[BB_DOG_HARASS_TARGET] = null - controller.blackboard[BB_DOG_HARASS_FRUSTRATION] = null - -/// A proc representing when the mob is pushed to actually attack the target. Again, subtypes can be used to represent different attacks from different animals, or it can be some other generic behavior -/datum/ai_behavior/harass/proc/attack(datum/ai_controller/controller, mob/living/living_target) - var/mob/living/living_pawn = controller.pawn - if(!istype(living_pawn)) +/// Let them know we mean business +/datum/ai_behavior/basic_melee_attack/dog/proc/growl_at(mob/living/living_pawn, atom/target, delta_time) + if(!DT_PROB(15, delta_time)) return - - controller.blackboard[BB_DOG_HARASS_FRUSTRATION] = world.time - - if(controller.blackboard[BB_DOG_HARASS_HARM]) - // make sure the pawn gets some temporary strength boost to actually attack the target instead of pathetically nuzzling them. - var/old_melee_lower = living_pawn.melee_damage_lower - var/old_melee_upper = living_pawn.melee_damage_upper - living_pawn.melee_damage_lower = max(5, old_melee_lower) - living_pawn.melee_damage_upper = max(10, old_melee_upper) - - living_pawn.UnarmedAttack(living_target, FALSE) - - living_pawn.melee_damage_lower = old_melee_lower - living_pawn.melee_damage_upper = old_melee_upper - else - if(prob(20)) - living_pawn.do_attack_animation(living_target, ATTACK_EFFECT_DISARM) - playsound(living_target, 'sound/weapons/thudswoosh.ogg', 50, TRUE, -1) - living_target.visible_message(span_danger("[living_pawn] paws ineffectually at [living_target]!"), span_danger("[living_pawn] paws ineffectually at you!")) + living_pawn.manual_emote("[pick("barks", "growls", "stares")] menacingly at [target]!") + if(!DT_PROB(40, delta_time)) + return + playsound(living_pawn, pick('sound/creatures/dog/growl1.ogg', 'sound/creatures/dog/growl2.ogg'), 50, TRUE, -1) diff --git a/code/datums/ai/dog/dog_controller.dm b/code/datums/ai/dog/dog_controller.dm index c06902426ad..5a42cb43a1e 100644 --- a/code/datums/ai/dog/dog_controller.dm +++ b/code/datums/ai/dog/dog_controller.dm @@ -1,320 +1,37 @@ -/datum/ai_controller/dog +/datum/ai_controller/basic_controller/dog blackboard = list( - BB_SIMPLE_CARRY_ITEM = null, - BB_FETCH_TARGET = null, - BB_FETCH_DELIVER_TO = null, - BB_DOG_FRIENDS = list(), - BB_FETCH_IGNORE_LIST = list(), - BB_DOG_ORDER_MODE = DOG_COMMAND_NONE, - BB_DOG_PLAYING_DEAD = FALSE, - BB_DOG_HARASS_TARGET = null, - BB_DOG_HARASS_FRUSTRATION = null, BB_DOG_HARASS_HARM = TRUE, BB_VISION_RANGE = AI_DOG_VISION_RANGE, + BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(), ) - ai_movement = /datum/ai_movement/jps + ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_dog planning_subtrees = list( /datum/ai_planning_subtree/random_speech/dog, - /datum/ai_planning_subtree/dog, + /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/dog_harassment, ) - COOLDOWN_DECLARE(heel_cooldown) - COOLDOWN_DECLARE(command_cooldown) - - -/datum/ai_controller/dog/process(delta_time) - if(ismob(pawn)) - var/mob/living/living_pawn = pawn - movement_delay = living_pawn.cached_multiplicative_slowdown - return ..() - -/datum/ai_controller/dog/TryPossessPawn(atom/new_pawn) - if(!isliving(new_pawn)) - return AI_CONTROLLER_INCOMPATIBLE - - RegisterSignal(new_pawn, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_attack_hand)) - RegisterSignal(new_pawn, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined)) - RegisterSignal(new_pawn, COMSIG_CLICK_ALT, PROC_REF(check_altclicked)) - RegisterSignals(new_pawn, list(COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING), PROC_REF(on_death)) - RegisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING, PROC_REF(listened_throw)) - return ..() //Run parent at end - -/datum/ai_controller/dog/UnpossessPawn(destroy) - var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] - if(carried_item) - pawn.visible_message(span_danger("[pawn] drops [carried_item]")) - carried_item.forceMove(pawn.drop_location()) - blackboard[BB_SIMPLE_CARRY_ITEM] = null - UnregisterSignal(pawn, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_PARENT_EXAMINE, COMSIG_CLICK_ALT, COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING)) - UnregisterSignal(SSdcs, COMSIG_GLOB_CARBON_THROW_THING) - return ..() //Run parent at end - -/datum/ai_controller/dog/able_to_run() - var/mob/living/living_pawn = pawn - - if(IS_DEAD_OR_INCAP(living_pawn)) - return FALSE - return ..() - -/// Someone has thrown something, see if it's someone we care about and start listening to the thrown item so we can see if we want to fetch it when it lands -/datum/ai_controller/dog/proc/listened_throw(datum/source, mob/living/carbon/carbon_thrower) - SIGNAL_HANDLER - if(blackboard[BB_FETCH_TARGET] || blackboard[BB_FETCH_DELIVER_TO] || blackboard[BB_DOG_PLAYING_DEAD]) // we're already busy - return - if(!COOLDOWN_FINISHED(src, heel_cooldown)) - return - if(!can_see(pawn, carbon_thrower, length=AI_DOG_VISION_RANGE)) - return - var/obj/item/thrown_thing = carbon_thrower.get_active_held_item() - if(!isitem(thrown_thing)) - return - if(blackboard[BB_FETCH_IGNORE_LIST][WEAKREF(thrown_thing)]) - return - - RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(listen_throw_land)) - -/// A throw we were listening to has finished, see if it's in range for us to try grabbing it -/datum/ai_controller/dog/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum) - SIGNAL_HANDLER - - UnregisterSignal(thrown_thing, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_THROW_LANDED)) - if(!istype(thrown_thing) || !isturf(thrown_thing.loc) || !can_see(pawn, thrown_thing, length=AI_DOG_VISION_RANGE)) - return - - var/mob/living/living_pawn = pawn - if(IS_DEAD_OR_INCAP(living_pawn)) - return - set_movement_target(thrown_thing) - blackboard[BB_FETCH_TARGET] = thrown_thing - blackboard[BB_FETCH_DELIVER_TO] = throwing_datum.thrower - if(living_pawn.buckled) - queue_behavior(/datum/ai_behavior/resist) - queue_behavior(/datum/ai_behavior/fetch) - -/// Someone's interacting with us by hand, see if they're being nice or mean -/datum/ai_controller/dog/proc/on_attack_hand(datum/source, mob/living/user) - SIGNAL_HANDLER - - var/mob/living/living_pawn = pawn - var/additional_text = HAS_TRAIT(user, TRAIT_NAIVE) ? "It looks like [living_pawn.p_theyre()] sleeping." : "[living_pawn.p_they(capitalized = TRUE)] seem[living_pawn.p_s()] to be dead." - if(living_pawn.stat == DEAD) - to_chat(user, span_warning("Touching [living_pawn], you feel [living_pawn.p_their()] cold skin through the fur. [additional_text]")) - return - if(user.combat_mode) - unfriend(user) - else - if(prob(AI_DOG_PET_FRIEND_PROB)) - befriend(user) - // if the dog has something in their mouth that they're not bringing to someone for whatever reason, have them drop it when pet by a friend - var/list/friends = blackboard[BB_DOG_FRIENDS] - if(blackboard[BB_SIMPLE_CARRY_ITEM] && !current_movement_target && friends[WEAKREF(user)]) - var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] - pawn.visible_message(span_danger("[pawn] drops [carried_item] at [user]'s feet!")) - // maybe have a dedicated proc for dropping things - carried_item.forceMove(get_turf(user)) - blackboard[BB_SIMPLE_CARRY_ITEM] = null - -/// Someone is being nice to us, let's make them a friend! -/datum/ai_controller/dog/proc/befriend(mob/living/new_friend) - var/list/friends = blackboard[BB_DOG_FRIENDS] - var/datum/weakref/friend_ref = WEAKREF(new_friend) - if(friends[friend_ref]) - return - if(in_range(pawn, new_friend)) - new_friend.visible_message("[pawn] licks at [new_friend] in a friendly manner!", span_notice("[pawn] licks at you in a friendly manner!")) - friends[friend_ref] = TRUE - RegisterSignal(new_friend, COMSIG_MOB_POINTED, PROC_REF(check_point)) - RegisterSignal(new_friend, COMSIG_MOB_SAY, PROC_REF(check_verbal_command)) - -/// Someone is being mean to us, take them off our friends (add actual enemies behavior later) -/datum/ai_controller/dog/proc/unfriend(mob/living/ex_friend) - var/list/friends = blackboard[BB_DOG_FRIENDS] - friends -= WEAKREF(ex_friend) - UnregisterSignal(ex_friend, list(COMSIG_MOB_POINTED, COMSIG_MOB_SAY)) - -/// Someone is looking at us, if we're currently carrying something then show what it is, and include a message if they're our friend -/datum/ai_controller/dog/proc/on_examined(datum/source, mob/user, list/examine_text) - SIGNAL_HANDLER - - var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] - if(carried_item) - examine_text += span_notice("[pawn.p_they(TRUE)] [pawn.p_are()] carrying [carried_item.get_examine_string(user)] in [pawn.p_their()] mouth.") - if(blackboard[BB_DOG_FRIENDS][WEAKREF(user)]) - var/mob/living/living_pawn = pawn - if(!IS_DEAD_OR_INCAP(living_pawn)) - examine_text += span_notice("[pawn.p_they(TRUE)] seem[pawn.p_s()] happy to see you!") - -/// If we died, drop anything we were carrying -/datum/ai_controller/dog/proc/on_death(mob/living/ol_yeller) - SIGNAL_HANDLER - - var/obj/item/carried_item = blackboard[BB_SIMPLE_CARRY_ITEM] - if(!carried_item) - return - - ol_yeller.visible_message(span_danger("[ol_yeller] drops [carried_item] as [ol_yeller.p_they()] die[ol_yeller.p_s()].")) - carried_item.forceMove(ol_yeller.drop_location()) - blackboard[BB_SIMPLE_CARRY_ITEM] = null - -// next section is regarding commands - -/// Someone alt clicked us, see if they're someone we should show the radial command menu to -/datum/ai_controller/dog/proc/check_altclicked(datum/source, mob/living/clicker) - SIGNAL_HANDLER - - if(!COOLDOWN_FINISHED(src, command_cooldown)) - return - if(!istype(clicker) || !blackboard[BB_DOG_FRIENDS][WEAKREF(clicker)]) - return - . = COMPONENT_CANCEL_CLICK_ALT - INVOKE_ASYNC(src, PROC_REF(command_radial), clicker) - -/// Show the command radial menu -/datum/ai_controller/dog/proc/command_radial(mob/living/clicker) - var/list/commands = list( - COMMAND_HEEL = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow"), - COMMAND_FETCH = image(icon = 'icons/mob/actions/actions_spells.dmi', icon_state = "summons"), - COMMAND_ATTACK = image(icon = 'icons/effects/effects.dmi', icon_state = "bite"), - COMMAND_DIE = image(icon = 'icons/mob/simple/pets.dmi', icon_state = "puppy_dead") - ) - - var/choice = show_radial_menu(clicker, pawn, commands, custom_check = CALLBACK(src, PROC_REF(check_menu), clicker), tooltips = TRUE) - if(!choice || !check_menu(clicker)) - return - set_command_mode(clicker, choice) - -/datum/ai_controller/dog/proc/check_menu(mob/user) - if(!istype(user)) - CRASH("A non-mob is trying to issue an order to [pawn].") - if(user.incapacitated() || !can_see(user, pawn)) - return FALSE - return TRUE - -/// One of our friends said something, see if it's a valid command, and if so, take action -/datum/ai_controller/dog/proc/check_verbal_command(mob/speaker, speech_args) - SIGNAL_HANDLER - - if(!blackboard[BB_DOG_FRIENDS][WEAKREF(speaker)]) - return - - if(!COOLDOWN_FINISHED(src, command_cooldown)) - return - - var/mob/living/living_pawn = pawn - if(IS_DEAD_OR_INCAP(living_pawn)) - return - - var/spoken_text = speech_args[SPEECH_MESSAGE] // probably should check for full words - var/command - if(findtext(spoken_text, "heel") || findtext(spoken_text, "sit") || findtext(spoken_text, "stay")) - command = COMMAND_HEEL - else if(findtext(spoken_text, "fetch")) - command = COMMAND_FETCH - else if(findtext(spoken_text, "attack") || findtext(spoken_text, "sic")) - command = COMMAND_ATTACK - else if(findtext(spoken_text, "play dead")) - command = COMMAND_DIE - else - return - - if(!can_see(pawn, speaker, length=AI_DOG_VISION_RANGE)) - return - set_command_mode(speaker, command) - -/// Whether we got here via radial menu or a verbal command, this is where we actually process what our new command will be -/datum/ai_controller/dog/proc/set_command_mode(mob/commander, command) - COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN) - - switch(command) - // heel: stop what you're doing, relax and try not to do anything for a little bit - if(COMMAND_HEEL) - pawn.visible_message(span_notice("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] sit[pawn.p_s()] down obediently, awaiting further orders.")) - blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE - COOLDOWN_START(src, heel_cooldown, AI_DOG_HEEL_DURATION) - CancelActions() - // fetch: whatever the commander points to, try and bring it back - if(COMMAND_FETCH) - pawn.visible_message(span_notice("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] bounce[pawn.p_s()] slightly in anticipation.")) - blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_FETCH - // attack: harass whoever the commander points to - if(COMMAND_ATTACK) - pawn.visible_message(span_danger("[pawn]'s ears prick up at [commander]'s command, and [pawn.p_they()] growl[pawn.p_s()] intensely.")) // imagine getting intimidated by a corgi - blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_ATTACK - if(COMMAND_DIE) - blackboard[BB_DOG_ORDER_MODE] = DOG_COMMAND_NONE - CancelActions() - queue_behavior(/datum/ai_behavior/play_dead) - -/// Someone we like is pointing at something, see if it's something we might want to interact with (like if they might want us to fetch something for them) -/datum/ai_controller/dog/proc/check_point(mob/pointing_friend, atom/movable/pointed_movable) - SIGNAL_HANDLER - - var/mob/living/living_pawn = pawn - if(IS_DEAD_OR_INCAP(living_pawn)) - return - - if(!COOLDOWN_FINISHED(src, command_cooldown)) - return - if(pointed_movable == pawn || blackboard[BB_FETCH_TARGET] || !istype(pointed_movable) || blackboard[BB_DOG_ORDER_MODE] == DOG_COMMAND_NONE) // busy or no command - return - if(!can_see(pawn, pointing_friend, length=AI_DOG_VISION_RANGE) || !can_see(pawn, pointed_movable, length=AI_DOG_VISION_RANGE)) - return - - COOLDOWN_START(src, command_cooldown, AI_DOG_COMMAND_COOLDOWN) - - switch(blackboard[BB_DOG_ORDER_MODE]) - if(DOG_COMMAND_FETCH) - if(!isitem(pointed_movable) || pointed_movable.anchored) - return - var/obj/item/pointed_item = pointed_movable - if(pointed_item.obj_flags & ABSTRACT) - return - pawn.visible_message(span_notice("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and barks excitedly!")) - set_movement_target(pointed_movable) - blackboard[BB_FETCH_TARGET] = pointed_movable - blackboard[BB_FETCH_DELIVER_TO] = pointing_friend - if(living_pawn.buckled) - queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something - queue_behavior(/datum/ai_behavior/fetch) - if(DOG_COMMAND_ATTACK) - pawn.visible_message(span_notice("[pawn] follows [pointing_friend]'s gesture towards [pointed_movable] and growls intensely!")) - set_movement_target(pointed_movable) - blackboard[BB_DOG_HARASS_TARGET] = WEAKREF(pointed_movable) - blackboard[BB_DOG_HARASS_HARM] = TRUE - if(living_pawn.buckled) - queue_behavior(/datum/ai_behavior/resist)//in case they are in bed or something - queue_behavior(/datum/ai_behavior/harass) - - /** * Same thing but with make tiny corgis and use access cards. */ -/datum/ai_controller/dog/corgi +/datum/ai_controller/basic_controller/dog/corgi blackboard = list( - BB_SIMPLE_CARRY_ITEM = null, - BB_FETCH_TARGET = null, - BB_FETCH_DELIVER_TO = null, - BB_DOG_FRIENDS = list(), - BB_FETCH_IGNORE_LIST = list(), - BB_DOG_ORDER_MODE = DOG_COMMAND_NONE, - BB_DOG_PLAYING_DEAD = FALSE, - BB_DOG_HARASS_TARGET = null, - BB_DOG_HARASS_FRUSTRATION = null, BB_DOG_HARASS_HARM = TRUE, BB_VISION_RANGE = AI_DOG_VISION_RANGE, - + BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(), BB_BABIES_PARTNER_TYPES = list(/mob/living/basic/pet/dog), BB_BABIES_CHILD_TYPES = list(/mob/living/basic/pet/dog/corgi/puppy = 95, /mob/living/basic/pet/dog/corgi/puppy/void = 5), ) planning_subtrees = list( /datum/ai_planning_subtree/random_speech/dog, - /datum/ai_planning_subtree/make_babies, - /datum/ai_planning_subtree/dog, + /datum/ai_planning_subtree/make_babies, // Ian WILL prioritise sex over following your instructions + /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/dog_harassment, ) -/datum/ai_controller/dog/corgi/get_access() +/datum/ai_controller/basic_controller/dog/corgi/get_access() var/mob/living/basic/pet/dog/corgi/corgi_pawn = pawn if(!istype(corgi_pawn)) return diff --git a/code/datums/ai/dog/dog_subtrees.dm b/code/datums/ai/dog/dog_subtrees.dm index ac1eca95d95..b655887941c 100644 --- a/code/datums/ai/dog/dog_subtrees.dm +++ b/code/datums/ai/dog/dog_subtrees.dm @@ -1,52 +1,37 @@ -/datum/ai_planning_subtree/dog - COOLDOWN_DECLARE(heel_cooldown) - COOLDOWN_DECLARE(reset_ignore_cooldown) +/// Find someone we don't like and annoy them +/datum/ai_planning_subtree/dog_harassment -/datum/ai_planning_subtree/dog/SelectBehaviors(datum/ai_controller/dog/controller, delta_time) - var/mob/living/living_pawn = controller.pawn - - // occasionally reset our ignore list - if(COOLDOWN_FINISHED(src, reset_ignore_cooldown) && length(controller.blackboard[BB_FETCH_IGNORE_LIST])) - COOLDOWN_START(src, reset_ignore_cooldown, AI_FETCH_IGNORE_DURATION) - controller.blackboard[BB_FETCH_IGNORE_LIST] = list() - - // if we were just ordered to heel, chill out for a bit - if(!COOLDOWN_FINISHED(src, heel_cooldown)) +/datum/ai_planning_subtree/dog_harassment/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(!DT_PROB(10, delta_time)) + return + controller.queue_behavior(/datum/ai_behavior/find_hated_dog_target, BB_DOG_HARASS_TARGET, BB_PET_TARGETTING_DATUM) + var/datum/weakref/weak_target = controller.blackboard[BB_DOG_HARASS_TARGET] + var/atom/harass_target = weak_target?.resolve() + if (isnull(harass_target)) return - // if we're not already carrying something and we have a fetch target (and we're not already doing something with it), see if we can eat/equip it - if(!controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_TARGET]) - var/atom/movable/interact_target = controller.blackboard[BB_FETCH_TARGET] - if(in_range(living_pawn, interact_target) && (isturf(interact_target.loc))) - controller.set_movement_target(type, interact_target) - if(IS_EDIBLE(interact_target)) - controller.queue_behavior(/datum/ai_behavior/eat_snack) - else if(isitem(interact_target)) - controller.queue_behavior(/datum/ai_behavior/simple_equip) - else - controller.blackboard[BB_FETCH_TARGET] = null - controller.blackboard[BB_FETCH_DELIVER_TO] = null - return + controller.queue_behavior(/datum/ai_behavior/basic_melee_attack/dog, BB_DOG_HARASS_TARGET, BB_PET_TARGETTING_DATUM) + return SUBTREE_RETURN_FINISH_PLANNING - // if we're carrying something and we have a destination to deliver it, do that - if(controller.blackboard[BB_SIMPLE_CARRY_ITEM] && controller.blackboard[BB_FETCH_DELIVER_TO]) - var/atom/return_target = controller.blackboard[BB_FETCH_DELIVER_TO] - if(!can_see(controller.pawn, return_target, length=AI_DOG_VISION_RANGE)) - // if the return target isn't in sight, we'll just forget about it and carry the thing around - controller.blackboard[BB_FETCH_DELIVER_TO] = null - return - controller.set_movement_target(type, return_target) - controller.queue_behavior(/datum/ai_behavior/deliver_item) - return +/datum/ai_behavior/find_hated_dog_target - if(DT_PROB(10, delta_time)) - for(var/mob/living/iter_living in oview(2, living_pawn)) - if(iter_living.stat != CONSCIOUS || !HAS_TRAIT(iter_living, TRAIT_HATED_BY_DOGS)) - continue +/datum/ai_behavior/find_hated_dog_target/setup(datum/ai_controller/controller, target_key, targetting_datum_key) + . = ..() + var/mob/living/dog = controller.pawn + var/datum/targetting_datum/targetting_datum = controller.blackboard[targetting_datum_key] + for(var/mob/living/iter_living in oview(2, dog)) + if(iter_living.stat != CONSCIOUS || !HAS_TRAIT(iter_living, TRAIT_HATED_BY_DOGS)) + continue + if(!targetting_datum.can_attack(dog, iter_living)) + continue - living_pawn.audible_message(span_warning("[living_pawn] growls at [iter_living], seemingly annoyed by [iter_living.p_their()] presence."), hearing_distance = COMBAT_MESSAGE_RANGE) - controller.set_movement_target(iter_living) - controller.blackboard[BB_DOG_HARASS_TARGET] = WEAKREF(iter_living) - controller.blackboard[BB_DOG_HARASS_HARM] = FALSE - controller.queue_behavior(/datum/ai_behavior/harass) - return + dog.audible_message(span_warning("[dog] growls at [iter_living], seemingly annoyed by [iter_living.p_their()] presence."), hearing_distance = COMBAT_MESSAGE_RANGE) + controller.blackboard[target_key] = WEAKREF(iter_living) + controller.blackboard[BB_DOG_HARASS_HARM] = FALSE + return TRUE + + controller.blackboard[target_key] = null + +/datum/ai_behavior/find_hated_dog_target/perform(delta_time, datum/ai_controller/controller, target_key) + . = ..() + finish_action(controller, TRUE) diff --git a/code/datums/ai/idle_behaviors/idle_dog.dm b/code/datums/ai/idle_behaviors/idle_dog.dm index 96396aa8af9..a97b16967c5 100644 --- a/code/datums/ai/idle_behaviors/idle_dog.dm +++ b/code/datums/ai/idle_behaviors/idle_dog.dm @@ -1,16 +1,13 @@ ///Dog specific idle behavior. -/datum/idle_behavior/idle_dog/perform_idle_behavior(delta_time, datum/ai_controller/dog/controller) +/datum/idle_behavior/idle_dog/perform_idle_behavior(delta_time, datum/ai_controller/basic_controller/dog/controller) var/mob/living/living_pawn = controller.pawn if(!isturf(living_pawn.loc) || living_pawn.pulledby) return - // if we were just ordered to heel, chill out for a bit - if(!COOLDOWN_FINISHED(controller, heel_cooldown)) - return - + var/datum/weakref/weak_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + var/obj/item/carry_item = weak_item?.resolve() // if we're just ditzing around carrying something, occasionally print a message so people know we have something - if(controller.blackboard[BB_SIMPLE_CARRY_ITEM] && DT_PROB(5, delta_time)) - var/obj/item/carry_item = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + if(carry_item && DT_PROB(5, delta_time)) living_pawn.visible_message(span_notice("[living_pawn] gently teethes on \the [carry_item] in [living_pawn.p_their()] mouth."), vision_distance=COMBAT_MESSAGE_RANGE) // Custom movement rate, for old corgis, etc. diff --git a/code/datums/components/food/edible.dm b/code/datums/components/food/edible.dm index bc929f6145d..75ef4ee7b38 100644 --- a/code/datums/components/food/edible.dm +++ b/code/datums/components/food/edible.dm @@ -74,7 +74,7 @@ Behavior that's still missing from this component that original food items had t /datum/component/edible/RegisterWithParent() RegisterSignal(parent, COMSIG_PARENT_EXAMINE, PROC_REF(examine)) - RegisterSignal(parent, COMSIG_ATOM_ATTACK_ANIMAL, PROC_REF(UseByAnimal)) + RegisterSignals(parent, COMSIG_ATOM_ATTACK_ANIMAL, PROC_REF(UseByAnimal)) RegisterSignal(parent, COMSIG_ATOM_CHECKPARTS, PROC_REF(OnCraft)) RegisterSignal(parent, COMSIG_ATOM_CREATEDBY_PROCESSING, PROC_REF(OnProcessed)) RegisterSignal(parent, COMSIG_FOOD_INGREDIENT_ADDED, PROC_REF(edible_ingredient_added)) diff --git a/code/datums/components/pet_commands/fetch.dm b/code/datums/components/pet_commands/fetch.dm new file mode 100644 index 00000000000..8898597de2d --- /dev/null +++ b/code/datums/components/pet_commands/fetch.dm @@ -0,0 +1,116 @@ +/** + * # Pet Command: Fetch + * Watch for someone throwing or pointing at something and then go get it and bring it back. + * If it's food we might eat it instead. + */ +/datum/pet_command/point_targetting/fetch + command_name = "Fetch" + command_desc = "Command your pet to retrieve something you throw or point at." + radial_icon = 'icons/mob/actions/actions_spells.dmi' + radial_icon_state = "summons" + speech_commands = list("fetch") + command_feedback = "bounces" + pointed_reaction = "with great interest" + /// If true, this command will trigger if the pet sees a friend throw any item, if they're not doing anything else + var/trigger_on_throw = TRUE + /// If true, this is a poorly trained pet who will eat food you throw instead of bringing it back + var/will_eat_targets = TRUE + +/datum/pet_command/point_targetting/fetch/New(mob/living/parent) + . = ..() + parent.AddElement(/datum/element/ai_held_item) // We don't remove this on destroy because they might still be holding something + +/datum/pet_command/point_targetting/fetch/add_new_friend(mob/living/tamer) + . = ..() + RegisterSignal(tamer, COMSIG_MOB_THROW, PROC_REF(listened_throw)) + +/datum/pet_command/point_targetting/fetch/remove_friend(mob/living/unfriended) + . = ..() + UnregisterSignal(unfriended, COMSIG_MOB_THROW) + +/// A friend has thrown something, if we're listening or at least not busy then go get it +/datum/pet_command/point_targetting/fetch/proc/listened_throw(mob/living/carbon/thrower) + SIGNAL_HANDLER + + var/mob/living/parent = weak_parent.resolve() + if (!parent) + return + + var/list/blackboard = parent.ai_controller.blackboard + if (!trigger_on_throw && !blackboard[BB_ACTIVE_PET_COMMAND]) + return // We don't have a command and we only listen to commands + if (blackboard[BB_ACTIVE_PET_COMMAND] && blackboard[BB_ACTIVE_PET_COMMAND] != WEAKREF(src)) + return // We have a command and it's not this one + if (blackboard[BB_CURRENT_PET_TARGET] || blackboard[BB_FETCH_DELIVER_TO]) + return // We're already very fetching + if (!can_see(parent, thrower, length = sense_radius)) + return // Can't see it + + var/obj/item/thrown_thing = thrower.get_active_held_item() + if (!isitem(thrown_thing)) + return + if (blackboard[BB_FETCH_IGNORE_LIST] && blackboard[BB_FETCH_IGNORE_LIST][WEAKREF(thrown_thing)]) + return // We're ignoring it already + + RegisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(listen_throw_land)) + +/// A throw we were listening to has finished, see if it's in range for us to try grabbing it +/datum/pet_command/point_targetting/fetch/proc/listen_throw_land(obj/item/thrown_thing, datum/thrownthing/throwing_datum) + SIGNAL_HANDLER + + UnregisterSignal(thrown_thing, COMSIG_MOVABLE_THROW_LANDED) + var/mob/living/parent = weak_parent.resolve() + if (!parent) + return + if (!isturf(thrown_thing.loc)) + return + if (!can_see(parent, thrown_thing, length = sense_radius)) + return + + try_activate_command(throwing_datum.thrower) + set_command_target(parent, thrown_thing) + parent.ai_controller.blackboard[BB_FETCH_DELIVER_TO] = WEAKREF(throwing_datum.thrower) + +// Don't try and fetch turfs or anchored objects if someone points at them +/datum/pet_command/point_targetting/fetch/look_for_target(mob/living/pointing_friend, obj/item/pointed_atom) + if (!istype(pointed_atom)) + return FALSE + if (pointed_atom.anchored) + return FALSE + . = ..() + if (!.) + return FALSE + + var/mob/living/parent = weak_parent.resolve() + parent.ai_controller.blackboard[BB_FETCH_DELIVER_TO] = WEAKREF(pointing_friend) + +// Finally, plan our actions +/datum/pet_command/point_targetting/fetch/execute_action(datum/ai_controller/controller) + controller.queue_behavior(/datum/ai_behavior/forget_failed_fetches) + + var/datum/weakref/weak_target = controller.blackboard[BB_CURRENT_PET_TARGET] + var/atom/target = weak_target?.resolve() + // We got something to fetch so go fetch it + if (target) + if (get_dist(controller.pawn, target) > 1) // We're not there yet + controller.queue_behavior(/datum/ai_behavior/fetch_seek, BB_CURRENT_PET_TARGET, BB_FETCH_DELIVER_TO) + return SUBTREE_RETURN_FINISH_PLANNING + // If mobs could attack food you would branch here to call `eat_fetched_snack`, however that's a task for the future + controller.queue_behavior(/datum/ai_behavior/pick_up_item, BB_CURRENT_PET_TARGET, BB_SIMPLE_CARRY_ITEM) + return SUBTREE_RETURN_FINISH_PLANNING + + var/datum/weakref/carried_ref = controller.blackboard[BB_SIMPLE_CARRY_ITEM] + var/obj/item/carried_item = carried_ref?.resolve() + if (!carried_item) + return + + var/datum/weakref/delivery_ref = controller.blackboard[BB_FETCH_DELIVER_TO] + var/atom/delivery_target = delivery_ref?.resolve() + if (!delivery_target || !can_see(controller.pawn, delivery_target, sense_radius)) + // We don't know where to return this to so we're just going to keep it + controller.blackboard[BB_ACTIVE_PET_COMMAND] = null + return + + // We got something to deliver and someone to deliver it to + controller.queue_behavior(/datum/ai_behavior/deliver_fetched_item, BB_FETCH_DELIVER_TO, BB_SIMPLE_CARRY_ITEM) + return SUBTREE_RETURN_FINISH_PLANNING diff --git a/code/datums/components/pet_commands/obeys_commands.dm b/code/datums/components/pet_commands/obeys_commands.dm index 8dc297e80dc..3b80bd88eb3 100644 --- a/code/datums/components/pet_commands/obeys_commands.dm +++ b/code/datums/components/pet_commands/obeys_commands.dm @@ -28,11 +28,14 @@ /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)) RegisterSignal(parent, COMSIG_CLICK_ALT, PROC_REF(display_menu)) /datum/component/obeys_commands/UnregisterFromParent() - UnregisterSignal(parent, list(COMSIG_LIVING_BEFRIENDED, COMSIG_CLICK_ALT)) + UnregisterSignal(parent, list(COMSIG_LIVING_BEFRIENDED, COMSIG_LIVING_UNFRIENDED, COMSIG_PARENT_EXAMINE, COMSIG_CLICK_ALT)) +/// Add someone to our friends list /datum/component/obeys_commands/proc/add_friend(datum/source, mob/living/new_friend) SIGNAL_HANDLER @@ -40,6 +43,26 @@ 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 (IS_DEAD_OR_INCAP(source)) + return + if (!source.ai_controller) + return + var/list/friends_list = source.ai_controller.blackboard[BB_FRIENDS_LIST] + if (!friends_list || !friends_list[WEAKREF(user)]) + return + examine_list += span_notice("[source.p_they(capitalized = TRUE)] seem[source.p_s()] happy to see you!") /// Displays a radial menu of commands /datum/component/obeys_commands/proc/display_menu(datum/source, mob/living/clicker) @@ -61,7 +84,10 @@ var/list/radial_options = list() for (var/command_name as anything in available_commands) var/datum/pet_command/command = available_commands[command_name] - radial_options += command.provide_radial_data() + var/datum/radial_menu_choice/choice = command.provide_radial_data() + if (!choice) + continue + radial_options += choice var/pick = show_radial_menu(clicker, clicker, radial_options, require_near = TRUE, tooltips = TRUE) if (!pick) diff --git a/code/datums/components/pet_commands/pet_command.dm b/code/datums/components/pet_commands/pet_command.dm index b732783f1d9..97a71b59470 100644 --- a/code/datums/components/pet_commands/pet_command.dm +++ b/code/datums/components/pet_commands/pet_command.dm @@ -6,18 +6,18 @@ /datum/pet_command /// Weak reference to who follows this command var/datum/weakref/weak_parent - /// Key for command applied when you receive an order - var/command_key = PET_COMMAND_NONE /// 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 /// Icon to display in radial menu var/icon/radial_icon /// Icon state to display in radial menu var/radial_icon_state /// Speech strings to listen out for - var/list/speech_commands + 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 @@ -26,12 +26,15 @@ /datum/pet_command/New(mob/living/parent) . = ..() weak_parent = WEAKREF(parent) - parent.ai_controller.blackboard[command_key] = WEAKREF(src) /// 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)) +/// Stop listening to a guy +/datum/pet_command/proc/remove_friend(mob/living/unfriended) + UnregisterSignal(unfriended, COMSIG_MOB_SAY) + /// 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 @@ -65,7 +68,7 @@ return if (IS_DEAD_OR_INCAP(parent)) // Probably can't hear them if we're dead return - if (parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] == command_key) // We're already doing it + if (parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] == WEAKREF(src)) // We're already doing it return set_command_active(parent, commander) @@ -74,7 +77,7 @@ set_command_target(parent, null) parent.ai_controller.CancelActions() // Stop whatever you're doing and do this instead - parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] = command_key + parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] = WEAKREF(src) if (command_feedback) parent.balloon_alert_to_viewers("[command_feedback]") // If we get a nicer runechat way to do this, refactor this @@ -84,6 +87,8 @@ /// Provide information about how to display this command in a radial menu /datum/pet_command/proc/provide_radial_data() + if (hidden) + return var/datum/radial_menu_choice/choice = new() choice.name = command_name choice.image = icon(icon = radial_icon, icon_state = radial_icon_state) @@ -117,27 +122,31 @@ . = ..() RegisterSignal(tamer, COMSIG_MOB_POINTED, PROC_REF(look_for_target)) +/datum/pet_command/point_targetting/remove_friend(mob/living/unfriended) + . = ..() + UnregisterSignal(unfriended, COMSIG_MOB_POINTED) + /// Target the pointed atom for actions /datum/pet_command/point_targetting/proc/look_for_target(mob/living/friend, atom/pointed_atom) SIGNAL_HANDLER - var/mob/living/parent = weak_parent.resolve() if (!parent) - return + return FALSE if (!parent.ai_controller) - return + return FALSE if (IS_DEAD_OR_INCAP(parent)) - return - if (parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] != command_key) // We're not listening right now - return + return FALSE + if (parent.ai_controller.blackboard[BB_ACTIVE_PET_COMMAND] != WEAKREF(src)) // We're not listening right now + return FALSE if (parent.ai_controller.blackboard[BB_CURRENT_PET_TARGET] == WEAKREF(pointed_atom)) // That's already our target - return + return FALSE if (!can_see(parent, pointed_atom, sense_radius)) - return + return FALSE parent.ai_controller.CancelActions() // Deciding if they can actually do anything with this target is the behaviour's job set_command_target(parent, pointed_atom) // These are usually hostile actions so should have a record in chat - parent.visible_message(span_warning("[parent] follows [friend]'s gesture towards [pointed_atom] and [pointed_reaction]!")) + parent.visible_message(span_warning("[parent] follows [friend]'s gesture towards [pointed_atom] [pointed_reaction]!")) + return TRUE diff --git a/code/datums/components/pet_commands/pet_commands_basic.dm b/code/datums/components/pet_commands/pet_commands_basic.dm index 641e3a68748..674b2b467d8 100644 --- a/code/datums/components/pet_commands/pet_commands_basic.dm +++ b/code/datums/components/pet_commands/pet_commands_basic.dm @@ -7,9 +7,8 @@ /datum/pet_command/idle command_name = "Stay" command_desc = "Command your pet to stay idle in this location." - radial_icon = 'icons/testing/turf_analysis.dmi' - radial_icon_state = "red_arrow" - command_key = PET_COMMAND_IDLE + radial_icon = 'icons/obj/objects.dmi' + radial_icon_state = "dogbed" speech_commands = list("sit", "stay", "stop") command_feedback = "sits" @@ -25,11 +24,11 @@ command_desc = "Allow your pet to resume its natural behaviours." radial_icon = 'icons/mob/actions/actions_spells.dmi' radial_icon_state = "repulse" - command_key = PET_COMMAND_NONE speech_commands = list("free", "loose") command_feedback = "relaxes" /datum/pet_command/free/execute_action(datum/ai_controller/controller) + controller.blackboard[BB_ACTIVE_PET_COMMAND] = null return // Just move on to the next planning subtree. /** @@ -39,9 +38,8 @@ /datum/pet_command/follow command_name = "Follow" command_desc = "Command your pet to accompany you." - radial_icon = 'icons/mob/actions/actions_spells.dmi' - radial_icon_state = "summons" - command_key = PET_COMMAND_FOLLOW + radial_icon = 'icons/testing/turf_analysis.dmi' + radial_icon_state = "red_arrow" speech_commands = list("heel", "follow") /datum/pet_command/follow/set_command_active(mob/living/parent, mob/living/commander) @@ -52,6 +50,52 @@ controller.queue_behavior(/datum/ai_behavior/pet_follow_friend, BB_CURRENT_PET_TARGET) return SUBTREE_RETURN_FINISH_PLANNING +/** + * # Pet Command: Play Dead + * Pretend to be dead for a random period of time + */ +/datum/pet_command/play_dead + command_name = "Play Dead" + command_desc = "Play a macabre trick." + radial_icon = 'icons/mob/simple/pets.dmi' + radial_icon_state = "puppy_dead" + speech_commands = list("play dead") // Don't get too creative here, people talk about dying pretty often + +/datum/pet_command/play_dead/execute_action(datum/ai_controller/controller) + controller.queue_behavior(/datum/ai_behavior/play_dead) + return SUBTREE_RETURN_FINISH_PLANNING + +/** + * # Pet Command: Good Boy + * React if complimented + */ +/datum/pet_command/good_boy + command_name = "Good Boy" + command_desc = "Give your pet a compliment." + hidden = TRUE + +/datum/pet_command/good_boy/New(mob/living/parent) + . = ..() + speech_commands += "good [parent.name]" + switch (parent.gender) + if (MALE) + speech_commands += "good boy" + return + if (FEMALE) + speech_commands += "good girl" + return + // If we get past this point someone has finally added a non-binary dog + +/datum/pet_command/good_boy/execute_action(datum/ai_controller/controller) + controller.blackboard[BB_ACTIVE_PET_COMMAND] = null + var/mob/living/parent = weak_parent.resolve() + if (!parent) + return SUBTREE_RETURN_FINISH_PLANNING + + new /obj/effect/temp_visual/heart(parent.loc) + parent.emote("spin") + return SUBTREE_RETURN_FINISH_PLANNING + /** * # Pet Command: Attack * Tells a pet to chase and bite the next thing you point at @@ -62,10 +106,9 @@ radial_icon = 'icons/effects/effects.dmi' radial_icon_state = "bite" - command_key = PET_COMMAND_ATTACK speech_commands = list("attack", "sic", "kill") command_feedback = "growl" - pointed_reaction = "growls" + pointed_reaction = "and growls" /// Balloon alert to display if providing an invalid target var/refuse_reaction = "shakes head" /// Attack behaviour to use, generally you will want to override this to add some kind of cooldown @@ -105,10 +148,9 @@ command_desc = "Command your pet to use one of its special skills on something that you point out to it." radial_icon = 'icons/mob/actions/actions_spells.dmi' radial_icon_state = "projectile" - command_key = PET_COMMAND_USE_ABILITY speech_commands = list("shoot", "blast", "cast") command_feedback = "growl" - pointed_reaction = "growls" + pointed_reaction = "and growls" /// Blackboard key where a reference to some kind of mob ability is stored var/pet_ability_key diff --git a/code/datums/elements/ai_held_item.dm b/code/datums/elements/ai_held_item.dm new file mode 100644 index 00000000000..9fbe9c77c05 --- /dev/null +++ b/code/datums/elements/ai_held_item.dm @@ -0,0 +1,71 @@ +/** + * # AI Held Item Element + * + * Manages holding an item for a mob which doesn't have hands but needs to for AI purposes. + */ +/datum/element/ai_held_item + +/datum/element/ai_held_item/Attach(datum/target) + . = ..() + if (!isliving(target)) + return ELEMENT_INCOMPATIBLE + var/mob/living/living_target = target + if (!living_target.ai_controller) + return ELEMENT_INCOMPATIBLE // If there's no blackboard this would need to be a component + + RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_click)) + RegisterSignal(target, COMSIG_ATOM_EXITED, PROC_REF(atom_exited)) + RegisterSignal(target, COMSIG_PARENT_EXAMINE, PROC_REF(on_examined)) + RegisterSignals(target, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), PROC_REF(on_death)) + +/// Returns the item held in a mob's blackboard, if it has one +/datum/element/ai_held_item/proc/get_held_item(mob/living/source) + var/datum/weakref/weak_item = source.ai_controller.blackboard[BB_SIMPLE_CARRY_ITEM] + return weak_item?.resolve() + +/// Someone's interacting with us by hand, if we have an item and like them we'll hand it over +/datum/element/ai_held_item/proc/on_click(mob/living/source, mob/living/user) + SIGNAL_HANDLER + + if (user.combat_mode) + return + + var/list/friends = source.ai_controller.blackboard[BB_FRIENDS_LIST] + if (!friends || !friends[WEAKREF(user)]) + return // We don't care about this bozo + var/obj/item/carried_item = get_held_item(source) + if (!carried_item) + return + + source.visible_message(span_danger("[source] drops [carried_item] at [user]'s feet!")) + carried_item.forceMove(get_turf(user)) + source.ai_controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null + +/// If our held item is removed from our atom then take it off the blackboard +/datum/element/ai_held_item/proc/atom_exited(mob/living/source, atom/movable/gone) + SIGNAL_HANDLER + + var/obj/item/carried_item = get_held_item(source) + if (carried_item == gone) + source.ai_controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null + +/// Report that we're holding an item. +/datum/element/ai_held_item/proc/on_examined(mob/living/source, mob/user, list/examine_text) + SIGNAL_HANDLER + + var/obj/item/carried_item = get_held_item(source) + if (!carried_item) + return + examine_text += span_notice("[source.p_they(TRUE)] [source.p_are()] carrying [carried_item.get_examine_string(user)].") + +/// If we died, drop anything we were carrying +/datum/element/ai_held_item/proc/on_death(mob/living/ol_yeller) + SIGNAL_HANDLER + + var/obj/item/carried_item = get_held_item(ol_yeller) + if(!carried_item) + return + + ol_yeller.visible_message(span_danger("[ol_yeller] drops [carried_item] as [ol_yeller.p_they()] die[ol_yeller.p_s()].")) + carried_item.forceMove(ol_yeller.drop_location()) + ol_yeller.ai_controller.blackboard[BB_SIMPLE_CARRY_ITEM] = null diff --git a/code/datums/elements/befriend_petting.dm b/code/datums/elements/befriend_petting.dm new file mode 100644 index 00000000000..41fdcf5e222 --- /dev/null +++ b/code/datums/elements/befriend_petting.dm @@ -0,0 +1,77 @@ +#define BEFRIEND_REPLACE_KEY_SOURCE "%SOURCE%" +#define BEFRIEND_REPLACE_KEY_TARGET "%TARGET%" + +/** + * # Befriend Petting + * + * Element which makes a mob befriend you if you pet it enough, and unfriend you if you hurt it. + */ +/datum/element/befriend_petting + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + /// Chance of success per interaction. + var/befriend_chance + /// Message to print if we gain a friend. String %SOURCE% and %TARGET% are replaced by names if present. + var/tamed_reaction + /// Message to print if we remove a friend. String %SOURCE% and %TARGET% are replaced by names if present. + var/untamed_reaction + +/datum/element/befriend_petting/Attach(datum/target, befriend_chance = AI_DOG_PET_FRIEND_PROB, tamed_reaction, untamed_reaction) + . = ..() + if (!isliving(target)) + return ELEMENT_INCOMPATIBLE + + src.befriend_chance = befriend_chance + src.tamed_reaction = tamed_reaction + src.untamed_reaction = untamed_reaction + target.AddElement(/datum/element/ai_retaliate) + RegisterSignal(target, COMSIG_ATOM_ATTACK_HAND, PROC_REF(on_click)) + RegisterSignal(target, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_hurt)) + +/datum/element/befriend_petting/Detach(datum/target) + . = ..() + UnregisterSignal(target, list(COMSIG_ATOM_ATTACK_HAND, COMSIG_ATOM_WAS_ATTACKED)) + +/// If it's a nice touch make friends +/datum/element/befriend_petting/proc/on_click(mob/living/owner, mob/living/user) + SIGNAL_HANDLER + + if (!istype(user)) + return + if (user.combat_mode) + return // We'll deal with this later + if (owner.stat == DEAD) + var/additional_text = HAS_TRAIT(user, TRAIT_NAIVE) || HAS_TRAIT(user.mind, TRAIT_NAIVE) ? "It looks like [owner.p_theyre()] sleeping." : "[owner.p_they(capitalized = TRUE)] seem[owner.p_s()] to be dead." + to_chat(user, span_warning("[owner] feels cold to the touch. [additional_text]")) + return + if (owner.stat != CONSCIOUS) + return + if (!prob(befriend_chance)) + return + if (!owner.befriend(user)) + return + if (!tamed_reaction) + return + var/display_message = replacetext(tamed_reaction, BEFRIEND_REPLACE_KEY_SOURCE, "[owner]") + display_message = replacetext(display_message, BEFRIEND_REPLACE_KEY_TARGET, "[user]") + owner.visible_message(span_notice(display_message)) + +/// If it's a bad touch make enemies +/datum/element/befriend_petting/proc/on_hurt(mob/living/owner, atom/attacker) + SIGNAL_HANDLER + + if (owner.stat != CONSCIOUS) + return + if (!isliving(attacker)) + return + var/mob/living/living_attacker = attacker + if (!owner.unfriend(living_attacker)) + return + if (!untamed_reaction) + return + var/display_message = replacetext(untamed_reaction, BEFRIEND_REPLACE_KEY_SOURCE, "[owner]") + display_message = replacetext(display_message, BEFRIEND_REPLACE_KEY_TARGET, "[attacker]") + owner.visible_message(span_notice(display_message)) + +#undef BEFRIEND_REPLACE_KEY_SOURCE +#undef BEFRIEND_REPLACE_KEY_TARGET diff --git a/code/modules/mob/living/basic/basic.dm b/code/modules/mob/living/basic/basic.dm index 40ec3beba53..8ebc78dee06 100644 --- a/code/modules/mob/living/basic/basic.dm +++ b/code/modules/mob/living/basic/basic.dm @@ -137,8 +137,15 @@ . = ..() if(basic_mob_flags & DEL_ON_DEATH) qdel(src) - return - health = 0 + else + health = 0 + look_dead() + +/** + * Apply the appearance and properties this mob has when it dies + * This is called by the mob pretending to be dead too so don't put loot drops in here or something + */ +/mob/living/basic/proc/look_dead() icon_state = icon_dead if(basic_mob_flags & FLIP_ON_DEATH) transform = transform.Turn(180) @@ -147,8 +154,12 @@ /mob/living/basic/revive(full_heal_flags = NONE, excess_healing = 0, force_grab_ghost = FALSE) . = ..() - if (!.) + if(!.) return + look_alive() + +/// Apply the appearance and properties this mob has when it is alive +/mob/living/basic/proc/look_alive() icon_state = icon_living if(basic_mob_flags & FLIP_ON_DEATH) transform = transform.Turn(180) diff --git a/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm b/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm index 81e1d722b01..eeb11c1fbf1 100644 --- a/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm +++ b/code/modules/mob/living/basic/farm_animals/cow/cow_ai.dm @@ -5,7 +5,7 @@ BB_BASIC_MOB_TIPPER = null, ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( diff --git a/code/modules/mob/living/basic/farm_animals/pig.dm b/code/modules/mob/living/basic/farm_animals/pig.dm index fee809e43eb..f6f5983859e 100644 --- a/code/modules/mob/living/basic/farm_animals/pig.dm +++ b/code/modules/mob/living/basic/farm_animals/pig.dm @@ -51,7 +51,7 @@ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(), ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk diff --git a/code/modules/mob/living/basic/farm_animals/rabbit.dm b/code/modules/mob/living/basic/farm_animals/rabbit.dm index 071470264db..1418786759a 100644 --- a/code/modules/mob/living/basic/farm_animals/rabbit.dm +++ b/code/modules/mob/living/basic/farm_animals/rabbit.dm @@ -50,7 +50,7 @@ BB_BASIC_MOB_FLEEING = TRUE, BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(), ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( diff --git a/code/modules/mob/living/basic/farm_animals/sheep.dm b/code/modules/mob/living/basic/farm_animals/sheep.dm index 95e1a6737df..94e37dc6f0b 100644 --- a/code/modules/mob/living/basic/farm_animals/sheep.dm +++ b/code/modules/mob/living/basic/farm_animals/sheep.dm @@ -85,7 +85,7 @@ BB_BASIC_MOB_FLEEING = TRUE, BB_TARGETTING_DATUM = new /datum/targetting_datum/basic/ignore_faction(), ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( diff --git a/code/modules/mob/living/basic/pets/dog.dm b/code/modules/mob/living/basic/pets/dog.dm index 0c379819b86..61270632452 100644 --- a/code/modules/mob/living/basic/pets/dog.dm +++ b/code/modules/mob/living/basic/pets/dog.dm @@ -1,5 +1,21 @@ //Dogs. +// Add 'walkies' as valid input +/datum/pet_command/follow/dog + speech_commands = list("heel", "follow", "walkies") + +// Add 'good dog' as valid input +/datum/pet_command/good_boy/dog + speech_commands = list("good dog") + +// Set correct attack behaviour +/datum/pet_command/point_targetting/attack/dog + attack_behaviour = /datum/ai_behavior/basic_melee_attack/dog + +/datum/pet_command/point_targetting/attack/dog/set_command_active(mob/living/parent, mob/living/commander) + . = ..() + parent.ai_controller.blackboard[BB_DOG_HARASS_HARM] = TRUE + /mob/living/basic/pet/dog mob_biotypes = MOB_ORGANIC|MOB_BEAST response_help_continuous = "pets" @@ -12,17 +28,29 @@ faction = list(FACTION_NEUTRAL) see_in_dark = 5 can_be_held = TRUE - ai_controller = /datum/ai_controller/dog - ///In the case 'melee_damage_upper' is somehow raised above 0 + ai_controller = /datum/ai_controller/basic_controller/dog + // The dog attack pet command can raise melee attack above 0 attack_verb_continuous = "bites" attack_verb_simple = "bite" attack_sound = 'sound/weapons/bite.ogg' attack_vis_effect = ATTACK_EFFECT_BITE + /// Instructions you can give to dogs + var/static/list/pet_commands = list( + /datum/pet_command/idle, + /datum/pet_command/free, + /datum/pet_command/good_boy/dog, + /datum/pet_command/follow/dog, + /datum/pet_command/point_targetting/attack/dog, + /datum/pet_command/point_targetting/fetch, + /datum/pet_command/play_dead, + ) /mob/living/basic/pet/dog/Initialize(mapload) . = ..() AddElement(/datum/element/pet_bonus, "woofs happily!") AddElement(/datum/element/footstep, FOOTSTEP_MOB_CLAW) + AddElement(/datum/element/befriend_petting, tamed_reaction = "%SOURCE% licks at %TARGET% in a friendly manner!", untamed_reaction = "%SOURCE% fixes %TARGET% with a look of betrayal.") + AddComponent(/datum/component/obeys_commands, pet_commands) /mob/living/basic/pet/dog/proc/update_dog_speech(datum/ai_planning_subtree/random_speech/speech) speech.speak = string_list(list("YAP", "Woof!", "Bark!", "AUUUUUU")) @@ -42,7 +70,7 @@ butcher_results = list(/obj/item/food/meat/slab/corgi = 3, /obj/item/stack/sheet/animalhide/corgi = 1) gold_core_spawnable = FRIENDLY_SPAWN collar_icon_state = "corgi" - ai_controller = /datum/ai_controller/dog/corgi + ai_controller = /datum/ai_controller/basic_controller/dog/corgi var/obj/item/inventory_head var/obj/item/inventory_back /// Access card for Ian. @@ -522,6 +550,7 @@ GLOBAL_LIST_INIT(strippable_corgi_items, create_strippable_list(list( desc = "At a ripe old age of [record_age], Ian's not as spry as he used to be, but he'll always be the HoP's beloved corgi." //RIP ai_controller?.blackboard[BB_DOG_IS_SLOW] = TRUE is_slow = TRUE + speed = 2 /mob/living/basic/pet/dog/corgi/ian/Life(delta_time = SSMOBS_DT, times_fired) if(!stat && SSticker.current_state == GAME_STATE_FINISHED && !memory_saved) diff --git a/code/modules/mob/living/basic/vermin/axolotl.dm b/code/modules/mob/living/basic/vermin/axolotl.dm index a56f1df5909..5a8b9d3c986 100644 --- a/code/modules/mob/living/basic/vermin/axolotl.dm +++ b/code/modules/mob/living/basic/vermin/axolotl.dm @@ -36,6 +36,6 @@ ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT) /datum/ai_controller/basic_controller/axolotl - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk diff --git a/code/modules/mob/living/basic/vermin/cockroach.dm b/code/modules/mob/living/basic/vermin/cockroach.dm index 0d51b92abfb..dbc203d1c45 100644 --- a/code/modules/mob/living/basic/vermin/cockroach.dm +++ b/code/modules/mob/living/basic/vermin/cockroach.dm @@ -57,7 +57,7 @@ BB_PET_TARGETTING_DATUM = new /datum/targetting_datum/not_friends(), ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( diff --git a/code/modules/mob/living/basic/vermin/mothroach.dm b/code/modules/mob/living/basic/vermin/mothroach.dm index 15f82d38f85..d9e765f239f 100644 --- a/code/modules/mob/living/basic/vermin/mothroach.dm +++ b/code/modules/mob/living/basic/vermin/mothroach.dm @@ -67,7 +67,7 @@ /datum/ai_controller/basic_controller/mothroach blackboard = list() - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( diff --git a/code/modules/mob/living/basic/vermin/mouse.dm b/code/modules/mob/living/basic/vermin/mouse.dm index 6e871f8f113..a5e2f182d6a 100644 --- a/code/modules/mob/living/basic/vermin/mouse.dm +++ b/code/modules/mob/living/basic/vermin/mouse.dm @@ -356,7 +356,7 @@ BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), // Use this to find people to run away from ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( @@ -400,7 +400,7 @@ BB_LOW_PRIORITY_HUNTING_TARGET = null, // cable ) - ai_traits = STOP_MOVING_WHEN_PULLED + ai_traits = STOP_MOVING_WHEN_PULLED | STOP_ACTING_WHILE_DEAD ai_movement = /datum/ai_movement/basic_avoidance idle_behavior = /datum/idle_behavior/idle_random_walk planning_subtrees = list( diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index ac0e7574c14..282e9c33e97 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2408,10 +2408,13 @@ GLOBAL_LIST_EMPTY(fire_appearances) . = ..() add_mood_event("gaming", /datum/mood_event/gaming) -/// Proc for giving a mob a new 'friend', generally used for AI control and targetting +/// Proc for giving a mob a new 'friend', generally used for AI control and targetting. Returns false if already friends. /mob/living/proc/befriend(mob/living/new_friend) SHOULD_CALL_PARENT(TRUE) - faction |= REF(new_friend) + var/friend_ref = REF(new_friend) + if (faction.Find(friend_ref)) + return FALSE + faction |= friend_ref if (ai_controller) var/list/friends = ai_controller.blackboard[BB_FRIENDS_LIST] if (!friends) @@ -2419,6 +2422,23 @@ GLOBAL_LIST_EMPTY(fire_appearances) friends[WEAKREF(new_friend)] = TRUE ai_controller.blackboard[BB_FRIENDS_LIST] = friends SEND_SIGNAL(src, COMSIG_LIVING_BEFRIENDED, new_friend) + return TRUE + +/// Proc for removing a friend you added with the proc 'befriend'. Returns true if you removed a friend. +/mob/living/proc/unfriend(mob/living/old_friend) + SHOULD_CALL_PARENT(TRUE) + var/friend_ref = REF(old_friend) + if (!faction.Find(friend_ref)) + return FALSE + faction -= friend_ref + if (ai_controller) + var/list/friends = ai_controller.blackboard[BB_FRIENDS_LIST] + if (!friends) + return + friends[WEAKREF(old_friend)] = FALSE + ai_controller.blackboard[BB_FRIENDS_LIST] = friends + SEND_SIGNAL(src, COMSIG_LIVING_UNFRIENDED, old_friend) + return TRUE /// Admin only proc for making the mob hallucinate a certain thing /mob/living/proc/admin_give_hallucination(mob/admin) diff --git a/code/modules/mob/living/simple_animal/hostile/hostile.dm b/code/modules/mob/living/simple_animal/hostile/hostile.dm index be9cd078bc2..01f308898a8 100644 --- a/code/modules/mob/living/simple_animal/hostile/hostile.dm +++ b/code/modules/mob/living/simple_animal/hostile/hostile.dm @@ -667,5 +667,7 @@ /mob/living/simple_animal/hostile/befriend(mob/living/new_friend) . = ..() + if (!.) + return friends += new_friend faction = new_friend.faction.Copy() diff --git a/code/modules/mob/living/simple_animal/hostile/regalrat.dm b/code/modules/mob/living/simple_animal/hostile/regalrat.dm index e86dfa6ad83..5342f551d33 100644 --- a/code/modules/mob/living/simple_animal/hostile/regalrat.dm +++ b/code/modules/mob/living/simple_animal/hostile/regalrat.dm @@ -387,7 +387,7 @@ /datum/pet_command/point_targetting/attack/mouse speech_commands = list("attack", "sic", "kill", "cheese em") command_feedback = "squeak!" // Frogs and roaches can squeak too it's fine - pointed_reaction = "squeaks aggressively!" + pointed_reaction = "and squeaks aggressively" refuse_reaction = "quivers" attack_behaviour = /datum/ai_behavior/basic_melee_attack/rat @@ -395,7 +395,7 @@ /datum/pet_command/point_targetting/attack/glockroach speech_commands = list("attack", "sic", "kill", "cheese em") command_feedback = "squeak!" - pointed_reaction = "cocks gun" + pointed_reaction = "and cocks its gun" refuse_reaction = "quivers" attack_behaviour = /datum/ai_behavior/basic_ranged_attack/glockroach diff --git a/tgstation.dme b/tgstation.dme index 14ed9cc4a81..f296f28a67b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -724,6 +724,7 @@ #include "code\datums\ai\basic_mobs\base_basic_controller.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\basic_attacking.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\nearest_targetting.dm" +#include "code\datums\ai\basic_mobs\basic_ai_behaviors\pick_up_item.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\run_away_from_target.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\targetting.dm" #include "code\datums\ai\basic_mobs\basic_ai_behaviors\tipped_reaction.dm" @@ -738,9 +739,11 @@ #include "code\datums\ai\basic_mobs\basic_subtrees\target_retaliate.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\targetted_mob_ability.dm" #include "code\datums\ai\basic_mobs\basic_subtrees\tipped_subtree.dm" +#include "code\datums\ai\basic_mobs\pet_commands\fetch.dm" #include "code\datums\ai\basic_mobs\pet_commands\pet_command_planning.dm" #include "code\datums\ai\basic_mobs\pet_commands\pet_follow_friend.dm" #include "code\datums\ai\basic_mobs\pet_commands\pet_use_targetted_ability.dm" +#include "code\datums\ai\basic_mobs\pet_commands\play_dead.dm" #include "code\datums\ai\basic_mobs\targetting_datums\basic_targetting_datum.dm" #include "code\datums\ai\basic_mobs\targetting_datums\dont_target_friends.dm" #include "code\datums\ai\cursed\cursed_behaviors.dm" @@ -981,6 +984,7 @@ #include "code\datums\components\food\decomposition.dm" #include "code\datums\components\food\edible.dm" #include "code\datums\components\food\ice_cream_holder.dm" +#include "code\datums\components\pet_commands\fetch.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_commands_basic.dm" @@ -1052,6 +1056,7 @@ #include "code\datums\diseases\advance\symptoms\youth.dm" #include "code\datums\elements\_element.dm" #include "code\datums\elements\ai_flee_while_injured.dm" +#include "code\datums\elements\ai_held_item.dm" #include "code\datums\elements\ai_retaliate.dm" #include "code\datums\elements\animal_variety.dm" #include "code\datums\elements\art.dm" @@ -1064,6 +1069,7 @@ #include "code\datums\elements\basic_eating.dm" #include "code\datums\elements\beauty.dm" #include "code\datums\elements\bed_tucking.dm" +#include "code\datums\elements\befriend_petting.dm" #include "code\datums\elements\bsa_blocker.dm" #include "code\datums\elements\bump_click.dm" #include "code\datums\elements\can_barricade.dm"