Audible emotes (#12708)
Co-authored-by: Visne <39844191+Visne@users.noreply.github.com>
This commit is contained in:
173
Content.Server/Chat/Systems/ChatSystem.Emote.cs
Normal file
173
Content.Server/Chat/Systems/ChatSystem.Emote.cs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Chat.Systems;
|
||||||
|
|
||||||
|
// emotes using emote prototype
|
||||||
|
public partial class ChatSystem
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, EmotePrototype> _wordEmoteDict = new();
|
||||||
|
|
||||||
|
private void InitializeEmotes()
|
||||||
|
{
|
||||||
|
_prototypeManager.PrototypesReloaded += OnPrototypeReloadEmotes;
|
||||||
|
CacheEmotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShutdownEmotes()
|
||||||
|
{
|
||||||
|
_prototypeManager.PrototypesReloaded -= OnPrototypeReloadEmotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPrototypeReloadEmotes(PrototypesReloadedEventArgs obj)
|
||||||
|
{
|
||||||
|
CacheEmotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CacheEmotes()
|
||||||
|
{
|
||||||
|
_wordEmoteDict.Clear();
|
||||||
|
var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
|
||||||
|
foreach (var emote in emotes)
|
||||||
|
{
|
||||||
|
foreach (var word in emote.ChatTriggers)
|
||||||
|
{
|
||||||
|
var lowerWord = word.ToLower();
|
||||||
|
if (_wordEmoteDict.ContainsKey(lowerWord))
|
||||||
|
{
|
||||||
|
var existingId = _wordEmoteDict[lowerWord].ID;
|
||||||
|
var errMsg = $"Duplicate of emote word {lowerWord} in emotes {emote.ID} and {existingId}";
|
||||||
|
Logger.Error(errMsg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wordEmoteDict.Add(lowerWord, emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The entity that is speaking</param>
|
||||||
|
/// <param name="emoteId">The id of emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param>
|
||||||
|
/// <param name="hideChat">Whether or not this message should appear in the chat window</param>
|
||||||
|
/// <param name="hideGlobalGhostChat">Whether or not this message should appear in the chat window for out-of-range ghosts (which otherwise ignore range restrictions)</param>
|
||||||
|
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||||
|
public void TryEmoteWithChat(EntityUid source, string emoteId, bool hideChat = false,
|
||||||
|
bool hideGlobalGhostChat = false, string? nameOverride = null)
|
||||||
|
{
|
||||||
|
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
|
||||||
|
return;
|
||||||
|
TryEmoteWithChat(source, proto, hideChat, hideGlobalGhostChat, nameOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes selected entity to emote using <see cref="EmotePrototype"/> and sends message to chat.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The entity that is speaking</param>
|
||||||
|
/// <param name="emote">The emote prototype. Should has valid <see cref="EmotePrototype.ChatMessages"/></param>
|
||||||
|
/// <param name="hideChat">Whether or not this message should appear in the chat window</param>
|
||||||
|
/// <param name="hideGlobalGhostChat">Whether or not this message should appear in the chat window for out-of-range ghosts (which otherwise ignore range restrictions)</param>
|
||||||
|
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||||
|
public void TryEmoteWithChat(EntityUid source, EmotePrototype emote, bool hideChat = false,
|
||||||
|
bool hideGlobalGhostChat = false, string? nameOverride = null)
|
||||||
|
{
|
||||||
|
// check if proto has valid message for chat
|
||||||
|
if (emote.ChatMessages.Count != 0)
|
||||||
|
{
|
||||||
|
var action = _random.Pick(emote.ChatMessages);
|
||||||
|
SendEntityEmote(source, action, hideChat, hideGlobalGhostChat, nameOverride, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// do the rest of emote event logic here
|
||||||
|
TryEmoteWithoutChat(source, emote);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||||
|
/// </summary>
|
||||||
|
public void TryEmoteWithoutChat(EntityUid uid, string emoteId)
|
||||||
|
{
|
||||||
|
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
|
||||||
|
return;
|
||||||
|
TryEmoteWithoutChat(uid, proto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||||
|
/// </summary>
|
||||||
|
public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto)
|
||||||
|
{
|
||||||
|
if (!_actionBlocker.CanEmote(uid))
|
||||||
|
return;
|
||||||
|
|
||||||
|
InvokeEmoteEvent(uid, proto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to find and play relevant emote sound in emote sounds collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if emote sound was played.</returns>
|
||||||
|
public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, EmotePrototype emote)
|
||||||
|
{
|
||||||
|
return TryPlayEmoteSound(uid, proto, emote.ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to find and play relevant emote sound in emote sounds collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if emote sound was played.</returns>
|
||||||
|
public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string emoteId)
|
||||||
|
{
|
||||||
|
if (proto == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// try to get specific sound for this emote
|
||||||
|
if (!proto.Sounds.TryGetValue(emoteId, out var sound))
|
||||||
|
{
|
||||||
|
// no specific sound - check fallback
|
||||||
|
sound = proto.FallbackSound;
|
||||||
|
if (sound == null)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if general params for all sounds set - use them
|
||||||
|
var param = proto.GeneralParams ?? sound.Params;
|
||||||
|
_audio.PlayPvs(sound, uid, param);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryEmoteChatInput(EntityUid uid, string textInput)
|
||||||
|
{
|
||||||
|
var actionLower = textInput.ToLower();
|
||||||
|
if (!_wordEmoteDict.TryGetValue(actionLower, out var emote))
|
||||||
|
return;
|
||||||
|
|
||||||
|
InvokeEmoteEvent(uid, emote);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
|
||||||
|
{
|
||||||
|
var ev = new EmoteEvent(proto);
|
||||||
|
RaiseLocalEvent(uid, ref ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised by chat system when entity made some emote.
|
||||||
|
/// Use it to play sound, change sprite or something else.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public struct EmoteEvent
|
||||||
|
{
|
||||||
|
public bool Handled;
|
||||||
|
public readonly EmotePrototype Emote;
|
||||||
|
|
||||||
|
public EmoteEvent(EmotePrototype emote)
|
||||||
|
{
|
||||||
|
Emote = emote;
|
||||||
|
Handled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
[Dependency] private readonly PopupSystem _popup = default!;
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
|
||||||
public const int VoiceRange = 10; // how far voice goes in world units
|
public const int VoiceRange = 10; // how far voice goes in world units
|
||||||
public const int WhisperRange = 2; // how far whisper goes in world units
|
public const int WhisperRange = 2; // how far whisper goes in world units
|
||||||
@@ -63,7 +64,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
|
base.Initialize();
|
||||||
InitializeRadio();
|
InitializeRadio();
|
||||||
|
InitializeEmotes();
|
||||||
_configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true);
|
_configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true);
|
||||||
_configurationManager.OnValueChanged(CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true);
|
_configurationManager.OnValueChanged(CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true);
|
||||||
|
|
||||||
@@ -72,7 +75,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
|
|
||||||
public override void Shutdown()
|
public override void Shutdown()
|
||||||
{
|
{
|
||||||
|
base.Shutdown();
|
||||||
ShutdownRadio();
|
ShutdownRadio();
|
||||||
|
ShutdownEmotes();
|
||||||
_configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged);
|
_configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +371,8 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {ToPrettyString(source):user}, original: {originalMessage}, transformed: {message}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SendEntityEmote(EntityUid source, string action, bool hideChat, bool hideGlobalGhostChat, string? nameOverride)
|
private void SendEntityEmote(EntityUid source, string action, bool hideChat,
|
||||||
|
bool hideGlobalGhostChat, string? nameOverride, bool checkEmote = true)
|
||||||
{
|
{
|
||||||
if (!_actionBlocker.CanEmote(source)) return;
|
if (!_actionBlocker.CanEmote(source)) return;
|
||||||
|
|
||||||
@@ -378,6 +384,8 @@ public sealed partial class ChatSystem : SharedChatSystem
|
|||||||
("entityName", name),
|
("entityName", name),
|
||||||
("message", FormattedMessage.EscapeText(action)));
|
("message", FormattedMessage.EscapeText(action)));
|
||||||
|
|
||||||
|
if (checkEmote)
|
||||||
|
TryEmoteChatInput(source, action);
|
||||||
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, hideChat, hideGlobalGhostChat);
|
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, hideChat, hideGlobalGhostChat);
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user}: {action}");
|
||||||
}
|
}
|
||||||
|
|||||||
33
Content.Server/Chemistry/ReagentEffects/Emote.cs
Normal file
33
Content.Server/Chemistry/ReagentEffects/Emote.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
|
using Content.Shared.Chemistry.Reagent;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Server.Chemistry.ReagentEffects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to force someone to emote (scream, laugh, etc).
|
||||||
|
/// </summary>
|
||||||
|
[UsedImplicitly]
|
||||||
|
public sealed class Emote : ReagentEffect
|
||||||
|
{
|
||||||
|
[DataField("emote", customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
|
||||||
|
public string? EmoteId;
|
||||||
|
|
||||||
|
[DataField("showInChat")]
|
||||||
|
public bool ShowInChat;
|
||||||
|
|
||||||
|
public override void Effect(ReagentEffectArgs args)
|
||||||
|
{
|
||||||
|
if (EmoteId == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var chatSys = args.EntityManager.System<ChatSystem>();
|
||||||
|
if (ShowInChat)
|
||||||
|
chatSys.TryEmoteWithChat(args.SolutionEntity, EmoteId, hideGlobalGhostChat: true);
|
||||||
|
else
|
||||||
|
chatSys.TryEmoteWithoutChat(args.SolutionEntity, EmoteId);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Content.Server.Speech;
|
|
||||||
using Content.Shared.Chemistry.Reagent;
|
|
||||||
|
|
||||||
namespace Content.Server.Chemistry.ReagentEffects;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Forces someone to scream their lungs out.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class Scream : ReagentEffect
|
|
||||||
{
|
|
||||||
public override void Effect(ReagentEffectArgs args)
|
|
||||||
{
|
|
||||||
EntitySystem.Get<VocalSystem>().TryScream(args.SolutionEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
Content.Server/Emoting/Components/BodyEmotesComponent.cs
Normal file
24
Content.Server/Emoting/Components/BodyEmotesComponent.cs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
using Content.Server.Emoting.Systems;
|
||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
|
||||||
|
namespace Content.Server.Emoting.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component required for entities to be able to do body emotions (clap, flip, etc).
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
[Access(typeof(BodyEmotesSystem))]
|
||||||
|
public sealed class BodyEmotesComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Emote sounds prototype id for body emotes.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("soundsId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
|
||||||
|
public string? SoundsId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loaded emote sounds prototype used for body emotes.
|
||||||
|
/// </summary>
|
||||||
|
public EmoteSoundsPrototype? Sounds;
|
||||||
|
}
|
||||||
48
Content.Server/Emoting/Systems/BodyEmotesSystem.cs
Normal file
48
Content.Server/Emoting/Systems/BodyEmotesSystem.cs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Server.Emoting.Components;
|
||||||
|
using Content.Server.Hands.Components;
|
||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.Emoting.Systems;
|
||||||
|
|
||||||
|
public sealed class BodyEmotesSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||||
|
[Dependency] private readonly ChatSystem _chat = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
SubscribeLocalEvent<BodyEmotesComponent, ComponentStartup>(OnStartup);
|
||||||
|
SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(OnEmote);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStartup(EntityUid uid, BodyEmotesComponent component, ComponentStartup args)
|
||||||
|
{
|
||||||
|
if (component.SoundsId == null)
|
||||||
|
return;
|
||||||
|
_proto.TryIndex(component.SoundsId, out component.Sounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEmote(EntityUid uid, BodyEmotesComponent component, ref EmoteEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var cat = args.Emote.Category;
|
||||||
|
if (cat.HasFlag(EmoteCategory.Hands))
|
||||||
|
{
|
||||||
|
args.Handled = TryEmoteHands(uid, args.Emote, component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryEmoteHands(EntityUid uid, EmotePrototype emote, BodyEmotesComponent component)
|
||||||
|
{
|
||||||
|
// check that user actually has hands to do emote sound
|
||||||
|
if (!TryComp(uid, out HandsComponent? hands) || hands.Count <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _chat.TryPlayEmoteSound(uid, component.Sounds, emote);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
|
|||||||
}
|
}
|
||||||
|
|
||||||
SetSpecies(uid, profile.Species, false, humanoid);
|
SetSpecies(uid, profile.Species, false, humanoid);
|
||||||
humanoid.Sex = profile.Sex;
|
SetSex(uid, profile.Sex, false, humanoid);
|
||||||
humanoid.EyeColor = profile.Appearance.EyeColor;
|
humanoid.EyeColor = profile.Appearance.EyeColor;
|
||||||
|
|
||||||
SetSkinColor(uid, profile.Appearance.SkinColor, false);
|
SetSkinColor(uid, profile.Appearance.SkinColor, false);
|
||||||
@@ -121,7 +121,7 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
|
|||||||
|
|
||||||
targetHumanoid.Species = sourceHumanoid.Species;
|
targetHumanoid.Species = sourceHumanoid.Species;
|
||||||
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
|
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
|
||||||
targetHumanoid.Sex = sourceHumanoid.Sex;
|
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
|
||||||
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
|
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
|
||||||
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
|
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,53 @@
|
|||||||
|
using Content.Server.Humanoid;
|
||||||
|
using Content.Server.Speech.EntitySystems;
|
||||||
using Content.Shared.Actions;
|
using Content.Shared.Actions;
|
||||||
using Content.Shared.Actions.ActionTypes;
|
using Content.Shared.Actions.ActionTypes;
|
||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
|
using Content.Shared.Humanoid;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||||
|
|
||||||
namespace Content.Server.Speech.Components;
|
namespace Content.Server.Speech.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Component required for entities to be able to scream.
|
/// Component required for entities to be able to do vocal emotions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
|
[Access(typeof(VocalSystem))]
|
||||||
public sealed class VocalComponent : Component
|
public sealed class VocalComponent : Component
|
||||||
{
|
{
|
||||||
[DataField("maleScream")]
|
/// <summary>
|
||||||
public SoundSpecifier MaleScream = new SoundCollectionSpecifier("MaleScreams");
|
/// Emote sounds prototype id for each sex (not gender).
|
||||||
|
/// Entities without <see cref="HumanoidComponent"/> considered to be <see cref="Sex.Unsexed"/>.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("sounds", customTypeSerializer: typeof(PrototypeIdValueDictionarySerializer<Sex, EmoteSoundsPrototype>))]
|
||||||
|
public Dictionary<Sex, string>? Sounds;
|
||||||
|
|
||||||
[DataField("femaleScream")]
|
[DataField("screamId", customTypeSerializer: typeof(PrototypeIdSerializer<EmotePrototype>))]
|
||||||
public SoundSpecifier FemaleScream = new SoundCollectionSpecifier("FemaleScreams");
|
public string ScreamId = "Scream";
|
||||||
|
|
||||||
[DataField("unsexedScream")]
|
|
||||||
public SoundSpecifier UnsexedScream = new SoundCollectionSpecifier("MaleScreams");
|
|
||||||
|
|
||||||
[DataField("wilhelm")]
|
[DataField("wilhelm")]
|
||||||
public SoundSpecifier Wilhelm = new SoundPathSpecifier("/Audio/Voice/Human/wilhelm_scream.ogg");
|
public SoundSpecifier Wilhelm = new SoundPathSpecifier("/Audio/Voice/Human/wilhelm_scream.ogg");
|
||||||
|
|
||||||
[DataField("audioParams")]
|
|
||||||
public AudioParams AudioParams = AudioParams.Default.WithVolume(4f);
|
|
||||||
|
|
||||||
[DataField("wilhelmProbability")]
|
[DataField("wilhelmProbability")]
|
||||||
public float WilhelmProbability = 0.01f;
|
public float WilhelmProbability = 0.01f;
|
||||||
|
|
||||||
public const float Variation = 0.125f;
|
[DataField("screamActionId", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
|
||||||
|
public string ScreamActionId = "Scream";
|
||||||
|
|
||||||
[DataField("actionId", customTypeSerializer:typeof(PrototypeIdSerializer<InstantActionPrototype>))]
|
[DataField("screamAction")]
|
||||||
public string ActionId = "Scream";
|
public InstantAction? ScreamAction;
|
||||||
|
|
||||||
[DataField("action")] // must be a data-field to properly save cooldown when saving game state.
|
/// <summary>
|
||||||
public InstantAction? ScreamAction = null;
|
/// Currently loaded emote sounds prototype, based on entity sex.
|
||||||
|
/// Null if no valid prototype for entity sex was found.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables]
|
||||||
|
public EmoteSoundsPrototype? EmoteSounds = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ScreamActionEvent : InstantActionEvent { };
|
public sealed class ScreamActionEvent : InstantActionEvent
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
105
Content.Server/Speech/EntitySystems/VocalSystem.cs
Normal file
105
Content.Server/Speech/EntitySystems/VocalSystem.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using Content.Server.Actions;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Server.Humanoid;
|
||||||
|
using Content.Server.Speech.Components;
|
||||||
|
using Content.Shared.Actions.ActionTypes;
|
||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
|
using Content.Shared.Humanoid;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Speech.EntitySystems;
|
||||||
|
|
||||||
|
public sealed class VocalSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly ChatSystem _chat = default!;
|
||||||
|
[Dependency] private readonly ActionsSystem _actions = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<VocalComponent, MapInitEvent>(OnMapInit);
|
||||||
|
SubscribeLocalEvent<VocalComponent, ComponentShutdown>(OnShutdown);
|
||||||
|
SubscribeLocalEvent<VocalComponent, SexChangedEvent>(OnSexChanged);
|
||||||
|
SubscribeLocalEvent<VocalComponent, EmoteEvent>(OnEmote);
|
||||||
|
SubscribeLocalEvent<VocalComponent, ScreamActionEvent>(OnScreamAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMapInit(EntityUid uid, VocalComponent component, MapInitEvent args)
|
||||||
|
{
|
||||||
|
// try to add scream action when vocal comp added
|
||||||
|
if (_proto.TryIndex(component.ScreamActionId, out InstantActionPrototype? proto))
|
||||||
|
{
|
||||||
|
component.ScreamAction = new InstantAction(proto);
|
||||||
|
_actions.AddAction(uid, component.ScreamAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadSounds(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
|
||||||
|
{
|
||||||
|
// remove scream action when component removed
|
||||||
|
if (component.ScreamAction != null)
|
||||||
|
{
|
||||||
|
_actions.RemoveAction(uid, component.ScreamAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSexChanged(EntityUid uid, VocalComponent component, SexChangedEvent args)
|
||||||
|
{
|
||||||
|
LoadSounds(uid, component);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEmote(EntityUid uid, VocalComponent component, ref EmoteEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled || !args.Emote.Category.HasFlag(EmoteCategory.Vocal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// snowflake case for wilhelm scream easter egg
|
||||||
|
if (args.Emote.ID == component.ScreamId)
|
||||||
|
{
|
||||||
|
args.Handled = TryPlayScreamSound(uid, component);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just play regular sound based on emote proto
|
||||||
|
args.Handled = _chat.TryPlayEmoteSound(uid, component.EmoteSounds, args.Emote);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnScreamAction(EntityUid uid, VocalComponent component, ScreamActionEvent args)
|
||||||
|
{
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_chat.TryEmoteWithChat(uid, component.ScreamActionId);
|
||||||
|
args.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryPlayScreamSound(EntityUid uid, VocalComponent component)
|
||||||
|
{
|
||||||
|
if (_random.Prob(component.WilhelmProbability))
|
||||||
|
{
|
||||||
|
_audio.PlayPvs(component.Wilhelm, uid, component.Wilhelm.Params);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _chat.TryPlayEmoteSound(uid, component.EmoteSounds, component.ScreamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSounds(EntityUid uid, VocalComponent component, Sex? sex = null)
|
||||||
|
{
|
||||||
|
if (component.Sounds == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
sex ??= CompOrNull<HumanoidAppearanceComponent>(uid)?.Sex ?? Sex.Unsexed;
|
||||||
|
|
||||||
|
if (!component.Sounds.TryGetValue(sex.Value, out var protoId))
|
||||||
|
return;
|
||||||
|
_proto.TryIndex(protoId, out component.EmoteSounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
using Content.Server.Humanoid;
|
|
||||||
using Content.Server.Popups;
|
|
||||||
using Content.Server.Speech.Components;
|
|
||||||
using Content.Shared.ActionBlocker;
|
|
||||||
using Content.Shared.Actions;
|
|
||||||
using Content.Shared.Actions.ActionTypes;
|
|
||||||
using Content.Shared.Humanoid;
|
|
||||||
using Content.Shared.Popups;
|
|
||||||
using Robust.Shared.Audio;
|
|
||||||
using Robust.Shared.Player;
|
|
||||||
using Robust.Shared.Prototypes;
|
|
||||||
using Robust.Shared.Random;
|
|
||||||
|
|
||||||
namespace Content.Server.Speech;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fer Screamin
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Or I guess other vocalizations, like laughing. If fun is ever legalized on the station.
|
|
||||||
/// </remarks>
|
|
||||||
public sealed class VocalSystem : EntitySystem
|
|
||||||
{
|
|
||||||
[Dependency] private readonly IRobustRandom _random = default!;
|
|
||||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
|
||||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
|
||||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
|
||||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
|
||||||
{
|
|
||||||
base.Initialize();
|
|
||||||
|
|
||||||
SubscribeLocalEvent<VocalComponent, ScreamActionEvent>(OnActionPerform);
|
|
||||||
SubscribeLocalEvent<VocalComponent, MapInitEvent>(OnMapInit);
|
|
||||||
SubscribeLocalEvent<VocalComponent, ComponentShutdown>(OnShutdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnMapInit(EntityUid uid, VocalComponent component, MapInitEvent args)
|
|
||||||
{
|
|
||||||
if (component.ScreamAction == null
|
|
||||||
&& _proto.TryIndex(component.ActionId, out InstantActionPrototype? act))
|
|
||||||
{
|
|
||||||
component.ScreamAction = new(act);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (component.ScreamAction != null)
|
|
||||||
_actions.AddAction(uid, component.ScreamAction, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnShutdown(EntityUid uid, VocalComponent component, ComponentShutdown args)
|
|
||||||
{
|
|
||||||
if (component.ScreamAction != null)
|
|
||||||
_actions.RemoveAction(uid, component.ScreamAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnActionPerform(EntityUid uid, VocalComponent component, ScreamActionEvent args)
|
|
||||||
{
|
|
||||||
if (args.Handled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
args.Handled = TryScream(uid, component);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryScream(EntityUid uid, VocalComponent? component = null)
|
|
||||||
{
|
|
||||||
if (!Resolve(uid, ref component, false))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_blocker.CanSpeak(uid))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var sex = CompOrNull<HumanoidAppearanceComponent>(uid)?.Sex ?? Sex.Unsexed;
|
|
||||||
|
|
||||||
if (_random.Prob(component.WilhelmProbability))
|
|
||||||
{
|
|
||||||
SoundSystem.Play(component.Wilhelm.GetSound(), Filter.Pvs(uid), uid, component.AudioParams);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var scale = (float) _random.NextGaussian(1, VocalComponent.Variation);
|
|
||||||
var pitchedParams = component.AudioParams.WithPitchScale(scale);
|
|
||||||
|
|
||||||
switch (sex)
|
|
||||||
{
|
|
||||||
case Sex.Male:
|
|
||||||
SoundSystem.Play(component.MaleScream.GetSound(), Filter.Pvs(uid), uid, pitchedParams);
|
|
||||||
break;
|
|
||||||
case Sex.Female:
|
|
||||||
SoundSystem.Play(component.FemaleScream.GetSound(), Filter.Pvs(uid), uid, pitchedParams);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
SoundSystem.Play(component.UnsexedScream.GetSound(), Filter.Pvs(uid), uid, pitchedParams);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_popupSystem.PopupEntity(Loc.GetString("scream-action-popup"), uid, PopupType.Medium);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,6 +23,9 @@ public sealed class ActiveZombieComponent : Component
|
|||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public float RandomGroanAttempt = 5;
|
public float RandomGroanAttempt = 5;
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public string GroanEmoteId = "Scream";
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public float LastDamageGroanCooldown = 0f;
|
public float LastDamageGroanCooldown = 0f;
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ using Content.Server.Speech;
|
|||||||
using Content.Shared.Bed.Sleep;
|
using Content.Shared.Bed.Sleep;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Server.Emoting.Systems;
|
||||||
|
using Content.Server.Speech.EntitySystems;
|
||||||
|
using Content.Shared.Movement.Systems;
|
||||||
using Content.Shared.Bed.Sleep;
|
using Content.Shared.Bed.Sleep;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Disease.Events;
|
using Content.Shared.Disease.Events;
|
||||||
@@ -30,7 +33,6 @@ namespace Content.Server.Zombies
|
|||||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||||
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
|
[Dependency] private readonly ZombifyOnDeathSystem _zombify = default!;
|
||||||
[Dependency] private readonly ServerInventorySystem _inv = default!;
|
[Dependency] private readonly ServerInventorySystem _inv = default!;
|
||||||
[Dependency] private readonly VocalSystem _vocal = default!;
|
|
||||||
[Dependency] private readonly ChatSystem _chat = default!;
|
[Dependency] private readonly ChatSystem _chat = default!;
|
||||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||||
@@ -40,6 +42,10 @@ namespace Content.Server.Zombies
|
|||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<ZombieComponent, ComponentStartup>(OnStartup);
|
||||||
|
SubscribeLocalEvent<ZombieComponent, EmoteEvent>(OnEmote, before:
|
||||||
|
new []{typeof(VocalSystem), typeof(BodyEmotesSystem)});
|
||||||
|
|
||||||
SubscribeLocalEvent<ZombieComponent, MeleeHitEvent>(OnMeleeHit);
|
SubscribeLocalEvent<ZombieComponent, MeleeHitEvent>(OnMeleeHit);
|
||||||
SubscribeLocalEvent<ZombieComponent, MobStateChangedEvent>(OnMobState);
|
SubscribeLocalEvent<ZombieComponent, MobStateChangedEvent>(OnMobState);
|
||||||
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
||||||
@@ -54,6 +60,21 @@ namespace Content.Server.Zombies
|
|||||||
args.Cancelled = true;
|
args.Cancelled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnStartup(EntityUid uid, ZombieComponent component, ComponentStartup args)
|
||||||
|
{
|
||||||
|
if (component.EmoteSoundsId == null)
|
||||||
|
return;
|
||||||
|
_protoManager.TryIndex(component.EmoteSoundsId, out component.EmoteSounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnEmote(EntityUid uid, ZombieComponent component, ref EmoteEvent args)
|
||||||
|
{
|
||||||
|
// always play zombie emote sounds and ignore others
|
||||||
|
if (args.Handled)
|
||||||
|
return;
|
||||||
|
args.Handled = _chat.TryPlayEmoteSound(uid, component.EmoteSounds, args.Emote);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
|
private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
|
||||||
{
|
{
|
||||||
if (args.NewMobState == MobState.Alive)
|
if (args.NewMobState == MobState.Alive)
|
||||||
@@ -155,7 +176,7 @@ namespace Content.Server.Zombies
|
|||||||
// [automated maintainer groan]
|
// [automated maintainer groan]
|
||||||
_chat.TrySendInGameICMessage(uid, "[automated zombie groan]", InGameICChatType.Speak, false);
|
_chat.TrySendInGameICMessage(uid, "[automated zombie groan]", InGameICChatType.Speak, false);
|
||||||
else
|
else
|
||||||
_vocal.TryScream(uid);
|
_chat.TryEmoteWithoutChat(uid, component.GroanEmoteId);
|
||||||
|
|
||||||
component.LastDamageGroanCooldown = component.GroanCooldown;
|
component.LastDamageGroanCooldown = component.GroanCooldown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,11 +111,6 @@ namespace Content.Server.Zombies
|
|||||||
var combat = AddComp<CombatModeComponent>(target);
|
var combat = AddComp<CombatModeComponent>(target);
|
||||||
combat.IsInCombatMode = true;
|
combat.IsInCombatMode = true;
|
||||||
|
|
||||||
var vocal = EnsureComp<VocalComponent>(target);
|
|
||||||
var scream = new SoundCollectionSpecifier ("ZombieScreams");
|
|
||||||
vocal.FemaleScream = scream;
|
|
||||||
vocal.MaleScream = scream;
|
|
||||||
|
|
||||||
//This is the actual damage of the zombie. We assign the visual appearance
|
//This is the actual damage of the zombie. We assign the visual appearance
|
||||||
//and range here because of stuff we'll find out later
|
//and range here because of stuff we'll find out later
|
||||||
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
||||||
|
|||||||
51
Content.Shared/Chat/Prototypes/EmotePrototype.cs
Normal file
51
Content.Shared/Chat/Prototypes/EmotePrototype.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
|
namespace Content.Shared.Chat.Prototypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IC emotes (scream, smile, clapping, etc).
|
||||||
|
/// Entities can activate emotes by chat input or code.
|
||||||
|
/// </summary>
|
||||||
|
[Prototype("emote")]
|
||||||
|
public sealed class EmotePrototype : IPrototype
|
||||||
|
{
|
||||||
|
[IdDataField]
|
||||||
|
public string ID { get; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Different emote categories may be handled by different systems.
|
||||||
|
/// Also may be used for filtering.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("category")]
|
||||||
|
public EmoteCategory Category = EmoteCategory.General;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection of words that will be sent to chat if emote activates.
|
||||||
|
/// Will be picked randomly from list.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("chatMessages")]
|
||||||
|
public List<string> ChatMessages = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger words for emote. Case independent.
|
||||||
|
/// When typed into players chat they will activate emote event.
|
||||||
|
/// All words should be unique across all emote prototypes.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("chatTriggers")]
|
||||||
|
public HashSet<string> ChatTriggers = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IC emote category. Usually physical source of emote,
|
||||||
|
/// like hands, voice, face, etc.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum EmoteCategory : byte
|
||||||
|
{
|
||||||
|
Invalid = 0,
|
||||||
|
Vocal = 1 << 0,
|
||||||
|
Hands = 1 << 1,
|
||||||
|
General = byte.MaxValue
|
||||||
|
}
|
||||||
36
Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs
Normal file
36
Content.Shared/Chat/Prototypes/EmoteSoundsPrototype.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||||
|
|
||||||
|
namespace Content.Shared.Chat.Prototypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sounds collection for each <see cref="EmotePrototype"/>.
|
||||||
|
/// Different entities may use different sounds collections.
|
||||||
|
/// </summary>
|
||||||
|
[Prototype("emoteSounds")]
|
||||||
|
public sealed class EmoteSoundsPrototype : IPrototype
|
||||||
|
{
|
||||||
|
[IdDataField]
|
||||||
|
public string ID { get; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional fallback sound that will play if collection
|
||||||
|
/// doesn't have specific sound for this emote id.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("sound")]
|
||||||
|
public SoundSpecifier? FallbackSound;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional audio params that will be applied to ALL sounds.
|
||||||
|
/// This will overwrite any params that may be set in sound specifiers.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("params")]
|
||||||
|
public AudioParams? GeneralParams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection of emote prototypes and their sounds.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("sounds", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<SoundSpecifier, EmotePrototype>))]
|
||||||
|
public Dictionary<string, SoundSpecifier> Sounds = new();
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
namespace Content.Shared.Chat;
|
namespace Content.Shared.Chat;
|
||||||
|
|
||||||
public abstract class SharedChatSystem : EntitySystem {}
|
public abstract class SharedChatSystem : EntitySystem
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,4 +12,10 @@ namespace Content.Shared.Humanoid
|
|||||||
Female,
|
Female,
|
||||||
Unsexed,
|
Unsexed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when entity has changed their sex.
|
||||||
|
/// This doesn't handle gender changes.
|
||||||
|
/// </summary>
|
||||||
|
public record struct SexChangedEvent(Sex OldSex, Sex NewSex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,4 +197,26 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
|||||||
if (sync)
|
if (sync)
|
||||||
Dirty(humanoid);
|
Dirty(humanoid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set a humanoid mob's sex. This will not change their gender.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uid">The humanoid mob's UID.</param>
|
||||||
|
/// <param name="sex">The sex to set the mob to.</param>
|
||||||
|
/// <param name="sync">Whether to immediately synchronize this to the humanoid mob, or not.</param>
|
||||||
|
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||||
|
public void SetSex(EntityUid uid, Sex sex, bool sync = true, HumanoidAppearanceComponent? humanoid = null)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref humanoid) || humanoid.Sex == sex)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var oldSex = humanoid.Sex;
|
||||||
|
humanoid.Sex = sex;
|
||||||
|
RaiseLocalEvent(uid, new SexChangedEvent(oldSex, sex));
|
||||||
|
|
||||||
|
if (sync)
|
||||||
|
{
|
||||||
|
Dirty(humanoid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Content.Shared.Chat.Prototypes;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
@@ -80,5 +81,10 @@ namespace Content.Shared.Zombies
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("beforeZombifiedSkinColor")]
|
[DataField("beforeZombifiedSkinColor")]
|
||||||
public Color BeforeZombifiedSkinColor;
|
public Color BeforeZombifiedSkinColor;
|
||||||
|
|
||||||
|
[DataField("emoteId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
|
||||||
|
public string? EmoteSoundsId = "Zombie";
|
||||||
|
|
||||||
|
public EmoteSoundsPrototype? EmoteSounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
Resources/Audio/Effects/Emotes/attributions.yml
Normal file
15
Resources/Audio/Effects/Emotes/attributions.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
- files:
|
||||||
|
- clap1.ogg
|
||||||
|
- clap2.ogg
|
||||||
|
- clap3.ogg
|
||||||
|
- clap4.ogg
|
||||||
|
license: "CC-BY-SA-3.0"
|
||||||
|
copyright: "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432"
|
||||||
|
source: "https://github.com/tgstation/tgstation"
|
||||||
|
- files:
|
||||||
|
- snap1.ogg
|
||||||
|
- snap2.ogg
|
||||||
|
- snap3.ogg
|
||||||
|
license: "CC-BY-4.0"
|
||||||
|
copyright: "Finger Snaps Pack by Snapper4298. Converted from WAV to OGG."
|
||||||
|
source: "https://freesound.org/people/Snapper4298/packs/11176/"
|
||||||
BIN
Resources/Audio/Effects/Emotes/clap1.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/clap1.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Effects/Emotes/clap2.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/clap2.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Effects/Emotes/clap3.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/clap3.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Effects/Emotes/clap4.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/clap4.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Effects/Emotes/snap1.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/snap1.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Effects/Emotes/snap2.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/snap2.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Effects/Emotes/snap3.ogg
Normal file
BIN
Resources/Audio/Effects/Emotes/snap3.ogg
Normal file
Binary file not shown.
@@ -1,2 +1,2 @@
|
|||||||
action-name-scream = Scream
|
action-name-scream = Scream
|
||||||
scream-action-popup = Screams!
|
action-description-scream = AAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
useDelay: 10
|
useDelay: 10
|
||||||
icon: Interface/Actions/scream.png
|
icon: Interface/Actions/scream.png
|
||||||
name: action-name-scream
|
name: action-name-scream
|
||||||
description: AAAAAAAAAAAAAAAAAAAAAAAAA
|
description: action-description-scream
|
||||||
serverEvent: !type:ScreamActionEvent
|
serverEvent: !type:ScreamActionEvent
|
||||||
checkCanInteract: false
|
checkCanInteract: false
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,10 @@
|
|||||||
- type: SkeletonAccent
|
- type: SkeletonAccent
|
||||||
- type: Actions
|
- type: Actions
|
||||||
- type: Vocal
|
- type: Vocal
|
||||||
maleScream: /Audio/Voice/Skeleton/skeleton_scream.ogg
|
sounds:
|
||||||
femaleScream: /Audio/Voice/Skeleton/skeleton_scream.ogg
|
Male: Skeleton
|
||||||
|
Female: Skeleton
|
||||||
|
Unsexed: Skeleton
|
||||||
- type: Emoting
|
- type: Emoting
|
||||||
- type: Grammar
|
- type: Grammar
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -839,10 +839,10 @@
|
|||||||
types:
|
types:
|
||||||
Blunt: 0.1
|
Blunt: 0.1
|
||||||
- type: Vocal
|
- type: Vocal
|
||||||
# mice are gender neutral who cares
|
sounds:
|
||||||
maleScream: /Audio/Animals/mouse_squeak.ogg
|
Male: Mouse
|
||||||
femaleScream: /Audio/Animals/mouse_squeak.ogg
|
Female: Mouse
|
||||||
unsexedScream: /Audio/Animals/mouse_squeak.ogg
|
Unsexed: Mouse
|
||||||
wilhelmProbability: 0.001
|
wilhelmProbability: 0.001
|
||||||
# TODO: Remove CombatMode when Prototype Composition is added
|
# TODO: Remove CombatMode when Prototype Composition is added
|
||||||
- type: CombatMode
|
- type: CombatMode
|
||||||
|
|||||||
@@ -276,9 +276,10 @@
|
|||||||
- Plague
|
- Plague
|
||||||
- TongueTwister
|
- TongueTwister
|
||||||
- type: Vocal
|
- type: Vocal
|
||||||
# mice are gender neutral who cares
|
sounds:
|
||||||
maleScream: /Audio/Animals/mouse_squeak.ogg
|
Male: Mouse
|
||||||
femaleScream: /Audio/Animals/mouse_squeak.ogg
|
Female: Mouse
|
||||||
|
Unsexed: Mouse
|
||||||
wilhelmProbability: 0.001
|
wilhelmProbability: 0.001
|
||||||
- type: GhostTakeoverAvailable
|
- type: GhostTakeoverAvailable
|
||||||
makeSentient: true
|
makeSentient: true
|
||||||
|
|||||||
@@ -17,9 +17,6 @@
|
|||||||
context: "human"
|
context: "human"
|
||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: InputMover
|
- type: InputMover
|
||||||
- type: Vocal
|
|
||||||
maleScream: /Audio/Voice/Reptilian/reptilian_scream.ogg
|
|
||||||
femaleScream: /Audio/Voice/Reptilian/reptilian_scream.ogg
|
|
||||||
- type: Alerts
|
- type: Alerts
|
||||||
- type: Eye
|
- type: Eye
|
||||||
- type: CameraRecoil
|
- type: CameraRecoil
|
||||||
|
|||||||
@@ -14,9 +14,6 @@
|
|||||||
context: "human"
|
context: "human"
|
||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: InputMover
|
- type: InputMover
|
||||||
- type: Vocal
|
|
||||||
maleScream: /Audio/Voice/Skeleton/skeleton_scream.ogg
|
|
||||||
femaleScream: /Audio/Voice/Skeleton/skeleton_scream.ogg
|
|
||||||
- type: Alerts
|
- type: Alerts
|
||||||
- type: Actions
|
- type: Actions
|
||||||
- type: Eye
|
- type: Eye
|
||||||
|
|||||||
@@ -17,9 +17,6 @@
|
|||||||
context: "human"
|
context: "human"
|
||||||
- type: MobMover
|
- type: MobMover
|
||||||
- type: InputMover
|
- type: InputMover
|
||||||
- type: Vocal
|
|
||||||
maleScream: /Audio/Voice/Vox/shriek1.ogg
|
|
||||||
femaleScream: /Audio/Voice/Vox/shriek1.ogg
|
|
||||||
- type: Alerts
|
- type: Alerts
|
||||||
- type: Eye
|
- type: Eye
|
||||||
- type: CameraRecoil
|
- type: CameraRecoil
|
||||||
|
|||||||
@@ -278,7 +278,13 @@
|
|||||||
- type: Speech
|
- type: Speech
|
||||||
speechSounds: Alto
|
speechSounds: Alto
|
||||||
- type: Vocal
|
- type: Vocal
|
||||||
|
sounds:
|
||||||
|
Male: MaleHuman
|
||||||
|
Female: FemaleHuman
|
||||||
|
Unsexed: MaleHuman
|
||||||
- type: Emoting
|
- type: Emoting
|
||||||
|
- type: BodyEmotes
|
||||||
|
soundsId: GeneralBodyEmotes
|
||||||
- type: Grammar
|
- type: Grammar
|
||||||
attributes:
|
attributes:
|
||||||
proper: true
|
proper: true
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
- type: LizardAccent
|
- type: LizardAccent
|
||||||
- type: Speech
|
- type: Speech
|
||||||
speechSounds: Lizard
|
speechSounds: Lizard
|
||||||
|
- type: Vocal
|
||||||
|
sounds:
|
||||||
|
Male: UnisexReptilian
|
||||||
|
Female: UnisexReptilian
|
||||||
|
Unsexed: UnisexReptilian
|
||||||
- type: DiseaseCarrier
|
- type: DiseaseCarrier
|
||||||
diseaseResist: 0.1
|
diseaseResist: 0.1
|
||||||
- type: Damageable
|
- type: Damageable
|
||||||
|
|||||||
@@ -49,6 +49,11 @@
|
|||||||
60: 0.9
|
60: 0.9
|
||||||
80: 0.7
|
80: 0.7
|
||||||
- type: Speech
|
- type: Speech
|
||||||
|
- type: Vocal
|
||||||
|
sounds:
|
||||||
|
Male: Skeleton
|
||||||
|
Female: Skeleton
|
||||||
|
Unsexed: Skeleton
|
||||||
- type: SkeletonAccent
|
- type: SkeletonAccent
|
||||||
- type: Fixtures
|
- type: Fixtures
|
||||||
fixtures:
|
fixtures:
|
||||||
|
|||||||
@@ -101,6 +101,11 @@
|
|||||||
spawned:
|
spawned:
|
||||||
- id: FoodMeatChicken
|
- id: FoodMeatChicken
|
||||||
amount: 5
|
amount: 5
|
||||||
|
- type: Vocal
|
||||||
|
sounds:
|
||||||
|
Male: UnisexVox
|
||||||
|
Female: UnisexVox
|
||||||
|
Unsexed: UnisexVox
|
||||||
|
|
||||||
- type: entity
|
- type: entity
|
||||||
id: MobVoxDummy
|
id: MobVoxDummy
|
||||||
|
|||||||
@@ -422,7 +422,8 @@
|
|||||||
key: Stutter
|
key: Stutter
|
||||||
component: StutteringAccent
|
component: StutteringAccent
|
||||||
- !type:Jitter
|
- !type:Jitter
|
||||||
- !type:Scream
|
- !type:Emote
|
||||||
|
emote: Scream
|
||||||
probability: 0.2
|
probability: 0.2
|
||||||
- !type:PopupMessage
|
- !type:PopupMessage
|
||||||
type: Local
|
type: Local
|
||||||
|
|||||||
@@ -116,7 +116,8 @@
|
|||||||
- !type:FlammableReaction
|
- !type:FlammableReaction
|
||||||
multiplier: 0.3
|
multiplier: 0.3
|
||||||
- !type:Ignite
|
- !type:Ignite
|
||||||
- !type:Scream
|
- !type:Emote
|
||||||
|
emote: Scream
|
||||||
probability: 0.2
|
probability: 0.2
|
||||||
- !type:PopupMessage
|
- !type:PopupMessage
|
||||||
messages: [ "clf3-it-burns", "clf3-get-away" ]
|
messages: [ "clf3-it-burns", "clf3-get-away" ]
|
||||||
|
|||||||
@@ -134,7 +134,8 @@
|
|||||||
damage:
|
damage:
|
||||||
groups:
|
groups:
|
||||||
Caustic: 0.5
|
Caustic: 0.5
|
||||||
- !type:Scream
|
- !type:Emote
|
||||||
|
emote: Scream
|
||||||
probability: 0.3
|
probability: 0.3
|
||||||
metabolisms:
|
metabolisms:
|
||||||
Poison:
|
Poison:
|
||||||
@@ -169,7 +170,8 @@
|
|||||||
damage:
|
damage:
|
||||||
groups:
|
groups:
|
||||||
Caustic: 0.3
|
Caustic: 0.3
|
||||||
- !type:Scream
|
- !type:Emote
|
||||||
|
emote: Scream
|
||||||
probability: 0.2
|
probability: 0.2
|
||||||
metabolisms:
|
metabolisms:
|
||||||
Poison:
|
Poison:
|
||||||
@@ -211,7 +213,8 @@
|
|||||||
damage:
|
damage:
|
||||||
groups:
|
groups:
|
||||||
Caustic: 0.1
|
Caustic: 0.1
|
||||||
- !type:Scream
|
- !type:Emote
|
||||||
|
emote: Scream
|
||||||
probability: 0.1
|
probability: 0.1
|
||||||
metabolisms:
|
metabolisms:
|
||||||
Poison:
|
Poison:
|
||||||
|
|||||||
25
Resources/Prototypes/SoundCollections/emotes.yml
Normal file
25
Resources/Prototypes/SoundCollections/emotes.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
- type: soundCollection
|
||||||
|
id: MaleLaugh
|
||||||
|
files:
|
||||||
|
- /Audio/Voice/Human/manlaugh1.ogg
|
||||||
|
- /Audio/Voice/Human/manlaugh2.ogg
|
||||||
|
|
||||||
|
- type: soundCollection
|
||||||
|
id: FemaleLaugh
|
||||||
|
files:
|
||||||
|
- /Audio/Voice/Human/womanlaugh.ogg
|
||||||
|
|
||||||
|
- type: soundCollection
|
||||||
|
id: Claps
|
||||||
|
files:
|
||||||
|
- /Audio/Effects/Emotes/clap1.ogg
|
||||||
|
- /Audio/Effects/Emotes/clap2.ogg
|
||||||
|
- /Audio/Effects/Emotes/clap3.ogg
|
||||||
|
- /Audio/Effects/Emotes/clap4.ogg
|
||||||
|
|
||||||
|
- type: soundCollection
|
||||||
|
id: Snaps
|
||||||
|
files:
|
||||||
|
- /Audio/Effects/Emotes/snap1.ogg
|
||||||
|
- /Audio/Effects/Emotes/snap2.ogg
|
||||||
|
- /Audio/Effects/Emotes/snap3.ogg
|
||||||
70
Resources/Prototypes/Voice/speech_emote_sounds.yml
Normal file
70
Resources/Prototypes/Voice/speech_emote_sounds.yml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# species
|
||||||
|
- type: emoteSounds
|
||||||
|
id: MaleHuman
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
|
sounds:
|
||||||
|
Scream:
|
||||||
|
collection: MaleScreams
|
||||||
|
Laugh:
|
||||||
|
collection: MaleLaugh
|
||||||
|
|
||||||
|
- type: emoteSounds
|
||||||
|
id: FemaleHuman
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
|
sounds:
|
||||||
|
Scream:
|
||||||
|
collection: FemaleScreams
|
||||||
|
Laugh:
|
||||||
|
collection: FemaleLaugh
|
||||||
|
|
||||||
|
- type: emoteSounds
|
||||||
|
id: UnisexReptilian
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
|
sounds:
|
||||||
|
Scream:
|
||||||
|
path: /Audio/Voice/Reptilian/reptilian_scream.ogg
|
||||||
|
Laugh:
|
||||||
|
path: /Audio/Animals/lizard_happy.ogg
|
||||||
|
|
||||||
|
- type: emoteSounds
|
||||||
|
id: UnisexVox
|
||||||
|
sound:
|
||||||
|
path: /Audio/Voice/Vox/shriek1.ogg
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
|
|
||||||
|
# body emotes
|
||||||
|
- type: emoteSounds
|
||||||
|
id: GeneralBodyEmotes
|
||||||
|
sounds:
|
||||||
|
Clap:
|
||||||
|
collection: Claps
|
||||||
|
Snap:
|
||||||
|
collection: Snaps
|
||||||
|
params:
|
||||||
|
volume: -6
|
||||||
|
|
||||||
|
# mobs
|
||||||
|
- type: emoteSounds
|
||||||
|
id: Zombie
|
||||||
|
sound:
|
||||||
|
collection: ZombieScreams
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
|
|
||||||
|
- type: emoteSounds
|
||||||
|
id: Skeleton
|
||||||
|
sound:
|
||||||
|
path: /Audio/Voice/Skeleton/skeleton_scream.ogg
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
|
|
||||||
|
- type: emoteSounds
|
||||||
|
id: Mouse
|
||||||
|
sound:
|
||||||
|
path: /Audio/Animals/mouse_squeak.ogg
|
||||||
|
params:
|
||||||
|
variation: 0.125
|
||||||
66
Resources/Prototypes/Voice/speech_emotes.yml
Normal file
66
Resources/Prototypes/Voice/speech_emotes.yml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# vocal emotes
|
||||||
|
- type: emote
|
||||||
|
id: Scream
|
||||||
|
category: Vocal
|
||||||
|
chatMessages: [screams, yells]
|
||||||
|
chatTriggers:
|
||||||
|
- scream
|
||||||
|
- screams
|
||||||
|
- screaming
|
||||||
|
- screamed
|
||||||
|
- shriek
|
||||||
|
- shrieks
|
||||||
|
- shrieking
|
||||||
|
- shrieked
|
||||||
|
- screech
|
||||||
|
- screechs
|
||||||
|
- screeching
|
||||||
|
- screeched
|
||||||
|
- yell
|
||||||
|
- yells
|
||||||
|
- yelled
|
||||||
|
- yelling
|
||||||
|
|
||||||
|
- type: emote
|
||||||
|
id: Laugh
|
||||||
|
category: Vocal
|
||||||
|
chatMessages: [laughs]
|
||||||
|
chatTriggers:
|
||||||
|
- laugh
|
||||||
|
- laughs
|
||||||
|
- laughing
|
||||||
|
- laughed
|
||||||
|
- chuckle
|
||||||
|
- chuckles
|
||||||
|
- chuckled
|
||||||
|
- chuckling
|
||||||
|
- giggle
|
||||||
|
- giggles
|
||||||
|
- giggling
|
||||||
|
- giggled
|
||||||
|
|
||||||
|
# hand emotes
|
||||||
|
- type: emote
|
||||||
|
id: Clap
|
||||||
|
category: Hands
|
||||||
|
chatMessages: [claps]
|
||||||
|
chatTriggers:
|
||||||
|
- clap
|
||||||
|
- claps
|
||||||
|
- clapping
|
||||||
|
- clapped
|
||||||
|
|
||||||
|
- type: emote
|
||||||
|
id: Snap
|
||||||
|
category: Hands
|
||||||
|
chatMessages: [snaps fingers]
|
||||||
|
chatTriggers:
|
||||||
|
- snap
|
||||||
|
- snaps
|
||||||
|
- snapping
|
||||||
|
- snapped
|
||||||
|
- snap fingers
|
||||||
|
- snaps fingers
|
||||||
|
- snapping fingers
|
||||||
|
- snapped fingers
|
||||||
|
|
||||||
Reference in New Issue
Block a user