Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 210 additions & 9 deletions Content.Client/Chat/UI/SpeechBubble.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -38,6 +39,13 @@ public enum SpeechType : byte
/// </summary>
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

/// <summary>
/// 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.
Expand All @@ -55,6 +63,12 @@ public enum SpeechType : byte
/// The time at which this bubble will die.
/// </summary>
private TimeSpan _deathTime;
// Arcane-start
private readonly TimeSpan _creationTime;
private readonly TimeSpan _revealTime;
private readonly float _maxRevealWeight;
private readonly List<SpeechTextReveal> _textReveals = new();
// Arcane-end

public float VerticalOffset { get; set; }
private float _verticalOffsetAchieved;
Expand Down Expand Up @@ -95,15 +109,27 @@ 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);

ForceRunStyleUpdate();

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
Comment thread
fox76055 marked this conversation as resolved.
_verticalOffsetAchieved = -ContentSize.Y;
_deathTime = _timing.RealTime + TotalTime;
Comment thread
fox76055 marked this conversation as resolved.
// _deathTime = _timing.RealTime + TotalTime; Arcane delete
}

protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null);
Expand All @@ -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))
{
Expand Down Expand Up @@ -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

/// <summary>
/// Causes the speech bubble to start fading IMMEDIATELY.
/// </summary>
Expand Down Expand Up @@ -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)
{
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand All @@ -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
Expand All @@ -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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Закомментить, а не удалить.

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
Expand All @@ -295,9 +494,11 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl
VerticalAlignment = VAlignment.Top
};

var panel = new PanelContainer

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Закомментить, а не удалить.

var panel = new BoxContainer // Arcane var panel = new PanelContainer -> var panel = new BoxContainer
{
Children = { mainPanel, headerPanel }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Закомментить, а не удалить.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

переделал чуток коммент

Orientation = BoxContainer.LayoutOrientation.Vertical, // Arcane
HorizontalAlignment = HAlignment.Center, // Arcane
Children = { headerPanel, mainPanel }
};

return panel;
Expand Down
52 changes: 51 additions & 1 deletion Content.Client/CombatMode/CombatModeSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -21,14 +24,18 @@ public sealed partial class CombatModeSystem : SharedCombatModeSystem
/// Raised whenever combat mode changes.
/// </summary>
public event Action<bool>? LocalPlayerCombatModeUpdated;
private EntityQuery<SpriteComponent> _spriteQuery; // Europa

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

SubscribeLocalEvent<CombatModeComponent, AfterAutoHandleStateEvent>(OnHandleState);

SubscribeLocalEvent<CombatModeComponent, GetStatusIconsEvent>(UpdateCombatModeIndicator); // Europa
Subs.CVar(_cfg, CCVars.CombatModeIndicatorsPointShow, OnShowCombatIndicatorsChanged, true);
Subs.CVar(_cfg, CCVars.CombatIndicator, (bool value) => OnShowCombatIndicatorChanged(value), true); // Europa

_spriteQuery = GetEntityQuery<SpriteComponent>(); // Europa
}

private void OnHandleState(EntityUid uid, CombatModeComponent component, ref AfterAutoHandleStateEvent args)
Expand Down Expand Up @@ -91,4 +98,47 @@ private void OnShowCombatIndicatorsChanged(bool isShow)
_overlayManager.RemoveOverlay<CombatModeIndicatorsOverlay>();
}
}

// 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
}
Loading
Loading