diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 4ec4b028376..00cc247d9df 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Numerics; using System.Threading; +using Content.Client._Scp.Knowledge; // Fire added - client-side SCP knowledge examine prediction using Content.Client.Verbs; using Content.Shared.Examine; using Content.Shared.IdentityManagement; @@ -32,6 +33,8 @@ public sealed class ExamineSystem : ExamineSystemShared [Dependency] private readonly VerbSystem _verbSystem = default!; [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly ScpKnowledgeSystem _scpKnowledge = default!; // Fire added - predict knowledge-gated examine identity and hint buttons + private List _verbList = new(); public const string StyleClassEntityTooltip = "entity-tooltip"; @@ -158,7 +161,7 @@ private void OnExamineInfoResponse(ExamineSystemMessages.ExamineInfoResponseMess // since there's probably one open already if it's coming in from the server. var entity = GetEntity(ev.EntityUid); - // Fire added start + // Fire added start - ignore stale examine replies for entities that disappeared before the server answered if (TerminatingOrDeleted(entity)) { CloseTooltip(); @@ -166,7 +169,8 @@ private void OnExamineInfoResponse(ExamineSystemMessages.ExamineInfoResponseMess } // Fire added end - OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget); + // Fire edit - apply server-provided knowledge-based name override to the tooltip title + OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget, ev.NameOverride); UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false); } @@ -181,7 +185,7 @@ public override void SendExamineTooltip(EntityUid player, EntityUid target, Form /// not fill it with information. This is done when the server sends examine info/verbs, /// or immediately if it's entirely clientside. /// - public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true) + public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true, string? nameOverride = null) // Fire edit - allow knowledge systems to override the displayed examine name { // Close any examine tooltip that might already be opened // Before we do that, save its position. We'll prioritize opening any new popups there if @@ -246,7 +250,8 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso if (knowTarget) { - var itemName = FormattedMessage.EscapeText(Identity.Name(target, EntityManager, player)); + // Fire edit - prefer knowledge-gated identity override when examine prediction/server response provides one + var itemName = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(target, EntityManager, player)); var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]"); var label = new RichTextLabel(); label.SetMessage(labelMessage); @@ -411,14 +416,38 @@ public void DoExamine(EntityUid entity, bool centeredOnCursor = true, EntityUid? return; FormattedMessage message; + var knowTarget = true; + string? nameOverride = null; + List? predictedVerbs = null; // Fire added - predicted OOC knowledge hint buttons + + // Fire added start - predict SCP knowledge-gated examine locally + if (_scpKnowledge.TryGetPredictedExamineData(entity, out var predictedKnowTarget, out var predictedNameOverride, out var predictedMessage)) + { + knowTarget = predictedKnowTarget; + nameOverride = predictedNameOverride; + message = predictedMessage ?? GetExamineText(entity, playerEnt); + } + else + { + message = GetExamineText(entity, playerEnt); + } + // Fire added end + + // Fire added start - predict SCP knowledge examine verbs locally + predictedVerbs = new List(); + _scpKnowledge.AddPredictedExamineVerbs(playerEnt.Value, entity, predictedVerbs); + if (predictedVerbs.Count == 0) + predictedVerbs = null; + // Fire added end - OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false); + // Fire edit start - apply predicted SCP identity and hint-button overrides before the server reply arrives + OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false, knowTarget, nameOverride); // Always update tooltip info from client first. // If we get it wrong, server will correct us later anyway. // This will usually be correct (barring server-only components, which generally only adds, not replaces text) - message = GetExamineText(entity, playerEnt); - UpdateTooltipInfo(playerEnt.Value, entity, message); + UpdateTooltipInfo(playerEnt.Value, entity, message, predictedVerbs); + // Fire edit end if (!IsClientSide(entity)) { diff --git a/Content.Client/Paper/UI/PaperBoundUserInterface.cs b/Content.Client/Paper/UI/PaperBoundUserInterface.cs index ec417f749b9..5aafbd7e164 100644 --- a/Content.Client/Paper/UI/PaperBoundUserInterface.cs +++ b/Content.Client/Paper/UI/PaperBoundUserInterface.cs @@ -13,6 +13,8 @@ public sealed class PaperBoundUserInterface : BoundUserInterface [ViewVariables] private PaperWindow? _window; + private PaperKnowledgeHighlightMessage? _highlightMessage; // Fire added - cache server-provided knowledge highlights between UI messages + public PaperBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { } @@ -37,8 +39,34 @@ protected override void Open() protected override void UpdateState(BoundUserInterfaceState state) { base.UpdateState(state); - _window?.Populate((PaperBoundUserInterfaceState) state); + + // Fire added start - reuse the server-provided highlighted paper text when it still matches the current state + var paperState = (PaperBoundUserInterfaceState) state; + string? highlightedText = null; + + if (_highlightMessage != null && paperState.Text == _highlightMessage.RawText) + highlightedText = _highlightMessage.HighlightedText; + else + _highlightMessage = null; + // Fire added end + + _window?.Populate(paperState, highlightedText); // Fire edit - pass highlighted paper text for knowledge hint rendering + } + + // Fire added start - receive out-of-band paper knowledge highlight updates without waiting for a full state resend + protected override void ReceiveMessage(BoundUserInterfaceMessage message) + { + base.ReceiveMessage(message); + + if (message is not PaperKnowledgeHighlightMessage highlight) + return; + + _highlightMessage = highlight; + + if (State is PaperBoundUserInterfaceState state) + _window?.Populate(state, state.Text == highlight.RawText ? highlight.HighlightedText : null); } + // Fire added end private void InputOnTextEntered(string text) { diff --git a/Content.Client/Paper/UI/PaperWindow.xaml.cs b/Content.Client/Paper/UI/PaperWindow.xaml.cs index 47964d20f85..6e020a545a2 100644 --- a/Content.Client/Paper/UI/PaperWindow.xaml.cs +++ b/Content.Client/Paper/UI/PaperWindow.xaml.cs @@ -246,7 +246,7 @@ protected override void Draw(DrawingHandleScreen handle) /// Initialize the paper contents, i.e. the text typed by the /// user and any stamps that have peen put on the page. /// - public void Populate(PaperComponent.PaperBoundUserInterfaceState state) + public void Populate(PaperComponent.PaperBoundUserInterfaceState state, string? highlightedText = null) // Fire edit - allow paper knowledge highlights to override rendered text { bool isEditing = state.Mode == PaperComponent.PaperAction.Write; bool wasEditing = InputContainer.Visible; @@ -254,7 +254,7 @@ public void Populate(PaperComponent.PaperBoundUserInterfaceState state) EditButtons.Visible = isEditing; var msg = new FormattedMessage(); - msg.AddMarkupPermissive(state.Text); + msg.AddMarkupPermissive(highlightedText ?? state.Text); // Fire edit - render highlighted paper text when knowledge hints are available // For premade documents, we want to be able to edit them rather than // replace them. diff --git a/Content.Client/_Scp/Knowledge/ScpKnowledgeSystem.cs b/Content.Client/_Scp/Knowledge/ScpKnowledgeSystem.cs new file mode 100644 index 00000000000..c7663a8ed06 --- /dev/null +++ b/Content.Client/_Scp/Knowledge/ScpKnowledgeSystem.cs @@ -0,0 +1,149 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Client.Examine; +using Content.Shared._Scp.Knowledge; +using Content.Shared._Scp.Knowledge.Components; +using Content.Shared.Examine; +using Content.Shared.Verbs; +using Robust.Client.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client._Scp.Knowledge; + +public sealed class ScpKnowledgeSystem : EntitySystem +{ + [Dependency] private readonly ExamineSystem _examine = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private readonly Dictionary>> _knowledgeByEntityPrototype = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPrototypesReloaded); + + RebuildCaches(); + } + + public bool TryGetPredictedExamineData( + EntityUid target, + out bool knowTarget, + out string? nameOverride, + out FormattedMessage? message) + { + knowTarget = true; + nameOverride = null; + message = null; + + if (!TryGetLocalKnowledgeState(out var knowledgeState) || + !TryGetKnowledgeIdsForEntity(target, out var knowledgeIds)) + { + return false; + } + + var requiresKnowledge = false; + for (var i = 0; i < knowledgeIds.Count; i++) + { + var knowledgeId = knowledgeIds[i]; + var knowledge = _prototype.Index(knowledgeId); + if (!knowledge.HideIdentityUntilKnown) + continue; + + requiresKnowledge = true; + if (!ScpKnowledgeLogic.WillBeKnownAfterExamine(knowledgeState, knowledgeId, knowledge)) + continue; + + nameOverride = Loc.GetString(knowledge.DisplayName); + return true; + } + + if (!requiresKnowledge) + return false; + + knowTarget = false; + message = new FormattedMessage(); + message.AddText(Loc.GetString("scp-knowledge-unknown-examine")); + return true; + } + + public void AddPredictedExamineVerbs(EntityUid user, EntityUid target, List verbs) + { + if (_player.LocalEntity != user || + !TryComp(user, out ScpKnowledgeComponent? knowledgeState) || + !TryGetKnowledgeIdsForEntity(target, out var knowledgeIds)) + { + return; + } + + for (var i = 0; i < knowledgeIds.Count; i++) + { + var knowledgeId = knowledgeIds[i]; + var knowledge = _prototype.Index(knowledgeId); + if (!ScpKnowledgeLogic.WillBeKnownAfterExamine(knowledgeState, knowledgeId, knowledge) || + knowledge.KnownExamineVerbText == null || + knowledge.KnownExamineText == null) + { + continue; + } + + var examineMessage = new FormattedMessage(); + examineMessage.AddText(Loc.GetString(knowledge.KnownExamineText.Value)); + + var examineVerb = new ExamineVerb + { + Act = () => _examine.SendExamineTooltip(user, target, examineMessage, false, false), + Text = Loc.GetString(knowledge.KnownExamineVerbText.Value), + Category = VerbCategory.Examine, + Icon = new SpriteSpecifier.Texture(new(ExamineSystemShared.DefaultIconTexture)), + ClientExclusive = true, + }; + + verbs.Add(examineVerb); + } + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified()) + return; + + RebuildCaches(); + } + + private bool TryGetLocalKnowledgeState([NotNullWhen(true)] out ScpKnowledgeComponent? knowledgeState) + { + knowledgeState = null; + return _player.LocalEntity is { } localEntity && TryComp(localEntity, out knowledgeState); + } + + private void RebuildCaches() + { + _knowledgeByEntityPrototype.Clear(); + + foreach (var knowledge in _prototype.EnumeratePrototypes()) + { + for (var i = 0; i < knowledge.EntityPrototypes.Count; i++) + { + var entityPrototype = knowledge.EntityPrototypes[i]; + if (!_knowledgeByEntityPrototype.TryGetValue(entityPrototype, out var knowledgeIds)) + { + knowledgeIds = []; + _knowledgeByEntityPrototype[entityPrototype] = knowledgeIds; + } + + knowledgeIds.Add(knowledge.ID); + } + } + } + + private bool TryGetKnowledgeIdsForEntity( + EntityUid target, + [NotNullWhen(true)] out List>? knowledgeIds) + { + knowledgeIds = null; + var prototypeId = Prototype(target)?.ID; + return prototypeId != null && _knowledgeByEntityPrototype.TryGetValue(prototypeId, out knowledgeIds); + } +} diff --git a/Content.IntegrationTests/Tests/_Scp/ScpKnowledgeSystemTest.cs b/Content.IntegrationTests/Tests/_Scp/ScpKnowledgeSystemTest.cs new file mode 100644 index 00000000000..4073918d345 --- /dev/null +++ b/Content.IntegrationTests/Tests/_Scp/ScpKnowledgeSystemTest.cs @@ -0,0 +1,640 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Content.IntegrationTests.Pair; +using Content.Server._Scp.Knowledge; +using Content.Server.Chat.Systems; +using Content.Server.Mind; +using Content.Server.Roles; +using Content.Shared.Chat; +using Content.Shared.Examine; +using Content.Shared._Scp.Knowledge; +using Content.Shared._Scp.Knowledge.Components; +using Content.Shared.Paper; +using Content.Shared.Roles; +using Content.Shared.UserInterface; +using Robust.Server.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Prototypes; + +namespace Content.IntegrationTests.Tests._Scp; + +[TestFixture] +public sealed class ScpKnowledgeSystemTest +{ + private const string PlayerPrototype = "MobHuman"; + private const string ClassDJob = "ClassD"; + + private static readonly ProtoId KnowledgeContainmentBreach = "ScpKnowledgeTermContainmentBreach"; + private static readonly ProtoId KnowledgeClassDZone = "ScpKnowledgeLocationClassDContainmentZone"; + private static readonly ProtoId KnowledgeHeavyContainment = "ScpKnowledgeTermHeavyContainmentZone"; + private static readonly ProtoId KnowledgeScp131 = "ScpKnowledgeEntityScp131"; + private static readonly ProtoId KnowledgeScp173 = "ScpKnowledgeEntityScp173"; + + [Test] + public async Task SpeechDoesNotMatchArbitraryGapsInScpDesignation() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var knowledge = pair.Server.System(); + EntityUid speaker = default; + + await pair.Server.WaitPost(() => + { + var coordinates = pair.Server.EntMan.GetComponent(setup.Body).Coordinates; + speaker = pair.Server.EntMan.SpawnEntity(PlayerPrototype, coordinates); + Assert.That(knowledge.TryGrantKnowledgeProgress(speaker, KnowledgeScp173, 2), Is.True); + + var chat = pair.Server.System(); + chat.TrySendInGameICMessage( + speaker, + "scp many random words 173", + InGameICChatType.Speak, + ChatTransmitRange.Normal, + ignoreActionBlocker: true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out _), Is.False); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task HumanKnowledgeIsConfiguredOnBodyViaJobSpecial() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var knowledge = pair.Server.System(); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(pair.Server.EntMan.TryGetComponent(setup.Body, out ScpKnowledgeComponent? _), Is.True); + Assert.That(pair.Server.EntMan.TryGetComponent(setup.Body, out ScpKnowledgeAcquisitionComponent? _), Is.True); + Assert.That(pair.Server.EntMan.TryGetComponent(setup.Mind, out ScpKnowledgeComponent? _), Is.False); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeContainmentBreach), Is.False); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeClassDZone), Is.False); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeHeavyContainment), Is.False); + }); + }); + + await pair.Server.WaitPost(() => ApplyJobToBody(pair, setup.Mind, setup.Body, ClassDJob)); + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeContainmentBreach), Is.True); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeClassDZone), Is.True); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeHeavyContainment), Is.False); + }); + }); + + EntityUid secondBody = default; + await pair.Server.WaitPost(() => + { + var entMan = pair.Server.EntMan; + var mindSystem = pair.Server.System(); + var coordinates = entMan.GetComponent(setup.Body).Coordinates; + + secondBody = entMan.SpawnEntity(PlayerPrototype, coordinates); + mindSystem.TransferTo(setup.Mind, secondBody); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(pair.Server.EntMan.TryGetComponent(secondBody, out ScpKnowledgeComponent? _), Is.True); + Assert.That(knowledge.HasKnowledge(secondBody, KnowledgeClassDZone), Is.False); + Assert.That(knowledge.HasKnowledge(secondBody, KnowledgeContainmentBreach), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task LocalSpeechAndRadioRequireExamineToUnlockScpKnowledge() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var knowledge = pair.Server.System(); + EntityUid speaker = default; + EntityUid scp173 = default; + + await pair.Server.WaitPost(() => + { + var coordinates = pair.Server.EntMan.GetComponent(setup.Body).Coordinates; + speaker = pair.Server.EntMan.SpawnEntity(PlayerPrototype, coordinates); + + var chat = pair.Server.System(); + chat.TrySendInGameICMessage( + speaker, + "объекту 173", + InGameICChatType.Speak, + ChatTransmitRange.Normal, + ignoreActionBlocker: true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out _), Is.False); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.False); + }); + }); + + await pair.Server.WaitPost(() => + { + Assert.That(knowledge.TryGrantKnowledgeProgress(speaker, KnowledgeScp173, 2), Is.True); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitPost(() => + { + var chat = pair.Server.System(); + chat.TrySendInGameICMessage( + speaker, + "SCP-173", + InGameICChatType.Speak, + ChatTransmitRange.Normal, + ignoreActionBlocker: true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.False); + }); + }); + + await pair.Server.WaitPost(() => + { + var chat = pair.Server.System(); + chat.TrySendInGameICMessage( + speaker, + "SCP-173", + InGameICChatType.Speak, + ChatTransmitRange.Normal, + ignoreActionBlocker: true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.False); + }); + }); + + await pair.Server.WaitPost(() => + { + var coordinates = pair.Server.EntMan.GetComponent(setup.Body).Coordinates; + scp173 = pair.Server.EntMan.SpawnEntity("Scp173", coordinates); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitPost(() => + { + pair.Server.EntMan.EventBus.RaiseLocalEvent( + speaker, + new RadioSpokeEvent(speaker, "Костоломка", [setup.Body]), + true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.False); + }); + }); + + await pair.Server.WaitPost(() => + { + Assert.That(knowledge.TryGrantExamineKnowledge(setup.Body, scp173), Is.True); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(2)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task PaperReadRequiresKnowledgeableAuthorOrCodeAndDoesNotFarmTheSameSource() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var knowledge = pair.Server.System(); + EntityUid ignorantWriter = default; + EntityUid knowledgeableWriter = default; + EntityUid scp131 = default; + + EntityUid ignorantPaper = default; + EntityUid knowledgeablePaper = default; + EntityUid codePaper = default; + await pair.Server.WaitPost(() => + { + var entMan = pair.Server.EntMan; + var coordinates = entMan.GetComponent(setup.Body).Coordinates; + ignorantWriter = entMan.SpawnEntity(PlayerPrototype, coordinates); + knowledgeableWriter = entMan.SpawnEntity(PlayerPrototype, coordinates); + ignorantPaper = entMan.SpawnEntity("Paper", coordinates); + knowledgeablePaper = entMan.SpawnEntity("Paper", coordinates); + codePaper = entMan.SpawnEntity("Paper", coordinates); + scp131 = entMan.SpawnEntity("Scp131A", coordinates); + + Assert.That(knowledge.TryGrantKnowledgeProgress(knowledgeableWriter, KnowledgeScp131, 2), Is.True); + + var paperSystem = pair.Server.System(); + paperSystem.SetContent(ignorantPaper, "scp 131", ignorantWriter); + paperSystem.SetContent(knowledgeablePaper, "scp 131", knowledgeableWriter); + paperSystem.SetContent(codePaper, "scp 131"); + }); + + await pair.Server.WaitPost(() => + { + pair.Server.EntMan.EventBus.RaiseLocalEvent( + ignorantPaper, + new BeforeActivatableUIOpenEvent(setup.Body), + true); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp131, out _), Is.False); + }); + + await pair.Server.WaitPost(() => + { + pair.Server.EntMan.EventBus.RaiseLocalEvent( + knowledgeablePaper, + new BeforeActivatableUIOpenEvent(setup.Body), + true); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp131, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp131), Is.False); + }); + + await pair.Server.WaitPost(() => + { + pair.Server.EntMan.EventBus.RaiseLocalEvent( + knowledgeablePaper, + new BeforeActivatableUIOpenEvent(setup.Body), + true); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp131, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp131), Is.False); + }); + + await pair.Server.WaitPost(() => + { + pair.Server.EntMan.EventBus.RaiseLocalEvent( + codePaper, + new BeforeActivatableUIOpenEvent(setup.Body), + true); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp131, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp131), Is.False); + }); + + await pair.Server.WaitPost(() => + { + Assert.That(knowledge.TryGrantExamineKnowledge(setup.Body, scp131), Is.True); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp131, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(2)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp131), Is.True); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task PaperOnlyAttributesLiteralChangesToKnowledgeableWriter() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var knowledge = pair.Server.System(); + EntityUid paper = default; + EntityUid knowledgeableWriter = default; + + await pair.Server.WaitPost(() => + { + var entMan = pair.Server.EntMan; + var coordinates = entMan.GetComponent(setup.Body).Coordinates; + paper = entMan.SpawnEntity("Paper", coordinates); + knowledgeableWriter = entMan.SpawnEntity(PlayerPrototype, coordinates); + + Assert.That(knowledge.TryGrantKnowledgeProgress(knowledgeableWriter, KnowledgeScp131, 2), Is.True); + + var paperSystem = pair.Server.System(); + paperSystem.SetContent(paper, "абзац про еду\nSCP-131", setup.Body); + paperSystem.SetContent(paper, "правка\nSCP-131", knowledgeableWriter); + }); + + await pair.Server.WaitPost(() => + { + pair.Server.EntMan.EventBus.RaiseLocalEvent( + paper, + new BeforeActivatableUIOpenEvent(setup.Body), + true); + }); + + await pair.RunTicksSync(2); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp131, out _), Is.False); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task ExamineHidesUnknownScpAndRevealsItAfterUnlock() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var probe = pair.Client.System(); + var knowledge = pair.Server.System(); + EntityUid speaker = default; + + EntityUid scp173 = default; + await pair.Server.WaitPost(() => + { + var coordinates = pair.Server.EntMan.GetComponent(setup.Body).Coordinates; + scp173 = pair.Server.EntMan.SpawnEntity("Scp173", coordinates); + speaker = pair.Server.EntMan.SpawnEntity(PlayerPrototype, coordinates); + }); + + await pair.RunTicksSync(5); + + var netEntity = pair.Server.EntMan.GetNetEntity(scp173); + + await pair.Client.WaitPost(() => probe.Reset()); + await pair.Client.WaitPost(() => probe.Request(netEntity, getVerbs: true)); + await pair.RunTicksSync(5); + + await pair.Client.WaitAssertion(() => + { + Assert.That(probe.Responses, Has.Count.EqualTo(1)); + + var response = probe.Responses.Single(); + Assert.Multiple(() => + { + Assert.That(response.KnowTarget, Is.False); + Assert.That(response.NameOverride, Is.Null); + Assert.That(response.Verbs, Is.Null.Or.Empty); + }); + }); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out var progress), Is.True); + Assert.That(progress, Is.EqualTo(1)); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.False); + }); + + await pair.Server.WaitPost(() => + { + Assert.That(knowledge.TryGrantKnowledgeProgress(speaker, KnowledgeScp173, 2), Is.True); + pair.Server.System().TrySendInGameICMessage( + speaker, + "SCP-173", + InGameICChatType.Speak, + ChatTransmitRange.Normal, + ignoreActionBlocker: true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp173), Is.True); + }); + + await pair.Client.WaitPost(() => probe.Reset()); + await pair.Client.WaitPost(() => probe.Request(netEntity, getVerbs: true)); + await pair.RunTicksSync(5); + + await pair.Client.WaitAssertion(() => + { + Assert.That(probe.Responses, Has.Count.EqualTo(1)); + + var response = probe.Responses.Single(); + Assert.Multiple(() => + { + Assert.That(response.KnowTarget, Is.True); + Assert.That(response.NameOverride, Is.EqualTo(Loc.GetString("scp-knowledge-scp173-name"))); + Assert.That(response.Verbs, Is.Not.Null.And.Not.Empty); + Assert.That(response.Verbs!.Any(verb => verb.Text == Loc.GetString("scp-knowledge-known-examine-verb")), Is.True); + }); + }); + + await pair.CleanReturnAsync(); + } + + [Test] + public async Task OrdinaryEntityAndUnknownTermDoNotAffectKnowledge() + { + await using var pair = await GetKnowledgePair(); + var setup = await SpawnPlayerMind(pair); + var knowledge = pair.Server.System(); + + EntityUid paper = default; + await pair.Server.WaitPost(() => + { + var coordinates = pair.Server.EntMan.GetComponent(setup.Body).Coordinates; + paper = pair.Server.EntMan.SpawnEntity("Paper", coordinates); + pair.Server.System().TrySendInGameICMessage( + setup.Body, + "абсолютно неизвестный термин", + InGameICChatType.Speak, + ChatTransmitRange.Normal, + ignoreActionBlocker: true); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + Assert.Multiple(() => + { + Assert.That(knowledge.TryGetKnowledgeProgress(setup.Body, KnowledgeScp173, out _), Is.False); + Assert.That(knowledge.TryGetUnknownExamineMessage(setup.Body, paper, out _), Is.False); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "SCP-173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "scp 173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "scp 131"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "сцп 173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "объект 173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "объекту 173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "объектом 173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "объекте 173"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "объект 131"), Does.Contain("[color=")); + Assert.That(knowledge.HighlightUnknownKnowledgeText(setup.Body, "абсолютно неизвестный термин"), Is.EqualTo("абсолютно неизвестный термин")); + Assert.That(knowledge.HasKnowledge(setup.Body, KnowledgeScp131), Is.False); + }); + }); + + await pair.CleanReturnAsync(); + } + + private static Task GetKnowledgePair() + { + return PoolManager.GetServerClient(new PoolSettings + { + Connected = true, + Dirty = true, + Fresh = true, + }); + } + + private static async Task SpawnPlayerMind(TestPair pair, string? jobId = null) + { + var map = await pair.CreateTestMap(); + EntityUid mind = default; + EntityUid body = default; + + await pair.Server.WaitPost(() => + { + var entMan = pair.Server.EntMan; + var player = pair.Server.ResolveDependency().Sessions.Single(); + var mindSystem = pair.Server.System(); + var initialCoordinates = map.GridCoords.Offset(new Vector2(0.5f, 0.5f)); + + mind = mindSystem.CreateMind(player.UserId); + body = entMan.SpawnEntity(PlayerPrototype, initialCoordinates); + mindSystem.TransferTo(mind, body); + + if (jobId != null) + ApplyJobToBody(pair, mind, body, jobId); + }); + + await pair.RunTicksSync(5); + + await pair.Server.WaitAssertion(() => + { + var session = pair.Server.ResolveDependency().Sessions.Single(); + Assert.That(session.AttachedEntity, Is.EqualTo(body)); + }); + + await pair.Client.WaitAssertion(() => + { + Assert.That(pair.Client.EntMan.EntityExists(pair.ToClientUid(body)), Is.True); + }); + + return new PlayerMindSetup(mind, body); + } + + private static void ApplyJobToBody(TestPair pair, EntityUid mind, EntityUid body, string jobId) + { + var roleSystem = pair.Server.System(); + var prototypeManager = pair.Server.ResolveDependency(); + + roleSystem.MindAddJobRole(mind, silent: true, jobPrototype: jobId); + + var job = prototypeManager.Index(jobId); + foreach (var special in job.Special) + { + special.AfterEquip(body); + } + } + + private readonly record struct PlayerMindSetup(EntityUid Mind, EntityUid Body); +} + +public sealed class ScpKnowledgeExamineProbeSystem : EntitySystem +{ + public readonly List Responses = new(); + + public override void Initialize() + { + base.Initialize(); + SubscribeNetworkEvent(OnResponse); + } + + public void Reset() + { + Responses.Clear(); + } + + public void Request(NetEntity entity, bool getVerbs) + { + RaiseNetworkEvent(new ExamineSystemMessages.RequestExamineInfoMessage(entity, 1, getVerbs)); + } + + private void OnResponse(ExamineSystemMessages.ExamineInfoResponseMessage message) + { + Responses.Add(message); + } +} diff --git a/Content.IntegrationTests/_Sunrise/DummyMidiManager.cs b/Content.IntegrationTests/_Sunrise/DummyMidiManager.cs index 0b0a8c8a571..4092304986b 100644 --- a/Content.IntegrationTests/_Sunrise/DummyMidiManager.cs +++ b/Content.IntegrationTests/_Sunrise/DummyMidiManager.cs @@ -1,6 +1,5 @@ #nullable enable // ReSharper disable once CheckNamespace -#pragma warning disable IDE0130 using System.Collections.Generic; using NFluidsynth; using Robust.Client.Audio.Midi; diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index e57e60cc979..5e85cdd5322 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -69,7 +69,7 @@ public sealed partial class ChatSystem : SharedChatSystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly AnnouncementSpeakerSystem _announcementSpeaker = default!; - public const string DefaultAnnouncementSound = "/Audio/_Scp/Effects/announcement.ogg"; // Fire edit + public const string DefaultAnnouncementSound = "/Audio/_Scp/Effects/announcement.ogg"; // Fire edit - use the SCP announcement asset instead of the upstream default sound private bool _loocEnabled = true; private bool _deadLoocEnabled; @@ -597,7 +597,11 @@ private void SendEntitySpeak( ("fontSize", speech.FontSize), ("message", isFormatted ? message : FormattedMessage.EscapeText(message))); //sunrise-edit - SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range); + // Fire added start - reuse one SCP knowledge analysis for local speech delivery and learning + var scpKnowledgeAnalysis = CreateScpKnowledgeTextAnalysis(message); + SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range, scpKnowledgeAnalysis: scpKnowledgeAnalysis); + RaiseScpKnowledgeSpeakEvent(source, scpKnowledgeAnalysis, range); + // Fire added end var ev = new EntitySpokeEvent(source, message, null, null); RaiseLocalEvent(source, ev, true); @@ -670,6 +674,15 @@ private void SendEntityWhisper( var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message", ("message", isFormatted ? obfuscatedMessage : FormattedMessage.EscapeText(obfuscatedMessage))); //sunrise-edit + // Fire added start - track actual whisper listeners for SCP knowledge + var clearListeners = new List(); + var obfuscatedListeners = new List(); + // Fire added end + + // Fire added start - reuse SCP knowledge analysis for whisper delivery and learning + var clearScpKnowledgeAnalysis = CreateScpKnowledgeTextAnalysis(message); + var obfuscatedScpKnowledgeAnalysis = CreateScpKnowledgeTextAnalysis(obfuscatedMessage); + // Fire added end foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange)) { @@ -683,15 +696,34 @@ private void SendEntityWhisper( 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. if (data.Range <= WhisperClearRange || data.Observer) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel); + { + var deliveredWrappedMessage = GetScpKnowledgeWrappedMessage(listener, source, message, wrappedMessage, clearScpKnowledgeAnalysis); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, message, deliveredWrappedMessage, source, false, session.Channel); + // Fire added - clear whisper recognition + clearListeners.Add(listener); + } //If listener is too far, they only hear fragments of the message else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange)) - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel); + { + var deliveredWrappedMessage = GetScpKnowledgeWrappedMessage(listener, source, obfuscatedMessage, wrappedobfuscatedMessage, obfuscatedScpKnowledgeAnalysis); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, deliveredWrappedMessage, source, false, session.Channel); + // Fire added - muffled whisper recognition + obfuscatedListeners.Add(listener); + } //If listener is too far and has no line of sight, they can't identify the whisperer's identity else - _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel); + { + var deliveredWrappedMessage = GetScpKnowledgeWrappedMessage(listener, source, obfuscatedMessage, wrappedUnknownMessage, obfuscatedScpKnowledgeAnalysis); + _chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, deliveredWrappedMessage, source, false, session.Channel); + // Fire added - unknown whisper recognition still uses obfuscated text + obfuscatedListeners.Add(listener); + } } + // Fire added start - deliver whisper text to SCP knowledge listeners + RaiseScpKnowledgeWhisperEvent(source, clearListeners, obfuscatedListeners, clearScpKnowledgeAnalysis, obfuscatedScpKnowledgeAnalysis); + // Fire added end + _replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage); @@ -918,7 +950,15 @@ private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChat /// /// Sends a chat message to the given players in range of the source entity. /// - public void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null, Color? color = null) + public void SendInVoiceRange( + ChatChannel channel, + string message, + string wrappedMessage, + EntityUid source, + ChatTransmitRange range, + NetUserId? author = null, + Color? color = null, + Content.Server._Scp.Knowledge.ScpKnowledgeTextAnalysis? scpKnowledgeAnalysis = null) { foreach (var (session, data) in GetRecipients(source, VoiceRange)) { @@ -926,7 +966,17 @@ public void SendInVoiceRange(ChatChannel channel, string message, string wrapped if (entRange == MessageRangeCheckResult.Disallowed) continue; var entHideChat = entRange == MessageRangeCheckResult.HideChat; - _chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author, colorOverride: color); + var deliveredWrappedMessage = wrappedMessage; + + // Fire added start - personalize SCP knowledge highlights for local speech + if (channel == ChatChannel.Local && + session.AttachedEntity is { Valid: true } listener) + { + deliveredWrappedMessage = GetScpKnowledgeWrappedMessage(listener, source, message, wrappedMessage, scpKnowledgeAnalysis); + } + // Fire added end + + _chatManager.ChatMessageToOne(channel, message, deliveredWrappedMessage, source, entHideChat, session.Channel, author: author, colorOverride: color); } _replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range))); @@ -1032,7 +1082,7 @@ public string SanitizeMessageReplaceWords(string message) var msg = message; - /* Fire edit - В ПИЗДУ + /* Fire edit - keep the Sunrise replacement accent disabled until its curated replacement set is restored msg = _wordreplacement.ApplyReplacements(msg, ChatSanitizeAccent); */ diff --git a/Content.Server/Examine/ExamineSystem.cs b/Content.Server/Examine/ExamineSystem.cs index 480974e28f3..717a8519e55 100644 --- a/Content.Server/Examine/ExamineSystem.cs +++ b/Content.Server/Examine/ExamineSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server._Scp.Knowledge; // Fire added - knowledge-gated SCP examine responses using Content.Server.Verbs; using Content.Shared.Examine; using Content.Shared.Verbs; @@ -11,6 +12,7 @@ namespace Content.Server.Examine [UsedImplicitly] public sealed class ExamineSystem : ExamineSystemShared { + [Dependency] private readonly ScpKnowledgeSystem _scpKnowledge = default!; // Fire added - filter SCP identity and known examine name overrides [Dependency] private readonly VerbSystem _verbSystem = default!; private readonly FormattedMessage _entityNotFoundMessage = new(); @@ -65,13 +67,32 @@ private void ExamineInfoRequest(ExamineSystemMessages.RequestExamineInfoMessage return; } + // Fire added start - still grant SCP knowledge from detail examine before hidden identity short-circuit + _scpKnowledge.TryGrantExamineKnowledge(playerEnt, entity); + // Fire added end + + // Fire added start - hide SCP entity identity until the mind learns it + if (_scpKnowledge.TryGetUnknownExamineMessage(playerEnt, entity, out var unknownExamineMessage)) + { + RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage( + request.NetEntity, + request.Id, + unknownExamineMessage, + knowTarget: false), channel); + return; + } + // Fire added end + SortedSet? verbs = null; if (request.GetVerbs) verbs = _verbSystem.GetLocalVerbs(entity, playerEnt, typeof(ExamineVerb)); + // Fire added start - attach knowledge-based name override for known SCP examine responses + _scpKnowledge.TryGetKnownExamineNameOverride(playerEnt, entity, out var nameOverride); var text = GetExamineText(entity, player.AttachedEntity); RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage( - request.NetEntity, request.Id, text, verbs?.ToList()), channel); + request.NetEntity, request.Id, text, verbs?.ToList(), nameOverride: nameOverride), channel); + // Fire added end } } } diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 4eefe20865f..119085f7115 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -120,7 +120,10 @@ private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, ref Rad if (TryComp(parent, out ActorComponent? actor)) { - _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); + // Fire edit start - personalize headset radio chat per listener for SCP knowledge highlights + var chatMessage = GetScpKnowledgeRadioChatMessage(parent, args.MessageSource, args.Message, args.ChatMsg); + _netMan.ServerSendMessage(chatMessage, actor.PlayerSession.Channel); + // Fire edit end if (parent != args.MessageSource && HasComp(args.MessageSource)) { args.Receivers.Add(parent); diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 7ea4c93d100..20ab703df05 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -1,4 +1,5 @@ using System.Globalization; +using Content.Server._Scp.Knowledge; // Fire added - SCP knowledge-aware radio chat highlighting using Content.Server._Sunrise.Chat.Sanitization; using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; @@ -35,6 +36,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 ScpKnowledgeSystem _knowledge = default!; // Fire added - personalize radio chat by listener knowledge [Dependency] private readonly AccessReaderSystem _accessReader = default!; // set used to prevent radio feedback loops. @@ -71,7 +73,13 @@ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent c // Sunrise-TTS-Start if (TryComp(uid, out ActorComponent? actor)) { - _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); + // Fire added start - personalize SCP knowledge highlights for direct radio receivers + var wrappedMessage = _knowledge.HighlightWrappedChatMessage(uid, args.Message, args.ChatMsg.Message.WrappedMessage, args.MessageSource); + var chatMessage = string.Equals(wrappedMessage, args.ChatMsg.Message.WrappedMessage, StringComparison.Ordinal) + ? args.ChatMsg + : ScpKnowledgeChatMessage.CloneWithWrappedMessage(args.ChatMsg, wrappedMessage); + _netMan.ServerSendMessage(chatMessage, actor.PlayerSession.Channel); + // Fire added end if (uid != args.MessageSource && HasComp(args.MessageSource)) { args.Receivers.Add(uid); diff --git a/Content.Server/_Scp/Knowledge/ChatSystem.ScpKnowledge.cs b/Content.Server/_Scp/Knowledge/ChatSystem.ScpKnowledge.cs new file mode 100644 index 00000000000..cacc7063fdb --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ChatSystem.ScpKnowledge.cs @@ -0,0 +1,72 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure +using Content.Server._Scp.Knowledge; +using Content.Shared._Scp.Helpers; +using Content.Shared.Chat; +using Content.Shared._Scp.Knowledge; + +namespace Content.Server.Chat.Systems; + +public sealed partial class ChatSystem +{ + [Dependency] private readonly ScpKnowledgeSystem _knowledge = default!; + + private ScpKnowledgeTextAnalysis? CreateScpKnowledgeTextAnalysis(string message) + { + var analysis = _knowledge.AnalyzeRecognitionText(message); + return analysis.HasMatchedKnowledge ? analysis : null; + } + + private void RaiseScpKnowledgeSpeakEvent(EntityUid source, ScpKnowledgeTextAnalysis? analysis, ChatTransmitRange range) + { + if (analysis == null) + return; + + using var listeners = ListPool.Rent(); + + foreach (var (session, data) in GetRecipients(source, VoiceRange)) + { + if (session.AttachedEntity is not { Valid: true } attachedEntity) + continue; + + if (MessageRangeCheck(session, data, range) == MessageRangeCheckResult.Disallowed) + continue; + + listeners.Value.Add(attachedEntity); + } + + RaiseScpKnowledgeSpeechEvent(source, analysis, listeners.Value); + } + + private void RaiseScpKnowledgeWhisperEvent( + EntityUid source, + List clearListeners, + List obfuscatedListeners, + ScpKnowledgeTextAnalysis? clearAnalysis, + ScpKnowledgeTextAnalysis? obfuscatedAnalysis) + { + RaiseScpKnowledgeSpeechEvent(source, clearAnalysis, clearListeners); + RaiseScpKnowledgeSpeechEvent(source, obfuscatedAnalysis, obfuscatedListeners); + } + + private void RaiseScpKnowledgeSpeechEvent( + EntityUid source, + ScpKnowledgeTextAnalysis? analysis, + List listeners) + { + if (analysis == null || listeners.Count == 0) + return; + + var ev = new ScpKnowledgeSpeechHeardEvent(source, analysis, listeners.ToArray()); + RaiseLocalEvent(ref ev); + } + + private string GetScpKnowledgeWrappedMessage( + EntityUid listener, + EntityUid source, + string message, + string wrappedMessage, + ScpKnowledgeTextAnalysis? analysis = null) + { + return _knowledge.HighlightWrappedChatMessage(listener, message, wrappedMessage, source, analysis); + } +} diff --git a/Content.Server/_Scp/Knowledge/HeadsetSystem.ScpKnowledge.cs b/Content.Server/_Scp/Knowledge/HeadsetSystem.ScpKnowledge.cs new file mode 100644 index 00000000000..6a2bcf2a66c --- /dev/null +++ b/Content.Server/_Scp/Knowledge/HeadsetSystem.ScpKnowledge.cs @@ -0,0 +1,24 @@ +#pragma warning disable IDE0130 // Namespace does not match folder structure +using Content.Server._Scp.Knowledge; +using Content.Shared.Chat; + +namespace Content.Server.Radio.EntitySystems; + +public sealed partial class HeadsetSystem +{ + [Dependency] private readonly ScpKnowledgeSystem _knowledge = default!; + + private MsgChatMessage GetScpKnowledgeRadioChatMessage(EntityUid listener, EntityUid source, string message, MsgChatMessage chatMessage) + { + var highlightedWrappedMessage = _knowledge.HighlightWrappedChatMessage( + listener, + message, + chatMessage.Message.WrappedMessage, + source); + + if (string.Equals(highlightedWrappedMessage, chatMessage.Message.WrappedMessage, StringComparison.Ordinal)) + return chatMessage; + + return ScpKnowledgeChatMessage.CloneWithWrappedMessage(chatMessage, highlightedWrappedMessage); + } +} diff --git a/Content.Server/_Scp/Knowledge/ScpKnowledgeChatMessage.cs b/Content.Server/_Scp/Knowledge/ScpKnowledgeChatMessage.cs new file mode 100644 index 00000000000..7881a250290 --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ScpKnowledgeChatMessage.cs @@ -0,0 +1,25 @@ +using Content.Shared.Chat; + +namespace Content.Server._Scp.Knowledge; + +public static class ScpKnowledgeChatMessage +{ + public static MsgChatMessage CloneWithWrappedMessage(MsgChatMessage message, string wrappedMessage) + { + var chat = message.Message; + + return new MsgChatMessage + { + Message = new ChatMessage( + chat.Channel, + chat.Message, + wrappedMessage, + chat.SenderEntity, + chat.SenderKey, + chat.HideChat, + chat.MessageColorOverride, + chat.AudioPath, + chat.AudioVolume) + }; + } +} diff --git a/Content.Server/_Scp/Knowledge/ScpKnowledgeSpeechHeardEvent.cs b/Content.Server/_Scp/Knowledge/ScpKnowledgeSpeechHeardEvent.cs new file mode 100644 index 00000000000..50fa93a0d2d --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ScpKnowledgeSpeechHeardEvent.cs @@ -0,0 +1,7 @@ +namespace Content.Server._Scp.Knowledge; + +[ByRefEvent] +public readonly record struct ScpKnowledgeSpeechHeardEvent( + EntityUid Source, + ScpKnowledgeTextAnalysis Analysis, + EntityUid[] Listeners); diff --git a/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.Acquisition.cs b/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.Acquisition.cs new file mode 100644 index 00000000000..767e088d7f6 --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.Acquisition.cs @@ -0,0 +1,135 @@ +using Content.Server.Chat.Systems; +using Content.Shared._Scp.Knowledge; +using Content.Shared._Scp.Knowledge.Components; +using Content.Shared.Examine; +using Content.Shared.Paper; +using Content.Shared.Verbs; +using Robust.Shared.Utility; + +namespace Content.Server._Scp.Knowledge; + +public sealed partial class ScpKnowledgeSystem +{ + private void OnSpeechHeard(ref ScpKnowledgeSpeechHeardEvent args) + { + if (!args.Analysis.HasMatchedKnowledge) + return; + + foreach (var listener in args.Listeners) + { + GrantKnowledgeFromText(listener, args.Analysis, args.Source, ScpKnowledgeAcquisitionChannel.Listen); + } + } + + private void OnRadioSpoke(RadioSpokeEvent args) + { + var analysis = AnalyzeRecognitionText(args.Message, includeMatches: false); + if (!analysis.HasMatchedKnowledge) + return; + + foreach (var receiver in args.Receivers) + { + GrantKnowledgeFromText(receiver, analysis, args.Source, ScpKnowledgeAcquisitionChannel.Listen); + } + } + + private void OnPaperRead(ScpKnowledgePaperReadEvent args) + { + if (!TryComp(args.Paper, out var paper)) + return; + + var analysis = GetOrCreatePaperAnalysis(args.Paper, args.Content); + if (!analysis.HasMatchedKnowledge) + return; + + foreach (var knowledgeId in analysis.MatchedKnowledgeIds) + { + if (!HasPaperKnowledgeMatch(paper, analysis, knowledgeId)) + continue; + + var knowledge = _prototype.Index(knowledgeId); + if (knowledge.ReadProgress <= 0) + continue; + + TryGrantKnowledgeProgress( + args.User, + knowledgeId, + knowledge.ReadProgress, + ScpKnowledgeAcquisitionChannel.Read, + args.Paper); + } + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + TryGrantExamineKnowledge(args.Examiner, ent.Owner); + } + + private void OnGetExamineVerbs(Entity ent, ref GetVerbsEvent args) + { + if (!TryGetKnowledgeIdsForEntity(ent.Owner, out var knowledgeIds)) + return; + + foreach (var knowledgeId in knowledgeIds) + { + if (!HasKnowledge(args.User, knowledgeId)) + continue; + + var knowledge = _prototype.Index(knowledgeId); + if (knowledge.KnownExamineVerbText == null || knowledge.KnownExamineText == null) + continue; + + var message = new FormattedMessage(); + message.AddText(Loc.GetString(knowledge.KnownExamineText.Value)); + + _examine.AddDetailedExamineVerb( + args, + ent.Comp, + message, + Loc.GetString(knowledge.KnownExamineVerbText.Value)); + } + } + + private void GrantKnowledgeFromText( + EntityUid uid, + ScpKnowledgeTextAnalysis analysis, + EntityUid source, + ScpKnowledgeAcquisitionChannel channel) + { + if (!analysis.HasMatchedKnowledge) + return; + + ScpKnowledgeComponent? sourceKnowledgeState = null; + if (channel == ScpKnowledgeAcquisitionChannel.Listen && + !TryGetKnowledgeState(source, out _, out sourceKnowledgeState)) + { + return; + } + + foreach (var knowledgeId in analysis.MatchedKnowledgeIds) + { + var knowledge = _prototype.Index(knowledgeId); + if (channel == ScpKnowledgeAcquisitionChannel.Listen && + (sourceKnowledgeState == null || !IsKnowledgeKnown(sourceKnowledgeState, knowledgeId, knowledge))) + { + continue; + } + + var progress = channel switch + { + ScpKnowledgeAcquisitionChannel.Listen => knowledge.ListenProgress, + ScpKnowledgeAcquisitionChannel.Read => knowledge.ReadProgress, + ScpKnowledgeAcquisitionChannel.Examine => knowledge.ExamineProgress, + _ => 0, + }; + + if (progress <= 0) + continue; + + TryGrantKnowledgeProgress(uid, knowledgeId, progress, channel, source, analysis.TokenizedText.NormalizedText); + } + } +} diff --git a/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.Presentation.cs b/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.Presentation.cs new file mode 100644 index 00000000000..c7a4738e6f1 --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.Presentation.cs @@ -0,0 +1,304 @@ +using System.Text; +using Content.Shared._Scp.Knowledge; +using Content.Shared._Scp.Knowledge.Components; +using Content.Shared.Mind; +using Content.Shared.Paper; +using Content.Shared.Popups; +using Content.Shared.UserInterface; +using Robust.Server.Audio; +using Robust.Server.GameObjects; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server._Scp.Knowledge; + +public sealed partial class ScpKnowledgeSystem +{ + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; + + private const string KnowledgeHighlightColor = "#8b0000"; + private readonly List _highlightRanges = []; + + public string HighlightUnknownKnowledgeText(EntityUid viewer, string text, EntityUid? source = null) + { + return BuildHighlightedText(viewer, text, escapeText: false, source, analysis: null); + } + + public string HighlightWrappedChatMessage( + EntityUid viewer, + string message, + string wrappedMessage, + EntityUid? source = null, + ScpKnowledgeTextAnalysis? analysis = null) + { + if (message.Length == 0 || wrappedMessage.Length == 0) + return wrappedMessage; + + var escapedMessage = FormattedMessage.EscapeText(message); + var highlightedEscapedMessage = BuildHighlightedText(viewer, message, escapeText: true, source, analysis: analysis); + if (!string.Equals(highlightedEscapedMessage, escapedMessage, StringComparison.Ordinal)) + return ReplaceFirstOccurrence(wrappedMessage, escapedMessage, highlightedEscapedMessage); + + var highlightedMessage = BuildHighlightedText(viewer, message, escapeText: false, source, analysis: analysis); + if (!string.Equals(highlightedMessage, message, StringComparison.Ordinal)) + return ReplaceFirstOccurrence(wrappedMessage, message, highlightedMessage); + + return wrappedMessage; + } + + private void OnPaperUiOpened(Entity ent, ref AfterActivatableUIOpenEvent args) + { + if (ent.Comp.Content.Length == 0) + return; + + var analysis = GetOrCreatePaperAnalysis(ent.Owner, ent.Comp.Content); + var highlightedText = HighlightUnknownPaperKnowledgeText(args.User, ent, analysis); + if (string.Equals(highlightedText, ent.Comp.Content, StringComparison.Ordinal)) + return; + + _ui.ServerSendUiMessage( + ent.Owner, + PaperComponent.PaperUiKey.Key, + new PaperComponent.PaperKnowledgeHighlightMessage(ent.Comp.Content, highlightedText), + args.User); + } + + private void ShowKnowledgeUnlockedFeedback( + EntityUid holderUid, + ScpKnowledgeAcquisitionComponent acquisition, + ScpKnowledgePrototype knowledge) + { + var popupTarget = holderUid; + + if (TryComp(holderUid, out var mind) && mind.CurrentEntity is { } currentEntity) + popupTarget = currentEntity; + + var popup = Loc.GetString( + "scp-knowledge-unlocked-popup", + ("knowledge", Loc.GetString(knowledge.DisplayName))); + + _popup.PopupEntity(popup, popupTarget, popupTarget, PopupType.Medium); + + if (acquisition.UnlockSound != null) + { + if (TryComp(popupTarget, out var actor)) + _audio.PlayGlobal(acquisition.UnlockSound, actor.PlayerSession); + else + _audio.PlayGlobal(acquisition.UnlockSound, popupTarget); + } + } + + private string HighlightUnknownPaperKnowledgeText( + EntityUid viewer, + Entity paper, + ScpKnowledgeTextAnalysis analysis) + { + return BuildHighlightedText( + viewer, + paper.Comp.Content, + escapeText: false, + source: null, + matchFilter: (knowledgeId, start, length) => DoesPaperRangeProvideKnowledge(paper.Comp, knowledgeId, start, length), + analysis: analysis); + } + + private string BuildHighlightedText( + EntityUid viewer, + string text, + bool escapeText, + EntityUid? source, + Func, int, int, bool>? matchFilter = null, + ScpKnowledgeTextAnalysis? analysis = null) + { + if (text.Length == 0) + return text; + + if (!TryGetKnowledgeState(viewer, out _, out var knowledgeState)) + return escapeText ? FormattedMessage.EscapeText(text) : text; + + ScpKnowledgeComponent? sourceKnowledgeState = null; + if (source != null && !TryGetKnowledgeState(source.Value, out _, out sourceKnowledgeState)) + return escapeText ? FormattedMessage.EscapeText(text) : text; + + analysis ??= AnalyzeRecognitionText(text); + if (!analysis.HasMatches) + return escapeText ? FormattedMessage.EscapeText(text) : text; + + _highlightRanges.Clear(); + + foreach (var match in analysis.Matches) + { + var knowledge = _prototype.Index(match.KnowledgeId); + if (IsKnowledgeKnown(knowledgeState, match.KnowledgeId, knowledge)) + continue; + + if (sourceKnowledgeState != null && !IsKnowledgeKnown(sourceKnowledgeState, match.KnowledgeId, knowledge)) + continue; + + if (matchFilter != null && !matchFilter(match.KnowledgeId, match.Start, match.Length)) + continue; + + _highlightRanges.Add(new TextHighlightRange(match.Start, match.End)); + } + + if (_highlightRanges.Count == 0) + return escapeText ? FormattedMessage.EscapeText(text) : text; + + _highlightRanges.Sort(static (left, right) => + { + var compare = left.Start.CompareTo(right.Start); + return compare != 0 ? compare : right.End.CompareTo(left.End); + }); + + return RenderHighlightedText(text, escapeText); + } + + private bool HasPaperKnowledgeMatch( + PaperComponent paper, + ScpKnowledgeTextAnalysis analysis, + ProtoId knowledgeId) + { + foreach (var match in analysis.Matches) + { + if (match.KnowledgeId != knowledgeId) + continue; + + if (DoesPaperRangeProvideKnowledge(paper, knowledgeId, match.Start, match.Length)) + return true; + } + + return false; + } + + private bool DoesPaperRangeProvideKnowledge(PaperComponent paper, ProtoId knowledgeId, int start, int length) + { + if (length <= 0) + return false; + + var authorRanges = paper.KnowledgeAuthorRanges; + if (authorRanges.Count == 0) + return true; + + var end = start + length; + var coveredUntil = start; + + foreach (var range in authorRanges) + { + if (range.End <= start) + continue; + + if (range.Start >= end) + break; + + if (range.Start > coveredUntil) + return false; + + if (range.Author == null) + { + coveredUntil = Math.Max(coveredUntil, Math.Min(range.End, end)); + continue; + } + + if (!HasKnowledge(range.Author.Value, knowledgeId)) + return false; + + coveredUntil = Math.Max(coveredUntil, Math.Min(range.End, end)); + } + + return coveredUntil >= end; + } + + private string RenderHighlightedText(string text, bool escapeText) + { + var builder = new StringBuilder(text.Length + _highlightRanges.Count * 32); + var currentIndex = 0; + var currentRange = _highlightRanges[0]; + + for (var i = 1; i < _highlightRanges.Count; i++) + { + var nextRange = _highlightRanges[i]; + if (nextRange.Start <= currentRange.End) + { + currentRange = new TextHighlightRange(currentRange.Start, Math.Max(currentRange.End, nextRange.End)); + continue; + } + + AppendHighlightedRange(builder, text, ref currentIndex, currentRange, escapeText); + currentRange = nextRange; + } + + AppendHighlightedRange(builder, text, ref currentIndex, currentRange, escapeText); + if (currentIndex < text.Length) + AppendRenderedSegment(builder, text, currentIndex, text.Length - currentIndex, escapeText); + + return builder.ToString(); + } + + private static void AppendHighlightedRange( + StringBuilder builder, + string text, + ref int currentIndex, + TextHighlightRange range, + bool escapeText) + { + if (range.Start > currentIndex) + AppendRenderedSegment(builder, text, currentIndex, range.Start - currentIndex, escapeText); + + builder.Append("[color="); + builder.Append(KnowledgeHighlightColor); + builder.Append(']'); + AppendRenderedSegment(builder, text, range.Start, range.End - range.Start, escapeText); + builder.Append("[/color]"); + currentIndex = range.End; + } + + private static void AppendRenderedSegment( + StringBuilder builder, + string text, + int start, + int length, + bool escapeText) + { + if (length <= 0) + return; + + if (!escapeText) + { + builder.Append(text, start, length); + return; + } + + var end = start + length; + foreach (var character in text.AsSpan(start, length)) + { + switch (character) + { + case '\\': + builder.Append("\\\\"); + break; + case '[': + builder.Append("\\["); + break; + default: + builder.Append(character); + break; + } + } + } + + private static string ReplaceFirstOccurrence(string text, string oldValue, string newValue) + { + var index = text.IndexOf(oldValue, StringComparison.Ordinal); + if (index == -1) + return text; + + return string.Concat( + text.AsSpan(0, index), + newValue, + text.AsSpan(index + oldValue.Length)); + } + + private readonly record struct TextHighlightRange(int Start, int End); +} diff --git a/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.cs b/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.cs new file mode 100644 index 00000000000..c8763de2304 --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ScpKnowledgeSystem.cs @@ -0,0 +1,512 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Server.Chat.Systems; +using Content.Server.Examine; +using Content.Server.Mind; +using Content.Server.Popups; +using Content.Shared._Scp.Knowledge; +using Content.Shared._Scp.Knowledge.Components; +using Content.Shared.Examine; +using Content.Shared.Mind; +using Content.Shared.Paper; +using Content.Shared.UserInterface; +using Content.Shared.Verbs; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server._Scp.Knowledge; + +public sealed partial class ScpKnowledgeSystem : EntitySystem +{ + [Dependency] private readonly ExamineSystem _examine = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private readonly Dictionary>> _knowledgeByEntityPrototype = []; + private readonly Dictionary> _knowledgePatternsByFirstToken = []; + private readonly Dictionary _paperAnalysisCache = []; + private readonly List _matchedKnowledgeBuffer = []; + private readonly HashSet> _matchedKnowledgeIdsBuffer = []; + private readonly List _recognitionMatchesBuffer = []; + private readonly ScpKnowledgePatternMatchContext _patternMatchContext = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPrototypesReloaded); + SubscribeLocalEvent(OnPaperRead); + SubscribeLocalEvent(OnPaperUiOpened); + SubscribeLocalEvent(OnPaperShutdown); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent>(OnGetExamineVerbs); + SubscribeLocalEvent(OnSpeechHeard); + SubscribeLocalEvent(OnRadioSpoke); + SubscribeLocalEvent(OnPlayerAttached); + + RebuildCaches(); + } + + public bool TryGetKnowledgeState( + EntityUid uid, + [NotNullWhen(true)] out EntityUid? holderUid, + [NotNullWhen(true)] out ScpKnowledgeComponent? knowledge) + { + if (TryComp(uid, out knowledge)) + { + holderUid = uid; + return true; + } + + if (TryComp(uid, out var mind) && + mind.CurrentEntity is { } currentEntity && + TryComp(currentEntity, out knowledge)) + { + holderUid = currentEntity; + return true; + } + + if (!_mind.TryGetMind(uid, out var resolvedMindUid, out _)) + { + holderUid = null; + knowledge = null; + return false; + } + + if (TryComp(resolvedMindUid, out knowledge)) + { + holderUid = resolvedMindUid; + return true; + } + + if (TryComp(resolvedMindUid, out var resolvedMind) && + resolvedMind.CurrentEntity is { } resolvedCurrentEntity && + TryComp(resolvedCurrentEntity, out knowledge)) + { + holderUid = resolvedCurrentEntity; + return true; + } + + holderUid = null; + knowledge = null; + return false; + } + + public bool HasKnowledge(EntityUid uid, ProtoId knowledgeId) + { + if (!TryGetKnowledgeState(uid, out _, out var knowledgeState)) + return false; + + var knowledge = _prototype.Index(knowledgeId); + return ScpKnowledgeLogic.IsKnowledgeKnown(knowledgeState, knowledgeId, knowledge); + } + + public bool TryGetKnowledgeProgress(EntityUid uid, ProtoId knowledgeId, out int progress) + { + progress = 0; + + if (!TryGetKnowledgeState(uid, out _, out var knowledge)) + return false; + + var knowledgePrototype = _prototype.Index(knowledgeId); + progress = ScpKnowledgeLogic.GetKnowledgeProgress(knowledge, knowledgeId, knowledgePrototype); + return progress > 0; + } + + public bool TryGrantKnowledgeProgress( + EntityUid uid, + ProtoId knowledgeId, + int progress, + ScpKnowledgeAcquisitionChannel channel = ScpKnowledgeAcquisitionChannel.Other, + EntityUid? source = null, + string? normalizedMessage = null) + { + return TryGrantKnowledgeProgress(uid, knowledgeId, progress, channel, source, true, normalizedMessage); + } + + public bool TryGetUnknownExamineMessage( + EntityUid examiner, + EntityUid target, + [NotNullWhen(true)] out FormattedMessage? message) + { + message = null; + + if (!TryGetKnowledgeIdsForEntity(target, out var knowledgeIds)) + return false; + + var requiresKnowledge = false; + + foreach (var knowledgeId in knowledgeIds) + { + var knowledge = _prototype.Index(knowledgeId); + if (!knowledge.HideIdentityUntilKnown) + continue; + + requiresKnowledge = true; + if (HasKnowledge(examiner, knowledgeId)) + return false; + } + + if (!requiresKnowledge) + return false; + + message = new FormattedMessage(); + message.AddText(Loc.GetString("scp-knowledge-unknown-examine")); + return true; + } + + public bool TryGetKnownExamineNameOverride( + EntityUid examiner, + EntityUid target, + [NotNullWhen(true)] out string? nameOverride) + { + nameOverride = null; + + if (!TryGetKnowledgeIdsForEntity(target, out var knowledgeIds)) + return false; + + foreach (var knowledgeId in knowledgeIds) + { + var knowledge = _prototype.Index(knowledgeId); + if (!knowledge.HideIdentityUntilKnown || !HasKnowledge(examiner, knowledgeId)) + continue; + + nameOverride = Loc.GetString(knowledge.DisplayName); + return true; + } + + return false; + } + + public bool TryGrantExamineKnowledge(EntityUid examiner, EntityUid target) + { + if (!TryGetKnowledgeIdsForEntity(target, out var knowledgeIds)) + return false; + + var changed = false; + + foreach (var knowledgeId in knowledgeIds) + { + var knowledge = _prototype.Index(knowledgeId); + if (knowledge.ExamineProgress <= 0) + continue; + + changed |= TryGrantKnowledgeProgress( + examiner, + knowledgeId, + knowledge.ExamineProgress, + ScpKnowledgeAcquisitionChannel.Examine, + target); + } + + return changed; + } + + private bool TryGrantKnowledgeProgress( + EntityUid uid, + ProtoId knowledgeId, + int progress, + ScpKnowledgeAcquisitionChannel channel, + EntityUid? source, + bool showUnlockFeedback, + string? normalizedMessage) + { + if (progress <= 0) + return false; + + if (!TryGetKnowledgeState(uid, out var holderUid, out var knowledgeState)) + return false; + + if (!TryComp(holderUid.Value, out var acquisition)) + return false; + + var knowledge = _prototype.Index(knowledgeId); + + if (!CanAcquireKnowledge(acquisition, knowledge, channel)) + return false; + + if (IsKnowledgeKnown(knowledgeState, knowledgeId, knowledge)) + return false; + + if (source != null && + channel is ScpKnowledgeAcquisitionChannel.Read or ScpKnowledgeAcquisitionChannel.Examine && + !knowledgeState.ProcessedSources.Add(new ScpKnowledgeSourceRecord(source.Value, knowledgeId, channel))) + { + return false; + } + + if (source != null && + channel == ScpKnowledgeAcquisitionChannel.Listen && + !string.IsNullOrEmpty(normalizedMessage) && + !knowledgeState.ProcessedMessages.Add(new ScpKnowledgeMessageRecord( + source.Value, + knowledgeId, + channel, + normalizedMessage))) + { + return false; + } + + var currentProgress = ScpKnowledgeLogic.GetKnowledgeProgress(knowledgeState, knowledgeId, knowledge); + var currentExposureFlags = ScpKnowledgeLogic.GetKnowledgeExposureFlags(knowledgeState, knowledgeId); + var updatedExposureFlags = currentExposureFlags; + + if (ScpKnowledgeLogic.RequiresTextAndExamine(knowledge) && + channel != ScpKnowledgeAcquisitionChannel.Other) + { + updatedExposureFlags |= ScpKnowledgeLogic.GetExposureFlags(channel); + progress = ScpKnowledgeLogic.GetExposureProgress(updatedExposureFlags); + } + + var clampedProgress = ScpKnowledgeLogic.RequiresTextAndExamine(knowledge) && + channel != ScpKnowledgeAcquisitionChannel.Other + ? Math.Min(knowledge.RequiredProgress, Math.Max(currentProgress, progress)) + : Math.Min(knowledge.RequiredProgress, currentProgress + progress); + + if (clampedProgress <= currentProgress && + updatedExposureFlags == currentExposureFlags) + { + return false; + } + + if (updatedExposureFlags == ScpKnowledgeExposureFlags.None) + knowledgeState.ExposureFlags.Remove(knowledgeId); + else + knowledgeState.ExposureFlags[knowledgeId] = updatedExposureFlags; + + if (clampedProgress < knowledge.RequiredProgress) + { + knowledgeState.Progress[knowledgeId] = clampedProgress; + Dirty(holderUid.Value, knowledgeState); + return true; + } + + knowledgeState.Progress.Remove(knowledgeId); + knowledgeState.ExposureFlags.Remove(knowledgeId); + knowledgeState.KnownKnowledge.Add(knowledgeId); + + Dirty(holderUid.Value, knowledgeState); + + if (showUnlockFeedback) + { + var unlockEvent = new ScpKnowledgeUnlockedEvent(knowledgeId, channel, source); + RaiseLocalEvent(holderUid.Value, ref unlockEvent, true); + ShowKnowledgeUnlockedFeedback(holderUid.Value, acquisition, knowledge); + } + + return true; + } + + private bool CanAcquireKnowledge( + ScpKnowledgeAcquisitionComponent acquisition, + ScpKnowledgePrototype knowledge, + ScpKnowledgeAcquisitionChannel channel) + { + return channel switch + { + ScpKnowledgeAcquisitionChannel.Listen => acquisition.CanLearnByListen && knowledge.AllowListen, + ScpKnowledgeAcquisitionChannel.Read => acquisition.CanLearnByRead && knowledge.AllowRead, + ScpKnowledgeAcquisitionChannel.Examine => acquisition.CanLearnByExamine && knowledge.AllowExamine, + ScpKnowledgeAcquisitionChannel.Other => acquisition.CanLearnByOther, + _ => false, + }; + } + + private void RebuildCaches() + { + _knowledgeByEntityPrototype.Clear(); + _knowledgePatternsByFirstToken.Clear(); + _paperAnalysisCache.Clear(); + + foreach (var knowledge in _prototype.EnumeratePrototypes()) + { + CacheKnowledgePatterns(knowledge); + CacheKnowledgeEntities(knowledge); + } + } + + private void CacheKnowledgePatterns(ScpKnowledgePrototype knowledge) + { + var compiledPatternCount = 0; + foreach (var patternId in knowledge.RecognitionPatterns) + { + var localizedPattern = Loc.GetString(patternId); + if (!ScpKnowledgeText.TryCompileRecognitionPattern(localizedPattern, out var compiledPattern, out var error)) + { + Log.Error( + $"Failed to compile SCP knowledge recognition pattern '{patternId}' for knowledge '{knowledge.ID}': {error}"); + continue; + } + + compiledPatternCount++; + + foreach (var firstToken in compiledPattern.FirstTokens) + { + if (!_knowledgePatternsByFirstToken.TryGetValue(firstToken, out var patterns)) + { + patterns = []; + _knowledgePatternsByFirstToken[firstToken] = patterns; + } + + patterns.Add(new CachedRecognitionPattern(knowledge.ID, compiledPattern)); + } + } + + if (compiledPatternCount == 0) + Log.Error($"SCP knowledge '{knowledge.ID}' has no valid recognition patterns."); + } + + private void CacheKnowledgeEntities(ScpKnowledgePrototype knowledge) + { + foreach (var entityPrototype in knowledge.EntityPrototypes) + { + if (!_knowledgeByEntityPrototype.TryGetValue(entityPrototype, out var knowledgeIds)) + { + knowledgeIds = []; + _knowledgeByEntityPrototype[entityPrototype] = knowledgeIds; + } + + knowledgeIds.Add(knowledge.ID); + } + } + + private bool TryGetKnowledgeIdsForEntity( + EntityUid target, + [NotNullWhen(true)] out List>? knowledgeIds) + { + knowledgeIds = null; + var prototypeId = Prototype(target)?.ID; + return prototypeId != null && _knowledgeByEntityPrototype.TryGetValue(prototypeId, out knowledgeIds); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified()) + return; + + RebuildCaches(); + } + + private void OnPlayerAttached(PlayerAttachedEvent args) + { + if (!TryGetKnowledgeState(args.Entity, out var holderUid, out var knowledgeState)) + return; + + Dirty(holderUid.Value, knowledgeState); + } + + private void OnPaperShutdown(Entity ent, ref ComponentShutdown args) + { + _paperAnalysisCache.Remove(ent.Owner); + } + + private static ScpKnowledgeExposureFlags GetExposureFlags( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId) + { + return ScpKnowledgeLogic.GetKnowledgeExposureFlags(knowledgeState, knowledgeId); + } + + private static bool IsKnowledgeKnown( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId, + ScpKnowledgePrototype knowledge) + { + return ScpKnowledgeLogic.IsKnowledgeKnown(knowledgeState, knowledgeId, knowledge); + } + + private static int GetKnowledgeProgress( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId, + ScpKnowledgePrototype knowledge) + { + return ScpKnowledgeLogic.GetKnowledgeProgress(knowledgeState, knowledgeId, knowledge); + } + + public ScpKnowledgeTextAnalysis AnalyzeRecognitionText(string text, bool includeMatches = true) + { + return AnalyzeRecognitionText(ScpKnowledgeText.TokenizeRecognitionText(text), includeMatches); + } + + public ScpKnowledgeTextAnalysis GetOrCreatePaperAnalysis(EntityUid paperUid, string content) + { + if (_paperAnalysisCache.TryGetValue(paperUid, out var cachedAnalysis) && + string.Equals(cachedAnalysis.Content, content, StringComparison.Ordinal)) + { + return cachedAnalysis.Analysis; + } + + var analysis = AnalyzeRecognitionText(content); + _paperAnalysisCache[paperUid] = new CachedPaperAnalysis(content, analysis); + return analysis; + } + + private ScpKnowledgeTextAnalysis AnalyzeRecognitionText( + ScpKnowledgeTokenizedText tokenizedText, + bool includeMatches) + { + CollectKnowledgeMatches(tokenizedText, includeMatches); + + if (_matchedKnowledgeIdsBuffer.Count == 0) + return new ScpKnowledgeTextAnalysis(tokenizedText, [], []); + + var matchedKnowledgeIds = new ProtoId[_matchedKnowledgeIdsBuffer.Count]; + _matchedKnowledgeIdsBuffer.CopyTo(matchedKnowledgeIds); + if (!includeMatches || _matchedKnowledgeBuffer.Count == 0) + return new ScpKnowledgeTextAnalysis(tokenizedText, [], matchedKnowledgeIds); + + var matches = new ScpKnowledgeAnalyzedMatch[_matchedKnowledgeBuffer.Count]; + var matchIndex = 0; + foreach (var match in _matchedKnowledgeBuffer) + { + matches[matchIndex++] = new ScpKnowledgeAnalyzedMatch(match.KnowledgeId, match.Start, match.Length); + } + + return new ScpKnowledgeTextAnalysis(tokenizedText, matches, matchedKnowledgeIds); + } + + private void CollectKnowledgeMatches(ScpKnowledgeTokenizedText tokenizedText, bool includeMatches) + { + _matchedKnowledgeBuffer.Clear(); + _matchedKnowledgeIdsBuffer.Clear(); + + if (tokenizedText.Tokens.Count == 0) + return; + + for (var tokenIndex = 0; tokenIndex < tokenizedText.Tokens.Count; tokenIndex++) + { + var token = tokenizedText.Tokens[tokenIndex].Value; + if (!_knowledgePatternsByFirstToken.TryGetValue(token, out var patterns)) + continue; + + foreach (var pattern in patterns) + { + _recognitionMatchesBuffer.Clear(); + ScpKnowledgeText.AddPatternMatchesAt(pattern.Pattern, tokenizedText, tokenIndex, _recognitionMatchesBuffer, _patternMatchContext); + + foreach (var match in _recognitionMatchesBuffer) + { + if (includeMatches) + _matchedKnowledgeBuffer.Add(new CachedKnowledgeMatch(pattern.KnowledgeId, match.SourceStart, match.Length)); + + _matchedKnowledgeIdsBuffer.Add(pattern.KnowledgeId); + } + } + } + } + + private readonly record struct CachedRecognitionPattern( + ProtoId KnowledgeId, + ScpKnowledgeCompiledPattern Pattern); + + private readonly record struct CachedPaperAnalysis(string Content, ScpKnowledgeTextAnalysis Analysis); + + private readonly record struct CachedKnowledgeMatch( + ProtoId KnowledgeId, + int Start, + int Length) + { + public int End => Start + Length; + } +} diff --git a/Content.Server/_Scp/Knowledge/ScpKnowledgeTextAnalysis.cs b/Content.Server/_Scp/Knowledge/ScpKnowledgeTextAnalysis.cs new file mode 100644 index 00000000000..574cf98aa04 --- /dev/null +++ b/Content.Server/_Scp/Knowledge/ScpKnowledgeTextAnalysis.cs @@ -0,0 +1,30 @@ +using Content.Shared._Scp.Knowledge; +using Robust.Shared.Prototypes; + +namespace Content.Server._Scp.Knowledge; + +public sealed class ScpKnowledgeTextAnalysis( + ScpKnowledgeTokenizedText tokenizedText, + IReadOnlyList matches, + IReadOnlyList> matchedKnowledgeIds) +{ + public static readonly ScpKnowledgeTextAnalysis Empty = new( + ScpKnowledgeTokenizedText.Empty, + [], + []); + + public ScpKnowledgeTokenizedText TokenizedText { get; } = tokenizedText; + public IReadOnlyList Matches { get; } = matches; + public IReadOnlyList> MatchedKnowledgeIds { get; } = matchedKnowledgeIds; + + public bool HasMatches => Matches.Count > 0; + public bool HasMatchedKnowledge => MatchedKnowledgeIds.Count > 0; +} + +public readonly record struct ScpKnowledgeAnalyzedMatch( + ProtoId KnowledgeId, + int Start, + int Length) +{ + public int End => Start + Length; +} diff --git a/Content.Server/_Sunrise/GameTicking/Events/RoundEndMusicSelectionEvent.cs b/Content.Server/_Sunrise/GameTicking/Events/RoundEndMusicSelectionEvent.cs index fbc2ff41214..312eb330467 100644 --- a/Content.Server/_Sunrise/GameTicking/Events/RoundEndMusicSelectionEvent.cs +++ b/Content.Server/_Sunrise/GameTicking/Events/RoundEndMusicSelectionEvent.cs @@ -1,6 +1,5 @@ using Robust.Shared.Audio; -#pragma warning disable IDE0130 // Namespace does not match folder structure namespace Content.Server._Sunrise.GameTicking.Events; [ByRefEvent] diff --git a/Content.Server/_Sunrise/GameTicking/Events/RoundLobbyReadyEvent.cs b/Content.Server/_Sunrise/GameTicking/Events/RoundLobbyReadyEvent.cs index 5aa7cf46294..7adbeed6f9f 100644 --- a/Content.Server/_Sunrise/GameTicking/Events/RoundLobbyReadyEvent.cs +++ b/Content.Server/_Sunrise/GameTicking/Events/RoundLobbyReadyEvent.cs @@ -1,4 +1,3 @@ -#pragma warning disable IDE0130 // Namespace does not match folder structure namespace Content.Server._Sunrise.GameTicking.Events; [ByRefEvent] diff --git a/Content.Shared/Examine/ExamineSystemMessages.cs b/Content.Shared/Examine/ExamineSystemMessages.cs index 5d5c67431a5..cb3f7995d09 100644 --- a/Content.Shared/Examine/ExamineSystemMessages.cs +++ b/Content.Shared/Examine/ExamineSystemMessages.cs @@ -36,9 +36,10 @@ public sealed class ExamineInfoResponseMessage : EntityEventArgs public readonly bool OpenAtOldTooltip; public readonly bool KnowTarget; + public readonly string? NameOverride; // Fire added - optional knowledge-based examine name override public ExamineInfoResponseMessage(NetEntity entityUid, int id, FormattedMessage message, List? verbs=null, - bool centerAtCursor=true, bool openAtOldTooltip=true, bool knowTarget = true) + bool centerAtCursor=true, bool openAtOldTooltip=true, bool knowTarget = true, string? nameOverride = null) // Fire edit - allow overriding the displayed examine identity { EntityUid = entityUid; Id = id; @@ -47,6 +48,7 @@ public ExamineInfoResponseMessage(NetEntity entityUid, int id, FormattedMessage CenterAtCursor = centerAtCursor; OpenAtOldTooltip = openAtOldTooltip; KnowTarget = knowTarget; + NameOverride = nameOverride; // Fire added - preserve knowledge-based name override for the client tooltip } } } diff --git a/Content.Shared/Paper/PaperComponent.cs b/Content.Shared/Paper/PaperComponent.cs index 1821b2fe7f4..a0582c1264a 100644 --- a/Content.Shared/Paper/PaperComponent.cs +++ b/Content.Shared/Paper/PaperComponent.cs @@ -13,6 +13,11 @@ public sealed partial class PaperComponent : Component [DataField("content"), AutoNetworkedField] public string Content { get; set; } = ""; + // Fire added start - track paper authorship for SCP knowledge sources + [NonSerialized] + public List KnowledgeAuthorRanges = []; + // Fire added end + [DataField("contentSize")] public int ContentSize { get; set; } = 10000; @@ -82,6 +87,21 @@ public PaperInputTextMessage(string text) } } + // Fire added start - send paper knowledge highlight markup separately from the replicated paper state + [Serializable, NetSerializable] + public sealed class PaperKnowledgeHighlightMessage : BoundUserInterfaceMessage + { + public readonly string RawText; + public readonly string HighlightedText; + + public PaperKnowledgeHighlightMessage(string rawText, string highlightedText) + { + RawText = rawText; + HighlightedText = highlightedText; + } + } + // Fire added end + [Serializable, NetSerializable] public enum PaperUiKey { @@ -108,4 +128,12 @@ public enum PaperStatus : byte Blank, Written } + + // Fire added start - track authored paper ranges for SCP knowledge gating + [Serializable] + public readonly record struct PaperKnowledgeAuthorRange(int Start, int Length, EntityUid? Author) + { + public int End => Start + Length; + } + // Fire added end } diff --git a/Content.Shared/Paper/PaperSystem.cs b/Content.Shared/Paper/PaperSystem.cs index 89500373f11..ee10e1cb265 100644 --- a/Content.Shared/Paper/PaperSystem.cs +++ b/Content.Shared/Paper/PaperSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Numerics; using Content.Shared.Administration.Logs; +using Content.Shared._Scp.Knowledge; // Fire added - paper read events for SCP knowledge sources using Content.Shared.UserInterface; using Content.Shared.Database; using Content.Shared.Examine; @@ -80,6 +81,9 @@ private void BeforeUIOpen(Entity entity, ref BeforeActivatableUI { entity.Comp.Mode = PaperAction.Read; UpdateUserInterface(entity); + // Fire added start - notify SCP knowledge sources about paper reading + RaiseLocalEvent(new ScpKnowledgePaperReadEvent(entity.Owner, args.User, entity.Comp.Content)); + // Fire added end } private void OnExamined(Entity entity, ref ExaminedEvent args) @@ -192,11 +196,13 @@ private void OnInputTextMessage(Entity entity, ref PaperInputTex if (ev.Cancelled) return; - if (args.Text.Length <= entity.Comp.ContentSize) + // Fire edit start - normalize text once and preserve authored content for knowledge-gated paper reading + var normalizedText = NormalizePaperContent(args.Text); + if (normalizedText.Length <= entity.Comp.ContentSize) { - SetContent(entity, args.Text); + SetContent(entity, normalizedText, args.Actor); - var paperStatus = string.IsNullOrWhiteSpace(args.Text) ? PaperStatus.Blank : PaperStatus.Written; + var paperStatus = string.IsNullOrWhiteSpace(normalizedText) ? PaperStatus.Blank : PaperStatus.Written; if (TryComp(entity, out var appearance)) _appearance.SetData(entity, PaperVisuals.Status, paperStatus, appearance); @@ -206,10 +212,11 @@ private void OnInputTextMessage(Entity entity, ref PaperInputTex _adminLogger.Add(LogType.Chat, LogImpact.Low, - $"{ToPrettyString(args.Actor):player} has written on {ToPrettyString(entity):entity} the following text: {args.Text}"); + $"{ToPrettyString(args.Actor):player} has written on {ToPrettyString(entity):entity} the following text: {normalizedText}"); _audio.PlayPvs(entity.Comp.Sound, entity); } + // Fire edit end entity.Comp.Mode = PaperAction.Read; UpdateUserInterface(entity); @@ -283,15 +290,19 @@ public void CopyStamps(Entity source, Entity t } } - public void SetContent(EntityUid entity, string content) + public void SetContent(EntityUid entity, string content, EntityUid? author = null) // Fire edit - allow callers to record paper authorship for knowledge gating { if (!TryComp(entity, out var paper)) return; - SetContent((entity, paper), content); + SetContent((entity, paper), content, author); } - public void SetContent(Entity entity, string content) + public void SetContent(Entity entity, string content, EntityUid? author = null) // Fire edit - propagate paper authorship through shared content updates { + // Fire added start - normalize content before author-range tracking so stored offsets stay stable + content = NormalizePaperContent(content); + UpdateKnowledgeAuthorRanges(entity, content, author); + // Fire added end entity.Comp.Content = content; Dirty(entity); UpdateUserInterface(entity); @@ -306,6 +317,201 @@ public void SetContent(Entity entity, string content) _appearance.SetData(entity, PaperVisuals.Status, status, appearance); } + // Fire added start - preserve paper authorship for SCP knowledge paper sources + private static void UpdateKnowledgeAuthorRanges(Entity entity, string newContent, EntityUid? author) + { + if (string.Equals(entity.Comp.Content, newContent, StringComparison.Ordinal)) + return; + + if (author == null) + { + ResetKnowledgeAuthorRanges(entity.Comp.KnowledgeAuthorRanges, newContent, null); + return; + } + + var oldContent = entity.Comp.Content; + if (oldContent.Length == 0) + { + ResetKnowledgeAuthorRanges(entity.Comp.KnowledgeAuthorRanges, newContent, author); + return; + } + + var oldAuthors = BuildKnowledgeAuthorMap(oldContent.Length, entity.Comp.KnowledgeAuthorRanges); + var newAuthors = new EntityUid?[newContent.Length]; + Array.Fill(newAuthors, author); + + CopyUnchangedKnowledgeAuthors(oldContent, newContent, oldAuthors, newAuthors); + WriteKnowledgeAuthorRanges(entity.Comp.KnowledgeAuthorRanges, newAuthors); + } + + private static void ResetKnowledgeAuthorRanges( + List ranges, + string content, + EntityUid? author) + { + ranges.Clear(); + + if (content.Length > 0) + ranges.Add(new PaperKnowledgeAuthorRange(0, content.Length, author)); + } + + private static EntityUid?[] BuildKnowledgeAuthorMap(int length, List ranges) + { + var authors = new EntityUid?[length]; + if (length == 0) + return authors; + + for (var i = 0; i < ranges.Count; i++) + { + var range = ranges[i]; + if (range.Length <= 0) + continue; + + var start = Math.Clamp(range.Start, 0, length); + var end = Math.Clamp(range.End, start, length); + for (var index = start; index < end; index++) + { + authors[index] = range.Author; + } + } + + return authors; + } + + private static void CopyUnchangedKnowledgeAuthors( + string oldContent, + string newContent, + EntityUid?[] oldAuthors, + EntityUid?[] newAuthors) + { + var prefixLength = GetCommonPrefixLength(oldContent, newContent); + for (var i = 0; i < prefixLength; i++) + { + newAuthors[i] = oldAuthors[i]; + } + + var suffixLength = GetCommonSuffixLength(oldContent, newContent, prefixLength); + for (var i = 0; i < suffixLength; i++) + { + var oldIndex = oldContent.Length - suffixLength + i; + var newIndex = newContent.Length - suffixLength + i; + newAuthors[newIndex] = oldAuthors[oldIndex]; + } + + var oldMiddleStart = prefixLength; + var oldMiddleLength = oldContent.Length - prefixLength - suffixLength; + var newMiddleStart = prefixLength; + var newMiddleLength = newContent.Length - prefixLength - suffixLength; + + if (oldMiddleLength <= 0 || newMiddleLength <= 0) + return; + + const long maxExactDiffArea = 4_000_000; + if ((long) oldMiddleLength * newMiddleLength > maxExactDiffArea) + return; + + var lcs = new int[oldMiddleLength + 1, newMiddleLength + 1]; + for (var oldIndex = 1; oldIndex <= oldMiddleLength; oldIndex++) + { + var oldCharacter = oldContent[oldMiddleStart + oldIndex - 1]; + for (var newIndex = 1; newIndex <= newMiddleLength; newIndex++) + { + if (oldCharacter == newContent[newMiddleStart + newIndex - 1]) + { + lcs[oldIndex, newIndex] = lcs[oldIndex - 1, newIndex - 1] + 1; + continue; + } + + lcs[oldIndex, newIndex] = Math.Max(lcs[oldIndex - 1, newIndex], lcs[oldIndex, newIndex - 1]); + } + } + + var oldCursor = oldMiddleLength; + var newCursor = newMiddleLength; + + while (oldCursor > 0 && newCursor > 0) + { + var oldIndex = oldMiddleStart + oldCursor - 1; + var newIndex = newMiddleStart + newCursor - 1; + + if (oldContent[oldIndex] == newContent[newIndex]) + { + newAuthors[newIndex] = oldAuthors[oldIndex]; + oldCursor--; + newCursor--; + continue; + } + + if (lcs[oldCursor - 1, newCursor] >= lcs[oldCursor, newCursor - 1]) + { + oldCursor--; + } + else + { + newCursor--; + } + } + } + + private static void WriteKnowledgeAuthorRanges(List ranges, EntityUid?[] authors) + { + ranges.Clear(); + if (authors.Length == 0) + return; + + var currentAuthor = authors[0]; + var currentStart = 0; + + for (var i = 1; i < authors.Length; i++) + { + if (authors[i] == currentAuthor) + continue; + + ranges.Add(new PaperKnowledgeAuthorRange(currentStart, i - currentStart, currentAuthor)); + currentStart = i; + currentAuthor = authors[i]; + } + + ranges.Add(new PaperKnowledgeAuthorRange(currentStart, authors.Length - currentStart, currentAuthor)); + } + + private static string NormalizePaperContent(string content) + { + if (content.Length == 0) + return content; + + return content.Replace("\r\n", "\n").Replace('\r', '\n'); + } + + private static int GetCommonPrefixLength(string left, string right) + { + var limit = Math.Min(left.Length, right.Length); + var index = 0; + + while (index < limit && left[index] == right[index]) + { + index++; + } + + return index; + } + + private static int GetCommonSuffixLength(string left, string right, int prefixLength) + { + var suffixLength = 0; + var suffixLimit = Math.Min(left.Length - prefixLength, right.Length - prefixLength); + + while (suffixLength < suffixLimit && + left[left.Length - suffixLength - 1] == right[right.Length - suffixLength - 1]) + { + suffixLength++; + } + + return suffixLength; + } + + // Fire added end + // Sunrise-Start public void SetImageContent(Entity entity, SpriteSpecifier content, Vector2? scale = null) { diff --git a/Content.Shared/_Scp/Knowledge/Components/ScpKnowledgeAcquisitionComponent.cs b/Content.Shared/_Scp/Knowledge/Components/ScpKnowledgeAcquisitionComponent.cs new file mode 100644 index 00000000000..cbd3dcdde66 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/Components/ScpKnowledgeAcquisitionComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Audio; + +namespace Content.Shared._Scp.Knowledge.Components; + +[RegisterComponent] +public sealed partial class ScpKnowledgeAcquisitionComponent : Component +{ + [DataField] + public bool CanLearnByListen = true; + + [DataField] + public bool CanLearnByRead = true; + + [DataField] + public bool CanLearnByExamine = true; + + [DataField] + public bool CanLearnByOther = true; + + [DataField] + public SoundSpecifier? UnlockSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg", AudioParams.Default.WithVolume(-6f)); +} diff --git a/Content.Shared/_Scp/Knowledge/Components/ScpKnowledgeComponent.cs b/Content.Shared/_Scp/Knowledge/Components/ScpKnowledgeComponent.cs new file mode 100644 index 00000000000..d97966445e4 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/Components/ScpKnowledgeComponent.cs @@ -0,0 +1,26 @@ +using Content.Shared._Scp.Knowledge; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Scp.Knowledge.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ScpKnowledgeComponent : Component +{ + public override bool SendOnlyToOwner => true; + + [DataField, AutoNetworkedField] + public HashSet> KnownKnowledge = []; + + [DataField, AutoNetworkedField] + public Dictionary, int> Progress = []; + + [DataField, AutoNetworkedField] + public Dictionary, ScpKnowledgeExposureFlags> ExposureFlags = []; + + [NonSerialized] + public HashSet ProcessedSources = []; + + [NonSerialized] + public HashSet ProcessedMessages = []; +} diff --git a/Content.Shared/_Scp/Knowledge/ScpKnowledgeEvents.cs b/Content.Shared/_Scp/Knowledge/ScpKnowledgeEvents.cs new file mode 100644 index 00000000000..da51d3e45f7 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/ScpKnowledgeEvents.cs @@ -0,0 +1,38 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._Scp.Knowledge; + +public enum ScpKnowledgeAcquisitionChannel : byte +{ + Listen, + Read, + Examine, + Other, +} + +[Flags] +[Serializable, NetSerializable] +public enum ScpKnowledgeExposureFlags : byte +{ + None = 0, + Text = 1 << 0, + Examine = 1 << 1, +} + +public readonly record struct ScpKnowledgeSourceRecord( + EntityUid Source, + ProtoId KnowledgeId, + ScpKnowledgeAcquisitionChannel Channel); + +public readonly record struct ScpKnowledgeMessageRecord( + EntityUid Source, + ProtoId KnowledgeId, + ScpKnowledgeAcquisitionChannel Channel, + string NormalizedMessage); + +[ByRefEvent] +public readonly record struct ScpKnowledgeUnlockedEvent( + ProtoId KnowledgeId, + ScpKnowledgeAcquisitionChannel Channel, + EntityUid? Source = null); diff --git a/Content.Shared/_Scp/Knowledge/ScpKnowledgeLogic.cs b/Content.Shared/_Scp/Knowledge/ScpKnowledgeLogic.cs new file mode 100644 index 00000000000..c77e34ec390 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/ScpKnowledgeLogic.cs @@ -0,0 +1,97 @@ +using Content.Shared._Scp.Knowledge.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Scp.Knowledge; + +public static class ScpKnowledgeLogic +{ + public static bool RequiresTextAndExamine(ScpKnowledgePrototype knowledge) + { + return knowledge.Kind == ScpKnowledgeKind.Entity; + } + + public static bool IsTextChannel(ScpKnowledgeAcquisitionChannel channel) + { + return channel is ScpKnowledgeAcquisitionChannel.Listen or ScpKnowledgeAcquisitionChannel.Read; + } + + public static ScpKnowledgeExposureFlags GetExposureFlags(ScpKnowledgeAcquisitionChannel channel) + { + return channel switch + { + ScpKnowledgeAcquisitionChannel.Listen => ScpKnowledgeExposureFlags.Text, + ScpKnowledgeAcquisitionChannel.Read => ScpKnowledgeExposureFlags.Text, + ScpKnowledgeAcquisitionChannel.Examine => ScpKnowledgeExposureFlags.Examine, + _ => ScpKnowledgeExposureFlags.None, + }; + } + + public static int GetExposureProgress(ScpKnowledgeExposureFlags flags) + { + var progress = 0; + + if ((flags & ScpKnowledgeExposureFlags.Text) != 0) + progress += 1; + + if ((flags & ScpKnowledgeExposureFlags.Examine) != 0) + progress += 1; + + return progress; + } + + public static ScpKnowledgeExposureFlags GetKnowledgeExposureFlags( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId) + { + return knowledgeState.ExposureFlags.GetValueOrDefault(knowledgeId, ScpKnowledgeExposureFlags.None); + } + + public static int GetKnowledgeProgress( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId, + ScpKnowledgePrototype knowledge) + { + if (knowledgeState.KnownKnowledge.Contains(knowledgeId)) + return knowledge.RequiredProgress; + + knowledgeState.Progress.TryGetValue(knowledgeId, out var progress); + + if (!RequiresTextAndExamine(knowledge)) + return progress; + + var exposureProgress = GetExposureProgress(GetKnowledgeExposureFlags(knowledgeState, knowledgeId)); + return Math.Max(progress, exposureProgress); + } + + public static bool IsKnowledgeKnown( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId, + ScpKnowledgePrototype knowledge) + { + return knowledgeState.KnownKnowledge.Contains(knowledgeId) || + GetKnowledgeProgress(knowledgeState, knowledgeId, knowledge) >= knowledge.RequiredProgress; + } + + public static bool WillBeKnownAfterExamine( + ScpKnowledgeComponent knowledgeState, + ProtoId knowledgeId, + ScpKnowledgePrototype knowledge) + { + if (IsKnowledgeKnown(knowledgeState, knowledgeId, knowledge)) + return true; + + if (!RequiresTextAndExamine(knowledge) || + !knowledge.AllowExamine || + knowledge.ExamineProgress <= 0) + { + return false; + } + + var exposureFlags = GetKnowledgeExposureFlags(knowledgeState, knowledgeId) | ScpKnowledgeExposureFlags.Examine; + var progress = Math.Min( + knowledge.RequiredProgress, + Math.Max(GetKnowledgeProgress(knowledgeState, knowledgeId, knowledge), GetExposureProgress(exposureFlags))); + + return progress >= knowledge.RequiredProgress; + } +} diff --git a/Content.Shared/_Scp/Knowledge/ScpKnowledgePaperReadEvent.cs b/Content.Shared/_Scp/Knowledge/ScpKnowledgePaperReadEvent.cs new file mode 100644 index 00000000000..f91bbba25a0 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/ScpKnowledgePaperReadEvent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared._Scp.Knowledge; + +public sealed class ScpKnowledgePaperReadEvent(EntityUid paper, EntityUid user, string content) : EntityEventArgs +{ + public readonly EntityUid Paper = paper; + public readonly EntityUid User = user; + public readonly string Content = content; +} diff --git a/Content.Shared/_Scp/Knowledge/ScpKnowledgePrototype.cs b/Content.Shared/_Scp/Knowledge/ScpKnowledgePrototype.cs new file mode 100644 index 00000000000..fd500669bf6 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/ScpKnowledgePrototype.cs @@ -0,0 +1,76 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; + +namespace Content.Shared._Scp.Knowledge; + +[Prototype] +public sealed partial class ScpKnowledgePrototype : IPrototype, IInheritingPrototype +{ + [IdDataField] + public string ID { get; private set; } = string.Empty; + + [ParentDataField(typeof(AbstractPrototypeIdArraySerializer))] + public string[]? Parents { get; private set; } + + [NeverPushInheritance] + [AbstractDataField] + public bool Abstract { get; private set; } + + [DataField(required: true)] + public ScpKnowledgeKind Kind; + + [DataField(required: true)] + public LocId DisplayName; + + [DataField] + [AlwaysPushInheritance] + public List Abbreviations = []; + + [DataField(required: true)] + public LocId Description; + + [DataField(required: true)] + [AlwaysPushInheritance] + public List RecognitionPatterns = []; + + [DataField] + [AlwaysPushInheritance] + public List EntityPrototypes = []; + + [DataField] + public int RequiredProgress = 2; + + [DataField] + public bool AllowListen = true; + + [DataField] + public bool AllowRead = true; + + [DataField] + public bool AllowExamine = true; + + [DataField] + public int ListenProgress = 1; + + [DataField] + public int ReadProgress = 1; + + [DataField] + public int ExamineProgress = 1; + + [DataField] + public bool HideIdentityUntilKnown; + + [DataField] + public LocId? KnownExamineVerbText; + + [DataField] + public LocId? KnownExamineText; +} + +public enum ScpKnowledgeKind : byte +{ + Term, + Location, + Entity, +} diff --git a/Content.Shared/_Scp/Knowledge/ScpKnowledgeText.cs b/Content.Shared/_Scp/Knowledge/ScpKnowledgeText.cs new file mode 100644 index 00000000000..8c7a571aa93 --- /dev/null +++ b/Content.Shared/_Scp/Knowledge/ScpKnowledgeText.cs @@ -0,0 +1,677 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace Content.Shared._Scp.Knowledge; + +public static class ScpKnowledgeText +{ + public static string NormalizeRecognitionText(string text) + { + return TokenizeRecognitionText(text).NormalizedText; + } + + public static ScpKnowledgeTokenizedText TokenizeRecognitionText(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return ScpKnowledgeTokenizedText.Empty; + + List tokens = []; + var tokenBuilder = new StringBuilder(text.Length); + var normalizedBuilder = new StringBuilder(text.Length); + var tokenStart = -1; + + for (var i = 0; i < text.Length; i++) + { + if (TryConsumeMarkupTag(text, ref i)) + continue; + + var character = NormalizeRecognitionCharacter(text[i]); + if (char.IsLetterOrDigit(character)) + { + if (tokenStart == -1) + tokenStart = i; + + tokenBuilder.Append(character); + continue; + } + + FlushToken(tokens, tokenBuilder, normalizedBuilder, ref tokenStart, i); + } + + FlushToken(tokens, tokenBuilder, normalizedBuilder, ref tokenStart, text.Length); + + return tokens.Count == 0 + ? ScpKnowledgeTokenizedText.Empty + : new ScpKnowledgeTokenizedText(text, normalizedBuilder.ToString(), tokens); + } + + public static bool TryCompileRecognitionPattern( + string pattern, + [NotNullWhen(true)] out ScpKnowledgeCompiledPattern? compiledPattern, + [NotNullWhen(false)] out string? error) + { + compiledPattern = null; + + if (string.IsNullOrWhiteSpace(pattern)) + { + error = "Pattern is empty."; + return false; + } + + try + { + var parser = new ScpKnowledgePatternParser(pattern); + var root = parser.ParsePattern(); + + if (root.MinTokenCount <= 0) + { + error = "Pattern does not contain any recognition tokens."; + return false; + } + + HashSet firstTokens = []; + root.CollectFirstTokens(firstTokens); + if (firstTokens.Count == 0) + { + error = "Pattern does not contain any recognition start tokens."; + return false; + } + + var firstTokensArray = new string[firstTokens.Count]; + firstTokens.CopyTo(firstTokensArray); + compiledPattern = new ScpKnowledgeCompiledPattern(pattern, root, firstTokensArray); + error = null; + return true; + } + catch (ScpKnowledgePatternParseException exception) + { + error = exception.Message; + return false; + } + } + + public static void AddPatternMatches( + ScpKnowledgeCompiledPattern pattern, + ScpKnowledgeTokenizedText tokenizedText, + List matches) + { + if (pattern.MinTokenCount <= 0 || tokenizedText.Tokens.Count < pattern.MinTokenCount) + return; + + var context = new ScpKnowledgePatternMatchContext(); + for (var tokenIndex = 0; tokenIndex <= tokenizedText.Tokens.Count - pattern.MinTokenCount; tokenIndex++) + { + AddPatternMatchesAt(pattern, tokenizedText, tokenIndex, matches, context); + } + } + + public static void AddPatternMatchesAt( + ScpKnowledgeCompiledPattern pattern, + ScpKnowledgeTokenizedText tokenizedText, + int tokenIndex, + List matches, + ScpKnowledgePatternMatchContext? context = null) + { + if (pattern.MinTokenCount <= 0 || + tokenIndex < 0 || + tokenIndex + pattern.MinTokenCount > tokenizedText.Tokens.Count) + { + return; + } + + context ??= new ScpKnowledgePatternMatchContext(); + context.Reset(); + + var endBuffer = context.RentIndicesBuffer(); + pattern.Root.CollectMatchEnds(tokenizedText.Tokens, tokenIndex, endBuffer, context); + + foreach (var endIndex in endBuffer) + { + if (endIndex <= tokenIndex) + continue; + + var startToken = tokenizedText.Tokens[tokenIndex]; + var endToken = tokenizedText.Tokens[endIndex - 1]; + AddUniqueMatch(matches, + new ScpKnowledgeTextMatch( + tokenIndex, + endIndex, + startToken.SourceStart, + endToken.SourceEnd)); + } + } + + private static char NormalizeRecognitionCharacter(char character) + { + var normalized = char.ToLowerInvariant(character); + return normalized == 'ё' ? 'е' : normalized; + } + + private static void FlushToken( + List tokens, + StringBuilder tokenBuilder, + StringBuilder normalizedBuilder, + ref int tokenStart, + int tokenEnd) + { + if (tokenBuilder.Length == 0 || tokenStart == -1) + return; + + var token = tokenBuilder.ToString(); + if (normalizedBuilder.Length > 0) + normalizedBuilder.Append(' '); + + normalizedBuilder.Append(token); + tokens.Add(new ScpKnowledgeTextToken(token, tokenStart, tokenEnd)); + tokenBuilder.Clear(); + tokenStart = -1; + } + + private static bool TryConsumeMarkupTag(string text, ref int index) + { + if (text[index] != '[') + return false; + + var current = index + 1; + if (current >= text.Length) + return false; + + if (text[current] == '/') + current++; + + while (current < text.Length && char.IsWhiteSpace(text[current])) + { + current++; + } + + if (current >= text.Length || !char.IsLetter(text[current])) + return false; + + while (current < text.Length) + { + if (text[current] == '\\') + { + current = Math.Min(current + 2, text.Length); + continue; + } + + if (text[current] == ']') + { + index = current; + return true; + } + + current++; + } + + return false; + } + + private static void AddUniqueMatch(List matches, ScpKnowledgeTextMatch match) + { + foreach (var existingMatch in matches) + { + if (existingMatch == match) + return; + } + + matches.Add(match); + } + + private static void AddUniqueIndex(List indices, int index) + { + foreach (var existingIndex in indices) + { + if (existingIndex == index) + return; + } + + indices.Add(index); + } + + private sealed class ScpKnowledgePatternParser(string pattern) + { + private int _index; + + public ScpKnowledgePatternNode ParsePattern() + { + var node = ParseExpression(); + if (_index != pattern.Length) + throw Error($"Unexpected trailing character '{pattern[_index]}' at index {_index}."); + + return node; + } + + private ScpKnowledgePatternNode ParseExpression(char? terminator = null) + { + List options = []; + var sawAlternative = false; + + while (true) + { + var option = ParseSequence(terminator); + options.Add(option); + + if (_index >= pattern.Length || pattern[_index] != '|') + break; + + sawAlternative = true; + _index++; + + if (_index >= pattern.Length || (terminator != null && pattern[_index] == terminator)) + throw Error("Alternative group contains an empty option."); + } + + if (!sawAlternative) + return options[0]; + + foreach (var optionNode in options) + { + if (optionNode.CanMatchEmpty) + throw Error("Alternative group contains an empty option."); + } + + return new ScpKnowledgeAlternativePatternNode(options); + } + + private ScpKnowledgePatternNode ParseSequence(char? terminator) + { + List children = []; + var literal = new StringBuilder(); + + while (_index < pattern.Length) + { + var character = pattern[_index]; + + if (character == '\\') + { + _index++; + if (_index >= pattern.Length) + throw Error("Pattern ends with an escape character."); + + literal.Append(pattern[_index]); + _index++; + continue; + } + + if (terminator != null && character == terminator) + break; + + if (character == '|') + break; + + switch (character) + { + case '(': + FlushLiteral(children, literal); + _index++; + var group = ParseExpression(')'); + ExpectTerminator(')'); + AddNode(children, group); + continue; + case '[': + FlushLiteral(children, literal); + _index++; + var optional = ParseExpression(']'); + ExpectTerminator(']'); + AddNode(children, new ScpKnowledgeOptionalPatternNode(optional)); + continue; + case ')': + case ']': + throw Error($"Unexpected closing character '{character}' at index {_index}."); + default: + literal.Append(character); + _index++; + continue; + } + } + + FlushLiteral(children, literal); + + return children.Count switch + { + 0 => ScpKnowledgeEmptyPatternNode.Instance, + 1 => children[0], + _ => new ScpKnowledgeSequencePatternNode(children), + }; + } + + private void ExpectTerminator(char terminator) + { + if (_index >= pattern.Length || pattern[_index] != terminator) + throw Error($"Expected closing '{terminator}'."); + + _index++; + } + + private void FlushLiteral(List children, StringBuilder literal) + { + if (literal.Length == 0) + return; + + var tokens = TokenizeRecognitionText(literal.ToString()).Tokens; + literal.Clear(); + + if (tokens.Count == 0) + return; + + var tokenValues = new string[tokens.Count]; + var tokenIndex = 0; + foreach (var token in tokens) + { + tokenValues[tokenIndex++] = token.Value; + } + + children.Add(new ScpKnowledgeLiteralPatternNode(tokenValues)); + } + + private static void AddNode(List children, ScpKnowledgePatternNode node) + { + if (node is ScpKnowledgeEmptyPatternNode) + return; + + children.Add(node); + } + + private ScpKnowledgePatternParseException Error(string message) + { + return new ScpKnowledgePatternParseException($"{message} Pattern: '{pattern}'."); + } + } + + internal static void AddUniquePatternIndex(List indices, int index) + { + AddUniqueIndex(indices, index); + } +} + +public sealed class ScpKnowledgeTokenizedText( + string sourceText, + string normalizedText, + IReadOnlyList tokens) +{ + public static readonly ScpKnowledgeTokenizedText Empty = + new(string.Empty, string.Empty, []); + + public string SourceText { get; } = sourceText; + public string NormalizedText { get; } = normalizedText; + public IReadOnlyList Tokens { get; } = tokens; +} + +public readonly record struct ScpKnowledgeTextToken(string Value, int SourceStart, int SourceEnd) +{ + public int Length => SourceEnd - SourceStart; +} + +public readonly record struct ScpKnowledgeTextMatch(int TokenStart, int TokenEnd, int SourceStart, int SourceEnd) +{ + public int Length => SourceEnd - SourceStart; +} + +public sealed class ScpKnowledgeCompiledPattern +{ + internal ScpKnowledgePatternNode Root { get; } + public string SourcePattern { get; } + public IReadOnlyList FirstTokens { get; } + public int MinTokenCount => Root.MinTokenCount; + + internal ScpKnowledgeCompiledPattern(string sourcePattern, ScpKnowledgePatternNode root, string[] firstTokens) + { + Root = root; + SourcePattern = sourcePattern; + FirstTokens = firstTokens; + } +} + +internal abstract class ScpKnowledgePatternNode +{ + public abstract int MinTokenCount { get; } + public abstract bool CanMatchEmpty { get; } + public abstract void CollectFirstTokens(HashSet firstTokens); + + public abstract void CollectMatchEnds( + IReadOnlyList tokens, + int startIndex, + List ends, + ScpKnowledgePatternMatchContext context); +} + +internal sealed class ScpKnowledgeEmptyPatternNode : ScpKnowledgePatternNode +{ + public static readonly ScpKnowledgeEmptyPatternNode Instance = new(); + + public override int MinTokenCount => 0; + public override bool CanMatchEmpty => true; + public override void CollectFirstTokens(HashSet firstTokens) + { + } + + public override void CollectMatchEnds( + IReadOnlyList tokens, + int startIndex, + List ends, + ScpKnowledgePatternMatchContext context) + { + ScpKnowledgeText.AddUniquePatternIndex(ends, startIndex); + } +} + +internal sealed class ScpKnowledgeLiteralPatternNode(string[] tokens) : ScpKnowledgePatternNode +{ + private readonly string[] _tokens = tokens; + + public override int MinTokenCount => _tokens.Length; + public override bool CanMatchEmpty => _tokens.Length == 0; + public override void CollectFirstTokens(HashSet firstTokens) + { + if (_tokens.Length > 0) + firstTokens.Add(_tokens[0]); + } + + public override void CollectMatchEnds( + IReadOnlyList tokens, + int startIndex, + List ends, + ScpKnowledgePatternMatchContext context) + { + if (startIndex + _tokens.Length > tokens.Count) + return; + + var offset = 0; + foreach (var token in _tokens) + { + if (!string.Equals(tokens[startIndex + offset].Value, token, StringComparison.Ordinal)) + return; + + offset++; + } + + ScpKnowledgeText.AddUniquePatternIndex(ends, startIndex + _tokens.Length); + } +} + +internal sealed class ScpKnowledgeSequencePatternNode(List children) : ScpKnowledgePatternNode +{ + private readonly ScpKnowledgePatternNode[] _children = children.ToArray(); + + public override int MinTokenCount + { + get + { + var total = 0; + foreach (var child in _children) + { + total += child.MinTokenCount; + } + + return total; + } + } + + public override void CollectFirstTokens(HashSet firstTokens) + { + foreach (var child in _children) + { + child.CollectFirstTokens(firstTokens); + if (!child.CanMatchEmpty) + break; + } + } + + public override bool CanMatchEmpty + { + get + { + foreach (var child in _children) + { + if (!child.CanMatchEmpty) + return false; + } + + return true; + } + } + + public override void CollectMatchEnds( + IReadOnlyList tokens, + int startIndex, + List ends, + ScpKnowledgePatternMatchContext context) + { + MatchChild(tokens, startIndex, 0, ends, context); + } + + private void MatchChild( + IReadOnlyList tokens, + int startIndex, + int childIndex, + List ends, + ScpKnowledgePatternMatchContext context) + { + if (childIndex >= _children.Length) + { + ScpKnowledgeText.AddUniquePatternIndex(ends, startIndex); + return; + } + + if (startIndex + GetRemainingMinTokenCount(childIndex) > tokens.Count) + return; + + var childEnds = context.RentIndicesBuffer(); + _children[childIndex].CollectMatchEnds(tokens, startIndex, childEnds, context); + + foreach (var childEnd in childEnds) + { + MatchChild(tokens, childEnd, childIndex + 1, ends, context); + } + } + + private int GetRemainingMinTokenCount(int childIndex) + { + var total = 0; + for (var i = childIndex; i < _children.Length; i++) + { + total += _children[i].MinTokenCount; + } + + return total; + } +} + +internal sealed class ScpKnowledgeAlternativePatternNode(List options) : ScpKnowledgePatternNode +{ + private readonly ScpKnowledgePatternNode[] _options = options.ToArray(); + + public override int MinTokenCount + { + get + { + var min = int.MaxValue; + foreach (var option in _options) + { + min = Math.Min(min, option.MinTokenCount); + } + + return min == int.MaxValue ? 0 : min; + } + } + + public override void CollectFirstTokens(HashSet firstTokens) + { + foreach (var option in _options) + { + option.CollectFirstTokens(firstTokens); + } + } + + public override bool CanMatchEmpty + { + get + { + foreach (var option in _options) + { + if (option.CanMatchEmpty) + return true; + } + + return false; + } + } + + public override void CollectMatchEnds( + IReadOnlyList tokens, + int startIndex, + List ends, + ScpKnowledgePatternMatchContext context) + { + foreach (var option in _options) + { + option.CollectMatchEnds(tokens, startIndex, ends, context); + } + } +} + +internal sealed class ScpKnowledgeOptionalPatternNode(ScpKnowledgePatternNode child) : ScpKnowledgePatternNode +{ + private readonly ScpKnowledgePatternNode _child = child; + + public override int MinTokenCount => 0; + public override bool CanMatchEmpty => true; + public override void CollectFirstTokens(HashSet firstTokens) + { + _child.CollectFirstTokens(firstTokens); + } + + public override void CollectMatchEnds( + IReadOnlyList tokens, + int startIndex, + List ends, + ScpKnowledgePatternMatchContext context) + { + ScpKnowledgeText.AddUniquePatternIndex(ends, startIndex); + _child.CollectMatchEnds(tokens, startIndex, ends, context); + } +} + +public sealed class ScpKnowledgePatternMatchContext +{ + private readonly List> _indexBuffers = []; + private int _bufferIndex; + + public void Reset() + { + _bufferIndex = 0; + } + + public List RentIndicesBuffer() + { + if (_bufferIndex >= _indexBuffers.Count) + _indexBuffers.Add([]); + + var buffer = _indexBuffers[_bufferIndex++]; + buffer.Clear(); + return buffer; + } +} + +internal sealed class ScpKnowledgePatternParseException(string message) : Exception(message); diff --git a/Content.Tests/Shared/_Scp/Knowledge/ScpKnowledgeTextTest.cs b/Content.Tests/Shared/_Scp/Knowledge/ScpKnowledgeTextTest.cs new file mode 100644 index 00000000000..044b87535a5 --- /dev/null +++ b/Content.Tests/Shared/_Scp/Knowledge/ScpKnowledgeTextTest.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using Content.Shared._Scp.Knowledge; +using NUnit.Framework; + +namespace Content.Tests.Shared._Scp.Knowledge; + +[TestFixture] +public sealed class ScpKnowledgeTextTest +{ + [Test] + public void AlternativesMatchAnyOption() + { + var matches = Match("(scp|сцп) 173", "СЦП-173 был замечен"); + + Assert.That(matches.Count, Is.EqualTo(1)); + } + + [Test] + public void OptionalGroupIsAppliedOnlyWhenPresent() + { + Assert.Multiple(() => + { + Assert.That(Match("object[ number] 049", "object 049").Count, Is.EqualTo(1)); + Assert.That(Match("object[ number] 049", "object number 049").Count, Is.EqualTo(1)); + }); + } + + [Test] + public void NestedGroupsMatchExpectedSequence() + { + var matches = Match("(((scp|сцп))|((объект)[ (номер)])) 131", "объект номер 131"); + + Assert.That(matches.Count, Is.EqualTo(1)); + } + + [Test] + public void EscapedMetacharactersAreParsedAsLiteralText() + { + var matches = Match("\\(object\\|number\\) 049", "(object|number) 049"); + + Assert.That(matches.Count, Is.EqualTo(1)); + } + + [Test] + public void SeparatorVariantsMatchTheSamePattern() + { + Assert.Multiple(() => + { + Assert.That(Match("scp 939", "scp-939").Count, Is.EqualTo(1)); + Assert.That(Match("scp 939", "scp 939").Count, Is.EqualTo(1)); + }); + } + + [Test] + public void ExplicitFillerWordsMustBeAuthored() + { + Assert.Multiple(() => + { + Assert.That(Match("object[ number] 049", "object number 049").Count, Is.EqualTo(1)); + Assert.That(Match("object[ number] 049", "object many random words 049").Count, Is.EqualTo(0)); + }); + } + + [Test] + public void MatchSpanCoversOriginalSourceText() + { + const string text = "Внимание: объект номер 049 уже здесь."; + var matches = Match("object[ number] 049|(объект)[ (номер)] 049", text); + + Assert.That(matches.Count, Is.EqualTo(1)); + Assert.That(text[matches[0].SourceStart..matches[0].SourceEnd], Is.EqualTo("объект номер 049")); + } + + [Test] + public void RussianAndEnglishPatternsBothWorkWhenResolved() + { + Assert.Multiple(() => + { + Assert.That(Match("(scp|сцп) 131", "СЦП 131").Count, Is.EqualTo(1)); + Assert.That(Match("object[ number] 049", "object number 049").Count, Is.EqualTo(1)); + }); + } + + private static List Match(string patternText, string text) + { + Assert.That( + ScpKnowledgeText.TryCompileRecognitionPattern(patternText, out var pattern, out var error), + Is.True, + error); + + var matches = new List(); + ScpKnowledgeText.AddPatternMatches(pattern!, ScpKnowledgeText.TokenizeRecognitionText(text), matches); + return matches; + } +} diff --git a/Resources/Locale/en-US/_strings/_scp/knowledge.ftl b/Resources/Locale/en-US/_strings/_scp/knowledge.ftl new file mode 100644 index 00000000000..dd69230ad37 --- /dev/null +++ b/Resources/Locale/en-US/_strings/_scp/knowledge.ftl @@ -0,0 +1,263 @@ +scp-knowledge-known-examine-verb = Briefing +scp-knowledge-unknown-examine = OOC: Your character cannot identify this without additional knowledge. +scp-knowledge-unlocked-popup = New knowledge: {$knowledge} +scp-knowledge-scp-prefixes = scp|сцп|s c p|с ц п|эс си пи +scp-knowledge-scp-colloquial-prefixes = сцпшка|сцпшки|сцпшке|сцпшку|сцпшкой +scp-knowledge-object-word-forms = object|item|объект|объекта|объекту|объектом|объекте +scp-knowledge-number-word-forms = number|no|num|номер|номера|номеру|номером|номере +scp-knowledge-number-link-word-forms = with|под|с|за|under +scp-knowledge-designation-word-forms = designation|designations|обозначение|обозначения|обозначению|обозначением|обозначении +scp-knowledge-ordinal-suffix-forms = й|го|му|м|ым +scp-knowledge-scp-full-prefix-forms = ({ scp-knowledge-scp-prefixes })[ ({ scp-knowledge-number-word-forms })]|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-scp-prefixes })|({ scp-knowledge-designation-word-forms }) ({ scp-knowledge-scp-prefixes })|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-designation-word-forms })|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-object-word-forms })[ ({ scp-knowledge-number-word-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-scp-prefixes })[ ({ scp-knowledge-number-word-forms })] + +scp-knowledge-term-tzs-name = HCZ +scp-knowledge-term-tzs-full = Heavy Containment Zone +scp-knowledge-term-tzs-patterns = ТЗС|Тяжелая зона содержания|зона тяжелого содержания|тяжелой зоне содержания|тяжелую зону содержания|тяжелой зоны содержания|тяжелая зона|тяжелой зоне|тяжелую зону|тяжелой зоны|тяжелым контейнментом|тяжелого контейнмента|тяжелому контейнменту|тяжелый контейнмент|heavy containment|тяжка|тяжке|тяжку|тяжкой|хеви зона|heavy zone|хеви|heavy +scp-knowledge-term-tzs-desc = OOC: An abbreviation for the Heavy Containment Zone, the sector that holds the most dangerous anomalies. + +scp-knowledge-term-lzs-name = LCZ +scp-knowledge-term-lzs-full = Light Containment Zone +scp-knowledge-term-lzs-patterns = ЛЗС|Легкая зона содержания|зона легкого содержания|легкой зоне содержания|легкую зону содержания|легкой зоны содержания|легкая зона|легкой зоне|легкую зону|легкой зоны|легким контейнментом|легкого контейнмента|легкому контейнменту|легкий контейнмент|light containment|лайт зона|light zone|лайт|лайте|light +scp-knowledge-term-lzs-desc = OOC: An abbreviation for the Light Containment Zone, the sector with less dangerous anomalies. + +scp-knowledge-term-vaz-name = EAZ +scp-knowledge-term-vaz-full = External Administrative Zone +scp-knowledge-term-vaz-patterns = ВАЗ|Внешняя административная зона|внешней административной зоне|внешнюю административную зону|внешней административной зоны|внешняя админ зона|внешней админ зоне|внешнюю админ зону|внешней админ зоны|external admin zone|external admin sector +scp-knowledge-term-vaz-desc = OOC: An abbreviation for the External Administrative Zone. It usually includes the LCZ and the administrative sector. + +scp-knowledge-term-az-name = AZ +scp-knowledge-term-az-full = Administrative Zone +scp-knowledge-term-az-patterns = АЗ|Административная зона|административной зоне|административную зону|административной зоны|административная зона комплекса|админ зона|админ зоне|админ зону|админ зоной|админзона|админки|админке|админку|админкой|административка|административки|административке|административку|административкой|admin zone|administrative sector +scp-knowledge-term-az-desc = OOC: An abbreviation for the facility's administrative sector and service spaces. + +scp-knowledge-term-ous-name = SCP Procedures +scp-knowledge-term-ous-full = Special Containment Procedures +scp-knowledge-term-ous-patterns = ОУС|Особые условия содержания|особых условий содержания|особым условиям содержания|особые процедуры содержания|специальные условия содержания|процедуры содержания|процедур содержания|процедурам содержания|процедурами содержания|спецпроцедуры содержания|спецпроцедур содержания|спецпроцедурам содержания|спецпроцедурами содержания +scp-knowledge-term-ous-desc = OOC: The rules and methods the Foundation uses to keep a specific SCP contained. + +scp-knowledge-term-nus-name = CB +scp-knowledge-term-nus-full = Containment Breach +scp-knowledge-term-nus-patterns = НУС|Нарушение условий содержания|нарушение содержания|нарушении условий содержания|нарушения условий содержания|брич|брича|бричу|бричем|бриче|контейнмент брич +scp-knowledge-term-nus-desc = OOC: A situation where an SCP has escaped control or left containment. + +scp-knowledge-term-vus-name = CR +scp-knowledge-term-vus-full = Containment Restoration +scp-knowledge-term-vus-patterns = ВУС|Восстановление условий содержания|восстановление содержания|восстановлении условий содержания|восстановления условий содержания|реконтейн|реконтейна|реконтейну|реконтейном|реконтейне|реконтейнмент|реконтейнмента|реконтейнменту|реконтейнментом|реконтейнменте|recontainment|операция реконтейна|операции реконтейна|операцию реконтейна|операцией реконтейна|операция восстановления содержания|операции восстановления содержания|recontain|recontain op +scp-knowledge-term-vus-desc = OOC: Operations to recontain an SCP and restore normal control. + +scp-knowledge-term-ks-name = CC +scp-knowledge-term-ks-full = Containment Chamber +scp-knowledge-term-ks-patterns = КС|Камера содержания|Containment Cell|камеры содержания|камере содержания|камеру содержания|камерой содержания|камера объекта|камеры объекта|камере объекта|камеру объекта|камерой объекта +scp-knowledge-term-ks-desc = OOC: A room or module used to hold an SCP. + +scp-knowledge-term-ru-name = RA +scp-knowledge-term-ru-full = Regional Administration +scp-knowledge-term-ru-patterns = РУ|Региональное управление|Regional Command|регионального управления|региональному управлению|регуправление|регуправления|регуправлению|регуправлением|регуправлении +scp-knowledge-term-ru-desc = OOC: The regional leadership and administrative authority of the Foundation. + +scp-knowledge-term-ph-name = CI +scp-knowledge-term-ph-full = Chaos Insurgency +scp-knowledge-term-ph-patterns = ПХ|Повстанцы Хаоса|Chaos Insurgents|хаосы|хаосов|хаосам|хаосами|хаосах|бойцы хаоса|бойцов хаоса|бойцам хаоса|бойцами хаоса|отряд хаоса|отряда хаоса|отряду хаоса|отрядом хаоса|forces of chaos insurgency +scp-knowledge-term-ph-desc = OOC: A hostile organization involved in sabotage, theft of anomalies, and attacks against the Foundation. + +scp-knowledge-term-gok-name = GOC +scp-knowledge-term-gok-full = Global Occult Coalition +scp-knowledge-term-gok-patterns = ГОК|Глобальная оккультная коалиция|оккультная коалиция|оккультной коалиции|оккультную коалицию|оккультной коалицией|occult coalition|global coalition +scp-knowledge-term-gok-desc = OOC: An international organization that prefers destroying anomalies instead of containing them. + +scp-knowledge-term-pii-name = PAI +scp-knowledge-term-pii-full = Personal Artificial Intelligence +scp-knowledge-term-pii-patterns = ПИИ|Персональный искусственный интеллект|персональный ии|персонального ии|персональному ии|персональным ии|персональном ии|паи|personal ai +scp-knowledge-term-pii-desc = OOC: An abbreviation for compact AI assistants and related systems. + +scp-knowledge-term-pk-name = LC +scp-knowledge-term-pk-full = Legal Code +scp-knowledge-term-pk-patterns = ПК|Правовой кодекс|правового кодекса|правовому кодексу|правовым кодексом|правовом кодексе +scp-knowledge-term-pk-desc = OOC: The main legal rules that facility personnel are expected to follow. + +scp-knowledge-term-ke-name = COE +scp-knowledge-term-ke-full = Code of Ethics +scp-knowledge-term-ke-patterns = КЭ|Кодекс этики|этический кодекс|этического кодекса|этическому кодексу|этическим кодексом|этическом кодексе|кодекс этики|кодекса этики|кодексу этики|кодексом этики|ethics code +scp-knowledge-term-ke-desc = OOC: A set of ethical limitations and conduct standards for Foundation staff. + +scp-knowledge-location-tzs-name = Heavy Containment Zone +scp-knowledge-location-tzs-patterns = ТЗС|Тяжелая зона содержания|зона тяжелого содержания|тяжелой зоне содержания|тяжелую зону содержания|тяжелой зоны содержания|тяжелая зона|тяжелой зоне|тяжелую зону|тяжелой зоны|тяжелым контейнментом|тяжелого контейнмента|тяжелому контейнменту|тяжелый контейнмент|heavy containment|тяжка|тяжке|тяжку|тяжкой|хеви зона|heavy zone|хеви|heavy +scp-knowledge-location-tzs-desc = OOC: The part of the facility that houses the most dangerous SCPs, with stronger security and tighter access control. + +scp-knowledge-location-lzs-name = Light Containment Zone +scp-knowledge-location-lzs-patterns = ЛЗС|Легкая зона содержания|зона легкого содержания|легкой зоне содержания|легкую зону содержания|легкой зоны содержания|легкая зона|легкой зоне|легкую зону|легкой зоны|легким контейнментом|легкого контейнмента|легкому контейнменту|легкий контейнмент|light containment|лайт зона|light zone|лайт|лайте|light +scp-knowledge-location-lzs-desc = OOC: The part of the facility with lower-risk anomalies, research spaces, and service infrastructure. + +scp-knowledge-location-zsd-abbr = CDZ +scp-knowledge-location-zsd-name = Class-D Containment Zone +scp-knowledge-location-zsd-patterns = ЗСД|Зона содержания класса Д|class d zone|d-class zone|зона класса д|зоны класса д|зоне класса д|зону класса д|д блок|д блока|д блоку|д блоком|д блоке|блок д класса|блок д-класса|блок д классу|блок д классом|d block|dblock|class d block|class-d block|dclass block|дешка|дешки|дешке|дешку|дешкой|дшка|дшки|дшке|дшку|дшкой|дкласс|дкласса|дклассу|дклассом|дклассе|д класс|д класса|д классу|д классом|д классе|dclass|classd|дэблок|дэблока|дэблоку|дэблоком|дэблоке +scp-knowledge-location-zsd-desc = OOC: The housing and security sector used to hold and manage Class-D personnel. + +scp-knowledge-scp035-name = SCP-035 +scp-knowledge-scp035-designation = ({ scp-knowledge-scp-full-prefix-forms }) 035|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 035|({ scp-knowledge-scp-prefixes }) 035 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 035|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 035 +scp-knowledge-scp035-object = ({ scp-knowledge-object-word-forms }) 035 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 035|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 035|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 035 +scp-knowledge-scp035-desc = OOC: An anomalous mask that seizes control of the wearer and bends them to its will. +scp-knowledge-scp035-known = OOC: Do not wear SCP-035. Isolate the mask and avoid direct contact with an infected host. + +scp-knowledge-scp049-name = SCP-049 +scp-knowledge-scp049-designation = ({ scp-knowledge-scp-full-prefix-forms }) 049|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 049|({ scp-knowledge-scp-prefixes }) 049 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 049|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 049 +scp-knowledge-scp049-object = ({ scp-knowledge-object-word-forms }) 049 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 049|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 049|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 049 +scp-knowledge-scp049-desc = OOC: A humanoid resembling a plague doctor. Deadly at close range and able to turn corpses into servants. +scp-knowledge-scp049-known = OOC: Do not let SCP-049 get close. Direct contact with it is lethal. + +scp-knowledge-scp082-name = SCP-082 +scp-knowledge-scp082-designation = ({ scp-knowledge-scp-full-prefix-forms }) 082|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 082|({ scp-knowledge-scp-prefixes }) 082 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 082|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 082 +scp-knowledge-scp082-object = ({ scp-knowledge-object-word-forms }) 082 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 082|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 082|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 082 +scp-knowledge-scp082-desc = OOC: A large, durable humanoid with great physical strength and dangerous close-quarters combat. +scp-knowledge-scp082-known = OOC: Keep your distance, avoid melee, and handle SCP-082 as a group. + +scp-knowledge-scp096-name = SCP-096 +scp-knowledge-scp096-designation = ({ scp-knowledge-scp-full-prefix-forms }) 096|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 096|({ scp-knowledge-scp-prefixes }) 096 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 096|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 096 +scp-knowledge-scp096-object = ({ scp-knowledge-object-word-forms }) 096 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 096|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 096|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 096 +scp-knowledge-scp096-desc = OOC: An anomalous humanoid that enters a violent rage when someone sees its face. +scp-knowledge-scp096-known = OOC: Do not look at SCP-096's face. Break line of sight and leave the area immediately. + +scp-knowledge-scp106-name = SCP-106 +scp-knowledge-scp106-designation = ({ scp-knowledge-scp-full-prefix-forms }) 106|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 106|({ scp-knowledge-scp-prefixes }) 106 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 106|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 106 +scp-knowledge-scp106-object = ({ scp-knowledge-object-word-forms }) 106 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 106|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 106|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 106 +scp-knowledge-scp106-desc = OOC: A corrosive humanoid capable of moving through obstacles and dragging victims away. +scp-knowledge-scp106-known = OOC: Do not rely on doors or walls. SCP-106 is slow, but ordinary barriers will not stop it. + +scp-knowledge-scp131-name = SCP-131 +scp-knowledge-scp131-designation = ({ scp-knowledge-scp-full-prefix-forms }) 131|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 131|({ scp-knowledge-scp-prefixes }) 131 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 131|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 131 +scp-knowledge-scp131-object = ({ scp-knowledge-object-word-forms }) 131 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 131|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 131|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 131 +scp-knowledge-scp131-desc = OOC: A small, peaceful, eye-like Safe-class entity. +scp-knowledge-scp131-known = OOC: SCP-131 is normally non-hostile and is not considered a threat on its own. + +scp-knowledge-scp173-name = SCP-173 +scp-knowledge-scp173-designation = ({ scp-knowledge-scp-full-prefix-forms }) 173|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 173|({ scp-knowledge-scp-prefixes }) 173 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 173|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 173 +scp-knowledge-scp173-object = ({ scp-knowledge-object-word-forms }) 173 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 173|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 173|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 173 +scp-knowledge-scp173-nickname = The Sculpture +scp-knowledge-scp173-desc = OOC: A moving statue that attacks the moment nobody is looking at it. +scp-knowledge-scp173-known = OOC: Maintain direct visual contact with SCP-173 and coordinate blinking with other people. + +scp-knowledge-scp247-name = SCP-247 +scp-knowledge-scp247-designation = ({ scp-knowledge-scp-full-prefix-forms }) 247|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 247|({ scp-knowledge-scp-prefixes }) 247 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 247|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 247 +scp-knowledge-scp247-object = ({ scp-knowledge-object-word-forms }) 247 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 247|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 247|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 247 +scp-knowledge-scp247-desc = OOC: A small predatory SCP resembling a dangerous feline creature. +scp-knowledge-scp247-known = OOC: Do not underestimate SCP-247. Its size does not make it safe in close quarters. + +scp-knowledge-scp457-name = SCP-457 +scp-knowledge-scp457-designation = ({ scp-knowledge-scp-full-prefix-forms }) 457|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 457|({ scp-knowledge-scp-prefixes }) 457 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 457|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 457 +scp-knowledge-scp457-object = ({ scp-knowledge-object-word-forms }) 457 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 457|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 457|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 457 +scp-knowledge-scp457-desc = OOC: A living fire entity that deals intense heat damage and is vulnerable to cooling. +scp-knowledge-scp457-known = OOC: Water and cold are effective against SCP-457. Do not let it reach flammable materials. + +scp-knowledge-scp939-name = SCP-939 +scp-knowledge-scp939-designation = ({ scp-knowledge-scp-full-prefix-forms }) 939|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 939|({ scp-knowledge-scp-prefixes }) 939 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 939|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 939 +scp-knowledge-scp939-object = ({ scp-knowledge-object-word-forms }) 939 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 939|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 939|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 939 +scp-knowledge-scp939-desc = OOC: A large predatory SCP with strong melee capability and good effectiveness in darkness. +scp-knowledge-scp939-known = OOC: Keep your distance, do not let SCP-939 force melee, and fight it with numbers. + +scp-knowledge-scp999-name = SCP-999 +scp-knowledge-scp999-designation = ({ scp-knowledge-scp-full-prefix-forms }) 999|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 999|({ scp-knowledge-scp-prefixes }) 999 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 999|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 999 +scp-knowledge-scp999-object = ({ scp-knowledge-object-word-forms }) 999 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 999|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 999|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 999 +scp-knowledge-scp999-desc = OOC: A peaceful slime-like Safe-class entity that can calm and help people. +scp-knowledge-scp999-known = OOC: SCP-999 is normally safe for staff and can help injured people when the situation allows. + +scp-knowledge-scp079-name = SCP-079 +scp-knowledge-scp079-designation = ({ scp-knowledge-scp-full-prefix-forms }) 079|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 079|({ scp-knowledge-scp-prefixes }) 079 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 079|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 079 +scp-knowledge-scp079-object = ({ scp-knowledge-object-word-forms }) 079 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 079|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 079|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 079 +scp-knowledge-scp079-desc = OOC: An anomalous artificial intelligence tied to computer hardware and facility systems. +scp-knowledge-scp079-known = OOC: SCP-079 acts through cameras, doors, and communications. Limit its access to infrastructure. + +scp-knowledge-scp005-name = SCP-005 +scp-knowledge-scp005-designation = ({ scp-knowledge-scp-full-prefix-forms }) 005|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 005|({ scp-knowledge-scp-prefixes }) 005 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 005|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 005 +scp-knowledge-scp005-object = ({ scp-knowledge-object-word-forms }) 005 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 005|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 005|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 005 +scp-knowledge-scp005-desc = OOC: An anomalous key that can open nearly any lock or door. +scp-knowledge-scp005-known = OOC: SCP-005 effectively grants near-total access to locks and doors. Losing it is a major security risk. + +scp-knowledge-scp012-name = SCP-012 +scp-knowledge-scp012-designation = ({ scp-knowledge-scp-full-prefix-forms }) 012|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 012|({ scp-knowledge-scp-prefixes }) 012 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 012|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 012 +scp-knowledge-scp012-object = ({ scp-knowledge-object-word-forms }) 012 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 012|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 012|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 012 +scp-knowledge-scp012-desc = OOC: An unfinished manuscript that compels people to complete it with their own blood. +scp-knowledge-scp012-known = OOC: Do not work with SCP-012 without protection and supervision. Prolonged observation is dangerous. + +scp-knowledge-scp018-name = SCP-018 +scp-knowledge-scp018-designation = ({ scp-knowledge-scp-full-prefix-forms }) 018|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 018|({ scp-knowledge-scp-prefixes }) 018 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 018|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 018 +scp-knowledge-scp018-object = ({ scp-knowledge-object-word-forms }) 018 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 018|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 018|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 018 +scp-knowledge-scp018-desc = OOC: An anomalous red ball with extremely dangerous rebound behavior and rapidly escalating ricochets. +scp-knowledge-scp018-known = OOC: Do not throw SCP-018 in enclosed spaces without preparation. The ricochets get out of control fast. + +scp-knowledge-scp101-name = SCP-101 +scp-knowledge-scp101-designation = ({ scp-knowledge-scp-full-prefix-forms }) 101|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 101|({ scp-knowledge-scp-prefixes }) 101 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 101|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 101 +scp-knowledge-scp101-object = ({ scp-knowledge-object-word-forms }) 101 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 101|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 101|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 101 +scp-knowledge-scp101-desc = OOC: An anomalous bag capable of transforming whatever is placed inside it. +scp-knowledge-scp101-known = OOC: Do not place living targets or critical equipment into SCP-101 without explicit research orders. + +scp-knowledge-scp109-name = SCP-109 +scp-knowledge-scp109-designation = ({ scp-knowledge-scp-full-prefix-forms }) 109|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 109|({ scp-knowledge-scp-prefixes }) 109 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 109|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 109 +scp-knowledge-scp109-object = ({ scp-knowledge-object-word-forms }) 109 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 109|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 109|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 109 +scp-knowledge-scp109-desc = OOC: An anomalous canteen that gradually refills itself with useful liquid. +scp-knowledge-scp109-known = OOC: SCP-109 is valuable as a source of water and restorative fluid. Control who gets access to it. + +scp-knowledge-scp124-name = SCP-124 +scp-knowledge-scp124-designation = ({ scp-knowledge-scp-full-prefix-forms }) 124|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 124|({ scp-knowledge-scp-prefixes }) 124 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 124|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 124 +scp-knowledge-scp124-object = ({ scp-knowledge-object-word-forms }) 124 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 124|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 124|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 124 +scp-knowledge-scp124-desc = OOC: An anomalous soil and soil bag that improves plant growth and recovery. +scp-knowledge-scp124-known = OOC: SCP-124 is useful for agricultural and restorative work. It is a utility object, not a combat threat. + +scp-knowledge-scp127-name = SCP-127 +scp-knowledge-scp127-designation = ({ scp-knowledge-scp-full-prefix-forms }) 127|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 127|({ scp-knowledge-scp-prefixes }) 127 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 127|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 127 +scp-knowledge-scp127-object = ({ scp-knowledge-object-word-forms }) 127 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 127|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 127|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 127 +scp-knowledge-scp127-desc = OOC: A living anomalous rifle that regenerates its own ammunition. +scp-knowledge-scp127-known = OOC: SCP-127 is dangerous as both a weapon and a living anomaly. Do not dismantle it without clearance. + +scp-knowledge-scp2022-name = SCP-2022 +scp-knowledge-scp2022-designation = ({ scp-knowledge-scp-full-prefix-forms }) 2022|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2022|({ scp-knowledge-scp-prefixes }) 2022 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 2022|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2022 +scp-knowledge-scp2022-object = ({ scp-knowledge-object-word-forms }) 2022 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 2022|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2022|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 2022 +scp-knowledge-scp2022-desc = OOC: Anomalous pills with strong psychoactive and stimulant effects. +scp-knowledge-scp2022-known = OOC: Use SCP-2022 only under specialist supervision. Self-medication is not recommended. + +scp-knowledge-scp207-name = SCP-207 +scp-knowledge-scp207-designation = ({ scp-knowledge-scp-full-prefix-forms }) 207|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 207|({ scp-knowledge-scp-prefixes }) 207 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 207|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 207 +scp-knowledge-scp207-object = ({ scp-knowledge-object-word-forms }) 207 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 207|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 207|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 207 +scp-knowledge-scp207-desc = OOC: An anomalous drink that sharply increases speed and alters the user's physiology. +scp-knowledge-scp207-known = OOC: SCP-207 provides a powerful short-term effect. Using it without control is risky. + +scp-knowledge-scp2398-name = SCP-2398 +scp-knowledge-scp2398-designation = ({ scp-knowledge-scp-full-prefix-forms }) 2398|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2398|({ scp-knowledge-scp-prefixes }) 2398 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 2398|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2398 +scp-knowledge-scp2398-object = ({ scp-knowledge-object-word-forms }) 2398 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 2398|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2398|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 2398 +scp-knowledge-scp2398-desc = OOC: An anomalous baseball bat with unusual kinetic behavior. +scp-knowledge-scp2398-known = OOC: SCP-2398 is dangerous in throws and strikes. Do not treat it like ordinary sports equipment. + +scp-knowledge-scp261-name = SCP-261 +scp-knowledge-scp261-designation = ({ scp-knowledge-scp-full-prefix-forms }) 261|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 261|({ scp-knowledge-scp-prefixes }) 261 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 261|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 261 +scp-knowledge-scp261-object = ({ scp-knowledge-object-word-forms }) 261 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 261|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 261|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 261 +scp-knowledge-scp261-desc = OOC: An anomalous vending machine that dispenses random food in exchange for bills. +scp-knowledge-scp261-known = OOC: SCP-261 is unpredictable in what it dispenses. Use it only in a controlled setting. + +scp-knowledge-scp268-name = SCP-268 +scp-knowledge-scp268-designation = ({ scp-knowledge-scp-full-prefix-forms }) 268|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 268|({ scp-knowledge-scp-prefixes }) 268 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 268|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 268 +scp-knowledge-scp268-object = ({ scp-knowledge-object-word-forms }) 268 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 268|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 268|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 268 +scp-knowledge-scp268-desc = OOC: An anomalous cap that makes the wearer harder to notice. +scp-knowledge-scp268-known = OOC: SCP-268 is useful for stealth, but it should never circulate freely among rule-breakers. + +scp-knowledge-scp330-name = SCP-330 +scp-knowledge-scp330-designation = ({ scp-knowledge-scp-full-prefix-forms }) 330|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 330|({ scp-knowledge-scp-prefixes }) 330 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 330|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 330 +scp-knowledge-scp330-object = ({ scp-knowledge-object-word-forms }) 330 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 330|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 330|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 330 +scp-knowledge-scp330-desc = OOC: An anomalous bowl of candy. Taking more than two is extremely dangerous. +scp-knowledge-scp330-known = OOC: Never take more than two candies from SCP-330. That is the basic handling rule for the object. + +scp-knowledge-scp427-name = SCP-427 +scp-knowledge-scp427-designation = ({ scp-knowledge-scp-full-prefix-forms }) 427|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 427|({ scp-knowledge-scp-prefixes }) 427 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 427|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 427 +scp-knowledge-scp427-object = ({ scp-knowledge-object-word-forms }) 427 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 427|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 427|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 427 +scp-knowledge-scp427-desc = OOC: An anomalous pendant with a strong regenerative effect on the wearer. +scp-knowledge-scp427-known = OOC: SCP-427 should be used carefully and only under observation. Unauthorized use is discouraged. + +scp-knowledge-scp500-name = SCP-500 +scp-knowledge-scp500-designation = ({ scp-knowledge-scp-full-prefix-forms }) 500|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 500|({ scp-knowledge-scp-prefixes }) 500 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 500|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 500 +scp-knowledge-scp500-object = ({ scp-knowledge-object-word-forms }) 500 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 500|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 500|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 500 +scp-knowledge-scp500-desc = OOC: Anomalous pills with universal healing properties. +scp-knowledge-scp500-known = OOC: SCP-500 is extremely valuable as an emergency treatment item. Its use must be strictly tracked. + +scp-knowledge-scp714-name = SCP-714 +scp-knowledge-scp714-designation = ({ scp-knowledge-scp-full-prefix-forms }) 714|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 714|({ scp-knowledge-scp-prefixes }) 714 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 714|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 714 +scp-knowledge-scp714-object = ({ scp-knowledge-object-word-forms }) 714 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 714|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 714|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 714 +scp-knowledge-scp714-desc = OOC: An anomalous ring that protects against some mental effects but heavily drains the wearer. +scp-knowledge-scp714-known = OOC: SCP-714 helps against certain mental threats, but it significantly weakens the user. diff --git a/Resources/Locale/ru-RU/_strings/_scp/knowledge.ftl b/Resources/Locale/ru-RU/_strings/_scp/knowledge.ftl new file mode 100644 index 00000000000..f82559b0d09 --- /dev/null +++ b/Resources/Locale/ru-RU/_strings/_scp/knowledge.ftl @@ -0,0 +1,263 @@ +scp-knowledge-known-examine-verb = Памятка +scp-knowledge-unknown-examine = OOC: Ваш персонаж не может опознать это без дополнительных знаний. +scp-knowledge-unlocked-popup = Новое знание: {$knowledge} +scp-knowledge-scp-prefixes = scp|сцп|s c p|с ц п|эс си пи +scp-knowledge-scp-colloquial-prefixes = сцпшка|сцпшки|сцпшке|сцпшку|сцпшкой +scp-knowledge-object-word-forms = объект|объекта|объекту|объектом|объекте|object|item +scp-knowledge-number-word-forms = номер|номера|номеру|номером|номере|number|no|num +scp-knowledge-number-link-word-forms = под|с|за|with|under +scp-knowledge-designation-word-forms = designation|designations|обозначение|обозначения|обозначению|обозначением|обозначении +scp-knowledge-ordinal-suffix-forms = й|го|му|м|ым +scp-knowledge-scp-full-prefix-forms = ({ scp-knowledge-scp-prefixes })[ ({ scp-knowledge-number-word-forms })]|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-scp-prefixes })|({ scp-knowledge-designation-word-forms }) ({ scp-knowledge-scp-prefixes })|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-designation-word-forms })|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-object-word-forms })[ ({ scp-knowledge-number-word-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-scp-prefixes })[ ({ scp-knowledge-number-word-forms })] + +scp-knowledge-term-tzs-name = ТЗС +scp-knowledge-term-tzs-full = Тяжелая зона содержания +scp-knowledge-term-tzs-patterns = HCZ|Heavy Containment Zone|зона тяжелого содержания|тяжелой зоне содержания|тяжелую зону содержания|тяжелой зоны содержания|тяжелая зона|тяжелой зоне|тяжелую зону|тяжелой зоны|тяжелым контейнментом|тяжелого контейнмента|тяжелому контейнменту|тяжелый контейнмент|heavy containment|тяжка|тяжке|тяжку|тяжкой|хеви зона|heavy zone|хеви|heavy +scp-knowledge-term-tzs-desc = OOC: Сокращение для тяжелой зоны содержания, сектора с наиболее опасными объектами. + +scp-knowledge-term-lzs-name = ЛЗС +scp-knowledge-term-lzs-full = Легкая зона содержания +scp-knowledge-term-lzs-patterns = LCZ|Light Containment Zone|зона легкого содержания|легкой зоне содержания|легкую зону содержания|легкой зоны содержания|легкая зона|легкой зоне|легкую зону|легкой зоны|легким контейнментом|легкого контейнмента|легкому контейнменту|легкий контейнмент|light containment|лайт зона|light zone|лайт|лайте|light +scp-knowledge-term-lzs-desc = OOC: Сокращение для легкой зоны содержания, сектора с менее опасными объектами. + +scp-knowledge-term-vaz-name = ВАЗ +scp-knowledge-term-vaz-full = Внешняя административная зона +scp-knowledge-term-vaz-patterns = EAZ|External Administrative Zone|внешней административной зоне|внешнюю административную зону|внешней административной зоны|внешняя админ зона|внешней админ зоне|внешнюю админ зону|внешней админ зоны|external admin zone|external admin sector +scp-knowledge-term-vaz-desc = OOC: Сокращение для внешней административной зоны. Обычно включает ЛЗС и административный сектор. + +scp-knowledge-term-az-name = АЗ +scp-knowledge-term-az-full = Административная зона +scp-knowledge-term-az-patterns = AZ|Administrative Zone|административной зоне|административную зону|административной зоны|административная зона комплекса|админ зона|админ зоне|админ зону|админ зоной|админзона|админки|админке|админку|админкой|административка|административки|административке|административку|административкой|admin zone|administrative sector +scp-knowledge-term-az-desc = OOC: Сокращение для административной зоны комплекса и его служебных помещений. + +scp-knowledge-term-ous-name = ОУС +scp-knowledge-term-ous-full = Особые условия содержания +scp-knowledge-term-ous-patterns = SCP Procedures|Special Containment Procedures|особых условий содержания|особым условиям содержания|особые процедуры содержания|специальные условия содержания|процедуры содержания|процедур содержания|процедурам содержания|процедурами содержания|спецпроцедуры содержания|спецпроцедур содержания|спецпроцедурам содержания|спецпроцедурами содержания +scp-knowledge-term-ous-desc = OOC: Сокращение для специальных правил, по которым Фонд удерживает конкретный SCP. + +scp-knowledge-term-nus-name = НУС +scp-knowledge-term-nus-full = Нарушение условий содержания +scp-knowledge-term-nus-patterns = CB|Containment Breach|нарушение содержания|нарушении условий содержания|нарушения условий содержания|брич|брича|бричу|бричем|бриче|контейнмент брич +scp-knowledge-term-nus-desc = OOC: Сокращение для ситуации, когда SCP вышел из-под контроля или покинул содержание. + +scp-knowledge-term-vus-name = ВУС +scp-knowledge-term-vus-full = Восстановление условий содержания +scp-knowledge-term-vus-patterns = CR|Containment Restoration|Recontainment|восстановление содержания|восстановлении условий содержания|восстановления условий содержания|реконтейн|реконтейна|реконтейну|реконтейном|реконтейне|реконтейнмент|реконтейнмента|реконтейнменту|реконтейнментом|реконтейнменте|операция реконтейна|операции реконтейна|операцию реконтейна|операцией реконтейна|операция восстановления содержания|операции восстановления содержания|recontain|recontain op +scp-knowledge-term-vus-desc = OOC: Сокращение для операций по повторной локализации и возврату SCP под контроль. + +scp-knowledge-term-ks-name = КС +scp-knowledge-term-ks-full = Камера содержания +scp-knowledge-term-ks-patterns = CC|Containment Chamber|Containment Cell|камеры содержания|камере содержания|камеру содержания|камерой содержания|камера объекта|камеры объекта|камере объекта|камеру объекта|камерой объекта +scp-knowledge-term-ks-desc = OOC: Сокращение для помещения или модуля, где удерживается SCP. + +scp-knowledge-term-ru-name = РУ +scp-knowledge-term-ru-full = Региональное управление +scp-knowledge-term-ru-patterns = RA|Regional Administration|Regional Command|регионального управления|региональному управлению|регуправление|регуправления|регуправлению|регуправлением|регуправлении +scp-knowledge-term-ru-desc = OOC: Сокращение для регионального руководства Фонда и его административного аппарата. + +scp-knowledge-term-ph-name = ПХ +scp-knowledge-term-ph-full = Повстанцы Хаоса +scp-knowledge-term-ph-patterns = CI|Chaos Insurgency|Chaos Insurgents|хаосы|хаосов|хаосам|хаосами|хаосах|бойцы хаоса|бойцов хаоса|бойцам хаоса|бойцами хаоса|отряд хаоса|отряда хаоса|отряду хаоса|отрядом хаоса|forces of chaos insurgency +scp-knowledge-term-ph-desc = OOC: Враждебная организация, связанная с диверсиями, похищением артефактов и нападениями на Фонд. + +scp-knowledge-term-gok-name = ГОК +scp-knowledge-term-gok-full = Глобальная оккультная коалиция +scp-knowledge-term-gok-patterns = GOC|Global Occult Coalition|оккультная коалиция|оккультной коалиции|оккультную коалицию|оккультной коалицией|occult coalition|global coalition +scp-knowledge-term-gok-desc = OOC: Международная структура, которая предпочитает уничтожать аномалии, а не содержать их. + +scp-knowledge-term-pii-name = ПИИ +scp-knowledge-term-pii-full = Персональный искусственный интеллект +scp-knowledge-term-pii-patterns = PAI|Personal Artificial Intelligence|Personal AI|персональный ии|персонального ии|персональному ии|персональным ии|персональном ии|паи +scp-knowledge-term-pii-desc = OOC: Сокращение для компактных ИИ-помощников и связанных с ними систем. + +scp-knowledge-term-pk-name = ПК +scp-knowledge-term-pk-full = Правовой кодекс +scp-knowledge-term-pk-patterns = LC|Legal Code|правовой кодекс|правового кодекса|правовому кодексу|правовым кодексом|правовом кодексе +scp-knowledge-term-pk-desc = OOC: Свод базовых юридических правил, которыми руководствуется персонал комплекса. + +scp-knowledge-term-ke-name = КЭ +scp-knowledge-term-ke-full = Кодекс этики +scp-knowledge-term-ke-patterns = COE|Code of Ethics|Ethics Code|этический кодекс|этического кодекса|этическому кодексу|этическим кодексом|этическом кодексе|кодекс этики|кодекса этики|кодексу этики|кодексом этики +scp-knowledge-term-ke-desc = OOC: Набор этических ограничений и норм поведения для сотрудников Фонда. + +scp-knowledge-location-tzs-name = Тяжелая зона содержания +scp-knowledge-location-tzs-patterns = ТЗС|HCZ|Heavy Containment Zone|зона тяжелого содержания|тяжелой зоне содержания|тяжелую зону содержания|тяжелой зоны содержания|тяжелая зона|тяжелой зоне|тяжелую зону|тяжелой зоны|тяжелым контейнментом|тяжелого контейнмента|тяжелому контейнменту|тяжелый контейнмент|heavy containment|тяжка|тяжке|тяжку|тяжкой|хеви зона|heavy zone|хеви|heavy +scp-knowledge-location-tzs-desc = OOC: Часть комплекса с наиболее опасными SCP, усиленной охраной и жестким режимом доступа. + +scp-knowledge-location-lzs-name = Легкая зона содержания +scp-knowledge-location-lzs-patterns = ЛЗС|LCZ|Light Containment Zone|зона легкого содержания|легкой зоне содержания|легкую зону содержания|легкой зоны содержания|легкая зона|легкой зоне|легкую зону|легкой зоны|легким контейнментом|легкого контейнмента|легкому контейнменту|легкий контейнмент|light containment|лайт зона|light zone|лайт|лайте|light +scp-knowledge-location-lzs-desc = OOC: Часть комплекса с менее опасными объектами, научной и служебной инфраструктурой. + +scp-knowledge-location-zsd-abbr = ЗСД +scp-knowledge-location-zsd-name = Зона содержания класса Д +scp-knowledge-location-zsd-patterns = CDZ|Class D Containment Zone|Class-D Containment Zone|class d zone|d-class zone|зона класса д|зоны класса д|зоне класса д|зону класса д|д блок|д блока|д блоку|д блоком|д блоке|блок д класса|блок д-класса|блок д классу|блок д классом|d block|dblock|class d block|class-d block|dclass block|дешка|дешки|дешке|дешку|дешкой|дшка|дшки|дшке|дшку|дшкой|дкласс|дкласса|дклассу|дклассом|дклассе|д класс|д класса|д классу|д классом|д классе|dclass|classd|дэблок|дэблока|дэблоку|дэблоком|дэблоке +scp-knowledge-location-zsd-desc = OOC: Жилой и режимный сектор, где размещают и контролируют персонал класса Д. + +scp-knowledge-scp035-name = SCP-035 +scp-knowledge-scp035-designation = ({ scp-knowledge-scp-full-prefix-forms }) 035|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 035|({ scp-knowledge-scp-prefixes }) 035 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 035|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 035 +scp-knowledge-scp035-object = ({ scp-knowledge-object-word-forms }) 035 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 035|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 035|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 035 +scp-knowledge-scp035-desc = OOC: Аномальная маска, захватывающая носителя и подчиняющая его своей воле. +scp-knowledge-scp035-known = OOC: Не надевайте SCP-035. Изолируйте маску и избегайте прямого контакта с зараженным носителем. + +scp-knowledge-scp049-name = SCP-049 +scp-knowledge-scp049-designation = ({ scp-knowledge-scp-full-prefix-forms }) 049|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 049|({ scp-knowledge-scp-prefixes }) 049 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 049|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 049 +scp-knowledge-scp049-object = ({ scp-knowledge-object-word-forms }) 049 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 049|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 049|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 049 +scp-knowledge-scp049-desc = OOC: Гуманоид в облике чумного доктора. Опасен прикосновением и способен превращать трупы в слуг. +scp-knowledge-scp049-known = OOC: Не подпускайте SCP-049 вплотную. Любой ближний контакт с ним смертельно опасен. + +scp-knowledge-scp082-name = SCP-082 +scp-knowledge-scp082-designation = ({ scp-knowledge-scp-full-prefix-forms }) 082|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 082|({ scp-knowledge-scp-prefixes }) 082 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 082|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 082 +scp-knowledge-scp082-object = ({ scp-knowledge-object-word-forms }) 082 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 082|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 082|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 082 +scp-knowledge-scp082-desc = OOC: Крупный и очень живучий гуманоид с высокой физической силой и опасным ближним боем. +scp-knowledge-scp082-known = OOC: Держите дистанцию, не вступайте в ближний бой и работайте по SCP-082 группой. + +scp-knowledge-scp096-name = SCP-096 +scp-knowledge-scp096-designation = ({ scp-knowledge-scp-full-prefix-forms }) 096|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 096|({ scp-knowledge-scp-prefixes }) 096 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 096|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 096 +scp-knowledge-scp096-object = ({ scp-knowledge-object-word-forms }) 096 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 096|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 096|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 096 +scp-knowledge-scp096-desc = OOC: Аномальный гуманоид, впадающий в ярость, если кто-то увидит его лицо. +scp-knowledge-scp096-known = OOC: Не смотрите SCP-096 в лицо. Разрывайте визуальный контакт и немедленно покидайте зону. + +scp-knowledge-scp106-name = SCP-106 +scp-knowledge-scp106-designation = ({ scp-knowledge-scp-full-prefix-forms }) 106|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 106|({ scp-knowledge-scp-prefixes }) 106 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 106|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 106 +scp-knowledge-scp106-object = ({ scp-knowledge-object-word-forms }) 106 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 106|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 106|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 106 +scp-knowledge-scp106-desc = OOC: Коррозионный гуманоид, проходящий сквозь преграды и утаскивающий жертв. +scp-knowledge-scp106-known = OOC: Не полагайтесь на двери и стены. SCP-106 медленный, но способен обходить обычные препятствия. + +scp-knowledge-scp131-name = SCP-131 +scp-knowledge-scp131-designation = ({ scp-knowledge-scp-full-prefix-forms }) 131|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 131|({ scp-knowledge-scp-prefixes }) 131 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 131|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 131 +scp-knowledge-scp131-object = ({ scp-knowledge-object-word-forms }) 131 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 131|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 131|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 131 +scp-knowledge-scp131-desc = OOC: Небольшая мирная глазоподобная сущность класса Safe. +scp-knowledge-scp131-known = OOC: SCP-131 обычно не проявляет враждебности и сам по себе не считается угрозой. + +scp-knowledge-scp173-name = SCP-173 +scp-knowledge-scp173-designation = ({ scp-knowledge-scp-full-prefix-forms }) 173|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 173|({ scp-knowledge-scp-prefixes }) 173 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 173|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 173 +scp-knowledge-scp173-object = ({ scp-knowledge-object-word-forms }) 173 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 173|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 173|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 173 +scp-knowledge-scp173-nickname = Костоломка +scp-knowledge-scp173-desc = OOC: Подвижная статуя, атакующая в момент, когда на нее никто не смотрит. +scp-knowledge-scp173-known = OOC: Поддерживайте прямой обзор на SCP-173 и координируйте моргание с другими людьми. + +scp-knowledge-scp247-name = SCP-247 +scp-knowledge-scp247-designation = ({ scp-knowledge-scp-full-prefix-forms }) 247|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 247|({ scp-knowledge-scp-prefixes }) 247 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 247|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 247 +scp-knowledge-scp247-object = ({ scp-knowledge-object-word-forms }) 247 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 247|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 247|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 247 +scp-knowledge-scp247-desc = OOC: Небольшой хищный SCP, напоминающий опасное кошачье существо. +scp-knowledge-scp247-known = OOC: Не недооценивайте SCP-247. Несмотря на размер, он опасен в ближнем бою. + +scp-knowledge-scp457-name = SCP-457 +scp-knowledge-scp457-designation = ({ scp-knowledge-scp-full-prefix-forms }) 457|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 457|({ scp-knowledge-scp-prefixes }) 457 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 457|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 457 +scp-knowledge-scp457-object = ({ scp-knowledge-object-word-forms }) 457 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 457|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 457|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 457 +scp-knowledge-scp457-desc = OOC: Живая огненная сущность, наносящая сильный термический урон и чувствительная к охлаждению. +scp-knowledge-scp457-known = OOC: Против SCP-457 эффективны вода и холод. Не подпускайте его к воспламеняемым объектам. + +scp-knowledge-scp939-name = SCP-939 +scp-knowledge-scp939-designation = ({ scp-knowledge-scp-full-prefix-forms }) 939|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 939|({ scp-knowledge-scp-prefixes }) 939 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 939|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 939 +scp-knowledge-scp939-object = ({ scp-knowledge-object-word-forms }) 939 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 939|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 939|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 939 +scp-knowledge-scp939-desc = OOC: Крупный хищный SCP с мощным ближним боем и хорошей работой в темноте. +scp-knowledge-scp939-known = OOC: Держитесь на расстоянии, не давайте SCP-939 навязать ближний бой и действуйте числом. + +scp-knowledge-scp999-name = SCP-999 +scp-knowledge-scp999-designation = ({ scp-knowledge-scp-full-prefix-forms }) 999|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 999|({ scp-knowledge-scp-prefixes }) 999 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 999|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 999 +scp-knowledge-scp999-object = ({ scp-knowledge-object-word-forms }) 999 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 999|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 999|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 999 +scp-knowledge-scp999-desc = OOC: Мирная слизеподобная сущность класса Safe, способная успокаивать и помогать людям. +scp-knowledge-scp999-known = OOC: SCP-999 обычно безопасен для персонала и может помочь пострадавшим, если обстановка позволяет. + +scp-knowledge-scp079-name = SCP-079 +scp-knowledge-scp079-designation = ({ scp-knowledge-scp-full-prefix-forms }) 079|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 079|({ scp-knowledge-scp-prefixes }) 079 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 079|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 079 +scp-knowledge-scp079-object = ({ scp-knowledge-object-word-forms }) 079 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 079|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 079|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 079 +scp-knowledge-scp079-desc = OOC: Аномальный искусственный интеллект, связанный с вычислительной техникой и системами комплекса. +scp-knowledge-scp079-known = OOC: SCP-079 действует через камеры, двери и связь. Ограничивайте его доступ к инфраструктуре. + +scp-knowledge-scp005-name = SCP-005 +scp-knowledge-scp005-designation = ({ scp-knowledge-scp-full-prefix-forms }) 005|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 005|({ scp-knowledge-scp-prefixes }) 005 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 005|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 005 +scp-knowledge-scp005-object = ({ scp-knowledge-object-word-forms }) 005 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 005|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 005|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 005 +scp-knowledge-scp005-desc = OOC: Аномальный ключ, открывающий почти любые замки и двери. +scp-knowledge-scp005-known = OOC: SCP-005 дает почти полный доступ к запирающим механизмам. Потеря объекта критична для безопасности. + +scp-knowledge-scp012-name = SCP-012 +scp-knowledge-scp012-designation = ({ scp-knowledge-scp-full-prefix-forms }) 012|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 012|({ scp-knowledge-scp-prefixes }) 012 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 012|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 012 +scp-knowledge-scp012-object = ({ scp-knowledge-object-word-forms }) 012 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 012|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 012|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 012 +scp-knowledge-scp012-desc = OOC: Незавершенная рукопись, вызывающая навязчивое желание дописать ее собственной кровью. +scp-knowledge-scp012-known = OOC: Не работайте с SCP-012 без защиты и контроля. Длительное наблюдение опасно. + +scp-knowledge-scp018-name = SCP-018 +scp-knowledge-scp018-designation = ({ scp-knowledge-scp-full-prefix-forms }) 018|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 018|({ scp-knowledge-scp-prefixes }) 018 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 018|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 018 +scp-knowledge-scp018-object = ({ scp-knowledge-object-word-forms }) 018 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 018|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 018|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 018 +scp-knowledge-scp018-desc = OOC: Аномальный красный мяч с крайне опасной отдачей и ускоряющимися отскоками. +scp-knowledge-scp018-known = OOC: Не бросайте SCP-018 в замкнутых помещениях без подготовки. Рикошеты быстро выходят из-под контроля. + +scp-knowledge-scp101-name = SCP-101 +scp-knowledge-scp101-designation = ({ scp-knowledge-scp-full-prefix-forms }) 101|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 101|({ scp-knowledge-scp-prefixes }) 101 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 101|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 101 +scp-knowledge-scp101-object = ({ scp-knowledge-object-word-forms }) 101 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 101|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 101|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 101 +scp-knowledge-scp101-desc = OOC: Аномальная сумка, способная перерабатывать содержимое внутри себя. +scp-knowledge-scp101-known = OOC: Не помещайте в SCP-101 живые цели и важное снаряжение без прямого приказа исследований. + +scp-knowledge-scp109-name = SCP-109 +scp-knowledge-scp109-designation = ({ scp-knowledge-scp-full-prefix-forms }) 109|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 109|({ scp-knowledge-scp-prefixes }) 109 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 109|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 109 +scp-knowledge-scp109-object = ({ scp-knowledge-object-word-forms }) 109 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 109|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 109|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 109 +scp-knowledge-scp109-desc = OOC: Аномальная фляга, постепенно восполняющая полезную жидкость. +scp-knowledge-scp109-known = OOC: SCP-109 ценен как источник воды и лечебного раствора. Контролируйте, кто к нему допускается. + +scp-knowledge-scp124-name = SCP-124 +scp-knowledge-scp124-designation = ({ scp-knowledge-scp-full-prefix-forms }) 124|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 124|({ scp-knowledge-scp-prefixes }) 124 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 124|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 124 +scp-knowledge-scp124-object = ({ scp-knowledge-object-word-forms }) 124 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 124|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 124|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 124 +scp-knowledge-scp124-desc = OOC: Аномальная почва и мешок с ней, улучшающие питание и восстановление растений. +scp-knowledge-scp124-known = OOC: SCP-124 применяют для аграрных и восстановительных задач. Это полезный, а не боевой объект. + +scp-knowledge-scp127-name = SCP-127 +scp-knowledge-scp127-designation = ({ scp-knowledge-scp-full-prefix-forms }) 127|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 127|({ scp-knowledge-scp-prefixes }) 127 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 127|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 127 +scp-knowledge-scp127-object = ({ scp-knowledge-object-word-forms }) 127 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 127|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 127|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 127 +scp-knowledge-scp127-desc = OOC: Живая аномальная винтовка, самостоятельно восстанавливающая боеприпасы. +scp-knowledge-scp127-known = OOC: SCP-127 опасен как оружие и как аномальный организм. Не разбирайте его без допуска. + +scp-knowledge-scp2022-name = SCP-2022 +scp-knowledge-scp2022-designation = ({ scp-knowledge-scp-full-prefix-forms }) 2022|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2022|({ scp-knowledge-scp-prefixes }) 2022 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 2022|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2022 +scp-knowledge-scp2022-object = ({ scp-knowledge-object-word-forms }) 2022 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 2022|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2022|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 2022 +scp-knowledge-scp2022-desc = OOC: Аномальные таблетки с выраженным психоактивным и стимулирующим эффектом. +scp-knowledge-scp2022-known = OOC: SCP-2022 применяйте только по указанию специалистов. Самовольный прием нежелателен. + +scp-knowledge-scp207-name = SCP-207 +scp-knowledge-scp207-designation = ({ scp-knowledge-scp-full-prefix-forms }) 207|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 207|({ scp-knowledge-scp-prefixes }) 207 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 207|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 207 +scp-knowledge-scp207-object = ({ scp-knowledge-object-word-forms }) 207 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 207|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 207|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 207 +scp-knowledge-scp207-desc = OOC: Аномальный напиток, резко ускоряющий пользователя и изменяющий его физиологию. +scp-knowledge-scp207-known = OOC: SCP-207 дает сильный краткосрочный эффект. Использование без контроля рискованно. + +scp-knowledge-scp2398-name = SCP-2398 +scp-knowledge-scp2398-designation = ({ scp-knowledge-scp-full-prefix-forms }) 2398|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2398|({ scp-knowledge-scp-prefixes }) 2398 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 2398|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2398 +scp-knowledge-scp2398-object = ({ scp-knowledge-object-word-forms }) 2398 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 2398|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 2398|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 2398 +scp-knowledge-scp2398-desc = OOC: Аномальная бейсбольная бита с необычным кинетическим эффектом. +scp-knowledge-scp2398-known = OOC: SCP-2398 опасен при бросках и ударах. Не применяйте его как обычный спортивный инвентарь. + +scp-knowledge-scp261-name = SCP-261 +scp-knowledge-scp261-designation = ({ scp-knowledge-scp-full-prefix-forms }) 261|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 261|({ scp-knowledge-scp-prefixes }) 261 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 261|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 261 +scp-knowledge-scp261-object = ({ scp-knowledge-object-word-forms }) 261 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 261|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 261|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 261 +scp-knowledge-scp261-desc = OOC: Аномальный автомат, выдающий случайные продукты в обмен на купюры. +scp-knowledge-scp261-known = OOC: SCP-261 непредсказуем по выдаче. Используйте его только в контролируемой обстановке. + +scp-knowledge-scp268-name = SCP-268 +scp-knowledge-scp268-designation = ({ scp-knowledge-scp-full-prefix-forms }) 268|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 268|({ scp-knowledge-scp-prefixes }) 268 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 268|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 268 +scp-knowledge-scp268-object = ({ scp-knowledge-object-word-forms }) 268 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 268|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 268|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 268 +scp-knowledge-scp268-desc = OOC: Аномальная кепка, делающая носителя труднее заметить. +scp-knowledge-scp268-known = OOC: SCP-268 полезен для скрытности, но не должен свободно попадать к нарушителям режима. + +scp-knowledge-scp330-name = SCP-330 +scp-knowledge-scp330-designation = ({ scp-knowledge-scp-full-prefix-forms }) 330|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 330|({ scp-knowledge-scp-prefixes }) 330 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 330|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 330 +scp-knowledge-scp330-object = ({ scp-knowledge-object-word-forms }) 330 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 330|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 330|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 330 +scp-knowledge-scp330-desc = OOC: Аномальная миска с конфетами. Попытка взять больше двух крайне опасна. +scp-knowledge-scp330-known = OOC: Из SCP-330 нельзя брать больше двух конфет. Это базовое правило обращения с объектом. + +scp-knowledge-scp427-name = SCP-427 +scp-knowledge-scp427-designation = ({ scp-knowledge-scp-full-prefix-forms }) 427|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 427|({ scp-knowledge-scp-prefixes }) 427 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 427|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 427 +scp-knowledge-scp427-object = ({ scp-knowledge-object-word-forms }) 427 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 427|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 427|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 427 +scp-knowledge-scp427-desc = OOC: Аномальный амулет с выраженным регенеративным воздействием на носителя. +scp-knowledge-scp427-known = OOC: SCP-427 используют с осторожностью и только под наблюдением. Самовольное ношение нежелательно. + +scp-knowledge-scp500-name = SCP-500 +scp-knowledge-scp500-designation = ({ scp-knowledge-scp-full-prefix-forms }) 500|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 500|({ scp-knowledge-scp-prefixes }) 500 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 500|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 500 +scp-knowledge-scp500-object = ({ scp-knowledge-object-word-forms }) 500 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 500|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 500|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 500 +scp-knowledge-scp500-desc = OOC: Аномальные таблетки универсального лечебного действия. +scp-knowledge-scp500-known = OOC: SCP-500 чрезвычайно ценен как аварийное средство лечения. Расход объекта должен быть строго учтен. + +scp-knowledge-scp714-name = SCP-714 +scp-knowledge-scp714-designation = ({ scp-knowledge-scp-full-prefix-forms }) 714|({ scp-knowledge-scp-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 714|({ scp-knowledge-scp-prefixes }) 714 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-scp-colloquial-prefixes })[ ({ scp-knowledge-number-word-forms })] 714|({ scp-knowledge-scp-colloquial-prefixes }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 714 +scp-knowledge-scp714-object = ({ scp-knowledge-object-word-forms }) 714 [({ scp-knowledge-ordinal-suffix-forms })]|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-word-forms }) 714|({ scp-knowledge-object-word-forms }) ({ scp-knowledge-number-link-word-forms }) ({ scp-knowledge-number-word-forms }) 714|({ scp-knowledge-number-word-forms }) ({ scp-knowledge-object-word-forms }) 714 +scp-knowledge-scp714-desc = OOC: Аномальное кольцо, защищающее от части ментальных воздействий, но сильно изматывающее носителя. +scp-knowledge-scp714-known = OOC: SCP-714 помогает против ряда ментальных угроз, но заметно ослабляет пользователя. diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index 47168121565..51223cc1864 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -8,6 +8,10 @@ # SUNRISE EDIT - type: Interactions # SUNRISE EDIT + # Fire added start - default human knowledge state + - type: ScpKnowledge + - type: ScpKnowledgeAcquisition + # Fire added end - type: Hunger - type: Icon # It will not have an icon in the adminspawn menu without this. Body parts seem fine for whatever reason. sprite: _Sunrise/Mobs/Species/Human/Basic/parts.rsi diff --git a/Resources/Prototypes/_Scp/Knowledge/knowledge.yml b/Resources/Prototypes/_Scp/Knowledge/knowledge.yml new file mode 100644 index 00000000000..5541eedb910 --- /dev/null +++ b/Resources/Prototypes/_Scp/Knowledge/knowledge.yml @@ -0,0 +1,588 @@ +- type: scpKnowledge + id: BaseScpKnowledge + abstract: true + requiredProgress: 2 + listenProgress: 1 + readProgress: 1 + examineProgress: 1 + +- type: scpKnowledge + id: BaseScpKnowledgeTerm + abstract: true + parent: BaseScpKnowledge + kind: Term + allowExamine: false + +- type: scpKnowledge + id: BaseScpKnowledgeLocation + abstract: true + parent: BaseScpKnowledgeTerm + kind: Location + +- type: scpKnowledge + id: BaseScpKnowledgeEntity + abstract: true + parent: BaseScpKnowledge + kind: Entity + hideIdentityUntilKnown: true + knownExamineVerbText: scp-knowledge-known-examine-verb + +- type: scpKnowledge + id: ScpKnowledgeTermHeavyContainmentZone + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-tzs-name + abbreviations: + - scp-knowledge-term-tzs-name + description: scp-knowledge-term-tzs-desc + recognitionPatterns: + - scp-knowledge-term-tzs-name + - scp-knowledge-term-tzs-full + - scp-knowledge-term-tzs-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermLightContainmentZone + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-lzs-name + abbreviations: + - scp-knowledge-term-lzs-name + description: scp-knowledge-term-lzs-desc + recognitionPatterns: + - scp-knowledge-term-lzs-name + - scp-knowledge-term-lzs-full + - scp-knowledge-term-lzs-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermExternalAdministrativeZone + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-vaz-name + abbreviations: + - scp-knowledge-term-vaz-name + description: scp-knowledge-term-vaz-desc + recognitionPatterns: + - scp-knowledge-term-vaz-name + - scp-knowledge-term-vaz-full + - scp-knowledge-term-vaz-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermAdministrativeZone + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-az-name + abbreviations: + - scp-knowledge-term-az-name + description: scp-knowledge-term-az-desc + recognitionPatterns: + - scp-knowledge-term-az-name + - scp-knowledge-term-az-full + - scp-knowledge-term-az-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermSpecialContainmentProcedures + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-ous-name + abbreviations: + - scp-knowledge-term-ous-name + description: scp-knowledge-term-ous-desc + recognitionPatterns: + - scp-knowledge-term-ous-name + - scp-knowledge-term-ous-full + - scp-knowledge-term-ous-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermContainmentBreach + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-nus-name + abbreviations: + - scp-knowledge-term-nus-name + description: scp-knowledge-term-nus-desc + recognitionPatterns: + - scp-knowledge-term-nus-name + - scp-knowledge-term-nus-full + - scp-knowledge-term-nus-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermContainmentRestoration + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-vus-name + abbreviations: + - scp-knowledge-term-vus-name + description: scp-knowledge-term-vus-desc + recognitionPatterns: + - scp-knowledge-term-vus-name + - scp-knowledge-term-vus-full + - scp-knowledge-term-vus-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermContainmentChamber + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-ks-name + abbreviations: + - scp-knowledge-term-ks-name + description: scp-knowledge-term-ks-desc + recognitionPatterns: + - scp-knowledge-term-ks-name + - scp-knowledge-term-ks-full + - scp-knowledge-term-ks-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermRegionalAdministration + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-ru-name + abbreviations: + - scp-knowledge-term-ru-name + description: scp-knowledge-term-ru-desc + recognitionPatterns: + - scp-knowledge-term-ru-name + - scp-knowledge-term-ru-full + - scp-knowledge-term-ru-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermChaosInsurgency + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-ph-name + abbreviations: + - scp-knowledge-term-ph-name + description: scp-knowledge-term-ph-desc + recognitionPatterns: + - scp-knowledge-term-ph-name + - scp-knowledge-term-ph-full + - scp-knowledge-term-ph-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermGlobalOccultCoalition + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-gok-name + abbreviations: + - scp-knowledge-term-gok-name + description: scp-knowledge-term-gok-desc + recognitionPatterns: + - scp-knowledge-term-gok-name + - scp-knowledge-term-gok-full + - scp-knowledge-term-gok-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermPersonalArtificialIntelligence + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-pii-name + abbreviations: + - scp-knowledge-term-pii-name + description: scp-knowledge-term-pii-desc + recognitionPatterns: + - scp-knowledge-term-pii-name + - scp-knowledge-term-pii-full + - scp-knowledge-term-pii-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermLegalCode + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-pk-name + abbreviations: + - scp-knowledge-term-pk-name + description: scp-knowledge-term-pk-desc + recognitionPatterns: + - scp-knowledge-term-pk-name + - scp-knowledge-term-pk-full + - scp-knowledge-term-pk-patterns + +- type: scpKnowledge + id: ScpKnowledgeTermCodeOfEthics + parent: BaseScpKnowledgeTerm + displayName: scp-knowledge-term-ke-name + abbreviations: + - scp-knowledge-term-ke-name + description: scp-knowledge-term-ke-desc + recognitionPatterns: + - scp-knowledge-term-ke-name + - scp-knowledge-term-ke-full + - scp-knowledge-term-ke-patterns + +- type: scpKnowledge + id: ScpKnowledgeLocationHeavyContainmentZone + parent: BaseScpKnowledgeLocation + displayName: scp-knowledge-location-tzs-name + abbreviations: + - scp-knowledge-term-tzs-name + description: scp-knowledge-location-tzs-desc + recognitionPatterns: + - scp-knowledge-term-tzs-name + - scp-knowledge-location-tzs-name + - scp-knowledge-location-tzs-patterns + +- type: scpKnowledge + id: ScpKnowledgeLocationLightContainmentZone + parent: BaseScpKnowledgeLocation + displayName: scp-knowledge-location-lzs-name + abbreviations: + - scp-knowledge-term-lzs-name + description: scp-knowledge-location-lzs-desc + recognitionPatterns: + - scp-knowledge-term-lzs-name + - scp-knowledge-location-lzs-name + - scp-knowledge-location-lzs-patterns + +- type: scpKnowledge + id: ScpKnowledgeLocationClassDContainmentZone + parent: BaseScpKnowledgeLocation + displayName: scp-knowledge-location-zsd-name + abbreviations: + - scp-knowledge-location-zsd-abbr + description: scp-knowledge-location-zsd-desc + recognitionPatterns: + - scp-knowledge-location-zsd-abbr + - scp-knowledge-location-zsd-name + - scp-knowledge-location-zsd-patterns + +- type: scpKnowledge + id: ScpKnowledgeEntityScp035 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp035-name + description: scp-knowledge-scp035-desc + recognitionPatterns: + - scp-knowledge-scp035-designation + - scp-knowledge-scp035-object + entityPrototypes: + - ClothingMaskScp035 + - MobServant035 + knownExamineText: scp-knowledge-scp035-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp049 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp049-name + description: scp-knowledge-scp049-desc + recognitionPatterns: + - scp-knowledge-scp049-designation + - scp-knowledge-scp049-object + entityPrototypes: + - Scp049 + - Scp049AlternativeCanon + - Scp049AlternativeRobotic + - Scp049AlternativeOldFashioned + knownExamineText: scp-knowledge-scp049-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp082 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp082-name + description: scp-knowledge-scp082-desc + recognitionPatterns: + - scp-knowledge-scp082-designation + - scp-knowledge-scp082-object + entityPrototypes: + - Scp082 + knownExamineText: scp-knowledge-scp082-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp096 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp096-name + description: scp-knowledge-scp096-desc + recognitionPatterns: + - scp-knowledge-scp096-designation + - scp-knowledge-scp096-object + entityPrototypes: + - Scp096 + knownExamineText: scp-knowledge-scp096-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp106 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp106-name + description: scp-knowledge-scp106-desc + recognitionPatterns: + - scp-knowledge-scp106-designation + - scp-knowledge-scp106-object + entityPrototypes: + - Scp106 + knownExamineText: scp-knowledge-scp106-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp131 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp131-name + description: scp-knowledge-scp131-desc + recognitionPatterns: + - scp-knowledge-scp131-designation + - scp-knowledge-scp131-object + entityPrototypes: + - Scp131A + - Scp131B + knownExamineText: scp-knowledge-scp131-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp173 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp173-name + description: scp-knowledge-scp173-desc + recognitionPatterns: + - scp-knowledge-scp173-designation + - scp-knowledge-scp173-object + - scp-knowledge-scp173-nickname + entityPrototypes: + - Scp173 + knownExamineText: scp-knowledge-scp173-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp247 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp247-name + description: scp-knowledge-scp247-desc + recognitionPatterns: + - scp-knowledge-scp247-designation + - scp-knowledge-scp247-object + entityPrototypes: + - Scp247 + knownExamineText: scp-knowledge-scp247-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp457 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp457-name + description: scp-knowledge-scp457-desc + recognitionPatterns: + - scp-knowledge-scp457-designation + - scp-knowledge-scp457-object + entityPrototypes: + - Scp457 + knownExamineText: scp-knowledge-scp457-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp939 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp939-name + description: scp-knowledge-scp939-desc + recognitionPatterns: + - scp-knowledge-scp939-designation + - scp-knowledge-scp939-object + entityPrototypes: + - Scp939 + knownExamineText: scp-knowledge-scp939-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp999 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp999-name + description: scp-knowledge-scp999-desc + recognitionPatterns: + - scp-knowledge-scp999-designation + - scp-knowledge-scp999-object + entityPrototypes: + - Scp999 + knownExamineText: scp-knowledge-scp999-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp079 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp079-name + description: scp-knowledge-scp079-desc + recognitionPatterns: + - scp-knowledge-scp079-designation + - scp-knowledge-scp079-object + entityPrototypes: + - Scp079Held + - PlayerScp079Empty + - Scp079EnabledFace + - Scp079Brain + - Scp079Holo + - Scp079HoloLocal + knownExamineText: scp-knowledge-scp079-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp005 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp005-name + description: scp-knowledge-scp005-desc + recognitionPatterns: + - scp-knowledge-scp005-designation + - scp-knowledge-scp005-object + entityPrototypes: + - Scp005 + knownExamineText: scp-knowledge-scp005-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp012 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp012-name + description: scp-knowledge-scp012-desc + recognitionPatterns: + - scp-knowledge-scp012-designation + - scp-knowledge-scp012-object + entityPrototypes: + - Scp012 + knownExamineText: scp-knowledge-scp012-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp018 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp018-name + description: scp-knowledge-scp018-desc + recognitionPatterns: + - scp-knowledge-scp018-designation + - scp-knowledge-scp018-object + entityPrototypes: + - Scp018 + knownExamineText: scp-knowledge-scp018-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp101 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp101-name + description: scp-knowledge-scp101-desc + recognitionPatterns: + - scp-knowledge-scp101-designation + - scp-knowledge-scp101-object + entityPrototypes: + - Scp101 + knownExamineText: scp-knowledge-scp101-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp109 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp109-name + description: scp-knowledge-scp109-desc + recognitionPatterns: + - scp-knowledge-scp109-designation + - scp-knowledge-scp109-object + entityPrototypes: + - Scp109 + knownExamineText: scp-knowledge-scp109-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp124 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp124-name + description: scp-knowledge-scp124-desc + recognitionPatterns: + - scp-knowledge-scp124-designation + - scp-knowledge-scp124-object + entityPrototypes: + - Scp124 + - Scp124Soil + knownExamineText: scp-knowledge-scp124-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp127 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp127-name + description: scp-knowledge-scp127-desc + recognitionPatterns: + - scp-knowledge-scp127-designation + - scp-knowledge-scp127-object + entityPrototypes: + - Scp127 + - Scp127Tooth + knownExamineText: scp-knowledge-scp127-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp2022 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp2022-name + description: scp-knowledge-scp2022-desc + recognitionPatterns: + - scp-knowledge-scp2022-designation + - scp-knowledge-scp2022-object + entityPrototypes: + - PillScp2022 + - PillCanisterScp2022 + knownExamineText: scp-knowledge-scp2022-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp207 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp207-name + description: scp-knowledge-scp207-desc + recognitionPatterns: + - scp-knowledge-scp207-designation + - scp-knowledge-scp207-object + entityPrototypes: + - Scp207Bottle + knownExamineText: scp-knowledge-scp207-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp2398 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp2398-name + description: scp-knowledge-scp2398-desc + recognitionPatterns: + - scp-knowledge-scp2398-designation + - scp-knowledge-scp2398-object + entityPrototypes: + - Scp2398 + knownExamineText: scp-knowledge-scp2398-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp261 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp261-name + description: scp-knowledge-scp261-desc + recognitionPatterns: + - scp-knowledge-scp261-designation + - scp-knowledge-scp261-object + entityPrototypes: + - Scp261 + knownExamineText: scp-knowledge-scp261-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp268 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp268-name + description: scp-knowledge-scp268-desc + recognitionPatterns: + - scp-knowledge-scp268-designation + - scp-knowledge-scp268-object + entityPrototypes: + - Scp268 + knownExamineText: scp-knowledge-scp268-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp330 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp330-name + description: scp-knowledge-scp330-desc + recognitionPatterns: + - scp-knowledge-scp330-designation + - scp-knowledge-scp330-object + entityPrototypes: + - Scp330Bowl + - Scp330CandyBlue + - Scp330CandyRed + - Scp330CandyGreen + - Scp330CandyYellow + - Scp330CandyPink + knownExamineText: scp-knowledge-scp330-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp427 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp427-name + description: scp-knowledge-scp427-desc + recognitionPatterns: + - scp-knowledge-scp427-designation + - scp-knowledge-scp427-object + entityPrototypes: + - Scp427 + knownExamineText: scp-knowledge-scp427-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp500 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp500-name + description: scp-knowledge-scp500-desc + recognitionPatterns: + - scp-knowledge-scp500-designation + - scp-knowledge-scp500-object + entityPrototypes: + - PillScp500 + - PillCanisterScp500 + knownExamineText: scp-knowledge-scp500-known + +- type: scpKnowledge + id: ScpKnowledgeEntityScp714 + parent: BaseScpKnowledgeEntity + displayName: scp-knowledge-scp714-name + description: scp-knowledge-scp714-desc + recognitionPatterns: + - scp-knowledge-scp714-designation + - scp-knowledge-scp714-object + entityPrototypes: + - Scp714 + knownExamineText: scp-knowledge-scp714-known diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/chief_technical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/chief_technical_specialist.yml index 71fb51b69eb..0e6ea6f3d49 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/chief_technical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/chief_technical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ChiefTechnicalSpecialist name: job-name-chief-technical-specialist description: job-description-chief-technical-specialist @@ -34,7 +34,53 @@ - EngineeringTechnicalService includeJobName: true - type: ScpRepairableCanSeeExamineHints - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: ChiefTechnicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/director.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/director.yml index 81729c3b758..f32afe6d744 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/director.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/director.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: Director name: job-name-director description: job-description-director @@ -33,7 +33,53 @@ channels: - Administration includeJobName: true - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: DirectorGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/head_of_medical_sector.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/head_of_medical_sector.yml index b113238e94c..4a35749b2a4 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/head_of_medical_sector.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/head_of_medical_sector.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: HeadOfMedicalSector name: job-name-head-of-medical-sector description: job-description-head-of-medical-sector @@ -41,7 +41,53 @@ - Exoremophobia # Врачи не пугаются трупов, у них не будет некрофобии - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: HeadOfMedicalSectorGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/personnel_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/personnel_officer.yml index e50f7e9cdc0..ed4bdae6253 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/personnel_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/personnel_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: PersonnelOfficer name: job-name-personnel-officer description: job-description-personnel-officer @@ -41,7 +41,53 @@ channels: - FoodAndSanitationService includeJobName: true - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: PersonnelOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/scientific_service_director.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/scientific_service_director.yml index 344e626f1a9..f77f1b6ee0a 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/scientific_service_director.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/scientific_service_director.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ScientificServiceDirector name: job-name-scientific-service-director description: job-description-scientific-service-director @@ -37,7 +37,42 @@ proximityBlockerLevel: None seenBlockerLevel: None - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 - type: startingGear id: ScientificServiceDirectorGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml index bcea05af290..72eb7ed3b12 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/security_commander.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: SecurityCommander name: job-name-security-commander description: job-description-security-commander @@ -72,7 +72,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: SecurityCommanderGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/senior_logistics_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/senior_logistics_officer.yml index ff6ea7590b0..2ebde503e03 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/Administration/senior_logistics_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/Administration/senior_logistics_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: SeniorLogisticsOfficer name: job-name-senior-logistics-officer description: job-description-senior-logistics-officer @@ -33,7 +33,53 @@ channels: - LogisticsService includeJobName: true - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: SeniorLogisticsOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml index 753d503f2d6..a6ed681334e 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_commandant.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ExternalAdministrativeZoneCommandant name: job-name-external-administrative-zone-commandant description: job-description-external-administrative-zone-commandant @@ -66,7 +66,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: ExternalAdministrativeZoneCommandantGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml index 10b8e31d5a5..f6124edc61b 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/external_administrative_zone_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ExternalAdministrativeZoneOfficer name: job-name-external-administrative-zone-officer description: job-description-external-administrative-zone-officer @@ -61,7 +61,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: ExternalAdministrativeZoneOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml index ad1027479c3..60bf9a53d14 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/field_medical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: FieldMedicalSpecialist name: job-name-field-medical-specialist description: job-description-field-medical-specialist @@ -35,7 +35,26 @@ phobias: - Exoremophobia # Врачи не пугаются трупов, у них не будет некрофобии - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: FieldMedicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml index bcd4dc7d15c..459a9e960da 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/junior_external_administrative_zone_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: JuniorExternalAdministrativeZoneOfficer name: job-name-junior-external-administrative-zone-officer description: job-description-junior-external-administrative-zone-officer @@ -50,7 +50,26 @@ Terror: 5 baseJitterTime: 3 fallOffChance: 0.02 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: JuniorExternalAdministrativeZoneOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml index e4353f351a1..a8e121560ba 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/CommandantSquad/senior_external_administrative_zone_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: SeniorExternalAdministrativeZoneOfficer name: job-name-senior-external-administrative-zone-officer description: job-description-senior-external-administrative-zone-officer @@ -61,7 +61,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: SeniorExternalAdministrativeZoneOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/electrical_technical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/electrical_technical_specialist.yml index 97a15483fd9..27c5f69a4cb 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/electrical_technical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/electrical_technical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ElectricalTechnicalSpecialist name: job-name-electrical-technical-specialist description: job-description-electrical-technical-specialist @@ -26,7 +26,14 @@ - type: AccessLevel level: Two - type: ScpRepairableCanSeeExamineHints - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: ElectricalTechnicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/general_technical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/general_technical_specialist.yml index 3e7451ed38a..fe7c0ea5f97 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/general_technical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/general_technical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: GeneralTechnicalSpecialist name: job-name-general-technical-specialist description: job-description-general-technical-specialist @@ -26,7 +26,14 @@ - type: AccessLevel level: Two - type: ScpRepairableCanSeeExamineHints - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: GeneralTechnicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/lead_technical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/lead_technical_specialist.yml index 0c10f7665e2..24ffa092f2e 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/lead_technical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/lead_technical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: LeadTechnicalSpecialist name: job-name-lead-technical-specialist description: job-description-lead-technical-specialist @@ -26,7 +26,14 @@ - type: AccessLevel level: Three - type: ScpRepairableCanSeeExamineHints - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: LeadTechnicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/technical_trainee.yml b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/technical_trainee.yml index 9ed16a4aa08..ad647c17ae2 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/technical_trainee.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/EngineeringTechnicalService/technical_trainee.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: TechnicalTrainee name: job-name-technical-trainee description: job-description-technical-trainee @@ -24,7 +24,14 @@ - type: AccessLevel level: Two - type: ScpRepairableCanSeeExamineHints - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: TechnicalTraineeGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/EthicsCommittee/ethics_committee_representative.yml b/Resources/Prototypes/_Scp/Roles/Jobs/EthicsCommittee/ethics_committee_representative.yml index 85386c36d71..d67ffdb9acf 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/EthicsCommittee/ethics_committee_representative.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/EthicsCommittee/ethics_committee_representative.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: EthicsCommitteeRepresentative name: job-name-ethics-committee-representative description: job-description-ethics-committee-representative @@ -23,7 +23,53 @@ class: B - type: AccessLevel level: Four - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: EthicsCommitteeRepresentativeGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/botanist_phytogeneticist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/botanist_phytogeneticist.yml index 6227b9525f1..84e01c2bfe3 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/botanist_phytogeneticist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/botanist_phytogeneticist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: BotanistPhytogeneticist name: job-name-botanist-phytogeneticist description: job-description-botanist-phytogeneticist @@ -22,7 +22,14 @@ class: C - type: AccessLevel level: One - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: BotanistPhytogeneticistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/gastronomic_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/gastronomic_specialist.yml index b87692cc02b..e7de5b4fb7f 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/gastronomic_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/gastronomic_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: GastronomicSpecialist name: job-name-gastronomic-specialist description: job-description-gastronomic-specialist @@ -19,7 +19,14 @@ class: C - type: AccessLevel level: One - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: GastronomicSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/service_personnel.yml b/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/service_personnel.yml index 27210bfc62b..ff7311f6755 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/service_personnel.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/FoodAndSanitationService/service_personnel.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ServicePersonnel name: job-name-service-personnel description: job-description-service-personnel @@ -19,7 +19,14 @@ class: C - type: AccessLevel level: One - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: ServicePersonnelGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/internal_security_operative.yml b/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/internal_security_operative.yml index 5ac1aef74c8..7df575e39e0 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/internal_security_operative.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/internal_security_operative.yml @@ -52,3 +52,23 @@ Anxiety: 2 Fear: 5 Terror: 10 + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/mental_control_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/mental_control_officer.yml index b0739d35498..0a588295dbe 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/mental_control_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/InternalSecurity/mental_control_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: MentalControlOfficer name: job-name-mental-control-officer description: job-description-mental-control-officer @@ -35,7 +35,26 @@ Anxiety: 2 Fear: 5 Terror: 10 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: MentalControlOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/judicial_inspector.yml b/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/judicial_inspector.yml index d838354bc14..06436eb66db 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/judicial_inspector.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/judicial_inspector.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: JudicialInspector name: job-name-judicial-inspector description: job-description-judicial-inspector @@ -23,7 +23,53 @@ class: A - type: AccessLevel level: Four - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics - type: startingGear id: JudicialInspectorGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/tribunal_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/tribunal_officer.yml index 7378b6b7031..2a263bb010e 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/tribunal_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/InternalTribunalService/tribunal_officer.yml @@ -21,3 +21,50 @@ class: B - type: AccessLevel level: Three + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermRegionalAdministration + - ScpKnowledgeTermChaosInsurgency + - ScpKnowledgeTermGlobalOccultCoalition + - ScpKnowledgeTermPersonalArtificialIntelligence + - ScpKnowledgeTermLegalCode + - ScpKnowledgeTermCodeOfEthics diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LogisticsService/logistics_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LogisticsService/logistics_officer.yml index 79b314fe41e..feac0e5e145 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LogisticsService/logistics_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LogisticsService/logistics_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: LogisticsOfficer name: job-name-logistics-officer description: job-description-logistics-officer @@ -27,7 +27,14 @@ class: C - type: AccessLevel level: One - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: LogisticsOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml index 00efb76ff5b..bfd53509fd1 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d.yml @@ -18,7 +18,13 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermContainmentBreach - type: startingGear id: ClassDGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml index ca349700658..4b6924db990 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_botanist.yml @@ -23,7 +23,13 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermContainmentBreach - type: startingGear id: ClassDBotanistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml index af37b4abf9e..df14e9c6534 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_cook.yml @@ -23,7 +23,13 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermContainmentBreach - type: startingGear id: ClassDCookGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml index 2eb3e3f2278..f42da529222 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/LowAccessPersonnel/class_d_janitor.yml @@ -20,7 +20,13 @@ - ExternalAdministrativeZoneSecurityService includeJobName: true requiredGameRunLevel: InRound - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeLocationClassDContainmentZone + - ScpKnowledgeTermContainmentChamber + - ScpKnowledgeTermContainmentBreach - type: startingGear id: ClassDJanitorGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/hospital_intern.yml b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/hospital_intern.yml index 94812c17cc0..deccb2be15f 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/hospital_intern.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/hospital_intern.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: HospitalIntern name: job-name-hospital-intern description: job-description-hospital-intern @@ -24,7 +24,14 @@ - type: AccessLevel level: Two # Интерн исключение из врачей, он пугается трупов, потому что слишком неопытен. - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: HospitalInternGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/medical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/medical_specialist.yml index cbbc37be84b..e0fec0ef60b 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/medical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/medical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: MedicalSpecialist name: job-name-medical-specialist description: job-description-medical-specialist @@ -30,7 +30,14 @@ - Exoremophobia # Врачи не пугаются трупов, у них не будет некрофобии - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: MedicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/pharmaceutical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/pharmaceutical_specialist.yml index 75075229f0e..7a0c8d50f73 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/pharmaceutical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/pharmaceutical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: PharmaceuticalSpecialist name: job-name-pharmaceutical-specialist description: job-description-pharmaceutical-specialist @@ -29,7 +29,14 @@ phobias: - Exoremophobia # Врачи не пугаются трупов, у них не будет некрофобии - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: PharmaceuticalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/psychotherapist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/psychotherapist.yml index 296439d8431..6e3a26a6dd5 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/psychotherapist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/psychotherapist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: Psychotherapist name: job-name-psychotherapist description: job-description-psychotherapist @@ -21,7 +21,14 @@ class: C - type: AccessLevel level: Two - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: PsychotherapistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/senior_medical_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/senior_medical_specialist.yml index 430dcdd243d..37f149b4568 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/senior_medical_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/MedicalService/senior_medical_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: SeniorMedicalSpecialist name: job-name-senior-medical-specialist description: job-description-senior-medical-specialist @@ -30,7 +30,14 @@ - Exoremophobia # Врачи не пугаются трупов, у них не будет некрофобии - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone - type: startingGear id: SeniorMedicalSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/anomarchology_researcher.yml b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/anomarchology_researcher.yml index 4cc1efc1942..a56bc2a1078 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/anomarchology_researcher.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/anomarchology_researcher.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: AnomarchologyResearcher name: job-name-anomarchology-researcher description: job-description-anomarchology-researcher @@ -29,7 +29,42 @@ proximityBlockerLevel: None seenBlockerLevel: None - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 - type: startingGear id: AnomarchologyResearcherGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/junior_researcher.yml b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/junior_researcher.yml index c05eafd8c87..b44c2c11e4e 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/junior_researcher.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/junior_researcher.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: JuniorResearcher name: job-name-junior-researcher description: job-description-junior-researcher @@ -32,7 +32,42 @@ proximityBlockerLevel: None seenBlockerLevel: None - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 - type: startingGear id: JuniorResearcherGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/lead_researcher.yml b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/lead_researcher.yml index 782b3d57c8f..0a81e6932d9 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/lead_researcher.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/lead_researcher.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: LeadResearcher name: job-name-lead-researcher description: job-description-lead-researcher @@ -29,7 +29,42 @@ proximityBlockerLevel: None seenBlockerLevel: None - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 - type: startingGear id: LeadResearcherGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/production_specialist.yml b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/production_specialist.yml index ba38f9b6e0f..3dbb846bb7c 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/production_specialist.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/ScientificService/production_specialist.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: ProductionSpecialist name: job-name-production-specialist description: job-description-production-specialist @@ -35,7 +35,42 @@ proximityBlockerLevel: None seenBlockerLevel: None - type: ShowScp096FaceDamageHud - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp131 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp247 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp999 + - ScpKnowledgeEntityScp079 + - ScpKnowledgeEntityScp005 + - ScpKnowledgeEntityScp012 + - ScpKnowledgeEntityScp018 + - ScpKnowledgeEntityScp101 + - ScpKnowledgeEntityScp109 + - ScpKnowledgeEntityScp124 + - ScpKnowledgeEntityScp127 + - ScpKnowledgeEntityScp2022 + - ScpKnowledgeEntityScp207 + - ScpKnowledgeEntityScp2398 + - ScpKnowledgeEntityScp261 + - ScpKnowledgeEntityScp268 + - ScpKnowledgeEntityScp330 + - ScpKnowledgeEntityScp427 + - ScpKnowledgeEntityScp500 + - ScpKnowledgeEntityScp714 - type: startingGear id: ProductionSpecialistGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml index ab5fe8465a1..7778ada6be1 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_commandant.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: HeavyContainmentZoneCommandant name: job-name-heavy-containment-zone-commandant description: job-description-heavy-containment-zone-commandant @@ -67,7 +67,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: HeavyContainmentZoneCommandantGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml index c6ab3659cbd..6ac3e1db100 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/heavy_containment_zone_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: HeavyContainmentZoneOfficer name: job-name-heavy-containment-zone-officer description: job-description-heavy-containment-zone-officer @@ -61,7 +61,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: HeavyContainmentZoneOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml index 99ad7d3e958..d1b482df8e7 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/junior_heavy_containment_zone_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: JuniorHeavyContainmentZoneOfficer name: job-name-junior-heavy-containment-zone-officer description: job-description-junior-heavy-containment-zone-officer @@ -50,7 +50,26 @@ Terror: 5 baseJitterTime: 3 fallOffChance: 0.02 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: JuniorHeavyContainmentZoneOfficerGear equipment: diff --git a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml index b5a6901a778..a9d2ad160c0 100644 --- a/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml +++ b/Resources/Prototypes/_Scp/Roles/Jobs/SpecialPurposeSquad/senior_heavy_containment_zone_officer.yml @@ -1,4 +1,4 @@ -- type: job +- type: job id: SeniorHeavyContainmentZoneOfficer name: job-name-senior-heavy-containment-zone-officer description: job-description-senior-heavy-containment-zone-officer @@ -61,7 +61,26 @@ Terror: 350 baseJitterTime: 2 fallOffChance: 0.01 - + - type: ScpKnowledge + knownKnowledge: + - ScpKnowledgeTermLightContainmentZone + - ScpKnowledgeLocationLightContainmentZone + - ScpKnowledgeTermHeavyContainmentZone + - ScpKnowledgeLocationHeavyContainmentZone + - ScpKnowledgeTermAdministrativeZone + - ScpKnowledgeTermExternalAdministrativeZone + - ScpKnowledgeTermSpecialContainmentProcedures + - ScpKnowledgeTermContainmentBreach + - ScpKnowledgeTermContainmentRestoration + - ScpKnowledgeEntityScp035 + - ScpKnowledgeEntityScp049 + - ScpKnowledgeEntityScp082 + - ScpKnowledgeEntityScp096 + - ScpKnowledgeEntityScp106 + - ScpKnowledgeEntityScp173 + - ScpKnowledgeEntityScp457 + - ScpKnowledgeEntityScp939 + - ScpKnowledgeEntityScp079 - type: startingGear id: SeniorHeavyContainmentZoneOfficerGear equipment: