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