diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs index 5022e540960..adbfe2d681f 100644 --- a/Content.Server/Language/TranslatorSystem.cs +++ b/Content.Server/Language/TranslatorSystem.cs @@ -1,15 +1,12 @@ using System.Linq; -using Content.Server.Language.Events; using Content.Server.Popups; using Content.Server.PowerCell; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Language; -using Content.Shared.Language.Events; using Content.Shared.Language.Systems; using Content.Shared.PowerCell; using Content.Shared.Language.Components.Translators; -using Robust.Shared.Utility; namespace Content.Server.Language; diff --git a/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs new file mode 100644 index 00000000000..e2d74ba5d9b --- /dev/null +++ b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs @@ -0,0 +1,36 @@ +using Content.Shared.Language; +using Content.Shared.Language.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server.Traits.Assorted; + +/// +/// When applied to a not-yet-spawned player entity, removes from the lists of their languages +/// and gives them a translator instead. +/// +[RegisterComponent] +public sealed partial class ForeignerTraitComponent : Component +{ + /// + /// The "base" language that is to be removed and substituted with a translator. + /// By default, equals to the fallback language, which is GalacticCommon. + /// + [DataField] + public ProtoId BaseLanguage = SharedLanguageSystem.FallbackLanguagePrototype; + + /// + /// Whether this trait prevents the entity from understanding the base language. + /// + public bool CantUnderstand = true; + + /// + /// Whether this trait prevents the entity from speaking the base language. + /// + public bool CantSpeak = true; + + /// + /// The base translator prototype to use when creating a translator for the entity. + /// + [DataField(required: true)] + public ProtoId BaseTranslator = default!; +} diff --git a/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs new file mode 100644 index 00000000000..58e974227ce --- /dev/null +++ b/Content.Server/Traits/Assorted/ForeignerTraitSystem.cs @@ -0,0 +1,105 @@ +using System.Linq; +using Content.Server.Hands.Systems; +using Content.Server.Language; +using Content.Server.Storage.EntitySystems; +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory; +using Content.Shared.Language; +using Content.Shared.Language.Components; +using Content.Shared.Language.Components.Translators; +using Content.Shared.Storage; +using Robust.Shared.Prototypes; + +namespace Content.Server.Traits.Assorted; + + +public sealed partial class ForeignerTraitSystem : EntitySystem +{ + [Dependency] private readonly EntityManager _entMan = default!; + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly LanguageSystem _languages = default!; + [Dependency] private readonly StorageSystem _storage = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnSpawn); // TraitSystem adds it after PlayerSpawnCompleteEvent so it's fine + } + + private void OnSpawn(Entity entity, ref ComponentInit args) + { + if (entity.Comp.CantUnderstand && !entity.Comp.CantSpeak) + Log.Warning($"Allowing entity {entity.Owner} to speak a language but not understand it leads to undefined behavior."); + + if (!TryComp(entity, out var knowledge)) + { + Log.Warning($"Entity {entity.Owner} does not have a LanguageKnowledge but has a ForeignerTrait!"); + return; + } + + var alternateLanguage = knowledge.SpokenLanguages.Find(it => it != entity.Comp.BaseLanguage); + if (alternateLanguage == null) + { + Log.Warning($"Entity {entity.Owner} does not have an alternative language to choose from (must have at least one non-GC for ForeignerTrait)!"); + return; + } + + if (TryGiveTranslator(entity.Owner, entity.Comp.BaseTranslator, entity.Comp.BaseLanguage, alternateLanguage, out var translator)) + { + _languages.RemoveLanguage(entity, entity.Comp.BaseLanguage, entity.Comp.CantSpeak, entity.Comp.CantUnderstand, knowledge); + } + } + + /// + /// Tries to create and give the entity a translator to translator that translates speech between the two specified languages. + /// + public bool TryGiveTranslator( + EntityUid uid, + string baseTranslatorPrototype, + ProtoId translatorLanguage, + ProtoId entityLanguage, + out EntityUid result) + { + result = EntityUid.Invalid; + if (translatorLanguage == entityLanguage) + return false; + + var translator = _entMan.SpawnNextToOrDrop(baseTranslatorPrototype, uid); + result = translator; + + if (!TryComp(translator, out var handheld)) + { + handheld = AddComp(translator); + handheld.ToggleOnInteract = true; + handheld.SetLanguageOnInteract = true; + } + + // Allows to speak the specified language and requires entities language. + handheld.SpokenLanguages = [translatorLanguage]; + handheld.UnderstoodLanguages = [translatorLanguage]; + handheld.RequiredLanguages = [entityLanguage]; + + // Try to put it in entities hand + if (_hands.TryPickupAnyHand(uid, translator, false, false, false)) + return true; + + // Try to find a valid clothing slot on the entity and equip the translator there + if (TryComp(translator, out var clothing) + && clothing.Slots != SlotFlags.NONE + && _inventory.TryGetSlots(uid, out var slots) + && slots.Any(it => _inventory.TryEquip(uid, translator, it.Name, true, false))) + return true; + + // Try to put the translator into entities bag, if it has one + if (_inventory.TryGetSlotEntity(uid, "back", out var bag) + && TryComp(bag, out var storage) + && _storage.Insert(bag.Value, translator, out _, null, storage, false, false)) + return true; + + // If all of the above has failed, just drop it at the same location as the entity + // This should ideally never happen, but who knows. + Transform(translator).Coordinates = Transform(uid).Coordinates; + + return true; + } +} diff --git a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs index 0632f5d9cb2..ddbdc742be4 100644 --- a/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs +++ b/Content.Shared/Language/Components/LanguageKnowledgeComponent.cs @@ -2,6 +2,8 @@ namespace Content.Shared.Language.Components; +// TODO: move to server side, it's never synchronized! + /// /// Stores data about entities' intrinsic language knowledge. /// diff --git a/Content.Shared/Language/Systems/SharedTranslatorSystem.cs b/Content.Shared/Language/Systems/SharedTranslatorSystem.cs index 08a016efa9c..4a72de791f0 100644 --- a/Content.Shared/Language/Systems/SharedTranslatorSystem.cs +++ b/Content.Shared/Language/Systems/SharedTranslatorSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Examine; using Content.Shared.Toggleable; using Content.Shared.Language.Components.Translators; @@ -17,11 +18,20 @@ public override void Initialize() private void OnExamined(EntityUid uid, HandheldTranslatorComponent component, ExaminedEvent args) { - var state = Loc.GetString(component.Enabled - ? "translator-enabled" - : "translator-disabled"); + var understoodLanguageNames = component.UnderstoodLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + var spokenLanguageNames = component.SpokenLanguages + .Select(it => Loc.GetString($"language-{it}-name")); + var requiredLanguageNames = component.RequiredLanguages + .Select(it => Loc.GetString($"language-{it}-name")); - args.PushMarkup(state); + args.PushMarkup(Loc.GetString("translator-examined-langs-understood", ("languages", string.Join(", ", understoodLanguageNames)))); + args.PushMarkup(Loc.GetString("translator-examined-langs-spoken", ("languages", string.Join(", ", spokenLanguageNames)))); + + args.PushMarkup(Loc.GetString(component.RequiresAllLanguages ? "translator-examined-requires-all" : "translator-examined-requires-any", + ("languages", string.Join(", ", requiredLanguageNames)))); + + args.PushMarkup(Loc.GetString(component.Enabled ? "translator-examined-enabled" : "translator-examined-disabled")); } protected void OnAppearanceChange(EntityUid translator, HandheldTranslatorComponent? comp = null) diff --git a/Resources/Locale/en-US/language/translator.ftl b/Resources/Locale/en-US/language/translator.ftl index b2a1e9b2b8c..8070d03be29 100644 --- a/Resources/Locale/en-US/language/translator.ftl +++ b/Resources/Locale/en-US/language/translator.ftl @@ -1,8 +1,13 @@ 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. + +translator-examined-langs-understood = It can translate from: [color=green]{$languages}[/color]. +translator-examined-langs-spoken = It can translate to: [color=green]{$languages}[/color]. +translator-examined-requires-any = It requires you to know at least one of these languages: [color=yellow]{$languages}[/color]. +translator-examined-requires-all = It requires you to know all of these languages: [color=yellow]{$languages}[/color]. +translator-examined-enabled = It appears to be [color=green]active[/color]. +translator-examined-disabled = It appears to be [color=red]turned off[/color]. diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 2f59d322282..f6e3e0b1fc1 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -42,3 +42,13 @@ trait-name-Thieving = Thieving trait-description-Thieving = You are deft with your hands, and talented at convincing people of their belongings. You can identify pocketed items, steal them quieter, and steal ~33% faster. + +trait-name-ForeignerLight = Foreigner (light) +trait-description-ForeignerLight = + You struggle to learn this station's primary language, and as such, cannot speak it. You can, however, comprehend what others say in that language. + To help you overcome this obstacle, you are equipped with a translator that helps you speak in this station's primary language. + +trait-name-Foreigner = Foreigner +trait-description-Foreigner = + For one reason or another you do not speak this station's primary language. + Instead, you have a translator issued to you that only you can use. diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml index 664626ea4b4..b28541253d4 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml @@ -1,5 +1,5 @@ - type: entity - abstract: true + noSpawn: true id: TranslatorUnpowered parent: BaseItem name: translator @@ -23,9 +23,14 @@ False: { visible: false } - type: HandheldTranslator enabled: false + - type: Clothing # To allow equipping translators on the neck slot + slots: [neck, pocket] + equipDelay: 0.3 + unequipDelay: 0.3 + quickEquip: false # Would conflict - type: entity - abstract: true + noSpawn: true id: Translator parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ] suffix: Powered @@ -34,7 +39,7 @@ drawRate: 1 - type: entity - abstract: true + noSpawn: true id: TranslatorEmpty parent: Translator suffix: Empty @@ -44,6 +49,13 @@ cell_slot: name: power-cell-slot-component-slot-name-default +- type: entity + noSpawn: true + id: TranslatorForeigner + parent: [ Translator, PowerCellSlotHighItem ] + name: foreigner's translator + description: A special-issue translator that helps foreigner's speak and understand this station's primary language. + - type: entity id: CanilunztTranslator diff --git a/Resources/Prototypes/Traits/inconveniences.yml b/Resources/Prototypes/Traits/inconveniences.yml index dcf53d9ab7f..8dc0264ffea 100644 --- a/Resources/Prototypes/Traits/inconveniences.yml +++ b/Resources/Prototypes/Traits/inconveniences.yml @@ -26,3 +26,26 @@ fourRandomProb: 0 threeRandomProb: 0 cutRandomProb: 0 + +- type: trait + id: ForeignerLight + category: Mental + points: 1 + requirements: + - !type:TraitGroupExclusionRequirement + prototypes: [ Foreigner ] + components: + - type: ForeignerTrait + cantUnderstand: false # Allows to understand + baseTranslator: TranslatorForeigner + +- type: trait + id: Foreigner + category: Mental + points: 2 + requirements: # TODO: Add a requirement to know at least 1 non-gc language + - !type:TraitGroupExclusionRequirement + prototypes: [ ForeignerLight ] + components: + - type: ForeignerTrait + baseTranslator: TranslatorForeigner