Files
HSCompare/HSCompare/PluginUI.cs
T

364 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Inventory;
using Dalamud.Interface.Colors;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
namespace HSCompare;
public class PluginUI : IDisposable
{
private readonly Configuration _config;
private readonly IKeyState _keyState;
private readonly IDataManager _data;
private readonly IGameInventory _gameInventory;
private InvItem? _hoveredItem;
private bool _configVisible;
public PluginUI(Configuration config, IKeyState keyState, IDataManager data, IGameInventory gameInventory)
{
_config = config;
_keyState = keyState;
_data = data;
_gameInventory = gameInventory;
}
public void SetHoveredItem(InvItem? item) => _hoveredItem = item;
public void ToggleConfig() => _configVisible = !_configVisible;
public void Draw()
{
if (_configVisible)
{
DrawConfigWindow();
return;
}
if (!_keyState[_config.ModifierKey])
return;
var hovered = _hoveredItem;
if (hovered?.Item == null)
return;
var armoryType = GetArmoryType(hovered.Item);
if (armoryType == GameInventoryType.Invalid || armoryType == GameInventoryType.ArmorySoulCrystal || armoryType == GameInventoryType.Inventory1)
return;
var equipped = GetEquippedItemForSlot(armoryType);
if (equipped != null && equipped.Item.RowId == hovered.Item.RowId)
return;
DrawComparisonWindow(hovered, equipped);
}
private void DrawConfigWindow()
{
if (!ImGui.Begin("HSCompare Settings", ref _configVisible, ImGuiWindowFlags.AlwaysAutoResize))
{
ImGui.End();
return;
}
ImGui.Text("Modifier key (hold to show comparison):");
var currentKey = (VirtualKey)_config.ModifierKeyCode;
var keys = new[] { VirtualKey.SHIFT, VirtualKey.CONTROL, VirtualKey.MENU };
var names = new[] { "Shift", "Ctrl", "Alt" };
for (var i = 0; i < keys.Length; i++)
{
if (ImGui.RadioButton(names[i], currentKey == keys[i]))
{
_config.ModifierKey = keys[i];
_config.Save();
}
if (i < keys.Length - 1) ImGui.SameLine();
}
var fontScale = _config.TooltipFontScale;
if (ImGui.SliderFloat("Tooltip font scale", ref fontScale, 0.5f, 2f, "%.1f"))
{
_config.TooltipFontScale = fontScale;
_config.Save();
}
var minWidth = _config.TooltipMinWidth;
if (ImGui.SliderFloat("Tooltip min width", ref minWidth, 200f, 600f, "%.0f"))
{
_config.TooltipMinWidth = minWidth;
_config.Save();
}
var padding = _config.TooltipPadding;
if (ImGui.SliderFloat("Tooltip padding", ref padding, 4f, 24f, "%.0f"))
{
_config.TooltipPadding = padding;
_config.Save();
}
ImGui.Separator();
ImGui.TextWrapped("Hold the modifier key and hover over equipment to see comparison with your currently equipped item. Stat changes (if you equip the hovered item) appear below in green (gains) and red (losses).");
ImGui.End();
}
private void DrawComparisonWindow(InvItem hovered, InvItem? equipped)
{
var pad = _config.TooltipPadding;
var minW = _config.TooltipMinWidth;
var scale = _config.TooltipFontScale;
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new System.Numerics.Vector2(pad, pad));
ImGui.SetNextWindowSizeConstraints(new System.Numerics.Vector2(minW, 0), new System.Numerics.Vector2(800, 2000));
if (scale != 1f)
ImGui.GetIO().FontGlobalScale = scale;
var flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration
| ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav;
if (!ImGui.Begin("HSCompare", flags))
{
if (scale != 1f) ImGui.GetIO().FontGlobalScale = 1f;
ImGui.PopStyleVar();
return;
}
// Two columns: hovered item | currently equipped (labels on same line so rows align)
ImGui.Columns(2, "", false);
ImGui.SetColumnWidth(0, minW * 0.5f);
// Row 1: column labels (same height so item names align)
ImGui.Text("Hovered item");
ImGui.NextColumn();
ImGui.Text("Currently equipped");
ImGui.NextColumn();
// Row 2: item names
var nameStr = hovered.Item.Name.ToString() ?? "";
ImGui.TextColored(ImGui.ColorConvertFloat4ToU32(new System.Numerics.Vector4(0.4f, 1f, 0.4f, 1f)), nameStr);
if (hovered.IsHq)
ImGui.SameLine();
ImGui.TextColored(ImGui.ColorConvertFloat4ToU32(new System.Numerics.Vector4(1f, 0.84f, 0f, 1f)), hovered.IsHq ? " (HQ)" : "");
ImGui.NextColumn();
if (equipped != null)
{
var eqName = equipped.Item.Name.ToString() ?? "";
ImGui.TextColored(ImGui.ColorConvertFloat4ToU32(new System.Numerics.Vector4(0.4f, 1f, 0.4f, 1f)), eqName);
if (equipped.IsHq) ImGui.SameLine();
ImGui.TextColored(ImGui.ColorConvertFloat4ToU32(new System.Numerics.Vector4(1f, 0.84f, 0f, 1f)), equipped.IsHq ? " (HQ)" : "");
}
else
ImGui.TextColored(ImGuiColors.DalamudGrey, "(Empty slot)");
ImGui.NextColumn();
// Row 3: item level
ImGui.Text($"Item Level {hovered.Item.LevelItem.RowId}");
ImGui.NextColumn();
if (equipped != null)
ImGui.Text($"Item Level {equipped.Item.LevelItem.RowId}");
else
ImGui.Text("");
ImGui.NextColumn();
// Row 4+: main stats and bonuses (aligned: same stat order and line count both sides)
DrawItemStatsAligned(hovered, equipped);
ImGui.Columns(1);
// Stat difference section (below) — WoW style
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextColored(ImGui.ColorConvertFloat4ToU32(new System.Numerics.Vector4(1f, 0.92f, 0.4f, 1f)), "If you replace this item, the following stat changes will occur:");
ImGui.Spacing();
var statsHovered = GetItemStats(hovered);
var statsEquipped = equipped != null ? GetItemStats(equipped) : new Dictionary<byte, short>();
var allKeys = new HashSet<byte>(statsHovered.Keys);
foreach (var k in statsEquipped.Keys) allKeys.Add(k);
var any = false;
foreach (var key in allKeys)
{
var vH = statsHovered.TryGetValue(key, out var h) ? h : (short)0;
var vE = statsEquipped.TryGetValue(key, out var e) ? e : (short)0;
var delta = vH - vE;
if (delta == 0) continue;
any = true;
var name = BaseParamToName(key);
var sign = delta > 0 ? "+" : "";
var colorU32 = delta > 0 ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
ImGui.TextColored(colorU32, $"{sign}{delta} {name}");
}
if (!any && equipped != null)
ImGui.TextColored(ImGuiColors.DalamudGrey, "No stat change.");
// Position near cursor (below default tooltip)
var mouse = ImGui.GetMousePos();
var size = ImGui.GetWindowSize();
var pos = new System.Numerics.Vector2(mouse.X + 20, mouse.Y + 20);
if (pos.X + size.X > ImGui.GetIO().DisplaySize.X) pos.X = ImGui.GetIO().DisplaySize.X - size.X - 10;
if (pos.Y + size.Y > ImGui.GetIO().DisplaySize.Y) pos.Y = ImGui.GetIO().DisplaySize.Y - size.Y - 10;
if (pos.X < 10) pos.X = 10;
if (pos.Y < 10) pos.Y = 10;
ImGui.SetWindowPos(pos, ImGuiCond.Always);
ImGui.End();
if (scale != 1f) ImGui.GetIO().FontGlobalScale = 1f;
ImGui.PopStyleVar();
}
private void DrawItemStatsShort(InvItem inv)
{
var stats = GetItemStats(inv);
foreach (var kv in stats)
{
var name = BaseParamToName(kv.Key);
ImGui.Text($"+{kv.Value} {name}");
}
}
/// <summary>Draw main stats and bonuses for both items with the same stat order so rows align.</summary>
private void DrawItemStatsAligned(InvItem hovered, InvItem? equipped)
{
var statsH = GetItemStats(hovered);
var statsE = equipped != null ? GetItemStats(equipped) : new Dictionary<byte, short>();
var allKeys = new HashSet<byte>(statsH.Keys);
foreach (var k in statsE.Keys) allKeys.Add(k);
var orderedKeys = allKeys.OrderBy(k => k).ToList();
foreach (var key in orderedKeys)
{
var name = BaseParamToName(key);
var vH = statsH.TryGetValue(key, out var h) ? h : (short)0;
var vE = statsE.TryGetValue(key, out var e) ? e : (short)0;
ImGui.Text(vH != 0 ? $"+{vH} {name}" : "");
ImGui.NextColumn();
ImGui.Text(vE != 0 ? $"+{vE} {name}" : "");
ImGui.NextColumn();
}
}
private static GameInventoryType GetArmoryType(Item item)
{
var cat = item.EquipSlotCategory.Value;
if (cat.MainHand == 1) return GameInventoryType.ArmoryMainHand;
if (cat.OffHand == 1) return GameInventoryType.ArmoryOffHand;
if (cat.Head == 1) return GameInventoryType.ArmoryHead;
if (cat.Body == 1) return GameInventoryType.ArmoryBody;
if (cat.Gloves == 1) return GameInventoryType.ArmoryHands;
if (cat.Waist == 1) return GameInventoryType.ArmoryWaist;
if (cat.Legs == 1) return GameInventoryType.ArmoryLegs;
if (cat.Feet == 1) return GameInventoryType.ArmoryFeets;
if (cat.Ears == 1) return GameInventoryType.ArmoryEar;
if (cat.Neck == 1) return GameInventoryType.ArmoryNeck;
if (cat.Wrists == 1) return GameInventoryType.ArmoryWrist;
if (cat.FingerL == 1 || cat.FingerR == 1) return GameInventoryType.ArmoryRings;
if (cat.SoulCrystal == 1) return GameInventoryType.ArmorySoulCrystal;
return GameInventoryType.Inventory1;
}
private InvItem? GetEquippedItemForSlot(GameInventoryType armoryType)
{
var items = _gameInventory.GetInventoryItems(GameInventoryType.EquippedItems);
var sheet = _data.GetExcelSheet<Item>();
if (sheet == null) return null;
foreach (var entry in items)
{
var baseItemId = entry.ItemId;
if (baseItemId == 0) continue;
if (baseItemId > 1_000_000) baseItemId -= 1_000_000; // HQ flag
if (baseItemId > 2_000_000) continue; // invalid/collectible etc.
var item = sheet.GetRow(baseItemId);
if (item.RowId == 0) continue;
if (GetArmoryType(item) != armoryType) continue;
return new InvItem(item, entry.IsHq);
}
return null;
}
private static Dictionary<byte, short> GetItemStats(InvItem invItem)
{
var bonusMap = new Dictionary<byte, short>();
// Main stats (Defense, Damage, Block)
AddMainStat(bonusMap, (byte)ItemBonusType.DEFENSE, (short)invItem.Item.DefensePhys);
AddMainStat(bonusMap, (byte)ItemBonusType.MAGIC_DEFENSE, (short)invItem.Item.DefenseMag);
AddMainStat(bonusMap, (byte)ItemBonusType.PHYSICAL_DAMAGE, (short)invItem.Item.DamagePhys);
AddMainStat(bonusMap, (byte)ItemBonusType.MAGIC_DAMAGE, (short)invItem.Item.DamageMag);
AddMainStat(bonusMap, (byte)ItemBonusType.BLOCK_STRENGTH, (short)invItem.Item.Block);
AddMainStat(bonusMap, (byte)ItemBonusType.BLOCK_RATE, (short)invItem.Item.BlockRate);
// Bonuses (substats): BaseParam 0..5 and BaseParamValue 0..5
try
{
var baseParam = invItem.Item.BaseParam;
var baseParamValue = invItem.Item.BaseParamValue;
var n = Math.Min(baseParam.Count, baseParamValue.Count);
for (var i = 0; i < n; i++)
{
var paramRef = baseParam[i];
var value = (short)baseParamValue[i];
if (paramRef.RowId == 0 || value == 0) continue;
var rowId = (byte)paramRef.RowId;
if (bonusMap.TryGetValue(rowId, out var existing))
bonusMap[rowId] = (short)(existing + value);
else
bonusMap[rowId] = value;
}
}
catch { }
// HQ bonuses: BaseParamSpecial and BaseParamValueSpecial
if (invItem.IsHq)
{
try
{
var baseParamSpecial = invItem.Item.BaseParamSpecial;
var baseParamValueSpecial = invItem.Item.BaseParamValueSpecial;
var n = Math.Min(baseParamSpecial.Count, baseParamValueSpecial.Count);
for (var i = 0; i < n; i++)
{
var paramRef = baseParamSpecial[i];
var value = (short)baseParamValueSpecial[i];
if (paramRef.RowId == 0 || value == 0) continue;
var rowId = (byte)paramRef.RowId;
if (bonusMap.TryGetValue(rowId, out var existing))
bonusMap[rowId] = (short)(existing + value);
else
bonusMap[rowId] = value;
}
}
catch { }
}
return bonusMap;
}
private static void AddMainStat(Dictionary<byte, short> map, byte key, short value)
{
if (value <= 0) return;
if (map.TryGetValue(key, out var v))
map[key] = (short)(v + value);
else
map[key] = value;
}
private static string BaseParamToName(byte baseParam)
{
if (Enum.IsDefined(typeof(ItemBonusType), baseParam))
return ((ItemBonusType)baseParam).ToDescriptionString();
return $"Stat {baseParam}";
}
public void Dispose() { }
}