diff --git a/RelationshipsDashboard.lua b/RelationshipsDashboard.lua deleted file mode 100644 index 2ccd21f..0000000 --- a/RelationshipsDashboard.lua +++ /dev/null @@ -1,4248 +0,0 @@ -------------------------------------------------------------------------------- --- RelationshipsDashboard - Character Stats, GearScore & Repair Cost for Turtle WoW / OctoWoW --- Compatible with WoW 1.12.1 (Interface 11200) / Lua 5.0 -------------------------------------------------------------------------------- - --- Constants -local RELATIONSHIPS_DASHBOARD_VERSION = "1.0.0"; - --- Current unit being displayed. Defaults to "player"; switched to "target" --- when the InspectFrame opens on another player. -RD_UNIT = "player"; - --- Equipment slot IDs (vanilla 1.12) -local EQUIPMENT_SLOTS = { - 1, -- HeadSlot - 2, -- NeckSlot - 3, -- ShoulderSlot - 5, -- ChestSlot - 6, -- WaistSlot - 7, -- LegsSlot - 8, -- FeetSlot - 9, -- WristSlot - 10, -- HandsSlot - 11, -- Finger0Slot - 12, -- Finger1Slot - 13, -- Trinket0Slot - 14, -- Trinket1Slot - 15, -- BackSlot - 16, -- MainHandSlot - 17, -- OffHandSlot - 18, -- RangedSlot -}; - --- Slot names for display -local SLOT_NAMES = { - [1] = "Head", - [2] = "Neck", - [3] = "Shoulder", - [5] = "Chest", - [6] = "Waist", - [7] = "Legs", - [8] = "Feet", - [9] = "Wrist", - [10] = "Hands", - [11] = "Ring 1", - [12] = "Ring 2", - [13] = "Trinket 1", - [14] = "Trinket 2", - [15] = "Back", - [16] = "Main Hand", - [17] = "Off Hand", - [18] = "Ranged", -}; - --- Note: The GearScore formula (rarity * itemLevel) does not use slot weights or quality multipliers. --- These constants are kept for potential future use in advanced scoring modes. --- Current GearScore formula: score = rarity * itemLevel * (twoHand ? 2 : 1) - --- Resistance indices in vanilla WoW --- 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane -local RESISTANCE_INDICES = { - arcane = 6, - fire = 2, - nature = 3, - frost = 4, - shadow = 5, -}; - --- School indices for spell crit/hit (vanilla 1.12) --- 1=Physical, 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow, 7=Arcane -local SPELL_SCHOOL_INDEX = 6; -- Use Arcane as representative for generic spell stats - --- Tooltip scanner for item info -local RelationshipsDashboardTooltip = nil; - -------------------------------------------------------------------------------- --- Durability persistence system: Tracks durability loss from deaths, combat --- hits, and repair events. When GetInventoryItemDurability() fails (which is --- common on Turtle WoW / OctoWoW outside repair NPCs), we use these stored --- estimates as a fallback so the dashboard always shows meaningful values. --- --- How it works: --- 1. On PLAYER_ENTERING_WORLD and after each update, try to read actual --- durability via GetInventoryItemDurability or tooltip scanning. --- 2. When actual values are available, store them in RD_SavedDurability. --- 3. When a player dies (PLAYER_DEAD), reduce all items' estimated --- durability by 10% (standard WoW death penalty = 10% of max dur). --- 4. While in combat, increment hit counters. On UNIT_HEALTH damage taken, --- apply small durability decay proportional to hits received. --- 5. At repair NPCs, use GetRepairAllCost() as exact ground truth. --- 6. When displaying, use actual if available, else use saved estimates. -------------------------------------------------------------------------------- --- NOTE: RD_SavedDurability is intentionally GLOBAL (no `local`) so that it --- can be persisted through the SavedVariable RelationshipsDashboardDB_SavedDurability --- across /reload and sessions. Rebound in OnLoad after saved vars load. -RD_SavedDurability = {}; -- [slotId] = { cur = X, max = Y } -local RD_InCombat = false; -local RD_CombatHitCount = 0; -local RD_LastHealth = 0; -local RD_InBattleground = false; -- True while in a BG (no durability loss in BGs) -local RD_InDuel = false; -- True during a duel (no durability loss in duels) - --- Track ghost state so we can detect spirit-healer resurrection --- (25% durability loss to all equipped + inventory items). -RD_WasGhost = false; -RD_PendingSpiritRez = false; -RD_LastMoney = 0; -- Track for detecting non-merchant repair (e.g. guild bank repair) --- True while a repair-CAPABLE merchant window is open. Used to decide when a --- durability reading of "full" can be trusted (see CalculateDurabilityAndRepair). -RD_AtRepairMerchant = false; --- Per-character multiplier that scales our away-from-merchant repair estimate so --- it matches the exact cost the game charges. Learned automatically the first --- time the player opens a repair merchant with damaged gear. Persisted in --- RelationshipsDashboardDB.repairCalibration. -RD_RepairCalibration = 1.0; -local RD_REPAIR_CALIBRATION_VERSION = 2; - --- Battleground zone names for vanilla WoW 1.12 (English only — these match GetRealZoneText()) -local RD_BATTLEGROUND_ZONES = { - -- Blizzard vanilla battlegrounds - ["Alterac Valley"] = true, - ["Warsong Gulch"] = true, - ["Arathi Basin"] = true, - -- Turtle WoW / OctoWoW custom or renamed battlegrounds - ["Gurubashi Arena"] = true, - ["Silithus"] = false, -- world PvP zone, NOT a real BG; do not skip durability here - ["Warsong Battleground"] = true, - ["Sunnyglade Valley"] = true, -- Turtle WoW custom BG - ["Blackrock Depths (Arena)"] = true, - ["Lower Blackrock Spire"] = false, -- not a BG (guard against false positives) -}; - --- Extra localized BG names Turtle/OctoWoW clients ship with. Kept separate --- so the vanilla table above stays authoritative; both are consulted. -local RD_BATTLEGROUND_ZONES_EXTRA = { - -- German - ["Alteractal"] = true, - ["Kriegshymnenschlucht"] = true, - ["Arathibecken"] = true, - -- French - ["Vallee d'Alterac"] = true, - ["Vall\195\169e d'Alterac"] = true, - ["Goulet des Chanteguerres"] = true, - ["Bassin d'Arathi"] = true, - -- Spanish / Portuguese - ["Valle de Alterac"] = true, - ["Garganta Grito de Guerra"] = true, - ["Cuenca de Arathi"] = true, - -- Russian - [chr and chr(1040) or "\208\144"] = false, -- placeholder, real names below - ["\208\144\208\187\209\140\209\130\208\181\209\128\208\176\208\186\209\129\208\186\208\176\209\143 \208\180\208\190\208\187\208\184\208\189\208\176"] = true, -}; - --- Slots that can have durability (armor + weapons, NOT accessories) -local RD_DURABILITY_SLOTS = { - [1] = true, -- HeadSlot - [3] = true, -- ShoulderSlot - [5] = true, -- ChestSlot - [7] = true, -- LegsSlot - [8] = true, -- FeetSlot - [9] = true, -- WristSlot - [10] = true, -- HandsSlot - [15] = true, -- BackSlot (some cloaks have durability) - [16] = true, -- MainHandSlot - [17] = true, -- OffHandSlot - [18] = true, -- RangedSlot -}; - --- Default max durability for items when we can't read it --- Vanilla WoW items typically have 35-165 max durability based on slot -local RD_DEFAULT_MAX_DURABILITY = { - [1] = 85, -- HeadSlot - [3] = 85, -- ShoulderSlot - [5] = 135, -- ChestSlot - [7] = 105, -- LegsSlot - [8] = 85, -- FeetSlot - [9] = 55, -- WristSlot - [10] = 85, -- HandsSlot - [15] = 55, -- BackSlot - [16] = 105, -- MainHandSlot - [17] = 105, -- OffHandSlot - [18] = 85, -- RangedSlot -}; - --- Helper: Reset tooltip for fresh scan --- In vanilla 1.12, tooltips can get "stuck" with stale data --- Always call this before SetInventoryItem or SetHyperlink -local function RelationshipsDashboard_ResetTooltip() - if RelationshipsDashboardTooltip then - RelationshipsDashboardTooltip:SetOwner(WorldFrame, "ANCHOR_NONE"); - RelationshipsDashboardTooltip:ClearLines(); - end -end - --- Player class cached on login -local RelationshipsDashboard_PlayerClass = nil; - -------------------------------------------------------------------------------- --- Helper: Check if the player is currently in a battleground --- In vanilla WoW 1.12, players do NOT take durability loss from death or --- combat while in a battleground. We check using zone name and also --- MiniMapBattlefieldFrame visibility as a fallback. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_IsInBattleground() - -- Method 1: Zone name check (English + a few localized variants) - local zone = GetRealZoneText and GetRealZoneText() or nil; - if zone and (RD_BATTLEGROUND_ZONES[zone] or RD_BATTLEGROUND_ZONES_EXTRA[zone]) then - return true; - end - local subZone = GetSubZoneText and GetSubZoneText() or nil; - if subZone and (RD_BATTLEGROUND_ZONES[subZone] or RD_BATTLEGROUND_ZONES_EXTRA[subZone]) then - return true; - end - -- Method 2: PvP zone info reports "combat" for active battlefields - if GetZonePVPInfo then - local pvpType = GetZonePVPInfo(); - if pvpType == "combat" then - return true; - end - end - -- Method 3: Ask the battlefield queue system directly. In 1.12 there are - -- up to MAX_BATTLEFIELD_QUEUES (3) slots. A status of "active" means the - -- player is currently inside that battleground. This is the most reliable - -- signal on Turtle WoW / OctoWoW even when zone strings do not match. - if GetBattlefieldStatus then - local maxQueues = MAX_BATTLEFIELD_QUEUES or 3; - for i = 1, maxQueues do - local status = GetBattlefieldStatus(i); - if status == "active" then - return true; - end - end - end - -- Method 4: MiniMapBattlefieldFrame is shown when inside a BG (fallback) - if MiniMapBattlefieldFrame and MiniMapBattlefieldFrame:IsVisible() then - local status = MiniMapBattlefieldFrame.status; - if status == "active" then - return true; - end - end - return false; -end - --- Snapshot of equipped durability captured on BG entry so we can restore --- exactly if any event mutates our tracker mid-BG (deaths, spirit rez, etc). -RD_BG_DurabilitySnapshot = nil; - -local function RelationshipsDashboard_SnapshotDurabilityForBG() - RD_BG_DurabilitySnapshot = {}; - for slotId, _ in pairs(RD_DURABILITY_SLOTS) do - local saved = RD_SavedDurability and RD_SavedDurability[slotId]; - if saved and saved.max then - RD_BG_DurabilitySnapshot[slotId] = { cur = saved.cur, max = saved.max }; - end - end -end - -local function RelationshipsDashboard_RestoreDurabilityFromBGSnapshot() - if not RD_BG_DurabilitySnapshot then return; end - for slotId, snap in pairs(RD_BG_DurabilitySnapshot) do - local saved = RD_SavedDurability and RD_SavedDurability[slotId]; - if saved then - saved.cur = snap.cur; - saved.max = snap.max; - end - end -end --- Tracked mana values for when druid shifts to feral forms --- In vanilla 1.12, UnitMana(RD_UNIT) returns the current form's power only --- We store the caster-form mana to display it alongside Energy/Rage -local RelationshipsDashboard_LastKnownMana = 0; -local RelationshipsDashboard_LastKnownMaxMana = 0; - -------------------------------------------------------------------------------- --- Helper: Check if player's class uses mana (has a mana pool regardless of form) --- This is class-based, not form-based, so druids in feral forms still return true -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetUnitClass() - -- Get class for RD_UNIT (defaults to "player", switches to "target" during inspect) - local _, class = UnitClass(RD_UNIT); - if not class or class == "" then - if RD_UNIT == "player" then - if not RelationshipsDashboard_PlayerClass then - _, RelationshipsDashboard_PlayerClass = UnitClass("player"); - end - class = RelationshipsDashboard_PlayerClass; - end - end - -- Cache player class when we can - if RD_UNIT == "player" and class and class ~= "" then - RelationshipsDashboard_PlayerClass = class; - end - return class; -end - -local function RelationshipsDashboard_ClassUsesMana() - local class = RelationshipsDashboard_GetUnitClass(); - return class == "MAGE" - or class == "WARLOCK" - or class == "PRIEST" - or class == "DRUID" - or class == "SHAMAN" - or class == "PALADIN" - or class == "HUNTER"; -end - -------------------------------------------------------------------------------- --- Helper: Check if unit is currently displaying mana (powerType == 0) -------------------------------------------------------------------------------- -local function RelationshipsDashboard_IsManaUser() - local powerType = UnitPowerType(RD_UNIT); - return powerType == 0; -end - -------------------------------------------------------------------------------- --- Helper: Check if unit uses spell power (caster class) -------------------------------------------------------------------------------- -local function RelationshipsDashboard_IsCaster() - local class = RelationshipsDashboard_GetUnitClass(); - return class == "MAGE" - or class == "WARLOCK" - or class == "PRIEST" - or class == "DRUID" - or class == "SHAMAN" - or class == "PALADIN"; -end - -------------------------------------------------------------------------------- --- Helper: Check if unit uses ranged weapons (Hunter primarily) -------------------------------------------------------------------------------- -local function RelationshipsDashboard_HasRanged() - local class = RelationshipsDashboard_GetUnitClass(); - return class == "HUNTER" - or class == "WARRIOR" - or class == "ROGUE"; -end - -------------------------------------------------------------------------------- --- Dynamic layout system: Re-anchors all visible rows sequentially after --- class visibility is applied. This eliminates gaps from hidden rows --- (e.g., hidden Ranged stats in Enhancements, hidden Block in Defenses). --- --- Layout order definition: --- Each section = { headerName, { row1, row2, ... } } --- Rows are re-anchored TOPLEFT to the previous visible element's BOTTOMLEFT. --- Section headers get a -4 spacing gap from the previous row. -------------------------------------------------------------------------------- -local RelationshipsDashboard_LayoutSections = { - { "", { - "RelationshipsDashboardGearScoreRow", - "RelationshipsDashboardAvgILvlRow", - }}, - { "RDHeaderGeneral", { - "RelationshipsDashboardHealthRow", - "RelationshipsDashboardPowerRow", - "RelationshipsDashboardManaRow", - "RelationshipsDashboardArmorRow", - "RelationshipsDashboardMovementRow", - "RelationshipsDashboardDurabilityRow", - "RelationshipsDashboardRepairCostRow", - }}, - { "RDHeaderAttributes", { - "RelationshipsDashboardStrengthRow", - "RelationshipsDashboardAgilityRow", - "RelationshipsDashboardStaminaRow", - "RelationshipsDashboardIntellectRow", - "RelationshipsDashboardSpiritRow", - }}, - { "RDHeaderOffense", { - "RelationshipsDashboardMeleeDmgRow", - "RelationshipsDashboardMeleeAPRow", - "RelationshipsDashboardMeleeSpeedRow", - "RelationshipsDashboardWeaponDPSRow", - "RelationshipsDashboardWeaponSkillRow", - "RelationshipsDashboardMeleeHitRow", - "RelationshipsDashboardMeleeCritRow", - }}, - { "RDHeaderEnhancements", { - "RelationshipsDashboardRangedAPRow", - "RelationshipsDashboardRangedCritRow", - "RelationshipsDashboardRangedSpeedRow", - "RelationshipsDashboardRangedDmgRow", - "RelationshipsDashboardSpellPowerRow", - "RelationshipsDashboardHealingPowerRow", - "RelationshipsDashboardSpellHitRow", - "RelationshipsDashboardSpellCritRow", - "RelationshipsDashboardManaRegenRow", - }}, - { "RDHeaderDefenses", { - "RelationshipsDashboardDefenseRow", - "RelationshipsDashboardDodgeRow", - "RelationshipsDashboardParryRow", - "RelationshipsDashboardBlockRow", - "RelationshipsDashboardAvoidanceRow", - }}, - { "RDHeaderResistances", { - "RelationshipsDashboardArcaneResRow", - "RelationshipsDashboardFireResRow", - "RelationshipsDashboardNatureResRow", - "RelationshipsDashboardFrostResRow", - "RelationshipsDashboardShadowResRow", - }}, -}; - -------------------------------------------------------------------------------- --- Helper: iterate every frame that lives inside the scrollable stat content. --- Vanilla 1.12 ScrollFrames do not clip nested child Frames reliably, so these --- frames need explicit level management and viewport culling. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ForEachScrollElement(callback) - for secIdx = 1, table.getn(RelationshipsDashboard_LayoutSections) do - local section = RelationshipsDashboard_LayoutSections[secIdx]; - local headerName = section[1]; - local rowNames = section[2]; - - if headerName and headerName ~= "" then - local header = getglobal(headerName); - if header then - callback(header); - end - end - - for rowIdx = 1, table.getn(rowNames) do - local row = getglobal(rowNames[rowIdx]); - if row then - callback(row); - end - end - end -end - -local function RelationshipsDashboard_SetLayoutShown(element, shown) - if not element then return; end - - -- rdLayoutShown is the real layout intent. Do NOT infer layout from - -- IsShown(), because viewport clipping also uses Hide()/Show() and can - -- temporarily hide otherwise-valid rows while they are scrolled out. - element.rdLayoutShown = shown; - element.rdIntendedShown = shown; - - if shown then - if not element:IsShown() then element:Show(); end - else - if element:IsShown() then element:Hide(); end - end -end - -------------------------------------------------------------------------------- --- Hard clip workaround for Vanilla/Turtle clients. --- The ScrollFrame viewport clips the ScrollChild's own regions, but child Frames --- inside that ScrollChild can still draw outside the viewport. We hide any row --- or header that is not fully inside the scroll viewport, preventing text from --- drawing above the title bar or below the panel. -------------------------------------------------------------------------------- -function RelationshipsDashboard_UpdateViewportClip() - if not RelationshipsDashboardScrollFrame then return; end - - local viewTop = RelationshipsDashboardScrollFrame:GetTop(); - local viewBottom = RelationshipsDashboardScrollFrame:GetBottom(); - if not viewTop or not viewBottom then return; end - - -- Vanilla/Turtle 1.12: SetAlpha(0) does NOT cascade to child FontStrings - -- inside a Frame, so alpha-clipping leaves overlapping labels visible when - -- rows scroll out of the viewport. We must Hide()/Show() the actual row - -- frames instead. To avoid re-showing rows that ApplyClassVisibility - -- intentionally hid (e.g. Ranged stats for casters), we consult the - -- `rdIntendedShown` flag set at the end of ApplyClassVisibility. - RelationshipsDashboard_ForEachScrollElement(function(element) - -- Never override an intentional class-based hide. - if element.rdIntendedShown == false then - if element:IsShown() then element:Hide(); end - return; - end - - local elemTop = element:GetTop(); - local elemBottom = element:GetBottom(); - local inView = elemTop and elemBottom - and elemTop <= (viewTop + 0.5) - and elemBottom >= (viewBottom - 0.5); - - if inView then - if not element:IsShown() then element:Show(); end - element:SetAlpha(1); - else - if element:IsShown() then element:Hide(); end - end - end); -end - -------------------------------------------------------------------------------- --- Show/Hide stats based on class role, then re-anchor all visible rows --- so hidden rows don't leave gaps in the layout. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ApplyClassVisibility() - local class = RelationshipsDashboard_GetUnitClass(); - local isCaster = RelationshipsDashboard_IsCaster(); - local hasRanged = RelationshipsDashboard_HasRanged(); - local isManaUser = RelationshipsDashboard_IsManaUser(); - local classUsesMana = RelationshipsDashboard_ClassUsesMana(); - - -- Reset every header/row to its normal layout state before applying - -- class-specific hides. This deliberately undoes viewport clipping hides; - -- clipping is re-applied after the rows are re-anchored below. - RelationshipsDashboard_ForEachScrollElement(function(element) - RelationshipsDashboard_SetLayoutShown(element, true); - end); - - -- ManaRow: Show for classes that have a mana pool, but only when - -- the current power type is NOT mana (druid in feral form shows - -- Energy/Rage on PowerRow + Mana on separate ManaRow). - -- When already showing mana on PowerRow, hide the duplicate ManaRow. - if classUsesMana and not isManaUser then - -- Class has mana but current form shows different power (e.g., druid bear/cat) - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardManaRow, true); - else - -- Either already showing mana on PowerRow, or class doesn't use mana at all - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardManaRow, false); - end - - -- Ranged stats: show for Hunter/Warrior/Rogue, hide for others - if hasRanged then - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedAPRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedCritRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedSpeedRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedDmgRow, true); - else - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedAPRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedCritRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedSpeedRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardRangedDmgRow, false); - end - - -- Spell stats: show for mana users/casters, hide for pure melee - if isCaster or classUsesMana then - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardSpellPowerRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardHealingPowerRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardSpellHitRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardSpellCritRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardManaRegenRow, true); - else - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardSpellPowerRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardHealingPowerRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardSpellHitRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardSpellCritRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardManaRegenRow, false); - end - - -- Melee stats: hide for pure casters (Mage, Warlock, Priest) - if class == "MAGE" or class == "WARLOCK" or class == "PRIEST" then - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeDmgRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeAPRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeSpeedRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardWeaponDPSRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardWeaponSkillRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeHitRow, false); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeCritRow, false); - else - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeDmgRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeAPRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeSpeedRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardWeaponDPSRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardWeaponSkillRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeHitRow, true); - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardMeleeCritRow, true); - end - - -- Block: only show for Warrior/Paladin/Shaman (classes that can use shields) - if class == "WARRIOR" or class == "PALADIN" or class == "SHAMAN" then - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardBlockRow, true); - else - RelationshipsDashboard_SetLayoutShown(RelationshipsDashboardBlockRow, false); - end - - -- ==== Dynamic re-anchoring: eliminate gaps from hidden rows ==== - -- Walk through all sections and rows, re-anchor each visible element - -- to the bottom of the previous visible element. - local prevElement = nil; -- last visible frame we anchored to - local stripeOn = false; -- alternating row stripe state - - for secIdx = 1, table.getn(RelationshipsDashboard_LayoutSections) do - local section = RelationshipsDashboard_LayoutSections[secIdx]; - local headerName = section[1]; - local rowNames = section[2]; - local header = getglobal(headerName); - - -- Count visible rows in this section BEFORE deciding whether to show its header. - -- Without this, class-hidden sections still render a title bar that visually - -- collides with rows from adjacent sections after re-anchoring. - local sectionHasVisibleRow = false; - for rowIdx = 1, table.getn(rowNames) do - local r = getglobal(rowNames[rowIdx]); - if r and r.rdLayoutShown ~= false then - sectionHasVisibleRow = true; - break; - end - end - - if header and sectionHasVisibleRow then - if prevElement then - -- Section header: anchor below previous visible element with gap - header:ClearAllPoints(); - header:SetPoint("TOPLEFT", prevElement, "BOTTOMLEFT", 0, -4); - else - -- First section: anchor to content top - header:ClearAllPoints(); - header:SetPoint("TOPLEFT", RelationshipsDashboardContent, "TOPLEFT", 0, 0); - end - RelationshipsDashboard_SetLayoutShown(header, true); - prevElement = header; - elseif header then - -- Every row in this section is class-hidden — hide the title too. - RelationshipsDashboard_SetLayoutShown(header, false); - end - - for rowIdx = 1, table.getn(rowNames) do - local rowName = rowNames[rowIdx]; - local row = getglobal(rowName); - if row and row.rdLayoutShown ~= false then - row:ClearAllPoints(); - if prevElement then - row:SetPoint("TOPLEFT", prevElement, "BOTTOMLEFT", 0, 0); - else - -- No previous element (first row, no section header): anchor to content top - row:SetPoint("TOPLEFT", RelationshipsDashboardContent, "TOPLEFT", 0, 0); - end - prevElement = row; - - -- Apply alternating row stripes - local bg = getglobal(rowName .. "BG"); - if bg then - if stripeOn then - bg:Show(); - bg:SetAlpha(0.04); - row.stripeOn = true; - else - bg:Hide(); - row.stripeOn = false; - end - end - stripeOn = not stripeOn; - end - end - end - - -- Update scroll child height based on last visible element. - -- IMPORTANT: do NOT clamp tall content down to the visible viewport. If the - -- ScrollChild is shorter than the actual rows, Vanilla lets nested row Frames - -- bleed outside the frame while the ScrollFrame has the wrong scroll range. - if prevElement then - local scrollFrame = RelationshipsDashboardScrollFrame; - local scrollVisibleHeight = scrollFrame:GetHeight() or 400; - local contentTop = RelationshipsDashboardContent:GetTop() or 0; - local elemBottom = prevElement:GetBottom() or 0; - local usedHeight = math.ceil(contentTop - elemBottom) + 10; - if usedHeight < 100 then - usedHeight = 100; - end - if usedHeight < scrollVisibleHeight then - usedHeight = scrollVisibleHeight; - end - RelationshipsDashboardContent:SetHeight(usedHeight); - - if scrollFrame.UpdateScrollChildRect then - scrollFrame:UpdateScrollChildRect(); - end - - local scrollBar = getglobal("RelationshipsDashboardScrollFrameScrollBar"); - if scrollBar then - local minVal, maxVal = scrollBar:GetMinMaxValues(); - if scrollBar:GetValue() > maxVal then - scrollBar:SetValue(maxVal); - elseif scrollBar:GetValue() < minVal then - scrollBar:SetValue(minVal); - end - end - end - - -- Snapshot which elements are legitimately shown right now (post class-filter - -- + relayout). UpdateViewportClip uses this to decide which rows may be - -- Show()n back into the viewport during scrolling, and which must stay - -- hidden because a class role rule hid them. - RelationshipsDashboard_ForEachScrollElement(function(element) - element.rdIntendedShown = (element.rdLayoutShown ~= false); - end); - - RelationshipsDashboard_BoostMaskLevels(); - RelationshipsDashboard_UpdateViewportClip(); -end - -------------------------------------------------------------------------------- --- Helper: Extract item ID from an item link string --- Vanilla 1.12 item link format: |cffXXXXXX|Hitem:itemId:enchantId:suffixId:uniqueId|h[Name]|h|r --- We need to parse out the numeric item ID for database lookup -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ExtractItemID(itemLink) - if not itemLink then return nil; end - local _, _, itemID = strfind(itemLink, "item:(%d+):%d+:%d+:%d+"); - if itemID then - return tonumber(itemID); - end - return nil; -end - -------------------------------------------------------------------------------- --- Helper: Safe wrapper for SetInventoryItem that catches errors -------------------------------------------------------------------------------- -local function RelationshipsDashboardTooltip_SetInventoryItemSafe(unit, slot) - if not RelationshipsDashboardTooltip then return false; end - local ok, err = pcall(function() RelationshipsDashboardTooltip:SetInventoryItem(unit, slot); end); - return ok; -end - -------------------------------------------------------------------------------- --- Helper: Format money (copper) to gold/silver/copper string -------------------------------------------------------------------------------- -local function RelationshipsDashboard_FormatMoney(copper) - if copper <= 0 then - return "|cff00ff000g 0s 0c|r"; - end - local gold = floor(copper / 10000); - local silver = floor(mod(copper, 10000) / 100); - local copperRem = mod(copper, 100); - local result = ""; - if gold > 0 then - result = result .. "|cffffff00" .. gold .. "g|r "; - end - if silver > 0 or gold > 0 then - result = result .. "|cffc0c0c0" .. silver .. "s|r "; - end - result = result .. "|cffcc8844" .. copperRem .. "c|r"; - return result; -end - -------------------------------------------------------------------------------- --- Helper: Format percentage -------------------------------------------------------------------------------- -local function RelationshipsDashboard_FormatPercent(value) - return string.format("%.2f%%", value); -end - -------------------------------------------------------------------------------- --- Helper: Format decimal -------------------------------------------------------------------------------- -local function RelationshipsDashboard_FormatDecimal(value) - return string.format("%.2f", value); -end - -------------------------------------------------------------------------------- --- Helper: Get item quality from item link --- Item links in vanilla: |Hitem:itemId:enchantId:suffixId:uniqueId|h[name]|h[qualityCode] --- Actually in 1.12: |cffXXXXXX|Hitem:itemId:enchant:suffix:unique|h[Name]|h|r --- The color code maps to quality: --- |cff9d9d9d = 0 (Poor), |cffffffff = 1 (Common), |cff1eff00 = 2 (Uncommon) --- |cff0070dd = 3 (Rare), |cffa335ee = 4 (Epic), |cffff8000 = 5 (Legendary) -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetItemQualityFromLink(itemLink) - if not itemLink then return 0; end - -- Match the color prefix to determine quality - local quality = 0; - if strfind(itemLink, "|cff9d9d9d") then - quality = 0; - elseif strfind(itemLink, "|cffffffff") then - quality = 1; - elseif strfind(itemLink, "|cff1eff00") then - quality = 2; - elseif strfind(itemLink, "|cff0070dd") then - quality = 3; - elseif strfind(itemLink, "|cffa335ee") then - quality = 4; - elseif strfind(itemLink, "|cffff8000") then - quality = 5; - else - -- Fallback: try to use GetItemQualityColor if we can parse itemId - -- This shouldn't normally happen - quality = 1; - end - return quality; -end - -------------------------------------------------------------------------------- --- Helper: Get item level using the built-in item database --- Primary method: Look up itemID in RelationshipsDashboardDB_ItemLevels --- Fallback method: Try GetItemInfo (works for cached items) --- Last resort: Tooltip scanning --- This is the proven approach for vanilla 1.12 where GetItemInfo is unreliable -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetItemLevel(itemLink) - if not itemLink then return 0; end - - -- Method 1: Extract itemID and look up in our database (most reliable) - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - if itemID and RelationshipsDashboardDB_ItemLevels then - local dbLevel = RelationshipsDashboardDB_ItemLevels[itemID]; - if dbLevel and dbLevel > 0 then - return dbLevel; - end - end - - -- Method 2: Try GetItemInfo (works for items cached in current session) - local _, _, _, itemLevel = GetItemInfo(itemLink); - if itemLevel and itemLevel > 0 then - return itemLevel; - end - - -- Method 3: Tooltip scanning as last resort - if RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - local ok, err = pcall(function() RelationshipsDashboardTooltip:SetHyperlink(itemLink); end); - if ok then - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, iLvl = strfind(text, "Item Level (%d+)"); - if iLvl then - return tonumber(iLvl); - end - end - end - end - end - end - - return 0; -end - -------------------------------------------------------------------------------- --- Helper: Check if an item is a two-handed weapon --- Uses itemEquipLoc from GetItemInfo (equipped items are always cached) --- INVTYPE_2HWEAPON is the equip location for two-hand weapons -------------------------------------------------------------------------------- -local function RelationshipsDashboard_IsTwoHandWeapon(itemLink) - if not itemLink then return false; end - -- Vanilla 1.12 GetItemInfo returns: name(1), link(2), quality(3), iLevel(4), - -- minLevel(5), type(6), subType(7), stackCount(8), equipLoc(9), texture(10). - -- The equipLoc is the 9th return, not the 8th. - local _, _, _, _, _, _, _, _, itemEquipLoc = GetItemInfo(itemLink); - if itemEquipLoc == "INVTYPE_2HWEAPON" then - return true; - end - return false; -end - -------------------------------------------------------------------------------- --- Calculate GearScore based on equipped items --- Formula: score = rarity * itemLevel per item, x2 bonus for two-hand weapons --- Also calculates average item level from the database -------------------------------------------------------------------------------- -local function RelationshipsDashboard_CalculateGearScore() - local totalScore = 0; - local totalILvl = 0; - local equippedCount = 0; -- items with a known item level (for avg ilvl) - local slotCount = 0; -- items equipped at all - - -- GearScoreLite-style quality scalar (rewards higher rarities much more, - -- so a low-level player in white/green gear can't out-score a mid-level - -- character in blue/purple gear like on real WoW). - local qualityScalar = { - [0] = 0.00, - [1] = 0.005, -- Common (white) - [2] = 0.25, -- Uncommon (green) - [3] = 0.50, -- Rare (blue) - [4] = 0.75, -- Epic (purple) - [5] = 1.00, -- Legendary (orange) - }; - - -- Slot weights approximate the TBC / WotLK GearScore standard - local slotWeight = { - [1] = 1.0000, [2] = 0.5625, [3] = 0.7500, [5] = 1.0000, - [6] = 0.7500, [7] = 1.0000, [8] = 0.7500, [9] = 0.5625, - [10] = 0.7500, [11] = 0.5625, [12] = 0.5625, [13] = 0.5625, - [14] = 0.5625, [15] = 0.5625, [16] = 1.0000, [17] = 1.0000, - [18] = 0.3164, - }; - - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink then - slotCount = slotCount + 1; - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - local itemLevel = 0; - if itemID and RelationshipsDashboardDB_ItemLevels then - itemLevel = RelationshipsDashboardDB_ItemLevels[itemID] or 0; - end - if itemLevel == 0 then - local _, _, _, giItemLevel = GetItemInfo(itemLink); - if giItemLevel and giItemLevel > 0 then - itemLevel = giItemLevel; - end - end - - -- Fallback for items missing from the DB and uncached from - -- GetItemInfo (common on Turtle WoW custom items, ESPECIALLY the - -- 2H weapon the player just equipped). Approximate from the - -- wearer's level so the score still reflects the equip/unequip. - if itemLevel == 0 then - local plvl = UnitLevel(RD_UNIT) or 1; - if plvl < 1 then plvl = 1; end - itemLevel = plvl; - end - - local quality = RelationshipsDashboard_GetItemQualityFromLink(itemLink); - local qf = qualityScalar[quality] or 0.005; - local sw = slotWeight[slotId] or 0.5625; - - -- Discount very low ilvl items (matches retail formula behavior) - local ilvlBase = itemLevel - 4; - if ilvlBase < 0 then ilvlBase = 0; end - - -- 9th return of GetItemInfo is equipLoc in vanilla 1.12. - local _, _, _, _, _, _, _, _, itemEquipLoc = GetItemInfo(itemLink); - local twoHandMult = 1; - if itemEquipLoc == "INVTYPE_2HWEAPON" then - twoHandMult = 2; - end - - -- Scale factor tuned so a full T2 (ilvl 76 epic) player lands - -- around ~2800 GearScore, a full BiS pre-raid ~2000, greens - -- at level 60 ~800-1000, and level 20 greens ~150. - local itemScore = ilvlBase * qf * sw * twoHandMult * 8; - - totalScore = totalScore + itemScore; - totalILvl = totalILvl + itemLevel; - equippedCount = equippedCount + 1; - end - end - - local avgILvl = 0; - if equippedCount > 0 then - avgILvl = totalILvl / equippedCount; - end - - return floor(totalScore), avgILvl, equippedCount; -end - -------------------------------------------------------------------------------- --- Calculate durability and repair costs --- Primary method: GetInventoryItemDurability (vanilla 1.12 API, works without repair NPC) --- Fallback: Tooltip scanning for "Durability: XX / XX" pattern --- Repair cost is estimated from durability ratio, item quality, item level, and slot type --- When a repair merchant is open, GetRepairAllCost() provides the exact cost -------------------------------------------------------------------------------- - --- Slots that never have durability in vanilla WoW (accessories) -local SLOTS_NO_DURABILITY = { - [2] = true, -- NeckSlot - [11] = true, -- Finger0Slot - [12] = true, -- Finger1Slot - [13] = true, -- Trinket0Slot - [14] = true, -- Trinket1Slot -}; - --- Slot type multipliers for repair cost estimation --- These represent the relative cost weight of each armor slot --- Based on vanilla WoW's internal repair cost distribution -local SLOT_REPAIR_MULTIPLIER = { - [1] = 0.88, -- HeadSlot - [3] = 0.75, -- ShoulderSlot - [5] = 1.00, -- ChestSlot (highest) - [6] = 0.50, -- WaistSlot (no durability in vanilla, but just in case) - [7] = 0.88, -- LegsSlot - [8] = 0.75, -- FeetSlot - [9] = 0.50, -- WristSlot - [10] = 0.75, -- HandsSlot - [15] = 0.50, -- BackSlot (usually no durability, but some cloaks do) - [16] = 0.38, -- MainHandSlot - [17] = 0.38, -- OffHandSlot - [18] = 0.38, -- RangedSlot -}; - --- DBC-backed repair multipliers from modern Classic Era DurabilityCosts.dbc. --- Server formula used by newer cores: --- repairCopper = lostDurability * DurabilityCosts[itemLevel][class/subclass] --- * DurabilityQuality[(quality + 1) * 2] --- This is much closer to WoW/Turtle/OctoWoW repair totals than a generic --- ilvl/slot approximation, and it naturally takes the equipped gear type, --- quality, and item level into account. -local RD_DBC_REPAIR_ARMOR = { - 1,1,1,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5, - 5,5,5,6,6,6,7,7,7,8,8,9,9,10,11,12,13,14,15,16, - 17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55, - 57,59,61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95, - 97,99,101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135, -}; - -local RD_DBC_REPAIR_SHIELD = { - 1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6, - 6,6,7,7,7,8,8,9,9,10,11,12,13,14,15,16,17,19,21,23, - 25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63, - 65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99,101,103, - 105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139,42,43, -}; - -local RD_DBC_REPAIR_WEAPON = { - 1,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5, - 5,6,6,6,7,7,7,8,8,9,9,10,11,12,13,14,15,16,17,19, - 21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59, - 61,63,65,67,69,71,73,75,77,79,81,83,85,87,89,91,93,95,97,99, - 101,103,105,107,109,111,113,115,117,119,121,123,125,127,129,131,133,135,137,139, -}; - -local RD_DBC_REPAIR_WEAPON_2H = { - 1,1,1,1,1,3,3,3,3,4,4,4,4,6,6,6,6,7,7,7, - 7,9,9,9,10,10,10,12,12,13,13,15,16,18,19,21,22,24,25,28, - 31,34,37,40,43,46,49,52,55,58,61,64,67,70,73,76,79,82,85,88, - 91,94,97,100,103,106,109,112,115,118,121,124,127,130,133,136,139,142,145,148, - 151,154,157,160,163,166,169,172,175,178,181,184,187,190,193,196,199,202,205,208, -}; - -local RD_DBC_REPAIR_WEAPON_SMALL = { - 1,1,1,1,1,1,1,1,1,2,2,2,2,3,3,3,3,3,3,3, - 3,4,4,4,5,5,5,6,6,6,6,7,8,9,9,10,11,12,12,14, - 15,17,18,20,21,23,24,26,27,29,30,32,33,35,36,38,39,41,42,44, - 45,47,48,50,51,53,54,56,57,59,60,62,63,65,66,68,69,71,72,74, - 75,77,78,80,81,83,84,86,87,89,90,92,93,95,96,98,99,101,102,104, -}; - -local RD_DBC_REPAIR_FISHING = { - 1,1,1,1,1,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4, - 4,4,4,4,5,5,5,6,6,6,7,7,8,8,8,9,9,9,9,9, - 9,10,10,10,11,11,11,12,12,12,13,13,13,14,14,14,14,14,14,15, - 15,15,16,16,16,17,17,17,18,18,18,18,18,18,19,19,19,20,20,20, - 21,21,21,22,22,22,23,23,23,23,23,23,24,24,24,25,25,25,26,26, -}; - -local RD_DBC_REPAIR_QUALITY = { - [0] = 0.60, -- Poor - [1] = 0.80, -- Common - [2] = 1.00, -- Uncommon - [3] = 1.25, -- Rare - [4] = 2.50, -- Epic - [5] = 3.00, -- Legendary -}; - -local function RelationshipsDashboard_GetRepairMultiplier(slotId, itemLink, itemLevel) - local ilvl = itemLevel or 1; - if ilvl < 1 then ilvl = 1; end - if ilvl > 100 then ilvl = 100; end - ilvl = floor(ilvl + 0.5); - - local _, _, _, _, _, itemType, itemSubType, _, itemEquipLoc = GetItemInfo(itemLink); - local subTypeLower = itemSubType and strlower(itemSubType) or ""; - - if itemEquipLoc == "INVTYPE_SHIELD" or strfind(subTypeLower, "shield") then - return RD_DBC_REPAIR_SHIELD[ilvl] or 0; - end - - if itemType == "Armor" - or itemEquipLoc == "INVTYPE_HEAD" - or itemEquipLoc == "INVTYPE_SHOULDER" - or itemEquipLoc == "INVTYPE_CHEST" - or itemEquipLoc == "INVTYPE_ROBE" - or itemEquipLoc == "INVTYPE_WAIST" - or itemEquipLoc == "INVTYPE_LEGS" - or itemEquipLoc == "INVTYPE_FEET" - or itemEquipLoc == "INVTYPE_WRIST" - or itemEquipLoc == "INVTYPE_HAND" - or itemEquipLoc == "INVTYPE_CLOAK" - or (slotId ~= 16 and slotId ~= 17 and slotId ~= 18) then - return RD_DBC_REPAIR_ARMOR[ilvl] or 0; - end - - if strfind(subTypeLower, "fishing") then - return RD_DBC_REPAIR_FISHING[ilvl] or 0; - end - if strfind(subTypeLower, "dagger") or strfind(subTypeLower, "wand") then - return RD_DBC_REPAIR_WEAPON_SMALL[ilvl] or 0; - end - if itemEquipLoc == "INVTYPE_2HWEAPON" - or strfind(subTypeLower, "two%-hand") - or strfind(subTypeLower, "staff") - or strfind(subTypeLower, "staves") - or strfind(subTypeLower, "polearm") - or strfind(subTypeLower, "crossbow") then - return RD_DBC_REPAIR_WEAPON_2H[ilvl] or 0; - end - - return RD_DBC_REPAIR_WEAPON[ilvl] or 0; -end - -local RelationshipsDashboard_CalculateInventoryRepairCost; - -local function RelationshipsDashboard_CalculateDurabilityAndRepair() - local totalCurrentDur = 0; - local totalMaxDur = 0; - local repairCost = 0; - local slotsWithDurability = 0; - - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local hasDurability = false; - local curDur, maxDur; - - -- Skip slots that never have durability - if SLOTS_NO_DURABILITY[slotId] then - -- Skip accessory slots entirely - elseif not RD_DURABILITY_SLOTS[slotId] then - -- Skip slots we don't track (e.g. waist in vanilla has no dur) - else - local itemLink = GetInventoryItemLink("player", slotId); - if itemLink then - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - local existingSaved = RD_SavedDurability[slotId]; - if existingSaved and existingSaved.itemID and itemID and existingSaved.itemID ~= itemID then - RD_SavedDurability[slotId] = nil; - end - -- Method 1: GetInventoryItemDurability (vanilla 1.12 API) - -- Returns currentDurability, maxDurability for the slot. - -- This is the most reliable method and works without a repair NPC. - if GetInventoryItemDurability then - curDur, maxDur = GetInventoryItemDurability(slotId); - if curDur and maxDur and maxDur > 0 then - -- Only TRUST this reading when it reports actual damage - -- (curDur < maxDur) or when a repair merchant is open. - -- On Turtle/OctoWoW the API frequently reports FULL - -- durability outside a merchant (notably right after a - -- /reload or login). Trusting a bogus "full" reading - -- would wipe our tracked estimate and falsely snap the - -- dashboard back to 100%. When it looks full and we are - -- not at a merchant, fall through to the saved estimate. - if curDur < maxDur or RD_AtRepairMerchant then - hasDurability = true; - RD_SavedDurability[slotId] = { cur = curDur, max = maxDur, itemID = itemID }; - end - end - end - - -- Method 2: Tooltip scanning for durability (fallback) - if not hasDurability and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe("player", slotId); - - for j = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. j); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, dCur, dMax = strfind(text, "Durability:%s*(%d+)%s*/%s*(%d+)"); - if dCur and dMax then - curDur = tonumber(dCur); - maxDur = tonumber(dMax); - -- Same trust rule as the API path: a "full" - -- reading outside a repair merchant may be a - -- server quirk, so keep the saved estimate - -- instead of resetting to 100%. - if maxDur and maxDur > 0 - and (curDur < maxDur or RD_AtRepairMerchant) then - hasDurability = true; - RD_SavedDurability[slotId] = { cur = curDur, max = maxDur, itemID = itemID }; - end - break; - end - end - end - end - end - - -- Method 3: Use saved/estimated durability (fallback when API fails) - -- This ensures we always show a meaningful value even when not - -- at a repair NPC and the vanilla API returns nothing. - if not hasDurability then - local saved = RD_SavedDurability[slotId]; - if saved and saved.max and saved.max > 0 then - -- Use the saved estimate - curDur = saved.cur; - maxDur = saved.max; - saved.itemID = itemID; - hasDurability = true; - else - -- No saved data: item is likely at full durability - -- (newly equipped or first time reading) - -- Use default max durability for the slot - local defaultMax = RD_DEFAULT_MAX_DURABILITY[slotId] or 85; - maxDur = defaultMax; - curDur = defaultMax; -- Assume full when unknown - hasDurability = true; - -- Save for future tracking - RD_SavedDurability[slotId] = { cur = curDur, max = maxDur, itemID = itemID }; - end - end - else - -- No item in this slot — clear saved data - RD_SavedDurability[slotId] = nil; - end - end - - if hasDurability and maxDur > 0 then - totalCurrentDur = totalCurrentDur + curDur; - totalMaxDur = totalMaxDur + maxDur; - slotsWithDurability = slotsWithDurability + 1; - - -- Estimate repair cost for this item when merchant is not open - -- Vanilla WoW repair formula is approximately: - -- cost = (maxDur - curDur) * slotMultiplier * qualityFactor * (itemLevel / 50) - -- where qualityFactor depends on item quality and slotMultiplier varies by slot - local itemLink = GetInventoryItemLink("player", slotId); - if itemLink then - local quality = RelationshipsDashboard_GetItemQualityFromLink(itemLink); - local itemLevel = RelationshipsDashboard_GetItemLevel(itemLink); - local durLost = maxDur - curDur; - - if durLost > 0 then - local dmultiplier = RelationshipsDashboard_GetRepairMultiplier(slotId, itemLink, itemLevel); - local qFactor = RD_DBC_REPAIR_QUALITY[quality] or 1.0; - local slotRepairCost = floor((durLost * dmultiplier * qFactor) + 0.5); - repairCost = repairCost + slotRepairCost; - end - end - end - end - - repairCost = repairCost + RelationshipsDashboard_CalculateInventoryRepairCost(); - - -- If a repair merchant is open, GetRepairAllCost() gives the exact cost. - -- This is handled separately in RelationshipsDashboard_UpdateRepairFromMerchant - -- and applied in UpdateStats when RelationshipsDashboard_MerchantRepairCost > 0. - - local avgDurability = 100; - if totalMaxDur > 0 then - avgDurability = (totalCurrentDur / totalMaxDur) * 100; - end - - return avgDurability, repairCost, slotsWithDurability; -end - --- Ensure RD_SavedDurability[slotId] exists for an equipped durability item so --- that death / spirit-healer / combat penalties can decrement it even when the --- character panel has never been opened yet (in which case CalculateDurability --- would not have populated saved state yet). -local function RelationshipsDashboard_EnsureSavedDurability(slotId) - local saved = RD_SavedDurability[slotId]; - local itemLink = GetInventoryItemLink("player", slotId); - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - if saved and saved.max and saved.max > 0 and (not itemID or not saved.itemID or saved.itemID == itemID) then - saved.itemID = itemID; - return saved; - end - if saved and itemID and saved.itemID and saved.itemID ~= itemID then - RD_SavedDurability[slotId] = nil; - end - local curDur, maxDur; - if GetInventoryItemDurability then - curDur, maxDur = GetInventoryItemDurability(slotId); - end - if not (curDur and maxDur and maxDur > 0) and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe("player", slotId); - for j = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. j); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, dCur, dMax = strfind(text, "Durability:%s*(%d+)%s*/%s*(%d+)"); - if dCur and dMax then - curDur = tonumber(dCur); - maxDur = tonumber(dMax); - break; - end - end - end - end - end - if not (curDur and maxDur and maxDur > 0) then - maxDur = RD_DEFAULT_MAX_DURABILITY[slotId] or 85; - curDur = maxDur; - end - RD_SavedDurability[slotId] = { cur = curDur, max = maxDur, itemID = itemID }; - return RD_SavedDurability[slotId]; -end - -local function RelationshipsDashboard_ReadContainerDurability(bag, slot) - local curDur, maxDur; - if GetContainerItemDurability then - local ok, c, m = pcall(function() return GetContainerItemDurability(bag, slot); end); - if ok then - curDur = c; - maxDur = m; - end - end - if not (curDur and maxDur and maxDur > 0) and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - local ok = pcall(function() RelationshipsDashboardTooltip:SetBagItem(bag, slot); end); - if ok then - for j = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. j); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, dCur, dMax = strfind(text, "Durability:%s*(%d+)%s*/%s*(%d+)"); - if dCur and dMax then - curDur = tonumber(dCur); - maxDur = tonumber(dMax); - break; - end - end - end - end - end - end - return curDur, maxDur; -end - -local function RelationshipsDashboard_GetSavedBagKey(bag, slot) - return "bag" .. bag .. ":" .. slot; -end - -local function RelationshipsDashboard_GetDefaultMaxForItemLink(itemLink) - local _, _, _, _, _, _, itemSubType, _, itemEquipLoc = GetItemInfo(itemLink); - local subTypeLower = itemSubType and strlower(itemSubType) or ""; - if itemEquipLoc == "INVTYPE_CHEST" or itemEquipLoc == "INVTYPE_ROBE" then return RD_DEFAULT_MAX_DURABILITY[5]; end - if itemEquipLoc == "INVTYPE_LEGS" then return RD_DEFAULT_MAX_DURABILITY[7]; end - if itemEquipLoc == "INVTYPE_HEAD" then return RD_DEFAULT_MAX_DURABILITY[1]; end - if itemEquipLoc == "INVTYPE_SHOULDER" then return RD_DEFAULT_MAX_DURABILITY[3]; end - if itemEquipLoc == "INVTYPE_FEET" then return RD_DEFAULT_MAX_DURABILITY[8]; end - if itemEquipLoc == "INVTYPE_WRIST" then return RD_DEFAULT_MAX_DURABILITY[9]; end - if itemEquipLoc == "INVTYPE_HAND" then return RD_DEFAULT_MAX_DURABILITY[10]; end - if itemEquipLoc == "INVTYPE_CLOAK" then return RD_DEFAULT_MAX_DURABILITY[15]; end - if itemEquipLoc == "INVTYPE_WEAPON" or itemEquipLoc == "INVTYPE_WEAPONMAINHAND" or itemEquipLoc == "INVTYPE_WEAPONOFFHAND" then return 90; end - if itemEquipLoc == "INVTYPE_2HWEAPON" or itemEquipLoc == "INVTYPE_RANGED" or itemEquipLoc == "INVTYPE_RANGEDRIGHT" or itemEquipLoc == "INVTYPE_SHIELD" then return 100; end - if strfind(subTypeLower, "shield") then return 100; end - return nil; -end - -local function RelationshipsDashboard_EstimateSingleRepairCost(slotId, itemLink, curDur, maxDur) - if not itemLink or not curDur or not maxDur or maxDur <= 0 then return 0; end - local durLost = maxDur - curDur; - if durLost <= 0 then return 0; end - local quality = RelationshipsDashboard_GetItemQualityFromLink(itemLink); - local itemLevel = RelationshipsDashboard_GetItemLevel(itemLink); - if not itemLevel or itemLevel <= 0 then - itemLevel = UnitLevel("player") or 1; - end - local dmultiplier = RelationshipsDashboard_GetRepairMultiplier(slotId, itemLink, itemLevel); - local qFactor = RD_DBC_REPAIR_QUALITY[quality] or 1.0; - return floor((durLost * dmultiplier * qFactor) + 0.5); -end - -RelationshipsDashboard_CalculateInventoryRepairCost = function() - local repairCost = 0; - if not GetContainerNumSlots or not GetContainerItemLink then return 0; end - - for bag = 0, 4 do - local numSlots = GetContainerNumSlots(bag) or 0; - for slot = 1, numSlots do - local itemLink = GetContainerItemLink(bag, slot); - if itemLink then - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - local key = RelationshipsDashboard_GetSavedBagKey(bag, slot); - local saved = RD_SavedDurability[key]; - if saved and itemID and saved.itemID and saved.itemID ~= itemID then - saved = nil; - RD_SavedDurability[key] = nil; - end - - local curDur, maxDur = RelationshipsDashboard_ReadContainerDurability(bag, slot); - if curDur and maxDur and maxDur > 0 then - if curDur < maxDur or RD_AtRepairMerchant then - RD_SavedDurability[key] = { cur = curDur, max = maxDur, itemID = itemID }; - saved = RD_SavedDurability[key]; - end - end - - if saved and saved.max and saved.max > 0 then - saved.itemID = itemID; - repairCost = repairCost + RelationshipsDashboard_EstimateSingleRepairCost(16, itemLink, saved.cur, saved.max); - end - else - RD_SavedDurability[RelationshipsDashboard_GetSavedBagKey(bag, slot)] = nil; - end - end - end - - return repairCost; -end - -local function RelationshipsDashboard_SyncEquippedDurabilityFromGame(allowFull) - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - if RD_DURABILITY_SLOTS[slotId] and not SLOTS_NO_DURABILITY[slotId] then - local itemLink = GetInventoryItemLink("player", slotId); - if itemLink then - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - local curDur, maxDur; - if GetInventoryItemDurability then - curDur, maxDur = GetInventoryItemDurability(slotId); - end - if curDur and maxDur and maxDur > 0 and (allowFull or curDur < maxDur) then - RD_SavedDurability[slotId] = { cur = curDur, max = maxDur, itemID = itemID }; - elseif RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe("player", slotId); - for j = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. j); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, dCur, dMax = strfind(text, "Durability:%s*(%d+)%s*/%s*(%d+)"); - if dCur and dMax then - curDur = tonumber(dCur); - maxDur = tonumber(dMax); - if maxDur and maxDur > 0 and (allowFull or curDur < maxDur) then - RD_SavedDurability[slotId] = { cur = curDur, max = maxDur, itemID = itemID }; - end - break; - end - end - end - end - end - else - RD_SavedDurability[slotId] = nil; - end - end - end -end - -------------------------------------------------------------------------------- --- Apply death penalty: Reduce all equipped items' estimated durability by 10% --- In vanilla WoW, dying costs 10% durability on all equipped items. --- This is applied even to items that already have 0 durability (they go negative --- in the internal representation, but we clamp to 0 for display). -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ApplyDeathPenalty() - -- No durability loss in battlegrounds or duels. Re-check live BG status - -- here in case ZONE_CHANGED has not yet updated the cached flag (some - -- Turtle WoW / OctoWoW builds fire PLAYER_DEAD before ZONE_CHANGED on - -- battleground entries that kill on-arrival). - if not RD_InBattleground then - RD_InBattleground = RelationshipsDashboard_IsInBattleground(); - end - if RD_InBattleground then - -- Belt-and-suspenders: if we somehow had already applied wear before - -- realizing we were in a BG, restore the snapshot taken on entry. - RelationshipsDashboard_RestoreDurabilityFromBGSnapshot(); - return; - end - if RD_InDuel then return; end - for i = 1, table.getn(EQUIPMENT_SLOTS) do - - local slotId = EQUIPMENT_SLOTS[i]; - if RD_DURABILITY_SLOTS[slotId] and not SLOTS_NO_DURABILITY[slotId] then - local itemLink = GetInventoryItemLink("player", slotId); - if itemLink then - -- Make sure we have a baseline for this slot even if the - -- character panel has never been opened this session. - local saved = RelationshipsDashboard_EnsureSavedDurability(slotId); - if saved and saved.max and saved.max > 0 then - -- Death penalty: lose 10% of max durability - local loss = floor(saved.max * 0.10); - if loss < 1 then loss = 1; end - saved.cur = saved.cur - loss; - if saved.cur < 0 then saved.cur = 0; end - end - end - end - end -end - -------------------------------------------------------------------------------- --- Apply combat wear: Small durability loss from taking hits in combat. --- Each time the player takes damage while in combat, there's a chance --- of durability loss on armor pieces. In vanilla WoW, durability loss from --- combat hits is approximately 0.1-0.5 durability per hit depending on damage. --- We use a simplified model: every N damage events, lose 1 durability point --- distributed across armor slots. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ApplyCombatWear(damageAmount) - if not RD_InCombat then return; end - -- No durability loss in battlegrounds or duels - if not RD_InBattleground then - RD_InBattleground = RelationshipsDashboard_IsInBattleground(); - end - if RD_InBattleground then return; end - if RD_InDuel then return; end - - if not damageAmount or damageAmount <= 0 then return; end - - RD_CombatHitCount = RD_CombatHitCount + 1; - - -- Every 20 combat hits, apply 1 durability point loss to a random armor slot - -- This approximates vanilla WoW's combat durability wear rate - -- (roughly 0.05-0.1 dur per hit depending on damage magnitude) - if RD_CombatHitCount >= 20 then - RD_CombatHitCount = 0; - - -- Pick armor slots to lose durability (not weapons, those don't decay from hits) - local armorSlots = { 1, 3, 5, 7, 8, 9, 10, 15 }; -- Head, Shoulder, Chest, Legs, Feet, Wrist, Hands, Back - -- Pick a random slot - local slotIdx = random(1, table.getn(armorSlots)); - local slotId = armorSlots[slotIdx]; - - local itemLink = GetInventoryItemLink("player", slotId); - if itemLink then - local saved = RelationshipsDashboard_EnsureSavedDurability(slotId); - if saved and saved.max and saved.max > 0 then - saved.cur = saved.cur - 1; - if saved.cur < 0 then saved.cur = 0; end - end - end - end -end - -------------------------------------------------------------------------------- --- Reset all saved durability to full (used when player repairs at a merchant) -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ResetDurabilityToFull() - for key, saved in pairs(RD_SavedDurability) do - if saved and saved.max and saved.max > 0 then - saved.cur = saved.max; -- Fully repaired - end - end -end - -------------------------------------------------------------------------------- --- Apply spirit-healer resurrection penalty. --- Spirit Healers resurrect a dead player at the cost of 25% durability loss --- to ALL items (equipped + inventory). We can only reliably estimate the --- equipped portion; inventory durability isn't cheaply readable in 1.12. --- Characters level 10 and under do NOT lose durability from spirit rez. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ApplySpiritHealerPenalty() - -- No durability loss in battlegrounds or duels (duels never trigger a - -- spirit-healer rez, but guard defensively in case the state races). - if not RD_InBattleground then - RD_InBattleground = RelationshipsDashboard_IsInBattleground(); - end - if RD_InBattleground then - RelationshipsDashboard_RestoreDurabilityFromBGSnapshot(); - return; - end - if RD_InDuel then return; end - if UnitLevel("player") <= 10 then return; end - - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - if RD_DURABILITY_SLOTS[slotId] and not SLOTS_NO_DURABILITY[slotId] then - local itemLink = GetInventoryItemLink("player", slotId); - if itemLink then - local saved = RelationshipsDashboard_EnsureSavedDurability(slotId); - if saved and saved.max and saved.max > 0 then - -- Spirit healer: lose 25% of max durability on top of the - -- 10% death penalty already applied by PLAYER_DEAD. - local loss = floor(saved.max * 0.25); - if loss < 1 then loss = 1; end - saved.cur = saved.cur - loss; - if saved.cur < 0 then saved.cur = 0; end - end - end - end - end - - -- Spirit healer repair bills include durability loss on damaged items in - -- bags as well. Track the equipped part exactly and bag items when the - -- client exposes their durability; otherwise seed a reasonable max from - -- equip type so the repair total does not ignore inventory damage. - if GetContainerNumSlots and GetContainerItemLink then - for bag = 0, 4 do - local numSlots = GetContainerNumSlots(bag) or 0; - for slot = 1, numSlots do - local itemLink = GetContainerItemLink(bag, slot); - if itemLink then - local itemID = RelationshipsDashboard_ExtractItemID(itemLink); - local key = RelationshipsDashboard_GetSavedBagKey(bag, slot); - local curDur, maxDur = RelationshipsDashboard_ReadContainerDurability(bag, slot); - if not (curDur and maxDur and maxDur > 0) then - maxDur = RelationshipsDashboard_GetDefaultMaxForItemLink(itemLink); - curDur = maxDur; - end - if curDur and maxDur and maxDur > 0 then - local saved = RD_SavedDurability[key]; - if not saved or not saved.max or saved.max <= 0 or (itemID and saved.itemID and saved.itemID ~= itemID) then - saved = { cur = curDur, max = maxDur, itemID = itemID }; - RD_SavedDurability[key] = saved; - end - local loss = floor(saved.max * 0.25); - if loss < 1 then loss = 1; end - saved.cur = saved.cur - loss; - if saved.cur < 0 then saved.cur = 0; end - end - end - end - end - end -end - -------------------------------------------------------------------------------- --- Try to get actual repair cost from an open merchant --- This is called when MERCHANT_SHOW / MERCHANT_UPDATE events fire --- Uses GetRepairAllCost() - the correct vanilla 1.12 API --- Returns repairAllCost, canRepair (only works at a repair-capable merchant) -------------------------------------------------------------------------------- -local RelationshipsDashboard_MerchantRepairCost = 0; - --- Tracks the previous non-zero repair cost while a repair merchant is open. --- When the reported cost transitions from >0 to 0, or the player's gold drops --- by approximately that amount, we know a repair just happened and can safely --- reset saved durability to full WITHOUT needing a /reload. -RD_PrevMerchantRepairCost = 0; - -local function RelationshipsDashboard_UpdateRepairFromMerchant() - -- GetRepairAllCost is the correct vanilla 1.12 API function name - -- It only returns a value when a repair-capable merchant window is open - if not GetRepairAllCost then return; end - - local cost, canRepair = GetRepairAllCost(); - -- Track whether a repair-capable merchant is currently open so the rest - -- of the addon knows it can trust "full" durability readings. - if canRepair then - RD_AtRepairMerchant = true; - else - RD_AtRepairMerchant = false; - end - - if not canRepair then - -- Merchant can't repair at all. Clear the transient state so a later - -- MERCHANT_UPDATE at a real repair NPC starts from a clean slate. - RelationshipsDashboard_MerchantRepairCost = 0; - RD_PrevMerchantRepairCost = 0; - return; - end - - -- CRITICAL: refresh actual durability from the API BEFORE we do anything - -- else. At a repair merchant GetInventoryItemDurability is reliable, so we - -- want RD_SavedDurability to reflect ground truth before we (a) calibrate - -- against the reported cost, or (b) decide the player just repaired. - RelationshipsDashboard_SyncEquippedDurabilityFromGame(true); - - if cost and cost > 0 then - RelationshipsDashboard_MerchantRepairCost = cost; - RD_PrevMerchantRepairCost = cost; - -- Calibrate our away-from-merchant estimate against the exact cost - -- the game reports. We recompute our DBC-based estimate for the CURRENT - -- (freshly-updated) damaged gear and store a narrow ratio, so future - -- estimates match this server while still staying grounded in gear data. - local _, rawEstimate = RelationshipsDashboard_CalculateDurabilityAndRepair(); - if rawEstimate and rawEstimate > 0 then - local factor = cost / rawEstimate; - if factor < 0.5 then factor = 0.5; end - if factor > 2.0 then factor = 2.0; end - RD_RepairCalibration = factor; - if RelationshipsDashboardDB then - RelationshipsDashboardDB.repairCalibration = factor; - RelationshipsDashboardDB.repairCalibrationVersion = RD_REPAIR_CALIBRATION_VERSION; - end - end - else - -- Repair cost is 0 at a repair-capable merchant. Two possibilities: - -- (a) Player just clicked Repair All / repaired every item. - -- (b) All gear was already at full durability when they opened - -- the merchant. - -- Either way the correct visible state is "everything full". Reset. - RelationshipsDashboard_MerchantRepairCost = 0; - RD_PrevMerchantRepairCost = 0; - RelationshipsDashboard_ResetDurabilityToFull(); - end -end - -------------------------------------------------------------------------------- --- Get melee hit chance --- In vanilla 1.12, melee hit rating doesn't exist as a rating. --- Hit chance comes from gear bonuses (+X% hit) and the base 5% against same level. --- We scan the paper doll tooltip for hit chance, or use GetCombatRatingBonus if available. --- For vanilla: scan tooltip of CharacterDamageFrame or use stat bonus calculations. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetMeleeHitChance() - -- In vanilla 1.12, there's no direct API for hit chance from gear - -- The best approach is to calculate from hit rating on gear if available, - -- or scan the character's melee hit bonus from tooltip - - -- Try using GetCombatRating / GetCombatRatingBonus if available (Turtle WoW extension) - if GetCombatRating and GetCombatRatingBonus then - -- CR_HIT_MELEE = 6 in later patches, may not exist in 1.12 - local hitRating = GetCombatRating(6); - if hitRating then - local hitBonus = GetCombatRatingBonus(6); - if hitBonus then - return 5.0 + hitBonus; -- Base 5% + gear bonus - end - end - end - - -- Fallback: try scanning the paper doll melee tooltip - -- In vanilla, hovering over the melee damage stat shows hit chance - if RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - -- Set to the melee damage tooltip - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, 16); -- Main hand - - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - -- Look for hit chance modifiers on weapon tooltip - local _, _, hitVal = strfind(text, "improves your chance to hit by (%d+)%%"); - if hitVal then - return 5.0 + tonumber(hitVal); - end - local _, _, hitVal2 = strfind(text, "%+(%d+)%% Hit"); - if hitVal2 then - return 5.0 + tonumber(hitVal2); - end - end - end - end - end - - -- Final fallback: base hit chance is 5% for melee against same-level target - -- We'll try to accumulate +hit from equipment via tooltip scanning - local hitBonus = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "%+(%d+)%% chance to hit"); - if val then hitBonus = hitBonus + tonumber(val); end - local _, _, val2 = strfind(text, "Equip: Increases your chance to hit by (%d+)%%"); - if val2 then hitBonus = hitBonus + tonumber(val2); end - end - end - end - end - end - return 5.0 + hitBonus; -end - -------------------------------------------------------------------------------- --- Get spell hit chance --- Similar approach to melee hit - scan gear for +spell hit bonuses -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetSpellHitChance() - -- Try GetCombatRatingBonus if available - if GetCombatRating and GetCombatRatingBonus then - -- CR_HIT_SPELL = 8 - local hitRating = GetCombatRating(8); - if hitRating then - local hitBonus = GetCombatRatingBonus(8); - if hitBonus then - return 5.0 + hitBonus; -- Base 5% + gear bonus - end - end - end - - -- Fallback: scan equipment for +spell hit - local hitBonus = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "chance to hit with spells by (%d+)%%"); - if val then hitBonus = hitBonus + tonumber(val); end - local _, _, val2 = strfind(text, "%+(%d+)%% Spell Hit"); - if val2 then hitBonus = hitBonus + tonumber(val2); end - local _, _, val3 = strfind(text, "Increases your chance to hit with spells by (%d+)%%"); - if val3 then hitBonus = hitBonus + tonumber(val3); end - end - end - end - end - end - return 5.0 + hitBonus; -end - -------------------------------------------------------------------------------- --- Get spell crit chance --- In vanilla 1.12, there's GetSpellCritChance() available as a global --- Or we can use UnitStat-based formulas and intellect contribution -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetSpellCritChance() - -- Method 1: Try GetSpellCritChance (may be available in Turtle WoW's extended API) - if GetSpellCritChance then - local crit = GetSpellCritChance(SPELL_SCHOOL_INDEX); - if crit and crit > 0 then - return crit; - end - end - - -- Method 2: Base spell crit + Intellect contribution - -- Base spell crit by class: - local _, class = UnitClass(RD_UNIT); - local baseSpellCrit = 0; - if class == "MAGE" then baseSpellCrit = 3.2; - elseif class == "WARLOCK" then baseSpellCrit = 2.6; - elseif class == "PRIEST" then baseSpellCrit = 2.4; - elseif class == "DRUID" then baseSpellCrit = 2.4; - elseif class == "SHAMAN" then baseSpellCrit = 2.4; - elseif class == "PALADIN" then baseSpellCrit = 2.4; - else baseSpellCrit = 0; end - - -- Intellect contribution: roughly 1% crit per 59.5 intellect at level 60 - local level = UnitLevel(RD_UNIT); - local intStat = UnitStat(RD_UNIT, 4); -- Intellect - local intPerCrit = 59.5; - if level < 60 then - intPerCrit = 59.5 * (level / 60); - end - local intCritBonus = intStat / intPerCrit; - - -- Scan gear for +spell crit - local gearCritBonus = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "critical strike chance with spells by (%d+)%%"); - if val then gearCritBonus = gearCritBonus + tonumber(val); end - local _, _, val2 = strfind(text, "%+(%d+)%% Spell Crit"); - if val2 then gearCritBonus = gearCritBonus + tonumber(val2); end - local _, _, val3 = strfind(text, "Improves your chance to get a critical strike with spells by (%d+)%%"); - if val3 then gearCritBonus = gearCritBonus + tonumber(val3); end - end - end - end - end - end - - return baseSpellCrit + intCritBonus + gearCritBonus; -end - -------------------------------------------------------------------------------- --- Get melee crit chance --- In vanilla 1.12: base melee crit + agility contribution + gear bonuses -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetMeleeCritChance() - -- Method 1: Try GetCritChance (may be available in Turtle WoW) - if GetCritChance then - local crit = GetCritChance(); - if crit and crit > 0 then - return crit; - end - end - - -- Method 2: Calculate from base + agility + gear - local _, class = UnitClass(RD_UNIT); - local level = UnitLevel(RD_UNIT); - - -- Base crit rate by class (approximate) - local baseCrit = 5.0; -- All classes have 5% base melee crit in vanilla - - -- Agility per crit varies by class and level - -- At level 60: Warrior=20, Rogue=~29, Hunter=~53, Paladin=~20, etc. - local agiPerCrit = 20; - if class == "WARRIOR" then agiPerCrit = 20; - elseif class == "ROGUE" then agiPerCrit = 29; - elseif class == "HUNTER" then agiPerCrit = 53; - elseif class == "PALADIN" then agiPerCrit = 20; - elseif class == "DRUID" then agiPerCrit = 20; - elseif class == "SHAMAN" then agiPerCrit = 20; - end - - -- Scale for level (these values are for level 60) - if level > 0 and level < 60 then - agiPerCrit = agiPerCrit * (level / 60); - if agiPerCrit < 1 then agiPerCrit = 1; end - end - - local agiStat = UnitStat(RD_UNIT, 2); -- Agility - local agiCritBonus = agiStat / agiPerCrit; - - -- Scan gear for +melee crit - local gearCritBonus = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "critical strike chance by (%d+)%%"); - if val then gearCritBonus = gearCritBonus + tonumber(val); end - local _, _, val2 = strfind(text, "%+(%d+)%% Crit"); - if val2 then gearCritBonus = gearCritBonus + tonumber(val2); end - local _, _, val3 = strfind(text, "Improves your chance to get a critical strike by (%d+)%%"); - if val3 then gearCritBonus = gearCritBonus + tonumber(val3); end - end - end - end - end - end - - return baseCrit + agiCritBonus + gearCritBonus; -end - -------------------------------------------------------------------------------- --- Get ranged crit chance --- For Hunters primarily - based on agility + gear -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetRangedCritChance() - -- Method 1: Try GetRangedCritChance (Turtle WoW extension) - if GetRangedCritChance then - local crit = GetRangedCritChance(); - if crit and crit > 0 then - return crit; - end - end - - -- Method 2: Similar to melee crit but with ranged-specific agi ratios - local _, class = UnitClass(RD_UNIT); - local level = UnitLevel(RD_UNIT); - local baseCrit = 5.0; - - -- Ranged crit from agility (Hunter-focused) - local agiPerCrit = 53; -- Hunter value - if class ~= "HUNTER" then - agiPerCrit = 40; -- Other classes - end - if level > 0 and level < 60 then - agiPerCrit = agiPerCrit * (level / 60); - if agiPerCrit < 1 then agiPerCrit = 1; end - end - - local agiStat = UnitStat(RD_UNIT, 2); - local agiCritBonus = agiStat / agiPerCrit; - - -- Scan gear for +ranged crit - local gearCritBonus = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "critical strike chance with ranged by (%d+)%%"); - if val then gearCritBonus = gearCritBonus + tonumber(val); end - local _, _, val2 = strfind(text, "%+(%d+)%% Ranged Crit"); - if val2 then gearCritBonus = gearCritBonus + tonumber(val2); end - end - end - end - end - end - - return baseCrit + agiCritBonus + gearCritBonus; -end - -------------------------------------------------------------------------------- --- Get spell power (highest school bonus) --- In vanilla 1.12, spell power is per-school. We show the highest. --- Uses GetSpellBonusDamage if available, or scans gear. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetSpellPower() - -- Method 1: Try GetSpellBonusDamage (Turtle WoW extension) - if GetSpellBonusDamage then - local maxPower = 0; - -- School indices: 2=Holy, 3=Fire, 4=Nature, 5=Frost, 6=Shadow, 7=Arcane - for school = 2, 7 do - local power = GetSpellBonusDamage(school); - if power and power > maxPower then - maxPower = power; - end - end - return maxPower; - end - - -- Method 2: Scan gear for +spell damage - local spellPower = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "%+(%d+) Spell Damage"); - if val then spellPower = spellPower + tonumber(val); end - local _, _, val2 = strfind(text, "up to (%d+) additional spell damage"); - if val2 then spellPower = spellPower + tonumber(val2); end - local _, _, val3 = strfind(text, "Increases damage done by magical spells and effects by up to (%d+)"); - if val3 then spellPower = spellPower + tonumber(val3); end - local _, _, val4 = strfind(text, "%+(%d+) Fire Damage"); - if val4 then spellPower = spellPower + tonumber(val4); end - local _, _, val5 = strfind(text, "%+(%d+) Frost Damage"); - if val5 then spellPower = spellPower + tonumber(val5); end - local _, _, val6 = strfind(text, "%+(%d+) Arcane Damage"); - if val6 then spellPower = spellPower + tonumber(val6); end - local _, _, val7 = strfind(text, "%+(%d+) Shadow Damage"); - if val7 then spellPower = spellPower + tonumber(val7); end - local _, _, val8 = strfind(text, "%+(%d+) Nature Damage"); - if val8 then spellPower = spellPower + tonumber(val8); end - local _, _, val9 = strfind(text, "%+(%d+) Holy Damage"); - if val9 then spellPower = spellPower + tonumber(val9); end - end - end - end - end - end - return spellPower; -end - -------------------------------------------------------------------------------- --- Get healing power --- In vanilla 1.12, healing power is separate from spell damage --- Uses GetSpellBonusHealing if available, or scans gear. -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetHealingPower() - -- Method 1: Try GetSpellBonusHealing (Turtle WoW extension) - if GetSpellBonusHealing then - local healing = GetSpellBonusHealing(); - if healing then - return healing; - end - end - - -- Method 2: Scan gear for +healing - local healingPower = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "healing spells by up to (%d+)"); - if val then healingPower = healingPower + tonumber(val); end - local _, _, val2 = strfind(text, "%+(%d+) Healing"); - if val2 then healingPower = healingPower + tonumber(val2); end - local _, _, val3 = strfind(text, "Increases healing done by spells and effects by up to (%d+)"); - if val3 then healingPower = healingPower + tonumber(val3); end - end - end - end - end - end - return healingPower; -end - -------------------------------------------------------------------------------- --- Get mana regeneration (spirit-based formula) --- In vanilla 1.12, mana regen comes from spirit (out of FSR) and gear bonuses -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetManaRegen() - -- Method 1: Try GetManaRegen (Turtle WoW extension) - if GetManaRegen then - local base, casting = GetManaRegen(); - if base then - return base; - end - end - - -- Method 2: Calculate from spirit - -- Spirit-based mana regen formula (out of combat, not casting): - -- Regen per tick (2 seconds) varies by class: - -- Priest: Spirit * 0.125 - -- Mage: Spirit * 0.125 - -- Warlock: Spirit * 0.1 - -- Druid: Spirit * 0.125 - -- Shaman: Spirit * 0.1 - -- Paladin: Spirit * 0.1 - -- Per second = tickRegen / 2 - local _, class = UnitClass(RD_UNIT); - local spirit = UnitStat(RD_UNIT, 5); - - -- Only show for mana classes - local powerType = UnitPowerType(RD_UNIT); - if powerType ~= 0 then - return 0; -- Not a mana user - end - - local spiritFactor = 0; - if class == "PRIEST" then spiritFactor = 0.125; - elseif class == "MAGE" then spiritFactor = 0.125; - elseif class == "WARLOCK" then spiritFactor = 0.1; - elseif class == "DRUID" then spiritFactor = 0.125; - elseif class == "SHAMAN" then spiritFactor = 0.1; - elseif class == "PALADIN" then spiritFactor = 0.1; - else return 0; end - - local baseRegen = spirit * spiritFactor / 2; -- per second - - -- Scan gear for +mana regen - local gearRegen = 0; - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, val = strfind(text, "mana every (%d+) sec"); - if val then - gearRegen = gearRegen + (tonumber(val) / 5); -- Convert per-5sec to per-second - end - local _, _, val2 = strfind(text, "%+(%d+) mana per 5 sec"); - if val2 then - gearRegen = gearRegen + (tonumber(val2) / 5); - end - local _, _, val3 = strfind(text, "Allows (%d+) mana per 5 sec"); - if val3 then - gearRegen = gearRegen + (tonumber(val3) / 5); - end - end - end - end - end - end - - return baseRegen + gearRegen; -end - -------------------------------------------------------------------------------- --- Get weapon skill for current weapon --- In vanilla 1.12, weapon skill affects hit/miss/glancing blow calculations --- We show the skill for the currently equipped main hand weapon type -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetWeaponSkill() - local itemLink = GetInventoryItemLink(RD_UNIT, 16); -- Main hand - if not itemLink then return "--"; end - - local _, _, _, _, _, itemType, itemSubType = GetItemInfo(itemLink); - if not itemSubType then return "--"; end - - -- Map weapon subtypes to skill line indices - -- In vanilla, skill lines are accessed by name - -- The skill value = min(level * 5, baseSkill + racialBonus + gearBonus) - local skillName = nil; - if itemSubType == "Axes" or itemSubType == "One-Handed Axes" then skillName = "Axes"; - elseif itemSubType == "Two-Handed Axes" then skillName = "Two-Handed Axes"; - elseif itemSubType == "Maces" or itemSubType == "One-Handed Maces" then skillName = "Maces"; - elseif itemSubType == "Two-Handed Maces" then skillName = "Two-Handed Maces"; - elseif itemSubType == "Swords" or itemSubType == "One-Handed Swords" then skillName = "Swords"; - elseif itemSubType == "Two-Handed Swords" then skillName = "Two-Handed Swords"; - elseif itemSubType == "Daggers" then skillName = "Daggers"; - elseif itemSubType == "Fist Weapons" then skillName = "Unarmed"; - elseif itemSubType == "Polearms" then skillName = "Polearms"; - elseif itemSubType == "Staves" then skillName = "Staves"; - elseif itemSubType == "Bows" then skillName = "Bows"; - elseif itemSubType == "Crossbows" then skillName = "Crossbows"; - elseif itemSubType == "Guns" then skillName = "Guns"; - elseif itemSubType == "Thrown" then skillName = "Thrown"; - end - - if not skillName then return "--"; end - - -- Try to get skill value from the skills API - -- In vanilla 1.12, we iterate skill lines - local numSkills = GetNumSkillLines(); - if numSkills then - for i = 1, numSkills do - local skillName2, _, _, skillRank = GetSkillLineInfo(i); - if skillName2 == skillName then - return skillRank or "--"; - end - end - end - - -- Fallback: max skill for level = level * 5 - local level = UnitLevel(RD_UNIT); - return level * 5 .. " (max)"; -end - -------------------------------------------------------------------------------- --- Get defense skill -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetDefense() - -- Method 1: Try GetDefense (Turtle WoW) - if GetDefense then - local base, mod = GetDefense("player"); - if base then - return (base + (mod or 0)); - end - end - - -- Method 2: From skill lines - local numSkills = GetNumSkillLines(); - if numSkills then - for i = 1, numSkills do - local skillName, _, _, skillRank = GetSkillLineInfo(i); - if skillName == "Defense" then - return skillRank or 0; - end - end - end - - return 0; -end - -------------------------------------------------------------------------------- --- Get block chance and value -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetBlockChance() - if GetBlockChance then - return GetBlockChance(); - end - -- Only warriors and paladins can block (with shield equipped) - return 0; -end - -------------------------------------------------------------------------------- --- Get parry chance -------------------------------------------------------------------------------- -local function RelationshipsDashboard_GetParryChance() - if GetParryChance then - return GetParryChance(); - end - -- Base parry is 5% for most melee classes - local _, class = UnitClass(RD_UNIT); - if class == "WARRIOR" or class == "PALADIN" or class == "ROGUE" or class == "HUNTER" or class == "DRUID" then - return 5.0; - end - return 0; -end - -------------------------------------------------------------------------------- --- Main Stats Update Function --- Called on show and on relevant events -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- --- INSPECT ESTIMATION --- In vanilla 1.12 the client does NOT expose another player's computed stats --- (UnitStat/UnitArmor/UnitAttackPower/UnitResistance/GetDodgeChance etc. all --- return the *player's* own values for a target). The only reliable target --- data available while inspecting is the equipped item set (item links + --- tooltips) plus level and class. So when inspecting we ESTIMATE every stat --- from the target's gear and level. Values are approximate. -------------------------------------------------------------------------------- - --- Approximate un-geared primary stats at level 60, tuned to real vanilla --- values (roughly the human baseline). Race adjustments applied below. --- Index: 1=STR 2=AGI 3=STA 4=INT 5=SPI -local RD_BASE_STATS_60 = { - WARRIOR = { 105, 75, 105, 30, 45 }, - PALADIN = { 100, 60, 95, 65, 75 }, - HUNTER = { 60, 115, 90, 60, 65 }, - ROGUE = { 85, 115, 85, 35, 50 }, - PRIEST = { 40, 50, 70, 115, 125 }, - SHAMAN = { 80, 60, 90, 85, 90 }, - MAGE = { 35, 50, 65, 120, 115 }, - WARLOCK = { 50, 55, 80, 105, 105 }, - DRUID = { 70, 60, 80, 95, 95 }, -}; - --- Per-race stat modifiers at level 60 relative to the human baseline above. --- Values are typical vanilla deltas; missing entries default to 0. --- Index: 1=STR 2=AGI 3=STA 4=INT 5=SPI -local RD_RACE_MODS_60 = { - Human = { 0, 0, 0, 0, 0 }, - Dwarf = { 5, -4, 1, -1, -1 }, - Gnome = { -5, 2, -1, 3, 0 }, - NightElf = { -4, 4, 0, 0, 0 }, - Orc = { 3, -3, 1, -3, 2 }, - Undead = { -1, -2, 0, -2, 5 }, - Tauren = { 5, -4, 1, -3, -2 }, - Troll = { 1, 2, 0, -4, 1 }, - Scourge = { -1, -2, 0, -2, 5 }, -- alt name for Undead - Goblin = { -3, 3, -1, 3, 0 }, - HighElf = { -4, 4, 0, 1, -1 }, -}; - --- Rough Agility-per-1%-crit (and used for dodge) by class. -local RD_AGI_PER_CRIT = { - WARRIOR = 20, PALADIN = 20, DRUID = 20, SHAMAN = 20, - ROGUE = 29, HUNTER = 53, MAGE = 20, PRIEST = 20, WARLOCK = 20, -}; - --- Classes that can parry in vanilla. -local RD_CAN_PARRY = { - WARRIOR = true, PALADIN = true, HUNTER = true, ROGUE = true, SHAMAN = true, -}; - --- Passive racial resistances (vanilla / Turtle / OctoWoW). --- Applied on top of gear resistances during inspect. Keys must match the --- normalized race name (spaces stripped) produced in UpdateStatsInspect. -local RD_RACE_RES = { - Dwarf = { frost = 10 }, - Gnome = { arcane = 10 }, - NightElf = { nature = 10 }, - Tauren = { nature = 10 }, - Troll = { nature = 10 }, - Undead = { shadow = 10 }, - Scourge = { shadow = 10 }, - HighElf = { arcane = 10 }, - Goblin = { fire = 5 }, -}; - --- Passive racial dodge (% points) — Night Elf Quickness. -local RD_RACE_DODGE = { - NightElf = 1, -}; - --- Passive racial health multiplier — Tauren Endurance (+5% base health). --- Not applied to Stamina directly because health is read from UnitHealthMax, --- but kept here for completeness / future use. -local RD_RACE_HEALTH_MULT = { - Tauren = 1.05, -}; - --- Parse a buff/racial tooltip line and add any recognized primary-stat, --- resistance, spell-power, AP, crit, hit, or mp5 bonuses into `est`. --- Mirrors the item parser but omits weapon-slot specific fields. --- Also handles the two common buff phrasings: --- "+X to Strength" (Mark of the Wild, Blessing of Kings post-scaling, etc.) --- "Increases Strength by X" --- "+X Strength" (item-style, same as gear) -local function RD_ParseBuffLineInto(est, text) - if not text then return; end - local v; - - local function addPrimary(stat, amt) - if not amt then return; end - amt = tonumber(amt); if not amt then return; end - if stat == "str" then est.str = est.str + amt; - elseif stat == "agi" then est.agi = est.agi + amt; - elseif stat == "sta" then est.sta = est.sta + amt; - elseif stat == "int" then est.int = est.int + amt; - elseif stat == "spi" then est.spi = est.spi + amt; - elseif stat == "all" then est.allStats = est.allStats + amt; end - end - - -- "+X " (Blessing of Might reads "+X Attack Power", Fortitude - -- reads "+X Stamina" on Turtle, etc.) - _,_,v = strfind(text, "%+(%d+) Strength"); addPrimary("str", v); - _,_,v = strfind(text, "%+(%d+) Agility"); addPrimary("agi", v); - _,_,v = strfind(text, "%+(%d+) Stamina"); addPrimary("sta", v); - _,_,v = strfind(text, "%+(%d+) Intellect"); addPrimary("int", v); - _,_,v = strfind(text, "%+(%d+) Spirit"); addPrimary("spi", v); - _,_,v = strfind(text, "%+(%d+) to All Stats"); addPrimary("all", v); - if not v then _,_,v = strfind(text, "%+(%d+) All Stats"); addPrimary("all", v); end - - -- "Increases by X" (Arcane Intellect, Divine Spirit, PW:F, etc.) - _,_,v = strfind(text, "[Ii]ncreases Strength by (%d+)"); addPrimary("str", v); - _,_,v = strfind(text, "[Ii]ncreases Agility by (%d+)"); addPrimary("agi", v); - _,_,v = strfind(text, "[Ii]ncreases Stamina by (%d+)"); addPrimary("sta", v); - _,_,v = strfind(text, "[Ii]ncreases Intellect by (%d+)"); addPrimary("int", v); - _,_,v = strfind(text, "[Ii]ncreases Spirit by (%d+)"); addPrimary("spi", v); - _,_,v = strfind(text, "[Ii]ncreases all attributes by (%d+)"); addPrimary("all", v); - _,_,v = strfind(text, "all party members by (%d+)"); -- Mark/Gift of the Wild form: "...by X" - if v and strfind(text, "attribute") then addPrimary("all", v); end - - -- Resistances (Shadow Protection, Prayer of Shadow, etc.) - _,_,v = strfind(text, "%+(%d+) Arcane Resistance"); if v then est.resArcane = est.resArcane + v; end - _,_,v = strfind(text, "%+(%d+) Fire Resistance"); if v then est.resFire = est.resFire + v; end - _,_,v = strfind(text, "%+(%d+) Nature Resistance"); if v then est.resNature = est.resNature + v; end - _,_,v = strfind(text, "%+(%d+) Frost Resistance"); if v then est.resFrost = est.resFrost + v; end - _,_,v = strfind(text, "%+(%d+) Shadow Resistance"); if v then est.resShadow = est.resShadow + v; end - _,_,v = strfind(text, "%+(%d+) to All Resistances"); if v then est.allRes = est.allRes + v; end - _,_,v = strfind(text, "resistance to all schools of magic by (%d+)"); - if v then est.allRes = est.allRes + v; end - _,_,v = strfind(text, "[Ss]hadow resistance by (%d+)"); if v then est.resShadow = est.resShadow + v; end - _,_,v = strfind(text, "[Ff]rost resistance by (%d+)"); if v then est.resFrost = est.resFrost + v; end - _,_,v = strfind(text, "[Ff]ire resistance by (%d+)"); if v then est.resFire = est.resFire + v; end - _,_,v = strfind(text, "[Nn]ature resistance by (%d+)"); if v then est.resNature = est.resNature + v; end - _,_,v = strfind(text, "[Aa]rcane resistance by (%d+)"); if v then est.resArcane = est.resArcane + v; end - - -- Attack power (Battle Shout, Trueshot Aura, Blessing of Might) - _,_,v = strfind(text, "melee attack power by (%d+)"); if v then est.ap = est.ap + v; end - _,_,v = strfind(text, "ranged attack power by (%d+)"); if v then est.rap = est.rap + v; end - _,_,v = strfind(text, "attack power by (%d+)"); - if v and not strfind(text, "ranged") then est.ap = est.ap + v; end - _,_,v = strfind(text, "%+(%d+) Attack Power"); if v then est.ap = est.ap + v; end - - -- Spell power / healing (Arcane Brilliance variants, Blessing of Wisdom, etc. - -- typical is "+X Spell Damage" or "+X Healing") - _,_,v = strfind(text, "%+(%d+) Spell Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Spell Power"); if v then est.spellDmg = est.spellDmg + v; est.healing = est.healing + v; end - _,_,v = strfind(text, "%+(%d+) Healing"); if v then est.healing = est.healing + v; end - - -- Crit % (Rallying Cry, Moonkin Aura, etc.) - _,_,v = strfind(text, "critical strike with spells by (%d+)%%"); - if v then est.critSpell = est.critSpell + v; end - _,_,v = strfind(text, "critical strike by (%d+)%%"); - if v then est.critMelee = est.critMelee + v; end - _,_,v = strfind(text, "critical hit chance by (%d+)%%"); - if v then est.critMelee = est.critMelee + v; est.critSpell = est.critSpell + v; end - - -- Mp5 (Blessing of Wisdom) - _,_,v = strfind(text, "Restores (%d+) mana per 5"); if v then est.mp5 = est.mp5 + v; end - _,_,v = strfind(text, "(%d+) mana every 5"); if v then est.mp5 = est.mp5 + v; end - - -- Armor (Inner Fire, Devotion Aura, Frost Armor, etc.) - _,_,v = strfind(text, "[Ii]ncreases armor by (%d+)"); if v then est.armor = est.armor + v; end - _,_,v = strfind(text, "%+(%d+) Armor"); if v then est.armor = est.armor + v; end - - -- Defense (Blessing of Sanctuary etc.) - _,_,v = strfind(text, "%+(%d+) Defense"); if v then est.defense = est.defense + v; end - - -- Dodge (Cat form etc.) - _,_,v = strfind(text, "chance to dodge by (%d+)%%"); if v then est.dodge = est.dodge + v; end -end - --- Walk every visible buff on the inspected unit and fold recognized stat --- bonuses into `est`. Uses the standard UnitBuff / GameTooltip:SetUnitBuff --- flow available in 1.12. Silently skipped if the APIs are unavailable. -local function RelationshipsDashboard_ScanInspectBuffs(est) - if not est then return; end - if not RelationshipsDashboardTooltip then return; end - if not UnitBuff then return; end - - for i = 1, 32 do - local buffTex = UnitBuff(RD_UNIT, i); - if not buffTex then break; end - RelationshipsDashboard_ResetTooltip(); - -- SetUnitBuff exists on 1.12 clients that support inspect; guard it. - local ok = pcall(function() - RelationshipsDashboardTooltip:SetUnitBuff(RD_UNIT, i); - end); - if ok then - for j = 1, RelationshipsDashboardTooltip:NumLines() do - local tl = getglobal("RelationshipsDashboardTooltipTextLeft" .. j); - local text = tl and tl:GetText(); - RD_ParseBuffLineInto(est, text); - end - end - end -end - - --- Scan every equipped item on the inspected unit and sum up the stat bonuses --- printed on their tooltips. Returns a table of accumulated values. -local function RelationshipsDashboard_ScanInspectGear() - local est = { - str = 0, agi = 0, sta = 0, int = 0, spi = 0, allStats = 0, - armor = 0, - resArcane = 0, resFire = 0, resNature = 0, resFrost = 0, resShadow = 0, allRes = 0, - ap = 0, rap = 0, - critMelee = 0, critSpell = 0, hitMelee = 0, hitSpell = 0, - spellDmg = 0, healing = 0, - defense = 0, dodge = 0, parry = 0, block = 0, blockValue = 0, - mp5 = 0, - mhMin = 0, mhMax = 0, mhSpeed = 0, - ohMin = 0, ohMax = 0, ohSpeed = 0, - rMin = 0, rMax = 0, rSpeed = 0, - }; - - if not RelationshipsDashboardTooltip then return est; end - - -- Parse a single tooltip text line and fold any recognized stat bonuses - -- into `est`. `slotId` is passed through so weapon/ranged lines can be - -- routed correctly. Kept local so both the item-body loop and the - -- set-bonus loop can share exactly the same parsing (no divergence). - local function parseStatLine(text, slotId) - if not text then return; end - local v; - - -- Primary attributes - _,_,v = strfind(text, "%+(%d+) Strength"); if v then est.str = est.str + v; end - _,_,v = strfind(text, "%+(%d+) Agility"); if v then est.agi = est.agi + v; end - _,_,v = strfind(text, "%+(%d+) Stamina"); if v then est.sta = est.sta + v; end - _,_,v = strfind(text, "%+(%d+) Intellect"); if v then est.int = est.int + v; end - _,_,v = strfind(text, "%+(%d+) Spirit"); if v then est.spi = est.spi + v; end - _,_,v = strfind(text, "%+(%d+) to All Stats"); if v then est.allStats = est.allStats + v; end - if not v then _,_,v = strfind(text, "%+(%d+) All Stats"); if v then est.allStats = est.allStats + v; end end - - -- Armor - _,_,v = strfind(text, "(%d+) Armor"); - if v and not strfind(text, "Penetration") then est.armor = est.armor + v; end - - -- Resistances - _,_,v = strfind(text, "%+(%d+) Arcane Resistance"); if v then est.resArcane = est.resArcane + v; end - _,_,v = strfind(text, "%+(%d+) Fire Resistance"); if v then est.resFire = est.resFire + v; end - _,_,v = strfind(text, "%+(%d+) Nature Resistance"); if v then est.resNature = est.resNature + v; end - _,_,v = strfind(text, "%+(%d+) Frost Resistance"); if v then est.resFrost = est.resFrost + v; end - _,_,v = strfind(text, "%+(%d+) Shadow Resistance"); if v then est.resShadow = est.resShadow + v; end - _,_,v = strfind(text, "%+(%d+) to All Resistances"); if v then est.allRes = est.allRes + v; end - if not v then _,_,v = strfind(text, "%+(%d+) All Resistances"); if v then est.allRes = est.allRes + v; end end - - -- Attack power (ranged first) - _,_,v = strfind(text, "ranged attack power by (%d+)"); - if v then - est.rap = est.rap + v; - else - _,_,v = strfind(text, "attack power by (%d+)"); if v then est.ap = est.ap + v; end - _,_,v = strfind(text, "%+(%d+) Attack Power"); if v then est.ap = est.ap + v; end - end - - -- Crit - _,_,v = strfind(text, "critical strike with spells by (%d+)%%"); - if v then - est.critSpell = est.critSpell + v; - else - _,_,v = strfind(text, "critical strike by (%d+)%%"); if v then est.critMelee = est.critMelee + v; end - end - - -- Hit - _,_,v = strfind(text, "chance to hit with spells by (%d+)%%"); - if v then - est.hitSpell = est.hitSpell + v; - else - _,_,v = strfind(text, "chance to hit by (%d+)%%"); if v then est.hitMelee = est.hitMelee + v; end - end - - -- Spell damage / healing - _,_,v = strfind(text, "damage and healing done by magical spells and effects by up to (%d+)"); - if v then - est.spellDmg = est.spellDmg + v; - est.healing = est.healing + v; - else - _,_,v = strfind(text, "damage done by magical spells and effects by up to (%d+)"); - if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "healing done by spells and effects by up to (%d+)"); - if v then est.healing = est.healing + v; end - _,_,v = strfind(text, "healing spells by up to (%d+)"); - if v then est.healing = est.healing + v; end - _,_,v = strfind(text, "%+(%d+) Healing"); if v then est.healing = est.healing + v; end - _,_,v = strfind(text, "%+(%d+) Spell Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Spell Power"); if v then est.spellDmg = est.spellDmg + v; est.healing = est.healing + v; end - _,_,v = strfind(text, "%+(%d+) Fire Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Frost Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Arcane Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Shadow Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Nature Damage"); if v then est.spellDmg = est.spellDmg + v; end - _,_,v = strfind(text, "%+(%d+) Holy Damage"); if v then est.spellDmg = est.spellDmg + v; end - end - - -- Defense - _,_,v = strfind(text, "%+(%d+) Defense"); if v then est.defense = est.defense + v; end - _,_,v = strfind(text, "Increased Defense %+(%d+)"); if v then est.defense = est.defense + v; end - - -- Dodge / Parry / Block - _,_,v = strfind(text, "chance to dodge an attack by (%d+)%%"); if v then est.dodge = est.dodge + v; end - _,_,v = strfind(text, "%+(%d+)%% Dodge"); if v then est.dodge = est.dodge + v; end - _,_,v = strfind(text, "chance to parry an attack by (%d+)%%"); if v then est.parry = est.parry + v; end - _,_,v = strfind(text, "%+(%d+)%% Parry"); if v then est.parry = est.parry + v; end - _,_,v = strfind(text, "chance to block attacks with a shield by (%d+)%%"); if v then est.block = est.block + v; end - _,_,v = strfind(text, "block value of your shield by (%d+)"); if v then est.blockValue = est.blockValue + v; end - - -- Mana per 5 - _,_,v = strfind(text, "Restores (%d+) mana per 5"); if v then est.mp5 = est.mp5 + v; end - if not v then _,_,v = strfind(text, "%+(%d+) mana per 5"); if v then est.mp5 = est.mp5 + v; end end - if not v then _,_,v = strfind(text, "(%d+) [Mm]ana [Pp]er 5"); if v then est.mp5 = est.mp5 + v; end end - if not v then _,_,v = strfind(text, "(%d+) mana every 5"); if v then est.mp5 = est.mp5 + v; end end - - -- Turtle/1.12 additional tooltip variants - _,_,v = strfind(text, "[Ii]ncreases healing done by .- up to (%d+)"); - if v then est.healing = est.healing + v; end - _,_,v = strfind(text, "%+(%d+) [Ss]pell [Cc]rit"); - if v then est.critSpell = est.critSpell + v; end - _,_,v = strfind(text, "%+(%d+)%% [Ss]pell [Cc]rit"); - if v then est.critSpell = est.critSpell + v; end - _,_,v = strfind(text, "[Ii]ncreases attack power in Cat, Bear.- forms by (%d+)"); - if v then est.ap = est.ap + v; end -- Druid feral AP - _,_,v = strfind(text, "Feral Attack Power[:%s]+(%d+)"); - if v then est.ap = est.ap + v; end - -- Turtle common enchant / item phrasing without "Equip:" prefix - _,_,v = strfind(text, "%+(%d+) [Ww]eapon [Dd]amage"); - if v and (slotId == 16 or slotId == 17 or slotId == 18) then - if slotId == 16 then est.mhMin = est.mhMin + v; est.mhMax = est.mhMax + v; - elseif slotId == 17 then est.ohMin = est.ohMin + v; est.ohMax = est.ohMax + v; - else est.rMin = est.rMin + v; est.rMax = est.rMax + v; end - end - _,_,v = strfind(text, "Increases the block value of your shield by (%d+)"); - if v then est.blockValue = est.blockValue + v; end - - -- Weapon / ranged damage + speed (only for the weapon slots) - if slotId == 16 or slotId == 17 or slotId == 18 then - local _, _, dMin, dMax = strfind(text, "(%d+) %- (%d+) Damage"); - local _, _, spd = strfind(text, "Speed (%d+%.%d+)"); - if slotId == 16 then - if dMin then est.mhMin = tonumber(dMin); est.mhMax = tonumber(dMax); end - if spd then est.mhSpeed = tonumber(spd); end - elseif slotId == 17 then - if dMin then est.ohMin = tonumber(dMin); est.ohMax = tonumber(dMax); end - if spd then est.ohSpeed = tonumber(spd); end - else - if dMin then est.rMin = tonumber(dMin); est.rMax = tonumber(dMax); end - if spd then est.rSpeed = tonumber(spd); end - end - end - end - - -- Sets whose bonuses we have already folded in. Each set's bonus lines - -- are printed on EVERY equipped piece's tooltip, so we must apply them - -- exactly once per set no matter how many pieces the target wears. - local processedSets = {}; - - for i = 1, table.getn(EQUIPMENT_SLOTS) do - local slotId = EQUIPMENT_SLOTS[i]; - local itemLink = GetInventoryItemLink(RD_UNIT, slotId); - if itemLink then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, slotId); - - local inSet = false; -- currently inside a set block? - local setSkip = false; -- skip stats for this block? - local setEquipped = 0; -- how many pieces of this set are equipped - for j = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. j); - local text = textLine and textLine:GetText(); - if text then - -- Set header of the form "SetName (equipped/total)". - -- Also matches "Set: SetName (equipped/total)". - local _, _, eqStr, totStr = strfind(text, "%((%d+)/(%d+)%)"); - if eqStr and totStr then - inSet = true; - setEquipped = tonumber(eqStr) or 0; - -- Derive a stable set name key by stripping the count. - local name = gsub(text, "%s*%(%d+/%d+%)%s*$", ""); - name = gsub(name, "^%s*[Ss]et:%s*", ""); - name = gsub(name, "^%s+", ""); - name = gsub(name, "%s+$", ""); - if name == "" then name = "unnamed"; end - if processedSets[name] then - setSkip = true; - else - processedSets[name] = true; - setSkip = false; - end - -- Header line itself carries no stats. - elseif inSet then - -- Inside a set block. Bonus lines can look like: - -- "(3) +25 Strength" - -- "(3) Set: +25 Strength" - -- " Set: (3) +25 Strength" - -- "Set 3: +25 Strength" (Turtle/custom) - -- Piece-list lines have no "(N)" marker. - -- - -- Detection strategy: - -- 1) If we can find a required-piece count anywhere - -- in the line, treat it as a bonus line and apply - -- when the target has enough pieces equipped. - -- 2) As a fallback (in case a tooltip format hides - -- the count), use the FontString color: vanilla - -- renders ACTIVE set bonus lines in green - -- (~0,1,0) and inactive ones in gray (~0.5s). - -- Green => apply, gray => skip. This makes us - -- robust to header/format differences on - -- Turtle WoW / OctoWoW where the plain regex - -- previously dropped every set bonus during - -- inspect. - local _, _, reqStr = strfind(text, "%((%d+)%)"); - if not reqStr then - _,_,reqStr = strfind(text, "[Ss]et%s+(%d+)%s*:"); - end - - local isBonus = false; - local active = false; - if reqStr then - isBonus = true; - local req = tonumber(reqStr) or 99; - active = req <= setEquipped; - end - - -- Color-based override / fallback. GetTextColor() may - -- not exist on very old clients — pcall for safety. - local ok, r, g, b = pcall(function() - return textLine:GetTextColor(); - end); - if ok and type(r) == "number" and type(g) == "number" and type(b) == "number" then - -- Green = active set bonus. - if g > 0.6 and r < 0.4 and b < 0.4 then - isBonus = true; - active = true; - -- Gray = inactive set bonus (only override when we - -- already thought it was a bonus line). - elseif isBonus and r > 0.4 and r < 0.7 - and g > 0.4 and g < 0.7 - and b > 0.4 and b < 0.7 - and math.abs(r - g) < 0.1 and math.abs(g - b) < 0.1 then - active = false; - end - end - - if isBonus and active and not setSkip then - parseStatLine(text, slotId); - end - else - -- Normal item body line. - parseStatLine(text, slotId); - end - end - end - end - end - - - - -- Fold "all stats" / "all resistances" into the individual buckets. - if est.allStats > 0 then - est.str = est.str + est.allStats; est.agi = est.agi + est.allStats; - est.sta = est.sta + est.allStats; est.int = est.int + est.allStats; - est.spi = est.spi + est.allStats; - end - if est.allRes > 0 then - est.resArcane = est.resArcane + est.allRes; est.resFire = est.resFire + est.allRes; - est.resNature = est.resNature + est.allRes; est.resFrost = est.resFrost + est.allRes; - est.resShadow = est.resShadow + est.allRes; - end - - return est; -end - --- Helper: format an estimated number. -local function RD_EstNum(n) - return tostring(floor(n + 0.5)); -end - --- Helper: format an estimated percentage. -local function RD_EstPct(n) - return string.format("%.1f%%", n); -end - --- Populate every dashboard row with estimates derived from the inspected --- unit's equipped gear and level. Called from UpdateStats when RD_UNIT is not --- "player". -function RelationshipsDashboard_UpdateStatsInspect() - local _, class = UnitClass(RD_UNIT); - class = class or "WARRIOR"; - local level = UnitLevel(RD_UNIT) or 1; - if level < 1 then level = 1; end - - local est = RelationshipsDashboard_ScanInspectGear(); - - -- Fold active buffs on the target into the same estimate bucket. This - -- covers Mark/Gift of the Wild, PW:Fortitude, Divine Spirit, Arcane - -- Intellect, Blessing of Kings/Might/Wisdom, Battle Shout, Trueshot - -- Aura, Shadow/Nature/etc. Protection, Inner Fire, and similar. The - -- personal panel gets these automatically via UnitStat; inspect does - -- not, so we parse buff tooltips. Re-fold allStats/allRes afterwards - -- because Gift of the Wild etc. add to those buckets. - local buffAllStatsBefore = est.allStats; - local buffAllResBefore = est.allRes; - RelationshipsDashboard_ScanInspectBuffs(est); - local dAll = est.allStats - buffAllStatsBefore; - if dAll > 0 then - est.str = est.str + dAll; est.agi = est.agi + dAll; - est.sta = est.sta + dAll; est.int = est.int + dAll; - est.spi = est.spi + dAll; - end - local dRes = est.allRes - buffAllResBefore; - if dRes > 0 then - est.resArcane = est.resArcane + dRes; est.resFire = est.resFire + dRes; - est.resNature = est.resNature + dRes; est.resFrost = est.resFrost + dRes; - est.resShadow = est.resShadow + dRes; - end - - - -- Base primary stats scaled by level, plus per-race offset, plus gear. - local anchors = RD_BASE_STATS_60[class] or RD_BASE_STATS_60.WARRIOR; - local _, raceKey = UnitRace(RD_UNIT); - if not raceKey then raceKey = UnitRace(RD_UNIT) or "Human"; end - -- Normalize race key: strip spaces (e.g. "Night Elf" -> "NightElf") and - -- collapse to the table's naming. - if raceKey then raceKey = gsub(raceKey, "%s+", ""); end - local raceMods = RD_RACE_MODS_60[raceKey] or RD_RACE_MODS_60.Human; - -- Vanilla base stats don't grow linearly from level 1. Blizzard's - -- curve is roughly quadratic: growth per level starts small and - -- accelerates. Fit: at L1 primaries are ~20, at L60 they match the - -- class anchor. Approximate with (L-1)/59 raised to ~1.4 so mid-level - -- (30-45) values match Turtle character sheets more closely than - -- straight linear scaling did. - local function baseStat(idx) - local anchor = anchors[idx] + (raceMods[idx] or 0); - if level >= 60 then return anchor; end - local low = 20; -- level-1 baseline - if anchor < low then low = anchor; end - local t = (level - 1) / 59; -- 0..1 - if t < 0 then t = 0; end - -- power curve; exp=1.35 matches vanilla growth within a couple points - local frac = t ^ 1.35; - local b = floor(low + (anchor - low) * frac + 0.5); - if b < 1 then b = 1; end - return b; - end - local totStr = baseStat(1) + est.str; - local totAgi = baseStat(2) + est.agi; - local totSta = baseStat(3) + est.sta; - local totInt = baseStat(4) + est.int; - local totSpi = baseStat(5) + est.spi; - - -- Passive racial resistances (Dwarf +10 Frost, Gnome +10 Arcane, - -- Undead +10 Shadow, NightElf/Tauren/Troll +10 Nature, etc.). - local raceRes = RD_RACE_RES[raceKey]; - if raceRes then - est.resArcane = est.resArcane + (raceRes.arcane or 0); - est.resFire = est.resFire + (raceRes.fire or 0); - est.resNature = est.resNature + (raceRes.nature or 0); - est.resFrost = est.resFrost + (raceRes.frost or 0); - est.resShadow = est.resShadow + (raceRes.shadow or 0); - end - -- Passive racial dodge (Night Elf Quickness = +1%). - est.dodge = est.dodge + (RD_RACE_DODGE[raceKey] or 0); - - - -- ==== General ==== - -- Health and mana ARE visible for a target, so use the real values. - local health = UnitHealth(RD_UNIT); - local maxHealth = UnitHealthMax(RD_UNIT); - if maxHealth and maxHealth > 0 then - RelationshipsDashboardHealthRowValue:SetText(health .. " / " .. maxHealth); - else - RelationshipsDashboardHealthRowValue:SetText("--"); - end - RelationshipsDashboardHealthRowValue:SetTextColor(0.2, 1, 0.2); - - local powerType = UnitPowerType(RD_UNIT); - local power = UnitMana(RD_UNIT); - local maxPower = UnitManaMax(RD_UNIT); - if powerType == 1 then RelationshipsDashboardPowerRowLabel:SetText("Rage:"); - elseif powerType == 2 then RelationshipsDashboardPowerRowLabel:SetText("Focus:"); - elseif powerType == 3 then RelationshipsDashboardPowerRowLabel:SetText("Energy:"); - else RelationshipsDashboardPowerRowLabel:SetText("Mana:"); end - if maxPower and maxPower > 0 then - RelationshipsDashboardPowerRowValue:SetText(power .. " / " .. maxPower); - else - RelationshipsDashboardPowerRowValue:SetText("--"); - end - RelationshipsDashboardPowerRowValue:SetTextColor(0.3, 0.6, 1); - - if RelationshipsDashboard_ClassUsesMana() and powerType ~= 0 then - RelationshipsDashboardManaRowLabel:SetText("Mana:"); - RelationshipsDashboardManaRowValue:SetText("--"); - RelationshipsDashboardManaRowValue:SetTextColor(0.3, 0.6, 1); - end - - -- Armor = item armor + Agility contribution (2 per Agi in vanilla). - local armorEst = est.armor + totAgi * 2; - RelationshipsDashboardArmorRowValue:SetText(RD_EstNum(armorEst)); - RelationshipsDashboardArmorRowValue:SetTextColor(1, 1, 1); - - -- Movement / durability / repair are not knowable for another player. - RelationshipsDashboardMovementRowValue:SetText("--"); - RelationshipsDashboardDurabilityRowValue:SetText("--"); - RelationshipsDashboardDurabilityRowValue:SetTextColor(1, 1, 1); - RelationshipsDashboardRepairCostRowValue:SetText("--"); - - -- GearScore + average item level work off the target's gear directly. - local gearScore, avgILvl = RelationshipsDashboard_CalculateGearScore(); - if gearScore and gearScore > 0 then - RelationshipsDashboardGearScoreRowValue:SetText(tostring(gearScore)); - else - RelationshipsDashboardGearScoreRowValue:SetText("--"); - end - if gearScore >= 3500 then RelationshipsDashboardGearScoreRowValue:SetTextColor(1, 0.5, 0); - elseif gearScore >= 2000 then RelationshipsDashboardGearScoreRowValue:SetTextColor(0.8, 0.2, 1); - elseif gearScore >= 1200 then RelationshipsDashboardGearScoreRowValue:SetTextColor(0, 0.44, 0.87); - elseif gearScore >= 500 then RelationshipsDashboardGearScoreRowValue:SetTextColor(0.12, 1, 0); - else RelationshipsDashboardGearScoreRowValue:SetTextColor(1, 1, 1); end - if avgILvl and avgILvl > 0 then - RelationshipsDashboardAvgILvlRowValue:SetText(string.format("%.1f", avgILvl)); - else - RelationshipsDashboardAvgILvlRowValue:SetText("--"); - end - - -- ==== Attributes ==== - RelationshipsDashboardStrengthRowValue:SetText(RD_EstNum(totStr)); - RelationshipsDashboardAgilityRowValue:SetText(RD_EstNum(totAgi)); - RelationshipsDashboardStaminaRowValue:SetText(RD_EstNum(totSta)); - RelationshipsDashboardIntellectRowValue:SetText(RD_EstNum(totInt)); - RelationshipsDashboardSpiritRowValue:SetText(RD_EstNum(totSpi)); - - -- ==== Offense ==== - -- Melee attack power by class formula. - local ap; - if class == "WARRIOR" or class == "PALADIN" then ap = level * 3 + totStr * 2 - 20; - elseif class == "SHAMAN" then ap = level * 2 + totStr * 2 - 20; - elseif class == "DRUID" then ap = totStr * 2 - 20; - elseif class == "ROGUE" or class == "HUNTER" then ap = level * 2 + totStr + totAgi - 20; - else ap = totStr - 10; end - if ap < 0 then ap = 0; end - ap = ap + est.ap; - est.apTotal = ap; - - -- Melee damage: weapon base + AP contribution (AP/14 per second). - if est.mhSpeed > 0 and est.mhMax > 0 then - local swingBonus = (ap / 14) * est.mhSpeed; - local effMin = est.mhMin + swingBonus; - local effMax = est.mhMax + swingBonus; - RelationshipsDashboardMeleeDmgRowValue:SetText(floor(effMin) .. " - " .. floor(effMax)); - RelationshipsDashboardWeaponDPSRowValue:SetText(string.format("%.1f", (effMin + effMax) / 2 / est.mhSpeed)); - else - RelationshipsDashboardMeleeDmgRowValue:SetText("--"); - RelationshipsDashboardWeaponDPSRowValue:SetText("--"); - end - - RelationshipsDashboardMeleeAPRowValue:SetText(RD_EstNum(ap)); - - if est.mhSpeed > 0 then - if est.ohSpeed > 0 then - RelationshipsDashboardMeleeSpeedRowValue:SetText(RelationshipsDashboard_FormatDecimal(est.mhSpeed) .. " / " .. RelationshipsDashboard_FormatDecimal(est.ohSpeed)); - else - RelationshipsDashboardMeleeSpeedRowValue:SetText(RelationshipsDashboard_FormatDecimal(est.mhSpeed)); - end - else - RelationshipsDashboardMeleeSpeedRowValue:SetText("--"); - end - - -- Weapon skill: player skill API is unavailable for a target, so show the - -- level cap (level * 5). - RelationshipsDashboardWeaponSkillRowValue:SetText(tostring(level * 5) .. " (max)"); - - RelationshipsDashboardMeleeHitRowValue:SetText(RD_EstPct(est.hitMelee)); - - local agiPerCrit = RD_AGI_PER_CRIT[class] or 20; - local meleeCrit = 5 + totAgi / agiPerCrit + est.critMelee; - RelationshipsDashboardMeleeCritRowValue:SetText(RD_EstPct(meleeCrit)); - - -- Ranged Attack Power (Turtle/1.12): Hunters get level*2 + Agi*2 - 20 - -- (2 RAP per Agility, not 1); other classes just use Agility - 10. - local rap; - if class == "HUNTER" then rap = level * 2 + totAgi * 2 - 20; - else rap = totAgi - 10; end - if rap < 0 then rap = 0; end - rap = rap + est.rap; - RelationshipsDashboardRangedAPRowValue:SetText(RD_EstNum(rap)); - - -- Ranged crit: base 5% + Agi/agiPerCrit + gear ranged crit (falls back - -- to melee crit bucket because most 1.12 items list a single crit stat). - local rangedCrit = 5 + totAgi / agiPerCrit + est.critMelee; - RelationshipsDashboardRangedCritRowValue:SetText(RD_EstPct(rangedCrit)); - - if est.rSpeed > 0 then - RelationshipsDashboardRangedSpeedRowValue:SetText(RelationshipsDashboard_FormatDecimal(est.rSpeed)); - else - RelationshipsDashboardRangedSpeedRowValue:SetText("--"); - end - if est.rMax > 0 then - RelationshipsDashboardRangedDmgRowValue:SetText(est.rMin .. " - " .. est.rMax); - else - RelationshipsDashboardRangedDmgRowValue:SetText("--"); - end - - RelationshipsDashboardSpellPowerRowValue:SetText(RD_EstNum(est.spellDmg)); - RelationshipsDashboardHealingPowerRowValue:SetText(RD_EstNum(est.healing)); - RelationshipsDashboardSpellHitRowValue:SetText(RD_EstPct(est.hitSpell)); - - -- Spell crit per Intellect varies by class in vanilla/Turtle. - -- Values are Int-per-1%-crit at level 60. - local intPerSpellCrit; - if class == "MAGE" then intPerSpellCrit = 59.5; - elseif class == "PRIEST" then intPerSpellCrit = 59.2; - elseif class == "WARLOCK" then intPerSpellCrit = 60.6; - elseif class == "DRUID" then intPerSpellCrit = 60.0; - elseif class == "SHAMAN" then intPerSpellCrit = 59.2; - elseif class == "PALADIN" then intPerSpellCrit = 54.0; - else intPerSpellCrit = 60.0; end - local spellCrit = 5 + totInt / intPerSpellCrit + est.critSpell; - RelationshipsDashboardSpellCritRowValue:SetText(RD_EstPct(spellCrit)); - - -- Mana regen: spirit-based (if a mana class) + gear mp5. - local spiritFactor = 0; - if class == "PRIEST" or class == "MAGE" or class == "DRUID" then spiritFactor = 0.125; - elseif class == "WARLOCK" or class == "SHAMAN" or class == "PALADIN" then spiritFactor = 0.1; end - local manaRegen = 0; - if spiritFactor > 0 then manaRegen = totSpi * spiritFactor / 2; end - manaRegen = manaRegen + est.mp5 / 5; - if manaRegen > 0 then - RelationshipsDashboardManaRegenRowValue:SetText(string.format("%.1f/s", manaRegen)); - else - RelationshipsDashboardManaRegenRowValue:SetText("--"); - end - - -- ==== Defenses ==== - local defense = level * 5 + est.defense; - RelationshipsDashboardDefenseRowValue:SetText(RD_EstNum(defense)); - - local dodge = totAgi / agiPerCrit + est.dodge; - RelationshipsDashboardDodgeRowValue:SetText(RD_EstPct(dodge)); - - if RD_CAN_PARRY[class] then - RelationshipsDashboardParryRowValue:SetText(RD_EstPct(5 + est.parry)); - else - RelationshipsDashboardParryRowValue:SetText("--"); - end - - if est.block > 0 or est.blockValue > 0 then - RelationshipsDashboardBlockRowValue:SetText(RD_EstPct(est.block)); - else - RelationshipsDashboardBlockRowValue:SetText("--"); - end - - -- Avoidance: rough miss (5%) + dodge + parry + block. - local avoidance = 5 + dodge + (RD_CAN_PARRY[class] and (5 + est.parry) or 0) + est.block; - RelationshipsDashboardAvoidanceRowValue:SetText(RD_EstPct(avoidance)); - - -- ==== Resistances ==== - RelationshipsDashboardArcaneResRowValue:SetText(RD_EstNum(est.resArcane)); - RelationshipsDashboardFireResRowValue:SetText(RD_EstNum(est.resFire)); - RelationshipsDashboardNatureResRowValue:SetText(RD_EstNum(est.resNature)); - RelationshipsDashboardFrostResRowValue:SetText(RD_EstNum(est.resFrost)); - RelationshipsDashboardShadowResRowValue:SetText(RD_EstNum(est.resShadow)); - - -- Apply class-specific visibility (hide irrelevant stats). - RelationshipsDashboard_ApplyClassVisibility(); -end - -------------------------------------------------------------------------------- --- Update throttling / coalescing. --- --- RelationshipsDashboard_UpdateStats_Impl() is the heavy per-frame refresh --- (iterates every equipment slot, scans tooltips for durability, walks buffs, --- computes GearScore and repair costs). Vanilla WoW fires the events that --- trigger it (UNIT_HEALTH, UNIT_MANA, UNIT_INVENTORY_CHANGED, PLAYER_MONEY, --- etc.) in dense bursts — sometimes many per frame — which produces a --- visible frame-time spike each time the panel is open. --- --- To fix that we split UpdateStats into two entry points: --- * RelationshipsDashboard_UpdateStats() -> mark dirty, defer to next tick --- * RelationshipsDashboard_UpdateStatsNow() -> run immediately (used when --- the panel first opens so the --- user never sees stale data) --- --- The OnUpdate handler flushes at most once per RD_UPDATE_THROTTLE seconds, --- which collapses a whole burst of events into a single refresh. -------------------------------------------------------------------------------- -local RD_UPDATE_THROTTLE = 0.15; -- seconds between coalesced refreshes -RD_StatsDirty = false; -RD_StatsLastUpdate = 0; -RD_StatsAccum = 0; - -function RelationshipsDashboard_UpdateStatsNow() - RD_StatsDirty = false; - RD_StatsAccum = 0; - RelationshipsDashboard_UpdateStats_Impl(); -end - -function RelationshipsDashboard_UpdateStats() - if not RelationshipsDashboardFrame:IsVisible() then - return; - end - -- Defer: OnUpdate will flush this within RD_UPDATE_THROTTLE seconds. - RD_StatsDirty = true; -end - -function RelationshipsDashboard_FlushPendingStats(elapsed) - if not RD_StatsDirty then return; end - RD_StatsAccum = RD_StatsAccum + (elapsed or 0); - if RD_StatsAccum < RD_UPDATE_THROTTLE then return; end - RD_StatsAccum = 0; - RD_StatsDirty = false; - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats_Impl(); - end -end - -function RelationshipsDashboard_UpdateStats_Impl() - - if not RelationshipsDashboardFrame:IsVisible() then - return; - end - - -- Update title text to reflect the unit being displayed - if RelationshipsDashboardTitleText then - local uName = UnitName(RD_UNIT); - if RD_UNIT == "player" or not uName or uName == "" then - RelationshipsDashboardTitleText:SetText("Character Dashboard"); - else - local _, uClass = UnitClass(RD_UNIT); - local uLevel = UnitLevel(RD_UNIT); - local lvlStr = (uLevel and uLevel > 0) and (" (Lvl " .. uLevel .. ")") or ""; - RelationshipsDashboardTitleText:SetText(uName .. lvlStr .. (uClass and (" - " .. uClass) or "")); - end - end - - -- When inspecting another player, the client does not expose their computed - -- stats. Estimate everything from their equipped gear + level instead. - if RD_UNIT ~= "player" then - RelationshipsDashboard_UpdateStatsInspect(); - return; - end - - -- ==== Section: General ==== - -- Health - local health = UnitHealth(RD_UNIT); - local maxHealth = UnitHealthMax(RD_UNIT); - if maxHealth > 0 then - RelationshipsDashboardHealthRowValue:SetText(health .. " / " .. maxHealth); - else - RelationshipsDashboardHealthRowValue:SetText("--"); - end - RelationshipsDashboardHealthRowValue:SetTextColor(0.2, 1, 0.2); - - -- Power (Mana/Rage/Energy) - -- When a mana-capable class is in a non-mana form (e.g., druid bear/cat), - -- PowerRow shows the form's power (Rage/Energy), and ManaRow shows base Mana. - local powerType = UnitPowerType(RD_UNIT); - local power = UnitMana(RD_UNIT); - local maxPower = UnitManaMax(RD_UNIT); - local classUsesMana = RelationshipsDashboard_ClassUsesMana(); - - -- Track mana values when in caster form for use when shifted - if powerType == 0 then - -- Currently in caster form with mana bar visible - RelationshipsDashboard_LastKnownMana = power; - RelationshipsDashboard_LastKnownMaxMana = maxPower; - end - - if powerType == 1 then - RelationshipsDashboardPowerRowLabel:SetText("Rage:"); - RelationshipsDashboardPowerRowValue:SetText(power .. " / " .. maxPower); - RelationshipsDashboardPowerRowValue:SetTextColor(1, 0.2, 0.2); - elseif powerType == 2 then - RelationshipsDashboardPowerRowLabel:SetText("Focus:"); - RelationshipsDashboardPowerRowValue:SetText(power .. " / " .. maxPower); - RelationshipsDashboardPowerRowValue:SetTextColor(1, 0.8, 0); - elseif powerType == 3 then - RelationshipsDashboardPowerRowLabel:SetText("Energy:"); - RelationshipsDashboardPowerRowValue:SetText(power .. " / " .. maxPower); - RelationshipsDashboardPowerRowValue:SetTextColor(1, 1, 0); - else - RelationshipsDashboardPowerRowLabel:SetText("Mana:"); - if maxPower > 0 then - RelationshipsDashboardPowerRowValue:SetText(power .. " / " .. maxPower); - else - RelationshipsDashboardPowerRowValue:SetText("--"); - end - RelationshipsDashboardPowerRowValue:SetTextColor(0.3, 0.6, 1); - end - - -- ManaRow: Show base mana for mana-capable classes in non-mana forms - -- (e.g., druid in bear/cat form shows Rage/Energy on PowerRow + Mana on ManaRow) - -- When already in caster form, ManaRow is hidden by ApplyClassVisibility - if classUsesMana and powerType ~= 0 then - local mana = RelationshipsDashboard_LastKnownMana or 0; - local maxMana = RelationshipsDashboard_LastKnownMaxMana or 0; - -- Try to get current mana directly (some vanilla clients allow this) - -- UnitMana with powerType 0 doesn't work when shifted, so use stored values - RelationshipsDashboardManaRowLabel:SetText("Mana:"); - if maxMana > 0 then - RelationshipsDashboardManaRowValue:SetText(mana .. " / " .. maxMana); - else - RelationshipsDashboardManaRowValue:SetText("--"); - end - RelationshipsDashboardManaRowValue:SetTextColor(0.3, 0.6, 1); - end - - -- Armor - local baseArmor, effectiveArmor, armorBonus, posBuff, negBuff = UnitArmor(RD_UNIT); - if effectiveArmor then - if armorBonus and armorBonus ~= 0 then - RelationshipsDashboardArmorRowValue:SetText(effectiveArmor .. " (+" .. armorBonus .. ")"); - else - RelationshipsDashboardArmorRowValue:SetText(tostring(effectiveArmor)); - end - elseif baseArmor then - RelationshipsDashboardArmorRowValue:SetText(tostring(baseArmor)); - else - RelationshipsDashboardArmorRowValue:SetText("--"); - end - - -- Movement Speed - -- GetUnitSpeed() does NOT exist in vanilla 1.12 / TurtleWoW / OctoWoW - -- Calculate movement speed from known speed-boosting buffs instead - local speedPct = 100; -- Base run speed = 7 yards/sec = 100% - local _, playerClass = UnitClass(RD_UNIT); - - -- Check for speed-increasing buffs by spell name - -- Ghost Wolf (Shaman): 40% bonus = 140% - -- Aspect of the Cheetah (Hunter): 30% bonus = 130% - -- Aspect of the Pack (Hunter): 30% bonus = 130% - -- Sprint (Rogue): 70% bonus = 170% - -- Dash (Druid cat): 70% bonus = 170% - -- Travel Form (Druid): 40% bonus = 140% - -- Cheeta (Druid travel): 40% bonus = 140% - -- Sprint (item/enchant): minor bonus - -- Swiftness potions: 50% bonus = 150% - -- Carrot on a Stick: 3% mounted bonus - -- Mithril Spurs: 4% mounted bonus - -- Riding enchant: 2% mounted bonus - -- Nifty Stopwatch: 5% bonus - - -- Check if mounted (mount speed varies by riding skill) - local isMounted = false; - for i = 1, 32 do - local texture, applications, spellName = UnitBuff(RD_UNIT, i); - if not texture then break; end - if spellName then - -- Mount detection - if strfind(spellName, "Mount") or strfind(spellName, "Steed") or - strfind(spellName, "Charger") or strfind(spellName, "Warhorse") or - strfind(spellName, "Kodo") or strfind(spellName, "Mechanostrider") or - strfind(spellName, "Ram") or strfind(spellName, "Tiger") or - strfind(spellName, "Wolf") or strfind(spellName, "Raptor") or - strfind(spellName, "Skeletal") or strfind(spellName, "Frostsaber") or - strfind(spellName, "Nightsaber") or strfind(spellName, "Horse") or - strfind(spellName, "Palomino") or strfind(spellName, "Pinto") or - strfind(spellName, "Stallion") or strfind(spellName, "Mare") then - isMounted = true; - speedPct = 160; -- Epic mount = 100% bonus (200% total = 160% of base run) - -- Check for standard vs epic mount: we can't easily distinguish - -- Default to showing 200% for epic mount speed - speedPct = 200; - break; - end - end - end - - if not isMounted then - -- Check for speed-boosting buffs - for i = 1, 32 do - local texture, applications, spellName = UnitBuff(RD_UNIT, i); - if not texture then break; end - if spellName then - -- Shaman: Ghost Wolf = 140% speed - if spellName == "Ghost Wolf" then speedPct = 140; break; end - -- Hunter: Aspect of the Cheetah/Pack = 130% speed - if spellName == "Aspect of the Cheetah" then speedPct = 130; break; end - if spellName == "Aspect of the Pack" then speedPct = 130; break; end - -- Rogue: Sprint = 170% speed - if spellName == "Sprint" then speedPct = 170; break; end - -- Druid: Travel Form / Swift Flight Form - if spellName == "Travel Form" then speedPct = 140; break; end - if spellName == "Dash" then speedPct = 170; break; end - if spellName == "Cat Form" then speedPct = 100; break; end - -- Swiftness Potion - if spellName == "Swiftness" then speedPct = 150; break; end - -- Nifty Stopwatch - if spellName == "Speed" then speedPct = 105; break; end - -- Any other speed buff (generic check) - if strfind(strlower(spellName), "sprint") then speedPct = 170; break; end - if strfind(strlower(spellName), "dash") then speedPct = 170; break; end - if strfind(strlower(spellName), "speed") then speedPct = 105; break; end - end - end - end - - RelationshipsDashboardMovementRowValue:SetText(string.format("%.0f%%", speedPct)); - - -- Durability - local avgDurability, repairCost, slotsWithDur = RelationshipsDashboard_CalculateDurabilityAndRepair(); - if RelationshipsDashboard_MerchantRepairCost > 0 then - -- At a repair merchant: show the exact cost the game charges. - repairCost = RelationshipsDashboard_MerchantRepairCost; - else - -- Away from a merchant: scale the raw estimate by the calibration factor - -- learned from the last real merchant repair cost so it matches reality. - repairCost = floor(repairCost * (RD_RepairCalibration or 1)); - end - local durText = RelationshipsDashboard_FormatPercent(avgDurability); - if avgDurability >= 75 then - RelationshipsDashboardDurabilityRowValue:SetTextColor(0.2, 1, 0.2); - elseif avgDurability >= 50 then - RelationshipsDashboardDurabilityRowValue:SetTextColor(1, 1, 0); - elseif avgDurability >= 25 then - RelationshipsDashboardDurabilityRowValue:SetTextColor(1, 0.5, 0); - else - RelationshipsDashboardDurabilityRowValue:SetTextColor(1, 0.2, 0.2); - end - RelationshipsDashboardDurabilityRowValue:SetText(durText); - - -- Repair Total - RelationshipsDashboardRepairCostRowValue:SetText(RelationshipsDashboard_FormatMoney(repairCost)); - - -- GearScore - local gearScore, avgILvl, equippedCount = RelationshipsDashboard_CalculateGearScore(); - if gearScore > 0 then - RelationshipsDashboardGearScoreRowValue:SetText(tostring(gearScore)); - else - RelationshipsDashboardGearScoreRowValue:SetText("--"); - end - if gearScore >= 3500 then - RelationshipsDashboardGearScoreRowValue:SetTextColor(1, 0.5, 0); - elseif gearScore >= 2000 then - RelationshipsDashboardGearScoreRowValue:SetTextColor(0.8, 0.2, 1); - elseif gearScore >= 1200 then - RelationshipsDashboardGearScoreRowValue:SetTextColor(0, 0.44, 0.87); - elseif gearScore >= 500 then - RelationshipsDashboardGearScoreRowValue:SetTextColor(0.12, 1, 0); - else - RelationshipsDashboardGearScoreRowValue:SetTextColor(1, 1, 1); - end - - -- Avg Item Level - if avgILvl > 0 then - RelationshipsDashboardAvgILvlRowValue:SetText(string.format("%.1f", avgILvl)); - else - RelationshipsDashboardAvgILvlRowValue:SetText("--"); - end - - -- ==== Section: Attributes ==== - -- UnitStat indices: 1=STR, 2=AGI, 3=STA, 4=INT, 5=SPI - local baseStr, effectiveStr = UnitStat(RD_UNIT, 1); - if effectiveStr then - local bonus = effectiveStr - baseStr; - if bonus ~= 0 then - RelationshipsDashboardStrengthRowValue:SetText(effectiveStr .. " (" .. baseStr .. " +" .. bonus .. ")"); - else - RelationshipsDashboardStrengthRowValue:SetText(tostring(effectiveStr)); - end - else - RelationshipsDashboardStrengthRowValue:SetText(tostring(baseStr)); - end - - local baseAgi, effectiveAgi = UnitStat(RD_UNIT, 2); - if effectiveAgi then - local bonus = effectiveAgi - baseAgi; - if bonus ~= 0 then - RelationshipsDashboardAgilityRowValue:SetText(effectiveAgi .. " (" .. baseAgi .. " +" .. bonus .. ")"); - else - RelationshipsDashboardAgilityRowValue:SetText(tostring(effectiveAgi)); - end - else - RelationshipsDashboardAgilityRowValue:SetText(tostring(baseAgi)); - end - - local baseSta, effectiveSta = UnitStat(RD_UNIT, 3); - if effectiveSta then - local bonus = effectiveSta - baseSta; - if bonus ~= 0 then - RelationshipsDashboardStaminaRowValue:SetText(effectiveSta .. " (" .. baseSta .. " +" .. bonus .. ")"); - else - RelationshipsDashboardStaminaRowValue:SetText(tostring(effectiveSta)); - end - else - RelationshipsDashboardStaminaRowValue:SetText(tostring(baseSta)); - end - - local baseInt, effectiveInt = UnitStat(RD_UNIT, 4); - if effectiveInt then - local bonus = effectiveInt - baseInt; - if bonus ~= 0 then - RelationshipsDashboardIntellectRowValue:SetText(effectiveInt .. " (" .. baseInt .. " +" .. bonus .. ")"); - else - RelationshipsDashboardIntellectRowValue:SetText(tostring(effectiveInt)); - end - else - RelationshipsDashboardIntellectRowValue:SetText(tostring(baseInt)); - end - - local baseSpi, effectiveSpi = UnitStat(RD_UNIT, 5); - if effectiveSpi then - local bonus = effectiveSpi - baseSpi; - if bonus ~= 0 then - RelationshipsDashboardSpiritRowValue:SetText(effectiveSpi .. " (" .. baseSpi .. " +" .. bonus .. ")"); - else - RelationshipsDashboardSpiritRowValue:SetText(tostring(effectiveSpi)); - end - else - RelationshipsDashboardSpiritRowValue:SetText(tostring(baseSpi)); - end - - -- ==== Section: Offense ==== - -- Damage (first in DejaStats Offense section) - local minDamage, maxDamage, minOff, maxOff, posBuffDmg, negBuffDmg = UnitDamage(RD_UNIT); - if minDamage and maxDamage then - local effectiveMin = minDamage + (posBuffDmg or 0) + (negBuffDmg or 0); - local effectiveMax = maxDamage + (posBuffDmg or 0) + (negBuffDmg or 0); - local dmgText = floor(effectiveMin) .. " - " .. floor(effectiveMax); - if minOff and maxOff and minOff > 0 then - local effectiveMinOff = minOff + (posBuffDmg or 0) + (negBuffDmg or 0); - local effectiveMaxOff = maxOff + (posBuffDmg or 0) + (negBuffDmg or 0); - dmgText = dmgText .. " / " .. floor(effectiveMinOff) .. " - " .. floor(effectiveMaxOff); - end - RelationshipsDashboardMeleeDmgRowValue:SetText(dmgText); - else - RelationshipsDashboardMeleeDmgRowValue:SetText("--"); - end - - -- Attack Power - local baseAP, posBuffAP, negBuffAP = UnitAttackPower(RD_UNIT); - if baseAP then - local totalAP = baseAP + (posBuffAP or 0) + (negBuffAP or 0); - local apBonus = (posBuffAP or 0) + (negBuffAP or 0); - if apBonus ~= 0 then - RelationshipsDashboardMeleeAPRowValue:SetText(totalAP .. " (+" .. apBonus .. ")"); - else - RelationshipsDashboardMeleeAPRowValue:SetText(tostring(totalAP)); - end - else - RelationshipsDashboardMeleeAPRowValue:SetText("--"); - end - - -- Attack Speed - local mainSpeed, offSpeed = UnitAttackSpeed(RD_UNIT); - if mainSpeed then - if offSpeed and offSpeed > 0 then - RelationshipsDashboardMeleeSpeedRowValue:SetText(RelationshipsDashboard_FormatDecimal(mainSpeed) .. " / " .. RelationshipsDashboard_FormatDecimal(offSpeed)); - else - RelationshipsDashboardMeleeSpeedRowValue:SetText(RelationshipsDashboard_FormatDecimal(mainSpeed)); - end - else - RelationshipsDashboardMeleeSpeedRowValue:SetText("--"); - end - - -- Weapon DPS - if mainSpeed and minDamage and maxDamage then - local effectiveMin = minDamage + (posBuffDmg or 0) + (negBuffDmg or 0); - local effectiveMax = maxDamage + (posBuffDmg or 0) + (negBuffDmg or 0); - local avgDmg = (effectiveMin + effectiveMax) / 2; - local weaponDPS = avgDmg / mainSpeed; - RelationshipsDashboardWeaponDPSRowValue:SetText(string.format("%.2f", weaponDPS)); - else - RelationshipsDashboardWeaponDPSRowValue:SetText("--"); - end - - -- Weapon Skill - RelationshipsDashboardWeaponSkillRowValue:SetText(RelationshipsDashboard_GetWeaponSkill()); - - -- Hit Chance - local meleeHit = RelationshipsDashboard_GetMeleeHitChance(); - RelationshipsDashboardMeleeHitRowValue:SetText(RelationshipsDashboard_FormatPercent(meleeHit)); - - -- Crit Chance - local meleeCrit = RelationshipsDashboard_GetMeleeCritChance(); - RelationshipsDashboardMeleeCritRowValue:SetText(RelationshipsDashboard_FormatPercent(meleeCrit)); - - -- ==== Section: Enhancements ==== - -- Ranged Attack Power - local baseRAP, posBuffRAP, negBuffRAP = UnitRangedAttackPower(RD_UNIT); - if baseRAP then - local totalRAP = baseRAP + (posBuffRAP or 0) + (negBuffRAP or 0); - RelationshipsDashboardRangedAPRowValue:SetText(tostring(totalRAP)); - else - RelationshipsDashboardRangedAPRowValue:SetText("--"); - end - - -- Ranged Crit - local rangedCrit = RelationshipsDashboard_GetRangedCritChance(); - RelationshipsDashboardRangedCritRowValue:SetText(RelationshipsDashboard_FormatPercent(rangedCrit)); - - -- Ranged Speed - -- UnitRangedDamage does NOT exist in vanilla 1.12 - -- Get ranged attack speed from the equipped ranged weapon's tooltip - local rangedSpeedVal = nil; - local rangedItemLink = GetInventoryItemLink(RD_UNIT, 18); - if rangedItemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - local ok, err = pcall(function() RelationshipsDashboardTooltip:SetInventoryItem(RD_UNIT, 18); end); - if ok then - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - -- Match weapon speed pattern: "Speed XX.XX" - local _, _, spd = strfind(text, "Speed (%d+%.%d+)"); - if spd then - rangedSpeedVal = tonumber(spd); - break; - end - end - end - end - end - end - if rangedSpeedVal then - RelationshipsDashboardRangedSpeedRowValue:SetText(RelationshipsDashboard_FormatDecimal(rangedSpeedVal)); - else - RelationshipsDashboardRangedSpeedRowValue:SetText("--"); - end - - -- Ranged Damage - local rangedItemLink = GetInventoryItemLink(RD_UNIT, 18); - if rangedItemLink and RelationshipsDashboardTooltip then - RelationshipsDashboard_ResetTooltip(); - RelationshipsDashboardTooltip_SetInventoryItemSafe(RD_UNIT, 18); - for i = 1, RelationshipsDashboardTooltip:NumLines() do - local textLine = getglobal("RelationshipsDashboardTooltipTextLeft" .. i); - if textLine then - local text = textLine:GetText(); - if text then - local _, _, dMin, dMax = strfind(text, "(%d+)%s*-%s*(%d+)%s+Damage"); - if dMin and dMax then - RelationshipsDashboardRangedDmgRowValue:SetText(dMin .. " - " .. dMax); - break; - end - end - end - end - else - RelationshipsDashboardRangedDmgRowValue:SetText("--"); - end - - -- Spell Power - local spellPower = RelationshipsDashboard_GetSpellPower(); - RelationshipsDashboardSpellPowerRowValue:SetText(tostring(spellPower)); - - -- Healing Power - local healingPower = RelationshipsDashboard_GetHealingPower(); - RelationshipsDashboardHealingPowerRowValue:SetText(tostring(healingPower)); - - -- Spell Hit - local spellHit = RelationshipsDashboard_GetSpellHitChance(); - RelationshipsDashboardSpellHitRowValue:SetText(RelationshipsDashboard_FormatPercent(spellHit)); - - -- Spell Crit - local spellCrit = RelationshipsDashboard_GetSpellCritChance(); - RelationshipsDashboardSpellCritRowValue:SetText(RelationshipsDashboard_FormatPercent(spellCrit)); - - -- Mana Regen - local manaRegen = RelationshipsDashboard_GetManaRegen(); - if manaRegen > 0 then - RelationshipsDashboardManaRegenRowValue:SetText(string.format("%.1f/s", manaRegen)); - else - RelationshipsDashboardManaRegenRowValue:SetText("--"); - end - - -- ==== Section: Defenses ==== - -- Defense - local defense = RelationshipsDashboard_GetDefense(); - RelationshipsDashboardDefenseRowValue:SetText(tostring(defense)); - - -- Dodge - if GetDodgeChance then - RelationshipsDashboardDodgeRowValue:SetText(RelationshipsDashboard_FormatPercent(GetDodgeChance())); - else - RelationshipsDashboardDodgeRowValue:SetText("--"); - end - - -- Parry - local parryChance = RelationshipsDashboard_GetParryChance(); - RelationshipsDashboardParryRowValue:SetText(RelationshipsDashboard_FormatPercent(parryChance)); - - -- Block - local blockChance = RelationshipsDashboard_GetBlockChance(); - RelationshipsDashboardBlockRowValue:SetText(RelationshipsDashboard_FormatPercent(blockChance)); - - -- Avoidance (Dodge + Parry + Block + Miss chance) - local missChance = 5; - local level = UnitLevel(RD_UNIT); - local defenseSkill = defense or (level * 5); - if defenseSkill > level * 5 then - missChance = 5 + (defenseSkill - level * 5) * 0.04; - end - local dodgeChance = (GetDodgeChance and GetDodgeChance()) or 0; - local avoidance = missChance + dodgeChance + parryChance + blockChance; - RelationshipsDashboardAvoidanceRowValue:SetText(RelationshipsDashboard_FormatPercent(avoidance)); - - -- ==== Section: Resistances ==== - -- Resistance indices: 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane - local resBase, resTotal, resBonus, resPos, resNeg; - - -- Arcane (index 6) - resBase, resTotal, resBonus, resPos, resNeg = UnitResistance(RD_UNIT, RESISTANCE_INDICES.arcane); - RelationshipsDashboardArcaneResRowValue:SetText(tostring(resTotal or 0)); - - -- Fire (index 2) - resBase, resTotal, resBonus, resPos, resNeg = UnitResistance(RD_UNIT, RESISTANCE_INDICES.fire); - RelationshipsDashboardFireResRowValue:SetText(tostring(resTotal or 0)); - - -- Nature (index 3) - resBase, resTotal, resBonus, resPos, resNeg = UnitResistance(RD_UNIT, RESISTANCE_INDICES.nature); - RelationshipsDashboardNatureResRowValue:SetText(tostring(resTotal or 0)); - - -- Frost (index 4) - resBase, resTotal, resBonus, resPos, resNeg = UnitResistance(RD_UNIT, RESISTANCE_INDICES.frost); - RelationshipsDashboardFrostResRowValue:SetText(tostring(resTotal or 0)); - - -- Shadow (index 5) - resBase, resTotal, resBonus, resPos, resNeg = UnitResistance(RD_UNIT, RESISTANCE_INDICES.shadow); - RelationshipsDashboardShadowResRowValue:SetText(tostring(resTotal or 0)); - - -- ==== Apply class-specific visibility (hide irrelevant stats) ==== - RelationshipsDashboard_ApplyClassVisibility(); -end - -------------------------------------------------------------------------------- --- Show/Hide Logic --- Toggle the stats frame visibility with the CharacterFrame -------------------------------------------------------------------------------- -local function RelationshipsDashboard_ToggleVisibility() - if CharacterFrame:IsVisible() then - RelationshipsDashboardFrame:Show(); - RelationshipsDashboard_UpdateStatsNow(); - - else - RelationshipsDashboardFrame:Hide(); - end -end - -------------------------------------------------------------------------------- --- Hook into CharacterFrame show/hide --- NOTE: HookScript() does NOT exist in vanilla 1.12 (added in TBC 2.1.0) --- hooksecurefunc() does NOT exist either (added in TBC 2.0) --- We must override the Show/Hide methods directly and call the originals. --- Lua 5.0 uses unpack(arg) instead of ... for varargs in some contexts, --- but function(...) works fine in Lua 5.0 as long as we forward args correctly. -------------------------------------------------------------------------------- -local RelationshipsDashboard_CharacterFrameHooked = false; - -local function RelationshipsDashboard_HookCharacterFrame() - if CharacterFrame and not RelationshipsDashboard_CharacterFrameHooked then - RelationshipsDashboard_CharacterFrameHooked = true; - - -- Override CharacterFrame.Show to also show our stats panel - local origShow = CharacterFrame.Show; - CharacterFrame.Show = function(...) - origShow(unpack(arg)); - RelationshipsDashboardFrame:Show(); - RelationshipsDashboard_UpdateStatsNow(); - - end; - - -- Override CharacterFrame.Hide to also hide our stats panel - local origHide = CharacterFrame.Hide; - CharacterFrame.Hide = function(...) - origHide(unpack(arg)); - RelationshipsDashboardFrame:Hide(); - end; - end -end - -------------------------------------------------------------------------------- --- Hook InspectFrame so that inspecting another player also opens this --- dashboard and shows THEIR stats (via unit "target"). When the inspect --- window closes we revert RD_UNIT back to "player". -------------------------------------------------------------------------------- -local RelationshipsDashboard_InspectFrameHooked = false; - -------------------------------------------------------------------------------- --- Force the scroll viewport mask frames to a very high frame level so they --- always draw ON TOP of the stat-row sub-frames inside the ScrollChild. --- Must be re-applied whenever we change RelationshipsDashboardFrame's parent --- or frame level (e.g. when hooking InspectFrame), because vanilla 1.12 --- cascades frame level changes down to child frames. -------------------------------------------------------------------------------- -function RelationshipsDashboard_BoostMaskLevels() - local base = RelationshipsDashboardFrame:GetFrameLevel() or 2; - local contentLevel = base + 10; - local target = base + 100; - - if RelationshipsDashboardScrollFrame then - RelationshipsDashboardScrollFrame:SetFrameLevel(contentLevel); - end - if RelationshipsDashboardContent then - RelationshipsDashboardContent:SetFrameLevel(contentLevel + 1); - end - RelationshipsDashboard_ForEachScrollElement(function(element) - element:SetFrameLevel(contentLevel + 2); - end); - - if RelationshipsDashboardMaskTop then - RelationshipsDashboardMaskTop:SetFrameLevel(target); - end - if RelationshipsDashboardMaskBottom then - RelationshipsDashboardMaskBottom:SetFrameLevel(target); - end -end - - -function RelationshipsDashboard_TryHookInspectFrame() - if RelationshipsDashboard_InspectFrameHooked then return; end - if not InspectFrame then return; end - RelationshipsDashboard_InspectFrameHooked = true; - - local origShow = InspectFrame.Show; - InspectFrame.Show = function(...) - origShow(unpack(arg)); - -- Switch to inspect target - RD_UNIT = "target"; - if UnitExists("target") and UnitIsPlayer("target") then - -- IMPORTANT: parent to UIParent, NOT InspectFrame. - -- InspectFrame has child sub-frames (PaperDoll, model) with high - -- framelevels that will draw above our BG texture if we are a - -- child of InspectFrame — that's what caused the "text bleeding - -- through / scrolling through the inspect UI" bug. - RelationshipsDashboardFrame:SetParent(UIParent); - RelationshipsDashboardFrame:ClearAllPoints(); - -- Match the self (CharacterFrame) dashboard position: sit just off - -- the frame's right edge with the same -30 overlap offset so the - -- inspect dashboard lines up exactly like the personal one. - RelationshipsDashboardFrame:SetPoint("TOPLEFT", InspectFrame, "TOPRIGHT", -30, -16); - RelationshipsDashboardFrame:SetFrameStrata("HIGH"); - RelationshipsDashboardFrame:SetFrameLevel(20); - RelationshipsDashboard_BoostMaskLevels(); - RelationshipsDashboardFrame:Show(); - RelationshipsDashboard_ApplyClassVisibility(); - RelationshipsDashboard_UpdateStats(); - end - end; - - local origHide = InspectFrame.Hide; - InspectFrame.Hide = function(...) - origHide(unpack(arg)); - -- Restore player context and re-parent to CharacterFrame with the - -- original overlapping anchor (CharacterFrame's right edge is empty - -- border, so overlap is safe there). - RD_UNIT = "player"; - RelationshipsDashboardFrame:Hide(); - RelationshipsDashboardFrame:SetParent(CharacterFrame); - RelationshipsDashboardFrame:ClearAllPoints(); - RelationshipsDashboardFrame:SetPoint("TOPLEFT", CharacterFrame, "TOPRIGHT", -30, -16); - RelationshipsDashboardFrame:SetFrameStrata("MEDIUM"); - RelationshipsDashboardFrame:SetFrameLevel(2); - RelationshipsDashboard_BoostMaskLevels(); - if CharacterFrame:IsVisible() then - RelationshipsDashboardFrame:Show(); - RelationshipsDashboard_ApplyClassVisibility(); - RelationshipsDashboard_UpdateStats(); - end - end; -end - -------------------------------------------------------------------------------- --- OnLoad - Initialize the addon -------------------------------------------------------------------------------- -function RelationshipsDashboard_OnLoad() - -- Create the tooltip scanner (hidden, used for data extraction) - RelationshipsDashboardTooltip = CreateFrame("GameTooltip", "RelationshipsDashboardTooltip", nil, "GameTooltipTemplate"); - RelationshipsDashboardTooltip:SetOwner(WorldFrame, "ANCHOR_NONE"); - - -- Initialize section header text (DejaStats-style gold section headers) - RDHeaderGeneralText:SetText("General"); - RDHeaderAttributesText:SetText("Attributes"); - RDHeaderOffenseText:SetText("Offense"); - RDHeaderEnhancementsText:SetText("Enhancements"); - RDHeaderDefensesText:SetText("Defenses"); - RDHeaderResistancesText:SetText("Resistances"); - - -- Initialize stat row labels matching DejaStats layout - -- Section: General - RelationshipsDashboardHealthRowLabel:SetText("Health:"); - RelationshipsDashboardPowerRowLabel:SetText("Mana:"); - RelationshipsDashboardManaRowLabel:SetText("Mana:"); - RelationshipsDashboardArmorRowLabel:SetText("Armor:"); - RelationshipsDashboardMovementRowLabel:SetText("Movement Speed:"); - RelationshipsDashboardDurabilityRowLabel:SetText("Durability:"); - RelationshipsDashboardRepairCostRowLabel:SetText("Repair Total:"); - RelationshipsDashboardRepairCostRowValue:SetTextColor(1, 0.2, 0.2); - RelationshipsDashboardGearScoreRowLabel:SetText("GearScore:"); - RelationshipsDashboardAvgILvlRowLabel:SetText("Avg Item Level:"); - - -- Section: Attributes - RelationshipsDashboardStrengthRowLabel:SetText("Strength:"); - RelationshipsDashboardAgilityRowLabel:SetText("Agility:"); - RelationshipsDashboardStaminaRowLabel:SetText("Stamina:"); - RelationshipsDashboardIntellectRowLabel:SetText("Intellect:"); - RelationshipsDashboardSpiritRowLabel:SetText("Spirit:"); - - -- Section: Offense - RelationshipsDashboardMeleeDmgRowLabel:SetText("Damage:"); - RelationshipsDashboardMeleeAPRowLabel:SetText("Attack Power:"); - RelationshipsDashboardMeleeSpeedRowLabel:SetText("Attack Speed:"); - RelationshipsDashboardWeaponDPSRowLabel:SetText("Weapon DPS:"); - RelationshipsDashboardWeaponSkillRowLabel:SetText("Weapon Skill:"); - RelationshipsDashboardMeleeHitRowLabel:SetText("Hit Chance:"); - RelationshipsDashboardMeleeCritRowLabel:SetText("Crit Chance:"); - - -- Section: Enhancements - RelationshipsDashboardRangedAPRowLabel:SetText("Ranged AP:"); - RelationshipsDashboardRangedCritRowLabel:SetText("Ranged Crit:"); - RelationshipsDashboardRangedSpeedRowLabel:SetText("Ranged Speed:"); - RelationshipsDashboardRangedDmgRowLabel:SetText("Ranged Damage:"); - RelationshipsDashboardSpellPowerRowLabel:SetText("Spell Power:"); - RelationshipsDashboardHealingPowerRowLabel:SetText("Healing Power:"); - RelationshipsDashboardSpellHitRowLabel:SetText("Spell Hit:"); - RelationshipsDashboardSpellCritRowLabel:SetText("Spell Crit:"); - RelationshipsDashboardManaRegenRowLabel:SetText("Mana Regen:"); - - -- Section: Defenses - RelationshipsDashboardDefenseRowLabel:SetText("Defense:"); - RelationshipsDashboardDodgeRowLabel:SetText("Dodge:"); - RelationshipsDashboardParryRowLabel:SetText("Parry:"); - RelationshipsDashboardBlockRowLabel:SetText("Block:"); - RelationshipsDashboardAvoidanceRowLabel:SetText("Avoidance:"); - RelationshipsDashboardAvoidanceRowValue:SetTextColor(0.2, 1, 0.2); - - -- Section: Resistances (school-colored labels) - RelationshipsDashboardArcaneResRowLabel:SetText("Arcane:"); - RelationshipsDashboardFireResRowLabel:SetText("Fire:"); - RelationshipsDashboardNatureResRowLabel:SetText("Nature:"); - RelationshipsDashboardFrostResRowLabel:SetText("Frost:"); - RelationshipsDashboardShadowResRowLabel:SetText("Shadow:"); - - -- Resistance label color overrides (school colors matching DejaStats) - RelationshipsDashboardArcaneResRowLabel:SetTextColor(0.7, 0.2, 1); -- Arcane (purple) - RelationshipsDashboardFireResRowLabel:SetTextColor(1, 0.4, 0.1); -- Fire (red-orange) - RelationshipsDashboardNatureResRowLabel:SetTextColor(0.2, 0.9, 0.2); -- Nature (green) - RelationshipsDashboardFrostResRowLabel:SetTextColor(0.3, 0.6, 1); -- Frost (blue) - RelationshipsDashboardShadowResRowLabel:SetTextColor(0.6, 0.2, 0.8); -- Shadow (violet) - - -- Power label override: update based on class power type at load - -- This will be refined on each UpdateStats call - local powerType = UnitPowerType("player"); - if powerType == 1 then - RelationshipsDashboardPowerRowLabel:SetText("Rage:"); - elseif powerType == 2 then - RelationshipsDashboardPowerRowLabel:SetText("Focus:"); - elseif powerType == 3 then - RelationshipsDashboardPowerRowLabel:SetText("Energy:"); - else - RelationshipsDashboardPowerRowLabel:SetText("Mana:"); - end - - -- Register for events - local frame = RelationshipsDashboardFrame; - frame:RegisterEvent("UNIT_STATS"); - frame:RegisterEvent("UNIT_ATTACK_POWER"); - frame:RegisterEvent("UNIT_RANGED_ATTACK_POWER"); - frame:RegisterEvent("UNIT_ATTACK_SPEED"); - frame:RegisterEvent("UNIT_RESISTANCES"); - frame:RegisterEvent("UNIT_DEFENSE"); - frame:RegisterEvent("UNIT_DAMAGE"); - frame:RegisterEvent("UPDATE_INVENTORY_ALERTS"); - frame:RegisterEvent("UNIT_INVENTORY_CHANGED"); - frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED"); - frame:RegisterEvent("UNIT_MODEL_CHANGED"); - frame:RegisterEvent("PLAYER_ENTERING_WORLD"); - frame:RegisterEvent("PLAYER_MONEY"); - frame:RegisterEvent("UPDATE_TOOLTIP"); - frame:RegisterEvent("MERCHANT_SHOW"); - frame:RegisterEvent("MERCHANT_UPDATE"); - frame:RegisterEvent("MERCHANT_CLOSED"); - frame:RegisterEvent("UNIT_DISPLAYPOWER"); - frame:RegisterEvent("UNIT_MANA"); - frame:RegisterEvent("UNIT_RAGE"); - frame:RegisterEvent("UNIT_ENERGY"); - frame:RegisterEvent("UNIT_FOCUS"); - frame:RegisterEvent("UNIT_HEALTH"); - frame:RegisterEvent("PLAYER_DEAD"); - frame:RegisterEvent("PLAYER_REGEN_DISABLED"); - frame:RegisterEvent("PLAYER_REGEN_ENABLED"); - frame:RegisterEvent("ZONE_CHANGED_NEW_AREA"); - frame:RegisterEvent("ZONE_CHANGED"); - frame:RegisterEvent("UNIT_MAXHEALTH"); - frame:RegisterEvent("UNIT_MAXMANA"); - frame:RegisterEvent("UNIT_MAXRAGE"); - frame:RegisterEvent("UNIT_MAXENERGY"); - frame:RegisterEvent("UNIT_MAXFOCUS"); - frame:RegisterEvent("ADDON_LOADED"); - frame:RegisterEvent("VARIABLES_LOADED"); - frame:RegisterEvent("PLAYER_TARGET_CHANGED"); - frame:RegisterEvent("PLAYER_UNGHOST"); -- Player is no longer a ghost (rez'd) - frame:RegisterEvent("PLAYER_ALIVE"); -- Fires on both corpse-run and SH rez - frame:RegisterEvent("CORPSE_IN_RANGE"); -- Near own corpse (retrieval possible) - frame:RegisterEvent("CORPSE_OUT_OF_RANGE"); - -- Explicitly flush the saved-durability table back to its SavedVariable - -- global on logout / /reload. WoW normally handles this automatically, - -- but this safety net protects against the reference being unhooked by - -- any future edit that reassigns RD_SavedDurability locally. - frame:RegisterEvent("DUEL_REQUESTED"); - frame:RegisterEvent("DUEL_FINISHED"); - frame:RegisterEvent("PLAYER_LOGOUT"); - - - - -- Hide the scrollbar and ALL its sub-elements for a flat UI - -- UIPanelScrollFrameTemplate (WoW 1.12) creates $parentScrollBar (a Slider) with: - -- $parentScrollUpButton (Button: NormalTexture, PushedTexture, DisabledTexture, HighlightTexture) - -- $parentScrollDownButton (Button: NormalTexture, PushedTexture, DisabledTexture, HighlightTexture) - -- $parentThumbTexture (Texture inheriting UIPanelScrollBarButton) - -- We hide AND set alpha=0 on every element to guarantee nothing is visible - local scrollBar = getglobal("RelationshipsDashboardScrollFrameScrollBar"); - if scrollBar then - scrollBar:Hide(); - scrollBar:SetAlpha(0); - - -- Hide and make transparent the up/down arrow buttons and all their texture states - local scrollUp = getglobal("RelationshipsDashboardScrollFrameScrollBarScrollUpButton"); - local scrollDown = getglobal("RelationshipsDashboardScrollFrameScrollBarScrollDownButton"); - if scrollUp then - scrollUp:Hide(); - scrollUp:SetAlpha(0); - -- Hide each texture state on the Up button - local tex; - tex = scrollUp:GetNormalTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - tex = scrollUp:GetPushedTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - tex = scrollUp:GetDisabledTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - tex = scrollUp:GetHighlightTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - end - if scrollDown then - scrollDown:Hide(); - scrollDown:SetAlpha(0); - -- Hide each texture state on the Down button - local tex; - tex = scrollDown:GetNormalTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - tex = scrollDown:GetPushedTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - tex = scrollDown:GetDisabledTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - tex = scrollDown:GetHighlightTexture(); if tex then tex:SetAlpha(0); tex:Hide(); end - end - - -- Hide and make transparent the thumb (the draggable block in the scrollbar) - -- In WoW 1.12, ThumbTexture is a single Texture (no Left/Right/Middle sub-textures) - local thumb = getglobal("RelationshipsDashboardScrollFrameScrollBarThumbTexture"); - if thumb then - thumb:SetAlpha(0); - thumb:Hide(); - end - end - - -- Enable mouse wheel scrolling on the scroll frame - -- The ScrollFrame doesn't natively handle mouse wheel, so we add it - local scrollFrame = RelationshipsDashboardScrollFrame; - scrollFrame:EnableMouseWheel(true); - scrollFrame:SetScript("OnMouseWheel", function() - -- In WoW 1.12 Lua 5.0, arg1 contains the scroll delta - -- Positive = scroll up, Negative = scroll down - local delta = arg1; - local scrollBar = getglobal("RelationshipsDashboardScrollFrameScrollBar"); - if scrollBar then - local minVal, maxVal = scrollBar:GetMinMaxValues(); - local current = scrollBar:GetValue(); - local step = 30; -- pixels per scroll step - if delta > 0 then - -- Scroll up - if current - step < minVal then - scrollBar:SetValue(minVal); - else - scrollBar:SetValue(current - step); - end - else - -- Scroll down - if current + step > maxVal then - scrollBar:SetValue(maxVal); - else - scrollBar:SetValue(current + step); - end - end - RelationshipsDashboard_UpdateViewportClip(); - end - end); - - frame:SetScript("OnUpdate", function() - -- Vanilla 1.12 passes elapsed via arg1 (Lua 5.0 has no `...` here). - local elapsed = arg1 or 0; - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateViewportClip(); - -- Flush any pending coalesced stat refresh (throttled). - RelationshipsDashboard_FlushPendingStats(elapsed); - end - -- Detect ghost-state transitions (spirit-release → ghost, or rez → - -- alive) as they happen, so the durability % and estimated repair - -- cost update the moment the player becomes a ghost instead of - -- waiting until they respawn / a later event. - local isGhostNow = (UnitIsGhost and UnitIsGhost("player")) and true or false; - if RD_GhostStateLast == nil then - RD_GhostStateLast = isGhostNow; - elseif isGhostNow ~= RD_GhostStateLast then - RD_GhostStateLast = isGhostNow; - if isGhostNow then - -- Player just released and became a ghost. The 10% death - -- durability penalty is normally applied on PLAYER_DEAD, but - -- guard against a missed application (e.g. PLAYER_DEAD arrived - -- before saved-durability baselines existed). - if not RD_DeathPenaltyApplied then - RelationshipsDashboard_ApplyDeathPenalty(); - RD_DeathPenaltyApplied = true; - end - RD_WasGhost = true; - else - RD_DeathPenaltyApplied = false; - end - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - end - end); - - - -- Ensure the scroll-viewport masks always draw above the stat row sub-frames - -- inside the ScrollChild. Vanilla ScrollFrame does NOT clip nested child - -- frames, so without these masks the row FontStrings render outside the - -- scroll area when scrolled. See RelationshipsDashboard.xml for details. - RelationshipsDashboard_BoostMaskLevels(); - - -- Hook into CharacterFrame - RelationshipsDashboard_HookCharacterFrame(); RelationshipsDashboard_TryHookInspectFrame(); - - -- Initialize saved variables - if not RelationshipsDashboardDB then - RelationshipsDashboardDB = {}; - end - - -- Initial update (will run when CharacterFrame opens) - -- The frame starts hidden, so we don't update immediately -end - -------------------------------------------------------------------------------- --- OnEvent - Handle registered events -------------------------------------------------------------------------------- -function RelationshipsDashboard_OnEvent(event) - if event == "ADDON_LOADED" then - if arg1 == "Blizzard_InspectUI" then - RelationshipsDashboard_TryHookInspectFrame(); - elseif arg1 == "RelationshipsDashboard" then - -- Bind the saved durability table so it persists across /reload - if RelationshipsDashboardDB_SavedDurability == nil then - RelationshipsDashboardDB_SavedDurability = {}; - end - RD_SavedDurability = RelationshipsDashboardDB_SavedDurability; - -- Restore the learned repair calibration factor. - if RelationshipsDashboardDB and RelationshipsDashboardDB.repairCalibration - and RelationshipsDashboardDB.repairCalibrationVersion == RD_REPAIR_CALIBRATION_VERSION then - RD_RepairCalibration = RelationshipsDashboardDB.repairCalibration; - else - RD_RepairCalibration = 1.0; - end - end - return; - elseif event == "VARIABLES_LOADED" then - if RelationshipsDashboardDB_SavedDurability == nil then - RelationshipsDashboardDB_SavedDurability = {}; - end - RD_SavedDurability = RelationshipsDashboardDB_SavedDurability; - if RelationshipsDashboardDB and RelationshipsDashboardDB.repairCalibration - and RelationshipsDashboardDB.repairCalibrationVersion == RD_REPAIR_CALIBRATION_VERSION then - RD_RepairCalibration = RelationshipsDashboardDB.repairCalibration; - else - RD_RepairCalibration = 1.0; - end - return; - elseif event == "PLAYER_LOGOUT" then - -- Guarantee the saved-durability table (and calibration) survive - -- /reload and full logout, even if some other code path reassigned - -- RD_SavedDurability to a fresh table after ADDON_LOADED. - if RD_SavedDurability then - RelationshipsDashboardDB_SavedDurability = RD_SavedDurability; - end - if RelationshipsDashboardDB and RD_RepairCalibration then - RelationshipsDashboardDB.repairCalibration = RD_RepairCalibration; - RelationshipsDashboardDB.repairCalibrationVersion = RD_REPAIR_CALIBRATION_VERSION; - end - return; - - elseif event == "PLAYER_TARGET_CHANGED" then - -- The dashboard can stay open while the user clicks between different - -- targets in the world (or opens/closes inspect). Resync RD_UNIT against - -- reality every time target changes, so class-based row filtering always - -- matches the unit whose stats we are about to display. - if RelationshipsDashboardFrame:IsVisible() then - if InspectFrame and InspectFrame:IsVisible() - and UnitExists("target") and UnitIsPlayer("target") then - RD_UNIT = "target"; - else - RD_UNIT = "player"; - end - RelationshipsDashboard_ApplyClassVisibility(); - RelationshipsDashboard_UpdateStats(); - end - return; - end - if event == "PLAYER_ENTERING_WORLD" then - -- Delay initial setup slightly - RelationshipsDashboard_HookCharacterFrame(); RelationshipsDashboard_TryHookInspectFrame(); - -- Initialize mana tracking for druid form support - local powerType = UnitPowerType("player"); - if powerType == 0 then - RelationshipsDashboard_LastKnownMana = UnitMana("player"); - RelationshipsDashboard_LastKnownMaxMana = UnitManaMax("player"); - end - RD_LastMoney = GetMoney(); - RD_InBattleground = RelationshipsDashboard_IsInBattleground(); - if RD_InBattleground and not RD_BG_DurabilitySnapshot then - RelationshipsDashboard_SnapshotDurabilityForBG(); - end - -- Sync stored ghost state to reality on login - if UnitIsGhost and UnitIsGhost("player") then - RD_WasGhost = true; - end - elseif event == "UNIT_DISPLAYPOWER" then - -- Power type changed (e.g., druid shifted forms) - if arg1 and arg1 ~= "player" then return; end - local powerType = UnitPowerType("player"); - if powerType == 0 then - RelationshipsDashboard_LastKnownMana = UnitMana("player"); - RelationshipsDashboard_LastKnownMaxMana = UnitManaMax("player"); - end - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_ApplyClassVisibility(); - RelationshipsDashboard_UpdateStats(); - end - elseif event == "MERCHANT_SHOW" or event == "MERCHANT_UPDATE" then - RelationshipsDashboard_UpdateRepairFromMerchant(); - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - elseif event == "MERCHANT_CLOSED" then - RelationshipsDashboard_MerchantRepairCost = 0; - RD_AtRepairMerchant = false; - RD_LastMoney = GetMoney(); - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - elseif event == "PLAYER_DEAD" then - -- Refresh BG state before deciding to apply the death penalty. - RD_InBattleground = RelationshipsDashboard_IsInBattleground(); - if RD_InBattleground and not RD_BG_DurabilitySnapshot then - -- We entered a BG without seeing ZONE_CHANGED first (rare but - -- observed on OctoWoW). Snapshot now so the restore is a no-op. - RelationshipsDashboard_SnapshotDurabilityForBG(); - end - -- Player died: apply 10% durability loss (skipped inside BGs). - RelationshipsDashboard_ApplyDeathPenalty(); - if RD_InBattleground then - RelationshipsDashboard_RestoreDurabilityFromBGSnapshot(); - end - RD_DeathPenaltyApplied = true; - RD_WasGhost = true; - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - elseif event == "PLAYER_UNGHOST" or event == "PLAYER_ALIVE" then - -- Detect spirit-healer resurrection: player was a ghost, and after - -- becoming alive is NOT near their corpse (corpse-run always leaves - -- you at the corpse location — spirit rez teleports you to the - -- graveyard). Vanilla exposes HasCorpse via GetCorpseRecoveryDelay(): - -- returns > 0 only after a spirit rez. That's our signal. - local isAlive = not (UnitIsGhost and UnitIsGhost("player")) - and not (UnitIsDead and UnitIsDead("player")); - if RD_WasGhost and isAlive then - local corpseDelay = 0; - if GetCorpseRecoveryDelay then - corpseDelay = GetCorpseRecoveryDelay() or 0; - end - if corpseDelay > 0 then - RelationshipsDashboard_ApplySpiritHealerPenalty(); - end - RD_WasGhost = false; - end - -- Clear the death-penalty guard once the player is alive again so a - -- subsequent death re-applies the 10% loss. - if not (UnitIsGhost and UnitIsGhost("player")) then - RD_DeathPenaltyApplied = false; - RD_GhostStateLast = false; - end - -- Always refresh the dashboard on rez (corpse-run OR spirit-heal). - -- The 10% death penalty was already applied on PLAYER_DEAD but the - -- character panel is usually closed at that moment, so this is the - -- first chance the visible dashboard has to redraw with the loss. - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - elseif event == "PLAYER_REGEN_DISABLED" then - -- Entering combat: start tracking hits for durability estimation - RD_InCombat = true; - RD_CombatHitCount = 0; - RD_LastHealth = UnitHealth("player"); - elseif event == "PLAYER_REGEN_ENABLED" then - -- Leaving combat: stop tracking - RD_InCombat = false; - RD_CombatHitCount = 0; - elseif event == "DUEL_REQUESTED" then - -- A duel has been initiated between the player and someone else. - -- Duels never cause real durability loss, so treat the duration - -- like a battleground for our purposes. We capture equipped - -- durability snapshots so we can restore them exactly if the duel - -- somehow does mutate our tracker. - RD_InDuel = true; - elseif event == "DUEL_FINISHED" then - -- Duel is over (accepted-and-completed, declined, or cancelled). - -- Restore estimated durability to full for equipped items so any - -- combat-wear ticks that fired mid-duel are undone. - if RD_InDuel then - RelationshipsDashboard_ResetDurabilityToFull(); - end - RD_InDuel = false; - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - - elseif event == "ZONE_CHANGED_NEW_AREA" or event == "ZONE_CHANGED" then - -- Zone changed: update battleground status - local wasInBG = RD_InBattleground; - RD_InBattleground = RelationshipsDashboard_IsInBattleground(); - -- Entering a battleground: snapshot current durability so we can - -- restore it exactly on exit regardless of any events fired inside. - if RD_InBattleground and not wasInBG then - RelationshipsDashboard_SnapshotDurabilityForBG(); - end - -- When leaving a battleground, restore the pre-BG snapshot instead - -- of resetting to full (avoids over-repairing gear that was already - -- damaged before queueing). - if wasInBG and not RD_InBattleground then - if RD_BG_DurabilitySnapshot then - RelationshipsDashboard_RestoreDurabilityFromBGSnapshot(); - RD_BG_DurabilitySnapshot = nil; - else - RelationshipsDashboard_ResetDurabilityToFull(); - end - end - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - elseif event == "PLAYER_MONEY" then - -- Gold changed. If a repair merchant is open this is very likely a - -- repair (or a purchase). Check whether money actually decreased and - -- whether the drop is consistent with the last-known repair-all cost. - -- If so, we KNOW a repair just happened -> force a full reset even if - -- GetRepairAllCost hasn't yet updated to 0 (some server builds lag by - -- a frame). - local newMoney = GetMoney(); - local spent = 0; - if RD_LastMoney and newMoney and newMoney < RD_LastMoney then - spent = RD_LastMoney - newMoney; - end - if RD_AtRepairMerchant then - -- Detect "just repaired": money dropped by ~90%+ of the previously - -- reported repair-all cost. Covers Repair All AND individual - -- right-click repairs (which drop by a slot's share). - if spent > 0 and RD_PrevMerchantRepairCost > 0 - and spent >= math.floor(RD_PrevMerchantRepairCost * 0.9) - and spent <= RD_PrevMerchantRepairCost + 100 then - RelationshipsDashboard_MerchantRepairCost = 0; - RD_PrevMerchantRepairCost = 0; - RelationshipsDashboard_ResetDurabilityToFull(); - else - -- Otherwise just re-query cost normally; the function will - -- itself trigger a reset if cost dropped to 0. - RelationshipsDashboard_UpdateRepairFromMerchant(); - end - end - RD_LastMoney = newMoney; - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - else - -- All other stat-related events (UNIT_MANA, UNIT_HEALTH, etc.) - -- arg1 is the unitID for UNIT_* events — only update for "player" - if arg1 and arg1 ~= "player" then - return; - end - -- Track damage taken in combat for durability estimation - if event == "UNIT_HEALTH" and RD_InCombat then - local currentHealth = UnitHealth("player"); - if RD_LastHealth > 0 and currentHealth < RD_LastHealth then - local damageTaken = RD_LastHealth - currentHealth; - -- Apply combat wear (the function internally tracks hit count) - RelationshipsDashboard_ApplyCombatWear(damageTaken); - end - RD_LastHealth = currentHealth; - end - if RelationshipsDashboardFrame:IsVisible() then - RelationshipsDashboard_UpdateStats(); - end - end -end