diff --git a/DelvCD/Config/ElementListConfig.cs b/DelvCD/Config/ElementListConfig.cs index 4186d4e..831efd0 100644 --- a/DelvCD/Config/ElementListConfig.cs +++ b/DelvCD/Config/ElementListConfig.cs @@ -19,7 +19,7 @@ public class ElementListConfig : IConfigPage [JsonIgnore] private ElementType _selectedType = ElementType.Icon; [JsonIgnore] private string _input = string.Empty; - [JsonIgnore] private string[] _options = new string[] { "Icon", "Bar", "Group" }; + [JsonIgnore] private string[] _options = new string[] { "Icon", "Bar", "Circle", "Group" }; [JsonIgnore] private int _swapX = -1; [JsonIgnore] private int _swapY = -1; @@ -180,6 +180,7 @@ private void CreateUIElement(ElementType type, string name) ElementType.Group => new Group(name), ElementType.Icon => Icon.GetDefaultUIElementIcon(name), ElementType.Bar => Bar.GetDefaultUIElementBar(name), + ElementType.Circle => Circle.GetDefaultUIElementCircle(name), _ => null }; diff --git a/DelvCD/Config/Styles/CircleStyleConfig.cs b/DelvCD/Config/Styles/CircleStyleConfig.cs new file mode 100644 index 0000000..3170869 --- /dev/null +++ b/DelvCD/Config/Styles/CircleStyleConfig.cs @@ -0,0 +1,247 @@ +using Dalamud.Interface; +using DelvCD.Helpers; +using DelvCD.Helpers.DataSources; +using ImGuiNET; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Interface.Utility; + +namespace DelvCD.Config +{ + public class CircleStyleConfig : IConfigPage + { + [JsonIgnore] private float _scale => ImGuiHelpers.GlobalScale; + + [JsonIgnore] public string Name => "Circle"; + + [JsonIgnore] private Vector2 _screenSize = ImGui.GetMainViewport().Size; + + [JsonIgnore] private string[] _directionOptions = Enum.GetNames(); + + [JsonIgnore] private DataSource[] _dataSources = new DataSource[] { }; + [JsonIgnore] private string[] _progressDataSourceOptions = new string[] { }; + [JsonIgnore] private string[] _progressDataSourceFieldOptions = new string[] { }; + + public int ProgressDataSourceIndex = 0; + public int ProgressDataSourceFieldIndex = 0; + public bool InvertValues = false; + + public Vector2 Position = Vector2.Zero; + public int Radius = 50; + public int Thickness = 10; + public int StartAngle = 0; + public int EndAngle = 360; + public CircleDirection Direction = CircleDirection.Clockwise; + public ConfigColor FillColor = new ConfigColor(1, 0.5f, 0.5f, 1); + public ConfigColor BackgroundColor = new ConfigColor(0, 0, 0, 0.5f); + + public bool ShowBorder = true; + public int BorderThickness = 1; + public ConfigColor BorderColor = new ConfigColor(0, 0, 0, 1); + + /* + public bool Chunked = false; + public bool ChunkedStacksFromTrigger = true; + public int ChunkCount = 5; + public int ChunkPadding = 2; + public ConfigColor IncompleteChunkColor = new ConfigColor(0.6f, 0.6f, 0.6f, 1); + */ + + public bool Glow = false; + public int GlowThickness = 2; + public int GlowSegments = 20; + public int GlowPadding = 5; + public float GlowSpeed = 1f; + public ConfigColor GlowColor = new ConfigColor(230f / 255f, 150f / 255f, 0f / 255f, 1f); + public ConfigColor GlowColor2 = new ConfigColor(0f / 255f, 0f / 255f, 0f / 255f, 0f); + + public IConfigPage GetDefault() => new CircleStyleConfig(); + + + public void UpdateDataSources(DataSource[] dataSources) + { + _dataSources = dataSources.Where(x => x.ProgressFieldNames.Count > 0).ToArray(); + + if (_dataSources.Length == 0) + { + _progressDataSourceOptions = new string[] { }; + _progressDataSourceFieldOptions = new string[] { }; + ProgressDataSourceIndex = 0; + ProgressDataSourceFieldIndex = 0; + return; + } + + ProgressDataSourceIndex = Math.Clamp(ProgressDataSourceIndex, 0, dataSources.Length - 1); + + List list = new(); + for (int i = 0; i < dataSources.Length; i++) + { + list.Add("Trigger " + (i + 1)); + } + + _progressDataSourceOptions = list.ToArray(); + _progressDataSourceFieldOptions = dataSources[ProgressDataSourceIndex].ProgressFieldNames.ToArray(); + + ProgressDataSourceFieldIndex = Math.Clamp(ProgressDataSourceFieldIndex, 0, _progressDataSourceFieldOptions.Length - 1); + } + + public void DrawConfig(IConfigurable parent, Vector2 size, float padX, float padY) + { + if (ImGui.BeginChild("##CircleStyleConfig", new Vector2(size.X, size.Y), true)) + { + if (_dataSources.Length > 0) + { + ImGui.PushItemWidth(100 * _scale); + if (ImGui.Combo("##DataSourceCombo", ref ProgressDataSourceIndex, _progressDataSourceOptions, _progressDataSourceOptions.Length)) + { + ProgressDataSourceFieldIndex = 0; + _progressDataSourceFieldOptions = _dataSources[ProgressDataSourceIndex].ProgressFieldNames.ToArray(); + } + ImGui.PopItemWidth(); + + ImGui.SameLine(); + ImGui.PushItemWidth(200 * _scale); + ImGui.Combo("##DataSourceFieldCombo", ref ProgressDataSourceFieldIndex, _progressDataSourceFieldOptions, _progressDataSourceFieldOptions.Length); + ImGui.PopItemWidth(); + + ImGui.SameLine(); + ImGui.Checkbox("Invert Values", ref InvertValues); + } + else + { + ImGui.Text("No applicable data found in triggers!"); + } + + // base config + ImGui.NewLine(); + ImGui.DragFloat2("Position", ref Position, 1, -_screenSize.X / 2, _screenSize.X / 2); + ImGui.DragInt("Radius", ref Radius, 1, 0, (int)_screenSize.Y); + ImGui.DragInt("Thickness", ref Thickness, 1, 0, (int)_screenSize.Y); + ImGui.NewLine(); + ImGui.DragInt("Start Angle", ref StartAngle, 1, -360, 360); + ImGui.DragInt("End Angle", ref EndAngle, 1, -360, 360); + + ImGui.Combo("Fill Direction", ref Unsafe.As(ref Direction) , _directionOptions, _directionOptions.Length); + + Vector4 vector = FillColor.Vector; + if (ImGui.ColorEdit4("Fill Color", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + FillColor.Vector = vector; + } + + vector = BackgroundColor.Vector; + if (ImGui.ColorEdit4("Background Color", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + BackgroundColor.Vector = vector; + } + + // border + ImGui.NewLine(); + ImGui.Checkbox("Show Border", ref ShowBorder); + if (ShowBorder) + { + DrawHelpers.DrawNestIndicator(1); + ImGui.DragInt("Border Thickness", ref BorderThickness, 1, 1, 100); + + DrawHelpers.DrawNestIndicator(1); + vector = BorderColor.Vector; + if (ImGui.ColorEdit4("Border Color", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + BorderColor.Vector = vector; + } + } + + /* + // chunked + ImGui.NewLine(); + ImGui.Checkbox("Draw in Chunks", ref Chunked); + if (Chunked) + { + DrawHelpers.DrawNestIndicator(1); + if (ImGui.RadioButton("Stacks from Trigger", ChunkedStacksFromTrigger)) + { + ChunkedStacksFromTrigger = true; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Assumes the trigger data to be stacks and each chunk will represent one stack."); + } + + ImGui.SameLine(); + if (ImGui.RadioButton("Custom", !ChunkedStacksFromTrigger)) + { + ChunkedStacksFromTrigger = false; + } + + if (!ChunkedStacksFromTrigger) + { + DrawHelpers.DrawNestIndicator(1); + ImGui.DragInt("Chunk Count", ref ChunkCount, 0.1f, 1, 200); + } + + DrawHelpers.DrawNestIndicator(1); + ImGui.DragInt("Chunk Padding", ref ChunkPadding, 0.1f, 0, 50); + + DrawHelpers.DrawNestIndicator(1); + vector = IncompleteChunkColor.Vector; + if (ImGui.ColorEdit4("Incomplete Chunk Color", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + IncompleteChunkColor.Vector = vector; + } + } + */ + + // glow + ImGui.NewLine(); + ImGui.Checkbox("Glow", ref Glow); + if (Glow) + { + DrawHelpers.DrawNestIndicator(1); + ImGui.DragInt("Thickness##Glow", ref GlowThickness, 1, 1, 16); + + DrawHelpers.DrawNestIndicator(1); + ImGui.DragInt("Glow Segments##Glow", ref GlowSegments, 1, 2, 40); + + DrawHelpers.DrawNestIndicator(1); + ImGui.DragInt("Glow Padding##Glow", ref GlowPadding, 1, 2, 200); + + DrawHelpers.DrawNestIndicator(1); + ImGui.DragFloat("Animation Speed##Glow", ref GlowSpeed, 0.05f, 0, 2f); + + DrawHelpers.DrawNestIndicator(1); + vector = GlowColor.Vector; + if (ImGui.ColorEdit4("Glow Color##Glow", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + GlowColor.Vector = vector; + } + + DrawHelpers.DrawNestIndicator(1); + vector = GlowColor2.Vector; + if (ImGui.ColorEdit4("Glow Color 2##Glow", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar)) + { + GlowColor2.Vector = vector; + } + } + } + + ImGui.EndChild(); + } + + private void DrawIconPreview(Vector2 iconPos, Vector2 iconSize, uint icon, bool crop, bool desaturate, bool text) + { + ImDrawListPtr drawList = ImGui.GetWindowDrawList(); + DrawHelpers.DrawIcon(icon, iconPos, iconSize, crop, 0, desaturate, 1f, drawList); + if (text) + { + string iconText = icon.ToString(); + Vector2 iconTextPos = iconPos + new Vector2(20 - ImGui.CalcTextSize(iconText).X / 2, 38); + drawList.AddText(iconTextPos, 0xFFFFFFFF, iconText); + } + } + } +} \ No newline at end of file diff --git a/DelvCD/DelvCD.csproj b/DelvCD/DelvCD.csproj index d3f1d29..fe9a1c1 100644 --- a/DelvCD/DelvCD.csproj +++ b/DelvCD/DelvCD.csproj @@ -98,7 +98,4 @@ - - - \ No newline at end of file diff --git a/DelvCD/Helpers/ConfigHelpers.cs b/DelvCD/Helpers/ConfigHelpers.cs index 704eace..f79406f 100644 --- a/DelvCD/Helpers/ConfigHelpers.cs +++ b/DelvCD/Helpers/ConfigHelpers.cs @@ -536,12 +536,14 @@ public class DelvCDSerializationBinder : ISerializationBinder private static List _configTypes = new List() { typeof(Bar), + typeof(Circle), typeof(Group), typeof(Icon), typeof(Label), typeof(UIElement), typeof(ElementListConfig), typeof(BarStyleConfig), + typeof(CircleStyleConfig), typeof(CooldownTrigger), typeof(ConfigColor), typeof(FontConfig), diff --git a/DelvCD/Helpers/DrawHelpers.cs b/DelvCD/Helpers/DrawHelpers.cs index b5c0081..a9fa7ef 100644 --- a/DelvCD/Helpers/DrawHelpers.cs +++ b/DelvCD/Helpers/DrawHelpers.cs @@ -299,6 +299,41 @@ public static void DrawGlow(Vector2 pos, Vector2 size, int thickness, int segmen DrawSegmentedLineHorizontal(drawList, c3.AddY(-thickness), -size.X, thickness, prog, segments, col1, col2); DrawSegmentedLineVertical(drawList, c4, thickness, -size.Y, prog, segments, col1, col2); } + + public static void DrawGlowCircle(Vector2 center, float radius, float thickness, float padding, int segments, float speed, ConfigColor col1, ConfigColor col2, ImDrawListPtr drawList) + { + speed = Math.Abs(speed); + int mod = speed == 0 ? 1 : (int)(250 / speed); + float prog = (float)(DateTimeOffset.Now.ToUnixTimeMilliseconds() % mod) / mod; + + float angleStep = (float)(Math.PI * 2 / segments); + float startAngle = -MathF.PI / 2f; + + for (int i = 0; i < segments; i++) + { + float currentAngle = startAngle + i * angleStep; + float nextAngle = currentAngle + angleStep; + + // Calculate the color gradient + float t = ((float)i / segments + prog) % 1; + Vector4 color = new Vector4( + col1.Vector.X * (1 - t) + col2.Vector.X * t, + col1.Vector.Y * (1 - t) + col2.Vector.Y * t, + col1.Vector.Z * (1 - t) + col2.Vector.Z * t, + col1.Vector.W * (1 - t) + col2.Vector.W * t + ); + + uint colorUint = ImGui.ColorConvertFloat4ToU32(color); + + // Draw the glowing segment around the outer edge with padding + drawList.PathArcTo(center, radius + thickness / 2 + padding, currentAngle, nextAngle, 2); + drawList.PathStroke(colorUint, ImDrawFlags.None, thickness / 2); + + // Draw the glowing segment around the inner edge with padding + drawList.PathArcTo(center, radius - thickness / 2 - padding, currentAngle, nextAngle, 2); + drawList.PathStroke(colorUint, ImDrawFlags.None, thickness / 2); + } + } public static void DrawGlowNGon(Vector2 center, float radius, int thickness, int sides, int segments, float speed, ConfigColor col1, ConfigColor col2, ImDrawListPtr drawList) { diff --git a/DelvCD/Helpers/Enums.cs b/DelvCD/Helpers/Enums.cs index d0c9ed1..6c996cc 100644 --- a/DelvCD/Helpers/Enums.cs +++ b/DelvCD/Helpers/Enums.cs @@ -10,6 +10,7 @@ public enum ElementType { Icon, Bar, + Circle, Group, Label } @@ -68,6 +69,12 @@ public enum BarDirection Up, Down, } + + public enum CircleDirection + { + Clockwise, + AntiClockwise, + } public enum ChunkStyles { diff --git a/DelvCD/UIElements/Circle.cs b/DelvCD/UIElements/Circle.cs new file mode 100644 index 0000000..bcf6a19 --- /dev/null +++ b/DelvCD/UIElements/Circle.cs @@ -0,0 +1,311 @@ +using DelvCD.Config; +using DelvCD.Helpers; +using DelvCD.Helpers.DataSources; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text.Json.Serialization; +using System.Threading; +using Dalamud.Logging; +using Dalamud.Plugin.Services; + +namespace DelvCD.UIElements +{ + public class Circle : UIElement + { + public override ElementType Type => ElementType.Circle; + + public CircleStyleConfig CircleStyleConfig { get; set; } + public LabelListConfig LabelListConfig { get; set; } + public VisibilityConfig VisibilityConfig { get; set; } + + [JsonIgnore] private TriggerDataOp[] _thresholdOperators = new TriggerDataOp[] { TriggerDataOp.LessThan, TriggerDataOp.GreaterThan, TriggerDataOp.LessThanEq, TriggerDataOp.GreaterThanEq }; + + [JsonIgnore] private TriggerConfig _triggerConfig = null!; + public TriggerConfig TriggerConfig + { + get => _triggerConfig; + set + { + if (_triggerConfig != null) + { + _triggerConfig.TriggerOptionsUpdateEvent -= OnTriggerOptionsChanged; + } + + _triggerConfig = value; + _triggerConfig.TriggerOptionsUpdateEvent += OnTriggerOptionsChanged; + + OnTriggerOptionsChanged(TriggerConfig); + } + } + + + [JsonIgnore] private StyleConditions _styleConditions = null!; + public StyleConditions StyleConditions + { + get => _styleConditions; + set + { + _styleConditions = value; + + if (TriggerConfig != null) + { + OnTriggerOptionsChanged(TriggerConfig); + } + } + } + + // Constructor for deserialization + public Circle() : this(string.Empty) { } + + public Circle(string name) : base(name) + { + CircleStyleConfig = new CircleStyleConfig(); + LabelListConfig = new LabelListConfig(); + TriggerConfig = new TriggerConfig(); + StyleConditions = new StyleConditions(); + VisibilityConfig = new VisibilityConfig(); + } + + private void OnTriggerOptionsChanged(TriggerConfig sender) + { + DataSource[] dataSources = sender.TriggerOptions.Select(x => x.DataSource).ToArray(); + + if (CircleStyleConfig != null) + { + CircleStyleConfig.UpdateDataSources(dataSources); + } + + if (StyleConditions != null) + { + StyleConditions.UpdateDataSources(dataSources); + } + } + + public override IEnumerable GetConfigPages() + { + yield return CircleStyleConfig; + yield return LabelListConfig; + yield return TriggerConfig; + + // ugly hack + StyleConditions.UpdateDefaultStyle(CircleStyleConfig); + + yield return StyleConditions; + yield return VisibilityConfig; + } + + public override void ImportPage(IConfigPage page) + { + switch (page) + { + case CircleStyleConfig newPage: + newPage.UpdateDataSources(TriggerConfig.TriggerOptions.Select(x => x.DataSource).ToArray()); + CircleStyleConfig = newPage; + break; + case LabelListConfig newPage: + LabelListConfig = newPage; + break; + case TriggerConfig newPage: + TriggerConfig = newPage; + break; + case StyleConditions newPage: + newPage.UpdateDefaultStyle(CircleStyleConfig); + newPage.UpdateDataSources(TriggerConfig.TriggerOptions.Select(x => x.DataSource).ToArray()); + StyleConditions = newPage; + break; + case VisibilityConfig newPage: + VisibilityConfig = newPage; + break; + } + } + + public override void Draw(Vector2 pos, Vector2? parentSize = null, bool parentVisible = true) + { + if (!TriggerConfig.TriggerOptions.Any()) + { + return; + } + + bool visible = VisibilityConfig.IsVisible(parentVisible); + if (!visible && !Preview) + { + return; + } + + bool triggered = TriggerConfig.IsTriggered(Preview, out int triggeredIndex); + DataSource data = TriggerConfig.TriggerOptions[triggeredIndex].DataSource; + DataSource[] datas = TriggerConfig.TriggerOptions.Select(x => x.DataSource).ToArray(); + CircleStyleConfig style = StyleConditions.GetStyle(datas) ?? CircleStyleConfig; + + Vector2 localPos = pos + style.Position; + Vector2 size = new Vector2(style.Radius * 2); + + if (Singletons.Get().ShouldClip()) + { + ClipRect? clipRect = Singletons.Get().GetClipRectForArea(localPos, size); + if (clipRect.HasValue) + { + return; + } + } + + LastFrameWasPreview = Preview; + + if (!triggered && !Preview) + { + StartData = null; + StartTime = null; + return; + } + + UpdateStartData(data); + UpdateDragData(localPos, size); + + float realValue = datas[style.ProgressDataSourceIndex].GetProgressValue(style.ProgressDataSourceFieldIndex); + float progressMaxValue = datas[style.ProgressDataSourceIndex].GetMaxValue(style.ProgressDataSourceFieldIndex); + + float progressValue = Math.Min(realValue, progressMaxValue); + + if (style.InvertValues && progressValue != 0) + { + progressValue = progressMaxValue - progressValue; + } + + DrawHelpers.DrawInWindow($"##{ID}", localPos, size, Preview, SetPosition, (drawList) => + { + if (Preview) + { + data = UpdatePreviewData(data); + if (LastFrameWasDragging) + { + localPos = ImGui.GetWindowPos(); + style.Position = localPos - pos; + } + } + + const int segments = 100; + float startAngle = (float)(-Math.PI / 2f + style.StartAngle * (Math.PI / 180f)); + float endAngle = (float)(-Math.PI / 2f + style.EndAngle * (Math.PI / 180f)); + + CircleData circle = CalculateCircle(startAngle, endAngle, progressValue, progressMaxValue, style.Direction); + + ConfigColor fillColor = circle.FillColor ?? style.FillColor; + + // Draw the background arc + drawList.PathArcTo(localPos, style.Radius, startAngle, endAngle, segments); + drawList.PathStroke(ImGui.ColorConvertFloat4ToU32(style.BackgroundColor.Vector), ImDrawFlags.None, style.Thickness); + + // Draw the filled arc + drawList.PathArcTo(localPos, style.Radius, circle.StartAngle, circle.EndAngle, segments); + drawList.PathStroke(ImGui.ColorConvertFloat4ToU32(fillColor.Vector), ImDrawFlags.None, style.Thickness); + + if (style.Glow) + { + DrawHelpers.DrawGlowCircle(localPos, style.Radius, style.Thickness, style.GlowPadding, style.GlowSegments, style.GlowSpeed, style.GlowColor, style.GlowColor2, drawList); + } + + if (style.ShowBorder) + { + drawList.PathArcTo(localPos, style.Radius - style.Thickness / 2f, startAngle, endAngle, segments); + drawList.PathStroke(ImGui.ColorConvertFloat4ToU32(style.BorderColor.Vector), ImDrawFlags.None, style.BorderThickness); + + drawList.PathArcTo(localPos, style.Radius + style.Thickness / 2f, startAngle, endAngle, segments); + drawList.PathStroke(ImGui.ColorConvertFloat4ToU32(style.BorderColor.Vector), ImDrawFlags.None, style.BorderThickness); + } + }); + + foreach (Label label in LabelListConfig.Labels) + { + if (!Preview && LastFrameWasPreview) + { + label.Preview = false; + } + else + { + label.Preview |= Preview; + } + + if (triggered || label.Preview) + { + label.UpdateDataSources(datas); + label.Draw(localPos, size, visible); + } + } + } + + + private CircleData CalculateCircle(float startAngle, float endAngle, float progress, float max, CircleDirection direction) + { + CircleData circle = new(); + + float fillPercent = max == 0 ? 1f : Math.Clamp(progress / max, 0f, 1f); + float angleRange = endAngle - startAngle; + + float fillAngle = angleRange * fillPercent; + + if (direction == CircleDirection.Clockwise) + { + circle.StartAngle = startAngle; + circle.EndAngle = startAngle + fillAngle; + } + else + { + circle.StartAngle = -(startAngle + (float)Math.PI); + circle.EndAngle = -(startAngle + (float)Math.PI); + } + + return circle; + } + + public void Resize(Vector2 size, bool conditions) + { + // CircleStyleConfig.Size = size; + + if (conditions) + { + foreach (var condition in StyleConditions.Conditions) + { + //condition.Style.Size = size; + } + } + } + + public void ScaleResolution(Vector2 scaleFactor, bool positionOnly) + { + CircleStyleConfig.Position *= scaleFactor; + + if (!positionOnly) + { + //CircleStyleConfig.Size *= scaleFactor; + } + + foreach (var condition in StyleConditions.Conditions) + { + condition.Style.Position *= scaleFactor; + + if (!positionOnly) + { + //condition.Style.Size *= scaleFactor; + } + } + } + + public static Circle GetDefaultUIElementCircle(string name) + { + Circle newCircle = new Circle(name); + newCircle.ImportPage(newCircle.LabelListConfig.GetDefault()); + return newCircle; + } + } + + internal struct CircleData + { + public float StartAngle; + public float EndAngle; + + public ConfigColor? FillColor; + } +} \ No newline at end of file