Move do_afters to an overlay (#10463)
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using Content.Client.DoAfter.UI;
|
||||
using Content.Client.DoAfter;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.CharacterInfo
|
||||
{
|
||||
@@ -14,7 +12,7 @@ namespace Content.Client.CharacterInfo
|
||||
{
|
||||
var dims = Texture != null ? GetDrawDimensions(Texture) : UIBox2.FromDimensions(Vector2.Zero, PixelSize);
|
||||
dims.Top = Math.Max(dims.Bottom - dims.Bottom * Progress,0);
|
||||
handle.DrawRect(dims, DoAfterHelpers.GetProgressColor(Progress));
|
||||
handle.DrawRect(dims, DoAfterOverlay.GetProgressColor(Progress));
|
||||
|
||||
base.Draw(handle);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Client.DoAfter.UI;
|
||||
using Content.Shared.DoAfter;
|
||||
|
||||
namespace Content.Client.DoAfter
|
||||
@@ -8,8 +7,6 @@ namespace Content.Client.DoAfter
|
||||
{
|
||||
public readonly Dictionary<byte, ClientDoAfter> DoAfters = new();
|
||||
|
||||
public readonly List<(TimeSpan CancelTime, ClientDoAfter Message)> CancelledDoAfters = new();
|
||||
|
||||
public DoAfterGui? Gui { get; set; }
|
||||
public readonly Dictionary<byte, ClientDoAfter> CancelledDoAfters = new();
|
||||
}
|
||||
}
|
||||
|
||||
141
Content.Client/DoAfter/DoAfterOverlay.cs
Normal file
141
Content.Client/DoAfter/DoAfterOverlay.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using Content.Client.Resources;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.DoAfter;
|
||||
|
||||
public sealed class DoAfterOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
private readonly SharedTransformSystem _transform;
|
||||
|
||||
private Texture _barTexture;
|
||||
private ShaderInstance _shader;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
|
||||
public DoAfterOverlay()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_transform = _entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
|
||||
_barTexture = IoCManager.Resolve<IResourceCache>()
|
||||
.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png");
|
||||
|
||||
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
|
||||
}
|
||||
|
||||
protected override void Draw(in OverlayDrawArgs args)
|
||||
{
|
||||
var handle = args.WorldHandle;
|
||||
var rotation = args.Viewport.Eye?.Rotation ?? Angle.Zero;
|
||||
var spriteQuery = _entManager.GetEntityQuery<SpriteComponent>();
|
||||
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
var scale = _configManager.GetCVar(CVars.DisplayUIScale);
|
||||
var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale));
|
||||
var rotationMatrix = Matrix3.CreateRotation(-rotation);
|
||||
handle.UseShader(_shader);
|
||||
|
||||
// TODO: Need active DoAfter component (or alternatively just make DoAfter itself active)
|
||||
foreach (var comp in _entManager.EntityQuery<DoAfterComponent>(true))
|
||||
{
|
||||
if (comp.DoAfters.Count == 0 ||
|
||||
!xformQuery.TryGetComponent(comp.Owner, out var xform) ||
|
||||
xform.MapID != args.MapId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var worldPosition = _transform.GetWorldPosition(xform);
|
||||
|
||||
if (!args.WorldAABB.Contains(worldPosition))
|
||||
continue;
|
||||
|
||||
var index = 0;
|
||||
var worldMatrix = Matrix3.CreateTranslation(worldPosition);
|
||||
|
||||
foreach (var (_, doAfter) in comp.DoAfters)
|
||||
{
|
||||
var elapsed = doAfter.Accumulator;
|
||||
var displayRatio = MathF.Min(1.0f,
|
||||
elapsed / doAfter.Delay);
|
||||
|
||||
Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
|
||||
Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
|
||||
|
||||
handle.SetTransform(matty);
|
||||
var offset = _barTexture.Height / scale * index;
|
||||
|
||||
// Use the sprite itself if we know its bounds. This means short or tall sprites don't get overlapped
|
||||
// by the bar.
|
||||
float yOffset;
|
||||
if (spriteQuery.TryGetComponent(comp.Owner, out var sprite))
|
||||
{
|
||||
yOffset = sprite.Bounds.Height / 2f + 0.05f;
|
||||
}
|
||||
else
|
||||
{
|
||||
yOffset = 0.5f;
|
||||
}
|
||||
|
||||
// Position above the entity (we've already applied the matrix transform to the entity itself)
|
||||
// Offset by the texture size for every do_after we have.
|
||||
var position = new Vector2(-_barTexture.Width / 2f / EyeManager.PixelsPerMeter,
|
||||
yOffset / scale + offset / EyeManager.PixelsPerMeter * scale);
|
||||
|
||||
// Draw the underlying bar texture
|
||||
handle.DrawTexture(_barTexture, position);
|
||||
|
||||
// Draw the bar itself
|
||||
var cancelled = doAfter.Cancelled;
|
||||
Color color;
|
||||
const float flashTime = 0.125f;
|
||||
|
||||
// if we're cancelled then flick red / off.
|
||||
if (cancelled)
|
||||
{
|
||||
var flash = Math.Floor(doAfter.CancelledAccumulator / flashTime) % 2 == 0;
|
||||
color = new Color(1f, 0f, 0f, flash ? 1f : 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
color = GetProgressColor(displayRatio);
|
||||
}
|
||||
|
||||
// Hardcoded width of the progress bar because it doesn't match the texture.
|
||||
const float startX = 2f;
|
||||
const float endX = 22f;
|
||||
|
||||
var xProgress = (endX - startX) * displayRatio + startX;
|
||||
|
||||
var box = new Box2(new Vector2(startX, 3f) / EyeManager.PixelsPerMeter, new Vector2(xProgress, 4f) / EyeManager.PixelsPerMeter);
|
||||
box = box.Translated(position);
|
||||
handle.DrawRect(box, color);
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
handle.UseShader(null);
|
||||
handle.SetTransform(Matrix3.Identity);
|
||||
}
|
||||
|
||||
public static Color GetProgressColor(float progress)
|
||||
{
|
||||
if (progress >= 1.0f)
|
||||
{
|
||||
return new Color(0f, 1f, 0f);
|
||||
}
|
||||
// lerp
|
||||
var hue = (5f / 18f) * progress;
|
||||
return Color.FromHsv((hue, 1f, 0.75f, 1f));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
using System.Linq;
|
||||
using Content.Client.DoAfter.UI;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Examine;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -20,18 +18,8 @@ namespace Content.Client.DoAfter
|
||||
[UsedImplicitly]
|
||||
public sealed class DoAfterSystem : EntitySystem
|
||||
{
|
||||
/*
|
||||
* How this is currently setup (client-side):
|
||||
* DoAfterGui handles the actual bars displayed above heads. It also uses FrameUpdate to flash cancellations
|
||||
* DoAfterEntitySystem handles checking predictions every tick as well as removing / cancelling DoAfters due to time elapsed.
|
||||
* DoAfterComponent handles network messages inbound as well as storing the DoAfter data.
|
||||
* It'll also handle overall cleanup when one is removed (i.e. removing it from DoAfterGui).
|
||||
*/
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
|
||||
|
||||
/// <summary>
|
||||
/// We'll use an excess time so stuff like finishing effects can show.
|
||||
@@ -43,9 +31,14 @@ namespace Content.Client.DoAfter
|
||||
base.Initialize();
|
||||
UpdatesOutsidePrediction = true;
|
||||
SubscribeNetworkEvent<CancelledDoAfterMessage>(OnCancelledDoAfter);
|
||||
SubscribeLocalEvent<DoAfterComponent, ComponentStartup>(OnDoAfterStartup);
|
||||
SubscribeLocalEvent<DoAfterComponent, ComponentShutdown>(OnDoAfterShutdown);
|
||||
SubscribeLocalEvent<DoAfterComponent, ComponentHandleState>(OnDoAfterHandleState);
|
||||
IoCManager.Resolve<IOverlayManager>().AddOverlay(new DoAfterOverlay());
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
IoCManager.Resolve<IOverlayManager>().RemoveOverlay<DoAfterOverlay>();
|
||||
}
|
||||
|
||||
private void OnDoAfterHandleState(EntityUid uid, DoAfterComponent component, ref ComponentHandleState args)
|
||||
@@ -86,24 +79,6 @@ namespace Content.Client.DoAfter
|
||||
|
||||
component.DoAfters.Add(doAfter.ID, doAfter);
|
||||
}
|
||||
|
||||
if (component.Gui == null || component.Gui.Disposed)
|
||||
return;
|
||||
|
||||
foreach (var (_, doAfter) in component.DoAfters)
|
||||
{
|
||||
component.Gui.AddDoAfter(doAfter);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDoAfterStartup(EntityUid uid, DoAfterComponent component, ComponentStartup args)
|
||||
{
|
||||
Enable(component);
|
||||
}
|
||||
|
||||
private void OnDoAfterShutdown(EntityUid uid, DoAfterComponent component, ComponentShutdown args)
|
||||
{
|
||||
Disable(component);
|
||||
}
|
||||
|
||||
private void OnCancelledDoAfter(CancelledDoAfterMessage ev)
|
||||
@@ -113,33 +88,6 @@ namespace Content.Client.DoAfter
|
||||
Cancel(doAfter, ev.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For handling PVS so we dispose of controls if they go out of range
|
||||
/// </summary>
|
||||
public void Enable(DoAfterComponent component)
|
||||
{
|
||||
if (component.Gui?.Disposed == false)
|
||||
return;
|
||||
|
||||
component.Gui = new DoAfterGui {AttachedEntity = component.Owner};
|
||||
|
||||
foreach (var (_, doAfter) in component.DoAfters)
|
||||
{
|
||||
component.Gui.AddDoAfter(doAfter);
|
||||
}
|
||||
|
||||
foreach (var (_, cancelled) in component.CancelledDoAfters)
|
||||
{
|
||||
component.Gui.CancelDoAfter(cancelled.ID);
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable(DoAfterComponent component)
|
||||
{
|
||||
component.Gui?.Dispose();
|
||||
component.Gui = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a DoAfter without showing a cancellation graphic.
|
||||
/// </summary>
|
||||
@@ -150,22 +98,10 @@ namespace Content.Client.DoAfter
|
||||
|
||||
var found = false;
|
||||
|
||||
for (var i = component.CancelledDoAfters.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var cancelled = component.CancelledDoAfters[i];
|
||||
|
||||
if (cancelled.Message == clientDoAfter)
|
||||
{
|
||||
component.CancelledDoAfters.RemoveAt(i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
component.CancelledDoAfters.Remove(clientDoAfter.ID);
|
||||
|
||||
if (!found)
|
||||
component.DoAfters.Remove(clientDoAfter.ID);
|
||||
|
||||
component.Gui?.RemoveDoAfter(clientDoAfter.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -174,98 +110,70 @@ namespace Content.Client.DoAfter
|
||||
/// Actual removal is handled by DoAfterEntitySystem.
|
||||
public void Cancel(DoAfterComponent component, byte id)
|
||||
{
|
||||
foreach (var (_, cancelled) in component.CancelledDoAfters)
|
||||
{
|
||||
if (cancelled.ID == id)
|
||||
return;
|
||||
}
|
||||
if (component.CancelledDoAfters.ContainsKey(id))
|
||||
return;
|
||||
|
||||
if (!component.DoAfters.ContainsKey(id))
|
||||
return;
|
||||
|
||||
var doAfterMessage = component.DoAfters[id];
|
||||
component.CancelledDoAfters.Add((_gameTiming.CurTime, doAfterMessage));
|
||||
component.Gui?.CancelDoAfter(id);
|
||||
doAfterMessage.Cancelled = true;
|
||||
component.CancelledDoAfters.Add(id, doAfterMessage);
|
||||
}
|
||||
|
||||
// TODO move this to an overlay
|
||||
// TODO separate DoAfter & ActiveDoAfter components for the entity query.
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
var attached = _player.LocalPlayer?.ControlledEntity;
|
||||
|
||||
// Can't see any I guess?
|
||||
if (attached == null || Deleted(attached))
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
// ReSharper disable once ConvertToLocalFunction
|
||||
var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data)
|
||||
=> uid == data.compOwner || uid == data.attachedEntity;
|
||||
var playerEntity = _player.LocalPlayer?.ControlledEntity;
|
||||
|
||||
var occluded = _examineSystem.IsOccluded(attached.Value);
|
||||
var viewbox = _eyeManager.GetWorldViewport().Enlarged(2.0f);
|
||||
var xforms = GetEntityQuery<TransformComponent>();
|
||||
var entXform = xforms.GetComponent(attached.Value);
|
||||
var playerPos = _xformSystem.GetWorldPosition(entXform, xforms);
|
||||
|
||||
foreach (var (comp, xform) in EntityManager.EntityQuery<DoAfterComponent, TransformComponent>(true))
|
||||
foreach (var (comp, xform) in EntityQuery<DoAfterComponent, TransformComponent>())
|
||||
{
|
||||
var doAfters = comp.DoAfters;
|
||||
|
||||
if (doAfters.Count == 0 || xform.MapID != entXform.MapID)
|
||||
if (doAfters.Count == 0)
|
||||
{
|
||||
Disable(comp);
|
||||
continue;
|
||||
}
|
||||
|
||||
var compPos = _xformSystem.GetWorldPosition(xform, xforms);
|
||||
|
||||
if (!viewbox.Contains(compPos))
|
||||
{
|
||||
Disable(comp);
|
||||
continue;
|
||||
}
|
||||
|
||||
var range = (compPos - playerPos).Length + 0.01f;
|
||||
|
||||
if (occluded &&
|
||||
comp.Owner != attached &&
|
||||
// Static ExamineSystemShared.InRangeUnOccluded has to die.
|
||||
!ExamineSystemShared.InRangeUnOccluded(
|
||||
new(playerPos, entXform.MapID),
|
||||
new(compPos, entXform.MapID), range,
|
||||
(comp.Owner, attached), predicate,
|
||||
entMan: EntityManager))
|
||||
{
|
||||
Disable(comp);
|
||||
continue;
|
||||
}
|
||||
|
||||
Enable(comp);
|
||||
|
||||
var userGrid = xform.Coordinates;
|
||||
var toRemove = new RemQueue<ClientDoAfter>();
|
||||
|
||||
// Check cancellations / finishes
|
||||
foreach (var (id, doAfter) in doAfters)
|
||||
{
|
||||
var elapsedTime = (currentTime - doAfter.StartTime).TotalSeconds;
|
||||
|
||||
// If we've passed the final time (after the excess to show completion graphic) then remove.
|
||||
if (elapsedTime > doAfter.Delay + ExcessTime)
|
||||
if ((doAfter.Accumulator + doAfter.CancelledAccumulator) > doAfter.Delay + ExcessTime)
|
||||
{
|
||||
toRemove.Add(doAfter);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't predict cancellation if it's already finished.
|
||||
if (elapsedTime > doAfter.Delay)
|
||||
if (doAfter.Cancelled)
|
||||
{
|
||||
doAfter.CancelledAccumulator += frameTime;
|
||||
continue;
|
||||
}
|
||||
|
||||
doAfter.Accumulator += frameTime;
|
||||
|
||||
// Well we finished so don't try to predict cancels.
|
||||
if (doAfter.Accumulator > doAfter.Delay)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Predictions
|
||||
if (comp.Owner != playerEntity)
|
||||
continue;
|
||||
|
||||
// TODO: Add these back in when I work out some system for changing the accumulation rate
|
||||
// based on ping. Right now these would show as cancelled near completion if we moved at the end
|
||||
// despite succeeding.
|
||||
continue;
|
||||
|
||||
if (doAfter.BreakOnUserMove)
|
||||
{
|
||||
if (!userGrid.InRange(EntityManager, doAfter.UserGrid, doAfter.MovementThreshold))
|
||||
@@ -292,16 +200,21 @@ namespace Content.Client.DoAfter
|
||||
Remove(comp, doAfter);
|
||||
}
|
||||
|
||||
var count = comp.CancelledDoAfters.Count;
|
||||
// Remove cancelled DoAfters after ExcessTime has elapsed
|
||||
for (var i = count - 1; i >= 0; i--)
|
||||
var toRemoveCancelled = new List<ClientDoAfter>();
|
||||
|
||||
foreach (var (_, doAfter) in comp.CancelledDoAfters)
|
||||
{
|
||||
var cancelled = comp.CancelledDoAfters[i];
|
||||
if ((currentTime - cancelled.CancelTime).TotalSeconds > ExcessTime)
|
||||
if (doAfter.CancelledAccumulator > ExcessTime)
|
||||
{
|
||||
Remove(comp, cancelled.Message);
|
||||
toRemoveCancelled.Add(doAfter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var doAfter in toRemoveCancelled)
|
||||
{
|
||||
Remove(comp, doAfter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.DoAfter.UI
|
||||
{
|
||||
public sealed class DoAfterBar : Control
|
||||
{
|
||||
private IGameTiming _gameTiming = default!;
|
||||
|
||||
private readonly ShaderInstance _shader;
|
||||
|
||||
/// <summary>
|
||||
/// Set from 0.0f to 1.0f to reflect bar progress
|
||||
/// </summary>
|
||||
public float Ratio
|
||||
{
|
||||
get => _ratio;
|
||||
set => _ratio = value;
|
||||
}
|
||||
|
||||
private float _ratio = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Flash red until removed
|
||||
/// </summary>
|
||||
public bool Cancelled
|
||||
{
|
||||
get => _cancelled;
|
||||
set
|
||||
{
|
||||
if (_cancelled == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cancelled = value;
|
||||
if (_cancelled)
|
||||
{
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
_lastFlash = _gameTiming.CurTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool _cancelled;
|
||||
|
||||
/// <summary>
|
||||
/// Is the cancellation bar red?
|
||||
/// </summary>
|
||||
private bool _flash = true;
|
||||
|
||||
/// <summary>
|
||||
/// Last time we swapped the flash.
|
||||
/// </summary>
|
||||
private TimeSpan _lastFlash;
|
||||
|
||||
/// <summary>
|
||||
/// How long each cancellation bar flash lasts in seconds.
|
||||
/// </summary>
|
||||
private const float FlashTime = 0.125f;
|
||||
|
||||
private const int XPixelDiff = 20 * DoAfterBarScale;
|
||||
|
||||
public const byte DoAfterBarScale = 2;
|
||||
|
||||
public DoAfterBar()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_shader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("unshaded").Instance();
|
||||
VerticalAlignment = VAlignment.Center;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (Cancelled)
|
||||
{
|
||||
if ((_gameTiming.CurTime - _lastFlash).TotalSeconds > FlashTime)
|
||||
{
|
||||
_lastFlash = _gameTiming.CurTime;
|
||||
_flash = !_flash;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
Color color;
|
||||
|
||||
if (Cancelled)
|
||||
{
|
||||
if ((_gameTiming.CurTime - _lastFlash).TotalSeconds > FlashTime)
|
||||
{
|
||||
_lastFlash = _gameTiming.CurTime;
|
||||
_flash = !_flash;
|
||||
}
|
||||
|
||||
color = new Color(1.0f, 0.0f, 0.0f, _flash ? 1.0f : 0.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
color = DoAfterHelpers.GetProgressColor(Ratio);
|
||||
}
|
||||
|
||||
handle.UseShader(_shader);
|
||||
// If you want to make this less hard-coded be my guest
|
||||
var leftOffset = 2 * DoAfterBarScale;
|
||||
var box = new UIBox2i(
|
||||
leftOffset,
|
||||
-2 + 2 * DoAfterBarScale,
|
||||
leftOffset + (int) (XPixelDiff * Ratio * UIScale),
|
||||
-2);
|
||||
handle.DrawRect(box, color);
|
||||
handle.UseShader(null);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DoAfterHelpers
|
||||
{
|
||||
public static Color GetProgressColor(float progress)
|
||||
{
|
||||
if (progress >= 1.0f)
|
||||
{
|
||||
return new Color(0f, 1f, 0f);
|
||||
}
|
||||
// lerp
|
||||
var hue = (5f / 18f) * progress;
|
||||
return Color.FromHsv((hue, 1f, 0.75f, 1f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using Content.Client.Resources;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.DoAfter.UI;
|
||||
|
||||
public sealed class DoAfterControl : PanelContainer
|
||||
{
|
||||
public float Ratio
|
||||
{
|
||||
get => _bar.Ratio;
|
||||
set => _bar.Ratio = value;
|
||||
}
|
||||
|
||||
public bool Cancelled
|
||||
{
|
||||
get => _bar.Cancelled;
|
||||
set => _bar.Cancelled = value;
|
||||
}
|
||||
|
||||
private DoAfterBar _bar;
|
||||
|
||||
public DoAfterControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
var cache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
AddChild(new TextureRect
|
||||
{
|
||||
Texture = cache.GetTexture("/Textures/Interface/Misc/progress_bar.rsi/icon.png"),
|
||||
TextureScale = Vector2.One * DoAfterBar.DoAfterBarScale,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
});
|
||||
|
||||
_bar = new DoAfterBar();
|
||||
AddChild(_bar);
|
||||
VerticalAlignment = VAlignment.Bottom;
|
||||
_bar.Measure(Vector2.Infinity);
|
||||
Measure(Vector2.Infinity);
|
||||
}
|
||||
}
|
||||
@@ -1,176 +0,0 @@
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.DoAfter.UI
|
||||
{
|
||||
public sealed class DoAfterGui : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private readonly Dictionary<byte, DoAfterControl> _doAfterControls = new();
|
||||
|
||||
// We'll store cancellations for a little bit just so we can flash the graphic to indicate it's cancelled
|
||||
private readonly Dictionary<byte, TimeSpan> _cancelledDoAfters = new();
|
||||
|
||||
public EntityUid? AttachedEntity { get; set; }
|
||||
|
||||
public DoAfterGui()
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(this);
|
||||
SeparationOverride = 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (Disposed)
|
||||
return;
|
||||
|
||||
foreach (var (_, control) in _doAfterControls)
|
||||
{
|
||||
control.Dispose();
|
||||
}
|
||||
|
||||
_doAfterControls.Clear();
|
||||
_cancelledDoAfters.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the necessary control for a DoAfter progress bar.
|
||||
/// </summary>
|
||||
public void AddDoAfter(ClientDoAfter message)
|
||||
{
|
||||
if (_doAfterControls.ContainsKey(message.ID))
|
||||
return;
|
||||
|
||||
var doAfterBar = new DoAfterControl();
|
||||
AddChild(doAfterBar);
|
||||
_doAfterControls.Add(message.ID, doAfterBar);
|
||||
Measure(Vector2.Infinity);
|
||||
}
|
||||
|
||||
// NOTE THAT THE BELOW ONLY HANDLES THE UI SIDE
|
||||
|
||||
/// <summary>
|
||||
/// Removes a DoAfter without showing a cancel graphic.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
public void RemoveDoAfter(byte id)
|
||||
{
|
||||
if (!_doAfterControls.ContainsKey(id))
|
||||
return;
|
||||
|
||||
var control = _doAfterControls[id];
|
||||
RemoveChild(control);
|
||||
control.DisposeAllChildren();
|
||||
_doAfterControls.Remove(id);
|
||||
_cancelledDoAfters.Remove(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a DoAfter and shows a graphic indicating it has been cancelled to the player.
|
||||
/// </summary>
|
||||
/// Can be called multiple times on the 1 DoAfter because of the client predicting the cancellation.
|
||||
/// <param name="id"></param>
|
||||
public void CancelDoAfter(byte id)
|
||||
{
|
||||
if (_cancelledDoAfters.ContainsKey(id))
|
||||
return;
|
||||
|
||||
if (!_doAfterControls.TryGetValue(id, out var doAfterControl))
|
||||
{
|
||||
doAfterControl = new DoAfterControl();
|
||||
AddChild(doAfterControl);
|
||||
}
|
||||
|
||||
doAfterControl.Cancelled = true;
|
||||
_cancelledDoAfters.Add(id, _gameTiming.CurTime);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!_entityManager.TryGetComponent(AttachedEntity, out DoAfterComponent? doAfterComponent))
|
||||
{
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var doAfters = doAfterComponent.DoAfters;
|
||||
|
||||
if (doAfters.Count == 0)
|
||||
{
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var transform = _entityManager.GetComponent<TransformComponent>(AttachedEntity.Value);
|
||||
|
||||
if (_eyeManager.CurrentMap != transform.MapID || !transform.Coordinates.IsValid(_entityManager))
|
||||
{
|
||||
Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Visible = true;
|
||||
var currentTime = _gameTiming.CurTime;
|
||||
var toRemove = new List<byte>();
|
||||
|
||||
// Cleanup cancelled DoAfters
|
||||
foreach (var (id, cancelTime) in _cancelledDoAfters)
|
||||
{
|
||||
if ((currentTime - cancelTime).TotalSeconds > DoAfterSystem.ExcessTime)
|
||||
toRemove.Add(id);
|
||||
}
|
||||
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
RemoveDoAfter(id);
|
||||
}
|
||||
|
||||
toRemove.Clear();
|
||||
|
||||
// Update 0 -> 1.0f of the things
|
||||
foreach (var (id, message) in doAfters)
|
||||
{
|
||||
if (_cancelledDoAfters.ContainsKey(id) || !_doAfterControls.ContainsKey(id))
|
||||
continue;
|
||||
|
||||
var control = _doAfterControls[id];
|
||||
var ratio = (currentTime - message.StartTime).TotalSeconds;
|
||||
control.Ratio = MathF.Min(1.0f,
|
||||
(float) ratio / message.Delay);
|
||||
|
||||
// Just in case it doesn't get cleaned up by the system for whatever reason.
|
||||
if (ratio > message.Delay + DoAfterSystem.ExcessTime)
|
||||
{
|
||||
toRemove.Add(id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in toRemove)
|
||||
{
|
||||
RemoveDoAfter(id);
|
||||
}
|
||||
|
||||
UpdatePosition(transform);
|
||||
}
|
||||
|
||||
public void UpdatePosition(TransformComponent xform)
|
||||
{
|
||||
var screenCoordinates = _eyeManager.CoordinatesToScreen(xform.Coordinates);
|
||||
var position = screenCoordinates.Position / UIScale - DesiredSize / 2f;
|
||||
LayoutContainer.SetPosition(this, position - new Vector2(0, 40f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,20 @@ namespace Content.Shared.DoAfter
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Merge this with the actual DoAfter
|
||||
/// <summary>
|
||||
/// We send a trimmed-down version of the DoAfter for the client for it to use.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ClientDoAfter
|
||||
{
|
||||
public bool Cancelled = false;
|
||||
|
||||
/// <summary>
|
||||
/// Accrued time when cancelled.
|
||||
/// </summary>
|
||||
public float CancelledAccumulator;
|
||||
|
||||
// To see what these do look at DoAfter and DoAfterEventArgs
|
||||
public byte ID { get; }
|
||||
|
||||
@@ -51,6 +59,8 @@ namespace Content.Shared.DoAfter
|
||||
|
||||
public EntityUid? Target { get; }
|
||||
|
||||
public float Accumulator;
|
||||
|
||||
public float Delay { get; }
|
||||
|
||||
// TODO: The other ones need predicting
|
||||
|
||||
Reference in New Issue
Block a user