diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm
new file mode 100644
index 00000000000..2331c66a2b9
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_ai.dm
@@ -0,0 +1,2 @@
+/// Signal sent when a blackboard key is set to a new value
+#define COMSIG_AI_BLACKBOARD_KEY_SET(blackboard_key) "ai_blackboard_key_set_[blackboard_key]"
diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm
index 84b415b90f1..ccf58c9bdf6 100644
--- a/code/datums/ai/_ai_controller.dm
+++ b/code/datums/ai/_ai_controller.dm
@@ -396,6 +396,9 @@ multiple modular subtrees with behaviors
// Assume it is an error when trying to set a value overtop a list
if(islist(blackboard[key]))
CRASH("set_blackboard_key attempting to set a blackboard value to key [key] when it's a list!")
+ // Don't do anything if it's already got this value
+ if (blackboard[key] == thing)
+ return
// Clear existing values
if(!isnull(blackboard[key]))
@@ -403,6 +406,7 @@ multiple modular subtrees with behaviors
TRACK_AI_DATUM_TARGET(thing, key)
blackboard[key] = thing
+ post_blackboard_key_set(key)
/**
* Sets the key at index thing to the passed value
@@ -416,9 +420,14 @@ multiple modular subtrees with behaviors
/datum/ai_controller/proc/set_blackboard_key_assoc(key, thing, value)
if(!islist(blackboard[key]))
CRASH("set_blackboard_key_assoc called on non-list key [key]!")
+ // Don't do anything if it's already got this value
+ if (blackboard[key][thing] == value)
+ return
+
TRACK_AI_DATUM_TARGET(thing, key)
TRACK_AI_DATUM_TARGET(value, key)
blackboard[key][thing] = value
+ post_blackboard_key_set(key)
/**
* Similar to [proc/set_blackboard_key_assoc] but operates under the assumption the key is a lazylist (so it will create a list)
@@ -430,9 +439,22 @@ multiple modular subtrees with behaviors
*/
/datum/ai_controller/proc/set_blackboard_key_assoc_lazylist(key, thing, value)
LAZYINITLIST(blackboard[key])
+ // Don't do anything if it's already got this value
+ if (blackboard[key][thing] == value)
+ return
+
TRACK_AI_DATUM_TARGET(thing, key)
TRACK_AI_DATUM_TARGET(value, key)
blackboard[key][thing] = value
+ post_blackboard_key_set(key)
+
+/**
+ * Called after we set a blackboard key, forwards signal information.
+ */
+/datum/ai_controller/proc/post_blackboard_key_set(key)
+ if (isnull(pawn))
+ return
+ SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_SET(key))
/**
* Adds the passed "thing" to the associated key
diff --git a/code/datums/components/aggro_emote.dm b/code/datums/components/aggro_emote.dm
new file mode 100644
index 00000000000..0688d277aa4
--- /dev/null
+++ b/code/datums/components/aggro_emote.dm
@@ -0,0 +1,54 @@
+/// A component for ai-controlled atoms which plays a sound if they switch to a living target which they can attack
+/datum/component/aggro_emote
+ /// Blackboard key in which target data is stored
+ var/target_key
+ /// If we want to limit emotes to only play at mobs
+ var/living_only
+ /// List of emotes to play
+ var/list/emote_list
+ /// Chance to play an emote
+ var/emote_chance
+ /// Chance to subtract every time we play an emote (permanently)
+ var/subtract_chance
+ /// Minimum chance to play an emote
+ var/minimum_chance
+
+/datum/component/aggro_emote/Initialize(
+ target_key = BB_BASIC_MOB_CURRENT_TARGET,
+ living_only = FALSE,
+ list/emote_list,
+ emote_chance = 30,
+ minimum_chance = 2,
+ subtract_chance = 7,
+)
+ . = ..()
+ if (!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+ var/atom/atom_parent = parent
+ if (!atom_parent.ai_controller)
+ return COMPONENT_INCOMPATIBLE
+
+ src.target_key = target_key
+ src.emote_list = emote_list
+ src.emote_chance = emote_chance
+ src.minimum_chance = minimum_chance
+ src.subtract_chance = subtract_chance
+
+/datum/component/aggro_emote/RegisterWithParent()
+ . = ..()
+ RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(target_key), PROC_REF(on_target_changed))
+
+/datum/component/aggro_emote/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(target_key))
+ return ..()
+
+/// When we get a new target, see if we want to bark at it
+/datum/component/aggro_emote/proc/on_target_changed(atom/source)
+ SIGNAL_HANDLER
+ var/atom/new_target = source.ai_controller.blackboard[target_key]
+ if (isnull(new_target) || !prob(emote_chance))
+ return
+ if (living_only && !isliving(new_target))
+ return // If we don't want to bark at food items or chairs or windows
+ emote_chance = max(emote_chance - subtract_chance, minimum_chance)
+ source.manual_emote("[pick(emote_list)] at [new_target].")
diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm
index 8c5a5c5c055..d718d149882 100644
--- a/code/datums/emotes.dm
+++ b/code/datums/emotes.dm
@@ -289,24 +289,27 @@
*
* Returns TRUE if it was able to run the emote, FALSE otherwise.
*/
-/mob/proc/manual_emote(text) //Just override the song and dance
- . = TRUE
- if(stat != CONSCIOUS)
- return
-
+/atom/proc/manual_emote(text)
if(!text)
CRASH("Someone passed nothing to manual_emote(), fix it")
log_message(text, LOG_EMOTE)
-
- var/ghost_text = "[src] [text]"
-
- var/origin_turf = get_turf(src)
- if(client)
- for(var/mob/ghost as anything in GLOB.dead_mob_list)
- if(!ghost.client || isnewplayer(ghost))
- continue
- if(ghost.client.prefs.chat_toggles & CHAT_GHOSTSIGHT && !(ghost in viewers(origin_turf, null)))
- ghost.show_message("[FOLLOW_LINK(ghost, src)] [ghost_text]")
-
visible_message(text, visible_message_flags = EMOTE_MESSAGE)
+ return TRUE
+
+/mob/manual_emote(text)
+ if (stat != CONSCIOUS)
+ return FALSE
+ . = ..()
+ if (!.)
+ return FALSE
+ if (!client)
+ return TRUE
+ var/ghost_text = "[src] [text]"
+ var/origin_turf = get_turf(src)
+ for(var/mob/ghost as anything in GLOB.dead_mob_list)
+ if(!ghost.client || isnewplayer(ghost))
+ continue
+ if(ghost.client.prefs.chat_toggles & CHAT_GHOSTSIGHT && !(ghost in viewers(origin_turf, null)))
+ ghost.show_message("[FOLLOW_LINK(ghost, src)] [ghost_text]")
+ return TRUE
diff --git a/code/modules/mob/living/basic/festivus_pole.dm b/code/modules/mob/living/basic/festivus_pole.dm
index 3cc80afb437..069da5b8906 100644
--- a/code/modules/mob/living/basic/festivus_pole.dm
+++ b/code/modules/mob/living/basic/festivus_pole.dm
@@ -43,6 +43,7 @@
/mob/living/basic/festivus/Initialize(mapload)
. = ..()
AddElement(/datum/element/death_drops, list(/obj/item/stack/rods))
+ AddComponent(/datum/component/aggro_emote, emote_list = string_list(list("growls")), emote_chance = 20)
var/datum/action/cooldown/mob_cooldown/charge_apc/charge_ability = new(src)
charge_ability.Grant(src)
ai_controller.set_blackboard_key(BB_FESTIVE_APC, charge_ability)
diff --git a/code/modules/mob/living/basic/space_fauna/carp/carp.dm b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
index 29a444707cd..f57b7b4f20d 100644
--- a/code/modules/mob/living/basic/space_fauna/carp/carp.dm
+++ b/code/modules/mob/living/basic/space_fauna/carp/carp.dm
@@ -110,6 +110,7 @@
AddElement(/datum/element/ai_flee_while_injured)
setup_eating()
+ AddComponent(/datum/component/aggro_emote, emote_list = string_list(list("gnashes")))
AddComponent(/datum/component/regenerator, outline_colour = regenerate_colour)
if (tamer)
on_tamed(tamer, feedback = FALSE)
diff --git a/code/modules/mob/living/basic/tree.dm b/code/modules/mob/living/basic/tree.dm
index c6abaefe4b2..4b07630722c 100644
--- a/code/modules/mob/living/basic/tree.dm
+++ b/code/modules/mob/living/basic/tree.dm
@@ -56,6 +56,7 @@
. = ..()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_PINE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
AddElement(/datum/element/death_drops, list(/obj/item/stack/sheet/mineral/wood))
+ AddComponent(/datum/component/aggro_emote, emote_list = string_list(list("growls")), emote_chance = 20)
/mob/living/basic/tree/Life(seconds_per_tick = SSMOBS_DT, times_fired)
..()
diff --git a/tgstation.dme b/tgstation.dme
index e33bd6c7520..9502c2a4b68 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -330,6 +330,7 @@
#include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movable.dm"
#include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movement.dm"
#include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_x_act.dm"
+#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_ai.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_arcade.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_carbon.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_living.dm"
@@ -869,6 +870,7 @@
#include "code\datums\components\acid.dm"
#include "code\datums\components\action_item_overlay.dm"
#include "code\datums\components\admin_popup.dm"
+#include "code\datums\components\aggro_emote.dm"
#include "code\datums\components\ai_retaliate_advanced.dm"
#include "code\datums\components\anti_magic.dm"
#include "code\datums\components\aquarium.dm"