Audible emotes (#12708)

Co-authored-by: Visne <39844191+Visne@users.noreply.github.com>
This commit is contained in:
Alex Evgrashin
2023-01-25 17:29:41 +01:00
committed by GitHub
parent 7ec896543f
commit ef452b38a9
45 changed files with 794 additions and 169 deletions

View 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;
}
}

View File

@@ -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}");
} }

View 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);
}
}

View File

@@ -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);
}
}

View 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;
}

View 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);
}
}

View File

@@ -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);

View File

@@ -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
{
}

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -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);

View 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
}

View 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();
}

View File

@@ -1,3 +1,5 @@
namespace Content.Shared.Chat; namespace Content.Shared.Chat;
public abstract class SharedChatSystem : EntitySystem {} public abstract class SharedChatSystem : EntitySystem
{
}

View File

@@ -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);
} }

View File

@@ -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);
}
}
} }

View File

@@ -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;
} }
} }

View 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/"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +1,2 @@
action-name-scream = Scream action-name-scream = Scream
scream-action-popup = Screams! action-description-scream = AAAAAAAAAAAAAAAAAAAAAAAAA

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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" ]

View File

@@ -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:

View 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

View 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

View 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