diff --git a/Expedition2Good.cs b/Expedition2Good.cs index 386e7df..39dcf52 100644 --- a/Expedition2Good.cs +++ b/Expedition2Good.cs @@ -113,8 +113,10 @@ public override void Render() { if (first && showOnMinimap) { - var color = value >= Settings.ValuableColorThreshold ? Settings.ValuableTextColor : Settings.TextColor; - var lines = GetMapText(overridden, value, allValidRecipes, label.Data, color); + var (minimapPrice, minimapOverridden, transferRecipes) = + GetMinimapRecipeData(label.Data, value, overridden, allValidRecipes); + var color = minimapPrice >= Settings.ValuableColorThreshold ? Settings.ValuableTextColor : Settings.TextColor; + var lines = GetMapText(minimapOverridden, minimapPrice, transferRecipes, label.Data, color); DrawMapText(lines, Graphics.GridToMap(entity.GridPos, entity.GridPos)); } @@ -144,18 +146,29 @@ public override void Render() if (entity?.HashComponents.GetValueOrDefault((ushort)0x87B2) is not 0 and { } dataAddr) { var data = RemoteMemoryObject.GetObjectStatic(dataAddr); - var expedition2RunesWeights = GameController.Files.Expedition2RunesWeights.EntriesList; - var allRecipes = GameController.Files.Expedition2Recipes.EntriesList.ToLookup(x => x.RuneCountRequired); - var allValidRecipes = GetRecipes(expedition2RunesWeights, areaLevel, allRecipes, data); - var recipes = allValidRecipes.FirstOrDefault(); - if (recipes != default) + if (TryGetSelectedRecipe(data, out var selectedRecipe)) { - var value = recipes.value.Item1; - var color = value >= Settings.ValuableColorThreshold ? Settings.ValuableTextColor : Settings.TextColor; - var lines = GetMapText(recipes.value.Item2, recipes.value.Item1, allValidRecipes.Select(x => x.x).ToList(), data, color); + var price = GetPriceOrDefault(selectedRecipe); + var color = price.Item1 >= Settings.ValuableColorThreshold ? Settings.ValuableTextColor : Settings.TextColor; + var lines = GetMapText(price.Item2, price.Item1, [selectedRecipe], data, color); DrawMapText(lines, Graphics.GridToMap(entity.GridPos, entity.GridPos)); found = true; } + else + { + var expedition2RunesWeights = GameController.Files.Expedition2RunesWeights.EntriesList; + var allRecipes = GameController.Files.Expedition2Recipes.EntriesList.ToLookup(x => x.RuneCountRequired); + var allValidRecipes = GetRecipes(expedition2RunesWeights, areaLevel, allRecipes, data); + var recipes = allValidRecipes.FirstOrDefault(); + if (recipes != default) + { + var value = recipes.value.Item1; + var color = value >= Settings.ValuableColorThreshold ? Settings.ValuableTextColor : Settings.TextColor; + var lines = GetMapText(recipes.value.Item2, recipes.value.Item1, allValidRecipes.Select(x => x.x).ToList(), data, color); + DrawMapText(lines, Graphics.GridToMap(entity.GridPos, entity.GridPos)); + found = true; + } + } } if (!found) @@ -204,6 +217,8 @@ public override void Render() first = false; } } + + DrawTransferredRunesPanel(); } private bool IsActivated(Entity entity) @@ -234,8 +249,7 @@ private bool IsActivated(Entity entity) { foreach (var position in positions) { - var runeIds = recipes.Select(x => x.Runes.ElementAtOrDefault(position)).Where(x => x != null) - .Select(r => r.Id).Distinct().OrderBy(x => x).ToList(); + var runeIds = GetTransferredRuneIds(recipes, position); if (Settings.HideNonHighlightedTransferredRunes) { runeIds = runeIds.Where(id => GetHighlightColor(id) != null).ToList(); @@ -267,6 +281,159 @@ private bool IsActivated(Entity entity) return lines; } + private void DrawTransferredRunesPanel() + { + if (!Settings.ShowTransferredRunesPanel) + { + return; + } + + var runeIds = CollectDistinctTransferredRunes(GetEncounterData()); + if (runeIds.Count == 0) + { + return; + } + + const float pad = 10f; + const float accentWidth = 4f; + const float rowHeight = 18f; + const float swatchSize = 8f; + const float swatchGap = 6f; + const float titleRowHeight = 22f; + const float separatorGap = 4f; + + var entries = runeIds + .Select(id => (Id: id, Highlight: GetHighlightColor(id))) + .OrderByDescending(x => x.Highlight != null) + .ThenBy(x => x.Id, StringComparer.OrdinalIgnoreCase) + .ToList(); + + const string title = "Transferred Runes"; + var countText = $"({entries.Count})"; + var titleWidth = Graphics.MeasureText(title).X + 6f + Graphics.MeasureText(countText).X; + var maxRuneWidth = entries.Max(e => Graphics.MeasureText(e.Id).X); + var contentWidth = Math.Max(titleWidth, swatchSize + swatchGap + maxRuneWidth); + var innerLeft = accentWidth + pad; + var panelWidth = innerLeft + contentWidth + pad; + var panelHeight = pad + titleRowHeight + separatorGap + 1f + pad / 2f + entries.Count * rowHeight + pad; + + var origin = new Vector2(Settings.TransferredRunesPanelX.Value, Settings.TransferredRunesPanelY.Value); + var panelBottomRight = origin + new Vector2(panelWidth, panelHeight); + + var bgColor = Color.FromArgb(215, 12, 12, 16); + var borderColor = Color.FromArgb(200, 100, 82, 52); + var titleColor = Color.FromArgb(255, 230, 210, 170); + var countColor = Color.FromArgb(170, 170, 170, 170); + var separatorColor = Color.FromArgb(110, 100, 82, 52); + + Graphics.DrawBox(origin, panelBottomRight, bgColor, 6f); + Graphics.DrawBox(origin, new Vector2(origin.X + accentWidth, panelBottomRight.Y), Settings.TopPickColor, 0f); + Graphics.DrawFrame(origin, panelBottomRight, borderColor, 6f, 1, 0); + + var textX = origin.X + innerLeft + swatchSize + swatchGap; + var y = origin.Y + pad; + + Graphics.DrawText(title, new Vector2(origin.X + innerLeft, y), titleColor); + Graphics.DrawText(countText, new Vector2(origin.X + innerLeft + Graphics.MeasureText(title).X + 6f, y), countColor); + + y += titleRowHeight; + Graphics.DrawLine( + new Vector2(origin.X + innerLeft, y), + new Vector2(panelBottomRight.X - pad, y), + 1f, + separatorColor); + y += separatorGap + 1f + pad / 2f; + + foreach (var (id, highlight) in entries) + { + var textColor = highlight ?? Settings.TextColor; + var defaultColor = Settings.TextColor.Value; + var swatchColor = highlight ?? Color.FromArgb(130, defaultColor.R, defaultColor.G, defaultColor.B); + + if (highlight != null) + { + var rowBg = Color.FromArgb(40, highlight.Value.R, highlight.Value.G, highlight.Value.B); + Graphics.DrawBox( + new Vector2(origin.X + accentWidth, y - 1f), + new Vector2(panelBottomRight.X, y + rowHeight - 3f), + rowBg, + 3f); + } + + var swatchTopLeft = new Vector2(textX - swatchSize - swatchGap, y + 3f); + var swatchBottomRight = swatchTopLeft + new Vector2(swatchSize, swatchSize); + Graphics.DrawBox(swatchTopLeft, swatchBottomRight, swatchColor, 2f); + if (highlight != null) + { + Graphics.DrawFrame(swatchTopLeft, swatchBottomRight, Color.FromArgb(180, 255, 255, 255), 2f, 1, 0); + } + + Graphics.DrawText(id, new Vector2(textX, y), textColor); + y += rowHeight; + } + } + + private IEnumerable GetEncounterData() + { + var seen = new HashSet(); + if (_labels.Value is { Count: > 0 } labels) + { + foreach (var (log, label) in labels) + { + if (IsActivated(log.ItemOnGround)) + { + continue; + } + + if (label?.Data is { Address: not 0 } data && seen.Add(data.Address)) + { + yield return data; + } + } + } + + foreach (var entity in GameController.EntityListWrapper.ValidEntitiesByType[EntityType.IngameIcon] + .Where(x => x.Metadata.StartsWith("Metadata/MiscellaneousObjects/Expedition2/Expedition2Encounter", StringComparison.Ordinal))) + { + if (IsActivated(entity)) + { + continue; + } + + if (entity?.HashComponents.GetValueOrDefault((ushort)0x87B2) is not 0 and { } dataAddr) + { + var data = RemoteMemoryObject.GetObjectStatic(dataAddr); + if (data is { Address: not 0 } && seen.Add(data.Address)) + { + yield return data; + } + } + } + } + + private List CollectDistinctTransferredRunes(IEnumerable encounters) + { + var runeIds = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var data in encounters) + { + if (!TryGetSelectedRecipe(data, out var recipe) || data.PassedOnRunePositions is not { Count: > 0 } positions) + { + continue; + } + + foreach (var position in positions) + { + var runeId = recipe.Runes.ElementAtOrDefault(position)?.Id; + if (!string.IsNullOrEmpty(runeId)) + { + runeIds.Add(runeId); + } + } + } + + return runeIds.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + } + private Color? GetHighlightColor(string runeId) { if (string.IsNullOrEmpty(runeId)) @@ -307,6 +474,50 @@ private void DrawMapText(List> lines, Vector2 a } } + private static bool TryGetSelectedRecipe(Expedition2EncounterData data, out Expedition2Recipe recipe) + { + recipe = null; + if (data?.SelectedRecipe is not { Address: not 0 } selected) + { + return false; + } + + if (selected.Runes is not { Count: > 0 }) + { + return false; + } + + recipe = selected; + return true; + } + + private (double price, bool overridden, IReadOnlyCollection transferRecipes) GetMinimapRecipeData( + Expedition2EncounterData data, + double fallbackPrice, + bool fallbackOverridden, + IReadOnlyCollection fallbackTransferRecipes) + { + if (TryGetSelectedRecipe(data, out var selectedRecipe)) + { + var price = GetPriceOrDefault(selectedRecipe); + return (price.Item1, price.Item2, [selectedRecipe]); + } + + return (fallbackPrice, fallbackOverridden, fallbackTransferRecipes); + } + + private static List GetTransferredRuneIds(IReadOnlyCollection recipes, int position) + { + if (recipes.Count == 1) + { + var runeId = recipes.First().Runes.ElementAtOrDefault(position)?.Id; + return string.IsNullOrEmpty(runeId) ? [] : [runeId]; + } + + return recipes.Select(x => x.Runes.ElementAtOrDefault(position)).Where(x => x != null) + .Select(r => r.Id).Distinct().OrderBy(x => x).ToList(); + } + private List<(Expedition2Recipe x, (double, bool) value)> GetRecipes(List expedition2RunesWeights, int areaLevel, ILookup allRecipes, Expedition2EncounterData data) { var allowedRuneCounts = expedition2RunesWeights.Where(x => x.RuneSlot - 1 == data?.FixedRunePosition) diff --git a/Expedition2GoodSettings.cs b/Expedition2GoodSettings.cs index ad34cf5..33f0800 100644 --- a/Expedition2GoodSettings.cs +++ b/Expedition2GoodSettings.cs @@ -42,6 +42,15 @@ public class Expedition2GoodSettings : ISettings [Menu(null, "When enabled, a transferred-rune line is only drawn if it contains a highlighted rune, and only the highlighted rune(s) are listed on that line.")] public ToggleNode HideNonHighlightedTransferredRunes { get; set; } = new ToggleNode(false); + [Menu(null, "Shows a screen panel listing all distinct transferred runes from encounters with a selected recipe.")] + public ToggleNode ShowTransferredRunesPanel { get; set; } = new ToggleNode(false); + + [Menu(null, "Horizontal position of the transferred-runes panel.")] + public RangeNode TransferredRunesPanelX { get; set; } = new RangeNode(100, 0, 4000); + + [Menu(null, "Vertical position of the transferred-runes panel.")] + public RangeNode TransferredRunesPanelY { get; set; } = new RangeNode(100, 0, 4000); + [Menu(null, "A transferred-rune name is drawn in its configured color when it matches one of these entries. Type the rune name exactly as it appears in the 'Transfers rune' line (case-insensitive), then pick a color.")] public ContentNode HighlightedTransferredRunes { get; set; } = new ContentNode { Content = [], EnableControls = true, EnableItemCollapsing = true, ItemFactory = () => new HighlightedRune() };