diff --git a/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs new file mode 100644 index 00000000000..cbeec20a720 --- /dev/null +++ b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs @@ -0,0 +1,260 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using Content.Shared.Ashfall.Animations; +using Content.Shared.Mobs; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; + +using Content.Shared.Mobs.Components; + +namespace Content.Client.Ashfall.Animations; + +public sealed partial class EmoteAnimationSystem : SharedEmoteAnimationSystem +{ + [Dependency] private AnimationPlayerSystem _animationPlayer = default!; + + private const string EmoteAnimKey = "AshfallEmoteAnimation"; + private readonly Dictionary _savedTransforms = new(); + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnAnimationCompleted); + } + + private bool TryStartAnimation(EntityUid uid, [NotNullWhen(true)] out SpriteComponent? sprite) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey) || !TryComp(uid, out sprite)) + { + sprite = null; + return false; + } + + _savedTransforms[uid] = (sprite.Offset, sprite.Rotation); + return true; + } + + private void StopRunningAnimation(EntityUid uid) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + { + _animationPlayer.Stop(uid, EmoteAnimKey); + if (_savedTransforms.Remove(uid, out var saved) && TryComp(uid, out var sprite)) + { + sprite.Offset = saved.Offset; + sprite.Rotation = saved.Rotation; + } + } + else + { + _savedTransforms.Remove(uid); + } + } + + private void OnAnimationCompleted(EntityUid uid, EmoteAnimationComponent comp, AnimationCompletedEvent args) + { + if (args.Key != EmoteAnimKey) + return; + + if (_savedTransforms.Remove(uid, out var saved) && TryComp(uid, out var sprite)) + { + sprite.Offset = saved.Offset; + sprite.Rotation = saved.Rotation; + } + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Alive) + { + StopRunningAnimation(ent.Owner); + PlayEmoteTail(ent.Owner, false); + } + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + StopRunningAnimation(ent.Owner); + PlayEmoteTail(ent.Owner, false); + } + + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (ent.Comp.CurAnimationIndex == ent.Comp.LastClientAnimationIndex) + return; + + ent.Comp.LastClientAnimationIndex = ent.Comp.CurAnimationIndex; + + if (TryComp(ent.Owner, out var mobState) && mobState.CurrentState != MobState.Alive) + return; + + switch (ent.Comp.AnimationId) + { + case AnimationFlip: + PlayEmoteFlip(ent.Owner); + break; + case AnimationJump: + PlayEmoteJump(ent.Owner); + break; + case AnimationTurn: + case AnimationSpin: + PlayEmoteTurn(ent.Owner); + break; + case AnimationTremble: + case AnimationShiver: + PlayEmoteTremble(ent.Owner); + break; + case AnimationTailWag: + PlayEmoteTail(ent.Owner, true); + break; + case AnimationTailStop: + PlayEmoteTail(ent.Owner, false); + break; + } + } + + public void PlayEmoteFlip(EntityUid uid) + { + if (!TryStartAnimation(uid, out var sprite)) + return; + + var baseAngle = sprite.Rotation; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(500), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees), 0f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 180), 0.25f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 360), 0.5f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteJump(EntityUid uid) + { + if (!TryStartAnimation(uid, out var sprite)) + return; + + var baseOffset = sprite.Offset; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(250), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Cubic, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(baseOffset, 0f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0, 0.65f), 0.125f), + new AnimationTrackProperty.KeyFrame(baseOffset, 0.25f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteTurn(EntityUid uid) + { + if (!TryStartAnimation(uid, out var sprite)) + return; + + var baseAngle = sprite.Rotation; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(600), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees), 0f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 180), 0.3f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 360), 0.6f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteTremble(EntityUid uid) + { + if (!TryStartAnimation(uid, out var sprite)) + return; + + var baseOffset = sprite.Offset; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(400), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(baseOffset, 0f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(-0.06f, 0), 0.05f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0.06f, 0), 0.10f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(-0.05f, 0), 0.15f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0.05f, 0), 0.20f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(-0.03f, 0), 0.25f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0.03f, 0), 0.30f), + new AnimationTrackProperty.KeyFrame(baseOffset, 0.40f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteTail(EntityUid uid, bool start) + { + if (!TryComp(uid, out var sprite)) + return; + + foreach (var layer in sprite.AllLayers) + { + if (layer.RsiState.Name != null && layer.RsiState.Name.Contains("tail", StringComparison.OrdinalIgnoreCase)) + { + layer.AutoAnimated = start; + if (!start) + layer.AnimationTime = 0; + } + } + } +} diff --git a/Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs b/Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs new file mode 100644 index 00000000000..d2a55c43425 --- /dev/null +++ b/Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs @@ -0,0 +1,1158 @@ +using System.Globalization; +using System.Numerics; +using System.Text; +using Content.Client.Chat.UI; +using Content.Client.Resources; +using Content.Client.Stylesheets.Fonts; +using Content.Shared.Ashfall.Chat; +using Content.Shared.CCVar; +using Content.Shared.Chat; +using Content.Shared.Ghost.Components; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Shared.Configuration; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client.Ashfall.Chat; + +public sealed partial class RunechatSpeechBubble : SpeechBubble +{ + private const string SayStyle = AshfallRunechatStyles.Say; + private const string WhisperStyle = AshfallRunechatStyles.Whisper; + private const string RadioStyle = AshfallRunechatStyles.Radio; + private const string EmoteStyle = AshfallRunechatStyles.Emote; + private const string LoocStyle = AshfallRunechatStyles.Looc; + + private const int LongestText = 80; + private const int ContinueTextLength = LongestText - 5; + private const float SplitChunkSeconds = 4f; + private const float SplitFinalSeconds = 6f; + private const float MinimumRunechatScale = 0.5f; + private const float MaximumRunechatScale = 2.5f; + private const float DefaultLangchatWidth = 140f; + private const float SplitLangchatWidth = 240f; + + private static readonly Color DefaultColor = Color.White; + private static readonly Color ObserverColor = Color.FromHex("#c51fb7"); + private static readonly Color LoocColor = Color.FromHex("#48d1cc"); + private static readonly Color PainColor = Color.FromHex("#c83232"); + private static readonly Color RadioColor = Color.FromHex("#73d48f"); + + /// + /// A run of text that shares the same bold/italic formatting and optional color override. + /// + private readonly record struct TextRun(string Text, bool Bold, bool Italic, Color? ColorOverride = null); + + public RunechatSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity) + : base( + message, + senderEntity, + GetStyleClass(type, message), + GetTextColor(type, message, senderEntity), + GetLifetime(GetRunPages(GetRuns(message, GetStyleClass(type, message))))) + { + RectClipContent = false; + VerticalOffsetAchieved = 0f; + SetHeight = ContentSize.Y; + } + + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) + { + var runs = GetRuns(message, speechStyleClass); + var (style, forceBold) = GetVisualStyle(message, speechStyleClass, runs); + var pages = GetRunPages(runs); + + if (forceBold) + { + for (int i = 0; i < pages.Count; i++) + pages[i] = ForceBold(pages[i]); + } + + Texture? languageIcon = null; + if (speechStyleClass is SayStyle or WhisperStyle) + { + TryGetLanguageIcon(message, out languageIcon); + } + + return new RunechatTextControl(pages, fontColor ?? DefaultColor, style, languageIcon); + } + + private static string GetStyleClass(SpeechType type, ChatMessage message) + { + return type switch + { + SpeechType.Emote => EmoteStyle, + SpeechType.Say => SayStyle, + SpeechType.Whisper => WhisperStyle, + SpeechType.Looc => LoocStyle, + _ => SayStyle, + }; + } + + private static Color GetTextColor(SpeechType type, ChatMessage message, EntityUid senderEntity) + { + if (message.MessageColorOverride is { } color) + return color; + + if (AshfallRunechatStyles.IsInterrupting(message.SpeechStyleClass)) + return PainColor; + + if (type == SpeechType.Looc) + return LoocColor; + + if (message.Channel == ChatChannel.Radio) + return RadioColor; + + var entityManager = IoCManager.Resolve(); + if (entityManager.HasComponent(senderEntity)) + return ObserverColor; + + return DefaultColor; + } + + private static (RunechatVisualStyle Style, bool ForceBold) GetVisualStyle( + ChatMessage message, + string speechStyleClass, + List runs) + { + if (message.SpeechStyleClass == AshfallRunechatStyles.Scream) + return (RunechatVisualStyle.Scream, true); + + if (message.SpeechStyleClass == AshfallRunechatStyles.Pain) + return (RunechatVisualStyle.Pain, true); + + if (speechStyleClass == EmoteStyle) + { + return IsYellEmote(RunsToPlainText(runs)) + ? (RunechatVisualStyle.EmoteYell, true) + : (RunechatVisualStyle.Emote, false); + } + + if (message.SpeechStyleClass == "megaphoneSpeech") + return (RunechatVisualStyle.Announce, true); + + if (message.SpeechStyleClass == "commanderSpeech") + return (RunechatVisualStyle.Bolded, true); + + if (speechStyleClass == SayStyle && IsWhollyBold(runs)) + return (RunechatVisualStyle.Bolded, false); + + var baseStyle = speechStyleClass switch + { + RadioStyle => RunechatVisualStyle.Radio, + WhisperStyle => RunechatVisualStyle.Whisper, + _ => RunechatVisualStyle.Normal, + }; + + return (baseStyle, false); + } + + private static bool IsYellEmote(string text) + { + return text.Contains("scream", StringComparison.OrdinalIgnoreCase) + || text.Contains("крик", StringComparison.OrdinalIgnoreCase) + || text.Contains("pain", StringComparison.OrdinalIgnoreCase) + || text.Contains("боль", StringComparison.OrdinalIgnoreCase) + || text.Contains("medic", StringComparison.OrdinalIgnoreCase) + || text.Contains("медик", StringComparison.OrdinalIgnoreCase); + } + + private static TimeSpan GetLifetime(IReadOnlyList> pages) + { + if (pages.Count <= 1) + { + var length = pages.Count == 0 ? 0 : RunsLength(pages[0]); + return TimeSpan.FromSeconds(length / (float)LongestText * SplitChunkSeconds + 2f); + } + + return TimeSpan.FromSeconds((pages.Count - 1) * SplitChunkSeconds + SplitFinalSeconds); + } + + private static List GetRuns(ChatMessage message, string speechStyleClass) + { + var raw = speechStyleClass switch + { + EmoteStyle => message.Message, + SayStyle => GetBubbleContent(message), + WhisperStyle => GetBubbleContent(message), + RadioStyle => FormatRadioText(message), + LoocStyle => $"LOOC: {message.Message}", + _ => message.WrappedMessage, + }; + + if (string.IsNullOrWhiteSpace(raw)) + raw = message.Message; + + var runs = ParseFormattingRuns(raw); + return NormalizeRuns(runs); + } + + private static string GetBubbleContent(ChatMessage message) + { + return SharedChatSystem.GetStringInsideTag(message, "BubbleContent"); + } + + private static string FormatRadioText(ChatMessage message) + { + return message.Message; + } + + private static List ParseFormattingRuns(string markupText) + { + var runs = new List(); + var boldDepth = 0; + var italicDepth = 0; + var colorStack = new Stack(); + var i = 0; + var sb = new StringBuilder(); + + Color? CurrentColor() => colorStack.Count > 0 ? colorStack.Peek() : (Color?)null; + + void Flush() + { + if (sb.Length > 0) + { + runs.Add(new TextRun(sb.ToString(), boldDepth > 0, italicDepth > 0, CurrentColor())); + sb.Clear(); + } + } + + while (i < markupText.Length) + { + var c = markupText[i]; + + if (c == '[') + { + var close = markupText.IndexOf(']', i); + if (close < 0) + { + sb.Append(c); + i++; + continue; + } + + var tag = markupText.Substring(i + 1, close - i - 1); + var lowerTag = tag.ToLowerInvariant(); + + switch (lowerTag) + { + case "bold": + Flush(); + boldDepth++; + i = close + 1; + continue; + case "/bold": + Flush(); + boldDepth = Math.Max(0, boldDepth - 1); + i = close + 1; + continue; + case "italic": + Flush(); + italicDepth++; + i = close + 1; + continue; + case "/italic": + Flush(); + italicDepth = Math.Max(0, italicDepth - 1); + i = close + 1; + continue; + case "bolditalic": + Flush(); + boldDepth++; + italicDepth++; + i = close + 1; + continue; + case "/bolditalic": + Flush(); + boldDepth = Math.Max(0, boldDepth - 1); + italicDepth = Math.Max(0, italicDepth - 1); + i = close + 1; + continue; + case "/color": + Flush(); + if (colorStack.Count > 0) + colorStack.Pop(); + i = close + 1; + continue; + default: + var equalsIndex = tag.IndexOf('='); + var tagName = (equalsIndex >= 0 ? tag[..equalsIndex] : tag).Trim().ToLowerInvariant(); + + if (tagName == "color") + { + Flush(); + var value = equalsIndex >= 0 ? tag[(equalsIndex + 1)..].Trim() : null; + if (!string.IsNullOrEmpty(value) && TryParseColor(value, out var parsedColor)) + colorStack.Push(parsedColor); + else + colorStack.Push(colorStack.Count > 0 ? colorStack.Peek() : DefaultColor); + + i = close + 1; + continue; + } + + if (tagName is "font" or "/font") + { + i = close + 1; + continue; + } + + sb.Append(markupText, i, close - i + 1); + i = close + 1; + continue; + } + } + + sb.Append(c); + i++; + } + + Flush(); + return runs; + } + + private static bool TryParseColor(string value, out Color color) + { + value = value.Trim('"', '\''); + + if (Color.TryFromHex(value, out var hexColor)) + { + color = hexColor; + return true; + } + + if (Color.TryFromName(value, out var namedColor)) + { + color = namedColor; + return true; + } + + color = default; + return false; + } + + private static List NormalizeRuns(List runs) + { + var result = new List(); + var previousWasWhitespace = true; + + foreach (var run in runs) + { + var sb = new StringBuilder(); + foreach (var ch in run.Text) + { + if (char.IsWhiteSpace(ch)) + { + if (!previousWasWhitespace) + sb.Append(' '); + + previousWasWhitespace = true; + } + else + { + sb.Append(ch); + previousWasWhitespace = false; + } + } + + if (sb.Length > 0) + result.Add(run with { Text = sb.ToString() }); + } + + return TrimRunsEnd(result); + } + + private static int RunsLength(IReadOnlyList runs) + { + var length = 0; + foreach (var run in runs) + length += run.Text.Length; + return length; + } + + private static string RunsToPlainText(IReadOnlyList runs) + { + var sb = new StringBuilder(); + foreach (var run in runs) + sb.Append(run.Text); + return sb.ToString(); + } + + private static bool IsWhollyBold(IReadOnlyList runs) + { + var hasContent = false; + foreach (var run in runs) + { + if (run.Text.Trim().Length == 0) + continue; + + hasContent = true; + if (!run.Bold) + return false; + } + + return hasContent; + } + + private static List ForceBold(IReadOnlyList runs) + { + var result = new List(runs.Count); + foreach (var run in runs) + result.Add(run with { Bold = true }); + return result; + } + + private static char GetCharAt(IReadOnlyList runs, int index) + { + var remaining = index; + foreach (var run in runs) + { + if (remaining < run.Text.Length) + return run.Text[remaining]; + + remaining -= run.Text.Length; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + + private static int FindLastSpaceIndex(IReadOnlyList runs, int windowStart, int windowEnd) + { + var index = windowEnd - 1; + while (index >= windowStart) + { + if (GetCharAt(runs, index) == ' ') + return index; + + index--; + } + + return -1; + } + + private static (List Left, List Right) SplitRuns(IReadOnlyList runs, int index) + { + var left = new List(); + var right = new List(); + var remainingIndex = index; + + foreach (var run in runs) + { + if (remainingIndex <= 0) + { + right.Add(run); + continue; + } + + if (remainingIndex >= run.Text.Length) + { + left.Add(run); + remainingIndex -= run.Text.Length; + continue; + } + + left.Add(run with { Text = run.Text[..remainingIndex] }); + right.Add(run with { Text = run.Text[remainingIndex..] }); + remainingIndex = 0; + } + + return (left, right); + } + + private static List TrimRunsEnd(List runs) + { + var result = new List(runs); + while (result.Count > 0) + { + var last = result[^1]; + var trimmed = last.Text.TrimEnd(' '); + + if (trimmed.Length == last.Text.Length) + break; + + if (trimmed.Length == 0) + { + result.RemoveAt(result.Count - 1); + continue; + } + + result[^1] = last with { Text = trimmed }; + break; + } + + return result; + } + + private static List TrimRunsStart(List runs) + { + var result = new List(runs); + while (result.Count > 0) + { + var first = result[0]; + var trimmed = first.Text.TrimStart(' '); + + if (trimmed.Length == first.Text.Length) + break; + + if (trimmed.Length == 0) + { + result.RemoveAt(0); + continue; + } + + result[0] = first with { Text = trimmed }; + break; + } + + return result; + } + + private static List PrependPlainRun(List runs, string text) + { + var result = new List { new(text, false, false) }; + result.AddRange(runs); + return result; + } + + private static List AppendPlainRun(List runs, string text) + { + var result = new List(runs) { new(text, false, false) }; + return result; + } + + private static List> GetRunPages(List runs) + { + var pages = new List>(); + var length = RunsLength(runs); + + if (length <= LongestText) + { + pages.Add(runs); + return pages; + } + + var remaining = runs; + while (RunsLength(remaining) > LongestText) + { + var split = GetRunSplitIndex(remaining); + var (left, right) = SplitRuns(remaining, split); + pages.Add(AppendPlainRun(TrimRunsEnd(left), "...")); + remaining = PrependPlainRun(TrimRunsStart(right), "..."); + } + + pages.Add(remaining); + return pages; + } + + private static int GetRunSplitIndex(IReadOnlyList runs) + { + var length = RunsLength(runs); + var max = Math.Min(ContinueTextLength, length); + var split = FindLastSpaceIndex(runs, 0, max); + + return split >= LongestText / 2 + ? split + : max; + } + + private static int ScaleFontSize(int fontSize, float scale) + { + return Math.Max(7, (int)MathF.Round(fontSize * scale)); + } + + private readonly record struct RunechatVisualStyle( + int FontSize, + bool PrefixEmoteIcon, + float MaxWidth, + float LineHeightOffset = 0f, + bool UsePanicShake = false) + { + public static readonly RunechatVisualStyle Normal = new(11, false, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Whisper = new(9, false, DefaultLangchatWidth, -1f); + public static readonly RunechatVisualStyle Radio = new(10, false, SplitLangchatWidth); + public static readonly RunechatVisualStyle Emote = new(10, true, DefaultLangchatWidth, -1f); + public static readonly RunechatVisualStyle EmoteYell = new(12, true, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Bolded = new(12, false, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Announce = new(14, false, SplitLangchatWidth); + public static readonly RunechatVisualStyle Pain = new(11, false, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Scream = new(12, false, DefaultLangchatWidth, UsePanicShake: true); + + public int GetScaledFontSize(float scale) + { + return ScaleFontSize(FontSize, scale); + } + + public float GetScaledMaxWidth(float scale) + { + return MaxWidth * scale; + } + + public float GetScaledLineHeightOffset(float scale) + { + return LineHeightOffset * scale; + } + } + + private sealed partial class RunechatTextControl : Control + { + private const float TextStrokeAlpha = 0.9f; + private const float DefaultEmoteIconPixelSize = 1.4f; + private const float PanicShakeDuration = 0.85f; + private const float PanicShakeFrequency = 18f; + private const float PanicShakeSize = 5f; + private const float EmoteIconVisibleLeft = 3f; + private const float EmoteIconVisibleRight = 8f; + private const float EmoteIconVisibleTop = 3f; + private const float EmoteIconVisibleBottom = 8f; + private const float EmoteIconAlpha = 200f / 255f; + + private static readonly Color EmoteIconBlue = Color.FromHex("#3399ff"); + + private static readonly string[] EmoteIcon = + { + ".........", + ".........", + ".........", + "...B#B#B.", + "...##B##.", + "...BBBBB.", + "...##B##.", + "...B#B#B.", + ".........", + }; + + [Dependency] private IConfigurationManager _configManager = default!; + [Dependency] private IResourceCache _resourceCache = default!; + + private readonly IReadOnlyList> _pages; + private readonly Color _color; + private readonly RunechatVisualStyle _style; + private readonly Texture? _languageIcon; + private readonly float _scale; + private readonly Font _regularFont; + private readonly Font _boldFont; + private readonly Font _italicFont; + private readonly Font _boldItalicFont; + + private readonly List _layouts = new(); + private Vector2 _cachedSize; + private bool _layoutDirty = true; + private int _currentPage; + private float _pageTime; + private float _animationTime; + + public RunechatTextControl(IReadOnlyList> pages, Color color, RunechatVisualStyle style, Texture? languageIcon = null) + { + IoCManager.InjectDependencies(this); + + MouseFilter = MouseFilterMode.Ignore; + _pages = pages; + _color = color; + _style = style; + _languageIcon = languageIcon; + _scale = Math.Clamp(_configManager.GetCVar(CCVars.ChatRunechatBubbleScale), MinimumRunechatScale, MaximumRunechatScale); + + var fontSize = style.GetScaledFontSize(_scale); + var stack = new NotoFontFamilyStack(_resourceCache); + _regularFont = stack.GetFont(fontSize, FontKind.Regular); + _boldFont = stack.GetFont(fontSize, FontKind.Bold); + _italicFont = stack.GetFont(fontSize, FontKind.Italic); + _boldItalicFont = stack.GetFont(fontSize, FontKind.BoldItalic); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + _animationTime += args.DeltaSeconds; + + if (_pages.Count <= 1 || _currentPage >= _pages.Count - 1) + return; + + _pageTime += args.DeltaSeconds; + while (_pageTime >= SplitChunkSeconds && _currentPage < _pages.Count - 1) + { + _pageTime -= SplitChunkSeconds; + _currentPage++; + } + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + EnsureLayout(); + return _cachedSize; + } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + if (_pages.Count == 0) + return; + + EnsureLayout(); + + var layout = _layouts[Math.Min(_currentPage, _layouts.Count - 1)]; + var textOpacity = _configManager.GetCVar(CCVars.SpeechBubbleTextOpacity); + var textColor = _color.WithAlpha(_color.A * textOpacity); + var lineHeight = GetLineHeight(); + var y = (PixelSize.Y - layout.Height) / 2f; + var shakeOffset = GetPanicShakeOffset(); + + for (var i = 0; i < layout.Lines.Count; i++) + { + var line = layout.Lines[i]; + var visibleBounds = GetVisibleBounds(line); + Texture? languageIcon = i == 0 ? _languageIcon : null; + var languageIconWidth = languageIcon != null ? GetLanguageIconSize() : 0f; + var iconWidth = _style.PrefixEmoteIcon && i == 0 ? GetVisibleIconWidth() : 0f; + var iconGap = iconWidth + languageIconWidth > 0f ? GetIconGap() : 0f; + + var contentWidth = iconWidth + languageIconWidth + iconGap + visibleBounds.Width; + var x = (PixelSize.X - contentWidth) / 2f + iconWidth + languageIconWidth + iconGap - visibleBounds.Left + shakeOffset; + var position = new Vector2(x, y); + + if (languageIcon != null) + { + var iconY = position.Y + + visibleBounds.Top + + visibleBounds.Height / 2f - + languageIconWidth / 2f; + + handle.DrawTextureRect( + languageIcon, + UIBox2.FromDimensions(position.X - iconGap - languageIconWidth, iconY, languageIconWidth, languageIconWidth), + Color.White.WithAlpha(textOpacity)); + } + + if (_style.PrefixEmoteIcon && i == 0) + { + var iconY = position.Y + + visibleBounds.Top + + visibleBounds.Height / 2f - + GetVisibleIconHeight() / 2f - + GetVisibleIconTop(); + + DrawEmoteIcon( + handle, + new Vector2(position.X - iconGap - iconWidth - GetVisibleIconLeft(), iconY), + textColor); + } + + DrawOutlinedLine(handle, position, line, textColor, textOpacity); + y += lineHeight; + } + } + + protected override void UIScaleChanged() + { + _layoutDirty = true; + base.UIScaleChanged(); + } + + private void EnsureLayout() + { + if (!_layoutDirty) + return; + + _layouts.Clear(); + + var width = 0f; + var height = 0f; + foreach (var page in _pages) + { + var layout = LayoutPage(page); + _layouts.Add(layout); + width = MathF.Max(width, layout.Width); + height = MathF.Max(height, layout.Height); + } + + var horizontalPadding = (GetPadding() + GetHorizontalSafetyPadding()) * UIScale; + var verticalPadding = GetPadding() * UIScale; + _cachedSize = new Vector2( + (width + horizontalPadding * 2f) / UIScale, + (height + verticalPadding * 2f) / UIScale); + _layoutDirty = false; + } + + private RunechatPageLayout LayoutPage(List pageRuns) + { + var lines = new List>(); + var lineWidths = new List(); + + WrapRunPage(pageRuns, lines, lineWidths); + + if (lines.Count == 0) + { + lines.Add(new List()); + lineWidths.Add(0f); + } + + var width = 0f; + foreach (var lineWidth in lineWidths) + { + width = MathF.Max(width, lineWidth); + } + + var height = GetLineHeight() * lines.Count; + return new RunechatPageLayout(lines, lineWidths, width, height); + } + + private void WrapRunPage(List pageRuns, List> lines, List lineWidths) + { + var words = SplitIntoWords(pageRuns); + var currentLine = new List(); + var currentWidth = 0f; + var spaceWidth = MeasureRunWidth(new TextRun(" ", false, false)); + + foreach (var word in words) + { + var wordWidth = MeasureRunsWidth(word); + var hasContent = currentLine.Count > 0; + var neededWidth = hasContent ? currentWidth + spaceWidth + wordWidth : wordWidth; + + if (neededWidth <= GetMaxWidth()) + { + if (hasContent) + currentLine.Add(new TextRun(" ", false, false)); + + currentLine.AddRange(word); + currentWidth = neededWidth; + continue; + } + + if (hasContent) + { + lines.Add(currentLine); + lineWidths.Add(currentWidth); + currentLine = new List(); + currentWidth = 0f; + } + + if (wordWidth <= GetMaxWidth()) + { + currentLine.AddRange(word); + currentWidth = wordWidth; + } + else + { + var brokenLines = BreakWordAcrossLines(word); + for (var i = 0; i < brokenLines.Count - 1; i++) + { + lines.Add(brokenLines[i]); + lineWidths.Add(MeasureRunsWidth(brokenLines[i])); + } + + currentLine = brokenLines[^1]; + currentWidth = MeasureRunsWidth(currentLine); + } + } + + if (currentLine.Count > 0) + { + lines.Add(currentLine); + lineWidths.Add(currentWidth); + } + } + + private static List> SplitIntoWords(IReadOnlyList runs) + { + var words = new List>(); + var current = new List(); + + void FlushWord() + { + if (current.Count > 0) + { + words.Add(current); + current = new List(); + } + } + + foreach (var run in runs) + { + var text = run.Text; + var start = 0; + + for (var i = 0; i < text.Length; i++) + { + if (text[i] != ' ') + continue; + + if (i > start) + current.Add(run with { Text = text[start..i] }); + + FlushWord(); + start = i + 1; + } + + if (start < text.Length) + current.Add(run with { Text = text[start..] }); + } + + FlushWord(); + return words; + } + + private List> BreakWordAcrossLines(List word) + { + var lines = new List>(); + var current = new List(); + var currentWidth = 0f; + + foreach (var piece in word) + { + var pieceStartWidth = currentWidth; + var builder = new StringBuilder(); + + foreach (var rune in piece.Text.EnumerateRunes()) + { + var candidateText = builder.ToString() + rune; + var candidateWidth = MeasureRunWidth(new TextRun(candidateText, piece.Bold, piece.Italic, piece.ColorOverride)); + + if (builder.Length > 0 && pieceStartWidth + candidateWidth > GetMaxWidth()) + { + current.Add(new TextRun(builder.ToString(), piece.Bold, piece.Italic, piece.ColorOverride)); + lines.Add(current); + + current = new List(); + builder.Clear(); + builder.Append(rune); + pieceStartWidth = 0f; + currentWidth = MeasureRunWidth(new TextRun(builder.ToString(), piece.Bold, piece.Italic, piece.ColorOverride)); + continue; + } + + builder.Append(rune); + currentWidth = pieceStartWidth + candidateWidth; + } + + if (builder.Length > 0) + current.Add(new TextRun(builder.ToString(), piece.Bold, piece.Italic, piece.ColorOverride)); + } + + lines.Add(current); + return lines; + } + + private float GetMaxWidth() + { + return _style.GetScaledMaxWidth(_scale) * UIScale; + } + + private Font GetFont(bool bold, bool italic) + { + if (bold && italic) + return _boldItalicFont; + if (bold) + return _boldFont; + if (italic) + return _italicFont; + return _regularFont; + } + + private float MeasureRunWidth(TextRun run) + { + var width = 0f; + var font = GetFont(run.Bold, run.Italic); + + foreach (var rune in run.Text.EnumerateRunes()) + { + var metrics = font.GetCharMetrics(rune, UIScale); + if (metrics == null) + continue; + + width += metrics.Value.Advance; + } + + return width; + } + + private float MeasureRunsWidth(IReadOnlyList runs) + { + var width = 0f; + foreach (var run in runs) + width += MeasureRunWidth(run); + return width; + } + + private (float Left, float Width, float Top, float Height) GetVisibleBounds(List lineRuns) + { + var cursor = 0f; + var left = 0f; + var right = 0f; + var top = 0f; + var bottom = 0f; + var foundGlyph = false; + + foreach (var run in lineRuns) + { + var font = GetFont(run.Bold, run.Italic); + var ascent = font.GetAscent(UIScale); + + foreach (var rune in run.Text.EnumerateRunes()) + { + var metrics = font.GetCharMetrics(rune, UIScale); + if (metrics == null) + continue; + + var glyphLeft = cursor + metrics.Value.BearingX; + var glyphRight = glyphLeft + metrics.Value.Width; + var glyphTop = ascent - metrics.Value.BearingY; + var glyphBottom = glyphTop + metrics.Value.Height; + + if (!foundGlyph) + { + left = glyphLeft; + right = glyphRight; + top = glyphTop; + bottom = glyphBottom; + foundGlyph = true; + } + else + { + left = MathF.Min(left, glyphLeft); + right = MathF.Max(right, glyphRight); + top = MathF.Min(top, glyphTop); + bottom = MathF.Max(bottom, glyphBottom); + } + + cursor += metrics.Value.Advance; + } + } + + return foundGlyph + ? (left, right - left, top, bottom - top) + : (0f, 0f, 0f, 0f); + } + + private float GetLineHeight() + { + return MathF.Max(1f, _regularFont.GetLineHeight(UIScale) + _style.GetScaledLineHeightOffset(_scale) * UIScale); + } + + private float GetPadding() + { + return _scale * 4f; + } + + private float GetHorizontalSafetyPadding() + { + return _scale * 16f; + } + + private float GetIconPixelSize() + { + return DefaultEmoteIconPixelSize * _scale; + } + + private float GetPanicShakeOffset() + { + if (!_style.UsePanicShake || _animationTime >= PanicShakeDuration) + return 0f; + + var amount = PanicShakeSize * _scale * UIScale; + return MathF.Sin(_animationTime * MathF.PI * PanicShakeFrequency) * amount; + } + + private float GetIconGap() + { + return _scale * 2f * UIScale; + } + + private float GetLanguageIconSize() + => 14f * _scale * UIScale; + + private float GetVisibleIconLeft() + { + return EmoteIconVisibleLeft * GetIconPixelSize() * UIScale; + } + + private float GetVisibleIconWidth() + { + return (EmoteIconVisibleRight - EmoteIconVisibleLeft) * GetIconPixelSize() * UIScale; + } + + private float GetVisibleIconTop() + { + return EmoteIconVisibleTop * GetIconPixelSize() * UIScale; + } + + private float GetVisibleIconHeight() + { + return (EmoteIconVisibleBottom - EmoteIconVisibleTop) * GetIconPixelSize() * UIScale; + } + + private void DrawOutlinedLine( + DrawingHandleScreen handle, + Vector2 position, + List lineRuns, + Color textColor, + float textOpacity) + { + var outline = new TextOutline(1.25f, Color.Black.WithAlpha(textColor.A * TextStrokeAlpha)); + var cursor = position; + + foreach (var run in lineRuns) + { + var font = GetFont(run.Bold, run.Italic); + var drawColor = run.ColorOverride is { } runColor + ? runColor.WithAlpha(runColor.A * textOpacity) + : textColor; + + handle.DrawString(font, cursor, run.Text, UIScale, drawColor, outline); + cursor += new Vector2(MeasureRunWidth(run), 0f); + } + } + + private void DrawEmoteIcon( + DrawingHandleScreen handle, + Vector2 position, + Color textColor) + { + var scale = MathF.Max(1f, GetIconPixelSize() * UIScale); + var iconOrigin = position; + + var iconAlpha = textColor.A * EmoteIconAlpha; + + for (var y = 0; y < EmoteIcon.Length; y++) + { + for (var x = 0; x < EmoteIcon[y].Length; x++) + { + var color = EmoteIcon[y][x] switch + { + '#' => Color.Black.WithAlpha(iconAlpha), + 'B' => EmoteIconBlue.WithAlpha(iconAlpha), + _ => (Color?)null, + }; + + if (color is { } iconColor) + DrawIconPixel(handle, iconOrigin, x, y, scale, iconColor); + } + } + } + + private static void DrawIconPixel( + DrawingHandleScreen handle, + Vector2 iconOrigin, + int x, + int y, + float scale, + Color color) + { + var position = iconOrigin + new Vector2(x * scale, y * scale); + var size = new Vector2(scale, scale); + handle.DrawRect(UIBox2.FromDimensions(position, size), color); + } + + private sealed record RunechatPageLayout( + IReadOnlyList> Lines, + IReadOnlyList LineWidths, + float Width, + float Height); + } +} diff --git a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs new file mode 100644 index 00000000000..76954597b85 --- /dev/null +++ b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs @@ -0,0 +1,104 @@ +using System.Numerics; +using Content.Shared.Ashfall.Overlays.Sandevistan; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Overlays.Sandevistan; + +public sealed partial class SandevistanVisionOverlay : Overlay +{ + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IPlayerManager _playerManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IGameTiming _timing = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private static readonly ProtoId ShaderProto = "AshfallSandevistanVision"; + private readonly ShaderInstance _shader; + private readonly TransformSystem _transformSystem; + + private readonly List<(Vector2 Position, Angle Rotation, TimeSpan Time)> _afterImages = new(); + private Vector2 _lastPosition = Vector2.Zero; + + public SandevistanVisionOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = 20; + _shader = _prototypeManager.Index(ShaderProto).InstanceUnique(); + _transformSystem = _entityManager.System(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return false; + + if (_playerManager.LocalEntity is not { Valid: true } player + || !_entityManager.TryGetComponent(player, out var comp) + || !comp.Enabled) + { + _afterImages.Clear(); + return false; + } + + if (!_entityManager.TryGetComponent(player, out EyeComponent? eyeComp) || args.Viewport.Eye != eyeComp.Eye) + return false; + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null || args.Viewport.Eye == null) + return; + + var player = _playerManager.LocalEntity; + if (player == null || !_entityManager.TryGetComponent(player.Value, out var playerSprite)) + return; + + if (!_entityManager.TryGetComponent(player.Value, out var playerXform)) + return; + + var worldHandle = args.WorldHandle; + var viewport = args.WorldBounds; + var eye = args.Viewport.Eye; + + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_shader); + worldHandle.DrawRect(viewport, Color.White); + worldHandle.UseShader(null); + + var playerPos = _transformSystem.GetWorldPosition(playerXform); + var playerRot = _transformSystem.GetWorldRotation(playerXform); + var curTime = _timing.CurTime; + + if (Vector2.Distance(playerPos, _lastPosition) > 0.35f) + { + _lastPosition = playerPos; + _afterImages.Add((playerPos, playerRot, curTime)); + } + + _afterImages.RemoveAll(x => (curTime - x.Time).TotalSeconds > 0.30f); + + // Render after-image motion trail silhouettes + foreach (var ghost in _afterImages) + { + var age = (float)(curTime - ghost.Time).TotalSeconds; + var alpha = Math.Clamp(1f - age / 0.30f, 0f, 0.5f); + var originalColor = playerSprite.Color; + playerSprite.Color = new Color(0.2f, 0.9f, 0.85f, alpha * 0.40f); + playerSprite.Render(worldHandle, eye.Rotation, ghost.Rotation, null, ghost.Position); + playerSprite.Color = originalColor; + } + + // Render player cleanly on top + playerSprite.Render(worldHandle, eye.Rotation, playerRot, null, playerPos); + worldHandle.SetTransform(Matrix3x2.Identity); + } +} diff --git a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs new file mode 100644 index 00000000000..acc03e52655 --- /dev/null +++ b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared.Ashfall.Overlays.Sandevistan; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.Ashfall.Overlays.Sandevistan; + +public sealed partial class SandevistanVisionSystem : EntitySystem +{ + [Dependency] private IOverlayManager _overlayMan = default!; + [Dependency] private IPlayerManager _playerMan = default!; + + private SandevistanVisionOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _overlay = new SandevistanVisionOverlay(); + + SubscribeLocalEvent(OnComponentStartup); + SubscribeLocalEvent(OnComponentShutdown); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + if (_playerMan.LocalEntity is { Valid: true } player + && HasComp(player) + && !_overlayMan.HasOverlay()) + { + _overlayMan.AddOverlay(_overlay); + } + } + + public override void Shutdown() + { + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); + base.Shutdown(); + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + if (ent.Owner == _playerMan.LocalEntity) + { + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); + _overlayMan.AddOverlay(_overlay); + } + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Owner == _playerMan.LocalEntity && _overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); + _overlayMan.AddOverlay(_overlay); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); + } +} diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs new file mode 100644 index 00000000000..c5c3e8b3b34 --- /dev/null +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs @@ -0,0 +1,139 @@ +using System.Numerics; +using Content.Shared.Ashfall.Overlays.ShockWave; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Overlays.ShockWave; + +public sealed partial class ShockWaveOverlay : Overlay +{ + [Dependency] private IEntityManager _entMan = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IGameTiming _timing = default!; + + private SharedTransformSystem? _xformSystem; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + private readonly ShaderInstance _shader; + private static readonly ProtoId ShaderProto = "ScreechShockWave"; + + private readonly List<(EntityUid Entity, InnerShaderInstance Instance)> _cached = new(); + private int _currentCount; + private const int MaximumInstances = 10; + + private readonly Vector2[] _positions; + private readonly float[] _waveStrengths; + private readonly float[] _waveSpeeds; + private readonly float[] _downScales; + private readonly float[] _fades; + private readonly float[] _times; + + public ShockWaveOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = 15; + _shader = _prototypeManager.Index(ShaderProto).InstanceUnique(); + _positions = new Vector2[MaximumInstances]; + _waveStrengths = new float[MaximumInstances]; + _waveSpeeds = new float[MaximumInstances]; + _downScales = new float[MaximumInstances]; + _fades = new float[MaximumInstances]; + _times = new float[MaximumInstances]; + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (args.Viewport.Eye == null) + return false; + + if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem)) + return false; + + _currentCount = 0; + + _cached.RemoveAll(entry => (float)(_timing.RealTime - entry.Instance.InitTime).TotalSeconds > entry.Instance.FadeTime || !_entMan.EntityExists(entry.Entity)); + + foreach (var (entityUid, distortion) in _cached) + { + if (!_entMan.TryGetComponent(entityUid, out var xform)) + continue; + + if (xform.MapID != args.MapId) + continue; + + var mapPos = _xformSystem.GetWorldPosition(xform); + var tempCoords = args.Viewport.WorldToLocal(mapPos); + + tempCoords.Y = 1 - tempCoords.Y / args.Viewport.Size.Y; + tempCoords.X /= args.Viewport.Size.X; + + var time = Math.Max(0f, (float)(_timing.RealTime - distortion.InitTime).TotalSeconds); + var fade = 1f - Math.Clamp(time / distortion.FadeTime, 0f, 1f); + + var i = _currentCount; + _positions[i] = tempCoords; + _waveStrengths[i] = distortion.WaveStrength; + _waveSpeeds[i] = distortion.WaveSpeed; + _downScales[i] = distortion.DownScale; + _fades[i] = fade; + _times[i] = time; + + _currentCount += 1; + if (_currentCount == MaximumInstances) + break; + } + + return _currentCount != 0; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null || args.Viewport.Eye == null) + return; + + _shader.SetParameter("positions", _positions); + _shader.SetParameter("waveSpeeds", _waveSpeeds); + _shader.SetParameter("downScales", _downScales); + _shader.SetParameter("waveStrengths", _waveStrengths); + _shader.SetParameter("fades", _fades); + _shader.SetParameter("times", _times); + _shader.SetParameter("count", _currentCount); + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _shader.SetParameter("renderScale", args.Viewport.RenderScale * args.Viewport.Eye.Scale); + + var worldHandle = args.WorldHandle; + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_shader); + worldHandle.DrawRect(args.WorldBounds, Color.White); + worldHandle.UseShader(null); + } + + public void Register(Entity ent) + { + _cached.Add((ent.Owner, new InnerShaderInstance + { + WaveSpeed = ent.Comp.WaveSpeed, + WaveStrength = ent.Comp.WaveStrength, + DownScale = ent.Comp.DownScale, + FadeTime = ent.Comp.FadeTime, + InitTime = ent.Comp.InitTime == TimeSpan.Zero ? _timing.RealTime : ent.Comp.InitTime + })); + } + + public void Clear() + { + _cached.Clear(); + _currentCount = 0; + } + + private struct InnerShaderInstance + { + public float WaveSpeed; + public float WaveStrength; + public float DownScale; + public float FadeTime; + public TimeSpan InitTime; + } +} diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs new file mode 100644 index 00000000000..25e2f3d9af9 --- /dev/null +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs @@ -0,0 +1,90 @@ +using Content.Client.Explosion; +using Content.Shared.Ashfall.Overlays.ShockWave; +using Content.Shared.Explosion; +using Content.Shared.Explosion.Components; +using Robust.Client.Graphics; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Overlays.ShockWave; + +public sealed partial class ShockWaveSystem : EntitySystem +{ + [Dependency] private IOverlayManager _overlayMan = default!; + [Dependency] private IGameTiming _timing = default!; + + private readonly HashSet _registered = new(); + private readonly HashSet _spawnedForExplosion = new(); + private ShockWaveOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); + _overlay = new ShockWaveOverlay(); + _overlayMan.AddOverlay(_overlay); + + SubscribeLocalEvent(OnShockWaveStartup); + SubscribeLocalEvent(OnShockWaveRemoved); + SubscribeLocalEvent(OnExplosionStateApplied); + SubscribeLocalEvent(OnExplosionVisualsShutdown); + SubscribeLocalEvent(OnExplosionTexturesStartup); + SubscribeLocalEvent(OnExplosionTexturesShutdown); + } + + public override void Shutdown() + { + _overlay.Clear(); + _overlayMan.RemoveOverlay(); + _registered.Clear(); + _spawnedForExplosion.Clear(); + base.Shutdown(); + } + + private void TrySpawnShockWave(EntityUid uid, ExplosionVisualsComponent visuals) + { + if (visuals.Epicenter != MapCoordinates.Nullspace && _spawnedForExplosion.Add(uid)) + { + Spawn("AshfallEffectShockWave", visuals.Epicenter); + } + } + + private void OnExplosionStateApplied(EntityUid uid, ExplosionVisualsComponent comp, ref ExplosionVisualsStateAppliedEvent args) + { + TrySpawnShockWave(uid, comp); + } + + private void OnExplosionTexturesStartup(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentStartup args) + { + if (TryComp(uid, out var visuals)) + TrySpawnShockWave(uid, visuals); + } + + private void OnExplosionVisualsShutdown(EntityUid uid, ExplosionVisualsComponent comp, ComponentShutdown args) + { + _spawnedForExplosion.Remove(uid); + } + + private void OnExplosionTexturesShutdown(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentShutdown args) + { + _spawnedForExplosion.Remove(uid); + } + + private void OnShockWaveStartup(Entity ent, ref ComponentStartup args) + { + if (!_registered.Add(ent.Owner)) + return; + + if (ent.Comp.InitTime == TimeSpan.Zero) + ent.Comp.InitTime = _timing.RealTime; + + _overlay.Register(ent); + } + + private void OnShockWaveRemoved(Entity ent, ref ComponentRemove args) + { + _registered.Remove(ent.Owner); + } +} diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs new file mode 100644 index 00000000000..c4c29368759 --- /dev/null +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs @@ -0,0 +1,23 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client.Ashfall.Weapons.Ranged.Tracer; + +public sealed class TracerOverlay : Overlay +{ + private readonly TracerSystem _tracer; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; + + public TracerOverlay(TracerSystem tracer) + { + _tracer = tracer; + ZIndex = 10; + IoCManager.InjectDependencies(this); + } + + protected override void Draw(in OverlayDrawArgs args) + { + _tracer.Draw(args.WorldHandle, args.MapId); + } +} diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs new file mode 100644 index 00000000000..44351f35b09 --- /dev/null +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs @@ -0,0 +1,222 @@ +using System.Numerics; +using Content.Shared.Ashfall.Weapons.Ranged.Tracer; +using Robust.Client.Graphics; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Weapons.Ranged.Tracer; + +public sealed partial class TracerSystem : EntitySystem +{ + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IOverlayManager _overlay = default!; + [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private IPrototypeManager _proto = default!; + + private static readonly ProtoId UnshadedShaderId = "unshaded"; + + private TracerOverlay _tracerOverlay = default!; + private ShaderInstance _unshadedShader = default!; + private readonly List _activeTrails = new(); + + private const float TracerThickness = 0.08f; + private const float TrailFadeDuration = 0.22f; + + public override void Initialize() + { + base.Initialize(); + _unshadedShader = _proto.Index(UnshadedShaderId).Instance(); + if (_overlay.HasOverlay()) + _overlay.RemoveOverlay(); + _tracerOverlay = new TracerOverlay(this); + _overlay.AddOverlay(_tracerOverlay); + + SubscribeLocalEvent(OnTracerStart); + SubscribeLocalEvent(OnTracerRemove); + } + + public override void Shutdown() + { + _overlay.RemoveOverlay(); + _activeTrails.Clear(); + base.Shutdown(); + } + + private void OnTracerStart(Entity ent, ref ComponentStartup args) + { + var xform = Transform(ent); + var currentPos = _transform.GetWorldPosition(xform); + var positions = new List { currentPos }; + + ent.Comp.Data = new TracerData( + positions, + _timing.RealTime + TimeSpan.FromSeconds(ent.Comp.Lifetime) + ); + + _activeTrails.Add(new ActiveTracerTrail + { + Entity = ent.Owner, + MapId = xform.MapID, + Color = ent.Comp.Color, + Length = ent.Comp.Length, + Positions = positions, + EndTime = _timing.RealTime + TimeSpan.FromSeconds(ent.Comp.Lifetime), + }); + } + + private void OnTracerRemove(Entity ent, ref ComponentRemove args) + { + foreach (var trail in _activeTrails) + { + if (trail.Entity == ent.Owner) + { + trail.Entity = null; + trail.FadeStartTime ??= _timing.RealTime; + } + } + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + var curTime = _timing.RealTime; + + for (var i = _activeTrails.Count - 1; i >= 0; i--) + { + var trail = _activeTrails[i]; + + if (trail.FadeStartTime is { } fadeStart) + { + if ((float)(curTime - fadeStart).TotalSeconds >= trail.FadeDuration) + { + _activeTrails.RemoveAt(i); + } + continue; + } + + if (trail.Entity == null || !Exists(trail.Entity) || Deleted(trail.Entity) || curTime > trail.EndTime) + { + trail.Entity = null; + trail.FadeStartTime = curTime; + continue; + } + + if (!TryComp(trail.Entity.Value, out TransformComponent? xform)) + { + trail.Entity = null; + trail.FadeStartTime = curTime; + continue; + } + + trail.MapId = xform.MapID; + var currentPos = _transform.GetWorldPosition(xform); + + if (trail.Positions.Count == 0 || Vector2.DistanceSquared(trail.Positions[^1], currentPos) > 0.0001f) + { + trail.Positions.Add(currentPos); + } + + while (trail.Positions.Count > 2 && GetTrailLength(trail.Positions) > trail.Length) + { + trail.Positions.RemoveAt(0); + } + + if (trail.Positions.Count >= 2) + { + var trailLen = GetTrailLength(trail.Positions); + if (trailLen > trail.Length) + { + var excess = trailLen - trail.Length; + var seg = trail.Positions[1] - trail.Positions[0]; + var segLen = seg.Length(); + if (segLen > 0.0001f) + { + var t = MathF.Min(excess / segLen, 1f); + trail.Positions[0] = Vector2.Lerp(trail.Positions[0], trail.Positions[1], t); + } + } + } + } + } + + private static float GetTrailLength(List positions) + { + var length = 0f; + for (var i = 1; i < positions.Count; i++) + { + length += Vector2.Distance(positions[i - 1], positions[i]); + } + return length; + } + + public void Draw(DrawingHandleWorld handle, MapId currentMap) + { + var curTime = _timing.RealTime; + var hasDrawnAny = false; + + handle.SetTransform(Matrix3x2.Identity); + + foreach (var trail in _activeTrails) + { + if (trail.MapId != currentMap || trail.Positions.Count < 2) + continue; + + var fade = 1.0f; + if (trail.FadeStartTime is { } fadeStart) + { + fade = 1.0f - Math.Clamp((float)(curTime - fadeStart).TotalSeconds / trail.FadeDuration, 0f, 1f); + if (fade <= 0.001f) + continue; + } + + if (!hasDrawnAny) + { + handle.UseShader(_unshadedShader); + hasDrawnAny = true; + } + + var segCount = trail.Positions.Count - 1; + for (var i = 1; i < trail.Positions.Count; i++) + { + var p0 = trail.Positions[i - 1]; + var p1 = trail.Positions[i]; + var diff = p1 - p0; + var len = diff.Length(); + if (len < 0.001f) + continue; + + var progress = (float)i / segCount; + var segAlpha = fade * MathHelper.Lerp(0.35f, 1.0f, progress); + var color = trail.Color.WithAlpha(trail.Color.A * segAlpha); + + var rect = new Box2Rotated( + Box2.FromDimensions(-len / 2f, -TracerThickness / 2f, len, TracerThickness), + diff.ToAngle(), + (p0 + p1) / 2f); + + handle.DrawRect(rect, color); + } + } + + if (hasDrawnAny) + { + handle.UseShader(null); + } + } + + private sealed class ActiveTracerTrail + { + public EntityUid? Entity; + public MapId MapId; + public Color Color; + public float Length; + public List Positions = new(); + public TimeSpan EndTime; + public TimeSpan? FadeStartTime; + public float FadeDuration = TrailFadeDuration; + } +} diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 87b47d0fd87..9dc7c527975 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,12 +1,19 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; +using Content.Client.Ashfall.Chat; using Content.Client.Chat.Managers; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Speech; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -16,8 +23,10 @@ public abstract partial class SpeechBubble : Control { [Dependency] private IGameTiming _timing = default!; [Dependency] private IEyeManager _eyeManager = default!; - [Dependency] private IEntityManager _entityManager = default!; + [Dependency] protected IEntityManager _entityManager = default!; [Dependency] protected IConfigurationManager ConfigManager = default!; + [Dependency] protected IPrototypeManager _prototypeManager = default!; + [Dependency] protected IResourceCache _resourceCache = default!; private readonly SharedTransformSystem _transformSystem; public enum SpeechType : byte @@ -56,8 +65,11 @@ public enum SpeechType : byte /// private TimeSpan _deathTime; + private bool _dying; + private bool _dead; + public float VerticalOffset { get; set; } - private float _verticalOffsetAchieved; + protected float VerticalOffsetAchieved; public Vector2 ContentSize { get; private set; } @@ -66,6 +78,9 @@ public enum SpeechType : byte public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity) { + if (IoCManager.Resolve().GetCVar(CCVars.ChatEnableRunechatBubbles)) + return new RunechatSpeechBubble(type, message, senderEntity); + switch (type) { case SpeechType.Emote: @@ -85,11 +100,12 @@ public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage messa } } - public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null, TimeSpan? totalTime = null) { IoCManager.InjectDependencies(this); _senderEntity = senderEntity; _transformSystem = _entityManager.System(); + MouseFilter = MouseFilterMode.Ignore; // Use text clipping so new messages don't overlap old ones being pushed up. RectClipContent = true; @@ -102,12 +118,44 @@ public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechSt bubble.Measure(Vector2Helpers.Infinity); ContentSize = bubble.DesiredSize; - _verticalOffsetAchieved = -ContentSize.Y; - _deathTime = _timing.RealTime + TotalTime; + VerticalOffsetAchieved = -ContentSize.Y; + _deathTime = _timing.RealTime + (totalTime ?? TotalTime); } protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); + protected virtual Vector2 GetWorldPositionOffset(EntityUid senderEntity, TransformComponent xform) + { + return Vector2.Zero; + } + + protected virtual Vector2 GetScreenPositionOffset(EntityUid senderEntity, TransformComponent xform) + { + return Vector2.Zero; + } + + protected virtual float GetSenderVisibilityAlpha() + { + var alpha = 1f; + + if (_entityManager.TryGetComponent(_senderEntity, out var sprite)) + { + if (!sprite.Visible && _entityManager.IsClientSide(_senderEntity)) + return 0f; + + alpha = sprite.Color.A; + } + + if (_entityManager.TryGetComponent(_senderEntity, out var stealth) && stealth.Enabled) + { + var stealthSys = _entityManager.System(); + var stealthAlpha = Math.Clamp(stealthSys.GetVisibility(_senderEntity, stealth), 0f, 1f); + alpha = Math.Min(alpha, stealthAlpha); + } + + return alpha; + } + protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); @@ -116,18 +164,22 @@ protected override void FrameUpdate(FrameEventArgs args) if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0) { // Timer spawn to prevent concurrent modification exception. - Timer.Spawn(0, Die); + if (!_dying) + { + _dying = true; + Timer.Spawn(0, Die); + } return; } // Lerp to our new vertical offset if it's been modified. - if (MathHelper.CloseToPercent(_verticalOffsetAchieved - VerticalOffset, 0, 0.1)) + if (MathHelper.CloseToPercent(VerticalOffsetAchieved - VerticalOffset, 0, 0.1)) { - _verticalOffsetAchieved = VerticalOffset; + VerticalOffsetAchieved = VerticalOffset; } else { - _verticalOffsetAchieved = MathHelper.Lerp(_verticalOffsetAchieved, VerticalOffset, 10 * args.DeltaSeconds); + VerticalOffsetAchieved = MathHelper.Lerp(VerticalOffsetAchieved, VerticalOffset, 10 * args.DeltaSeconds); } if (!_entityManager.TryGetComponent(_senderEntity, out var xform) || xform.MapID != _eyeManager.CurrentEye.Position.MapId) @@ -136,15 +188,17 @@ protected override void FrameUpdate(FrameEventArgs args) return; } + var alpha = GetSenderVisibilityAlpha(); + if (timeLeft <= FadeTime.TotalSeconds) { // Update alpha if we're fading. - Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds); + Modulate = Color.White.WithAlpha((timeLeft / (float)FadeTime.TotalSeconds) * alpha); } else { // Make opaque otherwise, because it might have been hidden before - Modulate = Color.White; + Modulate = Color.White.WithAlpha(alpha); } var baseOffset = 0f; @@ -153,26 +207,35 @@ protected override void FrameUpdate(FrameEventArgs args) baseOffset = speech.SpeechBubbleOffset; var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset); - var worldPos = _transformSystem.GetWorldPosition(xform) + offset; + var worldPos = _transformSystem.GetWorldPosition(xform) + offset + GetWorldPositionOffset(_senderEntity, xform); - var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale; - var screenPos = lowerCenter - new Vector2(ContentSize.X / 2, ContentSize.Y + _verticalOffsetAchieved); + var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale + GetScreenPositionOffset(_senderEntity, xform); + var screenPos = lowerCenter - new Vector2(ContentSize.X / 2, ContentSize.Y + VerticalOffsetAchieved); // Round to nearest 0.5 screenPos = (screenPos * 2).Rounded() / 2; LayoutContainer.SetPosition(this, screenPos); - var height = MathF.Ceiling(MathHelper.Clamp(lowerCenter.Y - screenPos.Y, 0, ContentSize.Y)); - SetHeight = height; + if (!RectClipContent) + { + SetHeight = ContentSize.Y; + } + else + { + var height = MathF.Ceiling(MathHelper.Clamp(lowerCenter.Y - screenPos.Y, 0, ContentSize.Y)); + SetHeight = height; + } } private void Die() { - if (Disposed) + if (Disposed || _dead) { return; } + _dead = true; OnDied?.Invoke(_senderEntity, this); + OnDied = null; } /// @@ -200,6 +263,35 @@ protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, return FormatSpeech(SharedChatSystem.GetStringInsideTag(message, tag), fontColor); } + protected bool TryGetLanguageIcon(ChatMessage message, [NotNullWhen(true)] out Texture? texture) + { + texture = null; + + if (string.IsNullOrEmpty(message.LanguageIcon)) + return false; + + if (_resourceCache.TryGetResource(new ResPath(message.LanguageIcon), out var textureResource)) + { + texture = textureResource.Texture; + return true; + } + + if (_entityManager.EntitySysManager.TryGetEntitySystem(out var spriteSystem)) + { + try + { + var specifier = new SpriteSpecifier.Rsi(new ResPath(message.LanguageIcon), "icon"); + texture = spriteSystem.Frame0(specifier); + return true; + } + catch + { + // ignored + } + } + + return false; + } } public sealed class TextSpeechBubble : SpeechBubble @@ -232,7 +324,6 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl public sealed class FancyTextSpeechBubble : SpeechBubble { - public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) : base(message, senderEntity, speechStyleClass, fontColor) { @@ -240,20 +331,42 @@ public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) { + if (speechStyleClass == "sayBox" && message.SpeechStyleClass != null) + { + speechStyleClass = message.SpeechStyleClass; + } + if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles)) { var label = new RichTextLabel { MaxWidth = SpeechMaxWidth, + StyleClasses = { "bubbleContent" }, OutlineColorOverride = TextOutline.Default.Color, }; label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + Control content = label; + if (TryGetLanguageIcon(message, out var iconTexture)) + { + var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; + var textureRect = new TextureRect + { + Texture = iconTexture, + TextureScale = Vector2.One * 0.5f, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 0, 4, 0) + }; + container.AddChild(textureRect); + container.AddChild(label); + content = container; + } + var unfanciedPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, - Children = { label }, + Children = { content }, ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)), }; return unfanciedPanel; @@ -275,11 +388,23 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl OutlineColorOverride = TextOutline.Default.Color, }; - //We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr + var headerContainer = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; + if (TryGetLanguageIcon(message, out var headerIcon)) + { + var iconTexture = new TextureRect + { + Texture = headerIcon, + TextureScale = Vector2.One * 0.5f, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 0, 4, 0) + }; + headerContainer.AddChild(iconTexture); + } + bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor)); + headerContainer.AddChild(bubbleHeader); bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); - //As for below: Some day this could probably be converted to xaml. But that is not today. -Myr var mainPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, @@ -293,7 +418,7 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl var headerPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, - Children = { bubbleHeader }, + Children = { headerContainer }, ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity) : 0f), HorizontalAlignment = HAlignment.Center, VerticalAlignment = VAlignment.Top diff --git a/Content.Client/Disposal/Unit/DisposalUnitSystem.cs b/Content.Client/Disposal/Unit/DisposalUnitSystem.cs index 9bb0f88c462..2a23ad7bc06 100644 --- a/Content.Client/Disposal/Unit/DisposalUnitSystem.cs +++ b/Content.Client/Disposal/Unit/DisposalUnitSystem.cs @@ -47,7 +47,7 @@ protected override void OnComponentInit(Entity ent, ref C anim.AnimationTracks.Add( new AnimationTrackPlaySound { - KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0) } + KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0, () => ent.Comp.FlushSound.Params) } } ); } diff --git a/Content.Client/Explosion/ExplosionOverlaySystem.cs b/Content.Client/Explosion/ExplosionOverlaySystem.cs index 5cde7b5f4ef..1aba551359f 100644 --- a/Content.Client/Explosion/ExplosionOverlaySystem.cs +++ b/Content.Client/Explosion/ExplosionOverlaySystem.cs @@ -47,6 +47,9 @@ private void OnExplosionHandleState(EntityUid uid, ExplosionVisualsComponent com component.ExplosionType = state.ExplosionType; component.SpaceMatrix = state.SpaceMatrix; component.SpaceTileSize = state.SpaceTileSize; + + var ev = new ExplosionVisualsStateAppliedEvent(component); + RaiseLocalEvent(uid, ref ev); } private void OnCompRemove(EntityUid uid, ExplosionVisualsComponent component, ComponentRemove args) @@ -97,3 +100,9 @@ public override void Shutdown() _overlayMan.RemoveOverlay(); } } + +/// +/// Event raised on an entity with ExplosionVisualsComponent after state has been handled and applied. +/// +[ByRefEvent] +public readonly record struct ExplosionVisualsStateAppliedEvent(ExplosionVisualsComponent Component); diff --git a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs index 561cda1aa37..3f90be8bd37 100644 --- a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs +++ b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs @@ -127,7 +127,7 @@ private Animation BlinkingAnimation(PoweredLightVisualsComponent comp) { KeyFrames = { - new AnimationTrackPlaySound.KeyFrame(sound, 0.5f) + new AnimationTrackPlaySound.KeyFrame(sound, 0.5f, () => comp.BlinkingSound.Params) } }); } diff --git a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs index ff706ea35e3..09f08e8c806 100644 --- a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs +++ b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs @@ -1,30 +1,36 @@ using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Robust.Client.GameObjects; namespace Content.Client.Movement.Systems; /// -/// Controls the switching of motion and standing still animation +/// Controls the switching of motion and standing still animation. /// public sealed partial class ClientSpriteMovementSystem : SharedSpriteMovementSystem { [Dependency] private SpriteSystem _sprite = default!; [Dependency] private EntityQuery _spriteQuery = default!; - public override void Initialize() + protected override void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) { - base.Initialize(); - - SubscribeLocalEvent(OnAfterAutoHandleState); + base.OnSpriteMoveInput(ent, ref args); + UpdateSprite(ent, args.IsMoving); } + [SubscribeLocalEvent] private void OnAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateSprite(ent, ent.Comp.IsMoving); + } + + private void UpdateSprite(Entity ent, bool isMoving) { if (!_spriteQuery.TryGetComponent(ent, out var sprite)) return; - if (ent.Comp.IsMoving) + if (isMoving) { foreach (var (layer, state) in ent.Comp.MovementLayers) { diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs index 647e4d3ec73..c1dcb38b273 100644 --- a/Content.Client/Silicons/Borgs/BorgSystem.cs +++ b/Content.Client/Silicons/Borgs/BorgSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Alert; +using Content.Shared.Alert; using Content.Shared.Mobs; using Content.Shared.Power.EntitySystems; using Content.Shared.PowerCell; @@ -104,6 +104,9 @@ private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref A if (!_appearance.TryGetData(uid, MMIVisuals.HasMind, out bool hasMind)) hasMind = false; + var lightColor = Color.White; + var lightVisible = false; + _sprite.LayerSetVisible((uid, sprite), MMIVisualLayers.Brain, brain); if (!brain) { @@ -115,6 +118,18 @@ private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref A ? component.HasMindState : component.NoMindState; _sprite.LayerSetRsiState((uid, sprite), MMIVisualLayers.Base, state); + + lightColor = hasMind + ? component.HasMindLightColor + : component.NoMindLightColor; + lightVisible = true; + } + + // Update color if it exists. + if (_sprite.LayerMapTryGet((uid, sprite), MMIVisualLayers.Unlit, out var layerIndex, logMissing: false)) + { + _sprite.LayerSetVisible((uid, sprite), layerIndex, lightVisible); + _sprite.LayerSetColor((uid, sprite), layerIndex, lightColor); } } diff --git a/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs index 16a5cfe62f7..1d670b09f02 100644 --- a/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs +++ b/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs @@ -1,4 +1,4 @@ -using Content.Client.Examine; +using Content.Client.Examine; using Content.Client.Stylesheets.Fonts; using Content.Client.Stylesheets.SheetletConfigs; using Content.Client.Stylesheets.Stylesheets; @@ -55,10 +55,36 @@ public override StyleRule[] GetRules(T sheet, object config) .Class("speechBox", "whisperBox") .ParentOf(E().Class("bubbleContent")) .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(12, FontKind.Italic)), + E() + .Class("speechBox", "whisperBox") + .ParentOf(E().ParentOf(E().Class("bubbleContent"))) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(12, FontKind.Italic)), E() .Class("speechBox", "emoteBox") .ParentOf(E().Class("bubbleContent")) .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(12, FontKind.Italic)), + E() + .Class("speechBox", "commanderSpeech") + .Panel(tooltipBox), + E() + .Class("speechBox", "commanderSpeech") + .ParentOf(E().Class("bubbleContent")) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(16, FontKind.Bold)), + E() + .Class("speechBox", "commanderSpeech") + .ParentOf(E().ParentOf(E().Class("bubbleContent"))) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(16, FontKind.Bold)), + E() + .Class("speechBox", "megaphoneSpeech") + .Panel(tooltipBox), + E() + .Class("speechBox", "megaphoneSpeech") + .ParentOf(E().Class("bubbleContent")) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(20, FontKind.Bold)), + E() + .Class("speechBox", "megaphoneSpeech") + .ParentOf(E().ParentOf(E().Class("bubbleContent"))) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(20, FontKind.Bold)), ]; } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 2d0229627f8..a6246425861 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -948,6 +948,16 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font", notoSansItalic12), }), + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "whisperBox"}, null, null), + new SelectorChild( + new SelectorElement(typeof(BoxContainer), null, null, null), + new SelectorElement(typeof(RichTextLabel), new[] {"bubbleContent"}, null, null))), + new[] + { + new StyleProperty("font", notoSansItalic12), + }), + new StyleRule(new SelectorChild( new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "emoteBox"}, null, null), new SelectorElement(typeof(RichTextLabel), null, null, null)), @@ -956,6 +966,52 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font", notoSansItalic12), }), + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "commanderSpeech" }, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null)), + new[] + { + new StyleProperty("font", notoSansBold16), + }), + + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "commanderSpeech" }, null, null), + new SelectorChild( + new SelectorElement(typeof(BoxContainer), null, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null))), + new[] + { + new StyleProperty("font", notoSansBold16), + }), + + new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "commanderSpeech"}, null, null), new[] + { + new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) + }), + + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "megaphoneSpeech" }, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null)), + new[] + { + new StyleProperty("font", notoSansBold20), + }), + + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "megaphoneSpeech" }, null, null), + new SelectorChild( + new SelectorElement(typeof(BoxContainer), null, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null))), + new[] + { + new StyleProperty("font", notoSansBold20), + }), + + new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "megaphoneSpeech"}, null, null), new[] + { + new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) + }), + new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassLabelKeyText}, null, null), new[] { new StyleProperty(Label.StylePropertyFont, notoSansBold12), diff --git a/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs b/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs index d6625146c6e..581ebeb352f 100644 --- a/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs +++ b/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs @@ -1,18 +1,18 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - using Content.Shared.GameTicking; using Content.Shared.Projectiles; using Robust.Client.Physics; +using Robust.Client.Player; using Robust.Shared.Physics.Systems; namespace Content.Client.Trauma.Prediction; /// /// Marks projectiles as participating in client-side physics prediction. -/// Only marks projectiles fired by the local player (via ShotPredictedProjectileEvent). +/// Seamlessly predicts projectiles shot by the local player without race conditions. /// public sealed partial class PredictedProjectileSystem : EntitySystem { + [Dependency] private IPlayerManager _player = default!; [Dependency] private SharedPhysicsSystem _physics = default!; private readonly HashSet _predictedProjectiles = new(); @@ -24,21 +24,47 @@ public override void Initialize() SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnPlayerShotProjectile); + SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnUpdateIsPredicted); SubscribeNetworkEvent(OnShotPredictedProjectile); SubscribeNetworkEvent(OnRoundRestart); } + private void TryPredict(EntityUid uid, ProjectileComponent comp) + { + if (_predictedProjectiles.Contains(uid)) + return; + + var netEnt = GetNetEntity(uid); + var isLocalShooter = comp.Shooter != null && comp.Shooter == _player.LocalEntity; + var isPending = _pendingNetEntities.Remove(netEnt); + + if (isLocalShooter || isPending) + { + _predictedProjectiles.Add(uid); + _physics.UpdateIsPredicted(uid); + } + } + private void OnStartup(Entity ent, ref ComponentStartup args) { - var netEnt = GetNetEntity(ent.Owner); - if (_pendingNetEntities.Remove(netEnt)) + TryPredict(ent.Owner, ent.Comp); + } + + private void OnPlayerShotProjectile(ref PlayerShotProjectileEvent args) + { + if (args.User == _player.LocalEntity && TryComp(args.Projectile, out var comp)) { - _predictedProjectiles.Add(ent.Owner); - _physics.UpdateIsPredicted(ent.Owner); + TryPredict(args.Projectile, comp); } } + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + TryPredict(ent.Owner, ent.Comp); + } + private void OnShutdown(Entity ent, ref ComponentShutdown args) { _predictedProjectiles.Remove(ent.Owner); diff --git a/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs b/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs index 23ebf503331..626b7d6221b 100644 --- a/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs +++ b/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs @@ -35,7 +35,7 @@ private void OnComponentInit(Entity ent, ref Compo ent.Comp.PrimingAnimation.AnimationTracks.Add( new AnimationTrackPlaySound() { - KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.PrimingSound), 0) } + KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.PrimingSound), 0, () => ent.Comp.PrimingSound.Params) } } ); } diff --git a/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs b/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs index 12d973ce4b1..c01cf7b0b08 100644 --- a/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs +++ b/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs @@ -22,6 +22,7 @@ using Content.Shared.DoAfter; using Content.Shared.Emp; using Content.Shared.FixedPoint; +using Content.Shared.Gibbing; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; @@ -436,4 +437,59 @@ public void GunBashPrototypeDealsBluntAndStaminaDamage() Assert.That(melee.Damage.DamageDict["Blunt"], Is.GreaterThan(FixedPoint2.Zero)); Assert.That(melee.BluntStaminaDamageFactor, Is.GreaterThan(FixedPoint2.Zero)); } + + [Test] + public async Task TorsoGibbingDropsAllLimbs() + { + var pair = Pair; + var server = pair.Server; + var map = await pair.CreateTestMap(); + + EntityUid human = default; + EntityUid torso = default; + EntityUid head = default; + EntityUid leftArm = default; + EntityUid rightArm = default; + EntityUid leftLeg = default; + EntityUid rightLeg = default; + EntityUid leftHand = default; + EntityUid rightHand = default; + EntityUid leftFoot = default; + EntityUid rightFoot = default; + + await server.WaitAssertion(() => + { + human = server.EntMan.Spawn("MobHuman", map.MapCoords); + var body = server.System(); + torso = body.GetOrgan(human, "Torso")!.Value; + head = body.GetOrgan(human, "Head")!.Value; + leftArm = body.GetOrgan(human, "ArmLeft")!.Value; + rightArm = body.GetOrgan(human, "ArmRight")!.Value; + leftLeg = body.GetOrgan(human, "LegLeft")!.Value; + rightLeg = body.GetOrgan(human, "LegRight")!.Value; + leftHand = body.GetOrgan(human, "HandLeft")!.Value; + rightHand = body.GetOrgan(human, "HandRight")!.Value; + leftFoot = body.GetOrgan(human, "FootLeft")!.Value; + rightFoot = body.GetOrgan(human, "FootRight")!.Value; + + server.System().Gib(torso); + }); + + await pair.RunTicksSync(5); + + await server.WaitAssertion(() => + { + Assert.That(server.EntMan.Deleted(torso), Is.True); + Assert.That(server.EntMan.Deleted(human), Is.True); + Assert.That(server.EntMan.Deleted(head), Is.False, "Head was deleted!"); + Assert.That(server.EntMan.Deleted(leftArm), Is.False, "Left arm was deleted!"); + Assert.That(server.EntMan.Deleted(rightArm), Is.False, "Right arm was deleted!"); + Assert.That(server.EntMan.Deleted(leftLeg), Is.False, "Left leg was deleted!"); + Assert.That(server.EntMan.Deleted(rightLeg), Is.False, "Right leg was deleted!"); + Assert.That(server.EntMan.Deleted(leftHand), Is.False, "Left hand was deleted!"); + Assert.That(server.EntMan.Deleted(rightHand), Is.False, "Right hand was deleted!"); + Assert.That(server.EntMan.Deleted(leftFoot), Is.False, "Left foot was deleted!"); + Assert.That(server.EntMan.Deleted(rightFoot), Is.False, "Right foot was deleted!"); + }); + } } diff --git a/Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs b/Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs new file mode 100644 index 00000000000..fb4ca0f6573 --- /dev/null +++ b/Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs @@ -0,0 +1,44 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Ashfall.Animations; +using Content.Shared.Chat; + +namespace Content.Server.Ashfall.Animations; + +public sealed partial class EmoteAnimationSystem : SharedEmoteAnimationSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnEmote); + } + + private void OnEmote(EntityUid uid, EmoteAnimationComponent component, ref EmoteEvent args) + { + if (args.Handled) + return; + + var emoteId = args.Emote.ID; + if (emoteId.Equals("Flip", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationFlip, component); + else if (emoteId.Equals("Jump", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationJump, component); + else if (emoteId.Equals("Turn", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Spin", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTurn, component); + else if (emoteId.Equals("Tremble", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Shiver", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Shudder", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTremble, component); + else if (emoteId.Equals("TailWag", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Wag", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTailWag, component); + else if (emoteId.Equals("TailStop", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTailStop, component); + } + + public void PlayAnimation(EntityUid uid, string animationId, EmoteAnimationComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return; + + comp.AnimationId = animationId; + comp.CurAnimationIndex++; + Dirty(uid, comp); + } +} diff --git a/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs b/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs index fa61b8317a0..076e36dcf8d 100644 --- a/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs +++ b/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later using Content.Shared.Alert; +using Content.Shared.Armor; using Content.Shared.Ashfall.Audio; using Content.Shared.Ashfall.Combat.Concussion; using Content.Shared.Explosion; @@ -11,6 +12,7 @@ using Content.Shared.Rejuvenate; using Content.Shared.Damage; using Content.Shared.Damage.Systems; +using Content.Shared.Inventory; using Content.Shared.Speech.Components; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -23,6 +25,7 @@ public sealed partial class ConcussionSystem : SharedConcussionSystem [Dependency] private IGameTiming _timing = default!; [Dependency] private AlertsSystem _alertsSystem = default!; [Dependency] private SharedDeafnessSystem _deafness = default!; + [Dependency] private InventorySystem _inventory = default!; private static readonly ProtoId ConcussionAlert = "Concussion"; @@ -32,7 +35,7 @@ public override void Initialize() SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnBeforeExplode); - SubscribeLocalEvent(OnFlashAttempt); + SubscribeLocalEvent(OnAfterFlashed); SubscribeLocalEvent(OnConcussionStateChanged); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnRejuvenate); @@ -47,16 +50,22 @@ private void OnDamageChanged(EntityUid uid, ConcussionThresholdComponent comp, D if (args.DamageDelta == null) return; + var helmetProtection = 1.0f; + if (_inventory.TryGetSlotEntity(uid, "head", out var headItem) && HasComp(headItem)) + { + helmetProtection = 0.5f; + } + // Heavy blunt trauma (batons, hammers, impacts) causes concussion shock if (args.DamageDelta.DamageDict.TryGetValue("Blunt", out var blunt) && blunt.Float() >= 15f) { - AddConcussionDamage(uid, comp, FixedPoint2.New(blunt.Float() * 1.2f)); + AddConcussionDamage(uid, comp, FixedPoint2.New(blunt.Float() * 1.2f * helmetProtection)); } // Heavy caliber piercing rounds deliver hydraulic/kinetic shock if (args.DamageDelta.DamageDict.TryGetValue("Piercing", out var piercing) && piercing.Float() >= 25f) { - AddConcussionDamage(uid, comp, FixedPoint2.New(piercing.Float() * 0.8f)); + AddConcussionDamage(uid, comp, FixedPoint2.New(piercing.Float() * 0.8f * helmetProtection)); } } @@ -111,18 +120,43 @@ private void OnBeforeExplode(EntityUid uid, ConcussionThresholdComponent comp, r if (totalDmg <= 0) return; - var concussionDmg = FixedPoint2.New(totalDmg * 1.5f); - AddConcussionDamage(uid, comp, concussionDmg); + var earProt = _deafness.GetEarProtection(uid); + var acousticMultiplier = MathF.Max(0.0f, 1.0f - earProt); + var concussionDmg = FixedPoint2.New(totalDmg * 1.5f * acousticMultiplier); - var deafDuration = TimeSpan.FromSeconds(Math.Clamp(totalDmg * 0.25f, 15f, 25f)); - _deafness.TryDeafen(uid, deafDuration); + if (concussionDmg > 0) + { + AddConcussionDamage(uid, comp, concussionDmg); + } + + if (earProt < 0.8f) + { + var deafDuration = TimeSpan.FromSeconds(Math.Clamp(totalDmg * 0.25f, 15f, 25f)); + _deafness.TryDeafen(uid, deafDuration); + } } - private void OnFlashAttempt(EntityUid uid, ConcussionThresholdComponent comp, ref FlashAttemptEvent args) + private void OnAfterFlashed(EntityUid uid, ConcussionThresholdComponent comp, ref AfterFlashedEvent args) { - // Flashbang or flash in close proximity causes head disorientation and deafening - AddConcussionDamage(uid, comp, FixedPoint2.New(35)); - _deafness.TryDeafen(uid, TimeSpan.FromSeconds(20)); + if (args.Target != uid) + return; + + var isFlashbang = args.Used is { } used && HasComp(used); + + if (isFlashbang) + { + if (_deafness.HasEarProtection(uid)) + return; + + // Flashbang explosive detonation causes acute acoustic shock and disorientation + AddConcussionDamage(uid, comp, FixedPoint2.New(35)); + _deafness.TryDeafen(uid, TimeSpan.FromSeconds(20)); + } + else + { + // Optical flash causes minor disorientation without permanent acoustic deafness + AddConcussionDamage(uid, comp, FixedPoint2.New(10)); + } } private void OnRefreshSpeed(EntityUid uid, ConcussionThresholdComponent comp, RefreshMovementSpeedModifiersEvent args) diff --git a/Content.Server/Audio/AmbientSoundSystem.cs b/Content.Server/Audio/AmbientSoundSystem.cs index ea0409c725d..9b2287f01f0 100644 --- a/Content.Server/Audio/AmbientSoundSystem.cs +++ b/Content.Server/Audio/AmbientSoundSystem.cs @@ -8,13 +8,21 @@ namespace Content.Server.Audio; public sealed partial class AmbientSoundSystem : SharedAmbientSoundSystem { + [Dependency] private PowerReceiverSystem _powerReceiver = default!; + public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(HandleMapInit); SubscribeLocalEvent(HandlePowerChange); SubscribeLocalEvent(HandlePowerSupply); } + private void HandleMapInit(EntityUid uid, AmbientOnPoweredComponent component, MapInitEvent args) + { + SetAmbience(uid, _powerReceiver.IsPowered(uid)); + } + private void HandlePowerSupply(EntityUid uid, AmbientOnPoweredComponent component, ref PowerNetBatterySupplyEvent args) { SetAmbience(uid, args.Supply); diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 204194d40f7..ab9cc981d99 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -99,7 +99,8 @@ public override void Initialize() SubscribeLocalEvent(OnActiveMicrowaveRemove); SubscribeLocalEvent(OnConstructionTemp); - SubscribeLocalEvent>(OnReactionAttempt); + SubscribeLocalEvent>(OnReactionAttemptRelay); + SubscribeLocalEvent(OnReactionAttempt); SubscribeLocalEvent(OnGetSecretRecipes); } @@ -144,9 +145,14 @@ private void OnConstructionTemp(Entity ent, ref OnC args.Result = HandleResult.False; } + private void OnReactionAttemptRelay(Entity ent, ref SolutionRelayEvent args) + { + OnReactionAttempt(ent, ref args.Event); + } + // Stop reagents from reacting if they are currently reserved for a microwave recipe. // For example Egg would cook into EggCooked, causing it to not being removed once we are done microwaving. - private void OnReactionAttempt(Entity ent, ref SolutionRelayEvent args) + private void OnReactionAttempt(Entity ent, ref ReactionAttemptEvent args) { if (!TryComp(ent.Comp.Microwave, out var activeMicrowaveComp)) return; @@ -158,9 +164,9 @@ private void OnReactionAttempt(Entity ent, ref Solu foreach (var reagent in recipeReagents) { - if (args.Event.Reaction.Reactants.ContainsKey(reagent)) + if (args.Reaction.Reactants.ContainsKey(reagent)) { - args.Event.Cancelled = true; + args.Cancelled = true; return; } } diff --git a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs index 8acfe0ac76b..eb6022006fb 100644 --- a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs +++ b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Light.Components; -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Power; @@ -8,28 +7,29 @@ namespace Content.Server.Light.EntitySystems public sealed partial class LitOnPoweredSystem : EntitySystem { [Dependency] private SharedPointLightSystem _lights = default!; + [Dependency] private PowerReceiverSystem _powerReceiver = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnPowerSupply); } + private void OnMapInit(EntityUid uid, LitOnPoweredComponent component, MapInitEvent args) + { + _lights.SetEnabled(uid, _powerReceiver.IsPowered(uid)); + } + private void OnPowerChanged(EntityUid uid, LitOnPoweredComponent component, ref PowerChangedEvent args) { - if (_lights.TryGetLight(uid, out var light)) - { - _lights.SetEnabled(uid, args.Powered, light); - } + _lights.SetEnabled(uid, args.Powered); } private void OnPowerSupply(EntityUid uid, LitOnPoweredComponent component, ref PowerNetBatterySupplyEvent args) { - if (_lights.TryGetLight(uid, out var light)) - { - _lights.SetEnabled(uid, args.Supply, light); - } + _lights.SetEnabled(uid, args.Supply); } } } diff --git a/Content.Server/Projectiles/LagCompProjectileSystem.cs b/Content.Server/Projectiles/LagCompProjectileSystem.cs index 7991685aef4..79b51d501d1 100644 --- a/Content.Server/Projectiles/LagCompProjectileSystem.cs +++ b/Content.Server/Projectiles/LagCompProjectileSystem.cs @@ -37,13 +37,16 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { + if (Deleted(uid)) + continue; + if (comp.Targets.Count == 0 || comp.ShooterSession == null) continue; var pos = _transform.GetMapCoordinates(uid); foreach (var target in comp.Targets) { - if (Deleted(target)) + if (Deleted(target) || target == comp.Shooter) continue; var lagPos = _transform.ToMapCoordinates(_lag.GetCoordinates(target, comp.ShooterSession)); @@ -80,6 +83,9 @@ private void OnStartCollide(Entity ent, ref StartCol return; var target = args.OtherEntity; + if (target == ent.Comp.Shooter) + return; + if (_lagQuery.HasComp(target)) ent.Comp.Targets.Add(target); } diff --git a/Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs b/Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs new file mode 100644 index 00000000000..6a57e659f81 --- /dev/null +++ b/Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ashfall.Animations; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +public sealed partial class EmoteAnimationComponent : Component +{ + [DataField, AutoNetworkedField] + public string AnimationId = string.Empty; + + [DataField, AutoNetworkedField] + public uint CurAnimationIndex; + + [ViewVariables] + public uint LastClientAnimationIndex; +} diff --git a/Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs b/Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs new file mode 100644 index 00000000000..4b148dbdc44 --- /dev/null +++ b/Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.Ashfall.Animations; + +public abstract class SharedEmoteAnimationSystem : EntitySystem +{ + public const string AnimationFlip = "Flip"; + public const string AnimationJump = "Jump"; + public const string AnimationTurn = "Turn"; + public const string AnimationSpin = "Spin"; + public const string AnimationTremble = "Tremble"; + public const string AnimationShiver = "Shiver"; + public const string AnimationTailWag = "TailWag"; + public const string AnimationTailStop = "TailStop"; +} diff --git a/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs b/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs index 11d4712b425..407254f224c 100644 --- a/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs +++ b/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs @@ -20,21 +20,27 @@ public override void Initialize() base.Initialize(); } - public bool HasEarProtection(EntityUid uid) + public float GetEarProtection(EntityUid uid) { - if (TryComp(uid, out var selfProt) && selfProt.Protection >= 0.8f) - return true; + var maxProt = 0f; + if (TryComp(uid, out var selfProt)) + maxProt = MathF.Max(maxProt, selfProt.Protection); if (_inventory.TryGetContainerSlotEnumerator(uid, out var slots, SlotFlags.EARS | SlotFlags.HEAD)) { while (slots.NextItem(out var item, out _)) { - if (TryComp(item, out var prot) && prot.Protection >= 0.8f) - return true; + if (TryComp(item, out var prot)) + maxProt = MathF.Max(maxProt, prot.Protection); } } - return false; + return maxProt; + } + + public bool HasEarProtection(EntityUid uid) + { + return GetEarProtection(uid) >= 0.8f; } public bool TryDeafen(EntityUid uid, TimeSpan duration, bool ignoreProtection = false, bool showPopup = true) diff --git a/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs b/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs index 01ae9152caa..97f31beccaa 100644 --- a/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs +++ b/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs @@ -159,7 +159,6 @@ public HumanoidCharacterProfile BuildProfile( "AshfallTraitServiceServiceability", "AshfallTraitAgroGreenThumb", "AshfallTraitBureaucraticPatience", - "Pacifist", "LightweightDrunk", "Snoring" }; diff --git a/Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs b/Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs new file mode 100644 index 00000000000..b8238f11c38 --- /dev/null +++ b/Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs @@ -0,0 +1,17 @@ +namespace Content.Shared.Ashfall.Chat; + +public static class AshfallRunechatStyles +{ + public const string Say = "runechatSay"; + public const string Whisper = "runechatWhisper"; + public const string Radio = "runechatRadio"; + public const string Emote = "runechatEmote"; + public const string Looc = "runechatLooc"; + public const string Scream = "scream"; + public const string Pain = "pain"; + + public static bool IsInterrupting(string? style) + { + return style == Scream || style == Pain; + } +} diff --git a/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs b/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs index 9941141e38f..a10fc4af98c 100644 --- a/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs +++ b/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs @@ -15,7 +15,7 @@ public sealed partial class ConcussionThresholdComponent : Component public FixedPoint2 AbsoluteCap = FixedPoint2.New(200); [DataField] - public FixedPoint2 HealRate = FixedPoint2.New(1.5f); + public FixedPoint2 HealRate = FixedPoint2.New(5.0f); [DataField] public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); diff --git a/Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs b/Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs new file mode 100644 index 00000000000..5c33b41c223 --- /dev/null +++ b/Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ashfall.Overlays.Sandevistan; + +/// +/// Attached to entities under adrenaline surge or Sandevistan reflex boost. +/// Causes the client to render chromatic distortion and fast-action motion trails. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SandevistanVisionComponent : Component +{ + [DataField, AutoNetworkedField] + public bool Enabled = true; +} diff --git a/Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs b/Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs new file mode 100644 index 00000000000..3fce4ae49fc --- /dev/null +++ b/Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ashfall.Overlays.ShockWave; + +/// +/// Displays a propagating refractive screen shockwave ring. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ShockWaveComponent : Component +{ + [DataField, AutoNetworkedField] + public float WaveSpeed = 15.3f; + + [DataField, AutoNetworkedField] + public float WaveStrength = 1.0f; + + [DataField, AutoNetworkedField] + public float DownScale = 1.5f; + + [DataField, AutoNetworkedField] + public float FadeTime = 1.5f; + + [DataField, AutoNetworkedField] + public TimeSpan InitTime; +} diff --git a/Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs b/Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs new file mode 100644 index 00000000000..3893d26dd18 --- /dev/null +++ b/Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs @@ -0,0 +1,40 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ashfall.Weapons.Ranged.Tracer; + +/// +/// Added to projectiles to give them tracer effects. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TracerComponent : Component +{ + /// + /// How long the tracer effect should remain active. + /// + [DataField, AutoNetworkedField] + public float Lifetime = 5f; + + /// + /// The maximum length of the tracer trail in meters. + /// + [DataField, AutoNetworkedField] + public float Length = 2.5f; + + /// + /// Color of the tracer line effect. + /// + [DataField, AutoNetworkedField] + public Color Color = Color.FromHex("#FFE082"); + + [ViewVariables] + public TracerData? Data; +} + +[Serializable, NetSerializable, DataRecord] +public partial struct TracerData(List positionHistory, TimeSpan endTime) +{ + public List PositionHistory = positionHistory; + public TimeSpan EndTime = endTime; +} diff --git a/Content.Shared/Body/BodySystem.Relay.cs b/Content.Shared/Body/BodySystem.Relay.cs index 2a69a6f49d1..7fc351e84e8 100644 --- a/Content.Shared/Body/BodySystem.Relay.cs +++ b/Content.Shared/Body/BodySystem.Relay.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Body.Events; using Content.Shared.Gibbing; using Content.Shared.Humanoid; @@ -39,9 +40,15 @@ public void RelayBodyEvent(EntityUid uid, BodyComponent component, T args) wh [PublicAPI] public void RelayEvent(Entity ent, ref T args) where T : struct { + if (ent.Comp.Organs is not { } organs) + return; + var ev = new BodyRelayedEvent(ent, args); - foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + foreach (var organ in organs.ContainedEntities.ToArray()) { + if (Deleted(organ)) + continue; + RaiseLocalEvent(organ, ref ev); } args = ev.Args; @@ -56,9 +63,15 @@ public void RelayEvent(Entity ent, ref T args) where T : struc [PublicAPI] public void RelayEvent(Entity ent, T args) where T : class { + if (ent.Comp.Organs is not { } organs) + return; + var ev = new BodyRelayedEvent(ent, args); - foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + foreach (var organ in organs.ContainedEntities.ToArray()) { + if (Deleted(organ)) + continue; + RaiseLocalEvent(organ, ref ev); } } diff --git a/Content.Shared/CCVar/CCVars.Chat.cs b/Content.Shared/CCVar/CCVars.Chat.cs index 9f31e0527e6..5744905d571 100644 --- a/Content.Shared/CCVar/CCVars.Chat.cs +++ b/Content.Shared/CCVar/CCVars.Chat.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Configuration; +using Robust.Shared.Configuration; namespace Content.Shared.CCVar; @@ -54,6 +54,18 @@ public sealed partial class CCVars CVar.CLIENTONLY | CVar.ARCHIVE, "Toggles displaying a background under the speaking character's name."); + public static readonly CVarDef ChatEnableRunechatBubbles = + CVarDef.Create("chat.enable_runechat_bubbles", + true, + CVar.CLIENTONLY | CVar.ARCHIVE, + "Toggles displaying runechat-style text speech bubbles overhead."); + + public static readonly CVarDef ChatRunechatBubbleScale = + CVarDef.Create("chat.runechat_bubble_scale", + 1.0f, + CVar.CLIENTONLY | CVar.ARCHIVE, + "The scale factor applied to runechat speech bubbles."); + /// /// A message broadcast to each player that joins the lobby. /// May be changed by admins ingame through use of the "set-motd" command. diff --git a/Content.Shared/Chat/MsgChatMessage.cs b/Content.Shared/Chat/MsgChatMessage.cs index 4666648624d..6f98e73b42a 100644 --- a/Content.Shared/Chat/MsgChatMessage.cs +++ b/Content.Shared/Chat/MsgChatMessage.cs @@ -37,11 +37,13 @@ public sealed class ChatMessage public Color? MessageColorOverride; public string? AudioPath; public float AudioVolume; + public string? SpeechStyleClass; + public string? LanguageIcon; [NonSerialized] public bool Read; - public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0) + public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, string? speechStyleClass = null, string? languageIcon = null) { Channel = channel; Message = message; @@ -52,6 +54,8 @@ public ChatMessage(ChatChannel channel, string message, string wrappedMessage, N MessageColorOverride = colorOverride; AudioPath = audioPath; AudioVolume = audioVolume; + SpeechStyleClass = speechStyleClass; + LanguageIcon = languageIcon; } public ChatMessage(ChatMessage copyFrom) @@ -65,6 +69,8 @@ public ChatMessage(ChatMessage copyFrom) MessageColorOverride = copyFrom.MessageColorOverride; AudioPath = copyFrom.AudioPath; AudioVolume = copyFrom.AudioVolume; + SpeechStyleClass = copyFrom.SpeechStyleClass; + LanguageIcon = copyFrom.LanguageIcon; Read = copyFrom.Read; } } diff --git a/Content.Shared/Gibbing/GibbingSystem.cs b/Content.Shared/Gibbing/GibbingSystem.cs index 794c62c9798..565b43d2644 100644 --- a/Content.Shared/Gibbing/GibbingSystem.cs +++ b/Content.Shared/Gibbing/GibbingSystem.cs @@ -1,6 +1,8 @@ +using Content.Shared.Body; using Content.Shared.Destructible; using Robust.Shared.Audio.Systems; using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Physics.Systems; using Robust.Shared.Random; @@ -12,6 +14,7 @@ public sealed partial class GibbingSystem : EntitySystem [Dependency] private INetManager _net = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private SharedAudioSystem _audio = default!; + [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedDestructibleSystem _destructible = default!; [Dependency] private SharedPhysicsSystem _physics = default!; [Dependency] private SharedTransformSystem _transform = default!; @@ -49,9 +52,20 @@ public HashSet Gib(EntityUid ent, bool dropGiblets = true, EntityUid? if (dropGiblets) { + var dropTarget = ent; + while (_container.TryGetContainingContainer(dropTarget, out var container)) + { + if (HasComp(container.Owner)) + { + dropTarget = container.Owner; + continue; + } + break; + } + foreach (var giblet in gibbed) { - _transform.DropNextTo(giblet, ent); + _transform.DropNextTo(giblet, dropTarget); FlingDroppedEntity(giblet); } } diff --git a/Content.Shared/Inventory/InventorySystem.Trauma.cs b/Content.Shared/Inventory/InventorySystem.Trauma.cs index 2c01e1a6f28..037015f9c04 100644 --- a/Content.Shared/Inventory/InventorySystem.Trauma.cs +++ b/Content.Shared/Inventory/InventorySystem.Trauma.cs @@ -1,11 +1,13 @@ using Content.Shared.Random; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Timing; namespace Content.Shared.Inventory; public partial class InventorySystem : EntitySystem { + [Dependency] private INetManager _net = default!; [Dependency] private RandomHelperSystem _randomHelper = default!; /// @@ -14,6 +16,9 @@ public partial class InventorySystem : EntitySystem /// public void DropSlotContents(Entity ent, string slotName) { + if (!_net.IsServer) + return; + if (!Resolve(ent, ref ent.Comp) || Transform(ent).MapID == MapId.Nullspace) return; diff --git a/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs b/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs index 651d34bbc03..4c8e469fbc5 100644 --- a/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs +++ b/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs @@ -297,7 +297,7 @@ protected void UpdateLight(EntityUid uid, { light.LastThunk = time; Dirty(uid, light); - _audio.PlayPredicted(light.TurnOnSound, uid, user: user, light.TurnOnSound.Params.AddVolume(-10f)); + _audio.PlayPredicted(light.TurnOnSound, uid, user: user, light.TurnOnSound.Params.AddVolume(-6f)); } } else diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs index 72bc619eeb6..523aefec7b9 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs @@ -33,13 +33,6 @@ private void OnRelayCanMoveUpdated(Entity ent, ref Can protected virtual void OnInputMoverCanMoveUpdated(Entity ent, ref CanMoveUpdatedEvent args) { - if (!args.CanMove) - { - // Remove from active mover query when entity cannot move - RemCompDeferred(ent); - return; - } - UpdateMoverStatus((ent, ent.Comp)); } diff --git a/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs b/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs index eb4bbc1be63..e7dd91b8f6c 100644 --- a/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs +++ b/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs @@ -3,16 +3,10 @@ namespace Content.Shared.Movement.Systems; -public abstract class SharedSpriteMovementSystem : EntitySystem +public abstract partial class SharedSpriteMovementSystem : EntitySystem { - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnSpriteMoveInput); - } - - private void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) + [SubscribeLocalEvent] + protected virtual void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) { if (ent.Comp.IsMoving == args.IsMoving) return; diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs index ceac98f10b7..89a0121b8ec 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.EntityEffects.Effects.Body; @@ -150,27 +150,23 @@ public bool CanConsume(EntityUid user, #region EdibleComponent + /// + /// Spawns trash for the edible entity, placing it in the same hand if held, otherwise spawning it nearby. + /// + /// Edible entity that will spawn trash. + /// Optional user that will attempt to pickup spawned trash. public void SpawnTrash(Entity entity, EntityUid? user = null) { if (entity.Comp.Trash.Count == 0) return; - var position = _transform.GetMapCoordinates(entity); - var trashes = entity.Comp.Trash; - var pickup = user != null && _hands.IsHolding(user.Value, entity, out _); - - foreach (var trash in trashes) + var pickup = user is not null && _hands.TryDrop(user.Value, entity); + foreach (var trash in entity.Comp.Trash) { - var spawnedTrash = EntityManager.PredictedSpawn(trash, position); - - // If the user is holding the item - if (!pickup) - continue; + var spawnedTrash = PredictedSpawnNextToOrDrop(trash, entity); - // Put the trash in the user's hand - // I am 100% confident we don't need this check but rider gets made at me if it's not here. - if (user != null) - _hands.TryPickupAnyHand(user.Value, spawnedTrash); + if (pickup) + _hands.TryPickupAnyHand(user!.Value, spawnedTrash); } } diff --git a/Content.Shared/Projectiles/ProjectileComponent.cs b/Content.Shared/Projectiles/ProjectileComponent.cs index 7a3a98769d2..1060389aeaa 100644 --- a/Content.Shared/Projectiles/ProjectileComponent.cs +++ b/Content.Shared/Projectiles/ProjectileComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Projectiles; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause] public sealed partial class ProjectileComponent : Component { /// diff --git a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs index 51031c930e3..cf84f7aa339 100644 --- a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Containers.ItemSlots; +using Content.Shared.Containers.ItemSlots; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -29,31 +29,68 @@ public sealed partial class MMIComponent : Component /// The sprite state when the brain inserted has a mind. /// [DataField] - public string HasMindState = "mmi_alive"; + public string HasMindState = "mmi_on"; /// /// The sprite state when the brain inserted doesn't have a mind. /// [DataField] - public string NoMindState = "mmi_dead"; + public string NoMindState = "mmi_off"; /// /// The sprite state when there is no brain inserted. /// [DataField] public string NoBrainState = "mmi_off"; + + /// + /// The color of the layer when the brain inserted has a mind. + /// + [DataField] + public Color HasMindLightColor = Color.FromHex("#0094ff"); + + /// + /// The color of the layer when the brain inserted doesn't have a mind. + /// + [DataField] + public Color NoMindLightColor = Color.FromHex("#ff3033"); } +/// +/// AppearanceData keys for the MMI. +/// [Serializable, NetSerializable] public enum MMIVisuals : byte { + /// + /// bool: Whether or not there is a brain in the MMI. + /// BrainPresent, + + /// + /// bool: Whether or not there is an active mind (a player) in the MMI. + /// HasMind } +/// +/// Sprite map keys for MMI visuals. +/// [Serializable, NetSerializable] public enum MMIVisualLayers : byte { + /// + /// The layer of the brain. + /// Brain, - Base + + /// + /// The layer of the housing. + /// + Base, + + /// + /// The optional layer of an indicator light. + /// + Unlit, } diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs index 00e63145b61..0a36faeb08d 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs @@ -7,11 +7,13 @@ using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Popups; +using Robust.Shared.Network; namespace Content.Medical.Shared.Body; public sealed partial class BodyEquipmentSystem : EntitySystem { + [Dependency] private INetManager _net = default!; [Dependency] private BodyPartSystem _part = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private SharedPopupSystem _popup = default!; @@ -67,6 +69,9 @@ private void OnCheckEquipmentPart(Entity ent, ref CheckE private void OnOrganRemovedFrom(Entity ent, ref OrganRemovedFromEvent args) { + if (!_net.IsServer) + return; + DropPartItems(ent.Owner, args.Organ); } @@ -89,6 +94,9 @@ private bool HasBodyPart(EntityUid body, BodyPartType part) /// public void DropPartItems(Entity ent, Entity part) { + if (!_net.IsServer) + return; + // don't drop for mobs being deleted, gibbing etc would handle it themselves if (!TerminatingOrDeleted(ent) && _inventoryQuery.Resolve(ent, ref ent.Comp) && diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs index 6770e3147eb..62ed2d2e158 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs @@ -6,7 +6,8 @@ using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Gibbing; -using Content.Shared.Throwing; +using Content.Medical.Common.Wounds; +using Content.Medical.Shared.Wounds; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; @@ -22,12 +23,11 @@ public sealed partial class BodyPartSystem : CommonBodyPartSystem { [Dependency] private BodySystem _body = default!; [Dependency] private BodyCacheSystem _cache = default!; + [Dependency] private OrganRelationSystem _organRelation = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedPuddleSystem _puddle = default!; - [Dependency] private SharedTransformSystem _transform = default!; - [Dependency] private ThrowingSystem _throwing = default!; [Dependency] private EntityQuery _query = default!; [Dependency] private EntityQuery _childQuery = default!; [Dependency] private EntityQuery _organQuery = default!; @@ -42,6 +42,7 @@ public override void Initialize() SubscribeLocalEvent(OnPartInserted); SubscribeLocalEvent(OnPartRemoved); SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent>(OnBodyRelayedBeingGibbed); } private void OnPartInserted(Entity ent, ref OrganGotInsertedEvent args) @@ -64,6 +65,9 @@ private void OnPartInserted(Entity ent, ref OrganGotInsertedE if (!_container.Remove(organ, container)) Log.Error($"Organ {ToPrettyString(organ)} got stuck inside of {ToPrettyString(ent)} after being inserted into {ToPrettyString(args.Target)}"); } + + // need to mark the part so it renders! + Dirty(ent); } private void OnPartRemoved(Entity ent, ref OrganGotRemovedEvent args) @@ -87,8 +91,9 @@ private void OnPartRemoved(Entity ent, ref OrganGotRemovedEve var container = EnsureSeveredOrgansContainer(ent); foreach (var (category, organ) in ent.Comp.Children.ToArray()) { + _body.RemoveOrgan(body, organ); // slot has an organ so try to put it in the container - if (!_container.Insert(organ, container)) + if (!_container.Insert(organ, container, force: true)) { // probably from failing to be removed, suspicious Log.Error($"Failed to store {ToPrettyString(ent)}'s {category} organ {ToPrettyString(organ)}!"); @@ -98,49 +103,99 @@ private void OnPartRemoved(Entity ent, ref OrganGotRemovedEve } private void OnBeingGibbed(Entity ent, ref BeingGibbedEvent args) + { + SpillBodyPartOrgans(ent, args.Giblets); + } + + private void OnBodyRelayedBeingGibbed(Entity ent, ref BodyRelayedEvent args) + { + // Only root body parts (like Torso) need to spill when relayed from body; + // child limbs will be detached by the root part. + if (!HasComp(ent)) + SpillBodyPartOrgans(ent, args.Args.Giblets); + } + + private void SpillBodyPartOrgans(Entity ent, HashSet giblets) { var organsToSpill = new List(); if (GetSeveredOrgansContainer(ent.AsNullable()) is {} container) { - foreach (var organ in container.ContainedEntities) + foreach (var organ in container.ContainedEntities.ToArray()) { organsToSpill.Add(organ); } } + Entity? body = _body.GetBody(ent.Owner) is { } bodyUid ? bodyUid : null; + foreach (var (category, organ) in ent.Comp.Children.ToArray()) { if (Deleted(organ) || organsToSpill.Contains(organ)) continue; - if (_organQuery.TryComp(organ, out var organComp) && organComp.Body is { } body) + // If the child is a body part itself (e.g. arm, leg, head attached to torso), + // sever it: gather its sub-children into its own container so they survive with the limb. + if (_query.TryComp(organ, out var childPartComp)) { - _body.RemoveOrgan(body, organ); + if (body != null) + { + var subChildren = _organRelation.AllChildren(organ).ToList(); + var partContainer = EnsureSeveredOrgansContainer((organ, childPartComp)); + + _body.RemoveOrgan(body.Value, organ); + + var delimbedEvent = new BodyPartDelimbedEvent(body.Value.Owner, organ, Crude: true); + RaiseLocalEvent(body.Value.Owner, ref delimbedEvent); + + foreach (var subChild in subChildren) + { + _body.RemoveOrgan(body.Value, subChild.Owner); + // Clear severed container of sub-child parts if any, to avoid nested OnPartInserted + if (_container.Insert(subChild.Owner, partContainer, force: true)) + { + if (_body.GetCategory(subChild.Owner) is { } subCat) + childPartComp.Children[subCat] = subChild.Owner; + } + } + DirtyField(organ, childPartComp, nameof(BodyPartComponent.Children)); + } + + if (TryComp(organ, out var woundable)) + { + woundable.WoundableSeverity = WoundableSeverity.Severed; + DirtyField(organ, woundable, nameof(WoundableComponent.WoundableSeverity)); + } + + organsToSpill.Add(organ); + } + else + { + // Internal organ without BodyPartComponent (e.g. heart, lungs, etc.) + if (body != null && _organQuery.TryComp(organ, out var organComp) && organComp.Body is { }) + { + _body.RemoveOrgan(body.Value, organ); + } + organsToSpill.Add(organ); } - organsToSpill.Add(organ); } + ent.Comp.Children.Clear(); + DirtyField(ent.Owner, ent.Comp, nameof(BodyPartComponent.Children)); + if (organsToSpill.Count == 0) return; - _audio.PlayPvs(GibSound, ent.Owner); + var dropTarget = body != null ? body.Value.Owner : ent.Owner; + _audio.PlayPvs(GibSound, dropTarget); var bloodSolution = new Solution(); bloodSolution.AddReagent(BloodReagent, FixedPoint2.New(15)); - _puddle.TrySpillAt(ent.Owner, bloodSolution, out _, sound: false); + _puddle.TrySpillAt(dropTarget, bloodSolution, out _, sound: false); - var rand = new System.Random(); foreach (var organ in organsToSpill) { - args.Giblets.Add(organ); - - _transform.DropNextTo(organ, ent.Owner); - - var angle = rand.NextSingle() * MathF.PI * 2f; - var dir = new System.Numerics.Vector2(MathF.Cos(angle), MathF.Sin(angle)); - var dist = 1.0f + rand.NextSingle() * 1.5f; - _throwing.TryThrow(organ, dir * dist, 1.5f, pushbackRatio: 0.2f); + giblets.Add(organ); } } diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs index 8b243134259..2e4baf7f6dd 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs @@ -20,7 +20,7 @@ public override void Initialize() SubscribeLocalEvent(OnRemoveAttempt); SubscribeLocalEvent(OnRemoved); - SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent(OnBeingGibbed, after: new[] { typeof(BodyPartSystem) }); } private void OnRemoveAttempt(Entity ent, ref OrganRemoveAttemptEvent args) @@ -30,14 +30,18 @@ private void OnRemoveAttempt(Entity ent, ref OrganRe private void OnRemoved(Entity ent, ref OrganGotRemovedEvent args) { - if (TerminatingOrDeleted(args.Target) || Transform(args.Target).MapID == MapId.Nullspace || _timing.ApplyingState) + if (TerminatingOrDeleted(args.Target) || EntityManager.IsQueuedForDeletion(args.Target) || Transform(args.Target).MapID == MapId.Nullspace || _timing.ApplyingState) return; // all good if it's being deleted or leaving pvs range // if you intentionally deleted the root part, please delete the body instead chud - if (!TerminatingOrDeleted(ent) && !HasComp(ent)) + if (TerminatingOrDeleted(ent) || EntityManager.IsQueuedForDeletion(ent)) { - Log.Warning($"{ToPrettyString(ent)} was deleted instead of the body, {ToPrettyString(args.Target)}!"); - PredictedQueueDel(args.Target); + if (!HasComp(ent)) + { + Log.Warning($"{ToPrettyString(ent)} was deleted instead of the body, {ToPrettyString(args.Target)}!"); + PredictedQueueDel(args.Target); + } + return; } Log.Warning($"{ToPrettyString(ent)} somehow got removed from {ToPrettyString(args.Target)}!"); @@ -49,7 +53,7 @@ private void OnBeingGibbed(Entity ent, ref BeingGibb if (HasComp(ent) || _body.GetBody(ent.Owner) is not {} body) return; - Log.Info($"Root part {ToPrettyString(ent)} was gibbed, gibbing {ToPrettyString(ent)} too!"); + Log.Info($"Root part {ToPrettyString(ent)} was gibbed, gibbing {ToPrettyString(body)} too!"); _gibbing.Gib(body); } } diff --git a/Content.Shared/Vehicle/Systems/VehicleSystem.cs b/Content.Shared/Vehicle/Systems/VehicleSystem.cs index 7073277622c..076a28fe0ef 100644 --- a/Content.Shared/Vehicle/Systems/VehicleSystem.cs +++ b/Content.Shared/Vehicle/Systems/VehicleSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Content.Shared.Access.Components; using Content.Shared.ActionBlocker; using Content.Shared.Damage; @@ -31,6 +31,7 @@ public sealed partial class VehicleSystem : EntitySystem [Dependency] private SharedContainerSystem _container = default!; [Dependency] private DamageableSystem _damageable = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; + [Dependency] private SharedEyeSystem _eye = default!; [Dependency] private EntityWhitelistSystem _entityWhitelist = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private SharedMoverController _mover = default!; @@ -166,6 +167,7 @@ public bool TrySetOperator(Entity entity, EntityUid operatorUi Dirty(operatorUid, vehicleOperator); _mover.SetRelay(operatorUid, entity); + _eye.SetTarget(operatorUid, entity.Owner); var enterEvent = new OnVehicleEnteredEvent(entity, operatorUid); RaiseLocalEvent(operatorUid, ref enterEvent); @@ -190,9 +192,9 @@ public bool TryRemoveOperator(Entity entity) if (entity.Comp.Operator is not { } currentOperator) return false; - _operatorQuery.TryComp(currentOperator, out var currentOperatorComponent); + _eye.SetTarget(currentOperator, null); - if (currentOperatorComponent != null) + if (_operatorQuery.TryComp(currentOperator, out var currentOperatorComponent)) { var exitEvent = new OnVehicleExitedEvent(entity, currentOperator); RaiseLocalEvent(currentOperator, ref exitEvent); @@ -211,7 +213,7 @@ public bool TryRemoveOperator(Entity entity) var setEvent = new VehicleOperatorSetEvent(null, currentOperator); RaiseLocalEvent(entity, ref setEvent); - Dirty(entity); + DirtyFields(entity.Owner, entity.Comp, null, nameof(VehicleComponent.Operator)); return true; } @@ -254,6 +256,7 @@ public bool TryRemoveOperator(Entity operatorEntity) if (_vehicleQuery.TryComp(vehicleUid, out var vehicle)) return TryRemoveOperator((vehicleUid.Value, vehicle)); + _eye.SetTarget(operatorEntity.Owner, null); UnblockHands(vehicleUid.Value, operatorEntity.Owner); ClearOperatorRelays(operatorEntity.Owner, vehicleUid.Value); operatorEntity.Comp.Vehicle = null; diff --git a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs index 3656fab2fb7..0b3c68dc6d5 100644 --- a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs +++ b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs @@ -333,7 +333,7 @@ joint is not DistanceJoint distance || if (grapplerBodyB.Mass < BaseWeightMass) massFactorB *= grapplerBodyB.Mass / BaseWeightMass; - if (sameGrid && physicalHook != _transform.GetGrid(joint.BodyAUid)) + if (!sameGrid || sameGrid && physicalHook != _transform.GetGrid(joint.BodyAUid)) _physics.ApplyLinearImpulse(grapplerUidA, -targetDirection * massFactorA * grappling.ReelForce * frameTime, grapplerOffsetA, body: grapplerBodyA); _physics.ApplyLinearImpulse(grapplerUidB, targetDirection * massFactorB * grappling.ReelForce * frameTime, grapplerOffsetB, body: grapplerBodyB); diff --git a/Resources/Audio/Machines/light_tube_on.ogg b/Resources/Audio/Machines/light_tube_on.ogg index deeffa73398..6f00a777c55 100644 Binary files a/Resources/Audio/Machines/light_tube_on.ogg and b/Resources/Audio/Machines/light_tube_on.ogg differ diff --git a/Resources/Locale/en-US/Ashfall/animations/emotes.ftl b/Resources/Locale/en-US/Ashfall/animations/emotes.ftl new file mode 100644 index 00000000000..cdd0185a3ac --- /dev/null +++ b/Resources/Locale/en-US/Ashfall/animations/emotes.ftl @@ -0,0 +1,17 @@ +chat-emote-name-flip = Do a flip +chat-emote-msg-flip = does a flip! + +chat-emote-name-jump = Jump +chat-emote-msg-jump = jumps! + +chat-emote-name-spin = Spin +chat-emote-msg-spin = spins in place! + +chat-emote-name-tremble = Tremble +chat-emote-msg-tremble = trembles slightly. + +chat-emote-name-tail-wag = Wag tail +chat-emote-msg-tail-wag = wags { POSS-ADJ($entity) } tail. + +chat-emote-name-tail-stop = Stop wagging tail +chat-emote-msg-tail-stop = stops wagging { POSS-ADJ($entity) } tail. diff --git a/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl b/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl new file mode 100644 index 00000000000..22f0a2cab89 --- /dev/null +++ b/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl @@ -0,0 +1,17 @@ +chat-emote-name-flip = Сделать сальто +chat-emote-msg-flip = делает сальто! + +chat-emote-name-jump = Подпрыгнуть +chat-emote-msg-jump = подпрыгивает! + +chat-emote-name-spin = Покружиться +chat-emote-msg-spin = кружится на месте! + +chat-emote-name-tremble = Дрожать +chat-emote-msg-tremble = мелко дрожит. + +chat-emote-name-tail-wag = Вилять хвостом +chat-emote-msg-tail-wag = виляет хвостом. + +chat-emote-name-tail-stop = Перестать вилять хвостом +chat-emote-msg-tail-stop = перестает вилять хвостом. diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl index 52b95bda8d7..5cc01875f51 100644 --- a/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl +++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl @@ -2,7 +2,7 @@ ent-ClothingHeadHelmetBase = { ent-ClothingHeadBase } .desc = { ent-ClothingHeadBase.desc } ent-ClothingHeadHelmetArmoredBase = { ent-ClothingHeadHelmetBase } .desc = { ent-ClothingHeadHelmetBase.desc } -ent-ClothingHeadHelmetBasic = шлем +ent-ClothingHeadHelmetBasic = шлем СБ .desc = Стандартная защитная экипировка. Защищает голову от ударов. ent-ClothingHeadHelmetMerc = шлем наёмника .desc = Боевой шлем обычно используется наёмниками, он прочный, лёгкий и пахнет порохом и джунглями. @@ -58,3 +58,5 @@ ent-ActionToggleJusticeHelm = Переключить шлем правосуди .desc = Включает или выключает шлем правосудия. ent-CardHelmet = картонный шлем .desc = Средневековый шлем, сделанный из картона. +ent-ClothingHeadHelmetHeavyDSS = армированный шлем + .desc = Улучшенная версия обычного шлема Службы Безопасности, оснащённая противоосколочным забралом. diff --git a/Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml b/Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml new file mode 100644 index 00000000000..90e0e9ccc7f --- /dev/null +++ b/Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml @@ -0,0 +1,11 @@ +- type: entity + id: AshfallEffectShockWave + categories: [ HideSpawnMenu ] + components: + - type: TimedDespawn + lifetime: 2.0 + - type: ShockWave + - type: EffectVisuals + - type: Tag + tags: + - HideContextMenu diff --git a/Resources/Prototypes/Ashfall/Shaders/shaders.yml b/Resources/Prototypes/Ashfall/Shaders/shaders.yml index e42027eb5f9..ef73b85bc73 100644 --- a/Resources/Prototypes/Ashfall/Shaders/shaders.yml +++ b/Resources/Prototypes/Ashfall/Shaders/shaders.yml @@ -22,3 +22,8 @@ id: AshfallSuppression kind: source path: "/Textures/Ashfall/Shaders/suppression.swsl" + +- type: shader + id: AshfallSandevistanVision + kind: source + path: "/Textures/Ashfall/Shaders/sandevistan_vision.swsl" diff --git a/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml b/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml index 549658f780f..417d7e45116 100644 --- a/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml +++ b/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml @@ -43,3 +43,72 @@ - миу - мьяу - миау + +- type: emote + id: Flip + name: chat-emote-name-flip + category: Hands + chatMessages: ["chat-emote-msg-flip"] + chatTriggers: + - flip + - flips + - сальто + - кувырок + +- type: emote + id: Jump + name: chat-emote-name-jump + category: Hands + chatMessages: ["chat-emote-msg-jump"] + chatTriggers: + - jump + - jumps + - прыгает + - подпрыгивает + - прыжок + +- type: emote + id: Spin + name: chat-emote-name-spin + category: Hands + chatMessages: ["chat-emote-msg-spin"] + chatTriggers: + - spin + - spins + - кружится + - вертится + +- type: emote + id: Tremble + name: chat-emote-name-tremble + category: Hands + chatMessages: ["chat-emote-msg-tremble"] + chatTriggers: + - tremble + - shiver + - shudder + - дрожит + - трясется + +- type: emote + id: TailWag + name: chat-emote-name-tail-wag + category: Hands + chatMessages: ["chat-emote-msg-tail-wag"] + chatTriggers: + - wag + - wags + - виляет хвостом + - виляет + +- type: emote + id: TailStop + name: chat-emote-name-tail-stop + category: Hands + chatMessages: ["chat-emote-msg-tail-stop"] + chatTriggers: + - stop wag + - stop wagging + - stopwag + - перестает вилять хвостом + - прекращает вилять хвостом diff --git a/Resources/Prototypes/Body/species_base.yml b/Resources/Prototypes/Body/species_base.yml index 0f35554ea40..39b5dbc1992 100644 --- a/Resources/Prototypes/Body/species_base.yml +++ b/Resources/Prototypes/Body/species_base.yml @@ -44,6 +44,7 @@ - type: Footprint - type: CPRTraining - type: OfferItem + - type: EmoteAnimation - type: Carriable - type: RangedDamageSound soundGroups: diff --git a/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml b/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml index ada194e1a20..b87de4a78ad 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml @@ -9,7 +9,7 @@ entity_storage: !type:AllSelector children: - !type:NestedSelector - tableId: AllSoapsTable + tableId: StandardSoapsTable - id: MopItem - id: MopBucketCubeWrapped - id: Bucket diff --git a/Resources/Prototypes/Catalog/Fills/Items/belt.yml b/Resources/Prototypes/Catalog/Fills/Items/belt.yml index 181808861e7..4d301049063 100644 --- a/Resources/Prototypes/Catalog/Fills/Items/belt.yml +++ b/Resources/Prototypes/Catalog/Fills/Items/belt.yml @@ -87,7 +87,7 @@ storagebase: !type:AllSelector children: - !type:NestedSelector - tableId: AllSoapsTable + tableId: StandardSoapsTable - id: SprayBottleSpaceCleaner - id: CleanerGrenade amount: 2 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml index 6ae3e82f05d..7d211b58000 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml @@ -78,6 +78,8 @@ - id: ClothingUniformJumpsuitSecGrey prob: 0.3 - id: ClothingHeadHelmetBasic + - id: ClothingHeadHelmetHeavyDSS + prob: 0.5 - id: ClothingOuterArmorBasic - id: ClothingBeltSecurityFilled - id: Flash diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml index c9b9e3ffda3..cc6cb6b9a41 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml @@ -24,6 +24,8 @@ ClothingEyesBlindfold: 1 ClothingShoesBootsCombat: 1 ClothingShoesBootsWinterSec: 2 + ClothingHeadHelmetBasic: 3 + ClothingHeadHelmetHeavyDSS: 2 ClothingHeadHelmetJustice: 1 contrabandInventory: ClothingMaskClownSecurity: 1 diff --git a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml index 8d9e6fd1e49..7e4ae69c89d 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml @@ -28,6 +28,8 @@ Heat: 0.9 - type: EarProtection protection: 0.95 + - type: ConcussionProtection + protection: 0.5 #Security Helmet - type: entity diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml index 1f4f4b4abab..22d7fe1669b 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml @@ -16,7 +16,7 @@ weight: 0.2 - type: entityTable # max size 1x2 - id: AllSoapsTable + id: StandardSoapsTable table: !type:GroupSelector children: - id: SoapNT @@ -24,6 +24,14 @@ - id: Soap - id: SoapHomemade - id: SoapDeluxe + +- type: entityTable # max size 1x2 + id: AllSoapsTable + table: !type:GroupSelector + children: + - !type:NestedSelector + tableId: StandardSoapsTable + weight: 5 - !type:GroupSelector # Rare soaps weight: 0.02 # 1 in 250 children: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index bebb412eed0..a1c8e0c9c92 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -340,8 +340,7 @@ trash: - TrashBananaPeel - type: ToolRefinable - refineResult: - - id: TrashBananaPeel + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -385,8 +384,7 @@ trash: - TrashMimanaPeel - type: ToolRefinable - refineResult: - - id: TrashMimanaPeel + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -1188,8 +1186,7 @@ trash: - FoodCornTrash - type: ToolRefinable - refineResult: - - id: FoodCornTrash #no idea why you'd ever do this, but it's just here for consistency + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -1865,8 +1862,7 @@ trash: - WeaponRevolverPython - type: ToolRefinable - refineResult: - - id: WeaponRevolverPython + refineResult: [] # Spawns trash on refine - type: Tag tags: - Fruit # It's in the name @@ -1903,8 +1899,7 @@ trash: - RevolverCapGun - type: ToolRefinable - refineResult: - - id: RevolverCapGun + refineResult: [] # Spawns trash on refine - type: Tag tags: - Fruit @@ -1925,8 +1920,7 @@ trash: - RevolverCapGunFake - type: ToolRefinable - refineResult: - - id: RevolverCapGunFake + refineResult: [] # Spawns trash on refine - type: entity name: rice bushel @@ -2335,8 +2329,7 @@ trash: - FoodBungoPit - type: ToolRefinable - refineResult: - - id: FoodBungoPit + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -2639,8 +2632,7 @@ trash: - TrashCherryPit - type: ToolRefinable - refineResult: - - id: TrashCherryPit + refineResult: [] # Spawns trash on refine - type: Solution solution: maxVol: 15 diff --git a/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml b/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml index 5ec8b87ae00..e50147a76c5 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml @@ -39,13 +39,15 @@ parent: BasePaper id: BasePhotograph name: photograph + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi - type: entity parent: BasePhotograph id: PhotographBlack components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: black @@ -54,7 +56,6 @@ id: PhotographRed components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: red @@ -63,7 +64,6 @@ id: PhotographBlue components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: blue @@ -72,7 +72,6 @@ id: PhotographGreen components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: green @@ -81,7 +80,6 @@ id: PhotographYellow components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: yellow @@ -90,7 +88,6 @@ id: PhotographPurple components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: purple @@ -99,7 +96,6 @@ id: PhotographRainbow components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: rainbow diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 69d84810c6b..0ff23c68209 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -12,6 +12,10 @@ visible: false - state: mmi_off map: ["enum.MMIVisualLayers.Base"] + - state: mmi_light + map: ["enum.MMIVisualLayers.Unlit"] + shader: unshaded + visible: false - type: Input context: human - type: MMI @@ -76,7 +80,8 @@ sprite: Objects/Specific/Robotics/mmi.rsi layers: - state: posibrain - map: ["base"] + - map: ["light"] + shader: unshaded - type: Input context: human - type: ToggleableGhostRole @@ -125,8 +130,8 @@ - type: GenericVisualizer visuals: enum.ToggleableGhostRoleVisuals.Status: - base: - Off: { state: posibrain } + light: + Off: { state: empty } # TODO: fix null states in SpriteComp.LayerSetData, use a null state Searching: { state: posibrain-searching } On: { state: posibrain-occupied } - type: GuideHelp diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml index 4025008f6ca..fae342a0c29 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml @@ -6,6 +6,7 @@ description: If you can see this you're probably dead! components: - type: Sprite + drawdepth: Effects noRot: false sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi layers: @@ -45,6 +46,9 @@ path: /Audio/Weapons/Guns/Hits/bullet_hit.ogg - type: TimedDespawn lifetime: 10 + - type: Tracer + color: "#FFE082" + length: 2.2 - type: entity parent: BaseBullet @@ -103,6 +107,9 @@ energy: 7.0 - type: IgniteOnCollide fireStacks: 0.25 + - type: Tracer + color: "#FF7043" + length: 3.0 - type: entity parent: BaseBullet diff --git a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml index c3af9de5c8e..a9eb2066117 100644 --- a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml @@ -318,6 +318,7 @@ light_bulb: !type:ContainerSlot - type: PoweredLight bulb: Bulb + - type: SpookyPoweredLight - type: ApcPowerReceiver - type: ExtensionCableReceiver - type: DeviceNetwork diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml index d7dc05f8c90..cec95c63ec2 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml @@ -27,6 +27,7 @@ - type: startingGear id: SecurityOfficerGear equipment: + head: ClothingHeadHelmetBasic eyes: ClothingEyesGlassesSecurity ears: ClothingHeadsetSecurity pocket1: WeaponPistolMk58 diff --git a/Resources/Prototypes/Traits/quirks.yml b/Resources/Prototypes/Traits/quirks.yml index f51264e6bce..56fad4425f3 100644 --- a/Resources/Prototypes/Traits/quirks.yml +++ b/Resources/Prototypes/Traits/quirks.yml @@ -1,12 +1,5 @@ # If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly! -- type: trait - id: Pacifist - name: trait-pacifist-name - description: trait-pacifist-desc - category: Quirks - components: - - type: Pacified - type: trait id: LightweightDrunk diff --git a/Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml new file mode 100644 index 00000000000..2b779e8025d --- /dev/null +++ b/Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml @@ -0,0 +1,23 @@ +- type: entity + parent: [ClothingHeadHelmetArmoredBase, BaseSecurityContraband] + id: ClothingHeadHelmetHeavyDSS + name: reinforced helmet + description: An improved version of a regular Security helmet, equipped with a shatterproof visor. + components: + - type: Sprite + sprite: _WL/Clothing/Head/Helmets/secheavy.rsi + - type: Clothing + sprite: _WL/Clothing/Head/Helmets/secheavy.rsi + - type: Armor + modifiers: + coefficients: + Blunt: 0.85 + Slash: 0.85 + Piercing: 0.85 + Heat: 0.9 + - type: ExplosionResistance + damageCoefficient: 0.95 + - type: Tag + tags: + - WhitelistChameleon + - SecurityHelmet diff --git a/Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl b/Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl new file mode 100644 index 00000000000..9a66752705d --- /dev/null +++ b/Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl @@ -0,0 +1,14 @@ +uniform sampler2D SCREEN_TEXTURE; + +void fragment() { + highp vec4 color = zTextureSpec(SCREEN_TEXTURE, UV); + + highp mat3 m = mat3( + vec3(0.150, 0.150, 0.150), + vec3(0.350, 1.000, 0.350), + vec3(0.150, 0.050, 0.150) + ); + highp vec3 result = color.rgb * m; + + COLOR = vec4(result, 1.0); +} diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/meta.json b/Resources/Textures/Objects/Misc/photograph.rsi/meta.json index a76a07d169e..f0b5176c40d 100644 --- a/Resources/Textures/Objects/Misc/photograph.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/photograph.rsi/meta.json @@ -1,32 +1,40 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Made by ketufaispikinut (Github)", + "copyright": "Made by ketufaispikinut (Github). Inhands by whatston3 (Github)", "size": { "x": 32, "y": 32 }, "states": [ { - "name": "black" + "name": "black" }, { - "name": "red" + "name": "red" }, { - "name": "blue" + "name": "blue" }, { - "name": "green" + "name": "green" }, { - "name": "yellow" + "name": "yellow" }, { - "name": "purple" + "name": "purple" }, { - "name": "rainbow" + "name": "rainbow" + }, + { + "name": "paper-inhand-left", + "directions": 4 + }, + { + "name": "paper-inhand-right", + "directions": 4 } ] } diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-left.png b/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-left.png new file mode 100644 index 00000000000..2dc05baac70 Binary files /dev/null and b/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-left.png differ diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-right.png b/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-right.png new file mode 100644 index 00000000000..3ec584c65e4 Binary files /dev/null and b/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/empty.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/empty.png new file mode 100644 index 00000000000..7244b37f5c7 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/empty.png differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json index 22a98ff79e7..c998357ae32 100644 --- a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json +++ b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/meta.json @@ -1,23 +1,23 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "mmi textures made by Marum (https://github.com/marianolarre). Posibrain taken from tgstation at https://github.com/tgstation/tgstation/commit/cf45322c7ee16f9d7e43d5260daf24ceb77c1b25", + "copyright": "mmi textures made by Marum (https://github.com/marianolarre). Posibrain taken from tgstation at https://github.com/tgstation/tgstation/commit/cf45322c7ee16f9d7e43d5260daf24ceb77c1b25, posibrain-*/mmi-on/mmi-light edited by whatston3", "size": { "x": 32, "y": 32 }, "states": [ { - "name": "mmi_icon" + "name": "empty" }, { - "name": "mmi_off" + "name": "mmi_icon" }, { - "name": "mmi_alive" + "name": "mmi_off" }, { - "name": "mmi_dead" + "name": "mmi_on" }, { "name": "mmi_brain" @@ -25,6 +25,9 @@ { "name": "mmi_brain_alien" }, + { + "name": "mmi_light" + }, { "name": "posibrain" }, @@ -32,26 +35,11 @@ "name": "posibrain-occupied", "delays": [ [ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1 + 0.1, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, + 0.2, 0.1, 0.1, 0.1, + 0.1, 0.1, 0.1, 0.1, + 0.3 ] ] }, @@ -59,22 +47,8 @@ "name": "posibrain-searching", "delays": [ [ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1 + 0.7, 0.1, 0.1, 0.1, + 0.3, 0.1, 0.1, 0.1 ] ] } diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_alive.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_alive.png deleted file mode 100644 index 1869540a853..00000000000 Binary files a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_alive.png and /dev/null differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_dead.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_dead.png deleted file mode 100644 index 8baa79fa7a9..00000000000 Binary files a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_dead.png and /dev/null differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_light.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_light.png new file mode 100644 index 00000000000..eb563b46802 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_light.png differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_on.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_on.png new file mode 100644 index 00000000000..d93b3205ddd Binary files /dev/null and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_on.png differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-occupied.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-occupied.png index c4ea2b81775..2fdd54f8c9c 100644 Binary files a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-occupied.png and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-occupied.png differ diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-searching.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-searching.png index 026a2bc7c52..f0b62a41ba2 100644 Binary files a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-searching.png and b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-searching.png differ diff --git a/Resources/Textures/Shaders/nightvision.swsl b/Resources/Textures/Shaders/nightvision.swsl index aca60972de0..7bbd05571af 100644 --- a/Resources/Textures/Shaders/nightvision.swsl +++ b/Resources/Textures/Shaders/nightvision.swsl @@ -5,15 +5,50 @@ uniform sampler2D SCREEN_TEXTURE; uniform highp float noise_amount; // How much animated noise/static to add (0..1). uniform highp float noise_multiplier; // Scales the intensity of the added noise. +const highp vec3 coolTint = vec3(0.55, 0.90, 0.70); +const highp vec3 lumaWeights = vec3(0.299, 0.587, 0.114); +const highp float CompressStart = 0.55; +const highp float NightVisionBoost = 2.4; +const highp float NightVisionThreshold = 0.30; +const highp float NightVisionLift = 0.035; +const highp float CompressStrength = 0.65; +const highp float MaxBrightness = 1.0; + void fragment() { - highp vec4 color = zTextureSpec(SCREEN_TEXTURE, UV); + highp vec4 texColor = zTextureSpec(SCREEN_TEXTURE, UV); + highp vec3 finalColor = texColor.rgb; + + highp float brightness = dot(finalColor, lumaWeights); + highp float darkFactor = 1.0 - smoothstep(0.0, NightVisionThreshold, brightness); + + highp float boost = mix(1.0, NightVisionBoost, darkFactor); + finalColor *= boost; + + finalColor += coolTint * (NightVisionLift * darkFactor); + + highp float lum = dot(finalColor, lumaWeights); + highp float over = max(lum - CompressStart, 0.0); + highp float compressedOver = over / (1.0 + over * CompressStrength); + highp float newLum = (lum - over) + compressedOver; + finalColor *= newLum / max(lum, 0.0001); + + highp float peak = max(finalColor.r, max(finalColor.g, finalColor.b)); + if (peak > MaxBrightness) + finalColor *= MaxBrightness / peak; - // Add some noise for realism. - lowp float noise = zRandom(UV + sin(TIME / 10.0)).x * noise_amount; + highp float afterLum = dot(finalColor, lumaWeights); + highp vec3 cool = finalColor * coolTint; + highp float coolLum = max(dot(cool, lumaWeights), 0.0001); + cool *= afterLum / coolLum; + finalColor = mix(finalColor, cool, darkFactor * 0.25); - // Blend the generated noise into the image. - color.rgb += mix(color.rgb, color.rgb * noise, noise) * noise_multiplier; + // Add noise if enabled + if (noise_amount > 0.001) + { + lowp float noise = zRandom(UV + sin(TIME / 10.0)).x * noise_amount; + finalColor += finalColor * noise * noise_multiplier * 0.5; + } - COLOR = color; + COLOR = vec4(finalColor, texColor.a); } diff --git a/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json b/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json index b6aa4d49906..44d01160da2 100644 --- a/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json +++ b/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json @@ -1,56 +1,57 @@ { - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/338c826d1c3172eecd86d8430608669543dce0cc", - "size": { - "x": 32, - "y": 32 - }, - "states": [ - { - "name": "icon" + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/338c826d1c3172eecd86d8430608669543dce0cc", + "size": { + "x": 32, + "y": 32 }, - { - "name": "panel" - }, - { - "name": "unlit", - "delays": [ - [ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1 - ] - ] - }, - { - "name": "building", - "delays": [ - [ - 0.1, - 0.1, - 0.1, - 0.1 - ] - ] - }, - { - "name": "inserting", - "delays": [ - [ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1 - ] - ] - } - ] + "states": [ + { + "name": "icon" + }, + { + "name": "panel" + }, + { + "name": "unlit", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "building", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "inserting", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + } + ] } diff --git a/Resources/Textures/Structures/Machines/biofabricator.rsi/unlit.png b/Resources/Textures/Structures/Machines/biofabricator.rsi/unlit.png index 10cb4358779..4ac908b9efc 100644 Binary files a/Resources/Textures/Structures/Machines/biofabricator.rsi/unlit.png and b/Resources/Textures/Structures/Machines/biofabricator.rsi/unlit.png differ diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/equipped-HELMET.png b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/equipped-HELMET.png new file mode 100644 index 00000000000..cf0bb21fb1f Binary files /dev/null and b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/equipped-HELMET.png differ diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/icon.png b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/icon.png new file mode 100644 index 00000000000..2b5f8036084 Binary files /dev/null and b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/icon.png differ diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-left.png b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-left.png new file mode 100644 index 00000000000..8637a77cbd4 Binary files /dev/null and b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-left.png differ diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-right.png b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-right.png new file mode 100644 index 00000000000..0b35247d1ae Binary files /dev/null and b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-right.png differ diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json new file mode 100644 index 00000000000..888093b8647 --- /dev/null +++ b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by MureixIoL for CorvaxWL", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-HELMET", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +}