diff --git a/Content.Client/_TSF/DamageEffects/TSFDamageEffectsSystem.cs b/Content.Client/_TSF/DamageEffects/TSFDamageEffectsSystem.cs
index c5af5771c4b..d2a8ca6fd33 100644
--- a/Content.Client/_TSF/DamageEffects/TSFDamageEffectsSystem.cs
+++ b/Content.Client/_TSF/DamageEffects/TSFDamageEffectsSystem.cs
@@ -24,6 +24,7 @@
using Robust.Shared.Audio.Components;
using Robust.Shared.Player;
using Robust.Shared.Configuration;
+using Robust.Shared.Localization;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@@ -166,6 +167,7 @@ private void OnTraumaticShockStateHandled(EntityUid uid, TraumaticShockComponent
public override void Shutdown()
{
base.Shutdown();
+ CleanupLocalPlayerDamageEffects();
if (_overlay != null)
{
_overlayManager.RemoveOverlay(_overlay);
@@ -175,6 +177,7 @@ public override void Shutdown()
private void OnLocalPlayerAttached(LocalPlayerAttachedEvent ev)
{
+ CleanupLocalPlayerDamageEffects();
_overlayManager.AddOverlay(_overlay!);
UpdateOverlayIntensity(ev.Entity);
var result = _audio.PlayGlobal(DamageMusic, Filter.Local(), true);
@@ -198,10 +201,33 @@ private void OnLocalPlayerAttached(LocalPlayerAttachedEvent ev)
private void OnLocalPlayerDetached(LocalPlayerDetachedEvent ev)
{
- _overlayManager.RemoveOverlay(_overlay!);
- _damageMusicStream = _audio.Stop(_damageMusicStream);
- _painSoundStream = _audio.Stop(_painSoundStream);
- _tinnitusStream = _audio.Stop(_tinnitusStream);
+ CleanupLocalPlayerDamageEffects();
+ }
+
+ private void CleanupLocalPlayerDamageEffects()
+ {
+ if (_overlay != null)
+ _overlayManager.RemoveOverlay(_overlay);
+ if (_damageMusicStream is {} dm)
+ {
+ _damageMusicStream = null;
+ if (Exists(dm))
+ QueueDel(dm);
+ }
+
+ if (_painSoundStream is {} ps)
+ {
+ _painSoundStream = null;
+ if (Exists(ps))
+ QueueDel(ps);
+ }
+
+ if (_tinnitusStream is {} ts)
+ {
+ _tinnitusStream = null;
+ if (Exists(ts))
+ QueueDel(ts);
+ }
if (_overlay != null)
{
_overlay.DamageStrength = 0f;
@@ -222,6 +248,24 @@ private void OnLocalPlayerDetached(LocalPlayerDetachedEvent ev)
TSFStatusMessageState.Message = null;
}
+ private bool HasLiveLocalControlledEntity()
+ {
+ return _playerManager.LocalEntity is { } le && Exists(le);
+ }
+
+ private void TryCleanupOrphanedLocalPlayerEffects()
+ {
+ if (HasLiveLocalControlledEntity())
+ return;
+ CleanupLocalPlayerDamageEffects();
+ }
+
+ public override void Update(float frameTime)
+ {
+ TryCleanupOrphanedLocalPlayerEffects();
+ base.Update(frameTime);
+ }
+
private bool ShouldTriggerTinnitus(EntityUid uid, DamageableComponent damageable)
{
var piercingDelta = FixedPoint2.Zero;
@@ -273,11 +317,14 @@ private void OnThresholdChecked(ref MobThresholdChecked ev)
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
- var local = _playerManager.LocalEntity;
- if (local == null || _overlay == null)
+ TryCleanupOrphanedLocalPlayerEffects();
+ if (!HasLiveLocalControlledEntity())
+ return;
+
+ if (_overlay == null)
return;
- var uid = local.Value;
+ var uid = _playerManager.LocalEntity!.Value;
if (TryComp(uid, out DamageableComponent? damageable))
{
var totalDamage = _damageable.GetTotalDamage((uid, damageable));
@@ -349,13 +396,13 @@ public override void FrameUpdate(float frameTime)
}
_disorientationBurstTime = Math.Max(0f, _disorientationBurstTime - frameTime);
- if (local != null && _disorientationBurstTime > 0f && TryComp(local, out MobStateComponent? mobState) && mobState.CurrentState == MobState.Alive)
+ if (_disorientationBurstTime > 0f && TryComp(uid, out MobStateComponent? mobState) && mobState.CurrentState == MobState.Alive)
{
var strength = _disorientationBurstTime / DisorientationBurstDuration;
var kick = new Vector2(
(_random.NextFloat() - 0.5f) * 2f * DisorientationKickMagnitude * strength,
(_random.NextFloat() - 0.5f) * 2f * DisorientationKickMagnitude * strength);
- _cameraRecoil.KickCamera(local.Value, kick);
+ _cameraRecoil.KickCamera(uid, kick);
}
}
@@ -558,7 +605,18 @@ private void TryUpdateStatusMessage(EntityUid uid)
var bucket = (int)(now / StatusMessageBucketSeconds);
var seed = bucket * 13 + phraseList.Length;
var idx = Math.Abs(seed % phraseList.Length);
- TSFStatusMessageState.Message = phraseList[idx];
- TSFStatusMessageState.DisplayUntil = now + StatusMessageDisplayDuration;
+ var phraseId = phraseList[idx];
+ var resolved = Loc.GetString(phraseId);
+ var revealSeconds = CountRunes(resolved) * TSFStatusMessageState.RevealSecondsPerRune;
+ TSFStatusMessageState.Message = phraseId;
+ TSFStatusMessageState.DisplayUntil = now + StatusMessageDisplayDuration + revealSeconds;
+ }
+
+ private static int CountRunes(string s)
+ {
+ var n = 0;
+ foreach (var _ in s.EnumerateRunes())
+ n++;
+ return n;
}
}
diff --git a/Content.Client/_TSF/DamageEffects/TSFStatusMessageState.cs b/Content.Client/_TSF/DamageEffects/TSFStatusMessageState.cs
index 4131b8e93de..d23c0e85b12 100644
--- a/Content.Client/_TSF/DamageEffects/TSFStatusMessageState.cs
+++ b/Content.Client/_TSF/DamageEffects/TSFStatusMessageState.cs
@@ -6,5 +6,9 @@ namespace Content.Client._TSF.DamageEffects;
public static class TSFStatusMessageState
{
public static string? Message { get; set; }
+
public static double DisplayUntil { get; set; }
+
+ /// Wall-clock delay between revealing each Unicode extended grapheme (rune).
+ public const float RevealSecondsPerRune = 0.042f;
}
diff --git a/Content.Client/_TSF/DamageEffects/TSFStatusMessageUIController.cs b/Content.Client/_TSF/DamageEffects/TSFStatusMessageUIController.cs
index 6bc8366e611..8024e564b63 100644
--- a/Content.Client/_TSF/DamageEffects/TSFStatusMessageUIController.cs
+++ b/Content.Client/_TSF/DamageEffects/TSFStatusMessageUIController.cs
@@ -8,6 +8,7 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
@@ -25,6 +26,17 @@ public sealed class TSFStatusMessageUIController : UIController
private const float ShakeAmount = 3f;
private const int FontSize = 22;
private const int BottomMargin = 200;
+ private static readonly Color StatusTint = new(255, 45, 45, 255);
+ private const float HideFadeSeconds = 0.42f;
+
+ private string? _revealLocId;
+ private string? _revealFullText;
+ private int _revealTotalRunes;
+ private int _revealVisibleRunes;
+ private float _revealCarrySeconds;
+
+ private float _hideFadeRemaining;
+ private string? _hideFadeText;
public override void Initialize()
{
@@ -101,6 +113,10 @@ private void OnScreenUnload()
_panel = null;
_labelContainer = null;
_label = null;
+ _revealLocId = null;
+ _revealFullText = null;
+ _hideFadeRemaining = 0f;
+ _hideFadeText = null;
}
}
@@ -110,9 +126,31 @@ public override void FrameUpdate(FrameEventArgs args)
if (_label == null || _panel == null || _labelContainer == null)
return;
var now = _timing.RealTime.TotalSeconds;
- if (TSFStatusMessageState.Message != null)
+ var msg = TSFStatusMessageState.Message;
+ if (msg != null)
{
- _label.Text = Loc.GetString(TSFStatusMessageState.Message);
+ _hideFadeRemaining = 0f;
+ _hideFadeText = null;
+
+ if (msg != _revealLocId)
+ {
+ _revealLocId = msg;
+ _revealFullText = Loc.GetString(msg);
+ _revealTotalRunes = CountRunes(_revealFullText);
+ _revealVisibleRunes = 0;
+ _revealCarrySeconds = 0f;
+ }
+
+ _revealCarrySeconds += args.DeltaSeconds;
+ var step = TSFStatusMessageState.RevealSecondsPerRune;
+ while (_revealVisibleRunes < _revealTotalRunes && _revealCarrySeconds >= step)
+ {
+ _revealCarrySeconds -= step;
+ _revealVisibleRunes++;
+ }
+
+ _label.Text = TakeFirstRunes(_revealFullText!, _revealVisibleRunes);
+ _label.Modulate = StatusTint;
_panel.Visible = true;
var shakeX = (MathF.Sin((float)(now * 95)) + MathF.Sin((float)(now * 160) * 0.7f)) * ShakeAmount;
var shakeY = (MathF.Sin((float)(now * 110) + 1.3f) + MathF.Sin((float)(now * 180) * 0.6f)) * ShakeAmount;
@@ -120,8 +158,81 @@ public override void FrameUpdate(FrameEventArgs args)
}
else
{
- _panel.Visible = false;
- LayoutContainer.SetPosition(_label, Vector2.Zero);
+ if (_hideFadeRemaining <= 0f)
+ {
+ if (!string.IsNullOrEmpty(_revealFullText))
+ {
+ _hideFadeText = _revealFullText;
+ _hideFadeRemaining = HideFadeSeconds;
+ }
+ else if (_panel.Visible && !string.IsNullOrEmpty(_label.Text))
+ {
+ _hideFadeText = _label.Text;
+ _hideFadeRemaining = HideFadeSeconds;
+ }
+ }
+
+ _revealLocId = null;
+ _revealFullText = null;
+ _revealTotalRunes = 0;
+ _revealVisibleRunes = 0;
+ _revealCarrySeconds = 0f;
+
+ if (_hideFadeRemaining > 0f)
+ {
+ _hideFadeRemaining -= args.DeltaSeconds;
+ var alphaNorm = Math.Clamp(Math.Max(0f, _hideFadeRemaining) / HideFadeSeconds, 0f, 1f);
+ _label.Text = _hideFadeText ?? string.Empty;
+ _label.Modulate = StatusTint.WithAlpha(alphaNorm);
+ _panel.Visible = true;
+ var shakeScale = alphaNorm;
+ var shakeX = (MathF.Sin((float)(now * 95)) + MathF.Sin((float)(now * 160) * 0.7f)) * ShakeAmount * shakeScale;
+ var shakeY = (MathF.Sin((float)(now * 110) + 1.3f) + MathF.Sin((float)(now * 180) * 0.6f)) * ShakeAmount * shakeScale;
+ LayoutContainer.SetPosition(_label, new Vector2(shakeX, shakeY));
+
+ if (_hideFadeRemaining <= 0f)
+ {
+ _hideFadeText = null;
+ _label.Text = string.Empty;
+ _label.Modulate = StatusTint;
+ _panel.Visible = false;
+ LayoutContainer.SetPosition(_label, Vector2.Zero);
+ }
+ }
+ else
+ {
+ _hideFadeText = null;
+ _label.Text = string.Empty;
+ _label.Modulate = StatusTint;
+ _panel.Visible = false;
+ LayoutContainer.SetPosition(_label, Vector2.Zero);
+ }
+ }
+ }
+
+ private static int CountRunes(string s)
+ {
+ var n = 0;
+ foreach (var _ in s.EnumerateRunes())
+ n++;
+ return n;
+ }
+
+ private static string TakeFirstRunes(string s, int runeCount)
+ {
+ if (runeCount <= 0)
+ return string.Empty;
+
+ var seen = 0;
+ var endUtf16 = 0;
+ foreach (var r in s.EnumerateRunes())
+ {
+ if (seen >= runeCount)
+ break;
+ endUtf16 += r.Utf16SequenceLength;
+ seen++;
}
+
+ return s[..endUtf16];
}
}
diff --git a/Content.Server/_TSF/AutonomousLobby/VoteManager.TSF.LobbyAutovote.cs b/Content.Server/_TSF/AutonomousLobby/VoteManager.TSF.LobbyAutovote.cs
index dcf9e63946a..2b58a06f8d5 100644
--- a/Content.Server/_TSF/AutonomousLobby/VoteManager.TSF.LobbyAutovote.cs
+++ b/Content.Server/_TSF/AutonomousLobby/VoteManager.TSF.LobbyAutovote.cs
@@ -20,10 +20,7 @@ public sealed partial class VoteManager
private static readonly string[] TSFLobbyVotePresetIds =
{
"Extended",
- "Nukeops",
- "Secret",
- "Zombie",
- "Revolutionary",
+ "Secret"
};
private Dictionary TSFGetLobbyVotePresets()
diff --git a/Content.Server/_TSF/Health/TSFPneumothoraxDamageSystem.cs b/Content.Server/_TSF/Health/TSFPneumothoraxDamageSystem.cs
index f8dcd1e6783..81d6fe56abe 100644
--- a/Content.Server/_TSF/Health/TSFPneumothoraxDamageSystem.cs
+++ b/Content.Server/_TSF/Health/TSFPneumothoraxDamageSystem.cs
@@ -33,7 +33,6 @@ public override void Update(float frameTime)
continue;
pn.NextDamage = _timing.CurTime + Interval;
- Dirty(uid, pn);
var spec = new DamageSpecifier();
spec.DamageDict["Asphyxiation"] = AsphyxPerTick;
diff --git a/LICENSE-AGPLv3.txt b/LICENSE-AGPLv3.txt
index c6ca1820041..be3f7b28e56 100644
--- a/LICENSE-AGPLv3.txt
+++ b/LICENSE-AGPLv3.txt
@@ -1,21 +1,3 @@
-Copyright (C) 2024-2026 The Space Frontier and TSF contributors
-
-Исходный код на C#, прототипы YAML, шейдеры и прочие
-человекочитаемые материалы в перечисленных ниже путях этого репозитория
-распространяются на условиях GNU Affero General Public License v3.0
-(полный текст лицензии следует ниже).
-
- Content.Shared/_TSF/
- Content.Server/_TSF/
- Content.Client/_TSF/
- Resources/Prototypes/_TSF/
- Resources/Textures/_TSF/
-
-Остальные материалы репозитория остаются на условиях, указанных в LICENSE.TXT,
-в RobustToolbox/legal.md, а также в метаданных отдельных файлов и ассетов.
-
-------------------------------------------------------------------------
-
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
diff --git a/README.md b/README.md
index 553a2d9cffb..c5103c9975f 100644
--- a/README.md
+++ b/README.md
@@ -1,40 +1,104 @@
-