diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 2284015a6cb..4372e5e80fb 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Client.Chat.Managers; +using System.Text; // Arcane using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Speech; @@ -38,6 +39,13 @@ public enum SpeechType : byte /// private static readonly TimeSpan FadeTime = TimeSpan.FromSeconds(0.25f); + // Arcane-start + private const float RevealRunesPerSecond = 15.5f; + private const float SpaceRevealWeight = 2.25f; + private static readonly TimeSpan MaxRevealTime = TimeSpan.FromSeconds(4); + protected virtual float RevealSpeedMultiplier => 1f; + // Arcane-end + /// /// The distance in world space to offset the speech bubble from the center of the entity. /// i.e. greater -> higher above the mob's head. @@ -55,6 +63,12 @@ public enum SpeechType : byte /// The time at which this bubble will die. /// private TimeSpan _deathTime; + // Arcane-start + private readonly TimeSpan _creationTime; + private readonly TimeSpan _revealTime; + private readonly float _maxRevealWeight; + private readonly List _textReveals = new(); + // Arcane-end public float VerticalOffset { get; set; } private float _verticalOffsetAchieved; @@ -95,6 +109,10 @@ public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechSt RectClipContent = true; var bubble = BuildBubble(message, speechStyleClass, fontColor); + // Arcane-start + bubble.HorizontalAlignment = HAlignment.Center; + bubble.VerticalAlignment = VAlignment.Bottom; + // Arcane-end AddChild(bubble); @@ -102,8 +120,16 @@ public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechSt bubble.Measure(Vector2Helpers.Infinity); ContentSize = bubble.DesiredSize; + // Arcane-start + bubble.MinWidth = ContentSize.X; + _creationTime = _timing.RealTime; + _maxRevealWeight = GetMaxRevealWeight(); + _revealTime = GetRevealTime(_maxRevealWeight); + _deathTime = _creationTime + TotalTime + _revealTime; + UpdateTextReveal(); + // Arcane-end _verticalOffsetAchieved = -ContentSize.Y; - _deathTime = _timing.RealTime + TotalTime; + // _deathTime = _timing.RealTime + TotalTime; Arcane delete } protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); @@ -120,6 +146,10 @@ protected override void FrameUpdate(FrameEventArgs args) return; } + // Arcane-start + UpdateTextReveal(); + // Arcane-end + // Lerp to our new vertical offset if it's been modified. if (MathHelper.CloseToPercent(_verticalOffsetAchieved - VerticalOffset, 0, 0.1)) { @@ -175,6 +205,173 @@ private void Die() OnDied?.Invoke(_senderEntity, this); } + // Arcane-start + protected void SetRevealedMessage(RichTextLabel label, FormattedMessage message) + { + label.SetMessage(message); + + var revealWeight = CountRevealWeight(message); + if (revealWeight <= 0f) + return; + + _textReveals.Add(new SpeechTextReveal(label, message, revealWeight)); + } + + private float GetMaxRevealWeight() + { + var revealWeight = 0f; + + foreach (var reveal in _textReveals) + { + revealWeight = Math.Max(revealWeight, reveal.RevealWeight); + } + + return revealWeight; + } + + private TimeSpan GetRevealTime(float revealWeight) + { + if (revealWeight <= 0f) + return TimeSpan.Zero; + + var seconds = revealWeight / (RevealRunesPerSecond * RevealSpeedMultiplier); + return TimeSpan.FromSeconds(MathF.Min(seconds, (float) MaxRevealTime.TotalSeconds)); + } + + private void UpdateTextReveal() + { + if (_textReveals.Count == 0) + return; + + var progress = _revealTime <= TimeSpan.Zero + ? 1f + : MathHelper.Clamp((float) ((_timing.RealTime - _creationTime).TotalSeconds / _revealTime.TotalSeconds), 0f, 1f); + + var visibleWeight = _maxRevealWeight * progress; + + foreach (var reveal in _textReveals) + { + var visibleRunes = CountVisibleRunes(reveal.Message, Math.Min(visibleWeight, reveal.RevealWeight)); + if (visibleRunes == reveal.LastVisibleRunes) + continue; + + reveal.LastVisibleRunes = visibleRunes; + reveal.Label.SetMessage(CreateRevealedMessage(reveal.Message, visibleRunes)); + } + } + + private static float CountRevealWeight(FormattedMessage message) + { + var weight = 0f; + + foreach (var node in message.Nodes) + { + if (node.Name != null || node.Value.StringValue == null) + continue; + + foreach (var rune in node.Value.StringValue.EnumerateRunes()) + { + weight += GetRevealWeight(rune); + } + } + + return weight; + } + + private static int CountVisibleRunes(FormattedMessage message, float visibleWeight) + { + var remaining = visibleWeight; + var count = 0; + + foreach (var node in message.Nodes) + { + if (node.Name != null) + continue; + + var text = node.Value.StringValue; + if (text == null || remaining <= 0f) + continue; + + foreach (var rune in text.EnumerateRunes()) + { + if (remaining <= 0f) + break; + + count++; + remaining -= GetRevealWeight(rune); + } + } + + return count; + } + + private static FormattedMessage CreateRevealedMessage(FormattedMessage message, int visibleRunes) + { + var result = new FormattedMessage(message.Count); + var remaining = visibleRunes; + + foreach (var node in message.Nodes) + { + if (node.Name != null) + { + result.AddMarkupOrThrow(node.ToString()); + continue; + } + + var text = node.Value.StringValue; + if (text == null) + continue; + + AddRevealedText(result, text, ref remaining); + } + + return result; + } + + private static void AddRevealedText(FormattedMessage result, string text, ref int remainingVisibleRunes) + { + var visible = new StringBuilder(); + var hidden = new StringBuilder(); + + foreach (var rune in text.EnumerateRunes()) + { + if (remainingVisibleRunes > 0) + { + visible.Append(rune); + remainingVisibleRunes--; + continue; + } + + hidden.Append(rune); + } + + if (visible.Length > 0) + result.AddText(visible.ToString()); + + if (hidden.Length == 0) + return; + + result.PushColor(Color.Transparent); + result.AddText(hidden.ToString()); + result.Pop(); + } + + private static float GetRevealWeight(Rune rune) + { + return rune.Value is ' ' or '\n' or '\t' + ? SpaceRevealWeight + : 1f; + } + + private sealed class SpeechTextReveal(RichTextLabel label, FormattedMessage message, float revealWeight) + { + public readonly RichTextLabel Label = label; + public readonly FormattedMessage Message = message; + public readonly float RevealWeight = revealWeight; + public int LastVisibleRunes = -1; + } + // Arcane-end + /// /// Causes the speech bubble to start fading IMMEDIATELY. /// @@ -204,6 +401,8 @@ protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, public sealed class TextSpeechBubble : SpeechBubble { + protected override float RevealSpeedMultiplier => 3f; // Arcane + public TextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) : base(message, senderEntity, speechStyleClass, fontColor) { @@ -216,7 +415,7 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl MaxWidth = SpeechMaxWidth, }; - label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor)); + SetRevealedMessage(label, FormatSpeech(message.WrappedMessage, fontColor)); // Arcane label.SetMessage(FormatSpeech(message.WrappedMessage, fontColor)); -> SetRevealedMessage(label, FormatSpeech(message.WrappedMessage, fontColor)); var panel = new PanelContainer { @@ -246,7 +445,7 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl MaxWidth = SpeechMaxWidth }; - label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + SetRevealedMessage(label, ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); // Arcane label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); -> SetRevealedMessage(label, ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); var unfanciedPanel = new PanelContainer { @@ -267,13 +466,13 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl { ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleTextOpacity)), MaxWidth = SpeechMaxWidth, - Margin = new Thickness(2, 6, 2, 2), + Margin = new Thickness(2, 2, 2, 2), // LuaM Margin = new Thickness(2, 6, 2, 2), -> Margin = new Thickness(2, 2, 2, 2), StyleClasses = { "bubbleContent" }, }; //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 bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor)); - bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + SetRevealedMessage(bubbleContent, ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); // Arcane bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); -> SetRevealedMessage(bubbleContent, 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 @@ -282,8 +481,8 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl Children = { bubbleContent }, ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)), HorizontalAlignment = HAlignment.Center, - VerticalAlignment = VAlignment.Bottom, - Margin = new Thickness(4, 14, 4, 2) + // VerticalAlignment = VAlignment.Bottom, Arcane delete + Margin = new Thickness(4, 0, 4, 2) // Arcane Margin = new Thickness(4, 14, 4, 2) -> Margin = new Thickness(4, 0, 4, 2) }; var headerPanel = new PanelContainer @@ -295,9 +494,11 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl VerticalAlignment = VAlignment.Top }; - var panel = new PanelContainer + var panel = new BoxContainer // Arcane var panel = new PanelContainer -> var panel = new BoxContainer { - Children = { mainPanel, headerPanel } + Orientation = BoxContainer.LayoutOrientation.Vertical, // Arcane + HorizontalAlignment = HAlignment.Center, // Arcane + Children = { headerPanel, mainPanel } }; return panel; diff --git a/Content.Client/CombatMode/CombatModeSystem.cs b/Content.Client/CombatMode/CombatModeSystem.cs index a364f107aeb..0a17a49f083 100644 --- a/Content.Client/CombatMode/CombatModeSystem.cs +++ b/Content.Client/CombatMode/CombatModeSystem.cs @@ -2,10 +2,13 @@ using Content.Client.NPC.HTN; using Content.Shared.CCVar; using Content.Shared.CombatMode; +using Content.Shared.StatusIcon.Components; // Europa +using Robust.Client.GameObjects; // Europa using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; using Robust.Shared.Configuration; +using Robust.Shared.Utility; // Europa namespace Content.Client.CombatMode; @@ -21,14 +24,18 @@ public sealed partial class CombatModeSystem : SharedCombatModeSystem /// Raised whenever combat mode changes. /// public event Action? LocalPlayerCombatModeUpdated; + private EntityQuery _spriteQuery; // Europa public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnHandleState); - + SubscribeLocalEvent(UpdateCombatModeIndicator); // Europa Subs.CVar(_cfg, CCVars.CombatModeIndicatorsPointShow, OnShowCombatIndicatorsChanged, true); + Subs.CVar(_cfg, CCVars.CombatIndicator, (bool value) => OnShowCombatIndicatorChanged(value), true); // Europa + + _spriteQuery = GetEntityQuery(); // Europa } private void OnHandleState(EntityUid uid, CombatModeComponent component, ref AfterAutoHandleStateEvent args) @@ -91,4 +98,47 @@ private void OnShowCombatIndicatorsChanged(bool isShow) _overlayManager.RemoveOverlay(); } } + + // Europa-Start + private bool _combatIndicatorEnabled = false; + + private void OnShowCombatIndicatorChanged(bool value) + { + _combatIndicatorEnabled = value; + } + + private void UpdateCombatModeIndicator(EntityUid uid, CombatModeComponent comp, ref GetStatusIconsEvent _) + { + if (!_combatIndicatorEnabled) + { + if (_spriteQuery.TryComp(uid, out var sprite) && sprite.LayerMapTryGet("combat_mode_indicator", out var layerToRemove)) + { + sprite.RemoveLayer(layerToRemove); + } + return; + } + + if (comp.IsInCombatMode) + { + if (!_spriteQuery.TryComp(uid, out var sprite)) + return; + + if (!sprite.LayerMapTryGet("combat_mode_indicator", out var layer)) + { + if (!_spriteQuery.TryComp(uid, out var sprite2)) + return; + + layer = sprite2.AddLayer(new SpriteSpecifier.Rsi(new ResPath("_Europa/Effects/combat_mode.rsi"), "combat_mode")); + sprite2.LayerMapSet("combat_mode_indicator", layer); + } + } + else + { + if (_spriteQuery.TryComp(uid, out var sprite) && sprite.LayerMapTryGet("combat_mode_indicator", out var layerToRemove)) + { + sprite.RemoveLayer(layerToRemove); + } + } + } + // Europa-End } diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs index 09473ea0cfc..962d2237665 100644 --- a/Content.Client/DoAfter/DoAfterOverlay.cs +++ b/Content.Client/DoAfter/DoAfterOverlay.cs @@ -23,6 +23,7 @@ public sealed class DoAfterOverlay : Overlay private readonly SharedContainerSystem _container; private readonly Texture _barTexture; + private readonly SpriteSpecifier _cogTexture; // Europa private readonly ShaderInstance _unshadedShader; /// @@ -47,6 +48,7 @@ public DoAfterOverlay(IEntityManager entManager, IPrototypeManager protoManager, _progressColor = _entManager.System(); var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon"); _barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite); + _cogTexture = new SpriteSpecifier.Rsi(new("/Textures/Backmen/Interface/Misc/progress_cog.rsi"), "cog"); // Europa _unshadedShader = protoManager.Index("unshaded").Instance(); } @@ -126,8 +128,14 @@ protected override void Draw(in OverlayDrawArgs args) var position = new Vector2(-_barTexture.Width / 2f / EyeManager.PixelsPerMeter, yOffset / scale + offset / EyeManager.PixelsPerMeter * scale); + // Europa-Start + var cogPos = new Vector2(position.X + _barTexture.Width / scale / EyeManager.PixelsPerMeter, position.Y + _barTexture.Height * 2 / scale) / EyeManager.PixelsPerMeter; + var cogTexture = _entManager.System().GetFrame(_cogTexture, curTime); + // Europa-End + // Draw the underlying bar texture handle.DrawTexture(_barTexture, position); + handle.DrawTexture(cogTexture, cogPos); // Europa Color color; float elapsedRatio; diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml index 4e1508f710b..2ae993d4e48 100644 --- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml +++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml @@ -4,6 +4,7 @@ + diff --git a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs index 7ae16b77929..52592f5d856 100644 --- a/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs @@ -14,6 +14,7 @@ public AccessibilityTab() Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox); Control.AddOptionCheckBox(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox); + Control.AddOptionCheckBox(CCVars.CombatIndicator, CombatIndicatorCheckBox); // Europa Control.AddOptionCheckBox(CCVars.ReducedMotion, ReducedMotionCheckBox); Control.AddOptionPercentSlider(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider); Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider); diff --git a/Content.Shared/CombatMode/SharedCombatModeSystem.cs b/Content.Shared/CombatMode/SharedCombatModeSystem.cs index d96e1371d63..95a3054d05e 100644 --- a/Content.Shared/CombatMode/SharedCombatModeSystem.cs +++ b/Content.Shared/CombatMode/SharedCombatModeSystem.cs @@ -1,5 +1,8 @@ using Content.Shared.Actions; +using Content.Shared.Bed.Sleep; // Europa using Content.Shared.Mind; +using Content.Shared.Mobs; +using Content.Shared.Mobs.Systems; // Europa using Content.Shared.MouseRotator; using Content.Shared.Movement.Components; using Content.Shared.Popups; @@ -11,9 +14,10 @@ namespace Content.Shared.CombatMode; public abstract partial class SharedCombatModeSystem : EntitySystem { [Dependency] protected IGameTiming Timing = default!; - [Dependency] private SharedActionsSystem _actionsSystem = default!; - [Dependency] private SharedPopupSystem _popup = default!; - [Dependency] private SharedMindSystem _mind = default!; + [Dependency] private SharedActionsSystem _actionsSystem = default!; + [Dependency] private SharedPopupSystem _popup = default!; + [Dependency] private SharedMindSystem _mind = default!; + [Dependency] private MobStateSystem _mobState = default!; // Europa public override void Initialize() { @@ -45,8 +49,8 @@ private void OnActionPerform(EntityUid uid, CombatModeComponent component, Toggl args.Handled = true; SetInCombatMode(uid, !component.IsInCombatMode, component); - var msg = component.IsInCombatMode ? "action-popup-combat-enabled" : "action-popup-combat-disabled"; - _popup.PopupClient(Loc.GetString(msg), args.Performer, args.Performer); +// var msg = component.IsInCombatMode ? "action-popup-combat-enabled" : "action-popup-combat-disabled"; // Europa-Remove +// _popup.PopupClient(Loc.GetString(msg), args.Performer, args.Performer); // Europa-Remove } public void SetCanDisarm(EntityUid entity, bool canDisarm, CombatModeComponent? component = null) @@ -70,6 +74,14 @@ public virtual void SetInCombatMode(EntityUid entity, bool value, CombatModeComp if (component.IsInCombatMode == value) return; + // Europa-Start | Dont let entity gone postal when unconscious + if (_mobState.IsDead(entity) || _mobState.IsCritical(entity) || HasComp(entity)) + { + if (value) + return; + } + // Europa-End + component.IsInCombatMode = value; Dirty(entity, component); diff --git a/Content.Shared/_Europa/CCVar/CCVars.Accessibility.cs b/Content.Shared/_Europa/CCVar/CCVars.Accessibility.cs new file mode 100644 index 00000000000..0b4a3928dfd --- /dev/null +++ b/Content.Shared/_Europa/CCVar/CCVars.Accessibility.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared.CCVar; + +public sealed partial class CCVars +{ + /// + /// When false - dont show combat indicator. + /// + public static readonly CVarDef CombatIndicator = + CVarDef.Create("accessibility.CombatIndicator", true, CVar.CLIENTONLY | CVar.ARCHIVE); +} diff --git a/Resources/Locale/en-US/_europa/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/_europa/escape-menu/ui/options-menu.ftl new file mode 100644 index 00000000000..58649621aab --- /dev/null +++ b/Resources/Locale/en-US/_europa/escape-menu/ui/options-menu.ftl @@ -0,0 +1 @@ +ui-options-combat-indicator = Show combat mode indicator diff --git a/Resources/Locale/ru-RU/_europa/escape-menu/ui/options-menu.ftl b/Resources/Locale/ru-RU/_europa/escape-menu/ui/options-menu.ftl new file mode 100644 index 00000000000..e46c6d02bcf --- /dev/null +++ b/Resources/Locale/ru-RU/_europa/escape-menu/ui/options-menu.ftl @@ -0,0 +1 @@ +ui-options-combat-indicator = Отображение индикатора боевого режима diff --git a/Resources/Textures/Backmen/Interface/Misc/progress_cog.rsi/cog.png b/Resources/Textures/Backmen/Interface/Misc/progress_cog.rsi/cog.png new file mode 100644 index 00000000000..4d098e2717b Binary files /dev/null and b/Resources/Textures/Backmen/Interface/Misc/progress_cog.rsi/cog.png differ diff --git a/Resources/Textures/Backmen/Interface/Misc/progress_cog.rsi/meta.json b/Resources/Textures/Backmen/Interface/Misc/progress_cog.rsi/meta.json new file mode 100644 index 00000000000..df81cc1146e --- /dev/null +++ b/Resources/Textures/Backmen/Interface/Misc/progress_cog.rsi/meta.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/tgstation/tgstation/blob/886ca0f8dddf83ecaf10c92ff106172722352192/icons/effects/progessbar.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "cog", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + } + ] +} diff --git a/Resources/Textures/_Europa/Effects/combat_mode.rsi/combat_mode.png b/Resources/Textures/_Europa/Effects/combat_mode.rsi/combat_mode.png new file mode 100644 index 00000000000..40c2444eb50 Binary files /dev/null and b/Resources/Textures/_Europa/Effects/combat_mode.rsi/combat_mode.png differ diff --git a/Resources/Textures/_Europa/Effects/combat_mode.rsi/meta.json b/Resources/Textures/_Europa/Effects/combat_mode.rsi/meta.json new file mode 100644 index 00000000000..7264508ae9b --- /dev/null +++ b/Resources/Textures/_Europa/Effects/combat_mode.rsi/meta.json @@ -0,0 +1,30 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/BlueMoon-Labs/MOLOT-BlueMoon-Station/blob/master/modular_sand/icons/mob/combat_indicator.dmi | Edited by PuroSlavKing (Github)", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "combat_mode", + "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 + ] + ] + } + ] +}