diff --git a/GlamourBrowser/DataCache.cs b/GlamourBrowser/DataCache.cs index 271af9a..aa9f9f8 100644 --- a/GlamourBrowser/DataCache.cs +++ b/GlamourBrowser/DataCache.cs @@ -12,7 +12,7 @@ namespace Glamaholic { .Where(row => row.EquipSlotCategory.RowId != 0 && row.EquipSlotCategory.Value!.SoulCrystal == 0) .ToImmutableList()); - /* + public static Lazy> StainLookup { get; } = new (() => Service.DataManager.GetExcelSheet(ClientLanguage.English)! @@ -20,7 +20,17 @@ namespace Glamaholic { .ToImmutableDictionary(static row => row.Name.ExtractText().Trim().ToLower(), static row => (byte) row.RowId)); + public static Lazy> AllStains { get; } = + new(() => Service.DataManager.GetExcelSheet(ClientLanguage.English)! + .Where(row => row.RowId != 0 && !row.Name.IsEmpty) + .Select(row => ((byte)row.RowId, row.Name.ExtractText(), SeColorToRgba(row.Color), row.IsMetallic)) + .ToImmutableList()); + + // Convert SE's BGR color format to RGBA + private static uint SeColorToRgba(uint color) + => ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000; + public static int GetNumStainSlots(uint itemId) => - Service.DataManager.GetExcelSheet(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0;*/ + Service.DataManager.GetExcelSheet(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0; } } diff --git a/GlamourBrowser/Ui/MainInterface.cs b/GlamourBrowser/Ui/MainInterface.cs index 306b53f..4804818 100644 --- a/GlamourBrowser/Ui/MainInterface.cs +++ b/GlamourBrowser/Ui/MainInterface.cs @@ -29,12 +29,15 @@ namespace Glamaholic.Ui { private Dictionary SelectedItems { get; set; } = new(); private Dictionary> FilteredItemsCache { get; set; } = new(); private Dictionary ScrollPositions { get; set; } = new(); + private Dictionary SelectedStain1 { get; set; } = new(); + private Dictionary SelectedStain2 { get; set; } = new(); private bool _visible; private string _itemFilter = string.Empty; private ItemCategory _currentCategory = ItemCategory.Head; private ItemCategory _previousCategory = ItemCategory.Head; private string _lastFilterUsed = string.Empty; + private string _dyeFilter = string.Empty; internal MainInterface(PluginUi ui) { this.Ui = ui; @@ -48,6 +51,19 @@ namespace Glamaholic.Ui { SelectedItems[ItemCategory.Gloves] = null; SelectedItems[ItemCategory.Legs] = null; SelectedItems[ItemCategory.Feet] = null; + + // Initialize dye selections (0 = no dye) + SelectedStain1[ItemCategory.Head] = 0; + SelectedStain1[ItemCategory.Body] = 0; + SelectedStain1[ItemCategory.Gloves] = 0; + SelectedStain1[ItemCategory.Legs] = 0; + SelectedStain1[ItemCategory.Feet] = 0; + + SelectedStain2[ItemCategory.Head] = 0; + SelectedStain2[ItemCategory.Body] = 0; + SelectedStain2[ItemCategory.Gloves] = 0; + SelectedStain2[ItemCategory.Legs] = 0; + SelectedStain2[ItemCategory.Feet] = 0; } private void LoadItemsByCategory() { @@ -213,11 +229,11 @@ namespace Glamaholic.Ui { var selectedItem = SelectedItems[category]; ImGui.TextUnformatted(label); - + var drawCursor = ImGui.GetCursorScreenPos(); var slotSize = SelectedGearIconSize + SelectedGearPaddingSize; var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border); - + ImGui.GetWindowDrawList().AddRect( drawCursor, drawCursor + new Vector2(slotSize), @@ -241,6 +257,23 @@ namespace Glamaholic.Ui { } ImGui.TextUnformatted(selectedItem.Value.Name.ExtractText()); + + // Draw dye controls - always show both slots + ImGui.PushItemWidth(-1); + + // Primary dye + if (DrawDyeCombo($"##dye1-{label}", SelectedStain1, category, true)) { + // Apply the dye change immediately + this.ApplyItemToCharacter(selectedItem.Value, category); + } + + // Secondary dye + if (DrawDyeCombo($"##dye2-{label}", SelectedStain2, category, false)) { + // Apply the dye change immediately + this.ApplyItemToCharacter(selectedItem.Value, category); + } + + ImGui.PopItemWidth(); } else { ImGui.SetCursorPos(cursorAfter); ImGui.TextUnformatted("(empty)"); @@ -261,6 +294,137 @@ namespace Glamaholic.Ui { } } + private unsafe bool DrawDyeCombo(string id, Dictionary stainDict, ItemCategory category, bool isPrimary) { + var allStains = DataCache.AllStains.Value; + var currentStain = stainDict[category]; + + // Find the current stain info + string previewName = "None"; + uint previewColor = 0; + bool previewGloss = false; + if (currentStain != 0) { + var stain = allStains.FirstOrDefault(s => s.Id == currentStain); + if (stain != default) { + previewName = stain.Name; + previewColor = stain.Color; + previewGloss = stain.Gloss; + } + } + + // Calculate contrast color for text + var textColor = GetContrastColor(previewColor); + + bool changed = false; + + // Draw preview button with color + if (previewColor != 0) { + ImGui.PushStyleColor(ImGuiCol.FrameBg, previewColor); + ImGui.PushStyleColor(ImGuiCol.Text, textColor); + } + + if (ImGui.BeginCombo(id, previewName, ImGuiComboFlags.HeightLarge)) { + if (previewColor != 0) { + ImGui.PopStyleColor(2); + } + + // Search filter + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##dyefilter", "Search dyes...", ref _dyeFilter, 256); + + ImGui.Separator(); + + // Filter dyes based on search + IEnumerable<(byte Id, string Name, uint Color, bool Gloss)> filteredStains = string.IsNullOrEmpty(_dyeFilter) + ? allStains + : allStains.Where(s => s.Name.Contains(_dyeFilter, StringComparison.OrdinalIgnoreCase)); + + // None option + var noneColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 1.0f)); + ImGui.PushStyleColor(ImGuiCol.Button, noneColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, noneColor + 0x202020); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, noneColor + 0x404040); + if (ImGui.Button("None", new Vector2(-1, 0))) { + stainDict[category] = 0; + changed = true; + ImGui.CloseCurrentPopup(); + } + ImGui.PopStyleColor(3); + + // All filtered dyes + foreach (var stain in filteredStains) { + var stainTextColor = GetContrastColor(stain.Color); + ImGui.PushStyleColor(ImGuiCol.Button, stain.Color); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AdjustBrightness(stain.Color, 1.1f)); + ImGui.PushStyleColor(ImGuiCol.ButtonActive, AdjustBrightness(stain.Color, 0.9f)); + ImGui.PushStyleColor(ImGuiCol.Text, stainTextColor); + + if (ImGui.Button($"{stain.Name}##{stain.Id}", new Vector2(-1, 0))) { + stainDict[category] = stain.Id; + changed = true; + ImGui.CloseCurrentPopup(); + } + + // Draw gloss overlay + if (stain.Gloss) { + var drawList = ImGui.GetWindowDrawList(); + var min = ImGui.GetItemRectMin(); + var max = ImGui.GetItemRectMax(); + drawList.AddRectFilledMultiColor(min, max, + 0x50FFFFFF, 0x50000000, 0x50FFFFFF, 0x50000000); + } + + // Selection indicator + if (currentStain == stain.Id) { + var drawList = ImGui.GetWindowDrawList(); + var min = ImGui.GetItemRectMin(); + var max = ImGui.GetItemRectMax(); + drawList.AddRect(min, max, 0xFF2020D0, 0, ImDrawFlags.None, 2.0f); + } + + ImGui.PopStyleColor(4); + } + + ImGui.EndCombo(); + } else { + if (previewColor != 0) { + ImGui.PopStyleColor(2); + } + + // Draw gloss overlay on preview + if (previewGloss) { + var drawList = ImGui.GetWindowDrawList(); + var min = ImGui.GetItemRectMin(); + var max = ImGui.GetItemRectMax(); + drawList.AddRectFilledMultiColor(min, max, + 0x50FFFFFF, 0x50000000, 0x50FFFFFF, 0x50000000); + } + } + + // Right-click to clear + if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && currentStain != 0) { + stainDict[category] = 0; + changed = true; + } + + return changed; + } + + private static uint GetContrastColor(uint rgba) { + float r = ((rgba >> 0) & 0xFF) / 255.0f; + float g = ((rgba >> 8) & 0xFF) / 255.0f; + float b = ((rgba >> 16) & 0xFF) / 255.0f; + float luminance = 0.299f * r + 0.587f * g + 0.114f * b; + return luminance > 0.5f ? 0xFF000000 : 0xFFFFFFFF; + } + + private static uint AdjustBrightness(uint rgba, float factor) { + byte r = (byte)Math.Min(255, ((rgba >> 0) & 0xFF) * factor); + byte g = (byte)Math.Min(255, ((rgba >> 8) & 0xFF) * factor); + byte b = (byte)Math.Min(255, ((rgba >> 16) & 0xFF) * factor); + byte a = (byte)((rgba >> 24) & 0xFF); + return (uint)(r | (g << 8) | (b << 16) | (a << 24)); + } + private void ApplySelectedGearToCharacter() { if (Service.ObjectTable.LocalPlayer == null) { Service.ChatGui.PrintError("[Glamour Browser] No character found."); @@ -285,9 +449,11 @@ namespace Glamaholic.Ui { }; foreach (var (category, glamourerSlot) in slotMappings) { - if (SelectedItems[category].HasValue) { - var item = SelectedItems[category].Value; - Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); + if (SelectedItems[category] is { } item) { + var stain1 = SelectedStain1[category]; + var stain2 = SelectedStain2[category]; + var stains = new byte[] { stain1, stain2 }; + Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains); } } @@ -441,7 +607,10 @@ namespace Glamaholic.Ui { }; if (slotMapping.TryGetValue(category, out var glamourerSlot)) { - Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); + var stain1 = SelectedStain1[category]; + var stain2 = SelectedStain2[category]; + var stains = new byte[] { stain1, stain2 }; + Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains); } } catch (Exception ex) { Service.Log.Error(ex, "Failed to apply item to character");