diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Server/Chat/Systems/ChatSystem.Emote.cs
index df13db7af1..459fcd20ae 100644
--- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.Emote.cs
@@ -55,11 +55,18 @@ public partial class ChatSystem
/// Whether or not this message should appear in the adminlog window
/// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...
/// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised.
- public void TryEmoteWithChat(EntityUid source, string emoteId, ChatTransmitRange range = ChatTransmitRange.Normal, bool hideLog = false, string? nameOverride = null)
+ public void TryEmoteWithChat(
+ EntityUid source,
+ string emoteId,
+ ChatTransmitRange range = ChatTransmitRange.Normal,
+ bool hideLog = false,
+ string? nameOverride = null,
+ bool ignoreActionBlocker = false
+ )
{
if (!_prototypeManager.TryIndex(emoteId, out var proto))
return;
- TryEmoteWithChat(source, proto, range, hideLog, nameOverride);
+ TryEmoteWithChat(source, proto, range, hideLog, nameOverride, ignoreActionBlocker);
}
///
@@ -71,35 +78,44 @@ public partial class ChatSystem
/// Whether or not this message should appear in the chat window
/// Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...
/// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised.
- public void TryEmoteWithChat(EntityUid source, EmotePrototype emote, ChatTransmitRange range = ChatTransmitRange.Normal, bool hideLog = false, string? nameOverride = null)
+ public void TryEmoteWithChat(
+ EntityUid source,
+ EmotePrototype emote,
+ ChatTransmitRange range = ChatTransmitRange.Normal,
+ bool hideLog = false,
+ string? nameOverride = null,
+ bool ignoreActionBlocker = false
+ )
{
// check if proto has valid message for chat
if (emote.ChatMessages.Count != 0)
{
- var action = _random.Pick(emote.ChatMessages);
- SendEntityEmote(source, action, range, nameOverride, false, hideLog);
+ // not all emotes are loc'd, but for the ones that are we pass in entity
+ var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
+ SendEntityEmote(source, action, range, nameOverride, false, hideLog, ignoreActionBlocker);
}
// do the rest of emote event logic here
- TryEmoteWithoutChat(source, emote);
+ TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
}
///
/// Makes selected entity to emote using without sending any messages to chat.
///
- public void TryEmoteWithoutChat(EntityUid uid, string emoteId)
+ public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
{
if (!_prototypeManager.TryIndex(emoteId, out var proto))
return;
- TryEmoteWithoutChat(uid, proto);
+
+ TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
}
///
/// Makes selected entity to emote using without sending any messages to chat.
///
- public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto)
+ public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
{
- if (!_actionBlocker.CanEmote(uid))
+ if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
return;
InvokeEmoteEvent(uid, proto);
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index 7b874e54da..856a4b4b79 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -137,10 +137,17 @@ public sealed partial class ChatSystem : SharedChatSystem
///
/// The player doing the speaking
/// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised.
- public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, bool hideChat, bool hideLog = false,
- IConsoleShell? shell = null, IPlayerSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true)
+ public void TrySendInGameICMessage(
+ EntityUid source,
+ string message,
+ InGameICChatType desiredType,
+ bool hideChat, bool hideLog = false,
+ IConsoleShell? shell = null,
+ IPlayerSession? player = null, string? nameOverride = null,
+ bool checkRadioPrefix = true,
+ bool ignoreActionBlocker = false)
{
- TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, shell, player, nameOverride, checkRadioPrefix);
+ TrySendInGameICMessage(source, message, desiredType, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, hideLog, shell, player, nameOverride, checkRadioPrefix, ignoreActionBlocker);
}
///
@@ -153,8 +160,19 @@ public sealed partial class ChatSystem : SharedChatSystem
///
/// The player doing the speaking
/// The name to use for the speaking entity. Usually this should just be modified via . If this is set, the event will not get raised.
- public void TrySendInGameICMessage(EntityUid source, string message, InGameICChatType desiredType, ChatTransmitRange range, bool hideLog = false,
- IConsoleShell? shell = null, IPlayerSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true)
+ /// If set to true, action blocker will not be considered for whether an entity can send this message.
+ public void TrySendInGameICMessage(
+ EntityUid source,
+ string message,
+ InGameICChatType desiredType,
+ ChatTransmitRange range,
+ bool hideLog = false,
+ IConsoleShell? shell = null,
+ IPlayerSession? player = null,
+ string? nameOverride = null,
+ bool checkRadioPrefix = true,
+ bool ignoreActionBlocker = false
+ )
{
if (HasComp(source))
{
@@ -187,7 +205,7 @@ public sealed partial class ChatSystem : SharedChatSystem
// Was there an emote in the message? If so, send it.
if (player != null && emoteStr != message && emoteStr != null)
{
- SendEntityEmote(source, emoteStr, range, nameOverride);
+ SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker);
}
// This can happen if the entire string is sanitized out.
@@ -199,7 +217,7 @@ public sealed partial class ChatSystem : SharedChatSystem
{
if (TryProccessRadioMessage(source, message, out var modMessage, out var channel))
{
- SendEntityWhisper(source, modMessage, range, channel, nameOverride);
+ SendEntityWhisper(source, modMessage, range, channel, nameOverride, ignoreActionBlocker);
return;
}
}
@@ -208,19 +226,25 @@ public sealed partial class ChatSystem : SharedChatSystem
switch (desiredType)
{
case InGameICChatType.Speak:
- SendEntitySpeak(source, message, range, nameOverride, hideLog);
+ SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker);
break;
case InGameICChatType.Whisper:
- SendEntityWhisper(source, message, range, null, nameOverride, hideLog);
+ SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker);
break;
case InGameICChatType.Emote:
- SendEntityEmote(source, message, range, nameOverride, hideLog);
+ SendEntityEmote(source, message, range, nameOverride, hideLog, ignoreActionBlocker);
break;
}
}
- public void TrySendInGameOOCMessage(EntityUid source, string message, InGameOOCChatType type, bool hideChat,
- IConsoleShell? shell = null, IPlayerSession? player = null)
+ public void TrySendInGameOOCMessage(
+ EntityUid source,
+ string message,
+ InGameOOCChatType type,
+ bool hideChat,
+ IConsoleShell? shell = null,
+ IPlayerSession? player = null
+ )
{
if (!CanSendInGame(message, shell, player))
return;
@@ -262,8 +286,13 @@ public sealed partial class ChatSystem : SharedChatSystem
/// The sender (Communications Console in Communications Console Announcement)
/// Play the announcement sound
/// Optional color for the announcement message
- public void DispatchGlobalAnnouncement(string message, string sender = "Central Command",
- bool playSound = true, SoundSpecifier? announcementSound = null, Color? colorOverride = null)
+ public void DispatchGlobalAnnouncement(
+ string message,
+ string sender = "Central Command",
+ bool playSound = true,
+ SoundSpecifier? announcementSound = null,
+ Color? colorOverride = null
+ )
{
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
@@ -282,8 +311,13 @@ public sealed partial class ChatSystem : SharedChatSystem
/// The sender (Communications Console in Communications Console Announcement)
/// Play the announcement sound
/// Optional color for the announcement message
- public void DispatchStationAnnouncement(EntityUid source, string message, string sender = "Central Command",
- bool playDefaultSound = true, SoundSpecifier? announcementSound = null, Color? colorOverride = null)
+ public void DispatchStationAnnouncement(
+ EntityUid source,
+ string message,
+ string sender = "Central Command",
+ bool playDefaultSound = true,
+ SoundSpecifier? announcementSound = null,
+ Color? colorOverride = null)
{
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
var station = _stationSystem.GetOwningStation(source);
@@ -312,9 +346,16 @@ public sealed partial class ChatSystem : SharedChatSystem
#region Private API
- private void SendEntitySpeak(EntityUid source, string originalMessage, ChatTransmitRange range, string? nameOverride, bool hideLog = false)
+ private void SendEntitySpeak(
+ EntityUid source,
+ string originalMessage,
+ ChatTransmitRange range,
+ string? nameOverride,
+ bool hideLog = false,
+ bool ignoreActionBlocker = false
+ )
{
- if (!_actionBlocker.CanSpeak(source))
+ if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
var message = TransformSpeech(source, originalMessage);
@@ -366,9 +407,17 @@ public sealed partial class ChatSystem : SharedChatSystem
}
}
- private void SendEntityWhisper(EntityUid source, string originalMessage, ChatTransmitRange range, RadioChannelPrototype? channel, string? nameOverride, bool hideLog = false)
+ private void SendEntityWhisper(
+ EntityUid source,
+ string originalMessage,
+ ChatTransmitRange range,
+ RadioChannelPrototype? channel,
+ string? nameOverride,
+ bool hideLog = false,
+ bool ignoreActionBlocker = false
+ )
{
- if (!_actionBlocker.CanSpeak(source))
+ if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
var message = TransformSpeech(source, originalMessage);
@@ -449,16 +498,27 @@ public sealed partial class ChatSystem : SharedChatSystem
}
}
- private void SendEntityEmote(EntityUid source, string action, ChatTransmitRange range, string? nameOverride, bool hideLog = false, bool checkEmote = true)
+ private void SendEntityEmote(
+ EntityUid source,
+ string action,
+ ChatTransmitRange range,
+ string? nameOverride,
+ bool hideLog = false,
+ bool checkEmote = true,
+ bool ignoreActionBlocker = false
+ )
{
- if (!_actionBlocker.CanEmote(source)) return;
+ if (!_actionBlocker.CanEmote(source) && !ignoreActionBlocker)
+ return;
// get the entity's apparent name (if no override provided).
- string name = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
+ var ent = Identity.Entity(source, EntityManager);
+ string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
+ ("entity", ent),
("message", FormattedMessage.EscapeText(action)));
if (checkEmote)
diff --git a/Content.Server/Mobs/CritMobActionsSystem.cs b/Content.Server/Mobs/CritMobActionsSystem.cs
new file mode 100644
index 0000000000..47da6961e7
--- /dev/null
+++ b/Content.Server/Mobs/CritMobActionsSystem.cs
@@ -0,0 +1,98 @@
+using Content.Server.Administration;
+using Content.Server.Chat.Systems;
+using Content.Server.GameTicking;
+using Content.Server.Mind.Components;
+using Content.Shared.Actions;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Mobs.Systems;
+using Robust.Server.Console;
+using Robust.Server.GameObjects;
+using System;
+
+namespace Content.Server.Mobs;
+
+///
+/// Handles performing crit-specific actions.
+///
+public sealed class CritMobActionsSystem : EntitySystem
+{
+ [Dependency] private readonly ChatSystem _chat = default!;
+ [Dependency] private readonly DeathgaspSystem _deathgasp = default!;
+ [Dependency] private readonly IServerConsoleHost _host = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly QuickDialogSystem _quickDialog = default!;
+
+ private const int MaxLastWordsLength = 30;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnSuccumb);
+ SubscribeLocalEvent(OnFakeDeath);
+ SubscribeLocalEvent(OnLastWords);
+ }
+
+ private void OnSuccumb(EntityUid uid, MobStateActionsComponent component, CritSuccumbEvent args)
+ {
+ if (!TryComp(uid, out var actor) || !_mobState.IsCritical(uid))
+ return;
+
+ _host.ExecuteCommand(actor.PlayerSession, "ghost");
+ args.Handled = true;
+ }
+
+ private void OnFakeDeath(EntityUid uid, MobStateActionsComponent component, CritFakeDeathEvent args)
+ {
+ if (!_mobState.IsCritical(uid))
+ return;
+
+ args.Handled = _deathgasp.Deathgasp(uid);
+ }
+
+ private void OnLastWords(EntityUid uid, MobStateActionsComponent component, CritLastWordsEvent args)
+ {
+ if (!TryComp(uid, out var actor))
+ return;
+
+ _quickDialog.OpenDialog(actor.PlayerSession, Loc.GetString("action-name-crit-last-words"), "",
+ (string lastWords) =>
+ {
+ if (actor.PlayerSession.AttachedEntity != uid
+ || !_mobState.IsCritical(uid))
+ return;
+
+ if (lastWords.Length > MaxLastWordsLength)
+ {
+ lastWords = lastWords.Substring(0, MaxLastWordsLength);
+ }
+ lastWords += "...";
+
+ _chat.TrySendInGameICMessage(uid, lastWords, InGameICChatType.Whisper, ChatTransmitRange.Normal, ignoreActionBlocker: true);
+ _host.ExecuteCommand(actor.PlayerSession, "ghost");
+ });
+
+ args.Handled = true;
+ }
+}
+
+///
+/// Only applies to mobs in crit capable of ghosting/succumbing
+///
+public sealed class CritSuccumbEvent : InstantActionEvent
+{
+}
+
+///
+/// Only applies/has functionality to mobs in crit that have
+///
+public sealed class CritFakeDeathEvent : InstantActionEvent
+{
+}
+
+///
+/// Only applies to mobs capable of speaking, as a last resort in crit
+///
+public sealed class CritLastWordsEvent : InstantActionEvent
+{
+}
diff --git a/Content.Server/Mobs/DeathgaspComponent.cs b/Content.Server/Mobs/DeathgaspComponent.cs
new file mode 100644
index 0000000000..01d9c321c9
--- /dev/null
+++ b/Content.Server/Mobs/DeathgaspComponent.cs
@@ -0,0 +1,18 @@
+using Content.Shared.Chat.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Mobs;
+
+///
+/// Mobs with this component will emote a deathgasp when they die.
+///
+///
+[RegisterComponent]
+public sealed class DeathgaspComponent : Component
+{
+ ///
+ /// The emote prototype to use.
+ ///
+ [DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer))]
+ public string Prototype = "DefaultDeathgasp";
+}
diff --git a/Content.Server/Mobs/DeathgaspSystem.cs b/Content.Server/Mobs/DeathgaspSystem.cs
new file mode 100644
index 0000000000..ef1cf24932
--- /dev/null
+++ b/Content.Server/Mobs/DeathgaspSystem.cs
@@ -0,0 +1,40 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.Mobs;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Mobs;
+
+///
+public sealed class DeathgaspSystem: EntitySystem
+{
+ [Dependency] private readonly ChatSystem _chat = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMobStateChanged);
+ }
+
+ private void OnMobStateChanged(EntityUid uid, DeathgaspComponent component, MobStateChangedEvent args)
+ {
+ // don't deathgasp if they arent going straight from crit to dead
+ if (args.NewMobState != MobState.Dead || args.OldMobState != MobState.Critical)
+ return;
+
+ Deathgasp(uid, component);
+ }
+
+ ///
+ /// Causes an entity to perform their deathgasp emote, if they have one.
+ ///
+ public bool Deathgasp(EntityUid uid, DeathgaspComponent? component = null)
+ {
+ if (!Resolve(uid, ref component, false))
+ return false;
+
+ _chat.TryEmoteWithChat(uid, component.Prototype, ignoreActionBlocker: true);
+
+ return true;
+ }
+}
diff --git a/Content.Shared/Mobs/Components/MobStateActionsComponent.cs b/Content.Shared/Mobs/Components/MobStateActionsComponent.cs
new file mode 100644
index 0000000000..7e9ef6f220
--- /dev/null
+++ b/Content.Shared/Mobs/Components/MobStateActionsComponent.cs
@@ -0,0 +1,27 @@
+using Content.Shared.Mobs.Systems;
+
+namespace Content.Shared.Mobs.Components;
+
+///
+/// Used for specifying actions that should be automatically added/removed on mob state transitions
+///
+///
+/// Mostly for crit-specific actions.
+///
+///
+[RegisterComponent]
+public sealed class MobStateActionsComponent : Component
+{
+ ///
+ /// Specifies a list of actions that should be available if a mob is in a given state.
+ ///
+ ///
+ /// actions:
+ /// Critical:
+ /// - CritSuccumb
+ /// Alive:
+ /// - AnimalLayEgg
+ ///
+ [DataField("actions")]
+ public Dictionary> Actions = new();
+}
diff --git a/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs b/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs
new file mode 100644
index 0000000000..b25babfe06
--- /dev/null
+++ b/Content.Shared/Mobs/Systems/MobStateActionsSystem.cs
@@ -0,0 +1,54 @@
+using Content.Shared.Actions;
+using Content.Shared.Actions.ActionTypes;
+using Content.Shared.Mobs.Components;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Mobs.Systems;
+
+///
+/// Adds and removes defined actions when a mob's changes.
+///
+public sealed class MobStateActionsSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMobStateChanged);
+ }
+
+ private void OnMobStateChanged(EntityUid uid, MobStateActionsComponent component, MobStateChangedEvent args)
+ {
+ if (!TryComp(uid, out var action))
+ return;
+
+ foreach (var (state, acts) in component.Actions)
+ {
+ if (state != args.NewMobState && state != args.OldMobState)
+ continue;
+
+ foreach (var item in acts)
+ {
+ if (!_proto.TryIndex(item, out var proto))
+ continue;
+
+ var instance = new InstantAction(proto);
+ if (state == args.OldMobState)
+ {
+ // Don't remove actions that would be getting readded anyway
+ if (component.Actions.TryGetValue(args.NewMobState, out var value)
+ && value.Contains(item))
+ continue;
+
+ _actions.RemoveAction(uid, instance, action);
+ }
+ else if (state == args.NewMobState)
+ {
+ _actions.AddAction(uid, instance, null, action);
+ }
+ }
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/actions/actions/crit.ftl b/Resources/Locale/en-US/actions/actions/crit.ftl
new file mode 100644
index 0000000000..1588c0ed7e
--- /dev/null
+++ b/Resources/Locale/en-US/actions/actions/crit.ftl
@@ -0,0 +1,9 @@
+action-name-crit-succumb = Succumb
+action-description-crit-succumb = Accept your fate.
+
+action-name-crit-fake-death = Fake Death
+action-description-crit-fake-death = Pretend to take your final breath while staying alive.
+
+action-name-crit-last-words = Say Last Words
+action-description-crit-last-words = Whisper your last words to anyone nearby, and then succumb to your fate. You only have 30 characters to work with.
+
diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl
index 2998dc8361..7c917ae939 100644
--- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl
+++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl
@@ -11,25 +11,36 @@ chat-manager-crit-looc-chat-enabled-message = Crit players can now use LOOC.
chat-manager-crit-looc-chat-disabled-message = Crit players can no longer use LOOC.
chat-manager-admin-ooc-chat-enabled-message = Admin OOC chat has been enabled.
chat-manager-admin-ooc-chat-disabled-message = Admin OOC chat has been disabled.
+
chat-manager-max-message-length-exceeded-message = Your message exceeded {$limit} character limit
chat-manager-no-headset-on-message = You don't have a headset on!
chat-manager-no-radio-key = No radio key specified!
chat-manager-no-such-channel = There is no channel with key '{$key}'!
chat-manager-whisper-headset-on-message = You can't whisper on the radio!
+
chat-manager-server-wrap-message = [bold]{$message}[/bold]
chat-manager-sender-announcement-wrap-message = [font size=14][bold]{$sender} Announcement:[/font][font size=12]
{$message}[/bold][/font]
chat-manager-entity-say-wrap-message = [bold]{$entityName}[/bold] says, "{$message}"
+
chat-manager-entity-whisper-wrap-message = [font size=11][italic]{$entityName} whispers, "{$message}"[/italic][/font]
chat-manager-entity-whisper-unknown-wrap-message = [font size=11][italic]Someone whispers, "{$message}"[/italic][/font]
-chat-manager-entity-me-wrap-message = [italic]{$entityName} {$message}[/italic]
+
+# THE() is not used here because the entity and its name can technically be disconnected if a nameOverride is passed...
+chat-manager-entity-me-wrap-message = [italic]{ PROPER($entity) ->
+ *[false] the {$entityName} {$message}[/italic]
+ [true] {$entityName} {$message}[/italic]
+ }
+
chat-manager-entity-looc-wrap-message = LOOC: [bold]{$entityName}:[/bold] {$message}
chat-manager-send-ooc-wrap-message = OOC: [bold]{$playerName}:[/bold] {$message}
chat-manager-send-ooc-patron-wrap-message = OOC: [bold][color={$patronColor}]{$playerName}[/color]:[/bold] {$message}
+
chat-manager-send-dead-chat-wrap-message = {$deadChannelName}: [bold]{$playerName}:[/bold] {$message}
chat-manager-send-admin-dead-chat-wrap-message = {$adminChannelName}: [bold]({$userName}):[/bold] {$message}
chat-manager-send-admin-chat-wrap-message = {$adminChannelName}: [bold]{$playerName}:[/bold] {$message}
chat-manager-send-admin-announcement-wrap-message = [bold]{$adminChannelName}: {$message}[/bold]
+
chat-manager-send-hook-ooc-wrap-message = OOC: [bold](D){$senderName}:[/bold] {$message}
chat-manager-dead-channel-name = DEAD
diff --git a/Resources/Locale/en-US/emotes/emotes.ftl b/Resources/Locale/en-US/emotes/emotes.ftl
new file mode 100644
index 0000000000..53c12312e5
--- /dev/null
+++ b/Resources/Locale/en-US/emotes/emotes.ftl
@@ -0,0 +1 @@
+emote-deathgasp = seizes up and falls limp, {POSS-ADJ($entity)} eyes dead and lifeless...
diff --git a/Resources/Prototypes/Actions/crit.yml b/Resources/Prototypes/Actions/crit.yml
new file mode 100644
index 0000000000..23a058c017
--- /dev/null
+++ b/Resources/Prototypes/Actions/crit.yml
@@ -0,0 +1,34 @@
+# Actions added to mobs in crit.
+- type: instantAction
+ id: CritSuccumb
+ name: action-name-crit-succumb
+ description: action-description-crit-succumb
+ itemIconStyle: NoItem
+ checkCanInteract: false
+ icon:
+ sprite: Mobs/Ghosts/ghost_human.rsi
+ state: icon
+ serverEvent: !type:CritSuccumbEvent
+
+- type: instantAction
+ id: CritFakeDeath
+ name: action-name-crit-fake-death
+ description: action-description-crit-fake-death
+ itemIconStyle: NoItem
+ checkCanInteract: false
+ icon:
+ sprite: Interface/Actions/actions_crit.rsi
+ state: fakedeath
+ serverEvent: !type:CritFakeDeathEvent
+ useDelay: 30
+
+- type: instantAction
+ id: CritLastWords
+ name: action-name-crit-last-words
+ description: action-description-crit-last-words
+ itemIconStyle: NoItem
+ checkCanInteract: false
+ icon:
+ sprite: Interface/Actions/actions_crit.rsi
+ state: lastwords
+ serverEvent: !type:CritLastWordsEvent
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index 18e1ae41e1..5f64b7a8c0 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -933,6 +933,13 @@
layer:
- SmallMobLayer
- type: MobState
+ - type: Deathgasp
+ - type: MobStateActions
+ actions:
+ Critical:
+ - CritSuccumb
+ - CritFakeDeath
+ - CritLastWords
- type: MobThresholds
thresholds:
0: Alive
@@ -2233,6 +2240,13 @@
layer:
- SmallMobLayer
- type: MobState
+ - type: Deathgasp
+ - type: MobStateActions
+ actions:
+ Critical:
+ - CritSuccumb
+ - CritFakeDeath
+ - CritLastWords
- type: MobThresholds
thresholds:
0: Alive
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
index 110e17bae0..887a800e7a 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml
@@ -91,6 +91,13 @@
types:
Heat : 0.1 #per second, scales with temperature & other constants
- type: MobState
+ - type: Deathgasp
+ - type: MobStateActions
+ actions:
+ Critical:
+ - CritSuccumb
+ - CritFakeDeath
+ - CritLastWords
- type: MobThresholds
thresholds:
0: Alive
diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml
index f1b15bcdea..60b23942f4 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml
@@ -72,6 +72,11 @@
layer:
- FlyingMobLayer
- type: MobState
+ - type: MobStateActions
+ actions:
+ Critical:
+ - CritSuccumb
+ - CritLastWords
- type: MobThresholds
thresholds:
0: Alive
diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml
index 623f8eb3ab..5dfe46021f 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/base.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml
@@ -198,6 +198,13 @@
thermalRegulationTemperatureThreshold: 25
- type: Internals
- type: MobState
+ - type: Deathgasp
+ - type: MobStateActions
+ actions:
+ Critical:
+ - CritSuccumb
+ - CritFakeDeath
+ - CritLastWords
- type: MobThresholds
thresholds:
0: Alive
diff --git a/Resources/Prototypes/Voice/speech_emotes.yml b/Resources/Prototypes/Voice/speech_emotes.yml
index c309b373c8..ca8b9d5502 100644
--- a/Resources/Prototypes/Voice/speech_emotes.yml
+++ b/Resources/Prototypes/Voice/speech_emotes.yml
@@ -124,7 +124,7 @@
- chittered
- chittered.
- chittered!
-
+
- type: emote
id: Squeak
category: Vocal
@@ -184,3 +184,8 @@
- salutes.
- salutes!
+- type: emote
+ id: DefaultDeathgasp
+ chatMessages: ["emote-deathgasp"]
+ chatTriggers:
+ - deathgasp
diff --git a/Resources/Textures/Interface/Actions/actions_crit.rsi/fakedeath.png b/Resources/Textures/Interface/Actions/actions_crit.rsi/fakedeath.png
new file mode 100644
index 0000000000..b24782634b
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_crit.rsi/fakedeath.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_crit.rsi/lastwords.png b/Resources/Textures/Interface/Actions/actions_crit.rsi/lastwords.png
new file mode 100644
index 0000000000..eddc23d3c8
Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_crit.rsi/lastwords.png differ
diff --git a/Resources/Textures/Interface/Actions/actions_crit.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_crit.rsi/meta.json
new file mode 100644
index 0000000000..402feadd54
--- /dev/null
+++ b/Resources/Textures/Interface/Actions/actions_crit.rsi/meta.json
@@ -0,0 +1,17 @@
+{
+ "version": 1,
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Created by mirrorcult for SS14, derivative of other licensed sprites in the repo",
+ "states": [
+ {
+ "name": "lastwords"
+ },
+ {
+ "name": "fakedeath"
+ }
+ ]
+}
diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings
index dbb20970b1..4ea4afc413 100644
--- a/SpaceStation14.sln.DotSettings
+++ b/SpaceStation14.sln.DotSettings
@@ -57,6 +57,7 @@
GD
GL
HW
+ IC
IL
IP
KHR