diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.cs index 843fd46c1a0a21..e5f35ee5d82763 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChatInputBox.cs @@ -1,5 +1,9 @@ -using Content.Shared.Chat; +using Content.Client._NF.Language.Systems.Chat.Controls; +using Content.Client.UserInterface.Systems.Language; +using Content.Shared.Chat; using Content.Shared.Input; +using Content.Shared.Language; +using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; namespace Content.Client.UserInterface.Systems.Chat.Controls; @@ -8,6 +12,7 @@ namespace Content.Client.UserInterface.Systems.Chat.Controls; public class ChatInputBox : PanelContainer { public readonly ChannelSelectorButton ChannelSelector; + public readonly LanguageSelectorButton LanguageSelector; // Frontier public readonly HistoryLineEdit Input; public readonly ChannelFilterButton FilterButton; protected readonly BoxContainer Container; @@ -30,6 +35,17 @@ public ChatInputBox() MinWidth = 75 }; Container.AddChild(ChannelSelector); + // frontier block - begin + LanguageSelector = new LanguageSelectorButton + { + Name = "LanguageSelector", + ToggleMode = true, + StyleClasses = { "chatSelectorOptionButton" }, + MinWidth = 75 + }; + Container.AddChild(LanguageSelector); + LanguageSelector.OnLanguageSelect += SelectLanguage; + // frontier block - end Input = new HistoryLineEdit { Name = "Input", @@ -52,6 +68,13 @@ private void UpdateActiveChannel(ChatSelectChannel selectedChannel) ActiveChannel = (ChatChannel) selectedChannel; } + // Frontier + private void SelectLanguage(LanguagePrototype language) + { + // This sucks a lot + IoCManager.Resolve().GetUIController().SetLanguage(language.ID); + } + private static string GetChatboxInfoPlaceholder() { return (BoundKeyHelper.IsBound(ContentKeyFunctions.FocusChat), BoundKeyHelper.IsBound(ContentKeyFunctions.CycleChatChannelForward)) switch diff --git a/Content.Client/_NF/Language/LanguageMenuWindow.xaml b/Content.Client/_NF/Language/LanguageMenuWindow.xaml new file mode 100644 index 00000000000000..c92eb2ffa3360b --- /dev/null +++ b/Content.Client/_NF/Language/LanguageMenuWindow.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/Language/LanguageMenuWindow.xaml.cs b/Content.Client/_NF/Language/LanguageMenuWindow.xaml.cs new file mode 100644 index 00000000000000..37704570edb962 --- /dev/null +++ b/Content.Client/_NF/Language/LanguageMenuWindow.xaml.cs @@ -0,0 +1,123 @@ +using Content.Client.Language.Systems; +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Console; +using Robust.Shared.Utility; +using static Content.Shared.Language.Systems.SharedLanguageSystem; + +namespace Content.Client._NF.Language; // This EXACT class must have the _NF part because of xaml linking + +[GenerateTypedNameReferences] +public sealed partial class LanguageMenuWindow : DefaultWindow +{ + private readonly LanguageSystem _language; + private readonly List _entries = new(); + + public Action? OnLanguageSelected; + + public LanguageMenuWindow() + { + RobustXamlLoader.Load(this); + _language = IoCManager.Resolve().GetEntitySystem(); + + Title = Loc.GetString("language-menu-window-title"); + } + + public void UpdateState(LanguageMenuStateMessage state) + { + var clanguage = _language.GetLanguage(state.CurrentLanguage); + CurrentLanguageLabel.Text = Loc.GetString("language-menu-current-language", ("language", clanguage?.LocalizedName ?? "")); + + OptionsList.RemoveAllChildren(); + _entries.Clear(); + + foreach (var language in state.Options) + { + AddLanguageEntry(language); + } + + // Disable the button for the currently chosen language + foreach (var entry in _entries) + { + if (entry.button != null) + entry.button.Disabled = entry.language == state.CurrentLanguage; + } + } + + private void AddLanguageEntry(string language) + { + var proto = _language.GetLanguage(language); + var state = new EntryState { language = language }; + + var container = new BoxContainer(); + container.Orientation = BoxContainer.LayoutOrientation.Vertical; + + // Create and add a header with the name and the button to select the language + { + var header = new BoxContainer(); + header.Orientation = BoxContainer.LayoutOrientation.Horizontal; + + header.Orientation = BoxContainer.LayoutOrientation.Horizontal; + header.HorizontalExpand = true; + header.SeparationOverride = 2; + + var name = new Label(); + name.Text = proto?.LocalizedName ?? ""; + name.MinWidth = 50; + name.HorizontalExpand = true; + + var button = new Button(); + button.Text = "Choose"; + button.OnPressed += _ => OnLanguageChosen(language); + state.button = button; + + header.AddChild(name); + header.AddChild(button); + + container.AddChild(header); + } + + // Create and add a collapsible description + { + var body = new CollapsibleBody(); + body.HorizontalExpand = true; + body.Margin = new Thickness(4f, 4f); + + var description = new RichTextLabel(); + description.SetMessage(proto?.LocalizedDescription ?? ""); + description.HorizontalExpand = true; + + body.AddChild(description); + + var collapser = new Collapsible(Loc.GetString("language-menu-description-header"), body); + collapser.Orientation = BoxContainer.LayoutOrientation.Vertical; + collapser.HorizontalExpand = true; + + container.AddChild(collapser); + } + + // Before adding, wrap the new container in a PanelContainer to give it a distinct look + var wrapper = new PanelContainer(); + wrapper.StyleClasses.Add("PdaBorderRect"); + + wrapper.AddChild(container); + OptionsList.AddChild(wrapper); + + _entries.Add(state); + } + + private void OnLanguageChosen(string id) + { + OnLanguageSelected?.Invoke(id); + } + + private struct EntryState + { + public string language; + public Button? button; + } +} diff --git a/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorButton.cs b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorButton.cs new file mode 100644 index 00000000000000..a26f1dd977600e --- /dev/null +++ b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorButton.cs @@ -0,0 +1,95 @@ +using System.Linq; +using System.Numerics; +using Content.Client.UserInterface.Systems.Chat.Controls; +using Content.Shared.Language; +using Robust.Shared.Utility; + +namespace Content.Client._NF.Language.Systems.Chat.Controls; + +// Mostly copied from ChannelSelectorButton +public sealed class LanguageSelectorButton : ChatPopupButton +{ + public event Action? OnLanguageSelect; + + public LanguagePrototype? SelectedLanguage { get; private set; } + + private const int SelectorDropdownOffset = 38; + + public LanguageSelectorButton() + { + Name = "LanguageSelector"; + + Popup.Selected += OnLanguageSelected; + + if (Popup.FirstLanguage is { } firstSelector) + { + Select(firstSelector); + } + } + + protected override UIBox2 GetPopupPosition() + { + var globalLeft = GlobalPosition.X; + var globalBot = GlobalPosition.Y + Height; + return UIBox2.FromDimensions( + new Vector2(globalLeft, globalBot), + new Vector2(SizeBox.Width, SelectorDropdownOffset)); + } + + private void OnLanguageSelected(LanguagePrototype channel) + { + Select(channel); + } + + public void Select(LanguagePrototype language) + { + if (Popup.Visible) + { + Popup.Close(); + } + + if (SelectedLanguage == language) + return; + SelectedLanguage = language; + OnLanguageSelect?.Invoke(language); + + Text = LanguageSelectorName(language); + } + + public static string LanguageSelectorName(LanguagePrototype language, bool full = false) + { + var name = language.LocalizedName; + + // if the language name is short enough, just return it + if (full || name.Length < 5) + return name; + + // If the language name is multi-word, collect first letters and capitalize them + if (name.Contains(' ')) + { + var result = name + .Split(" ") + .Select(it => it.FirstOrNull()) + .Where(it => it != null) + .Select(it => char.ToUpper(it!.Value)); + + return new string(result.ToArray()); + } + + // Alternatively, take the first 5 letters + return name[..5]; + } + + // public Color ChannelSelectColor(ChatSelectChannel channel) + // { + // return channel switch + // { + // ChatSelectChannel.Radio => Color.LimeGreen, + // ChatSelectChannel.LOOC => Color.MediumTurquoise, + // ChatSelectChannel.OOC => Color.LightSkyBlue, + // ChatSelectChannel.Dead => Color.MediumPurple, + // ChatSelectChannel.Admin => Color.HotPink, + // _ => Color.DarkGray + // }; + // } +} diff --git a/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorItemButton.cs b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorItemButton.cs new file mode 100644 index 00000000000000..caa5eb31219215 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorItemButton.cs @@ -0,0 +1,28 @@ +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Systems.Chat; +using Content.Client.UserInterface.Systems.Chat.Controls; +using Content.Shared.Chat; +using Content.Shared.Language; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client._NF.Language.Systems.Chat.Controls; + +// Mostly copied from ChannelSelectorItemButton +public sealed class LanguageSelectorItemButton : Button +{ + public readonly LanguagePrototype Language; + + public bool IsHidden => Parent == null; + + public LanguageSelectorItemButton(LanguagePrototype language) + { + Language = language; + AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton); + + Text = LanguageSelectorButton.LanguageSelectorName(language, full: true); + + // var prefix = ChatUIController.ChannelPrefixes[selector]; + // if (prefix != default) + // Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix)); + } +} diff --git a/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorPopup.cs b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorPopup.cs new file mode 100644 index 00000000000000..38eddd4daac601 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/Chat/Controls/LanguageSelectorPopup.cs @@ -0,0 +1,109 @@ +using System.Reflection.Metadata.Ecma335; +using Content.Client.Language.Systems; +using Content.Client.UserInterface.Systems.Chat.Controls; +using Content.Client.UserInterface.Systems.Language; +using Content.Shared.Chat; +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Client.UserInterface.Controls; +using static Robust.Client.UserInterface.Controls.BaseButton; + +namespace Content.Client._NF.Language.Systems.Chat.Controls; + +// Mostly copied from LanguageSelectorPopup +public sealed class LanguageSelectorPopup : Popup +{ + private readonly BoxContainer _channelSelectorHBox; + private readonly Dictionary _selectorStates = new(); + private readonly LanguageMenuUIController _languageMenuController; + + public event Action? Selected; + + public LanguageSelectorPopup() + { + _channelSelectorHBox = new BoxContainer + { + Orientation = BoxContainer.LayoutOrientation.Horizontal, + SeparationOverride = 1 + }; + + _languageMenuController = UserInterfaceManager.GetUIController(); + _languageMenuController.LanguagesChanged += SetLanguages; + + AddChild(_channelSelectorHBox); + } + + public LanguagePrototype? FirstLanguage + { + get + { + foreach (var selector in _selectorStates.Values) + { + if (!selector.IsHidden) + return selector.Language; + } + + return null; + } + } + + private bool IsPreferredAvailable() + { + var preferred = _languageMenuController.LastPreferredLanguage; + return preferred != null && _selectorStates.TryGetValue(preferred, out var selector) && !selector.IsHidden; + } + + public void SetLanguages(List languages) + { + var languageSystem = IoCManager.Resolve().GetEntitySystem(); + _channelSelectorHBox.RemoveAllChildren(); + + foreach (var language in languages) + { + if (!_selectorStates.TryGetValue(language, out var selector)) + { + var proto = languageSystem.GetLanguage(language); + if (proto == null) + continue; + + selector = new LanguageSelectorItemButton(proto); + _selectorStates[language] = selector; + selector.OnPressed += OnSelectorPressed; + } + + if (selector.IsHidden) + { + _channelSelectorHBox.AddChild(selector); + } + } + + var isPreferredAvailable = IsPreferredAvailable(); + if (!isPreferredAvailable) + { + var first = FirstLanguage; + if (first != null) + Select(first); + } + } + + private void OnSelectorPressed(ButtonEventArgs args) + { + var button = (LanguageSelectorItemButton) args.Button; + Select(button.Language); + } + + private void Select(LanguagePrototype language) + { + Selected?.Invoke(language); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (!disposing) + return; + + _languageMenuController.LanguagesChanged -= SetLanguages; + } +} diff --git a/Content.Client/_NF/Language/Systems/LanguageSystem.cs b/Content.Client/_NF/Language/Systems/LanguageSystem.cs new file mode 100644 index 00000000000000..a10cb9df57c5c8 --- /dev/null +++ b/Content.Client/_NF/Language/Systems/LanguageSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Language.Systems; + +namespace Content.Client.Language.Systems; + +public sealed class LanguageSystem : SharedLanguageSystem +{ + +} diff --git a/Content.Client/_NF/Language/Systems/TranslatorImplanterSystem.cs b/Content.Client/_NF/Language/Systems/TranslatorImplanterSystem.cs new file mode 100644 index 00000000000000..da19b3decf97cf --- /dev/null +++ b/Content.Client/_NF/Language/Systems/TranslatorImplanterSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Language.Systems; + +namespace Content.Client.Language.Systems; + +public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem +{ + +} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 649607772a9f2c..c21b8419308387 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -5,6 +5,8 @@ using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.GameTicking; +using Content.Server.Language; +using Content.Server.Speech; using Content.Server.Speech.Components; using Content.Server.Speech.EntitySystems; using Content.Server.Station.Components; @@ -13,21 +15,18 @@ using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Database; -using Content.Shared.Decals; using Content.Shared.Ghost; -using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; +using Content.Shared.Language; using Content.Shared.Mobs.Systems; using Content.Shared.Players; using Content.Shared.Radio; -using Content.Shared.Speech; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Console; -using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; @@ -44,6 +43,8 @@ namespace Content.Server.Chat.Systems; /// public sealed partial class ChatSystem : SharedChatSystem { + public const float DefaultObfuscationFactor = 0.2f; + [Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; @@ -59,6 +60,7 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly ReplacementAccentSystem _wordreplacement = default!; + [Dependency] private readonly LanguageSystem _language = default!; public const int VoiceRange = 10; // how far voice goes in world units public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units @@ -70,10 +72,6 @@ public sealed partial class ChatSystem : SharedChatSystem private bool _critLoocEnabled; private readonly bool _adminLoocEnabled = true; - [ValidatePrototypeId] - private const string ChatNamePalette = "ChatNames"; - private string[] _chatNameColors = default!; - public override void Initialize() { base.Initialize(); @@ -83,13 +81,6 @@ public override void Initialize() _configurationManager.OnValueChanged(CCVars.CritLoocEnabled, OnCritLoocEnabledChanged, true); SubscribeLocalEvent(OnGameChange); - - var nameColors = _prototypeManager.Index(ChatNamePalette).Colors.Values.ToArray(); - _chatNameColors = new string[nameColors.Length]; - for (var i = 0; i < nameColors.Length; i++) - { - _chatNameColors[i] = nameColors[i].ToHex(); - } } public override void Shutdown() @@ -188,7 +179,8 @@ public void TrySendInGameICMessage( ICommonSession? player = null, string? nameOverride = null, bool checkRadioPrefix = true, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Frontier - languages mechanic ) { if (HasComp(source)) @@ -233,7 +225,7 @@ public void TrySendInGameICMessage( } bool shouldCapitalize = (desiredType != InGameICChatType.Emote); - bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation); + bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation) && (desiredType != InGameICChatType.Emote); // Capitalizing the word I only happens in English, so we check language here bool shouldCapitalizeTheWordI = (!CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Parent.Name == "en") || (CultureInfo.CurrentCulture.IsNeutralCulture && CultureInfo.CurrentCulture.Name == "en"); @@ -263,11 +255,12 @@ public void TrySendInGameICMessage( // Otherwise, send whatever type. switch (desiredType) { + // Frontier - languages mechanic case InGameICChatType.Speak: - SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker); + SendEntitySpeak(source, message, range, nameOverride, hideLog, ignoreActionBlocker, languageOverride: languageOverride); break; case InGameICChatType.Whisper: - SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker); + SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker, languageOverride: languageOverride); break; case InGameICChatType.Emote: SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); @@ -393,7 +386,9 @@ private void SendEntitySpeak( ChatTransmitRange range, string? nameOverride, bool hideLog = false, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Frontier: languages mechanic + ) { if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) @@ -404,8 +399,6 @@ private void SendEntitySpeak( if (message.Length == 0) return; - var speech = GetSpeechVerb(source, message); - // get the entity's apparent name (if no override provided). string name; if (nameOverride != null) @@ -417,33 +410,25 @@ private void SendEntitySpeak( var nameEv = new TransformSpeakerNameEvent(source, Name(source)); RaiseLocalEvent(source, nameEv); name = nameEv.Name; - // Check for a speech verb override - if (nameEv.SpeechVerb != null && _prototypeManager.TryIndex(nameEv.SpeechVerb, out var proto)) - speech = proto; } - name = FormattedMessage.EscapeText(name); - - // color the name unless it's something like "the old man" - string coloredName = name; - if (!TryComp(source, out var grammar) || grammar.ProperNoun == true) - coloredName = $"[color={GetNameColor(name)}]{name}[/color]"; + // Frontier - languages mechanic + var language = languageOverride ?? _language.GetLanguage(source); - var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", - ("entityName", coloredName), - ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), - ("message", FormattedMessage.EscapeText(message))); + name = FormattedMessage.EscapeText(name); + var wrappedMessage = WrapPublicMessage(source, name, message); + var obfuscated = _language.ObfuscateSpeech(source, message, language); + var wrappedObfuscated = WrapPublicMessage(source, name, obfuscated); - SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); + // Frontier: languages mechanic + SendInVoiceRange(ChatChannel.Local, name, message, wrappedMessage, obfuscated, wrappedObfuscated, source, range, languageOverride: language); - var ev = new EntitySpokeEvent(source, message, null, null); + var ev = new EntitySpokeEvent(source, message, null, false, language); RaiseLocalEvent(source, ev, true); // To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc. // Also doesn't log if hideLog is true. - if (!HasComp(source) || hideLog == true) + if (!HasComp(source) || hideLog) return; if (originalMessage == message) @@ -471,7 +456,9 @@ private void SendEntityWhisper( RadioChannelPrototype? channel, string? nameOverride, bool hideLog = false, - bool ignoreActionBlocker = false + bool ignoreActionBlocker = false, + LanguagePrototype? languageOverride = null // Frontier: languages mechanic + ) { if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker) @@ -481,8 +468,8 @@ private void SendEntityWhisper( if (message.Length == 0) return; - var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); - + // Frontier - languages mechanic + // var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); // get the entity's name by visual identity (if no override provided). string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager)); // get the entity's name by voice (if no override provided). @@ -499,45 +486,58 @@ private void SendEntityWhisper( } name = FormattedMessage.EscapeText(name); - // color the name unless it's something like "the old man" - if (!TryComp(source, out var grammar) || grammar.ProperNoun == true) - name = $"[color={GetNameColor(name)}]{name}[/color]"; - - var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", - ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - - var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message", - ("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedMessage))); - - var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", - ("message", FormattedMessage.EscapeText(obfuscatedMessage))); - + // Frontier - languages mechanic (+ everything in the foreach loop) + var language = languageOverride ?? _language.GetLanguage(source); + var languageObfuscatedMessage = _language.ObfuscateSpeech(source, message, language); + // There are 6 possible message states. It's not worth to precompute everything here. foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange)) { - EntityUid listener; - - if (session.AttachedEntity is not { Valid: true } playerEntity) + if (session.AttachedEntity is not { Valid: true } listener) continue; - listener = session.AttachedEntity.Value; - if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full) continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them. + var canUnderstand = _language.CanUnderstand(listener, language); + var finalMessage = canUnderstand ? message : languageObfuscatedMessage; + if (data.Range <= WhisperClearRange) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel); - //If listener is too far, they only hear fragments of the message - //Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind - else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) //Shared.Physics.CollisionGroup.Opaque - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel); - //If listener is too far and has no line of sight, they can't identify the whisperer's identity + { + var wrappedResult = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", name), + ("message", FormattedMessage.EscapeText(finalMessage))); + + // If the listener is in the clear range, do not perform further obfuscations + _chatManager.ChatMessageToOne(ChatChannel.Whisper, finalMessage, wrappedResult, source, false, session.ConnectedClient); + } + else if (_interactionSystem.InRangeUnobstructed(source, listener, WhisperMuffledRange, Shared.Physics.CollisionGroup.Opaque)) + { + + // If the listener is too far, they only hear fragments of the message + // Collisiongroup.Opaque is not ideal for this use. Preferably, there should be a check specifically with "Can Ent1 see Ent2" in mind + var result = ObfuscateMessageReadability(finalMessage); + var wrappedResult = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(result))); + + _chatManager.ChatMessageToOne(ChatChannel.Whisper, result, wrappedResult, source, false, session.ConnectedClient); + } else - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel); + { + //If listener is too far and has no line of sight, they can't identify the whisperer's identity + var result = ObfuscateMessageReadability(finalMessage); + var wrappedResult = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", + ("message", FormattedMessage.EscapeText(result))); + + _chatManager.ChatMessageToOne(ChatChannel.Whisper, result, wrappedResult, source, false, session.ConnectedClient); + } } - _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); + var replayWrap = Loc.GetString("chat-manager-entity-whisper-wrap-message", + ("entityName", name), + ("message", FormattedMessage.EscapeText(message))); + _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, replayWrap, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); - var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage); + var ev = new EntitySpokeEvent(source, message, channel, true, language); RaiseLocalEvent(source, ev, true); if (!hideLog) if (originalMessage == message) @@ -584,7 +584,8 @@ private void SendEntityEmote( if (checkEmote) TryEmoteChatInput(source, action); - SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author); + // Frontier - languages (emotes are skipped in obfuscation check) + SendInVoiceRange(ChatChannel.Emotes, name, action, wrappedMessage, obfuscated: "", obfuscatedWrappedMessage: "", source, range, author); if (!hideLog) if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {ToPrettyString(source):user} as {name}: {action}"); @@ -611,7 +612,14 @@ private void SendLOOC(EntityUid source, ICommonSession player, string message, b ("entityName", name), ("message", FormattedMessage.EscapeText(message))); - SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId); + // Frontier - languages mechanic + SendInVoiceRange(ChatChannel.LOOC, name, message, wrappedMessage, + obfuscated: string.Empty, + obfuscatedWrappedMessage: string.Empty, // will be skipped anyway + source, + hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, + player.UserId, + languageOverride: LanguageSystem.Universal); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}"); } @@ -624,7 +632,7 @@ private void SendDeadChat(EntityUid source, ICommonSession player, string messag { wrappedMessage = Loc.GetString("chat-manager-send-admin-dead-chat-wrap-message", ("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), - ("userName", player.Channel.UserName), + ("userName", player.ConnectedClient.UserName), ("message", FormattedMessage.EscapeText(message))); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {player:Player}: {message}"); } @@ -643,17 +651,6 @@ private void SendDeadChat(EntityUid source, ICommonSession player, string messag #region Utility - /// - /// Returns the chat name color for a mob - /// - /// Name of the mob - /// Hex value of the color - public string GetNameColor(string name) - { - var colorIdx = Math.Abs(name.GetHashCode() % _chatNameColors.Length); - return _chatNameColors[colorIdx]; - } - private enum MessageRangeCheckResult { Disallowed, @@ -703,15 +700,32 @@ private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChat /// /// Sends a chat message to the given players in range of the source entity. /// - private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null) + private void SendInVoiceRange(ChatChannel channel, string name, string message, string wrappedMessage, string obfuscated, string obfuscatedWrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null, LanguagePrototype? languageOverride = null) { + + var language = languageOverride ?? _language.GetLanguage(source); // frontier + foreach (var (session, data) in GetRecipients(source, VoiceRange)) { var entRange = MessageRangeCheck(session, data, range); if (entRange == MessageRangeCheckResult.Disallowed) continue; var entHideChat = entRange == MessageRangeCheckResult.HideChat; - _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author); + + // Frontier - languages mechanic + if (session.AttachedEntity is not { Valid: true } playerEntity) + continue; + EntityUid listener = session.AttachedEntity.Value; + + + if (channel == ChatChannel.LOOC || channel == ChatChannel.Emotes || _language.CanUnderstand(listener, language)) + { + _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.ConnectedClient, author: author); + } + else + { + _chatManager.ChatMessageToOne(channel, obfuscated, obfuscatedWrappedMessage, source, entHideChat, session.ConnectedClient, author: author); + } } _replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); @@ -794,7 +808,7 @@ private IEnumerable GetDeadChatClients() .AddWhereAttachedEntity(HasComp) .Recipients .Union(_adminManager.ActiveAdmins) - .Select(p => p.Channel); + .Select(p => p.ConnectedClient); } private string SanitizeMessagePeriod(string message) @@ -821,6 +835,19 @@ public string SanitizeMessageReplaceWords(string message) return msg; } + // Frontier - languages mechanic + public string WrapPublicMessage(EntityUid source, string name, string message) + { + var speech = GetSpeechVerb(source, message); + var verbName = Loc.GetString(_random.Pick(speech.SpeechVerbStrings)); + return Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message", + ("entityName", name), + ("verb", verbName), + ("fontType", speech.FontId), + ("fontSize", speech.FontSize), + ("message", FormattedMessage.EscapeText(message))); + } + /// /// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1. /// @@ -867,7 +894,7 @@ public readonly record struct ICChatRecipientData(float Range, bool Observer, bo { } - private string ObfuscateMessageReadability(string message, float chance) + public string ObfuscateMessageReadability(string message, float chance = DefaultObfuscationFactor) { var modifiedMessage = new StringBuilder(message); @@ -887,16 +914,6 @@ private string ObfuscateMessageReadability(string message, float chance) return modifiedMessage.ToString(); } - public string BuildGibberishString(IReadOnlyList charOptions, int length) - { - var sb = new StringBuilder(); - for (var i = 0; i < length; i++) - { - sb.Append(_random.Pick(charOptions)); - } - return sb.ToString(); - } - #endregion } @@ -912,13 +929,11 @@ public sealed class TransformSpeakerNameEvent : EntityEventArgs { public EntityUid Sender; public string Name; - public string? SpeechVerb; - public TransformSpeakerNameEvent(EntityUid sender, string name, string? speechVerb = null) + public TransformSpeakerNameEvent(EntityUid sender, string name) { Sender = sender; Name = name; - SpeechVerb = speechVerb; } } @@ -956,7 +971,9 @@ public sealed class EntitySpokeEvent : EntityEventArgs { public readonly EntityUid Source; public readonly string Message; - public readonly string? ObfuscatedMessage; // not null if this was a whisper + // Frontier - languages mechanic + public readonly bool IsWhisper; + public readonly LanguagePrototype Language; /// /// If the entity was trying to speak into a radio, this was the channel they were trying to access. If a radio @@ -964,12 +981,13 @@ public sealed class EntitySpokeEvent : EntityEventArgs /// public RadioChannelPrototype? Channel; - public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, string? obfuscatedMessage) + public EntitySpokeEvent(EntityUid source, string message, RadioChannelPrototype? channel, bool isWhisper, LanguagePrototype language) { Source = source; Message = message; Channel = channel; - ObfuscatedMessage = obfuscatedMessage; + IsWhisper = isWhisper; + Language = language; } } diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs index be8dbbaf52acbe..d970aa948d6e64 100644 --- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs +++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs @@ -1,8 +1,12 @@ +using System.Linq; using Content.Server.Ghost.Roles.Components; using Content.Server.Speech.Components; using Content.Shared.Chemistry.Reagent; +using Content.Shared.Language; +using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Robust.Shared.Prototypes; +using Content.Shared.Humanoid; //Delta-V - Banning humanoids from becoming ghost roles. namespace Content.Server.Chemistry.ReagentEffects; @@ -22,6 +26,22 @@ public override void Effect(ReagentEffectArgs args) entityManager.RemoveComponent(uid); entityManager.RemoveComponent(uid); + // Frontier - languages mechanic + // Try to make the entity speak GalacticCommon - the default language for sentient species + var speaker = entityManager.EnsureComponent(uid); + var gc = SharedLanguageSystem.GalacticCommon.ID; + + if (!speaker.UnderstoodLanguages.Contains(gc)) + speaker.UnderstoodLanguages.Add(gc); + + if (!speaker.SpokenLanguages.Contains(gc)) + { + speaker.CurrentLanguage = gc; + speaker.SpokenLanguages.Add(gc); + + args.EntityManager.EventBus.RaiseLocalEvent(uid, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + } + // Stops from adding a ghost role to things like people who already have a mind if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind) { @@ -34,6 +54,15 @@ public override void Effect(ReagentEffectArgs args) return; } + // Delta-V: Do not allow humanoids to become sentient. Intended to stop people from + // repeatedly cloning themselves and using cognizine on their bodies. + // HumanoidAppearanceComponent is common to all player species, and is also used for the + // Ripley pilot whitelist, so there's a precedent for using it for this kind of check. + if (entityManager.HasComponent(uid)) + { + return; + } + ghostRole = entityManager.AddComponent(uid); entityManager.EnsureComponent(uid); diff --git a/Content.Server/Mind/Commands/MakeSentientCommand.cs b/Content.Server/Mind/Commands/MakeSentientCommand.cs index 5e19d135b6fd7b..5efeb563bdc644 100644 --- a/Content.Server/Mind/Commands/MakeSentientCommand.cs +++ b/Content.Server/Mind/Commands/MakeSentientCommand.cs @@ -1,7 +1,10 @@ using Content.Server.Administration; +using Content.Server.Language; using Content.Shared.Administration; using Content.Shared.Emoting; using Content.Shared.Examine; +using Content.Shared.Language; +using Content.Shared.Language.Systems; using Content.Shared.Mind.Components; using Content.Shared.Movement.Components; using Content.Shared.Speech; @@ -55,6 +58,14 @@ public static void MakeSentient(EntityUid uid, IEntityManager entityManager, boo { entityManager.EnsureComponent(uid); entityManager.EnsureComponent(uid); + + var language = IoCManager.Resolve().GetEntitySystem(); + var speaker = entityManager.EnsureComponent(uid); + // The logic is simple, if the speaker knows any language (like monkey or robot), it should keep speaking that language + if (speaker.SpokenLanguages.Count == 0) + { + language.AddLanguage(speaker, SharedLanguageSystem.GalacticCommon.ID); + } } entityManager.EnsureComponent(uid); diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index d18b044205c0e0..fcbb9e77bb4682 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -1,6 +1,9 @@ using Content.Server.Chat.Systems; using Content.Server.Emp; +using Content.Server.Language; using Content.Server.Radio.Components; +using Content.Server.Speech; +using Content.Shared.Chat; using Content.Shared.Inventory.Events; using Content.Shared.Radio; using Content.Shared.Radio.Components; @@ -14,6 +17,7 @@ public sealed class HeadsetSystem : SharedHeadsetSystem { [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly RadioSystem _radio = default!; + [Dependency] private readonly LanguageSystem _language = default!; // Frontier - languages mechanic public override void Initialize() { @@ -99,8 +103,17 @@ public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref RadioReceiveEvent args) { - if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor)) - _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); + // Frontier - languages mechanic + var parent = Transform(uid).ParentUid; + if (TryComp(parent, out ActorComponent? actor)) + { + var canUnderstand = _language.CanUnderstand(parent, args.Language); + var msg = new MsgChatMessage + { + Message = canUnderstand ? args.UnderstoodChatMsg : args.NotUnderstoodChatMsg + }; + _netMan.ServerSendMessage(msg, actor.PlayerSession.ConnectedClient); + } } private void OnEmpPulse(EntityUid uid, HeadsetComponent component, ref EmpPulseEvent args) diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index ace7d8ae31ade0..eb91e8eb041544 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -1,12 +1,13 @@ using Content.Server.Chat.Systems; using Content.Server.Interaction; +using Content.Server.Language; using Content.Server.Popups; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Radio.Components; using Content.Server.Speech; using Content.Server.Speech.Components; -using Content.Shared.UserInterface; +using Content.Server.UserInterface; using Content.Shared.Chat; using Content.Shared.Examine; using Content.Shared.Interaction; @@ -29,6 +30,7 @@ public sealed class RadioDeviceSystem : EntitySystem [Dependency] private readonly InteractionSystem _interaction = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; + [Dependency] private readonly LanguageSystem _language = default!; // Used to prevent a shitter from using a bunch of radios to spam chat. private HashSet<(string, EntityUid)> _recentlySent = new(); @@ -208,7 +210,9 @@ private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref ("originalName", nameEv.Name)); // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios - _chat.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false); + // Frontier - languages mechanic + var message = args.UnderstoodChatMsg.Message; // The chat system will handle the rest and re-obfuscate if needed. + _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false, languageOverride: args.Language); } private void OnBeforeIntercomUiOpen(EntityUid uid, IntercomComponent component, BeforeActivatableUIOpenEvent args) diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 5d3074b06b61db..3907a03100d08b 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -1,13 +1,16 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; +using Content.Server.Language; using Content.Server.Power.Components; using Content.Server.Radio.Components; +using Content.Server.Speech; using Content.Server.VoiceMask; using Content.Shared.Chat; using Content.Shared.Database; +using Content.Shared.Language; using Content.Shared.Radio; using Content.Shared.Radio.Components; -using Content.Shared.Speech; +using Robust.Server.GameObjects; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; @@ -15,6 +18,7 @@ using Robust.Shared.Random; using Robust.Shared.Replays; using Robust.Shared.Utility; +using Content.Shared.IdentityManagement; // Frontier namespace Content.Server.Radio.EntitySystems; @@ -29,6 +33,7 @@ public sealed class RadioSystem : EntitySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly LanguageSystem _language = default!; // set used to prevent radio feedback loops. private readonly HashSet _messages = new(); @@ -44,7 +49,7 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent { if (args.Channel != null && component.Channels.Contains(args.Channel.ID)) { - SendRadioMessage(uid, args.Message, args.Channel, uid); + SendRadioMessage(uid, args.Message, args.Channel, uid, args.Language); args.Channel = null; // prevent duplicate messages from other listeners. } } @@ -52,15 +57,25 @@ private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args) { if (TryComp(uid, out ActorComponent? actor)) - _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); + { + // Frontier - languages mechanic + var listener = component.Owner; + var msg = args.UnderstoodChatMsg; + if (listener != null && !_language.CanUnderstand(listener, args.Language)) + { + msg = args.NotUnderstoodChatMsg; + } + + _netMan.ServerSendMessage(new MsgChatMessage { Message = msg}, actor.PlayerSession.ConnectedClient); + } } /// /// Send radio message to all active radio listeners /// - public void SendRadioMessage(EntityUid messageSource, string message, ProtoId channel, EntityUid radioSource, bool escapeMarkup = true) + public void SendRadioMessage(EntityUid messageSource, string message, ProtoId channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) { - SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup); + SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, language: language); } /// @@ -68,51 +83,58 @@ public void SendRadioMessage(EntityUid messageSource, string message, ProtoId /// Entity that spoke the message /// Entity that picked up the message and will send it, e.g. headset - public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true) + /// The language to send the message in. + public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, LanguagePrototype? language = null, bool escapeMarkup = true) { + // Frontier - languages mechanic + if (language == null) + { + language = _language.GetLanguage(messageSource); + } + // TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this. if (!_messages.Add(message)) return; - var name = TryComp(messageSource, out VoiceMaskComponent? mask) && mask.Enabled - ? mask.VoiceName - : MetaData(messageSource).EntityName; + var name = MetaData(messageSource).EntityName; // Frontier - code block to allow multi masks. + var mode = "Unknown"; - name = FormattedMessage.EscapeText(name); - - SpeechVerbPrototype speech; - if (mask != null - && mask.Enabled - && mask.SpeechVerb != null - && _prototype.TryIndex(mask.SpeechVerb, out var proto)) + if (TryComp(messageSource, out VoiceMaskComponent? mask) && mask.Enabled) { - speech = proto; - } - else - speech = _chat.GetSpeechVerb(messageSource, message); + switch (mask.Mode) + { + case Mode.Real: + mode = Identity.Name(messageSource, EntityManager); + break; + case Mode.Fake: + mode = mask.VoiceName; + break; + case Mode.Unknown: + break; + default: + throw new ArgumentOutOfRangeException($"No implemented mask radio behavior for {mask.Mode}!"); + } + name = mode; + } // Frontier - code block to allow multi masks. + + name = FormattedMessage.EscapeText(name); + // most radios are relayed to chat, so lets parse the chat message beforehand var content = escapeMarkup ? FormattedMessage.EscapeText(message) : message; - var wrappedMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", - ("color", channel.Color), - ("fontType", speech.FontId), - ("fontSize", speech.FontSize), - ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), - ("channel", $"\\[{channel.LocalizedName}\\]"), - ("name", name), - ("message", content)); + // Frontier - languages mechanic + // A message that the listener could understand + var wrappedMessage = WrapRadioMessage(messageSource, channel, name, content); + var msg = new ChatMessage(ChatChannel.Radio, content, wrappedMessage, NetEntity.Invalid, null); - // most radios are relayed to chat, so lets parse the chat message beforehand - var chat = new ChatMessage( - ChatChannel.Radio, - message, - wrappedMessage, - NetEntity.Invalid, - null); - var chatMsg = new MsgChatMessage { Message = chat }; - var ev = new RadioReceiveEvent(message, messageSource, channel, chatMsg); + // ... you guess it + var obfuscated = _language.ObfuscateSpeech(null, content, language); + var obfuscatedWrapped = WrapRadioMessage(messageSource, channel, name, obfuscated); + var notUdsMsg = new ChatMessage(ChatChannel.Radio, obfuscated, obfuscatedWrapped, NetEntity.Invalid, null); + + var ev = new RadioReceiveEvent(messageSource, channel, msg, notUdsMsg, language); var sendAttemptEv = new RadioSendAttemptEvent(channel, radioSource); RaiseLocalEvent(ref sendAttemptEv); @@ -158,10 +180,24 @@ public void SendRadioMessage(EntityUid messageSource, string message, RadioChann else _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {message}"); - _replay.RecordServerMessage(chat); + _replay.RecordServerMessage(msg); _messages.Remove(message); } + // Frontier - languages mechanic (extracted from above) + private string WrapRadioMessage(EntityUid source, RadioChannelPrototype channel, string name, string message) + { + var speech = _chat.GetSpeechVerb(source, message); + return Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap", + ("color", channel.Color), + ("fontType", speech.FontId), + ("fontSize", speech.FontSize), + ("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))), + ("channel", $"\\[{channel.LocalizedName}\\]"), + ("name", name), + ("message", FormattedMessage.EscapeText(message))); + } + /// private bool HasActiveServer(MapId mapId, string channelId) { diff --git a/Content.Server/Radio/RadioEvent.cs b/Content.Server/Radio/RadioEvent.cs index 69d764ffe67d99..5db1465e03f27c 100644 --- a/Content.Server/Radio/RadioEvent.cs +++ b/Content.Server/Radio/RadioEvent.cs @@ -1,10 +1,22 @@ using Content.Shared.Chat; +using Content.Shared.Language; using Content.Shared.Radio; namespace Content.Server.Radio; +/// +/// The message to display when the speaker can understand "language" +/// The message to display when the speaker cannot understand "language" +/// [ByRefEvent] -public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, MsgChatMessage ChatMsg); +public readonly record struct RadioReceiveEvent( + // Frontier - languages mechanic + EntityUid MessageSource, + RadioChannelPrototype Channel, + ChatMessage UnderstoodChatMsg, + ChatMessage NotUnderstoodChatMsg, + LanguagePrototype Language +); /// /// Use this event to cancel sending message per receiver diff --git a/Content.Server/Speech/EntitySystems/ListeningSystem.cs b/Content.Server/Speech/EntitySystems/ListeningSystem.cs index ea3569e055c9ed..72ecc5015c8f1f 100644 --- a/Content.Server/Speech/EntitySystems/ListeningSystem.cs +++ b/Content.Server/Speech/EntitySystems/ListeningSystem.cs @@ -8,6 +8,7 @@ namespace Content.Server.Speech.EntitySystems; /// public sealed class ListeningSystem : EntitySystem { + [Dependency] private readonly ChatSystem _chat = default!; // Frontier - languages mechanic [Dependency] private readonly SharedTransformSystem _xforms = default!; public override void Initialize() @@ -18,10 +19,10 @@ public override void Initialize() private void OnSpeak(EntitySpokeEvent ev) { - PingListeners(ev.Source, ev.Message, ev.ObfuscatedMessage); + PingListeners(ev.Source, ev.Message, ev.IsWhisper); } - public void PingListeners(EntityUid source, string message, string? obfuscatedMessage) + public void PingListeners(EntityUid source, string message, bool isWhisper) { // TODO whispering / audio volume? Microphone sensitivity? // for now, whispering just arbitrarily reduces the listener's max range. @@ -32,10 +33,11 @@ public void PingListeners(EntityUid source, string message, string? obfuscatedMe var attemptEv = new ListenAttemptEvent(source); var ev = new ListenEvent(message, source); - var obfuscatedEv = obfuscatedMessage == null ? null : new ListenEvent(obfuscatedMessage, source); + // Frontier - languages mechanic + var obfuscatedEv = !isWhisper ? null : new ListenEvent(_chat.ObfuscateMessageReadability(message), source); var query = EntityQueryEnumerator(); - while(query.MoveNext(out var listenerUid, out var listener, out var xform)) + while (query.MoveNext(out var listenerUid, out var listener, out var xform)) { if (xform.MapID != sourceXform.MapID) continue; diff --git a/Content.Server/_NF/Language/Commands/ListLanguagesCommand.cs b/Content.Server/_NF/Language/Commands/ListLanguagesCommand.cs new file mode 100644 index 00000000000000..d64573daeba7e6 --- /dev/null +++ b/Content.Server/_NF/Language/Commands/ListLanguagesCommand.cs @@ -0,0 +1,39 @@ +using System.Linq; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server.Language.Commands; + +[AnyCommand] +public sealed class ListLanguagesCommand : IConsoleCommand +{ + public string Command => "lslangs"; + public string Description => "List languages your current entity can speak at the current moment."; + public string Help => "lslangs"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + var languages = IoCManager.Resolve().GetEntitySystem(); + + var (spokenLangs, knownLangs) = languages.GetAllLanguages(playerEntity); + + shell.WriteLine("Spoken: " + string.Join(", ", spokenLangs)); + shell.WriteLine("Understood: " + string.Join(", ", knownLangs)); + } +} diff --git a/Content.Server/_NF/Language/Commands/SayLanguageCommand.cs b/Content.Server/_NF/Language/Commands/SayLanguageCommand.cs new file mode 100644 index 00000000000000..8acbe5c78c4025 --- /dev/null +++ b/Content.Server/_NF/Language/Commands/SayLanguageCommand.cs @@ -0,0 +1,53 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server.Language.Commands; + +[AnyCommand] +public sealed class SayLanguageCommand : IConsoleCommand +{ + public string Command => "lsay"; + public string Description => "Send chat languages to the local channel or a specific chat channel, in a specific language."; + public string Help => "lsay "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + if (args.Length < 2) + return; + + var languageId = args[0]; + var message = string.Join(" ", args, startIndex: 1, count: args.Length - 1).Trim(); + + if (string.IsNullOrEmpty(message)) + return; + + var languages = IoCManager.Resolve().GetEntitySystem(); + var chats = IoCManager.Resolve().GetEntitySystem(); + + var language = languages.GetLanguage(languageId); + if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + { + shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + return; + } + + chats.TrySendInGameICMessage(playerEntity, message, InGameICChatType.Speak, ChatTransmitRange.Normal, false, shell, player, languageOverride: language); + } +} diff --git a/Content.Server/_NF/Language/Commands/SelectLanguageCommand.cs b/Content.Server/_NF/Language/Commands/SelectLanguageCommand.cs new file mode 100644 index 00000000000000..c43794d6f2f79c --- /dev/null +++ b/Content.Server/_NF/Language/Commands/SelectLanguageCommand.cs @@ -0,0 +1,48 @@ +using System.Linq; +using Content.Shared.Administration; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server.Language.Commands; + +[AnyCommand] +public sealed class SelectLanguageCommand : IConsoleCommand +{ + public string Command => "lsselectlang"; + public string Description => "Open a menu to select a langauge to speak."; + public string Help => "lsselectlang"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not { } player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not { } playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + if (args.Length < 1) + return; + + var languageId = args[0]; + + var languages = IoCManager.Resolve().GetEntitySystem(); + + var language = languages.GetLanguage(languageId); + if (language == null || !languages.CanSpeak(playerEntity, language.ID)) + { + shell.WriteError($"Language {languageId} is invalid or you cannot speak it!"); + return; + } + + languages.SetLanguage(playerEntity, language.ID); + } +} diff --git a/Content.Server/_NF/Language/LanguageSystem.Windows.cs b/Content.Server/_NF/Language/LanguageSystem.Windows.cs new file mode 100644 index 00000000000000..8916a0c9dfe5bb --- /dev/null +++ b/Content.Server/_NF/Language/LanguageSystem.Windows.cs @@ -0,0 +1,40 @@ +using Content.Shared.Language; +using Robust.Server.GameObjects; +using Robust.Shared.Player; + +namespace Content.Server.Language; + +public sealed partial class LanguageSystem +{ + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + + public void InitializeWindows() + { + SubscribeNetworkEvent(OnLanguagesRequest); + SubscribeLocalEvent(OnLanguageSwitch); + } + + private void OnLanguagesRequest(RequestLanguageMenuStateMessage args, EntitySessionEventArgs session) + { + var uid = session.SenderSession.AttachedEntity; + if (uid == null) + return; + + var langs = GetLanguages(uid.Value); + if (langs == null) + return; + + var state = new LanguageMenuStateMessage(langs.CurrentLanguage, langs.SpokenLanguages); + RaiseNetworkEvent(state, uid.Value); + } + + private void OnLanguageSwitch(EntityUid uid, LanguageSpeakerComponent component, LanguagesUpdateEvent args) + { + var langs = GetLanguages(uid); + if (langs == null) + return; + + var state = new LanguageMenuStateMessage(langs.CurrentLanguage, langs.SpokenLanguages); + RaiseNetworkEvent(state, uid); + } +} diff --git a/Content.Server/_NF/Language/LanguageSystem.cs b/Content.Server/_NF/Language/LanguageSystem.cs new file mode 100644 index 00000000000000..b10e78d76a2c9e --- /dev/null +++ b/Content.Server/_NF/Language/LanguageSystem.cs @@ -0,0 +1,303 @@ +using System.Linq; +using System.Text; +using Content.Shared.GameTicking; +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Shared.Random; +using Robust.Shared.Player; +using Robust.Server.GameObjects; +using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent; + +namespace Content.Server.Language; + +public sealed partial class LanguageSystem : SharedLanguageSystem +{ + /// + /// A random number added to each pseudo-random number's seed. Changes every round. + /// + public int RandomRoundSeed { get; private set; } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInitLanguageSpeaker); + SubscribeAllEvent(it => RandomRoundSeed = _random.Next()); + + InitializeWindows(); + } + + private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent component, ComponentInit args) + { + if (string.IsNullOrEmpty(component.CurrentLanguage)) + { + component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype); + } + } + + /// + /// Obfuscate speech of the given entity, or using the given language. + /// + /// The speaker whose message needs to be obfuscated. Must not be null if "language" is not set. + /// The language for obfuscation. Must not be null if "source" is null. + public string ObfuscateSpeech(EntityUid? source, string message, LanguagePrototype? language = null) + { + if (language == null) + { + if (source is not { Valid: true }) + { + throw new NullReferenceException("Either source or language must be set."); + } + language = GetLanguage(source.Value); + } + + var builder = new StringBuilder(); + if (language.ObfuscateSyllables) + { + ObfuscateSyllables(builder, message, language); + } + else + { + ObfuscatePhrases(builder, message, language); + } + + //_sawmill.Info($"Got {message}, obfuscated to {builder}. Language: {language.ID}"); + + return builder.ToString(); + } + + private void ObfuscateSyllables(StringBuilder builder, string message, LanguagePrototype language) + { + // Go through each word. Calculate its hash sum and count the number of letters. + // Replicate it with pseudo-random syllables of pseudo-random (but similar) length. Use the hash code as the seed. + // This means that identical words will be obfuscated identically. Simple words like "hello" or "yes" in different langs can be memorized. + var wordBeginIndex = 0; + var hashCode = 0; + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + // A word ends when one of the following is found: a space, a sentence end, or EOM + if (char.IsWhiteSpace(ch) || IsSentenceEnd(ch) || i == message.Length - 1) + { + var wordLength = i - wordBeginIndex; + if (wordLength > 0) + { + var newWordLength = PseudoRandomNumber(hashCode, 1, 4); + + for (var j = 0; j < newWordLength; j++) + { + var index = PseudoRandomNumber(hashCode + j, 0, language.Replacement.Count); + builder.Append(language.Replacement[index]); + } + } + + builder.Append(ch); + hashCode = 0; + wordBeginIndex = i + 1; + } + else + { + hashCode = hashCode * 31 + ch; + } + } + } + + private void ObfuscatePhrases(StringBuilder builder, string message, LanguagePrototype language) + { + // In a similar manner, each phrase is obfuscated with a random number of conjoined obfuscation phrases. + // However, the number of phrases depends on the number of characters in the original phrase. + var sentenceBeginIndex = 0; + for (var i = 0; i < message.Length; i++) + { + var ch = char.ToLower(message[i]); + if (IsSentenceEnd(ch) || i == message.Length - 1) + { + var length = i - sentenceBeginIndex; + if (length > 0) + { + var newLength = (int) Math.Clamp(Math.Cbrt(length) - 1, 1, 4); // 27+ chars for 2 phrases, 64+ for 3, 125+ for 4. + + for (var j = 0; j < newLength; j++) + { + var phrase = _random.Pick(language.Replacement); + builder.Append(phrase); + } + } + sentenceBeginIndex = i + 1; + + if (IsSentenceEnd(ch)) + builder.Append(ch).Append(" "); + } + } + } + + public bool CanUnderstand(EntityUid listener, + LanguagePrototype language, + LanguageSpeakerComponent? listenerLanguageComp = null) + { + if (language.ID == UniversalPrototype || HasComp(listener)) + return true; + + var listenerLanguages = GetLanguages(listener, listenerLanguageComp)?.UnderstoodLanguages; + + return listenerLanguages?.Contains(language.ID, StringComparer.Ordinal) ?? false; + } + + public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponent? speakerComp = null) + { + if (HasComp(speaker)) + return true; + + var langs = GetLanguages(speaker, speakerComp)?.UnderstoodLanguages; + return langs?.Contains(language, StringComparer.Ordinal) ?? false; + } + + // + // Returns the current language of the given entity. Assumes Universal if not specified. + // + public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? languageComp = null) + { + var id = GetLanguages(speaker, languageComp)?.CurrentLanguage; + if (id == null) + return Universal; // Fallback + + _prototype.TryIndex(id, out LanguagePrototype? proto); + + return proto ?? Universal; + } + + // + // Set the CurrentLanguage of the given entity. + // + public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? languageComp = null) + { + if (!CanSpeak(speaker, language)) + return; + + if (languageComp == null && !TryComp(speaker, out languageComp)) + return; + + if (languageComp.CurrentLanguage == language) + return; + + languageComp.CurrentLanguage = language; + + RaiseLocalEvent(speaker, new LanguagesUpdateEvent(), true); + } + + /// + /// Adds a new language to the lists of understood and/or spoken languages of the given component. + /// + public void AddLanguage(LanguageSpeakerComponent comp, string language, bool addSpoken = true, bool addUnderstood = true) + { + if (addSpoken && !comp.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + comp.SpokenLanguages.Add(language); + + if (addUnderstood && !comp.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + comp.UnderstoodLanguages.Add(language); + + RaiseLocalEvent(comp.Owner, new LanguagesUpdateEvent(), true); + } + + private static bool IsSentenceEnd(char ch) + { + return ch is '.' or '!' or '?'; + } + + // This event is reused because re-allocating it each time is way too costly. + private readonly DetermineEntityLanguagesEvent _determineLanguagesEvent = new(string.Empty, new(), new()); + + /// + /// Returns a pair of (spoken, understood) languages of the given entity. + /// + public (List, List) GetAllLanguages(EntityUid speaker) + { + var languages = GetLanguages(speaker); + if (languages == null) + return (new(), new()); + + // The lists need to be copied because the internal ones are re-used for performance reasons. + return (new List(languages.SpokenLanguages), new List(languages.UnderstoodLanguages)); + } + + /// + /// Dynamically resolves the current language of the entity and the list of all languages it speaks. + /// The returned event is reused and thus must not be held as a reference anywhere but inside the caller function. + /// + private DetermineEntityLanguagesEvent? GetLanguages(EntityUid speaker, LanguageSpeakerComponent? comp = null) + { + if (comp == null && !TryComp(speaker, out comp)) + return null; + + var ev = _determineLanguagesEvent; + ev.SpokenLanguages.Clear(); + ev.UnderstoodLanguages.Clear(); + + ev.CurrentLanguage = comp.CurrentLanguage; + ev.SpokenLanguages.AddRange(comp.SpokenLanguages); + ev.UnderstoodLanguages.AddRange(comp.UnderstoodLanguages); + + RaiseLocalEvent(speaker, ev, true); + + if (ev.CurrentLanguage.Length == 0) + ev.CurrentLanguage = !string.IsNullOrEmpty(comp.CurrentLanguage) ? comp.CurrentLanguage : UniversalPrototype; // Fall back to account for admemes like admins possessing a bread + return ev; + } + + /// + /// Generates a stable pseudo-random number in the range [min, max) for the given seed. Each input seed corresponds to exactly one random number. + /// + private int PseudoRandomNumber(int seed, int min, int max) + { + // This is not a uniform distribution, but it shouldn't matter: given there's 2^31 possible random numbers, + // The bias of this function should be so tiny it will never be noticed. + seed += RandomRoundSeed; + var random = ((seed * 1103515245) + 12345) & 0x7fffffff; // Source: http://cs.uccs.edu/~cs591/bufferOverflow/glibc-2.2.4/stdlib/random_r.c + return random % (max - min) + min; + } + + /// + /// Ensures the given entity has a valid language as its current language. + /// If not, sets it to the first entry of its SpokenLanguages list, or universal if it's empty. + /// + public void EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp = null) + { + if (comp == null && !TryComp(entity, out comp)) + return; + + var langs = GetLanguages(entity, comp); + + if (langs != null && !langs.SpokenLanguages.Contains(comp!.CurrentLanguage, StringComparer.Ordinal)) + { + comp.CurrentLanguage = langs.SpokenLanguages.FirstOrDefault(UniversalPrototype); + } + } + + /// + /// Raised in order to determine the language an entity speaks at the current moment, + /// as well as the list of all languages the entity may speak and understand. + /// + public sealed class DetermineEntityLanguagesEvent : EntityEventArgs + { + /// + /// The default language of this entity. If empty, remain unchanged. + /// This field has no effect if the entity decides to speak in a concrete language. + /// + public string CurrentLanguage; + /// + /// The list of all languages the entity may speak. Must NOT be held as a reference! + /// + public List SpokenLanguages; + /// + /// The list of all languages the entity may understand. Must NOT be held as a reference! + /// + public List UnderstoodLanguages; + + public DetermineEntityLanguagesEvent(string currentLanguage, List spokenLanguages, List understoodLanguages) + { + CurrentLanguage = currentLanguage; + SpokenLanguages = spokenLanguages; + UnderstoodLanguages = understoodLanguages; + } + } +} diff --git a/Content.Server/_NF/Language/TranslatorImplanterSystem.cs b/Content.Server/_NF/Language/TranslatorImplanterSystem.cs new file mode 100644 index 00000000000000..0886dffdbcfb89 --- /dev/null +++ b/Content.Server/_NF/Language/TranslatorImplanterSystem.cs @@ -0,0 +1,79 @@ +using System.Linq; +using Content.Server.Administration.Logs; +using Content.Server.Popups; +using Content.Shared.Database; +using Content.Shared.Interaction; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Content.Shared.Language.Systems; +using Content.Shared.Mobs.Components; + +namespace Content.Server.Language; + +public sealed class TranslatorImplanterSystem : SharedTranslatorImplanterSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly LanguageSystem _language = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnImplant); + } + + private void OnImplant(EntityUid implanter, TranslatorImplanterComponent component, AfterInteractEvent args) + { + if (component.Used || !args.CanReach || args.Target is not { Valid: true } target) + return; + + if (!TryComp(target, out var speaker)) + return; + + if (component.MobsOnly && !HasComp(target)) + { + _popup.PopupEntity("translator-implanter-refuse", component.Owner); + return; + } + + var (_, understood) = _language.GetAllLanguages(target); + if (component.RequiredLanguages.Count > 0 && !component.RequiredLanguages.Any(lang => understood.Contains(lang))) + { + RefusesPopup(implanter, target); + return; + } + + var intrinsic = EnsureComp(target); + intrinsic.Enabled = true; + + foreach (var lang in component.SpokenLanguages.Where(lang => !intrinsic.SpokenLanguages.Contains(lang))) + intrinsic.SpokenLanguages.Add(lang); + + foreach (var lang in component.UnderstoodLanguages.Where(lang => !intrinsic.UnderstoodLanguages.Contains(lang))) + intrinsic.UnderstoodLanguages.Add(lang); + + component.Used = true; + SuccessPopup(implanter, target); + + _adminLogger.Add(LogType.Action, LogImpact.Medium, + $"{ToPrettyString(args.User):player} used {ToPrettyString(implanter):implanter} to give {ToPrettyString(target):target} the following languages:" + + $"\nSpoken: {string.Join(", ", component.SpokenLanguages)}; Understood: {string.Join(", ", component.UnderstoodLanguages)}"); + + OnAppearanceChange(implanter, component); + RaiseLocalEvent(target, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + } + + private void RefusesPopup(EntityUid implanter, EntityUid target) + { + _popup.PopupEntity( + Loc.GetString("translator-implanter-refuse", ("implanter", implanter), ("target", target)), + implanter); + } + + private void SuccessPopup(EntityUid implanter, EntityUid target) + { + _popup.PopupEntity( + Loc.GetString("translator-implanter-success", ("implanter", implanter), ("target", target)), + implanter); + } +} diff --git a/Content.Server/_NF/Language/TranslatorSystem.cs b/Content.Server/_NF/Language/TranslatorSystem.cs new file mode 100644 index 00000000000000..98588c0551db3c --- /dev/null +++ b/Content.Server/_NF/Language/TranslatorSystem.cs @@ -0,0 +1,243 @@ +using System.Linq; +using Content.Server.Popups; +using Content.Server.PowerCell; +using Content.Shared.Hands; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Content.Shared.Language.Systems; +using Content.Shared.PowerCell; +using static Content.Server.Language.LanguageSystem; +using HandheldTranslatorComponent = Content.Shared.Language.Components.HandheldTranslatorComponent; +using HoldsTranslatorComponent = Content.Shared.Language.Components.HoldsTranslatorComponent; +using IntrinsicTranslatorComponent = Content.Shared.Language.Components.IntrinsicTranslatorComponent; + +namespace Content.Server.Language; + +// this does not support holding multiple translators at once yet. +// that should not be an issue for now, but it better get fixed later. +public sealed class TranslatorSystem : SharedTranslatorSystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly LanguageSystem _language = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + + private ISawmill _sawmill = default!; + + public override void Initialize() + { + base.Initialize(); + _sawmill = Logger.GetSawmill("translator"); + + // I wanna die. But my death won't help us discover polymorphism. + SubscribeLocalEvent(ApplyTranslation); + SubscribeLocalEvent(ApplyTranslation); + SubscribeLocalEvent(ApplyTranslation); + // TODO: make this thing draw power + // SubscribeLocalEvent(...); + + SubscribeLocalEvent(OnTranslatorToggle); + SubscribeLocalEvent(OnPowerCellSlotEmpty); + + SubscribeLocalEvent( + (uid, component, args) => TranslatorEquipped(args.User, uid, component)); + SubscribeLocalEvent( + (uid, component, args) => TranslatorUnequipped(args.User, uid, component)); + } + + private void ApplyTranslation(EntityUid uid, IntrinsicTranslatorComponent component, + DetermineEntityLanguagesEvent ev) + { + if (!component.Enabled) + return; + + if (!_powerCell.HasActivatableCharge(uid)) + return; + + var addUnderstood = true; + var addSpoken = true; + if (component.RequiredLanguages.Count > 0) + { + if (component.RequiresAllLanguages) + { + // Add langs when the wielder has all of the required languages + foreach (var language in component.RequiredLanguages) + { + if (!ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + addSpoken = false; + + if (!ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + addUnderstood = false; + } + } + else + { + // Add langs when the wielder has at least one of the required languages + addUnderstood = false; + addSpoken = false; + foreach (var language in component.RequiredLanguages) + { + if (ev.SpokenLanguages.Contains(language, StringComparer.Ordinal)) + addSpoken = true; + + if (ev.UnderstoodLanguages.Contains(language, StringComparer.Ordinal)) + addUnderstood = true; + } + } + } + + if (addSpoken) + { + foreach (var language in component.SpokenLanguages) + { + AddIfNotExists(ev.SpokenLanguages, language); + } + + if (component.CurrentSpeechLanguage != null && ev.CurrentLanguage.Length == 0) + { + ev.CurrentLanguage = component.CurrentSpeechLanguage; + } + } + + if (addUnderstood) + { + foreach (var language in component.UnderstoodLanguages) + { + AddIfNotExists(ev.UnderstoodLanguages, language); + } + } + } + + private void TranslatorEquipped(EntityUid holder, EntityUid translator, HandheldTranslatorComponent component) + { + if (!EntityManager.HasComponent(holder)) + return; + + var intrinsic = EntityManager.EnsureComponent(holder); + UpdateBoundIntrinsicComp(component, intrinsic, component.Enabled); + + UpdatedLanguages(holder); + } + + private void TranslatorUnequipped(EntityUid holder, EntityUid translator, HandheldTranslatorComponent component) + { + if (!EntityManager.TryGetComponent(holder, out var intrinsic)) + return; + + if (intrinsic.Issuer == component) + { + + intrinsic.Enabled = false; + EntityManager.RemoveComponent(holder, intrinsic); + } + + _language.EnsureValidLanguage(holder); + + UpdatedLanguages(holder); + } + + private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponent component, ActivateInWorldEvent args) + { + if (!component.ToggleOnInteract) + return; + + var hasPower = _powerCell.HasDrawCharge(translator); + + if (Transform(args.Target).ParentUid is { Valid: true } holder && EntityManager.HasComponent(holder)) + { + // This translator is held by a language speaker and thus has an intrinsic counterpart bound to it. Make sure it's up-to-date. + var intrinsic = EntityManager.EnsureComponent(holder); + var isEnabled = !component.Enabled; + if (intrinsic.Issuer != component) + { + // The intrinsic comp wasn't owned by this handheld component, so this comp wasn't the active translator. + // Thus it needs to be turned on regardless of its previous state. + intrinsic.Issuer = component; + isEnabled = true; + } + + isEnabled &= hasPower; + UpdateBoundIntrinsicComp(component, intrinsic, isEnabled); + component.Enabled = isEnabled; + _powerCell.SetPowerCellDrawEnabled(translator, isEnabled); + + _language.EnsureValidLanguage(holder); + UpdatedLanguages(holder); + } + else + { + // This is a standalone translator (e.g. lying on the ground). Simply toggle its state. + component.Enabled = !component.Enabled && hasPower; + _powerCell.SetPowerCellDrawEnabled(translator, !component.Enabled && hasPower); + } + + OnAppearanceChange(translator, component); + + // HasPower shows a popup when there's no power, so we do not proceed in that case + if (hasPower) + { + var message = + Loc.GetString(component.Enabled ? "translator-component-turnon" : "translator-component-shutoff", ("translator", component.Owner)); + _popup.PopupEntity(message, component.Owner, args.User); + } + } + + private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorComponent component, PowerCellSlotEmptyEvent args) + { + component.Enabled = false; + _powerCell.SetPowerCellDrawEnabled(translator, false); + OnAppearanceChange(translator, component); + + if (Transform(translator).ParentUid is { Valid: true } holder && EntityManager.HasComponent(holder)) + { + if (!EntityManager.TryGetComponent(holder, out var intrinsic)) + return; + + if (intrinsic.Issuer == component) + { + intrinsic.Enabled = false; + EntityManager.RemoveComponent(holder, intrinsic); + } + + _language.EnsureValidLanguage(holder); + UpdatedLanguages(holder); + } + } + + /// + /// Copies the state from the handheld [comp] to the [intrinsic] comp, using [isEnabled] as the enabled state. + /// + private void UpdateBoundIntrinsicComp(HandheldTranslatorComponent comp, HoldsTranslatorComponent intrinsic, bool isEnabled) + { + if (isEnabled) + { + intrinsic.SpokenLanguages = new List(comp.SpokenLanguages); + intrinsic.UnderstoodLanguages = new List(comp.UnderstoodLanguages); + intrinsic.CurrentSpeechLanguage = comp.CurrentSpeechLanguage; + } + else + { + intrinsic.SpokenLanguages.Clear(); + intrinsic.UnderstoodLanguages.Clear(); + intrinsic.CurrentSpeechLanguage = null; + } + + intrinsic.Enabled = isEnabled; + intrinsic.Issuer = comp; + } + + private static void AddIfNotExists(List list, string item) + { + if (list.Contains(item)) + return; + list.Add(item); + } + + private void UpdatedLanguages(EntityUid uid) + { + RaiseLocalEvent(uid, new SharedLanguageSystem.LanguagesUpdateEvent(), true); + } +} diff --git a/Content.Shared/_NF/Language/Components/LanguageSpeakerComponent.cs b/Content.Shared/_NF/Language/Components/LanguageSpeakerComponent.cs new file mode 100644 index 00000000000000..d23097a9f48383 --- /dev/null +++ b/Content.Shared/_NF/Language/Components/LanguageSpeakerComponent.cs @@ -0,0 +1,47 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language; + +[RegisterComponent, AutoGenerateComponentState] +public sealed partial class LanguageSpeakerComponent : Component +{ + /// + /// The current language the entity may use to speak. + /// Other listeners will hear the entity speak in this language. + /// + [ViewVariables(VVAccess.ReadWrite)] + [AutoNetworkedField] + public string CurrentLanguage = default!; + + /// + /// List of languages this entity can speak. + /// + [ViewVariables] + [DataField("speaks", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List SpokenLanguages = new(); + + /// + /// List of languages this entity can understand. + /// + [ViewVariables] + [DataField("understands", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)] + public List UnderstoodLanguages = new(); + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("languageMenuAction", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string LanguageMenuAction = "ActionLanguageMenu"; + + [DataField] public EntityUid? Action; +} + +[Serializable, NetSerializable] +public enum LanguageMenuUiKey : byte +{ + Key +} + +public sealed partial class LanguageMenuActionEvent : InstantActionEvent { } diff --git a/Content.Shared/_NF/Language/Components/TranslatorComponent.cs b/Content.Shared/_NF/Language/Components/TranslatorComponent.cs new file mode 100644 index 00000000000000..196363a8c9e2d9 --- /dev/null +++ b/Content.Shared/_NF/Language/Components/TranslatorComponent.cs @@ -0,0 +1,93 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +public abstract partial class BaseTranslatorComponent : Component +{ + /// + /// The language this translator changes the speaker's language to when they don't specify one. + /// If null, does not modify the default language. + /// + [DataField("default-language")] + [ViewVariables(VVAccess.ReadWrite)] + public string? CurrentSpeechLanguage = null; + + /// + /// The list of additional languages this translator allows the wielder to speak. + /// + [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer))] + [ViewVariables(VVAccess.ReadWrite)] + public List SpokenLanguages = new(); + + /// + /// The list of additional languages this translator allows the wielder to understand. + /// + [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer))] + [ViewVariables(VVAccess.ReadWrite)] + public List UnderstoodLanguages = new(); + + /// + /// The languages the wielding MUST know in order for this translator to have effect. + /// The field [RequiresAllLanguages] indicates whether all of them are required, or just one. + /// + [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer))] + [ViewVariables(VVAccess.ReadWrite)] + public List RequiredLanguages = new(); + + /// + /// If true, the wielder must understand all languages in [RequiredLanguages] to speak [SpokenLanguages], + /// and understand all languages in [RequiredLanguages] to understand [UnderstoodLanguages]. + /// + /// Otherwise, at least one language must be known (or the list must be empty). + /// + [DataField("requires-all")] + [ViewVariables(VVAccess.ReadWrite)] + public bool RequiresAllLanguages = false; + + [DataField("enabled")] + public bool Enabled = true; +} + +/// +/// A translator that must be held in a hand or a pocket of an entity in order ot have effect. +/// +[RegisterComponent] +public sealed partial class HandheldTranslatorComponent : BaseTranslatorComponent +{ + /// + /// Whether or not interacting with this translator + /// toggles it on or off. + /// + [DataField("toggleOnInteract")] + public bool ToggleOnInteract = true; +} + +/// +/// A translator attached to an entity that translates its speech. +/// An example is a translator implant that allows the speaker to speak another language. +/// +[RegisterComponent, Virtual] +public partial class IntrinsicTranslatorComponent : BaseTranslatorComponent +{ +} + +/// +/// Applied internally to the holder of [HandheldTranslatorComponent]. +/// Do not use directly. Use [HandheldTranslatorComponent] instead. +/// +[RegisterComponent] +public sealed partial class HoldsTranslatorComponent : IntrinsicTranslatorComponent +{ + public Component? Issuer = null; +} + +/// +/// Applied to entities who were injected with a translator implant. +/// +[RegisterComponent] +public sealed partial class ImplantedTranslatorComponent : IntrinsicTranslatorComponent +{ +} diff --git a/Content.Shared/_NF/Language/Components/TranslatorImplanterComponent.cs b/Content.Shared/_NF/Language/Components/TranslatorImplanterComponent.cs new file mode 100644 index 00000000000000..d198d1e58124e4 --- /dev/null +++ b/Content.Shared/_NF/Language/Components/TranslatorImplanterComponent.cs @@ -0,0 +1,34 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Shared.Language.Components; + +/// +/// An item that, when used on a mob, adds an intrinsic translator to it. +/// +[RegisterComponent] +public sealed partial class TranslatorImplanterComponent : Component +{ + [DataField("spoken", customTypeSerializer: typeof(PrototypeIdListSerializer)), ViewVariables] + public List SpokenLanguages = new(); + + [DataField("understood", customTypeSerializer: typeof(PrototypeIdListSerializer)), ViewVariables] + public List UnderstoodLanguages = new(); + + /// + /// The list of languages the mob must understand in order for this translator to have effect. + /// Knowing one language is enough. + /// + [DataField("requires", customTypeSerializer: typeof(PrototypeIdListSerializer)), ViewVariables] + public List RequiredLanguages = new(); + + /// + /// If true, only allows to use this implanter on mobs. + /// + [DataField("mobs-only")] + public bool MobsOnly = true; + + /// + /// Whether this implant has been used already. + /// + public bool Used = false; +} diff --git a/Content.Shared/_NF/Language/Components/UniversalLanguageSpeakerComponent.cs b/Content.Shared/_NF/Language/Components/UniversalLanguageSpeakerComponent.cs new file mode 100644 index 00000000000000..680255469355be --- /dev/null +++ b/Content.Shared/_NF/Language/Components/UniversalLanguageSpeakerComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.Language.Components; + +// +// Signifies that this entity can speak and understand any language. +// Applies to such entities as ghosts. +// +[RegisterComponent] +public sealed partial class UniversalLanguageSpeakerComponent : Component +{ + +} diff --git a/Content.Shared/_NF/Language/LanguagePrototype.cs b/Content.Shared/_NF/Language/LanguagePrototype.cs new file mode 100644 index 00000000000000..512c291c8d44a6 --- /dev/null +++ b/Content.Shared/_NF/Language/LanguagePrototype.cs @@ -0,0 +1,28 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Language; + +[Prototype("language")] +public sealed class LanguagePrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + // + // If true, obfuscated phrases of creatures speaking this language will have their syllables replaced with "replacement" syllables. + // Otherwise entire sentences will be replaced. + // + [DataField("obfuscateSyllables", required: true)] + public bool ObfuscateSyllables { get; private set; } = false; + + // + // Lists all syllables that are used to obfuscate a message a listener cannot understand if obfuscateSyllables is true, + // Otherwise uses all possible phrases the creature can make when trying to say anything. + // + [DataField("replacement", required: true)] + public List Replacement = new(); + + public string LocalizedName => Loc.GetString("language-" + ID + "-name"); + + public string LocalizedDescription => Loc.GetString("language-" + ID + "-description"); +} diff --git a/Content.Shared/_NF/Language/Systems/SharedLanguageSystem.cs b/Content.Shared/_NF/Language/Systems/SharedLanguageSystem.cs new file mode 100644 index 00000000000000..48aba995e8c474 --- /dev/null +++ b/Content.Shared/_NF/Language/Systems/SharedLanguageSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Serialization; + +namespace Content.Shared.Language.Systems; + +public abstract class SharedLanguageSystem : EntitySystem +{ + [ValidatePrototypeId] + public static readonly string GalacticCommonPrototype = "GalacticCommon"; + [ValidatePrototypeId] + public static readonly string UniversalPrototype = "Universal"; + public static LanguagePrototype GalacticCommon { get; private set; } = default!; + public static LanguagePrototype Universal { get; private set; } = default!; + + [Dependency] private readonly SharedActionsSystem _action = default!; + [Dependency] protected readonly IPrototypeManager _prototype = default!; + [Dependency] protected readonly IRobustRandom _random = default!; + protected ISawmill _sawmill = default!; + + public override void Initialize() + { + GalacticCommon = _prototype.Index("GalacticCommon"); + Universal = _prototype.Index("Universal"); + _sawmill = Logger.GetSawmill("language"); + + SubscribeLocalEvent(OnInit); + } + + public LanguagePrototype? GetLanguage(string id) + { + _prototype.TryIndex(id, out var proto); + return proto; + } + + private void OnInit(EntityUid uid, LanguageSpeakerComponent component, MapInitEvent args) + { + _action.AddAction(uid, ref component.Action, component.LanguageMenuAction, uid); + } + + /// + /// Raised on an entity when its list of languages changes. + /// + public sealed class LanguagesUpdateEvent : EntityEventArgs + { + } + + /// + /// Sent when a client wants to update its language menu. + /// + [Serializable, NetSerializable] + public sealed class RequestLanguageMenuStateMessage : EntityEventArgs + { + } + + /// + /// Sent by the server when the client needs to update its language menu, + /// or directly after [RequestLanguageMenuStateMessage]. + /// + [Serializable, NetSerializable] + public sealed class LanguageMenuStateMessage : EntityEventArgs + { + public string CurrentLanguage; + public List Options; + + public LanguageMenuStateMessage(string currentLanguage, List options) + { + CurrentLanguage = currentLanguage; + Options = options; + } + } +} diff --git a/Content.Shared/_NF/Language/Systems/SharedTranslatorImplanterSystem.cs b/Content.Shared/_NF/Language/Systems/SharedTranslatorImplanterSystem.cs new file mode 100644 index 00000000000000..a13225378cd68d --- /dev/null +++ b/Content.Shared/_NF/Language/Systems/SharedTranslatorImplanterSystem.cs @@ -0,0 +1,36 @@ +using Content.Shared.Examine; +using Content.Shared.Implants.Components; +using Content.Shared.Language.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Language.Systems; + +public abstract class SharedTranslatorImplanterSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + } + + private void OnExamined(EntityUid uid, TranslatorImplanterComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + var text = !component.Used + ? Loc.GetString("translator-implanter-ready") + : Loc.GetString("translator-implanter-used"); + + args.PushText(text); + } + + protected void OnAppearanceChange(EntityUid implanter, TranslatorImplanterComponent component) + { + var used = component.Used; + _appearance.SetData(implanter, ImplanterVisuals.Full, !used); + } +} diff --git a/Content.Shared/_NF/Language/Systems/SharedTranslatorSystem.cs b/Content.Shared/_NF/Language/Systems/SharedTranslatorSystem.cs new file mode 100644 index 00000000000000..b64276eba8c396 --- /dev/null +++ b/Content.Shared/_NF/Language/Systems/SharedTranslatorSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Examine; +using Content.Shared.Language.Components; +using Content.Shared.Toggleable; + +namespace Content.Shared.Language.Systems; + +public abstract class SharedTranslatorSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnExamined); + } + + private void OnExamined(EntityUid uid, HandheldTranslatorComponent component, ExaminedEvent args) + { + var state = Loc.GetString(component.Enabled + ? "translator-enabled" + : "translator-disabled"); + + args.PushMarkup(state); + } + + protected void OnAppearanceChange(EntityUid translator, HandheldTranslatorComponent? comp = null) + { + if (comp == null && !TryComp(translator, out comp)) + return; + + _appearance.SetData(translator, ToggleVisuals.Toggled, comp.Enabled); + } +} diff --git a/Resources/Locale/en-US/_NF/language/language-menu.ftl b/Resources/Locale/en-US/_NF/language/language-menu.ftl new file mode 100644 index 00000000000000..c5dcaf54d51b9f --- /dev/null +++ b/Resources/Locale/en-US/_NF/language/language-menu.ftl @@ -0,0 +1,3 @@ +language-menu-window-title = Language Menu +language-menu-current-language = Current Language: {$language} +language-menu-description-header = Description diff --git a/Resources/Locale/en-US/_NF/language/languages.ftl b/Resources/Locale/en-US/_NF/language/languages.ftl new file mode 100644 index 00000000000000..0cb0ab120ecbab --- /dev/null +++ b/Resources/Locale/en-US/_NF/language/languages.ftl @@ -0,0 +1,72 @@ +language-Universal-name = Universal +language-Universal-description = What are you? + +language-GalacticCommon-name = Galactic common +language-GalacticCommon-description = Commonly used for inter-species communications and official purposes. + +language-Bubblish-name = Bubblish +language-Bubblish-description = The language of slimes. It's a mixture of bubbling noises and pops. Very difficult to speak without mechanical aid for humans. + +language-RootSpeak-name = Rootspeak +language-RootSpeak-description = Strange whistling language spoken by the diona. + +language-CodeSpeak-name = Codespeak +language-CodeSpeak-description = Syndicate operatives can use a series of codewords to convey complex information, while sounding like random concepts and drinks to anyone listening in. + +language-Nekomimetic-name = Nekomimetic +language-Nekomimetic-description = To the casual observer, this langauge is an incomprehensible mess of broken Japanese. To the felinids, it's somehow comprehensible. + +language-Draconic-name = Draconic +language-Draconic-description = The common language of lizard-people, composed of sibilant hisses and rattles. + +language-Canilunzt-name = Canilunzt +language-Canilunzt-description = The guttural language spoken and utilized by the inhabitants of Vazzend system, composed of growls, barks, yaps, and heavy utilization of ears and tail movements, Vulpkanin speak this language with ease. + +language-SolCommon-name = Sol common +language-SolCommon-description = The language species from the Sol System use between each other. + +language-Cat-name = Animal Cat +language-Cat-description = Primitive sounds made by cats. Somehow they convey meanings! + +language-Dog-name = Animal Dog +language-Dog-description = Barking and growling sounds, used to convey primitive meanings. + +language-Mothroach-name = Animal Mothroach +language-Mothroach-description = Cute squeaking noises that sometimes make meaningful phrases. + +language-Xeno-name = Xeno +language-Xeno-description = A forgotten language spoken by the xenomorphs. + +language-RobotTalk-name = Binary encoded +language-RobotTalk-description = Not a language by itself, used by robots and machines to exchange data. + +language-Monkey-name = Primate +language-Monkey-description = A collection of sounds and gestures made by the primates for the purpose of communication. + +language-Bee-name = Animal bee +language-Bee-description = A strange language based on movements that bees use to communicate. + +language-Mouse-name = Animal mouse +language-Mouse-description = Cute squeaking sounds mice use to beg for food. + +# These ones are half-assed because these creatures are almost never played as. +language-Chicken-name = Animal chicken +language-Chicken-description = A collection of sounds made by chickens. + +language-Duck-name = Animal duck +language-Duck-description = A collection of sounds made by ducks. + +language-Cow-name = Animal cow +language-Cow-description = A collection of sounds made by cows. + +language-Sheep-name = Animal sheep +language-Sheep-description = A collection of sounds made by sheep. + +language-Kangaroo-name = Animal kangaroo +language-Kangaroo-description = A collection of sounds made by kangaroos. + +language-Pig-name = Animal pig +language-Pig-description = A collection of sounds made by pigs. + +language-Moffic-name = Moffic +language-Moffic-description = The language of the mothpeople borders on complete unintelligibility. diff --git a/Resources/Locale/en-US/_NF/language/translator.ftl b/Resources/Locale/en-US/_NF/language/translator.ftl new file mode 100644 index 00000000000000..b9e146df404f89 --- /dev/null +++ b/Resources/Locale/en-US/_NF/language/translator.ftl @@ -0,0 +1,14 @@ +translator-component-shutoff = The {$translator} shuts off. +translator-component-turnon = The {$translator} turns on. +translator-enabled = It appears to be active. +translator-disabled = It appears to be disabled. + +translator-implanter-refuse = The {$implanter} has no effect on {$target}. +translator-implanter-success = The {$implanter} successfully injected {$target}. +translator-implanter-ready = This implanter appears to be ready to use. +translator-implanter-used = This implanter seems empty. + + + +research-technology-basic-translation = Basic Translation +research-technology-advanced-translation = Advanced Translation \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml index dc6e718290b75b..3b5bfe1cb8b19a 100644 --- a/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml +++ b/Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml @@ -209,6 +209,14 @@ - type: GuideHelp guides: - Cyborgs + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity id: BaseBorgChassisNT diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 1251884965e96b..682fa8186af6cf 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -48,8 +48,14 @@ flavorKind: station-event-random-sentience-flavor-organic - type: Bloodstream bloodMaxVolume: 50 - - type: ReplacementAccent - accent: mouse + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mouse + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - Mouse - type: MeleeWeapon soundHit: path: /Audio/Effects/bite.ogg @@ -121,7 +127,13 @@ rootTask: task: SimpleHostileCompound - type: ZombieImmune - + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Bee + understands: + - Bee + - type: entity name: bee suffix: Angry @@ -221,8 +233,14 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ReplacementAccent - accent: chicken + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: chicken + - type: LanguageSpeaker + speaks: + - Chicken + understands: + - Chicken - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -589,8 +607,14 @@ - type: EggLayer eggSpawn: - id: FoodEgg - - type: ReplacementAccent - accent: duck + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: duck + - type: LanguageSpeaker + speaks: + - Duck + understands: + - Duck - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember @@ -775,6 +799,12 @@ - type: GuideHelp guides: - Chef + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Cow + understands: + - Cow - type: entity name: crab @@ -1062,8 +1092,14 @@ - type: Inventory speciesId: kangaroo templateId: kangaroo - - type: ReplacementAccent - accent: kangaroo + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: kangaroo + - type: LanguageSpeaker + speaks: + - Kangaroo + understands: + - Kangaroo - type: InventorySlots - type: Strippable - type: Butcherable @@ -1262,6 +1298,12 @@ Burn: 3 clumsySound: path: /Audio/Animals/monkey_scream.ogg + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Monkey + understands: + - Monkey - type: entity name: monkey @@ -1544,8 +1586,14 @@ spawned: - id: FoodMeatRat amount: 1 - - type: ReplacementAccent - accent: mouse + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mouse + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - Mouse - type: Tag tags: - Trash @@ -2108,8 +2156,14 @@ - type: MeleeChemicalInjector transferAmount: 0.75 solution: melee - - type: ReplacementAccent - accent: xeno + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-tarantula @@ -2469,8 +2523,12 @@ spawned: - id: FoodMeat amount: 2 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup interactSuccessString: petting-success-dog interactFailureString: petting-failure-generic @@ -2487,6 +2545,12 @@ - type: Tag tags: - VimPilot + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: entity name: corrupted corgi @@ -2621,8 +2685,14 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: cat + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: cat + - type: LanguageSpeaker + speaks: + - Cat + understands: + - Cat - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-cat @@ -2984,8 +3054,14 @@ spawned: - id: FoodMeat amount: 1 - - type: ReplacementAccent - accent: mouse + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: mouse + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - Mouse - type: Tag tags: - VimPilot @@ -3091,8 +3167,14 @@ interactSuccessSpawn: EffectHearts interactSuccessSound: path: /Audio/Animals/pig_oink.ogg - - type: ReplacementAccent - accent: pig + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: pig + - type: LanguageSpeaker + speaks: + - Pig + understands: + - Pig - type: SentienceTarget flavorKind: station-event-random-sentience-flavor-organic - type: NpcFactionMember diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml index 472daed59b7a27..e3a9551a249098 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/argocyte.yml @@ -15,8 +15,14 @@ - type: Sprite sprite: Mobs/Aliens/Argocyte/argocyte_common.rsi - type: SolutionContainerManager - - type: ReplacementAccent - accent: xeno + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: Bloodstream bloodReagent: FerrochromicAcid bloodMaxVolume: 75 #we don't want the map to become pools of blood diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml index bc63deeac36f13..34a68d984bd64e 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml @@ -5,6 +5,12 @@ abstract: true description: A floating demon aspect of the honkmother. components: + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: GhostRole allowMovement: true makeSentient: true @@ -112,9 +118,6 @@ Radiation: 10 - type: Input context: "human" - - type: Bloodstream - bloodMaxVolume: 300 - bloodReagent: Laughter - type: entity name: behonker diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 73082674736452..6824cce08e9ff4 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -70,8 +70,14 @@ tags: - Carp - DoorBumpOpener - - type: ReplacementAccent - accent: genericAggressive + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: genericAggressive + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: Speech speechVerb: LargeMob - type: InteractionPopup @@ -167,23 +173,24 @@ - type: HTN rootTask: task: DragonCarpCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno - type: entity id: MobCarpDungeon parent: MobCarp suffix: Dungeon components: - - type: MobThresholds - thresholds: - 0: Alive - 50: Dead - - type: SlowOnDamage - speedModifierThresholds: - 25: 0.7 - type: MeleeWeapon damage: types: - Slash: 6 + Slash: 5 - type: entity name: sharkminnow diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml b/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml index 245d4227fdcdd6..9bdd868a2ee74c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/dummy_npcs.yml @@ -1,6 +1,6 @@ - type: entity save: false - name: pathfinding dummy + name: Pathfinding Dummy parent: BaseMobHuman id: MobHumanPathDummy description: A miserable pile of secrets. diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index 9694148287dbc3..40e8c489945705 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -118,9 +118,9 @@ collection: GlassBreak - !type:SpawnEntitiesBehavior spawn: - SpaceQuartz1: - min: 2 - max: 4 + SpaceQuartz: + min: 4 + max: 6 - !type:DoActsBehavior acts: [ "Destruction" ] @@ -150,8 +150,8 @@ - !type:SpawnEntitiesBehavior spawn: SteelOre1: - min: 2 - max: 4 + min: 4 + max: 6 - !type:DoActsBehavior acts: [ "Destruction" ] @@ -181,8 +181,8 @@ - !type:SpawnEntitiesBehavior spawn: UraniumOre1: - min: 1 - max: 3 + min: 3 + max: 6 - !type:DoActsBehavior acts: [ "Destruction" ] - type: PointLight @@ -214,8 +214,8 @@ - !type:SpawnEntitiesBehavior spawn: SilverOre1: - min: 1 - max: 3 + min: 4 + max: 6 - !type:DoActsBehavior acts: [ "Destruction" ] @@ -264,7 +264,7 @@ animation: WeaponArcBite damage: types: - Slash: 8 + Slash: 15 - type: MeleeChemicalInjector solution: bloodstream transferAmount: 5 @@ -288,6 +288,12 @@ solution: bloodstream - type: DrainableSolution solution: bloodstream + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Bubblish + understands: + - Bubblish - type: entity name: Reagent Slime Spawner @@ -310,10 +316,6 @@ - ReagentSlimeToxin - ReagentSlimeNapalm - ReagentSlimeOmnizine - - ReagentSlimeMuteToxin - - ReagentSlimeNorepinephricAcid - - ReagentSlimeEphedrine - - ReagentSlimeRobustHarvest chance: 1 - type: entity @@ -349,9 +351,6 @@ - map: [ "enum.DamageStateVisualLayers.Base" ] state: alive color: "#AAAAAA" - - type: MeleeChemicalInjector - solution: bloodstream - transferAmount: 1 - type: entity id: ReagentSlimeNocturine @@ -369,9 +368,6 @@ - map: [ "enum.DamageStateVisualLayers.Base" ] state: alive color: "#128e80" - - type: MeleeChemicalInjector - solution: bloodstream - transferAmount: 3 - type: entity id: ReagentSlimeTHC @@ -457,71 +453,3 @@ - map: [ "enum.DamageStateVisualLayers.Base" ] state: alive color: "#fcf7f9" - -- type: entity - id: ReagentSlimeMuteToxin - parent: ReagentSlime - suffix: Mute Toxin - components: - - type: Bloodstream - bloodReagent: MuteToxin - - type: PointLight - color: "#0f0f0f" - - type: Sprite - drawdepth: Mobs - sprite: Mobs/Aliens/elemental.rsi - layers: - - map: [ "enum.DamageStateVisualLayers.Base" ] - state: alive - color: "#0f0f0f" - -- type: entity - id: ReagentSlimeNorepinephricAcid - parent: ReagentSlime - suffix: Norepinephric Acid - components: - - type: Bloodstream - bloodReagent: NorepinephricAcid - - type: PointLight - color: "#96a8b5" - - type: Sprite - drawdepth: Mobs - sprite: Mobs/Aliens/elemental.rsi - layers: - - map: [ "enum.DamageStateVisualLayers.Base" ] - state: alive - color: "#96a8b5" - -- type: entity - id: ReagentSlimeEphedrine - parent: ReagentSlime - suffix: Ephedrine - components: - - type: Bloodstream - bloodReagent: Ephedrine - - type: PointLight - color: "#D2FFFA" - - type: Sprite - drawdepth: Mobs - sprite: Mobs/Aliens/elemental.rsi - layers: - - map: [ "enum.DamageStateVisualLayers.Base" ] - state: alive - color: "#D2FFFA" - -- type: entity - id: ReagentSlimeRobustHarvest - parent: ReagentSlime - suffix: Robust Harvest - components: - - type: Bloodstream - bloodReagent: RobustHarvest - - type: PointLight - color: "#3e901c" - - type: Sprite - drawdepth: Mobs - sprite: Mobs/Aliens/elemental.rsi - layers: - - map: [ "enum.DamageStateVisualLayers.Base" ] - state: alive - color: "#3e901c" diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml index 06ab02dedc906c..a1bf795bae8ba0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/flesh.yml @@ -52,8 +52,14 @@ damage: types: Slash: 6 - - type: ReplacementAccent - accent: genericAggressive + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: genericAggressive + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: entity parent: BaseMobFlesh @@ -252,6 +258,10 @@ Slash: 6 - type: ReplacementAccent accent: genericAggressive + - type: GhostRole + prob: 0.25 + name: ghost-role-information-salvage-flesh-name + description: ghost-role-information-salvage-flesh-description - type: SalvageMobRestrictions - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml index 9427989edaf3b5..c50074bc31d74c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/human.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/human.yml @@ -1,5 +1,5 @@ - type: entity - name: civilian + name: Civilian parent: BaseMobHuman id: MobCivilian description: A miserable pile of secrets. @@ -13,7 +13,7 @@ - NanoTrasen - type: entity - name: salvager + name: Salvager parent: BaseMobHuman id: MobSalvager components: @@ -30,7 +30,7 @@ task: SimpleHumanoidHostileCompound - type: entity - name: spirate + name: Spirate parent: BaseMobHuman id: MobSpirate description: Yarr! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml b/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml index edbfb5bf12d3eb..1a9709a0c80b37 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/lavaland.yml @@ -69,6 +69,12 @@ interactFailureString: petting-failure-generic interactSuccessSound: path: /Audio/Animals/lizard_happy.ogg + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: entity id: MobWatcherLavaland diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml b/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml index cc75405e1033c6..52a0a1c5897db1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/living_light.yml @@ -18,8 +18,8 @@ - SimpleHostile - type: MovementIgnoreGravity - type: MovementSpeedModifier - baseWalkSpeed: 3.5 - baseSprintSpeed: 3.5 + baseWalkSpeed: 5.5 + baseSprintSpeed: 5.5 - type: Sprite drawdepth: Mobs sprite: Mobs/Elemental/living_light/luminous_person.rsi @@ -33,10 +33,7 @@ - type: MobThresholds thresholds: 0: Alive - 50: Dead - - type: SlowOnDamage - speedModifierThresholds: - 20: 0.5 + 100: Dead - type: DamageStateVisuals states: Alive: @@ -73,15 +70,15 @@ types: Heat: -0.2 - type: NoSlip - - type: Pullable - type: ZombieImmune - type: NameIdentifier group: GenericNumber - type: GhostTakeoverAvailable - type: PointLight - radius: 3.0 - energy: 4.5 - color: "#6270bb" + color: "#e4de6c" + radius: 8 + softness: 2 + energy: 5 - type: FootstepModifier footstepSoundCollection: collection: FootstepBells @@ -90,17 +87,16 @@ - type: Tag tags: - FootstepSound + - DoorBumpOpener - type: Destructible thresholds: - - trigger: - !type:DamageTrigger - damage: 50 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] - - !type:PlaySoundBehavior - sound: - collection: GlassBreak + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 150 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] - type: entity id: MobLuminousPerson @@ -109,7 +105,7 @@ - type: MeleeWeapon damage: types: - Heat: 10 + Heat: 16 animation: WeaponArcFist - type: StaminaDamageOnHit damage: 16 @@ -143,7 +139,16 @@ - type: MeleeWeapon damage: types: - Heat: 6 + Heat: 8 + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 80 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] - type: entity id: MobLuminousEntity @@ -162,7 +167,7 @@ - type: MobThresholds thresholds: 0: Alive - 40: Dead + 60: Dead - type: DamageStateVisuals states: Alive: @@ -176,7 +181,7 @@ types: Heat: 5 - type: HitscanBatteryAmmoProvider - proto: RedLaser + proto: Pulse fireCost: 140 - type: Battery maxCharge: 1000 @@ -195,3 +200,12 @@ path: /Audio/Weapons/Guns/Gunshots/laser3.ogg soundEmpty: path: /Audio/Items/Lighters/lighter_off.ogg + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 100 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml index 657ac466f84660..635f31a171f123 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/mimic.yml @@ -41,3 +41,11 @@ - type: MovementSpeedModifier baseWalkSpeed : 1 baseSprintSpeed : 1 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml index 633a4ff3cac704..74ac666769e76d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/miscellaneous.yml @@ -66,3 +66,9 @@ interactFailureString: petting-failure-generic interactSuccessSound: path: /Audio/Animals/lizard_happy.ogg + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 6ad5a4f647c25d..c0654b8a646475 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -110,6 +110,7 @@ - type: NpcFactionMember factions: - PetsNT + - Cat - type: HTN rootTask: task: SimpleHostileCompound @@ -131,6 +132,7 @@ - type: NpcFactionMember factions: - PetsNT + - Cat - type: Grammar attributes: proper: true @@ -146,6 +148,9 @@ parent: MobCatCaracal description: He out here. components: + - type: NpcFactionMember + factions: + - Cat - type: Fixtures fixtures: fix1: @@ -186,6 +191,9 @@ id: MobBingus description: Bingus my beloved... components: + - type: NpcFactionMember + factions: + - Cat - type: Sprite drawdepth: Mobs sprite: Mobs/Pets/bingus.rsi @@ -288,8 +296,14 @@ spawned: - id: FoodMeat amount: 2 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup successChance: 0.5 interactSuccessString: petting-success-dog @@ -387,8 +401,14 @@ spawned: - id: FoodMeat amount: 3 - - type: ReplacementAccent - accent: dog + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: dog + - type: LanguageSpeaker + speaks: + - Dog + understands: + - Dog - type: InteractionPopup successChance: 0.7 interactSuccessString: petting-success-dog @@ -557,15 +577,8 @@ drawdepth: SmallMobs sprite: Mobs/Pets/hamlet.rsi layers: - - map: ["enum.DamageStateVisualLayers.Base", "movement"] + - map: ["enum.DamageStateVisualLayers.Base"] state: hamster-0 - - type: SpriteMovement - movementLayers: - movement: - state: hamster-moving-0 - noMovementLayers: - movement: - state: hamster-0 - type: GhostRole makeSentient: true allowSpeech: true @@ -615,26 +628,35 @@ - type: Sprite drawdepth: Mobs layers: - - map: ["enum.DamageStateVisualLayers.Base", "movement"] + - map: ["enum.DamageStateVisualLayers.Base"] state: shiva sprite: Mobs/Pets/shiva.rsi - - type: SpriteMovement - movementLayers: - movement: - state: shiva-moving - noMovementLayers: - movement: - state: shiva - type: HTN rootTask: task: SimpleHostileCompound - type: Physics + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.35 + density: 130 + mask: + - MobMask + layer: + - MobLayer - type: DamageStateVisuals states: Alive: Base: shiva Dead: Base: shiva_dead + - type: Butcherable + spawned: + - id: FoodMeatSpider + amount: 2 + - type: CombatMode - type: MobThresholds thresholds: 0: Alive @@ -648,6 +670,17 @@ types: Piercing: 8 Poison: 8 + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno + - type: NoSlip + - type: Spider + - type: IgnoreSpiderWeb - type: Grammar attributes: proper: true @@ -656,8 +689,6 @@ tags: - CannotSuicide - VimPilot - - DoorBumpOpener - - FootstepSound - type: StealTarget stealGroup: AnimalShiva @@ -772,6 +803,8 @@ makeSentient: true allowSpeech: true allowMovement: true + requirements: + - !type:WhitelistRequirement name: ghost-role-information-punpun-name description: ghost-role-information-punpun-description - type: GhostTakeoverAvailable @@ -821,5 +854,3 @@ proper: true gender: male # - type: AlwaysRevolutionaryConvertible - - type: StealTarget - stealGroup: AnimalTropico diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index a2875582127222..5247c17fc87f86 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -114,6 +114,14 @@ - type: Grammar attributes: gender: male + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Mouse + understands: + - GalacticCommon + - Mouse - type: entity id: MobRatKingBuff @@ -182,15 +190,8 @@ drawdepth: SmallMobs sprite: Mobs/Animals/mouse.rsi layers: - - map: ["enum.DamageStateVisualLayers.Base", "movement"] + - map: ["enum.DamageStateVisualLayers.Base"] state: mouse-3 - - type: SpriteMovement - movementLayers: - movement: - state: mouse-moving-3 - noMovementLayers: - movement: - state: mouse-3 - type: Physics bodyType: KinematicController - type: Fixtures @@ -272,6 +273,13 @@ - type: GuideHelp guides: - MinorAntagonists + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - GalacticCommon + - Mouse - type: weightedRandomEntity id: RatKingLoot diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 68ebf52dc06548..531df7c1574c94 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -93,3 +93,11 @@ - RevenantTheme - type: Speech speechVerb: Ghost + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 42b7ff9e2119f9..b8377b2a3f7ab0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -107,6 +107,14 @@ - type: TypingIndicator proto: robot - type: ZombieImmune + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity parent: MobSiliconBase diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 41fb6a5eec4384..fbacd05bea8835 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -111,8 +111,14 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic - - type: ReplacementAccent - accent: slimes + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: slimes + - type: LanguageSpeaker + speaks: + - Bubblish + understands: + - Bubblish - type: GhostTakeoverAvailable - type: GhostRole makeSentient: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml index a08b46fb512272..57bb8519ac6347 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/space.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/space.yml @@ -165,8 +165,14 @@ - type: FootstepModifier footstepSoundCollection: collection: FootstepBounce - - type: ReplacementAccent - accent: kangaroo + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: kangaroo + - type: LanguageSpeaker + speaks: + - Kangaroo + understands: + - Kangaroo - type: InventorySlots - type: Strippable - type: UserInterface @@ -238,18 +244,24 @@ damage: types: Piercing: 6 - Poison: 4 + Poison: 2 - type: SolutionContainerManager solutions: melee: reagents: - ReagentId: ChloralHydrate - Quantity: 80 + Quantity: 60 - type: MeleeChemicalInjector solution: melee - transferAmount: 4 - - type: ReplacementAccent - accent: xeno + transferAmount: 3 + # Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.20 interactSuccessString: petting-success-tarantula @@ -268,8 +280,6 @@ Male: UnisexArachnid Female: UnisexArachnid Unsexed: UnisexArachnid - - type: TypingIndicator - proto: spider - type: entity id: MobSpiderSpaceSalvage @@ -351,8 +361,14 @@ - type: MeleeChemicalInjector solution: melee transferAmount: 6 - - type: ReplacementAccent - accent: xeno +# Frontier - languages mechanic +# - type: ReplacementAccent +# accent: xeno + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: InteractionPopup successChance: 0.2 interactSuccessString: petting-success-snake diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml index 0a2b4f80bbcd34..26295f52f753ec 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/spacetick.yml @@ -74,8 +74,14 @@ Quantity: 5 - type: MeleeChemicalInjector solution: melee - - type: ReplacementAccent - accent: genericAggressive + # Frontier - languages mechanic + # - type: ReplacementAccent + # accent: genericAggressive + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: Speech speechVerb: SmallMob - type: NonSpreaderZombie diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 01ff22368913a9..d972936de41c11 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -120,6 +120,12 @@ molsPerSecondPerUnitMass: 0.0005 - type: Speech speechVerb: LargeMob + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Xeno + understands: + - Xeno - type: entity name: Praetorian diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index d188070b269020..7b635e09efd8d8 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -123,6 +123,14 @@ - type: Tag tags: - CannotSuicide + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Xeno + understands: + - GalacticCommon + - Xeno - type: entity parent: BaseMobDragon diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 63e27b8c53059c..c69d392c1035fd 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -30,6 +30,13 @@ - PetsNT - type: Alerts - type: Familiar + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - Mouse + understands: + - GalacticCommon + - Mouse - type: entity name: Cerberus @@ -88,3 +95,10 @@ Male: Cerberus Female: Cerberus Unsexed: Cerberus + - type: LanguageSpeaker + speaks: + - Dog + understands: + - GalacticCommon + - CodeSpeak + - Dog \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index d892b31fac3a53..8e3379563960b6 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -134,6 +134,14 @@ - type: HTN rootTask: task: SimpleHumanoidHostileCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - CodeSpeak + understands: + - GalacticCommon + - CodeSpeak # From Wizard deck of cards - type: entity @@ -161,6 +169,12 @@ map: [ "enum.DamageStateVisualLayers.BaseUnshaded" ] color: "#40a7d7" shader: unshaded + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity name: HoloClown @@ -241,6 +255,12 @@ - type: HTN rootTask: task: SimpleHumanoidHostileCompound + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity id: ActionToggleGuardian diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml index 114c3fa747986b..b62de96a35fca0 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml @@ -48,6 +48,8 @@ - type: Tag tags: - BypassInteractionRangeChecks + # Frontier - languages mechanic + - type: UniversalLanguageSpeaker # Ghosts should understand any language. - type: entity id: ActionGhostBoo diff --git a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml index ad9b37f63e1d27..789a291ad4716d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml @@ -7,3 +7,5 @@ - type: MovementSpeedModifier baseSprintSpeed: 24 baseWalkSpeed: 16 + # Frontier - languages mechanic + - type: UniversalLanguageSpeaker \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 072c419bc86ad5..45a5c8c5c9083e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -69,7 +69,15 @@ - type: Tag tags: - ShoesRequiredStepTriggerImmune - + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk + - type: entity name: drone id: Drone diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index eb104e05fa3090..a72030aac69de7 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -301,6 +301,12 @@ Asphyxiation: -1.0 - type: FireVisuals alternateState: Standing + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + understands: + - GalacticCommon - type: entity save: false diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index 6371fb74eb8457..783768c2b98c4d 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -94,6 +94,13 @@ - type: BodyEmotes soundsId: DionaBodyEmotes - type: IgnoreKudzu + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RootSpeak + understands: + - GalacticCommon + - RootSpeak - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml index 2c0ab1e15d78fc..f266205b56a1a0 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml @@ -51,7 +51,15 @@ accent: dwarf - type: Speech speechSounds: Bass - + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - SolCommon + understands: + - GalacticCommon + - SolCommon + - type: entity parent: BaseSpeciesDummy id: MobDwarfDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index d469d6c60fb5c6..51c21e93f03863 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -15,6 +15,14 @@ spawned: - id: FoodMeatHuman amount: 5 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - SolCommon + understands: + - GalacticCommon + - SolCommon - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/ipc.yml b/Resources/Prototypes/Entities/Mobs/Species/ipc.yml index 39c5295a0e1296..9b73dd186f0edd 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/ipc.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/ipc.yml @@ -129,6 +129,14 @@ - type: Inventory templateId: ipc - type: ZombieImmune + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity name: Urist McIPC diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index 199e99bef39078..605c4b7d144e1e 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -115,6 +115,13 @@ sprite: "Effects/creampie.rsi" state: "creampie_moth" visible: false + - type: LanguageSpeaker # Frontier + speaks: + - GalacticCommon + - Moffic + understands: + - GalacticCommon + - Moffic - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index f00c2f6db393d9..47840a44a5e2cb 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -58,6 +58,14 @@ types: Heat : 1.5 #per second, scales with temperature & other constants - type: Wagging + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Draconic + understands: + - GalacticCommon + - Draconic - type: entity parent: BaseSpeciesDummy diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index dfb2dd3919e256..142c8ba116e71b 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -73,6 +73,14 @@ types: Asphyxiation: -1.0 maxSaturation: 15 + # Frontier - languages mechanic + - type: LanguageSpeaker + speaks: + - GalacticCommon + - Bubblish + understands: + - GalacticCommon + - Bubblish - type: entity parent: MobHumanDummy diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index ac926119849033..7a4ad5fdf0093b 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -314,6 +314,21 @@ - FauxTileAstroIce - OreBagOfHolding - DeviceQuantumSpinInverter + # Frontier - languages mechanic + - BubblishTranslator + - DraconicTranslator + - SolCommonTranslator + - RootSpeakTranslator + - XenoTranslator + - BasicGalaticCommonTranslatorImplanter + - AdvancedGalaticCommonTranslatorImplanter + - BubblishTranslatorImplanter + - DraconicTranslatorImplanter + - SolCommonTranslatorImplanter + - RootSpeakTranslatorImplanter + - AnimalTranslator + - MofficTranslatorImplanter + - MofficTranslator - type: EmagLatheRecipes emagDynamicRecipes: - ExplosivePayload diff --git a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml index 0bfb173c8019ee..ead973690ddaaf 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/vending_machines.yml @@ -101,6 +101,14 @@ price: 100 - type: Appearance - type: WiresVisuals + # Frontier - languages mechanic (for ghost takeover, mainly) + - type: LanguageSpeaker + speaks: + - GalacticCommon + - RobotTalk + understands: + - GalacticCommon + - RobotTalk - type: entity parent: VendingMachine diff --git a/Resources/Prototypes/_NF/Actions/language.yml b/Resources/Prototypes/_NF/Actions/language.yml new file mode 100644 index 00000000000000..be81f3eca9bf75 --- /dev/null +++ b/Resources/Prototypes/_NF/Actions/language.yml @@ -0,0 +1,10 @@ +- type: entity + id: ActionLanguageMenu + name: Language Menu + description: Show the language menu. + noSpawn: true + components: + - type: InstantAction + icon: _NF/Interface/Actions/language.png + event: !type:LanguageMenuActionEvent + useDelay: 2 diff --git a/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translator_implants.yml b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translator_implants.yml new file mode 100644 index 00000000000000..abdaf86ce392cc --- /dev/null +++ b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translator_implants.yml @@ -0,0 +1,145 @@ +- type: entity + id: BaseTranslatorImplanter + parent: [ BaseItem ] + name: Basic translator implant + description: "Translates speech." + components: + - type: Sprite + sprite: Objects/Specific/Medical/implanter.rsi + state: implanter0 + layers: + - state: implanter1 + map: [ "implantFull" ] + visible: true + - state: implanter0 + map: [ "implantBroken" ] + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ImplanterVisuals.Full: + implantFull: + True: {visible: true} + False: {visible: false} + implantBroken: + True: {visible: false} + False: {visible: true} + +- type: entity + id: BasicGalaticCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Basic Galatic Common translator implant + description: "An implant giving the ability to understand Galatic Common." + components: + - type: TranslatorImplanter + understood: + - GalacticCommon + +- type: entity + id: AdvancedGalaticCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Advanced Galatic Common translator implant + description: "An implant giving the ability to understand and speak Galatic Common." + components: + - type: TranslatorImplanter + spoken: + - GalacticCommon + understood: + - GalacticCommon + +- type: entity + id: BubblishTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Bubblish translator implant + description: "An implant giving the ability to understand and speak Bubblish." + components: + - type: TranslatorImplanter + spoken: + - Bubblish + understood: + - Bubblish + +- type: entity + id: NekomimeticTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Nekomimetic translator implant + description: "An implant giving the ability to understand and speak Nekomimetic, Nya~!" + components: + - type: TranslatorImplanter + spoken: + - Nekomimetic + understood: + - Nekomimetic + +- type: entity + id: DraconicTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Draconic translator implant + description: "An implant giving the ability to understand and speak Draconic." + components: + - type: TranslatorImplanter + spoken: + - Draconic + understood: + - Draconic + +- type: entity + id: CanilunztTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Canilunzt translator implant + description: "An implant giving the ability to understand and speak Canilunzt, Yeeps!" + components: + - type: TranslatorImplanter + spoken: + - Canilunzt + understood: + - Canilunzt + +- type: entity + id: SolCommonTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: SolCommon translator implant + description: "An implant giving the ability to understand and speak SolCommon, raaagh!" + components: + - type: TranslatorImplanter + spoken: + - SolCommon + understood: + - SolCommon + +- type: entity + id: RootSpeakTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: RootSpeak translator implant + description: "An implant giving the ability to understand and speak RootSpeak." + components: + - type: TranslatorImplanter + spoken: + - RootSpeak + understood: + - RootSpeak + +- type: entity + id: MofficTranslatorImplanter + parent: [ BaseTranslatorImplanter ] + name: Moffic translator implant + description: "An implant giving the ability to understand and speak Moffic." + components: + - type: TranslatorImplanter + spoken: + - Moffic + understood: + - Moffic + +- type: entity + id: CodeSpeakImplanter + parent: [ BaseTranslatorImplanter ] + name: CodeSpeak Implanter + description: "\"CodeSpeak(tm) - Secure your communication with metaphors so elaborate, they seem randomly generated!\"" + components: + - type: TranslatorImplanter + spoken: + - CodeSpeak + understood: + - CodeSpeak + - type: StaticPrice + price: 150 diff --git a/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translators.yml b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translators.yml new file mode 100644 index 00000000000000..f875b40102b3a7 --- /dev/null +++ b/Resources/Prototypes/_NF/Entities/Objects/Devices/Misc/translators.yml @@ -0,0 +1,236 @@ +- type: entity + id: TranslatorUnpowered + parent: [ BaseItem ] + name: Translator + description: "Translates speech." + components: + - type: Sprite + sprite: _NF/Objects/Devices/translator.rsi + state: icon + layers: + - state: icon + - state: translator + shader: unshaded + visible: false + map: [ "enum.ToggleVisuals.Layer", "enum.PowerDeviceVisualLayers.Powered" ] + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ToggleVisuals.Toggled: + enum.ToggleVisuals.Layer: + True: { visible: true } + False: { visible: false } + - type: HandheldTranslator + enabled: false + +- type: entity + id: Translator + parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ] + suffix: Powered + components: + - type: PowerCellDraw + drawRate: 1 + +- type: entity + id: TranslatorEmtpy + parent: [ Translator ] + suffix: Empty + components: + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + +- type: entity + id: VulpTranslator + parent: [ Translator ] + name: Vulpkanin translator + description: "Used only by Vulpkanin to understand and speak with Galatic Common speakers." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + understood: + - GalacticCommon + requires: + - Canilunzt + requires-all: false + - type: PowerCellDraw + drawRate: 0.1 + - type: StaticPrice + price: 35 + +- type: entity + id: CanilunztTranslator + parent: [ TranslatorEmtpy ] + name: Canilunzt translator + description: "Translates speech between Canilunzt and Galactic Common. Commonly used by Vulpkanin to communicate with galactic common speakers" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Canilunzt + understood: + - GalacticCommon + - Canilunzt + requires: + - GalacticCommon + - Canilunzt + requires-all: false + +- type: entity + id: BubblishTranslator + parent: [ TranslatorEmtpy ] + name: Bubblish translator + description: "Translates speech between Bubblish and Galactic Common." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Bubblish + understood: + - GalacticCommon + - Bubblish + requires: + - GalacticCommon + - Bubblish + requires-all: false + +- type: entity + id: NekomimeticTranslator + parent: [ TranslatorEmtpy ] + name: Nekomimetic translator + description: "Translates speech between Nekomimetic and Galactic Common. Why would you want that?" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Nekomimetic + understood: + - GalacticCommon + - Nekomimetic + requires: + - GalacticCommon + - Nekomimetic + requires-all: false + +- type: entity + id: DraconicTranslator + parent: [ TranslatorEmtpy ] + name: Draconic translator + description: "Translates speech between Draconic and Galactic Common." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Draconic + understood: + - GalacticCommon + - Draconic + requires: + - GalacticCommon + - Draconic + requires-all: false + +- type: entity + id: SolCommonTranslator + parent: [ TranslatorEmtpy ] + name: Sol Common translator + description: "Translates speech between Sol Common and Galactic Common. Like a true Earthman!" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - SolCommon + understood: + - GalacticCommon + - SolCommon + requires: + - GalacticCommon + - SolCommon + requires-all: false + +- type: entity + id: RootSpeakTranslator + parent: [ TranslatorEmtpy ] + name: RootSpeak translator + description: "Translates speech between RootSpeak and Galactic Common. Like a true plant?" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - RootSpeak + understood: + - GalacticCommon + - RootSpeak + requires: + - GalacticCommon + - RootSpeak + requires-all: false + +- type: entity + id: MofficTranslator + parent: [ TranslatorEmtpy ] + name: Moffic translator + description: "Translates speech between Moffic and Galactic Common. Like a true moth... or bug?" + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Moffic + understood: + - GalacticCommon + - Moffic + requires: + - GalacticCommon + - Moffic + requires-all: false + +- type: entity + id: XenoTranslator + parent: [ TranslatorEmtpy ] + name: Xeno translator + description: "Translates speech between Xeno and Galactic Common. Not sure if that will help." + components: + - type: HandheldTranslator + default-language: GalacticCommon + spoken: + - GalacticCommon + - Xeno + understood: + - GalacticCommon + - Xeno + requires: + - GalacticCommon + +- type: entity + id: AnimalTranslator + parent: [ TranslatorEmtpy ] + name: Animal translator + description: "Translates all the cutes nosies that animals make into a more understandable form!" + components: + - type: HandheldTranslator + understood: + - Cat + - Dog + - Mothroach + - Monkey + - Bee + - Mouse + - Chicken + - Duck + - Cow + - Sheep + - Kangaroo + - Pig + requires: + - GalacticCommon + requires-all: false diff --git a/Resources/Prototypes/_NF/Language/languages.yml b/Resources/Prototypes/_NF/Language/languages.yml new file mode 100644 index 00000000000000..5b5e747a26e4db --- /dev/null +++ b/Resources/Prototypes/_NF/Language/languages.yml @@ -0,0 +1,407 @@ +# The universal language, assumed if the entity has a UniversalLanguageSpeakerComponent. +# Do not use otherwise. Try to use the respective component instead of this language. +- type: language + id: Universal + obfuscateSyllables: false + replacement: + - "*incomprehensible*" + +# The common galactic tongue. +- type: language + id: GalacticCommon + obfuscateSyllables: true + replacement: + - Blah + - Blah + - Blah + - dingle-doingle + - dingle + - dangle + - jibber-jabber + - jubber + - bleh + - zippity + - zoop + - wibble + - wobble + - wiggle + - yada + - meh + - neh + - nah + - wah + +# Spoken by slimes. +- type: language + id: Bubblish + obfuscateSyllables: true + replacement: + - blob + - plop + - pop + - bop + - boop + +# Spoken by moths. +- type: language + id: Moffic + obfuscateSyllables: true + replacement: + - år + - i + - går + - sek + - mo + - ff + - ok + - gj + - ø + - gå + - la + - le + - lit + - ygg + - van + - dår + - næ + - møt + - idd + - hvo + - ja + - på + - han + - så + - ån + - det + - att + - nå + - gö + - bra + - int + - tyc + - om + - när + - två + - må + - dag + - sjä + - vii + - vuo + - eil + - tun + - käyt + - teh + - vä + - hei + - huo + - suo + - ää + - ten + - ja + - heu + - stu + - uhr + - kön + - we + - hön + + # Spoken by dionas. +- type: language + id: RootSpeak + obfuscateSyllables: true + replacement: + - hs + - zt + - kr + - st + - sh + +# Syndicate operatives can use a series of codewords to convey complex information. +- type: language + id: CodeSpeak + obfuscateSyllables: true + replacement: + - WhiteRussian + - Station Representative + - Sherrif + - Deputy + - Vault + - Frontier + - Station + - Security + - Coffee + - Cola + - Water + - Engineering + - Captain + - Calling a friend + - Atmos + - Robotic + - Medical + - AI + - Human + - Vulpkanin + - Lizard + - Moth + - Tea + - Chair + - Sofa + - Ship + - Shuttle + - Weapon + - Weapons + - Laser + - Carp + - Space Carp + - Xeno + - Xenomorph + - Biohazard + - Money + - Space + - Danger + - Monkey + - Pun pun + - In + - Running + - Killing + - Kill + - Save + - Life + - Dragon + - Ninja + - Secret + +# Spoken by the Lizard race. +- type: language + id: Draconic + obfuscateSyllables: true + replacement: + - za + - az + - ze + - ez + - zi + - iz + - zo + - oz + - zu + - uz + - zs + - sz + - ha + - ah + - he + - eh + - hi + - ih + - ho + - oh + - hu + - uh + - hs + - sh + - la + - al + - le + - el + - li + - il + - lo + - ol + - lu + - ul + - ls + - sl + - ka + - ak + - ke + - ek + - ki + - ik + - ko + - ok + - ku + - uk + - ks + - sk + - sa + - as + - se + - es + - si + - is + - so + - os + - su + - us + - ss + - ss + - ra + - ar + - re + - er + - ri + - ir + - ro + - or + - ru + - ur + - rs + - sr + - a + - a + - e + - e + - i + - i + - o + - o + - u + - u + - s + - s + +# The common language of the Sol system. +- type: language + id: SolCommon + obfuscateSyllables: true + replacement: + - tao + - shi + - tzu + - yi + - com + - be + - is + - i + - op + - vi + - ed + - lec + - mo + - cle + - te + - dis + - e + +# Languages spoken by various critters. +- type: language + id: Cat + obfuscateSyllables: true + replacement: + - murr + - meow + - purr + - mrow + +- type: language + id: Dog + obfuscateSyllables: true + replacement: + - woof + - bark + - ruff + - bork + - raff + - garr + +- type: language + id: Mothroach + obfuscateSyllables: false + replacement: + - Chitter + - Buzz + - Chirp + - Squeak + - Peep + - Eeee + - Eep + +- type: language + id: Xeno + obfuscateSyllables: true + replacement: + - sss + - sSs + - SSS + +- type: language + id: RobotTalk + obfuscateSyllables: true + replacement: + - beep + - boop + +- type: language + id: Monkey + obfuscateSyllables: true + replacement: + - ok + - ook + - oook + - ooook + - oooook + +- type: language + id: Bee + obfuscateSyllables: false + replacement: + - Buz + - Buuz + - Buzz + - Buzzz + - Buuzz + +- type: language + id: Mouse + obfuscateSyllables: false + replacement: + - Squeak + - Piep + - Chuu + - Eeee + - Pip + - Fwiep + - Heep + +- type: language + id: Chicken + obfuscateSyllables: false + replacement: + - Coo + - Coot + - Cooot + +- type: language + id: Duck + obfuscateSyllables: false + replacement: + - Quack + +- type: language + id: Cow + obfuscateSyllables: false + replacement: + - Moo + - Mooo + +- type: language + id: Sheep + obfuscateSyllables: false + replacement: + - Ba + - Baa + - Baaa + +- type: language + id: Kangaroo + obfuscateSyllables: false + replacement: + - Shreak + - Chuu + +- type: language + id: Pig + obfuscateSyllables: false + replacement: + - Oink diff --git a/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml b/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml new file mode 100644 index 00000000000000..9daa187bd0844a --- /dev/null +++ b/Resources/Prototypes/_NF/Recipes/Lathes/devices.yml @@ -0,0 +1,170 @@ +# Translators +- type: latheRecipe + id: BubblishTranslator + result: BubblishTranslator + completetime: 2 + materials: + Steel: 500 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: DraconicTranslator + result: DraconicTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: SolCommonTranslator + result: SolCommonTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: RootSpeakTranslator + result: RootSpeakTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: MofficTranslator + result: MofficTranslator + completetime: 2 + materials: + Steel: 500 + Glass: 100 + Plastic: 50 + Gold: 50 + +- type: latheRecipe + id: BasicGalaticCommonTranslatorImplanter + result: BasicGalaticCommonTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: XenoTranslator + result: XenoTranslator + completetime: 2 + materials: + Steel: 200 + Plastic: 50 + Gold: 50 + Plasma: 50 + Silver: 50 + +- type: latheRecipe + id: AdvancedGalaticCommonTranslatorImplanter + result: AdvancedGalaticCommonTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: BubblishTranslatorImplanter + result: BubblishTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: NekomimeticTranslatorImplanter + result: NekomimeticTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: DraconicTranslatorImplanter + result: DraconicTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: CanilunztTranslatorImplanter + result: CanilunztTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: SolCommonTranslatorImplanter + result: SolCommonTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: RootSpeakTranslatorImplanter + result: RootSpeakTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: MofficTranslatorImplanter + result: MofficTranslatorImplanter + completetime: 2 + materials: + Steel: 500 + Glass: 500 + Plastic: 100 + Gold: 50 + Silver: 50 + +- type: latheRecipe + id: AnimalTranslator + result: AnimalTranslator + completetime: 2 + materials: + Steel: 200 + Plastic: 50 + Gold: 50 + Plasma: 50 + Silver: 5 diff --git a/Resources/Prototypes/_NF/Research/civilianservices.yml b/Resources/Prototypes/_NF/Research/civilianservices.yml new file mode 100644 index 00000000000000..7a44847d30b02f --- /dev/null +++ b/Resources/Prototypes/_NF/Research/civilianservices.yml @@ -0,0 +1,38 @@ +# Tier 2 - entrypoint to translators +- type: technology + id: BasicTranslation + name: research-technology-basic-translation + icon: + sprite: _NF/Objects/Devices/translator.rsi + state: icon + discipline: CivilianServices + tier: 2 + cost: 10000 + recipeUnlocks: + - BubblishTranslator + - DraconicTranslator + - SolCommonTranslator + - RootSpeakTranslator + - BasicGalaticCommonTranslatorImplanter + - MofficTranslator + + +# Frontier - languages mechanic +- type: technology + id: AdvancedTranslation + name: research-technology-advanced-translation + icon: + sprite: _NF/Objects/Devices/translator.rsi + state: icon + discipline: CivilianServices + tier: 3 + cost: 15000 + recipeUnlocks: + - XenoTranslator + - AdvancedGalaticCommonTranslatorImplanter + - BubblishTranslatorImplanter + - DraconicTranslatorImplanter + - SolCommonTranslatorImplanter + - RootSpeakTranslatorImplanter + - AnimalTranslator + - MofficTranslatorImplanter diff --git a/Resources/Textures/_NF/Actions/Actions/language.png b/Resources/Textures/_NF/Actions/Actions/language.png new file mode 100644 index 00000000000000..51962871ac7350 Binary files /dev/null and b/Resources/Textures/_NF/Actions/Actions/language.png differ diff --git a/Resources/Textures/_NF/Objects/Devices/translator.rsi/icon.png b/Resources/Textures/_NF/Objects/Devices/translator.rsi/icon.png new file mode 100644 index 00000000000000..6871c808ccdd49 Binary files /dev/null and b/Resources/Textures/_NF/Objects/Devices/translator.rsi/icon.png differ diff --git a/Resources/Textures/_NF/Objects/Devices/translator.rsi/meta.json b/Resources/Textures/_NF/Objects/Devices/translator.rsi/meta.json new file mode 100644 index 00000000000000..0202c0c39c71b1 --- /dev/null +++ b/Resources/Textures/_NF/Objects/Devices/translator.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 2, + "license": "CC-BY-SA-3.0", + "copyright": "baystation12", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "translator" + } + ] +} diff --git a/Resources/Textures/_NF/Objects/Devices/translator.rsi/translator.png b/Resources/Textures/_NF/Objects/Devices/translator.rsi/translator.png new file mode 100644 index 00000000000000..6c54a0b86366ce Binary files /dev/null and b/Resources/Textures/_NF/Objects/Devices/translator.rsi/translator.png differ