Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Content.Shared.Mobs;

namespace Content.Server.Imperial.SCP.SCP008.Components;

[RegisterComponent]
public sealed partial class SCP008InfectionAuraComponent : Component
{
[DataField("radius")]
public float Radius = 8f;

[DataField("zombifyDelay")]
public TimeSpan ZombifyDelay = TimeSpan.FromSeconds(12);

[DataField("updateInterval")]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);

[DataField]
public LookupFlags LookupFlags = LookupFlags.Dynamic;

[DataField("allowCritical")]
public bool AllowCritical = false;

[DataField("warningDelay")]
public TimeSpan WarningDelay = TimeSpan.FromSeconds(3);

[DataField("warningPopup")]
public string WarningPopup = "SCP-008: покиньте зону немедленно, иначе вы превратитесь в зомби!";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Захардкоженная строка вместо локализации.

WarningPopup содержит русский текст напрямую. В SS14 принято использовать систему локализации (Loc.GetString) с ключом вместо встроенных строк, чтобы поддерживать возможность перевода и централизованное управление текстами. Аналогичная проблема имеется и в SCP096RageOnLookComponent (строки 37, 40, 43).

🛠️ Предлагаемое исправление
-    [DataField("warningPopup")]
-    public string WarningPopup = "SCP-008: покиньте зону немедленно, иначе вы превратитесь в зомби!";
+    [DataField("warningPopup")]
+    public LocId WarningPopup = "scp008-infection-aura-warning";

Соответственно, в системе использовать Loc.GetString(comp.WarningPopup) при вызове _popup.PopupEntity(...).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Content.Server/Imperial/SCP/SCP008/Components/SCP008InfectionAuraComponent.cs`
around lines 26 - 27, Replace hardcoded Russian text in WarningPopup on
SCP008InfectionAuraComponent with a localization key string (e.g.,
"scp008-warning-popup") and change call sites that pass comp.WarningPopup into
popup text to wrap it with Loc.GetString(comp.WarningPopup) when calling
_popup.PopupEntity(...); do the same for the analogous hardcoded strings in
SCP096RageOnLookComponent (the fields around the string constants and their
_popup.PopupEntity usages) so all popup messages use localization keys and
Loc.GetString at display time.


[ViewVariables]
public TimeSpan NextUpdate;

[ViewVariables]
public TimeSpan LastUpdate;

[ViewVariables]
public Dictionary<EntityUid, TimeSpan> ExposureTime = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using Content.Server.Imperial.SCP.SCP008.Components;
using Content.Server.Zombies;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.Zombies;
using Robust.Shared.Timing;

namespace Content.Server.Imperial.SCP.SCP008.Systems;

public sealed class SCP008InfectionAuraSystem : EntitySystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly IGameTiming _timing = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<SCP008InfectionAuraComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SCP008InfectionAuraComponent, ComponentShutdown>(OnShutdown);
}

private void OnMapInit(Entity<SCP008InfectionAuraComponent> ent, ref MapInitEvent args)
{
ent.Comp.LastUpdate = _timing.CurTime;
ent.Comp.NextUpdate = _timing.CurTime + ent.Comp.UpdateInterval;
ent.Comp.ExposureTime.Clear();
}

private void OnShutdown(Entity<SCP008InfectionAuraComponent> ent, ref ComponentShutdown args)
{
ent.Comp.ExposureTime.Clear();
}

public override void Update(float frameTime)
{
base.Update(frameTime);

var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<SCP008InfectionAuraComponent>();

while (query.MoveNext(out var uid, out var comp))
{
if (comp.NextUpdate > curTime)
continue;

var elapsed = curTime - comp.LastUpdate;
comp.LastUpdate = curTime;
comp.NextUpdate = curTime + comp.UpdateInterval;

if (elapsed <= TimeSpan.Zero)
continue;

var currentlyInRange = new HashSet<EntityUid>();
var nearbyEntities = _lookup.GetEntitiesInRange(uid, comp.Radius, comp.LookupFlags);

foreach (var target in nearbyEntities)
{
if (target == uid)
continue;

if (!TryComp<MobStateComponent>(target, out var mobState))
continue;

var validState = mobState.CurrentState == MobState.Alive ||
(comp.AllowCritical && mobState.CurrentState == MobState.Critical);

if (!validState)
continue;

if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
continue;

currentlyInRange.Add(target);

var totalExposure = elapsed;
var previousExposure = TimeSpan.Zero;
if (comp.ExposureTime.TryGetValue(target, out var existingExposure))
{
previousExposure = existingExposure;
totalExposure += previousExposure;
}

var warningStart = comp.ZombifyDelay - comp.WarningDelay;
if (warningStart < TimeSpan.Zero)
warningStart = TimeSpan.Zero;

if (previousExposure < warningStart && totalExposure >= warningStart && totalExposure < comp.ZombifyDelay)
_popup.PopupEntity(comp.WarningPopup, target, target, PopupType.MediumCaution);

if (totalExposure >= comp.ZombifyDelay)
{
_zombie.ZombifyEntity(target, mobState);
comp.ExposureTime.Remove(target);
continue;
}

comp.ExposureTime[target] = totalExposure;
}

var toRemove = new List<EntityUid>();
foreach (var (tracked, _) in comp.ExposureTime)
{
if (!currentlyInRange.Contains(tracked))
toRemove.Add(tracked);
}

foreach (var tracked in toRemove)
{
comp.ExposureTime.Remove(tracked);
}
Comment on lines +106 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Лишняя аллокация List для удаления.

List<EntityUid> создаётся каждый тик для сбора удаляемых ключей. Можно использовать переиспользуемый список (см. предыдущий комментарий) или итерировать по снимку ключей напрямую.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/Imperial/SCP/SCP008/Systems/SCP008InfectionAuraSystem.cs`
around lines 104 - 114, The code allocates a new List<EntityUid> named toRemove
each tick when pruning comp.ExposureTime; replace this with either a reusable
field-level list or iterate over a snapshot of comp.ExposureTime.Keys (or use
comp.ExposureTime.Keys.ToArray()) to avoid per-tick allocation. Update the
pruning logic that currently uses toRemove and the loops that reference
tracked/currentlyInRange/comp.ExposureTime so they use the reusable list or key
snapshot instead, preserving the same removal semantics.

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace Content.Server.Imperial.SCP.SCP096.Components;

using System.Collections.Generic;
using Robust.Shared.Audio;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Директивы using расположены после объявления namespace — нарушение стандартного соглашения C#.

В стандартном стиле C# (и в большинстве файлов репозитория) директивы using располагаются до файлового пространства имён.

♻️ Предлагаемое исправление
+using System.Collections.Generic;
+using Robust.Shared.Audio;
+
 namespace Content.Server.Imperial.SCP.SCP096.Components;
-
-using System.Collections.Generic;
-using Robust.Shared.Audio;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
namespace Content.Server.Imperial.SCP.SCP096.Components;
using System.Collections.Generic;
using Robust.Shared.Audio;
using System.Collections.Generic;
using Robust.Shared.Audio;
namespace Content.Server.Imperial.SCP.SCP096.Components;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/Imperial/SCP/SCP096/Components/SCP096RageOnLookComponent.cs`
around lines 1 - 4, The using directives (System.Collections.Generic and
Robust.Shared.Audio) are placed after the namespace declaration in
SCP096RageOnLookComponent (namespace
Content.Server.Imperial.SCP.SCP096.Components); move those using lines so they
appear above the namespace declaration to follow C# conventions and match the
repository style.


[RegisterComponent]
public sealed partial class SCP096RageOnLookComponent : Component
{
[DataField("observeRadius")]
public float ObserveRadius = 9f;

[DataField("minLookDot")]
public float MinLookDot = 0.42f;

[DataField("requireUnobstructed")]
public bool RequireUnobstructed = true;

[DataField("enragedWalkModifier")]
public float EnragedWalkModifier = 1.7f;

[DataField("enragedSprintModifier")]
public float EnragedSprintModifier = 1.7f;

[DataField("enragedAttackRate")]
public float EnragedAttackRate = 1.7f;

[DataField("calmAttackRate")]
public float CalmAttackRate = 1.0f;

[DataField("rageWindup")]
public TimeSpan RageWindup = TimeSpan.FromSeconds(10);

[DataField("rageDuration")]
public TimeSpan RageDuration = TimeSpan.FromSeconds(120);

[DataField("rageWindupPopup")]
public string RageWindupPopup = "SCP-096 начинает входить в ярость!";

[DataField("ragePopup")]
public string RagePopup = "SCP-096 входит в ярость!";

[DataField("rageCalmPopup")]
public string RageCalmPopup = "SCP-096 успокаивается.";

[DataField("rageSound")]
public SoundSpecifier RageSound = new SoundPathSpecifier("/Audio/Voice/Human/malescream_1.ogg");

[DataField("cryingSound")]
public SoundSpecifier CryingSound = new SoundPathSpecifier("/Audio/Voice/Human/cry_male_1.ogg");

[DataField("rageLoopSound")]
public SoundSpecifier RageLoopSound = new SoundPathSpecifier("/Audio/Ambience/Objects/anomaly_generator_ambi.ogg");
Comment on lines +50 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

RageLoopSound по умолчанию использует звук генератора аномалий — вероятно, заглушка.

/Audio/Ambience/Objects/anomaly_generator_ambi.ogg — это эмбиент генератора аномалий, не связанный с SCP-096. Если это временный звук-заглушка, стоит заменить его на соответствующий аудиоресурс SCP-096 или оставить комментарий о намеренном использовании.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Server/Imperial/SCP/SCP096/Components/SCP096RageOnLookComponent.cs`
around lines 51 - 52, The default RageLoopSound in SCP096RageOnLookComponent is
set to an unrelated anomaly generator ambient file; update the SoundSpecifier
for the RageLoopSound field (public SoundSpecifier RageLoopSound) to point to
the correct SCP-096 loop audio asset, or if this is intentionally a placeholder,
replace the hardcoded SoundPathSpecifier with a neutral null/default value and
add an inline comment on the RageLoopSound field indicating it is a temporary
stub awaiting the proper SCP-096 audio resource so the intent is clear to future
reviewers.


[ViewVariables]
public bool IsEnraged;

[ViewVariables]
public bool IsRageWindup;

[ViewVariables]
public TimeSpan RageWindupEndTime;

[ViewVariables]
public TimeSpan RageEndTime;

[ViewVariables]
public bool UsingRageLoopSound;

[ViewVariables]
public HashSet<EntityUid> RageTargets = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Content.Server.Imperial.SCP.SCP096.Components;

[RegisterComponent]
public sealed partial class SCP096RageSuppressorComponent : Component
{
}
Loading
Loading