diff --git a/CHANGELOG.md b/CHANGELOG.md index 96080cb..8834bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,37 @@ Changes in this fork (OEVG/ShaguTweaks-ClassicAPI), newest first. Each entry is This fork is based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at commit `4391564` (2026-09-08). Earlier changes are in that repository's history. +## 3.8 — 2026-11-04 — Player frames, movable bars and UI polish + +### Added + +- **Golden UI:** skip the opaque NPC target name strip when Big Health for Target is enabled, leaving its enlarged health fill unobstructed. +- **Standalone Player Frame:** vertically center health percentage and real-health values in the enlarged player bar, retaining left/right alignment; grouped frame offsets remain unchanged. +- **Loss of Control Alert** now uses a classic tooltip panel and icon rim, native fonts and a textured timer bar. Panel/icon borders follow the active default, Darkened or Golden UI palette while the timer fill retains effect-type colors. +- **Nameplate Castbar, Nameplate Scale and Nameplate Class Colors** removed to avoid conflicts with dedicated nameplate addons. The [migration guide](Docs/EXTRAS_MERGE.md#nameplate-alternatives) links BlizzNameplatesPlus (Blizzard-like), UnitPlates (more detail), and ShaguPlates (extensive customization). +- **Debuff Timer** removed to avoid overlapping target-aura timers. Use [TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff) with ClassicAPI; see [the Extras migration guide](Docs/EXTRAS_MERGE.md#removed-later). +- **Big Health** is split into **Unit Frame Big Health for Player** and **Unit Frame Big Health for Target**. Existing shared settings migrate to both unless a new choice already exists. Standalone player mana sizing follows the player option. +- **Cast-bar movement** is always enabled, independent of Reduced Actionbar Size and old saved toggle values. Removed its checkbox; existing positions and Ctrl+Shift+Alt drag/right-click reset controls are retained. +- **Reduced Actionbar Size** now includes movement automatically, with no separate checkbox. Hold Ctrl+Shift+Alt to drag labeled markers for the main, extra, pet, stance, experience and reputation bars; right-click a marker to reset it. Positions persist per character. Nearby drops snap to neighbouring bars using their original layout spacing and insets (12 UI-unit tolerance); hidden bars are not snap targets. XP at level cap, reputation without a watched faction or visible status bar, and transparent frames are excluded even if their containers remain shown. Attached movable bars stay in place when their owner is moved, and native relayout cannot erase saved anchors. +- **Reduced Actionbar Size** includes XP and reputation movement, including hidden-bar markers. Removed unnecessary native saved-position calls that caused movement errors on drop/reset. Detached progress bars no longer become anchor points for Reduced Actionbar's automatic action stack. Main-bar snapping to other action bars now aligns visible button edges using native button spacing (6 units when unavailable), instead of preserving unused artwork space. The main and pet bar drag outlines use the actual button edges, preserving the offset when dropped; other bars retain native-frame outline sizing. Bar movement uses an empty outline and moves the actual bar once when dropped; native modifier events are preferred, with a throttled fallback for older clients. +- **Hide Player Name** (Unit Frames, off): hide the detached name piece while Standalone Player Frame is enabled. Does not hide the name on the ordinary grouped player frame. +- **Hide Player Portrait** (Unit Frames, off): hide the portrait image while the player name, health and power remain visible. Works with class-icon portraits and the standalone layout; it leaves the portrait's surrounding artwork intact. +- **Standalone Player Frame** (Unit Frames, off): detach the native Blizzard portrait, name, health and power into four independently movable pieces. Hold Ctrl+Shift+Alt to drag; right-click with those keys held to reset that piece. Positions are saved per character. Big Health also enlarges the power bar to match health. Preserves native bar objects, event updates, child number/tick overlays and normal unit clicks; splits the existing artwork around the pieces. The ordinary grouped player-frame mover is bypassed while this option is enabled. + +### Fixed + +- **Quest/gossip parchment:** Golden and Darkened UI retain cropped original parchment and torn edges instead of the squared stationery overlay; the surrounding frame keeps its selected theme. Restored parchment draws above native tinted tiles and material textures so texture batching cannot leave orange/dark sections. +- **Golden UI unit names:** use an opaque native window-title strip for NPC target names only, matching the brown map/quest window headers. Player targets and the player frame retain native/class-colored backgrounds; switching targets restores the appropriate header. +- **TrinketMenu styling:** exclude its windows, descendants and minimap border from Golden UI and Darkened UI recoloring. TrinketMenu now owns its styling in every UI mode. +- **Standalone portrait shading:** rebuild both lower ring quadrants from matching clean artwork and restore the level badge through a feature-detected round mask. This removes the dark shadow patch beside the badge. Older clients without texture masking retain the native left artwork. +- **Standalone portrait/name:** retain a single level badge on the left, with the resting/combat icon covering the level number while active. Give the detached name its own class-colored background using the health texture, so Big Health's hidden stock name background no longer leaves it blank. +- **Standalone artwork:** replace shared-frame crops on the name, health and power pieces with separate rounded Blizzard border outlines. Complete the portrait ring by mirroring its clean half, removing the attached bar artwork. Feature-detect ClassicAPI's round portrait mask for the status glow. +- **Modern Class Colors:** use the modern health texture on player and target class-colored name backgrounds as well as health bars, including when Big Health is disabled. + +### Validation + +- Thirteen Lua 5.0 suites cover standalone frame layout and text, independent Big Health settings and migration, bar movement/snapping, always-on cast-bar movement, Loss of Control styling, parchment/name headers, options search, rarity updates and palette compatibility. Mock checks cannot validate client rendering, native event order or FPS; visual changes were iterated with in-game feedback. + ## 3.7.4 — 2026-11-03 — Advanced Options search ### Added diff --git a/Docs/CLASSICAPI_UPDATES.md b/Docs/CLASSICAPI_UPDATES.md index c0b17e6..a4db32f 100644 --- a/Docs/CLASSICAPI_UPDATES.md +++ b/Docs/CLASSICAPI_UPDATES.md @@ -1,5 +1,9 @@ # ClassicAPI integration updates +The Debuff Timer notes below describe older releases. In 3.8 that module was +removed in favor of [TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff) +with ClassicAPI; see [Removed later](EXTRAS_MERGE.md#removed-later). + These changes use newer ClassicAPI features while retaining older fallback paths. ClassicAPI v1.18.13 was the latest release reviewed on 2026-10-31 (the previous review covered v1.17.6 on 2026-10-24). Update both ClassicAPI's DLL and its diff --git a/Docs/EXTRAS_MERGE.md b/Docs/EXTRAS_MERGE.md index 2a0d21e..dddcc71 100644 --- a/Docs/EXTRAS_MERGE.md +++ b/Docs/EXTRAS_MERGE.md @@ -51,3 +51,13 @@ The textures these modules use (the gryphons and the XP bar) moved to this addon | Module | Version | Reason | |---|---|---| | **Key-Down Casting** and **Alt Self-Cast** | 3.7.1 | ClassicAPI now has the game's own `ActionButtonUseKeyDown` setting. Type `/console ActionButtonUseKeyDown 1` once instead. Details in the [changelog](../CHANGELOG.md). | +| **Debuff Timer** | 3.8 | [TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff) already provides target buff/debuff countdowns with ClassicAPI. Use its timer settings instead of running overlapping timers in ShaguTweaks. Without ClassicAPI, TargetFrameBuff expands the aura icons but does not add timers. | +| **Nameplate Castbar**, **Nameplate Scale** and **Nameplate Class Colors** | 3.8 | Enabling these together with another nameplate addon can cause conflicts because both addons modify the same nameplates. Use one dedicated nameplate addon instead; alternatives are listed below. | + +### Nameplate alternatives + +Choose one to handle your nameplates: + +- [BlizzNameplatesPlus](https://github.com/Wurmschwanz/BlizzNameplatesPlus) for a similar Blizzard-like appearance. +- [UnitPlates](https://github.com/Redbu11dev/UnitPlates) for more detailed nameplates (requires SuperWoW). +- [ShaguPlates](https://github.com/Dinkleberrrg/ShaguPlates) for extensive customization. diff --git a/README.md b/README.md index 49c145d..1ad2855 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is an English-only customization of [Dusk/ShaguTweaks-ClassicAPI](https://o > This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**. -**Version 3.7.4.** What changed in each update: see the [changelog](CHANGELOG.md). +**Version 3.8.** What changed in each update: see the [changelog](CHANGELOG.md). > **Upgrading to 3.7:** the modules of the separate *ShaguTweaks Extras ClassicAPI* addon are now built into ShaguTweaks. **Disable or delete `ShaguTweaks-extras`** after updating, otherwise its older copies replace the built-in ones. Your on/off settings carry over. Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy were not merged. See [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md) for where each module went and why some were left out. @@ -31,26 +31,35 @@ ClassicAPI is the main API layer. Settings: **Esc → Advanced Options**. +**Reduced Actionbar Size** automatically enables movement for all bars; there is no separate movement checkbox. Hold **Ctrl+Shift+Alt** to show labeled markers for the main, extra, pet, stance, experience and reputation bars. Drag a marker to move its bar; right-click it with those keys held to reset it. Dropping within 12 UI units of a neighbouring bar's original relative position snaps into alignment, preserving default spacing and insets. Main-bar snapping to action bars instead uses visible button edges and native button spacing, excluding unused artwork space. Positions are saved per character, and attached movable bars are pinned in place when moving their owner. Ordinary action-button clicks and drags remain unchanged when locked. + +XP and reputation movement is included with **Reduced Actionbar Size**. The progress bars have move markers even when the native bars are hidden. Moving a progress bar does not make Reduced Actionbar stack its action bars at that detached location. Movement uses an empty drag outline and applies the real position once on release; current ClassicAPI uses modifier events without idle polling. + Use the **Search** field to filter by option name, description, category or slider label. Search ignores case and includes options inside collapsed categories. Click **X** to clear it; checkbox and slider changes are retained while filtering. ## ✨ What's different in this fork **Unit frames** +- **Hide Player Portrait** (off): hides only the player's portrait image, including a class-icon portrait, without hiding the name, health or power bar. The surrounding portrait artwork remains. +- **Hide Player Name** (off): hides the detached name piece only while **Standalone Player Frame** is enabled. It has no effect on the ordinary grouped player frame. +- **Standalone Player Frame** (off): detaches the existing Blizzard portrait, name, health and power into four independently movable pieces. Hold **Ctrl+Shift+Alt** and drag a piece; right-click with those keys held to reset only that piece. Positions are stored per character. Existing bars, updates, numeric overlays, textures and click actions are retained. With **Unit Frame Big Health for Player**, the power bar matches the enlarged health bar's width and height. This option handles its own movement even when Movable Unit Frames is off; the ordinary player-frame mover yields to it. Disabling the option and reloading restores the grouped frame while keeping detached positions for later use. + +The standalone bars use clean rounded Blizzard border outlines, following the active UI palette. The name has a separate class-colored fill using the health texture, including with Big Health enabled. The portrait keeps one level badge on the left; the resting/combat icon covers its level number while active. ClassicAPI's portrait mask is used for the status glow when available; it is feature-detected. - **Modern Class Colors:** optional palette for all nine Vanilla classes, including blue shamans. Listed directly below **Unit Frame Class Colors**. - **Modern textures:** class-tinted health artwork and precolored rage, energy and focus artwork. Mana uses the light-blue rectangular retail fill on every supported frame, including the druid's separate mana bar in bear/cat forms. - **Real Health Numbers:** an independent module. Percentage on the left and current health on the right, inset 4 pixels; power numbers remain centered. It uses direct values when available and a lazy Vanilla health estimator for percentage-only targets, so unknown absolute target health stays percentage-only. - **Number styling:** health, percentage and power numbers copy the corresponding character name's font, size and shadow. Gold is the default; **White Unit Frame Numbers** under Unit Frames switches the numbers to white independently of Modern Class Colors. -- **Unit Frame Big Health** is presentation-only and no longer forces numeric health/power text. With it, the resting pulse is limited to the portrait, and the full status artwork is restored outside resting or in combat. +- **Unit Frame Big Health for Player** and **Unit Frame Big Health for Target** independently enlarge their respective health bars. Both are presentation-only and no longer forces numeric health/power text. With it, the resting pulse is limited to the portrait, and the full status artwork is restored outside resting or in combat. - **Movable Unit Frames:** Player, Target, Party, Minimap, Buffs, Debuffs and Weapon Buffs share one mover and alignment grid. Hold **Ctrl+Shift+Alt** to drag; right-click while unlocked to reset. - **Hide Pet Unit Frame:** hides the pet frame below the player frame for pet classes. The stock pet code keeps running, so nothing breaks when the pet is summoned, dismissed or dies. **Loss of Control Alert and Totem Timers** (both off by default; they need ClassicAPI) -- **Loss of Control Alert** (Interface): a large icon, the name of the effect and a countdown above the middle of the screen when you are stunned, feared, confused, charmed, silenced, pacified, disarmed, rooted or locked out of a spell school after an interrupt. If several apply, the most disabling one is shown. Hold Ctrl+Shift+Alt to show a preview and drag it, right-click to reset (like the Swing Timer). Uses ClassicAPI's `C_LossOfControl`. +- **Loss of Control Alert** (Interface): a large icon, the name of the effect and a countdown above the middle of the screen when you are stunned, feared, confused, charmed, silenced, pacified, disarmed, rooted or locked out of a spell school after an interrupt. If several apply, the most disabling one is shown. Hold Ctrl+Shift+Alt to show a preview and drag it, right-click to reset (like the Swing Timer). Uses ClassicAPI's `C_LossOfControl`. A classic tooltip panel and icon rim follow the default, Darkened or Golden UI palette; the timer fill retains its effect-type color. - **Totem Timers** (Interface, Shaman only): one icon per totem you have out (Fire, Earth, Water, Air) with the time left, turning yellow at 10 seconds and red at 5. Left-click an icon to target the totem; hover for its tooltip. Hold Ctrl+Shift+Alt to show four preview icons and drag them, right-click to reset. Uses ClassicAPI's `GetTotemInfo` and `PLAYER_TOTEM_UPDATE`. -**Movable Cast Bar** (Action Bar, on): hold Ctrl+Shift+Alt, drag the marker that appears over the cast bar, right-click to reset. When you drag it, the swing bars stay where they are. The position is saved per character and survives other addons re-anchoring the bar; the Swing Timer, Wand Timer and Improved Castbar icon follow it. **Enemy Castbars** (the target's cast bar) works the same way: its marker appears even without a target, and the saved spot is an offset from the target frame, so the bar follows the frame. +**Cast-bar movement is always available** (no checkbox): hold Ctrl+Shift+Alt, drag the marker that appears over the cast bar, right-click to reset. When you drag it, the swing bars stay where they are. The position is saved per character and survives other addons re-anchoring the bar; the Swing Timer, Wand Timer and Improved Castbar icon follow it. **Enemy Castbars** (the target's cast bar) works the same way: its marker appears even without a target, and the saved spot is an offset from the target frame, so the bar follows the frame. **Swing Timer and Wand Timer** @@ -80,7 +89,7 @@ Use the **Search** field to filter by option name, description, category or slid - Reduced per-frame work, tooltip scans and repeated UI updates. - **Bounded player cache:** the shared player cache expires old entries and holds at most 10,000 players, so the saved variables file no longer grows without limit. See [Docs/PERFORMANCE.md](Docs/PERFORMANCE.md). -- Event-driven target and nameplate castbars where possible. +- Event-driven target castbars where possible. - Safer GUID-based handling for same-name enemies. - Safer hooks and fewer destructive global overrides. - **English-only loading:** only English data and translations are included, avoiding unused locale allocations. @@ -104,8 +113,6 @@ Modules that use ClassicAPI directly: - Loot History - Movable Unit Frames - Real Health Numbers -- Nameplate Class Colors -- Nameplate Castbar - No Toggle Auto-Attack - Sell Junk - Skip Gossip Text @@ -137,7 +144,7 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Cooldown Numbers - Hide Gryphons *(off)* - Improved Castbar *(off)* -- Movable Cast Bar (hold Ctrl+Shift+Alt to drag it, right-click to reset) +- Cast-bar movement is built in: hold Ctrl+Shift+Alt to drag it, right-click to reset (no checkbox) - Swing Timer *(off)* - Wand Timer *(off)* - No Toggle Auto-Attack @@ -154,7 +161,8 @@ Names match the in-game options list. Modules marked *(off)* are disabled until ### Unit Frames -- Unit Frame Big Health *(off)* +- Unit Frame Big Health for Player *(off)* +- Unit Frame Big Health for Target *(off)* - Unit Frame Class Colors *(off)* - Modern Class Colors *(off)* - Unit Frame Class Portraits *(off)* @@ -165,13 +173,11 @@ Names match the in-game options list. Modules marked *(off)* are disabled until - Real Health Numbers *(off)* - White Unit Frame Numbers *(off)* - Enemy Castbars -- Debuff Timer -### Nameplates +### Nameplate addons + +Nameplate Scale, Class Colors and Castbar were removed in 3.8 because enabling them alongside another nameplate addon can cause conflicts. See [the migration guide](Docs/EXTRAS_MERGE.md#nameplate-alternatives) for BlizzNameplatesPlus, UnitPlates and ShaguPlates alternatives. -- Nameplate Scale -- Nameplate Class Colors -- Nameplate Castbar ### Tooltip & Items @@ -233,7 +239,11 @@ Names match the in-game options list. Modules marked *(off)* are disabled until ### Interface - Darkened UI *(off)* -- Golden UI *(off)*: gold borders, tooltips and dialogs like the WoW Forever client. Darkened UI and Golden UI also recolor the borders of ShaguDPS, Bagshui, Outfitter, SoulstoneTracker, TrinketMenu, OctoBus, RollFor and VCB, and of addon minimap buttons; Golden UI retains the default text colors. +- Golden UI *(off)*: gold borders, tooltips and dialogs like the WoW Forever client. Darkened UI and Golden UI also recolor the borders of ShaguDPS, Bagshui, Outfitter, SoulstoneTracker, OctoBus, RollFor and VCB, and of addon minimap buttons; Golden UI retains the default text colors. TrinketMenu keeps its own styling in every UI mode. + +Golden and Darkened UI keep the original NPC quest/gossip parchment, including its torn edges, while styling the outer frame. No stationery overlay is used on those panels. + +When **Unit Frame Big Health for Target** is disabled, Golden UI's NPC target name header uses an opaque native window-title texture matching the map/quest titles. Player targets and your own player frame retain their native/class-colored backgrounds. The header updates when switching targets. - Hide Errors *(off)* - Smaller Error Frame *(off)* - Loss of Control Alert *(off)* diff --git a/ShaguTweaks.toc b/ShaguTweaks.toc index 7072ddc..7b1ffe9 100644 --- a/ShaguTweaks.toc +++ b/ShaguTweaks.toc @@ -2,7 +2,7 @@ ## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI] ## Author: Shagu ## Notes: A collection of tweaks and enhancements to the Default UI -## Version: 3.7.4 +## Version: 3.8 ## Dependencies: !!!ClassicAPI ## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache ## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache @@ -45,17 +45,13 @@ mods\minimap-tracking.lua mods\actionbar-range-color.lua mods\free-slots.lua mods\xpbar-text.lua -mods\nameplate-scale.lua mods\sell-junk.lua mods\unitframes-classcolor.lua mods\vendor-values.lua mods\worldmap-coordinates.lua mods\worldmap-window.lua -mods\nameplate-classcolor.lua -mods\nameplate-castbar.lua mods\move-unitframes.lua mods\target-castbar.lua -mods\target-debufftimer.lua mods\social-colors.lua mods\item-colors.lua mods\auto-dismount.lua @@ -70,12 +66,16 @@ mods\worldmap-colors.lua mods\unitframes-bighealth.lua mods\health-numbers.lua mods\unitframes-classportrait.lua +mods\unitframes-hide-player-portrait.lua +mods\unitframes-hide-player-name.lua +mods\unitframes-standalone-player.lua mods\unitframes-energy-tick.lua mods\unitframes-hide-pet.lua mods\cooldown-numbers.lua mods\turtle-wow.lua mods\superwow.lua mods\reduced-actionbar.lua +mods\actionbar-move.lua mods\no-toggle.lua mods\aoe-loot.lua mods\loot-cursor.lua diff --git a/compat-dragonflightui.lua b/compat-dragonflightui.lua index fc1b618..026bbf4 100644 --- a/compat-dragonflightui.lua +++ b/compat-dragonflightui.lua @@ -102,7 +102,8 @@ Block("Movable Unit Frames") -- health/power text. Keep the real ShaguTweaks modules disabled there even -- though Real Health Numbers is once again a genuine core module. Block("Real Health Numbers") -Block("Unit Frame Big Health") +Block("Unit Frame Big Health for Player") +Block("Unit Frame Big Health for Target") Block("Reduced Actionbar Size") Block("Unit Frame Class Colors") Block("Unit Frame Class Portraits") diff --git a/locales/translations_enUS.lua b/locales/translations_enUS.lua index 678a842..a9a15ea 100644 --- a/locales/translations_enUS.lua +++ b/locales/translations_enUS.lua @@ -25,7 +25,6 @@ ShaguTweaks_translation["enUS"] = { ["Darkened UI"] = nil, ["Golden UI"] = nil, ["Turns the borders of the interface, tooltips and dialogs gold, like the WoW Forever client. Darkened UI takes priority when both are enabled."] = nil, - ["Debuff Timer"] = nil, ["Display health, class color, guild name, guild rank and current target on unit tooltips."] = nil, ["Display the remaining duration as text on every cooldown."] = nil, ["Draw the mini map in a squared shape instead of a round one."] = nil, @@ -49,9 +48,6 @@ ShaguTweaks_translation["enUS"] = { ["MiniMap Tweaks"] = nil, ["Movable Unit Frames"] = nil, ["N/A"] = nil, - ["Nameplate Castbar"] = nil, - ["Nameplate Class Colors"] = nil, - ["Nameplate Scale"] = nil, ["Nameplates"] = nil, ["Player"] = nil, ["Player and Target unit frames can be moved while and are pressed together."] = nil, diff --git a/main.lua b/main.lua index 57ed611..fb6d171 100644 --- a/main.lua +++ b/main.lua @@ -63,6 +63,16 @@ ShaguTweaks:SetScript("OnEvent", function() -- Gold numbers belong to Real Health Numbers; retire the duplicate toggle. ShaguTweaks_config["Unit Frame Health Colors"] = nil + -- Split the former shared setting without overriding either new choice. + local oldBigHealth = ShaguTweaks_config[ShaguTweaks.T["Unit Frame Big Health"]] + if oldBigHealth ~= nil then + local playerKey = ShaguTweaks.T["Unit Frame Big Health for Player"] + local targetKey = ShaguTweaks.T["Unit Frame Big Health for Target"] + if ShaguTweaks_config[playerKey] == nil then ShaguTweaks_config[playerKey] = oldBigHealth end + if ShaguTweaks_config[targetKey] == nil then ShaguTweaks_config[targetKey] = oldBigHealth end + ShaguTweaks_config[ShaguTweaks.T["Unit Frame Big Health"]] = nil + end + -- initialize overwrites ShaguTweaks_config.overwrites = ShaguTweaks_config.overwrites or {} @@ -94,6 +104,11 @@ ShaguTweaks:SetScript("OnEvent", function() end end + -- Cast-bar movement is always available, independent of saved option flags. + if expansion == "vanilla" and ShaguTweaks.EnableCastBarMover then + ShaguTweaks.EnableCastBarMover() + end + -- Libraries that can stay dormant until a module actually consumes them -- get one final activation pass after every enabled module has registered -- its callbacks. diff --git a/mods/actionbar-move.lua b/mods/actionbar-move.lua new file mode 100644 index 0000000..f816428 --- /dev/null +++ b/mods/actionbar-move.lua @@ -0,0 +1,413 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API +local hooksecurefunc = ShaguTweaks.hooksecurefunc +local S = { states = {} } + +local function Unlocked() + return API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown() +end + +function S.IsMoved(name) + return S.positions and S.positions[name] ~= nil +end + +local function Apply(state) + local position = S.positions[state.name] + if not position then return end + state.clear(state.frame) + state.point(state.frame, "TOPLEFT", UIParent, "BOTTOMLEFT", position[1], position[2]) +end + +local function Layout() + if _G.UIParent_ManageFramePositions then _G.UIParent_ManageFramePositions() end + if _G.ReputationWatchBar_Update then _G.ReputationWatchBar_Update() end +end + +-- Detect anchor chains as well as parent chains. Pin attached, movable bars +-- before moving their owner so moving one does not drag the others with it. +local function DependsOn(frame, owner, depth) + if not frame or frame == UIParent or depth > 12 then return false end + if frame == owner then return true end + if frame.GetParent and DependsOn(frame:GetParent(), owner, depth + 1) then return true end + if frame.GetPoint then + local count = frame.GetNumPoints and frame:GetNumPoints() or 1 + for i = 1, count do + local point, relative = frame:GetPoint(i) + if type(relative) == "string" then relative = _G[relative] end + if point and relative and DependsOn(relative, owner, depth + 1) then return true end + end + end + return false +end + +local function PinDependents(owner) + for _, state in ipairs(S.states) do + if state ~= owner and not S.positions[state.name] and DependsOn(state.frame, owner.frame, 0) then + local x, y = state.frame:GetLeft(), state.frame:GetTop() + if x and y then S.positions[state.name] = { x, y }; Apply(state) end + end + end +end + +-- Capture native neighbour offsets before applying any saved positions. +-- Keeping the entire offset preserves intentional insets as well as gaps +-- (the pet/stance buttons do not start at the main frame's left edge). +local function CaptureSnapOffsets() + for _, state in ipairs(S.states) do + state.snapOffsets = {} + local frame = state.frame + local scale = frame:GetEffectiveScale() / UIParent:GetEffectiveScale() + local x, y = frame:GetLeft(), frame:GetTop() + if x and y then + x, y = x * scale, y * scale + for _, other in ipairs(S.states) do + if other ~= state then + local target = other.frame + local targetScale = target:GetEffectiveScale() / UIParent:GetEffectiveScale() + local tx, ty = target:GetLeft(), target:GetTop() + if tx and ty then + local dx, dy = x - tx * targetScale, y - ty * targetScale + local width = math.max(frame:GetWidth() * scale, target:GetWidth() * targetScale) + local height = math.max(frame:GetHeight() * scale, target:GetHeight() * targetScale) + if math.abs(dx) <= width + 20 and math.abs(dy) <= height + 20 then + table.insert(state.snapOffsets, { target = other, x = dx, y = dy }) + end + end + end + end + end + end +end + +local function SnapTargetVisible(state) + local frame = state.frame + if not frame:IsVisible() then return false end + local parent = frame + while parent do + if parent.GetAlpha and parent:GetAlpha() == 0 then return false end + parent = parent:GetParent() + end + if state.name == "MainMenuExpBar" then + -- The container can remain shown at the level cap without an XP bar. + if _G.UnitLevel and UnitLevel("player") >= (_G.MAX_PLAYER_LEVEL or 60) then return false end + elseif state.name == "ReputationWatchBar" then + if not _G.GetWatchedFactionInfo or not GetWatchedFactionInfo() then return false end + local status = _G.ReputationWatchStatusBar + if not status or not status:IsVisible() or (status.GetAlpha and status:GetAlpha() == 0) then return false end + end + return true +end + +local function ButtonBounds(state) + local first, last = _G[state.first or ""], _G[state.last or ""] + if state.name == "MainMenuBar" and _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then + first, last = _G.BonusActionButton1, _G.BonusActionButton12 + end + if not first or not last then return end + local l, t, r, b = first:GetLeft(), first:GetTop(), last:GetRight(), last:GetBottom() + if not l or not t or not r or not b then return end + local a = first:GetEffectiveScale() / UIParent:GetEffectiveScale() + local z = last:GetEffectiveScale() / UIParent:GetEffectiveScale() + return l * a, t * a, r * z, b * z +end + +local function MainActionPair(a, b) + return not a.progress and not b.progress + and (a.name == "MainMenuBar" or b.name == "MainMenuBar") +end + +function S.Snap(state, x, y) + local scale = state.frame:GetEffectiveScale() / UIParent:GetEffectiveScale() + local px, py = x * scale, y * scale + local best, resultX, resultY = 289, x, y + for _, offset in ipairs(state.snapOffsets) do + local target = offset.target.frame + if not MainActionPair(state, offset.target) and SnapTargetVisible(offset.target) then + local targetScale = target:GetEffectiveScale() / UIParent:GetEffectiveScale() + local tx, ty = target:GetLeft(), target:GetTop() + if tx and ty then + tx, ty = tx * targetScale + offset.x, ty * targetScale + offset.y + local dx, dy = tx - px, ty - py + local distance = dx * dx + dy * dy + if math.abs(dx) <= 12 and math.abs(dy) <= 12 and distance < best then + best, resultX, resultY = distance, tx / scale, ty / scale + end + end + end + end + -- MainMenuBar's native rectangle contains unused artwork. Its old frame + -- offsets must never determine the visible gap between action-button rows. + local l, t, r, b = ButtonBounds(state) + if l then + local ox = l - state.frame:GetLeft() * scale + local oy = t - state.frame:GetTop() * scale + local height = t - b + local gap = 6 * scale + if _G.ActionButton1 and _G.ActionButton2 then + local right, left = ActionButton1:GetRight(), ActionButton2:GetLeft() + if right and left then + local measured = left * ActionButton2:GetEffectiveScale() / UIParent:GetEffectiveScale() + - right * ActionButton1:GetEffectiveScale() / UIParent:GetEffectiveScale() + if measured >= 0 and measured <= 20 then gap = measured end + end + end + for _, other in ipairs(S.states) do + if other ~= state and MainActionPair(state, other) and SnapTargetVisible(other) then + local tl, tt, tr, tb = ButtonBounds(other) + if tl then + for side = 1, 2 do + local tx = tl - ox + local ty = (side == 1 and tb - gap or tt + gap + height) - oy + local dx, dy = tx - px, ty - py + local distance = dx * dx + dy * dy + if math.abs(dx) <= 12 and math.abs(dy) <= 12 and distance < best then + best, resultX, resultY = distance, tx / scale, ty / scale + end + end + end + end + end + end + return resultX, resultY +end + +local function Finish(save) + local state = S.dragging + if not state then return end + S.dragging = nil + S.outline:StopMovingOrSizing() + local x, y = S.outline:GetLeft(), S.outline:GetTop() + S.outline:Hide() + if save and x and y then + x, y = S.Snap(state, x - state.dragOffsetX, y - state.dragOffsetY) + PinDependents(state) + S.positions[state.name] = { x, y } + Apply(state) + -- Positions are stored in addon settings; native bars need not be movable. + Layout() + end +end + +local function Reset(state) + Finish(false) + S.positions[state.name] = nil + state.clear(state.frame) + for _, point in ipairs(state.defaults) do state.point(state.frame, unpack(point)) end + + Layout() +end + +local function Begin() + if not Unlocked() then return end + Finish(true) + local state = this.state + local x, y = state.frame:GetLeft(), state.frame:GetTop() + if not x or not y then return end + local left, top = x, y + local width, height = state.frame:GetWidth(), math.max(state.frame:GetHeight(), 14) + -- Main and pet frames include artwork beyond the visible button row. + -- Read button edges directly; anchored marker dimensions are not reliable + -- on every supported client. + if state.name == "MainMenuBar" or state.name == "PetActionBarFrame" then + local first, last = _G.ActionButton1, _G.ActionButton12 + if state.name == "PetActionBarFrame" then + first, last = _G.PetActionButton1, _G.PetActionButton10 + elseif _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then + first, last = _G.BonusActionButton1, _G.BonusActionButton12 + end + if first and last then + local scale = first:GetEffectiveScale() / state.frame:GetEffectiveScale() + local lastScale = last:GetEffectiveScale() / state.frame:GetEffectiveScale() + local l, t, r, b = first:GetLeft(), first:GetTop(), last:GetRight(), last:GetBottom() + if l and t and r and b then + left, top = l * scale - 2, t * scale + 2 + width, height = r * lastScale + 2 - left, top - b * lastScale + 2 + end + end + end + state.dragOffsetX, state.dragOffsetY = left - x, top - y + S.dragging = state + local outline = S.outline + outline:SetScale(state.frame:GetEffectiveScale() / UIParent:GetEffectiveScale()) + outline:SetWidth(width) + outline:SetHeight(height) + outline:ClearAllPoints() + outline:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top) + outline:Show() + outline:StartMoving() +end + +local function MarkerClick() + if Unlocked() and arg1 == "RightButton" then Reset(this.state) end +end + +local function UpdateMarkers() + local unlocked = Unlocked() + if not unlocked then Finish(true) end + for _, state in ipairs(S.states) do + local controller = state.frame.ShaguTweaksMouseoverController + local visible = state.progress or state.frame:IsVisible() or (controller and controller.enabled) + if unlocked and visible then + -- Follow actual button bounds; the main frame also includes gryphons + -- and XP artwork outside its clickable action-button row. + local first = state.first and _G[state.first] + local last = state.last and _G[state.last] + if state.name == "MainMenuBar" and _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible() then + first, last = _G.BonusActionButton1, _G.BonusActionButton12 + elseif state.name == "ShapeshiftBarFrame" and type(_G.GetNumShapeshiftForms) == "function" then + first, last = _G.ShapeshiftButton1, _G["ShapeshiftButton" .. GetNumShapeshiftForms()] + end + state.marker:ClearAllPoints() + if first and last then + state.marker:SetPoint("TOPLEFT", first, "TOPLEFT", -2, 2) + state.marker:SetPoint("BOTTOMRIGHT", last, "BOTTOMRIGHT", 2, -2) + elseif state.progress then + local anchor = state.name == "MainMenuExpBar" and "TOPLEFT" or "BOTTOMLEFT" + state.marker:SetPoint(anchor, state.frame, anchor, 0, 0) + state.marker:SetWidth(state.frame:GetWidth()) + state.marker:SetHeight(14) + else + state.marker:SetAllPoints(state.frame) + end + state.marker:Show() + else + if S.dragging == state then Finish(true) end + state.marker:Hide() + end + end +end + +local function MarkerStyle(marker) + marker:SetFrameStrata("DIALOG") + marker:SetFrameLevel(100) + marker:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 8, + edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) + marker:SetBackdropColor(0, 0, 0, .7) + local color = ShaguTweaks.UIBorder + marker:SetBackdropBorderColor(color and color.r or 1, color and color.g or .82, color and color.b or 0, 1) +end + +local function Add(definition) + local frame = _G[definition[1]] + if not frame then return end + local state = { name = definition[1], frame = frame, defaults = {}, first = definition[3], + last = definition[4], progress = definition[5], point = frame.SetPoint, clear = frame.ClearAllPoints } + for i = 1, (frame.GetNumPoints and frame:GetNumPoints() or 1) do + local p, relative, rp, x, y = frame:GetPoint(i) + if p then table.insert(state.defaults, { p, relative or UIParent, rp or p, x or 0, y or 0 }) end + end + -- Guard BOTH operations: ignoring SetPoint alone would let a preceding + -- ClearAllPoints detach a manually positioned bar during native relayout. + frame.ClearAllPoints = function(self) + if S.positions[state.name] then return end + state.clear(self) + end + frame.SetPoint = function(self, a, b, c, d, e) + if S.positions[state.name] then return end + state.point(self, a, b, c, d, e) + end + local marker = CreateFrame("Button", "ShaguTweaksMover" .. state.name, UIParent) + MarkerStyle(marker) + marker:SetScale(frame:GetEffectiveScale() / UIParent:GetEffectiveScale()) + marker:EnableMouse(true) + marker:RegisterForDrag("LeftButton") + marker:RegisterForClicks("RightButtonUp") + marker.state = state + marker:SetScript("OnDragStart", Begin) + marker:SetScript("OnDragStop", function() Finish(true) end) + marker:SetScript("OnClick", MarkerClick) + marker:SetScript("OnHide", function() if S.dragging == this.state then Finish(true) end end) + marker.text = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + marker.text:SetPoint("CENTER", marker, "CENTER", 0, 0) + marker.text:SetText(T[definition[2]]) + marker:Hide() + state.marker = marker + table.insert(S.states, state) + ShaguTweaks.HookScript(frame, "OnShow", UpdateMarkers) + ShaguTweaks.HookScript(frame, "OnHide", UpdateMarkers) +end + +local function Setup() + if S.installed then return end + S.installed = true + ShaguTweaks_config.ActionBarPositions = ShaguTweaks_config.ActionBarPositions or {} + S.positions = ShaguTweaks_config.ActionBarPositions + ShaguTweaks.ActionBarMovers = S + S.outline = CreateFrame("Frame", "ShaguTweaksActionBarDragOutline", UIParent) + MarkerStyle(S.outline) + S.outline:SetMovable(true) + S.outline:SetClampedToScreen(true) + S.outline:SetScript("OnHide", function() if S.dragging then Finish(true) end end) + S.outline:Hide() + if S.actions then + local definitions = { + { "MainMenuBar", "Main Action Bar", "ActionButton1", "ActionButton12" }, + { "MultiBarBottomLeft", "Bottom Left Bar", "MultiBarBottomLeftButton1", "MultiBarBottomLeftButton12" }, + { "MultiBarBottomRight", "Bottom Right Bar", "MultiBarBottomRightButton1", "MultiBarBottomRightButton12" }, + { "MultiBarRight", "Right Bar", "MultiBarRightButton1", "MultiBarRightButton12" }, + { "MultiBarLeft", "Right Bar 2", "MultiBarLeftButton1", "MultiBarLeftButton12" }, + { "PetActionBarFrame", "Pet Action Bar", "PetActionButton1", "PetActionButton10" }, + { "ShapeshiftBarFrame", "Stance Bar" }, + } + for _, definition in ipairs(definitions) do Add(definition) end + end + if S.progress then + Add({ "MainMenuExpBar", "Experience Bar", nil, nil, true }) + Add({ "ReputationWatchBar", "Reputation Bar", nil, nil, true }) + end + CaptureSnapOffsets() + for _, state in ipairs(S.states) do Apply(state) end + S.events = CreateFrame("Frame") + S.events:SetScript("OnEvent", function() + if event == "PLAYER_ENTERING_WORLD" then + for _, state in ipairs(S.states) do Apply(state) end + end + UpdateMarkers() + end) + S.events:RegisterEvent("PLAYER_ENTERING_WORLD") + if API.modifierstate then + S.events:RegisterEvent("MODIFIER_STATE_CHANGED") + else + S.events.elapsed = 0 + S.events:SetScript("OnUpdate", function() + this.elapsed = this.elapsed + (arg1 or 0) + if this.elapsed < .1 then return end + this.elapsed = 0 + UpdateMarkers() + end) + end + UpdateMarkers() +end + +local function Schedule() + if S.scheduled then return end + S.scheduled = true + local ready = CreateFrame("Frame") + ready:RegisterEvent("PLAYER_ENTERING_WORLD") + ready:SetScript("OnEvent", function() + this:UnregisterAllEvents() + this:SetScript("OnUpdate", function() + this:SetScript("OnUpdate", nil) + this:Hide() + if API.timer then API.Defer(Setup) else Setup() end + end) + end) +end + +local function ReducedEnabled() + local enabled = ShaguTweaks_config[T["Reduced Actionbar Size"]] + if enabled == nil then + local reduced = ShaguTweaks.mods[T["Reduced Actionbar Size"]] + enabled = reduced and reduced.enabled and 1 or 0 + end + return enabled == 1 +end + +ShaguTweaks.EnableActionBarMovers = function() + if not ReducedEnabled() then return end + S.actions = true + S.progress = true + Schedule() +end diff --git a/mods/castbar-move.lua b/mods/castbar-move.lua index f609355..c09b6cf 100644 --- a/mods/castbar-move.lua +++ b/mods/castbar-move.lua @@ -8,14 +8,6 @@ local _G = ShaguTweaks.GetGlobalEnv() local T = ShaguTweaks.T -local module = ShaguTweaks:register({ - title = T["Movable Cast Bar"], - description = T["Hold , and to move the cast bar; right-click to reset."], - expansions = { ["vanilla"] = true }, - category = T["Action Bar"], - enabled = true, -}) - local POSITION_KEY = "CastBarPosition" -- One table for state: Lua 5.0 allows only 32 upvalues per function. @@ -118,7 +110,7 @@ local function CreateMover() S.mover = m end -module.enable = function(self) +ShaguTweaks.EnableCastBarMover = function() if not CastingBarFrame then return end if not S.mover then @@ -149,10 +141,3 @@ module.enable = function(self) S.events:RegisterEvent("MODIFIER_STATE_CHANGED") Apply() end - -module.disable = function(self) - if not S.mover then return end - SetPreview(false) - S.events:UnregisterAllEvents() - CastingBarFrame.SetPoint = nil -end diff --git a/mods/dark-ui-elements.lua b/mods/dark-ui-elements.lua index cb78f1e..19e2d38 100644 --- a/mods/dark-ui-elements.lua +++ b/mods/dark-ui-elements.lua @@ -76,8 +76,6 @@ local backgrounds = { ["^SpellBookFrame$"] = { 325, 355, 17, -74 }, ["^ItemTextFrame$"] = { 300, 355, 24, -74 }, ["^QuestLogDetailScrollFrame$"] = { QuestLogDetailScrollChildFrame:GetWidth(), QuestLogDetailScrollChildFrame:GetHeight(), 0, 0 }, - ["^QuestFrame(.+)Panel$"] = { 300, 330, 24, -82 }, - ["^GossipFrameGreetingPanel$"] = { 300, 330, 24, -82 }, } local borders = { @@ -251,9 +249,52 @@ local function TintSlotBorder(texture, button) end end +-- TrinketMenu owns its styling; its future fork must not compete with this +-- palette, including the initial recursive pass over UIParent. +local function IsExcludedFrame(frame) + local name = frame and frame.GetName and frame:GetName() + return name and string.find(name, "TrinketMenu", 1, true) == 1 +end + +local function RestoreQuestParchment(frame) + local name = frame.GetName and frame:GetName() + if not name or not (string.find(name, "^QuestFrame.*Panel$") + or name == "GossipFrameGreetingPanel") then return end + for _, source in pairs({frame:GetRegions()}) do + local path = source.GetTexture and source:GetTexture() + if type(path) == "string" and not source.stQuestPaper and not source.stOriginalQuestPaper then + local lower = string.lower(path) + local left = string.find(lower, "%-topleft$") or string.find(lower, "%-botleft$") + local right = string.find(lower, "%-topright$") or string.find(lower, "%-botright$") + local top = string.find(lower, "%-topleft$") or string.find(lower, "%-topright$") + if (left or right) and (string.find(lower, "quest", 1, true) or string.find(lower, "gossip", 1, true)) then + -- Native 256px tiles contain both the frame and torn parchment. + -- Copy only the paper area; leave the palette on the outer artwork. + local x1, x2 = left and 22 or 0, left and 256 or 67 + local y1, y2 = top and 74 or 0, top and 256 or 148 + -- Use a higher layer than both the native BACKGROUND tiles and + -- BORDER material textures. Equal-layer texture batching can draw + -- tinted native tiles over only some of our restored paper pieces. + local paper = frame:CreateTexture(nil, "ARTWORK") + paper.stOriginalQuestPaper = true + paper:SetTexture(path) + local tileWidth = left and 256 or 128 + paper:SetTexCoord(x1/tileWidth, x2/tileWidth, y1/256, y2/256) + local sx, sy = source:GetWidth()/tileWidth, source:GetHeight()/256 + paper:SetWidth((x2-x1)*sx) + paper:SetHeight((y2-y1)*sy) + paper:SetPoint("TOPLEFT", source, "TOPLEFT", x1*sx, -y1*sy) + paper:SetVertexColor(1, 1, 1, 1) + source.stQuestPaper = paper + end + end + end +end + local function DarkenFrame(frame, r, g, b, a) -- dont't do anything if neither Darkened UI nor Golden UI is enabled if not tint then return end + if IsExcludedFrame(frame) then return end -- set defaults if not r and not g and not b then @@ -293,6 +334,8 @@ local function DarkenFrame(frame, r, g, b, a) if region:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then -- monochrome buttons -- region:SetDesaturated(true) + elseif region.stOriginalQuestPaper then + -- Untinted copies of native quest/gossip parchment. elseif region.stSlotEdge then -- Border strips are maintained by their source texture. elseif IsSlotArtwork(region:GetTexture()) then @@ -314,6 +357,7 @@ local function DarkenFrame(frame, r, g, b, a) end end end + if frame and frame.GetRegions then RestoreQuestParchment(frame) end end -- register darken frame to global @@ -324,7 +368,7 @@ ShaguTweaks.DarkenFrame = DarkenFrame -- sets its border color again later, or builds a window only when opened. local ADDON_PREFIXES = { "ShaguDPS", "Bagshui", "Outfitter", "SoulstoneTracker", "SSWarlock", - "SSTClassic", "SSTracker", "TrinketMenu", "OctoBus", "RollFor", "VCB_", + "SSTClassic", "SSTracker", "OctoBus", "RollFor", "VCB_", "aux_frame", "AuxCraft", "AuxDragOutline", -- Filled mail slots get rarity borders after their item texture changes. "MailAttachment", "SendMailPackageButton", @@ -408,6 +452,7 @@ end local function LockTree(frame) if not frame then return end + if IsExcludedFrame(frame) then return end LockBorder(frame) if frame.GetRegions then local regions = { frame:GetRegions() } @@ -430,7 +475,9 @@ local function FindAddonFrames(frame) local children = { frame:GetChildren() } for i = 1, table.getn(children) do local child = children[i] - if IsAddonFrameName(child.GetName and child:GetName()) then + if IsExcludedFrame(child) then + -- Leave the whole TrinketMenu subtree to its own styling code. + elseif IsAddonFrameName(child.GetName and child:GetName()) then LockTree(child) else FindAddonFrames(child) @@ -442,6 +489,7 @@ end -- border texture). Those buttons are often made after login. local function TintMinimapButton(frame) if not frame or not frame.GetRegions then return end + if IsExcludedFrame(frame) then return end local regions = { frame:GetRegions() } for i = 1, table.getn(regions) do local region = regions[i] @@ -889,6 +937,46 @@ local function ShadeRollForLoot(frame, color) ShadeStripInFrame(frame, { 68, -12, -25, -30 }, "BORDER", color) end +local function GoldenUnitNameBar(frame) + local title = frame and frame.name + if not title or frame.stGoldenNameBar then return end + local parent = title:GetParent() + local point, relative, relativePoint, x, y = title:GetPoint(1) + if not parent or not point then return end + local bar = CreateFrame("Frame", nil, parent) + bar:SetFrameLevel(parent:GetFrameLevel() + 1) + bar:SetWidth(119) + bar:SetHeight(14) + bar:SetPoint(point, relative, relativePoint, x, y) + bar.texture = bar:CreateTexture(nil, "BACKGROUND") + -- Opaque stone from the same native title strip used by the character + -- window, excluding its metal edges and portrait. No world bleed-through. + bar.texture:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-General-TopLeft") + bar.texture:SetTexCoord(80/256, 255/256, 18/256, 32/256) + bar.texture:SetAllPoints(bar) + local color = golden.config["goldmode.bar"] + bar.texture:SetVertexColor((tonumber(tint.r) or 1) * (tonumber(color.r) or .8), + (tonumber(tint.g) or .78) * (tonumber(color.g) or .63), + (tonumber(tint.b) or .4) * (tonumber(color.b) or .62), 1) + local originalAlpha = title:GetAlpha() + bar.title = RedrawTitle(bar, title) + frame.stGoldenNameBar = bar + local function UpdateNameBar() + if UnitExists("target") and not UnitIsPlayer("target") then + title:SetAlpha(0) + bar:Show() + else + bar:Hide() + title:SetAlpha(originalAlpha) + end + end + bar:RegisterEvent("PLAYER_TARGET_CHANGED") + bar:RegisterEvent("PLAYER_ENTERING_WORLD") + bar:SetScript("OnEvent", UpdateNameBar) + ShaguTweaks.HookScript(frame, "OnShow", UpdateNameBar) + UpdateNameBar() +end + golden.enable = function(self) -- Both recolor the same textures; Darkened UI wins when both are ticked. if ShaguTweaks_config and ShaguTweaks_config[T["Darkened UI"]] == 1 then return end @@ -896,6 +984,13 @@ golden.enable = function(self) Apply(golden.config["goldmode.color"], golden.config["goldmode.border"], false) ShadeTitleBars(golden.config["goldmode.bar"]) + -- The opaque title artwork is only for NPC targets. Player names retain + -- their native/class-colored background, including our own player frame. + -- Big Health moves the name into the enlarged health fill. An opaque + -- header there would cover its upper portion, regardless of module order. + if not ShaguTweaks_config or ShaguTweaks_config[T["Unit Frame Big Health for Target"]] ~= 1 then + GoldenUnitNameBar(TargetFrame) + end RestoreMailParchment() golden.rollforLoot = function(frame) ShadeRollForLoot(frame, golden.config["goldmode.bar"]) end if _G.RollForLootFrameHeader then golden.rollforLoot(_G.RollForLootFrameHeader) end diff --git a/mods/health-numbers.lua b/mods/health-numbers.lua index a407e12..7e16ed9 100644 --- a/mods/health-numbers.lua +++ b/mods/health-numbers.lua @@ -143,6 +143,9 @@ module.enable = function(self) if bar then bar:SetStatusBarTexture(texture) end end local playerHealthY = PlayerFrameHealthBar:GetHeight() >= 20 and -7 or 0.5 + -- Grouped Big Health reserves the top for the player's name. The + -- standalone frame has a separate name holder, so center its numbers. + if ShaguTweaks_config[T["Standalone Player Frame"]] == 1 then playerHealthY = 0.5 end local targetHealthY = TargetFrameHealthBar:GetHeight() >= 20 and -7 or 0.5 AnchorHealth(self.valueTexts.playerHealth, PlayerFrameHealthBar, playerHealthY) diff --git a/mods/loss-of-control.lua b/mods/loss-of-control.lua index ac7284d..dfb448f 100644 --- a/mods/loss-of-control.lua +++ b/mods/loss-of-control.lua @@ -18,7 +18,8 @@ local module = ShaguTweaks:register({ }) local ICON = 56 -local WIDTH = 220 +local WIDTH = 300 +local BAR_WIDTH = WIDTH - ICON - 30 local THROTTLE = .05 local FALLBACK_ICON = "Interface\\Icons\\Ability_Kick" local POSITION_KEY = "LossOfControlPosition" @@ -48,6 +49,7 @@ local COLORS = { DISARM = { 1, .6, .1 }, ROOT = { 1, .6, .1 }, SCHOOL_INTERRUPT = { .3, .6, 1 }, } +local DEFAULT_COLOR = COLORS.STUN -- One table for state: Lua 5.0 allows only 32 upvalues per function. local S = { unlocked = false, elapsed = 0 } @@ -90,12 +92,22 @@ local function SchoolName(data) end end +local function ApplyTheme() + local f = S.frame + local color = ShaguTweaks.UIBorder + local r, g, b = color and color.r or .6, color and color.g or .6, color and color.b or .6 + local a = color and color.a or 1 + f:SetBackdropBorderColor(r, g, b, a) + f.border:SetBackdropBorderColor(r, g, b, a) + f.icon:SetVertexColor(1, 1, 1, 1) +end + local function Apply(data) local f = S.frame - local color = COLORS[data.locType] or { 1, .2, .2 } + local color = COLORS[data.locType] or DEFAULT_COLOR f.icon:SetTexture((data.iconTexture and data.iconTexture ~= "") and data.iconTexture or FALLBACK_ICON) - f.border:SetVertexColor(color[1], color[2], color[3]) + ApplyTheme() f.bar:SetVertexColor(color[1], color[2], color[3]) f.title:SetText(LABELS[data.locType] or data.displayText or "") @@ -117,7 +129,7 @@ local function UpdateTime() f.time:SetText(FormatTime(left)) local duration = data.duration if duration and duration > 0 then - f.bar:SetWidth(math.max(1, (WIDTH - ICON - 8) * math.min(left / duration, 1))) + f.bar:SetWidth(math.max(1, BAR_WIDTH * math.min(left / duration, 1))) f.bar:Show() else f.bar:Hide() @@ -180,9 +192,9 @@ local function SetUnlocked(on) if on then f.data = nil f.icon:SetTexture(FALLBACK_ICON) - f.border:SetVertexColor(1, .2, .2) + ApplyTheme() f.bar:SetVertexColor(1, .2, .2) - f.bar:SetWidth(WIDTH - ICON - 8) + f.bar:SetWidth(BAR_WIDTH) f.bar:Show() f.title:SetText(T["Stunned"]) f.detail:SetText(T["Drag to move, right-click to reset"]) @@ -200,7 +212,11 @@ local function CreateAlert() local f = CreateFrame("Frame", "ShaguTweaksLossOfControl", UIParent) f:SetWidth(WIDTH) - f:SetHeight(ICON + 14) + f:SetHeight(ICON + 24) + f:SetBackdrop({ bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", tile = true, tileSize = 16, + edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } }) + f:SetBackdropColor(0, 0, 0, .9) f:SetFrameStrata("HIGH") f:SetMovable(true) f:SetClampedToScreen(true) @@ -211,38 +227,41 @@ local function CreateAlert() f.icon = f:CreateTexture(nil, "ARTWORK") f.icon:SetWidth(ICON) f.icon:SetHeight(ICON) - f.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0) + f.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -10) f.icon:SetTexCoord(.08, .92, .08, .92) - -- colored frame around the icon: a slightly larger texture behind it - f.border = f:CreateTexture(nil, "BACKGROUND") - f.border:SetTexture("Interface\\Buttons\\WHITE8X8") - f.border:SetPoint("TOPLEFT", f.icon, "TOPLEFT", -2, 2) - f.border:SetPoint("BOTTOMRIGHT", f.icon, "BOTTOMRIGHT", 2, -2) + -- Classic tooltip rim, kept separate from the effect-colored timer fill. + f.border = CreateFrame("Frame", nil, f) + f.border:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 12, insets = { left = 3, right = 3, top = 3, bottom = 3 } }) + f.border:SetPoint("TOPLEFT", f.icon, "TOPLEFT", -3, 3) + f.border:SetPoint("BOTTOMRIGHT", f.icon, "BOTTOMRIGHT", 3, -3) f.title = f:CreateFontString(nil, "OVERLAY") - f.title:SetFont("Fonts\\FRIZQT__.TTF", 20, "OUTLINE") + f.title:SetFontObject(GameFontNormalLarge) f.title:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 10, -2) f.title:SetJustifyH("LEFT") - f.title:SetTextColor(1, 1, 1) + f.title:SetWidth(WIDTH - ICON - 80) f.detail = f:CreateFontString(nil, "OVERLAY") - f.detail:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE") + f.detail:SetFontObject(GameFontHighlightSmall) f.detail:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -2) f.detail:SetJustifyH("LEFT") f.detail:SetTextColor(.85, .85, .85) + f.detail:SetWidth(BAR_WIDTH) + f.detail:SetHeight(28) f.time = f:CreateFontString(nil, "OVERLAY") - f.time:SetFont("Fonts\\FRIZQT__.TTF", 26, "OUTLINE") - f.time:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -2) + f.time:SetFont("Fonts\\FRIZQT__.TTF", 22, "") + f.time:SetPoint("TOPRIGHT", f, "TOPRIGHT", -10, -10) f.time:SetJustifyH("RIGHT") f.time:SetTextColor(1, 1, 1) -- remaining time as a bar under the text f.bar = f:CreateTexture(nil, "ARTWORK") - f.bar:SetTexture("Interface\\Buttons\\WHITE8X8") + f.bar:SetTexture("Interface\\TargetingFrame\\UI-StatusBar") f.bar:SetHeight(4) - f.bar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", ICON + 8, 0) + f.bar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", ICON + 20, 10) f:SetScript("OnMouseDown", function() if arg1 == "LeftButton" then this:StartMoving() end @@ -270,6 +289,7 @@ local function CreateAlert() end end) + ApplyTheme() RestorePosition() end diff --git a/mods/move-unitframes.lua b/mods/move-unitframes.lua index 0a1c8af..7ddafdd 100644 --- a/mods/move-unitframes.lua +++ b/mods/move-unitframes.lua @@ -124,6 +124,7 @@ module.enable = function(self) } local function Resolve(target) + if target.name == "PlayerFrame" and ShaguTweaks_config[T["Standalone Player Frame"]] == 1 then return end local handle = _G[target.name] if not handle then return end diff --git a/mods/nameplate-castbar.lua b/mods/nameplate-castbar.lua deleted file mode 100644 index aef1dc1..0000000 --- a/mods/nameplate-castbar.lua +++ /dev/null @@ -1,402 +0,0 @@ -local _G = ShaguTweaks.GetGlobalEnv() -local T = ShaguTweaks.T -local API = ShaguTweaks.API -local LegacyCastingInfo = ShaguTweaks.UnitCastingInfo -local LegacyChannelInfo = ShaguTweaks.UnitChannelInfo - -local module = ShaguTweaks:register({ - title = T["Nameplate Castbar"], - description = T["Adds a castbar to enemy nameplates."], - expansions = { ["vanilla"] = true }, - category = T["Nameplates"], - enabled = true, -}) - -local function QueryLegacy(query) - if not query then return end - - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = LegacyCastingInfo(query) - if cast then - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, false - end - - local channel - channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = LegacyChannelInfo(query) - if channel then - return channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill, true - end -end - -local function QuerySuperWoW(guid, channelSpellID, hasChannel) - if not guid then return end - - local cached = ShaguTweaks.libcast.db[guid] - if not cached then return end - - if cached.channel then - if not hasChannel or cached.spellID ~= channelSpellID then return end - - local channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = LegacyChannelInfo(guid) - if channel then - return channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill, true - end - return - end - - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = LegacyCastingInfo(guid) - if cast then - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, false - end -end - -module.enable = function(self) - if ShaguPlates then return end - - local backdrop = { - edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", - tile = true, tileSize = 8, edgeSize = 12, - insets = { left = 3, right = 3, top = 3, bottom = 3 } - } - - local platesByUnit = {} - local useClassicNameplateTokens = false - - local function GetCastbarWidth(plate) - if not plate then return end - - -- Match the visible healthbar, not the outer nameplate frame. Nameplate - -- Scale reparents/scales the healthbar, so convert its width back into the - -- plate's coordinate space using effective scales. - local healthbar = plate.healthbar - if not healthbar or not healthbar.GetWidth then - healthbar = plate:GetChildren() - end - - if healthbar and healthbar.GetWidth then - local width = healthbar:GetWidth() - local healthScale = healthbar.GetEffectiveScale and healthbar:GetEffectiveScale() - local plateScale = plate.GetEffectiveScale and plate:GetEffectiveScale() - - if width and width > 0 and healthScale and healthScale > 0 - and plateScale and plateScale > 0 - then - width = width * healthScale / plateScale - if width > 0 then return width end - elseif width and width > 0 then - return width - end - end - - -- Defensive fallback matching the historical ShaguTweaks layout. - local width = plate:GetWidth() - return width and width > 22 and width - 22 or nil - end - - local function SyncCastbarWidth(plate) - if not plate or not plate.castbar then return end - - local width = GetCastbarWidth(plate) - if not width then return end - - local current = plate.castbar:GetWidth() - if not current or math.abs(current - width) > .5 then - plate.castbar:SetWidth(width) - end - end - - local function create_castbar(plate) - if not plate or plate.castbar then return end - - plate.castbar = CreateFrame("StatusBar", nil, plate) - plate.castbar:SetPoint("BOTTOM", plate, "BOTTOM", 8, -11) - plate.castbar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar") - plate.castbar:SetStatusBarColor(1, .8, 0, 1) - plate.castbar:SetWidth(GetCastbarWidth(plate) or 100) - plate.castbar:SetHeight(10) - plate.castbar.ownerPlate = plate - - plate.castbar.texture = CreateFrame("Frame", nil, plate.castbar) - plate.castbar.texture:SetPoint("RIGHT", plate.castbar, "LEFT", 0, 0) - plate.castbar.texture:SetHeight(18) - plate.castbar.texture:SetWidth(18) - plate.castbar.texture.icon = plate.castbar.texture:CreateTexture(nil, "BACKGROUND") - plate.castbar.texture.icon:SetPoint("CENTER", 0, 0) - plate.castbar.texture.icon:SetWidth(12) - plate.castbar.texture.icon:SetHeight(12) - plate.castbar.texture:SetBackdrop(backdrop) - plate.castbar.texture:SetBackdropBorderColor(1,.8,0) - - plate.castbar.bg = plate.castbar:CreateTexture(nil, "BACKGROUND") - plate.castbar.bg:SetTexture("Interface\\TARGETINGFRAME\\UI-StatusBar") - plate.castbar.bg:SetVertexColor(.1, .1, 0, .8) - plate.castbar.bg:SetAllPoints(true) - - plate.castbar.spark = plate.castbar:CreateTexture(nil, "OVERLAY") - plate.castbar.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark") - plate.castbar.spark:SetWidth(20) - plate.castbar.spark:SetHeight(20) - plate.castbar.spark:SetBlendMode("ADD") - - plate.castbar.backdrop = CreateFrame("Frame", nil, plate.castbar) - plate.castbar.backdrop:SetFrameLevel(plate.castbar:GetFrameLevel()) - plate.castbar.backdrop:SetPoint("TOPLEFT", plate.castbar, "TOPLEFT", -3, 3) - plate.castbar.backdrop:SetPoint("BOTTOMRIGHT", plate.castbar, "BOTTOMRIGHT", 3, -3) - plate.castbar.backdrop:SetBackdrop(backdrop) - plate.castbar.backdrop:SetBackdropBorderColor(1,.8,0) - - plate.castbar.text = plate.castbar:CreateFontString(nil, "HIGH", "GameFontWhite") - plate.castbar.text:SetPoint("CENTER", plate.castbar, "CENTER", 0, 0) - local font, size = plate.castbar.text:GetFont() - plate.castbar.text:SetFont(font, size - 3, "THINOUTLINE") - - plate.castbar:Hide() - end - - local function HideCast(plate) - if not plate or not plate.castbar then return end - plate.castbar.startTime = nil - plate.castbar.endTime = nil - plate.castbar.isChannel = nil - plate.castbar:Hide() - end - - local function QueryPlateCast(plate) - if not plate then return end - - -- nameplateN tokens are tied to a GUID by ClassicAPI. Verify the mapping - -- before trusting a token kept on a recycled nameplate frame. - local unit = plate.unit - if unit and plate.guid and API.UnitGUID(unit) ~= plate.guid then - unit = nil - end - - if unit and API.casts then - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = API.GetCastInfo(unit) - if cast then - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, false - end - - local channel, notInterruptible, channelSpellID, hasChannel - channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill, - notInterruptible, channelSpellID, hasChannel = API.GetChannelInfo(unit) - if channel then - return channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill, true - end - - -- With a real ClassicAPI unit token, "no cast" is authoritative. Never - -- fall through to the old name-keyed database, otherwise two mobs with - -- the same name can incorrectly share a castbar. The GUID-keyed - -- SuperWoW fallback remains safe; channels must also match ClassicAPI's - -- live channel spellID before their cached timing is trusted. - if ShaguTweaks.superwow_active and plate.guid then - return QuerySuperWoW(plate.guid, channelSpellID, hasChannel) - end - return - end - - -- SuperWoW's GUID path is safe because it identifies the exact creature. - if ShaguTweaks.superwow_active and plate.guid then - local a, b, c, d, e, f, g, h = QueryLegacy(plate.guid) - if a then return a, b, c, d, e, f, g, h end - end - - -- Only legacy environments without ClassicAPI nameplate identity are - -- allowed to fall back to names. This preserves old compatibility while - -- preventing same-name leakage on the ClassicAPI path. - if not useClassicNameplateTokens then - local name = plate.name and plate.name:GetText() - return QueryLegacy(name) - end - end - - local function UpdateProgress(plate) - if not plate or not plate.castbar then return end - local bar = plate.castbar - local startTime = bar.startTime - local endTime = bar.endTime - - if not startTime or not endTime or endTime <= startTime then - HideCast(plate) - return - end - - local now = GetTime() - if now >= endTime then - HideCast(plate) - return - end - - local max = endTime - startTime - local cur - if bar.isChannel then - cur = endTime - now - else - cur = now - startTime - end - - cur = cur > max and max or cur - cur = cur < 0 and 0 or cur - bar:SetValue(cur) - - local percent = max > 0 and cur / max or 0 - local x = bar:GetWidth() * percent - bar.spark:ClearAllPoints() - bar.spark:SetPoint("CENTER", bar, "LEFT", x, 0) - bar:SetAlpha(plate:GetAlpha()) - end - - local function RefreshPlate(plate) - if not plate then return end - if not plate.castbar then create_castbar(plate) end - - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill, isChannel = QueryPlateCast(plate) - if not cast or not startTime or not endTime or endTime <= startTime then - HideCast(plate) - return - end - - local bar = plate.castbar - - -- NAME_PLATE_UNIT_ADDED can arrive before other visual modules finish - -- sizing the plate. Re-read the final healthbar width when a cast is - -- actually shown instead of keeping the early creation width. - SyncCastbarWidth(plate) - - bar.startTime = startTime / 1000 - bar.endTime = endTime / 1000 - bar.isChannel = isChannel - - local max = bar.endTime - bar.startTime - bar:SetMinMaxValues(0, max) - bar.text:SetText(cast) - - if texture then - bar.texture.icon:SetTexture(texture) - bar.texture.icon:Show() - else - bar.texture.icon:Hide() - end - - bar:Show() - UpdateProgress(plate) - end - - local function AttachUnit(unit) - if not unit or not API.GetNamePlateForUnit then return end - local plate = API.GetNamePlateForUnit(unit) - if not plate then return end - - local oldUnit = plate.unit - if oldUnit and oldUnit ~= unit and platesByUnit[oldUnit] == plate then - platesByUnit[oldUnit] = nil - end - - plate.unit = unit - plate.guid = API.UnitGUID(unit) - platesByUnit[unit] = plate - - if not plate.castbar then create_castbar(plate) end - RefreshPlate(plate) - return plate - end - - -- A castbar animates only while visible. Hidden bars receive no useful work, - -- while cast discovery itself is driven by ClassicAPI events below. - local function EnableBarAnimation(plate) - if not plate or not plate.castbar or plate.castbar.animationHooked then return end - plate.castbar.animationHooked = true - plate.castbar:SetScript("OnUpdate", function() - local owner = this.ownerPlate - if owner then UpdateProgress(owner) end - end) - end - - local oldCreate = create_castbar - create_castbar = function(plate) - oldCreate(plate) - EnableBarAnimation(plate) - end - - local listener = CreateFrame("Frame") - local eventValid = API and API.eventutils and _G.C_EventUtils and _G.C_EventUtils.IsEventValid - - if API and API.nameplateevents and API.casts then - listener:RegisterEvent("NAME_PLATE_UNIT_ADDED") - listener:RegisterEvent("NAME_PLATE_UNIT_REMOVED") - useClassicNameplateTokens = true - end - - local castEvents = { - "UNIT_SPELLCAST_START", - "UNIT_SPELLCAST_STOP", - "UNIT_SPELLCAST_INTERRUPTED", - "UNIT_SPELLCAST_FAILED", - "UNIT_SPELLCAST_DELAYED", - "UNIT_SPELLCAST_CHANNEL_START", - "UNIT_SPELLCAST_CHANNEL_STOP", - "UNIT_SPELLCAST_CHANNEL_UPDATE", - } - - local hasClassicCastEvents = false - if eventValid and useClassicNameplateTokens then - for _, eventName in pairs(castEvents) do - if _G.C_EventUtils.IsEventValid(eventName) then - listener:RegisterEvent(eventName) - hasClassicCastEvents = true - end - end - end - - listener:SetScript("OnEvent", function() - if event == "NAME_PLATE_UNIT_ADDED" then - AttachUnit(arg1) - return - end - - if event == "NAME_PLATE_UNIT_REMOVED" then - local unit = arg1 - local plate = platesByUnit[unit] - if not plate and API.GetNamePlateForUnit then - plate = API.GetNamePlateForUnit(unit) - end - - if plate then - HideCast(plate) - if plate.unit == unit then - plate.unit = nil - plate.guid = nil - end - end - platesByUnit[unit] = nil - return - end - - local unit = arg1 - if useClassicNameplateTokens and unit - and string.find(unit, "^nameplate%d+$") then - local plate = platesByUnit[unit] or AttachUnit(unit) - if plate then RefreshPlate(plate) end - end - end) - - if useClassicNameplateTokens and hasClassicCastEvents then - -- ClassicAPI owns identity and cast discovery. libnameplate is still used - -- by the other visual modules, but this module no longer queries every - -- visible plate's cast state on every rendered frame. - table.insert(ShaguTweaks.libnameplate.OnShow, function(plate) - plate = plate or this - if plate.unit then - RefreshPlate(plate) - end - end) - else - -- Compatibility path for older ClassicAPI / plain Vanilla. Preserve the - -- original per-plate discovery behavior where modern nameplate tokens or - -- spellcast events do not exist. - table.insert(ShaguTweaks.libnameplate.OnUpdate, function(plate) - plate = plate or this - if not plate.castbar then create_castbar(plate) end - RefreshPlate(plate) - end) - end -end diff --git a/mods/nameplate-classcolor.lua b/mods/nameplate-classcolor.lua deleted file mode 100644 index 62147e3..0000000 --- a/mods/nameplate-classcolor.lua +++ /dev/null @@ -1,84 +0,0 @@ -local _G = ShaguTweaks.GetGlobalEnv() -local T = ShaguTweaks.T -local GetUnitData = ShaguTweaks.GetUnitData -local API = ShaguTweaks.API - -local module = ShaguTweaks:register({ - title = T["Nameplate Class Colors"], - description = T["Changes the nameplate health bar color to the class color."], - expansions = { ["vanilla"] = true }, - category = T["Nameplates"], - enabled = true, -}) - -module.enable = function(self) - if ShaguPlates then return end - - local colorsByName = {} - local colorsVersion = 0 - local useClassicTokens = API and API.nameplateevents - - if useClassicTokens then - local listener = CreateFrame("Frame") - listener:RegisterEvent("NAME_PLATE_UNIT_ADDED") - listener:RegisterEvent("NAME_PLATE_UNIT_REMOVED") - listener:SetScript("OnEvent", function() - local unit = arg1 - if not unit then return end - - -- Use only the exact nameplateN unit token here. Do not request or - -- decorate ClassicAPI's Lua frame wrapper: the legacy libnameplate - -- discovery path is deliberately kept isolated after native crashes. - local name = UnitName(unit) - if not name then return end - - if event == "NAME_PLATE_UNIT_ADDED" then - local color - if UnitIsPlayer(unit) then - local _, class = UnitClass(unit) - color = class and RAID_CLASS_COLORS[class] or nil - end - colorsByName[name] = color or false - colorsVersion = colorsVersion + 1 - elseif event == "NAME_PLATE_UNIT_REMOVED" then - colorsByName[name] = nil - colorsVersion = colorsVersion + 1 - end - end) - end - - table.insert(ShaguTweaks.libnameplate.OnUpdate, function() - if useClassicTokens then - -- Refresh the cached desired color only when ClassicAPI reports a - -- nameplate identity/class change. Turtle/Blizzard can still restore the - -- hostile red statusbar color later, so compare the actual bar color each - -- frame and write only when it drifted from the cached class color. - if this.ShaguTweaksClassColorVersion ~= colorsVersion then - this.ShaguTweaksClassColorVersion = colorsVersion - local name = this.name and this.name:GetText() - this.ShaguTweaksClassColor = name and colorsByName[name] or false - end - - local color = this.ShaguTweaksClassColor - if color then - local r, g, b = this.healthbar:GetStatusBarColor() - if not r - or math.abs(r - color.r) > .01 - or math.abs(g - color.g) > .01 - or math.abs(b - color.b) > .01 - then - this.healthbar:SetStatusBarColor(color.r, color.g, color.b, 1) - end - end - return - end - - -- Plain Vanilla / old ClassicAPI compatibility path. - local name = this.name:GetText() - local class, _, _, player = GetUnitData(name, true) - if class and player then - local color = RAID_CLASS_COLORS[class] or { r = .5, g = .5, b = .5, a = 1 } - this.healthbar:SetStatusBarColor(color.r, color.g, color.b, 1) - end - end) -end diff --git a/mods/nameplate-scale.lua b/mods/nameplate-scale.lua deleted file mode 100644 index cfb7ce7..0000000 --- a/mods/nameplate-scale.lua +++ /dev/null @@ -1,57 +0,0 @@ -local _G = ShaguTweaks.GetGlobalEnv() -local T = ShaguTweaks.T - -local module = ShaguTweaks:register({ - title = T["Nameplate Scale"], - description = T["Makes all nameplates honor the UI-Scale setting."], - expansions = { ["vanilla"] = true }, - category = T["Nameplates"], - enabled = true, -}) - -module.enable = function(self) - if ShaguPlates then return end - - table.insert(ShaguTweaks.libnameplate.OnInit, function(plate) - local owidth = plate:GetWidth() - local oheight = plate:GetHeight() - - -- create new plate - local new = CreateFrame("Frame", nil, plate) - plate.new = new - - new:SetScale(UIParent:GetScale()) - new:SetAllPoints(plate) - new.plate = plate - - plate.healthbar:SetParent(new) - plate.healthbar:SetFrameLevel(1) - - plate:SetWidth(owidth*UIParent:GetScale()) - plate:SetHeight(oheight*UIParent:GetScale()) - - for i, object in pairs({plate:GetRegions()}) do - object:SetParent(new) - - -- adjust level icon position - if ShaguTweaks:GetExpansion() == "vanilla" and i == 5 or i == 7 then - object:SetPoint("BOTTOMRIGHT", -6, 3) - end - end - - new:SetScript("OnShow", function() - -- adjust sizes - this:SetScale(UIParent:GetScale()) - this.plate:SetWidth(owidth*UIParent:GetScale()) - this.plate:SetHeight(oheight*UIParent:GetScale()) - end) - end) - - table.insert(ShaguTweaks.libnameplate.OnUpdate, function() - local alpha = this:GetAlpha() - if this.new.ShaguTweaksAlpha ~= alpha then - this.new.ShaguTweaksAlpha = alpha - this.new:SetAlpha(alpha) - end - end) -end diff --git a/mods/reduced-actionbar.lua b/mods/reduced-actionbar.lua index 286c1ea..02f843f 100644 --- a/mods/reduced-actionbar.lua +++ b/mods/reduced-actionbar.lua @@ -4,7 +4,7 @@ local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc local module = ShaguTweaks:register({ title = T["Reduced Actionbar Size"], - description = T["Reduces the actionbar size by removing several items such as the bag panel and microbar"], + description = T["Reduces the actionbar size by removing the bag panel and microbar. Hold Ctrl+Shift+Alt to move action, pet, stance, XP and reputation bars; right-click a marker to reset it."], expansions = { ["vanilla"] = true }, category = T["Action Bar"], enabled = nil, @@ -126,6 +126,11 @@ module.enable = function(self) local function UpdateActionBarPositions() local expVisible = MainMenuExpBar:IsVisible() local reputationVisible = ReputationWatchBar:IsVisible() + local movers = ShaguTweaks.ActionBarMovers + if movers then + expVisible = expVisible and not movers.IsMoved("MainMenuExpBar") + reputationVisible = reputationVisible and not movers.IsMoved("ReputationWatchBar") + end local bottomLeftVisible = MultiBarBottomLeft:IsVisible() local bottomRightVisible = MultiBarBottomRight:IsVisible() local shapeshiftVisible = ShapeshiftBarFrame:IsVisible() @@ -134,7 +139,7 @@ module.enable = function(self) -- move top actionbar if xp or reputation is tracked MultiBarBottomLeft:ClearAllPoints() if expVisible or reputationVisible then - local anchor = GetWatchedFactionInfo() and ReputationWatchBar or MainMenuExpBar + local anchor = reputationVisible and ReputationWatchBar or MainMenuExpBar MultiBarBottomLeft:SetPoint("BOTTOM", anchor, "TOP", 2, 3) else MultiBarBottomLeft:SetPoint("BOTTOM", MainMenuBar, "TOP", 2, -3) @@ -185,4 +190,5 @@ module.enable = function(self) for i = 1, 5 do _G["ContainerFrame" .. i .. "PortraitButton"]:SetScript("OnClick", ReplaceBag) end + ShaguTweaks.EnableActionBarMovers() end diff --git a/mods/target-debufftimer.lua b/mods/target-debufftimer.lua deleted file mode 100644 index db9df6a..0000000 --- a/mods/target-debufftimer.lua +++ /dev/null @@ -1,266 +0,0 @@ -local _G = ShaguTweaks.GetGlobalEnv() -local T = ShaguTweaks.T -local API = ShaguTweaks.API - -local module = ShaguTweaks:register({ - title = T["Debuff Timer"], - description = T["Show debuff durations on the target unit frame."], - expansions = { ["vanilla"] = true }, - category = T["Unit Frames"], - enabled = true, -}) - -local libdebuff = ShaguTweaks.libdebuff -local TimeConvert = ShaguTweaks.TimeConvert - -local function GetLegacyDebuff(index) - if libdebuff and libdebuff.UnitDebuff then - return libdebuff:UnitDebuff("target", index) - end -end - -local function NormalizeAuraString(value) - return value and value ~= "" and value or nil -end - -local function ResolveDebuffTiming(name, icon, stacks, dtype, duration, expirationTime, index, now) - icon = NormalizeAuraString(icon) - dtype = NormalizeAuraString(dtype) - duration = duration and duration > 0 and duration or nil - - if duration and expirationTime and expirationTime > 0 then - local timeleft = expirationTime - now - if timeleft < 0 then timeleft = 0 end - return name, nil, icon, stacks, dtype, duration, timeleft - end - - -- ClassicAPI always knows the aura metadata, but expiration can be unknown - -- when the client did not observe its application. Keep ShaguTweaks' old - -- estimate only for that missing-timing case. - if duration then - local effect, rank, texture, legacyStacks, legacyType, - legacyDuration, legacyTimeleft = GetLegacyDebuff(index) - if effect then - return name or effect, rank, icon or texture, stacks or legacyStacks, - dtype or legacyType, legacyDuration, legacyTimeleft - end - end - - -- Duration 0 is a permanent/indefinite aura. Keep its metadata visible but - -- do not create a fake cooldown. - return name, nil, icon, stacks, dtype, nil, -1 -end - -local function GetTargetDebuff(index, now) - -- Prefer ClassicAPI's positional accessor: same useful aura/timing data as - -- AuraData, but no temporary Lua table allocation for every debuff button. - if API and API.aurapositional and API.UnitDebuff then - local name, icon, stacks, dtype, duration, expirationTime = - API.UnitDebuff("target", index) - if name then - return ResolveDebuffTiming(name, icon, stacks, dtype, duration, - expirationTime, index, now) - end - return - end - - -- Compatibility with older ClassicAPI builds that only expose AuraData. - local aura = API and API.GetDebuffDataByIndex and - API.GetDebuffDataByIndex("target", index) - if aura then - return ResolveDebuffTiming(aura.name, aura.icon, aura.applications, - aura.dispelName, aura.duration, aura.expirationTime, index, now) - end - - return GetLegacyDebuff(index) -end - --- All readable debuff timers share one throttled updater. The old code attached --- one OnUpdate handler to every visible debuff, multiplying GetTime/SetText --- work as more auras appeared on the target. -local timerUpdater = CreateFrame("Frame") -local activeReadables = {} -local activeCount = 0 - -timerUpdater.elapsed = 0 -timerUpdater:Hide() - -local function DeactivateReadable(readable) - if not readable then return end - - local index = readable.activeIndex - if index then - local last = activeReadables[activeCount] - activeReadables[index] = last - if last and last ~= readable then - last.activeIndex = index - end - activeReadables[activeCount] = nil - activeCount = activeCount - 1 - readable.activeIndex = nil - end - - readable:Hide() - - if activeCount == 0 then - timerUpdater.elapsed = 0 - timerUpdater:Hide() - end -end - -local function UpdateReadableText(readable, remaining) - local text = TimeConvert(remaining) - if readable.lastText ~= text then - readable.lastText = text - readable.text:SetText(text) - end -end - -local function ActivateReadable(readable, start, duration, timeleft) - readable.start = start - readable.duration = duration - - if not readable.activeIndex then - activeCount = activeCount + 1 - activeReadables[activeCount] = readable - readable.activeIndex = activeCount - timerUpdater:Show() - end - - local parent = readable:GetParent() - if parent then - local alpha = parent:GetAlpha() - if readable.lastAlpha ~= alpha then - readable.lastAlpha = alpha - readable:SetAlpha(alpha) - end - end - - UpdateReadableText(readable, timeleft) - readable:Show() -end - -timerUpdater:SetScript("OnUpdate", function() - this.elapsed = this.elapsed + arg1 - if this.elapsed < .1 then return end - this.elapsed = 0 - - local now = GetTime() - local i = activeCount - while i >= 1 do - local readable = activeReadables[i] - local parent = readable and readable:GetParent() - - if not readable or not parent then - if readable then DeactivateReadable(readable) end - else - local remaining = readable.duration - (now - readable.start) - if remaining >= 0 then - local alpha = parent:GetAlpha() - if readable.lastAlpha ~= alpha then - readable.lastAlpha = alpha - readable:SetAlpha(alpha) - end - UpdateReadableText(readable, remaining) - else - DeactivateReadable(readable) - end - end - - i = i - 1 - end -end) - -local function CreateTextCooldown(cooldown) - if cooldown.readable then return cooldown.readable end - - -- These overlays never need global frame names. Keeping them anonymous - -- avoids collisions when several target debuffs are visible at once. - cooldown.readable = CreateFrame("Frame", nil, cooldown:GetParent()) - cooldown.readable:SetAllPoints(cooldown) - cooldown.readable:SetFrameLevel(cooldown:GetParent():GetFrameLevel() + 1) - cooldown.readable.text = cooldown.readable:CreateFontString(nil, "OVERLAY") - - cooldown.readable.text:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE") - cooldown.readable.text:SetPoint("CENTER", cooldown.readable, "CENTER", 0, 0) - - return cooldown.readable -end - -module.enable = function(self) - local HookTargetDebuffButton_Update = TargetDebuffButton_Update - TargetDebuffButton_Update = function() - HookTargetDebuffButton_Update() - - -- All buttons in this refresh share the same clock sample. Besides being - -- cheaper, this keeps their displayed countdowns perfectly aligned. - local now = GetTime() - - for i=1, MAX_TARGET_DEBUFFS do - local effect, rank, texture, stacks, dtype, duration, timeleft = - GetTargetDebuff(i, now) - local button = _G["TargetFrameDebuff"..i] - - if button and not button.cd then - local frameType = API.nativecooldowns and "Cooldown" or "Model" - button.cd = CreateFrame(frameType, "TargetFrameDebuff"..i.."Cooldown", button, "CooldownFrameTemplate") - button.cd.noCooldownCount = true - -- The timer text is drawn by the overlay below. A native Cooldown - -- would otherwise add a second set of numbers when the - -- countdownForCooldowns CVar is turned on. - if type(button.cd.SetHideCountdownNumbers) == "function" then - button.cd:SetHideCountdownNumbers(true) - end - button.cd:SetAllPoints() - button.cd:SetScale(.6) - button.cd:SetAlpha(.8) - end - - local dCount = _G["TargetFrameDebuff" .. i .. "Count"] - if button and dCount then - if not dCount.fixup then - dCount.fixup = true - dCount:SetPoint("BOTTOMRIGHT", "TargetFrameDebuff" .. i, "BOTTOMRIGHT", 6, -3) - end - if stacks and stacks > 1 then - local stackText = "|c0000ff3b" .. stacks - if dCount.shaguStackText ~= stackText then - dCount.shaguStackText = stackText - dCount:SetText(stackText) - end - dCount:Show() - else - dCount.shaguStackText = nil - dCount:Hide() - end - end - - local dBorder = _G["TargetFrameDebuff" .. i .. "Border"] - if button and dBorder then - local color = dtype and DebuffTypeColor[dtype] or DebuffTypeColor["none"] - dBorder:SetVertexColor(color.r, color.g, color.b) - end - - if button and effect and duration and timeleft and timeleft >= 0 then - local start = now + timeleft - duration - local readable = CreateTextCooldown(button.cd) - - if button.cd.shaguStart ~= start or button.cd.shaguDuration ~= duration then - button.cd.shaguStart = start - button.cd.shaguDuration = duration - CooldownFrame_SetTimer(button.cd, start, duration, 1) - end - - ActivateReadable(readable, start, duration, timeleft) - button.cd:Show() - elseif button then - if button.cd.shaguDuration then - button.cd.shaguStart = nil - button.cd.shaguDuration = nil - CooldownFrame_SetTimer(button.cd, 0, 0, 0) - end - if button.cd.readable then DeactivateReadable(button.cd.readable) end - end - end - end -end diff --git a/mods/unitframes-bighealth.lua b/mods/unitframes-bighealth.lua index 2d08ee4..03ea6cb 100644 --- a/mods/unitframes-bighealth.lua +++ b/mods/unitframes-bighealth.lua @@ -1,9 +1,17 @@ local T = ShaguTweaks.T local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc -local module = ShaguTweaks:register({ - title = T["Unit Frame Big Health"], - description = T["Increases the healthbar of the player and target unitframe."], +local playerModule = ShaguTweaks:register({ + title = T["Unit Frame Big Health for Player"], + description = T["Increases the healthbar of the player unitframe."], + expansions = { ["vanilla"] = true }, + category = T["Unit Frames"], + enabled = nil, +}) + +local targetModule = ShaguTweaks:register({ + title = T["Unit Frame Big Health for Target"], + description = T["Increases the healthbar of the target unitframe."], expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = nil, @@ -21,7 +29,7 @@ for i = 1, table.getn(tocs) do end addonpath = addonpath or "Interface\\AddOns\\ShaguTweaks" -module.enable = function(self) +local function Enable(player, target) local normalTexture = addonpath .. "\\img\\UI-TargetingFrame" local eliteTexture = addonpath .. "\\img\\UI-TargetingFrame-Elite" local rareTexture = addonpath .. "\\img\\UI-TargetingFrame-Rare" @@ -42,56 +50,60 @@ module.enable = function(self) local b = tonumber(color.b) or color.b local a = tonumber(color.a) or color.a or 1 - if PlayerFrameTexture then PlayerFrameTexture:SetVertexColor(r, g, b, a) end - if PlayerStatusTexture then PlayerStatusTexture:SetVertexColor(r, g, b, a) end - if TargetFrameTexture then TargetFrameTexture:SetVertexColor(r, g, b, a) end + if player and PlayerFrameTexture then PlayerFrameTexture:SetVertexColor(r, g, b, a) end + if player and PlayerStatusTexture then PlayerStatusTexture:SetVertexColor(r, g, b, a) end + if target and TargetFrameTexture then TargetFrameTexture:SetVertexColor(r, g, b, a) end end -- Big Health is presentation-only. Numeric health/power values are owned by -- the independent Real Health Numbers module. - PlayerFrameTexture:SetTexture(normalTexture) - PlayerFrameHealthBar:SetPoint("TOPLEFT", 106, -22) - PlayerFrameHealthBar:SetHeight(30) + if player then + PlayerFrameTexture:SetTexture(normalTexture) + PlayerFrameHealthBar:SetPoint("TOPLEFT", 106, -22) + PlayerFrameHealthBar:SetHeight(30) - PlayerStatusTexture:SetTexture(addonpath .. "\\img\\UI-Player-Status") + PlayerStatusTexture:SetTexture(addonpath .. "\\img\\UI-Player-Status") - -- The additive status artwork contains both the portrait ring and bar edge. - -- Crop only the latter while resting, shrinking the displayed width by the - -- same ratio so the portrait ring keeps its original size and position. - -- Native status code continues to own visibility, tint, alpha and pulsing. - local statusCoords = {PlayerStatusTexture:GetTexCoord()} - local statusWidth = PlayerStatusTexture:GetWidth() - local portraitRight = 72 / 256 -- right edge of the portrait in UI-Player-Status - local restingCrop = false + -- The additive status artwork contains both the portrait ring and bar edge. + -- Crop only the latter while resting, shrinking the displayed width by the + -- same ratio so the portrait ring keeps its original size and position. + -- Native status code continues to own visibility, tint, alpha and pulsing. + local statusCoords = {PlayerStatusTexture:GetTexCoord()} + local statusWidth = PlayerStatusTexture:GetWidth() + local portraitRight = 72 / 256 -- right edge of the portrait in UI-Player-Status + local restingCrop = false - local function UpdateRestingGlow() - local resting = IsResting() - and not UnitAffectingCombat("player") - local left, right = statusCoords[1], statusCoords[5] - if not left or not right or right <= left - or portraitRight <= left or portraitRight >= right then return end + local function UpdateRestingGlow() + local resting = IsResting() + and not UnitAffectingCombat("player") + local left, right = statusCoords[1], statusCoords[5] + if not left or not right or right <= left + or portraitRight <= left or portraitRight >= right then return end - if resting and not restingCrop then - PlayerStatusTexture:SetTexCoord( - left, portraitRight, statusCoords[2], statusCoords[4]) - PlayerStatusTexture:SetWidth( - statusWidth * (portraitRight - left) / (right - left)) - restingCrop = true - elseif not resting and restingCrop then - PlayerStatusTexture:SetTexCoord( - left, right, statusCoords[2], statusCoords[4]) - PlayerStatusTexture:SetWidth(statusWidth) - restingCrop = false + if resting and not restingCrop then + PlayerStatusTexture:SetTexCoord( + left, portraitRight, statusCoords[2], statusCoords[4]) + PlayerStatusTexture:SetWidth( + statusWidth * (portraitRight - left) / (right - left)) + restingCrop = true + elseif not resting and restingCrop then + PlayerStatusTexture:SetTexCoord( + left, right, statusCoords[2], statusCoords[4]) + PlayerStatusTexture:SetWidth(statusWidth) + restingCrop = false + end end + + hooksecurefunc("PlayerFrame_UpdateStatus", UpdateRestingGlow) + UpdateRestingGlow() end - hooksecurefunc("PlayerFrame_UpdateStatus", UpdateRestingGlow) - UpdateRestingGlow() - - TargetFrameTexture:SetTexture(normalTexture) - TargetFrameHealthBar:SetPoint("TOPRIGHT", -106, -22) - TargetFrameHealthBar:SetHeight(30) + if target then + TargetFrameTexture:SetTexture(normalTexture) + TargetFrameHealthBar:SetPoint("TOPRIGHT", -106, -22) + TargetFrameHealthBar:SetHeight(30) + end ApplyDarkModeTextures() @@ -124,7 +136,7 @@ module.enable = function(self) ApplyDarkModeTextures() end - hooksecurefunc("TargetFrame_CheckClassification", UpdateTargetClassificationTexture) + if target then hooksecurefunc("TargetFrame_CheckClassification", UpdateTargetClassificationTexture) end local playerSetStatusBarColor = PlayerFrameHealthBar.SetStatusBarColor local targetSetStatusBarColor = TargetFrameHealthBar.SetStatusBarColor @@ -143,7 +155,7 @@ module.enable = function(self) -- Keep the Big Health colors without replacing SetStatusBarColor with a -- no-op. Other addons may still call the original method normally. - if PlayerFrameNameBackground then + if player and PlayerFrameNameBackground then hooksecurefunc(PlayerFrameHealthBar, "SetStatusBarColor", ApplyPlayerHealthColor) hooksecurefunc(PlayerFrameNameBackground, "Show", function() PlayerFrameNameBackground:Hide() @@ -152,7 +164,7 @@ module.enable = function(self) ApplyPlayerHealthColor() end - if TargetFrameNameBackground then + if target and TargetFrameNameBackground then hooksecurefunc(TargetFrameHealthBar, "SetStatusBarColor", ApplyTargetHealthColor) hooksecurefunc(TargetFrameNameBackground, "Show", function() TargetFrameNameBackground:Hide() @@ -161,9 +173,14 @@ module.enable = function(self) ApplyTargetHealthColor() end - hooksecurefunc("TargetFrame_CheckFaction", ApplyTargetHealthColor) + if target then hooksecurefunc("TargetFrame_CheckFaction", ApplyTargetHealthColor) end - UpdateTargetClassificationTexture() - TargetFrame_CheckFaction() + if target then + UpdateTargetClassificationTexture() + TargetFrame_CheckFaction() + end end) end + +playerModule.enable = function() Enable(true, false) end +targetModule.enable = function() Enable(false, true) end diff --git a/mods/unitframes-classcolor.lua b/mods/unitframes-classcolor.lua index 1fa890d..4662414 100644 --- a/mods/unitframes-classcolor.lua +++ b/mods/unitframes-classcolor.lua @@ -3,6 +3,7 @@ local T = ShaguTweaks.T local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc local FALLBACK_CLASS_COLOR = { r = .5, g = .5, b = .5, a = 1 } +local MODERN_HEALTH_TEXTURE = "Interface\\AddOns\\ShaguTweaks\\img\\BlizzardRetailBar" local module = ShaguTweaks:register({ title = T["Unit Frame Class Colors"], @@ -67,7 +68,8 @@ module.enable = function(self) -- add name background to player frame PlayerFrameNameBackground = PlayerFrame:CreateTexture(nil, "BACKGROUND") - PlayerFrameNameBackground:SetTexture("Interface\\TargetingFrame\\UI-TargetingFrame-LevelBackground") + PlayerFrameNameBackground:SetTexture(ShaguTweaks.ModernClassColors and MODERN_HEALTH_TEXTURE + or "Interface\\TargetingFrame\\UI-TargetingFrame-LevelBackground") PlayerFrameNameBackground:SetWidth(119) PlayerFrameNameBackground:SetHeight(19) PlayerFrameNameBackground:SetPoint("TOPLEFT", 106, -22) @@ -224,9 +226,11 @@ modern.enable = function(self) RefreshPowerBars() for _, prefix in ipairs({"PlayerFrame", "TargetFrame", "PetFrame", "TargetofTarget", "PartyMemberFrame1", "PartyMemberFrame2", "PartyMemberFrame3", "PartyMemberFrame4"}) do local bar = _G[prefix .. "HealthBar"] - if bar then bar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\BlizzardRetailBar") end + if bar then bar:SetStatusBarTexture(MODERN_HEALTH_TEXTURE) end end if not PlayerFrameNameBackground then return end + PlayerFrameNameBackground:SetTexture(MODERN_HEALTH_TEXTURE) + if TargetFrameNameBackground then TargetFrameNameBackground:SetTexture(MODERN_HEALTH_TEXTURE) end local color = GetUnitClassColor("player") PlayerFrameNameBackground:SetVertexColor(color.r, color.g, color.b, 1) if TargetFrame_CheckFaction then TargetFrame_CheckFaction() end diff --git a/mods/unitframes-hide-player-name.lua b/mods/unitframes-hide-player-name.lua new file mode 100644 index 0000000..00918f5 --- /dev/null +++ b/mods/unitframes-hide-player-name.lua @@ -0,0 +1,17 @@ +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Hide Player Name"], + description = T["Hides your detached player name. Only applies while Standalone Player Frame is enabled."], + expansions = { ["vanilla"] = true }, + category = T["Unit Frames"], + enabled = nil, +}) + +module.enable = function(self) + if ShaguTweaks_config[T["Standalone Player Frame"]] ~= 1 then return end + local layout = ShaguTweaks.StandalonePlayerFrame + if layout and layout.name then layout.name:Hide() end + -- Standalone setup also reads the saved setting after all modules load, + -- so this works regardless of their initialization order. +end diff --git a/mods/unitframes-hide-player-portrait.lua b/mods/unitframes-hide-player-portrait.lua new file mode 100644 index 0000000..b49cac2 --- /dev/null +++ b/mods/unitframes-hide-player-portrait.lua @@ -0,0 +1,21 @@ +local T = ShaguTweaks.T + +local module = ShaguTweaks:register({ + title = T["Hide Player Portrait"], + description = T["Hides only your player portrait image. The player name, health bar and power bar remain visible."], + expansions = { ["vanilla"] = true }, + category = T["Unit Frames"], + enabled = nil, +}) + +module.enable = function(self) + if self.installed or not PlayerPortrait then return end + self.installed = true + local setAlpha = PlayerPortrait.SetAlpha + setAlpha(PlayerPortrait, 0) + -- Keep the texture object available to the stock portrait code and class + -- portrait module. Neither its visibility nor its parent's bars are altered. + ShaguTweaks.hooksecurefunc(PlayerPortrait, "SetAlpha", function(texture) + setAlpha(texture, 0) + end) +end diff --git a/mods/unitframes-standalone-player.lua b/mods/unitframes-standalone-player.lua new file mode 100644 index 0000000..5231d1a --- /dev/null +++ b/mods/unitframes-standalone-player.lua @@ -0,0 +1,371 @@ +local _G = ShaguTweaks.GetGlobalEnv() +local T = ShaguTweaks.T +local API = ShaguTweaks.API + +local module = ShaguTweaks:register({ + title = T["Standalone Player Frame"], + description = T["Separates the existing player portrait, name, health and power bars. Hold Ctrl+Shift+Alt to drag each piece; right-click with those keys held to reset it. Big Health also enlarges the power bar."], + expansions = { ["vanilla"] = true }, + category = T["Unit Frames"], + enabled = nil, +}) + +local function Unlocked() + return API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown() +end + +local function Position(holder, point) + holder:ClearAllPoints() + holder:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", point.x, point.y) +end + +local function StopDrag(holder) + if not holder.dragging then return end + holder:StopMovingOrSizing() + holder:SetUserPlaced(false) + holder.dragging = nil + holder.ignoreClick = true + local x, y = holder:GetLeft(), holder:GetTop() + if x and y then + holder.positions[holder.key] = { x = x, y = y } + Position(holder, holder.positions[holder.key]) + end +end + +local function MouseDown() + local holder = this.standaloneHolder + holder.ignoreClick = nil + if Unlocked() and arg1 == "RightButton" then + StopDrag(holder) + holder.positions[holder.key] = nil + Position(holder, holder.defaultPosition) + holder.ignoreClick = true + elseif not Unlocked() and this.standaloneMouseDown then + this.standaloneMouseDown() + end +end + +local function DragStart() + if not Unlocked() then return end + local holder = this.standaloneHolder + holder.dragging = true + holder:StartMoving() +end + +local function DragStop() + StopDrag(this.standaloneHolder) +end + +local function UnitClick() + local holder = this.standaloneHolder + if holder.ignoreClick or holder.dragging or Unlocked() then return end + if arg1 == "RightButton" and not SpellIsTargeting() then + ToggleDropDownMenu(1, nil, PlayerFrameDropDown, holder:GetName(), 0, 0) + elseif this.standaloneMouseUp then + this.standaloneMouseUp() + else + PlayerFrame_OnClick(arg1) + end +end + +local function BindHandle(handle, holder) + handle.standaloneHolder = holder + handle.standaloneMouseDown = handle:GetScript("OnMouseDown") + handle:RegisterForDrag("LeftButton") + handle:EnableMouse(true) + handle:SetScript("OnMouseDown", MouseDown) + handle:SetScript("OnDragStart", DragStart) + handle:SetScript("OnDragStop", DragStop) + if handle:GetObjectType() == "Button" then + handle:RegisterForClicks("LeftButtonUp", "RightButtonUp") + handle:SetScript("OnClick", UnitClick) + else + handle.standaloneMouseUp = handle:GetScript("OnMouseUp") + handle:SetScript("OnMouseUp", UnitClick) + end +end + +local function MoveRegion(region, holder, x, y) + if not region then return end + region:SetParent(holder) + region:ClearAllPoints() + region:SetPoint("TOPLEFT", holder, "TOPLEFT", x, y) +end + +module.enable = function(self) + if self.scheduled then return end + self.scheduled = true + local function Setup() + if self.installed then return end + if not PlayerFrame or not PlayerPortrait or not PlayerFrameTexture + or not PlayerFrameHealthBar or not PlayerFrameManaBar then return end + if not PlayerFrameHealthBar:GetLeft() or not PlayerFrameHealthBar:GetTop() + or not PlayerPortrait:GetLeft() or not PlayerPortrait:GetTop() then return end + self.installed = true + ShaguTweaks_config.StandalonePlayerFrame = ShaguTweaks_config.StandalonePlayerFrame or {} + local positions = ShaguTweaks_config.StandalonePlayerFrame + local scale = PlayerFrame:GetEffectiveScale() / UIParent:GetEffectiveScale() + local big = ShaguTweaks_config[T["Unit Frame Big Health for Player"]] == 1 + local width = PlayerFrameHealthBar:GetWidth() + local healthHeight = PlayerFrameHealthBar:GetHeight() + local manaHeight = big and healthHeight or PlayerFrameManaBar:GetHeight() + local left = PlayerFrameHealthBar:GetLeft() - 4 + local top = PlayerFrameHealthBar:GetTop() + 3 + local portraitLeft, portraitTop = PlayerPortrait:GetLeft() - 8, PlayerPortrait:GetTop() + 4 + + local function Holder(key, w, h, x, y) + local holder = CreateFrame("Button", "ShaguTweaksPlayer" .. key, UIParent) + holder:SetScale(scale) + holder:SetWidth(w) + holder:SetHeight(h) + holder:SetMovable(true) + holder:SetClampedToScreen(true) + holder:SetFrameStrata(PlayerFrame:GetFrameStrata()) + holder:SetFrameLevel(PlayerFrame:GetFrameLevel() + 3) + holder.unit = "player" + holder.key, holder.positions = key, positions + holder.defaultPosition = { x = x, y = y } + Position(holder, positions[key] or holder.defaultPosition) + BindHandle(holder, holder) + holder:SetScript("OnHide", function() StopDrag(this) end) + holder:SetScript("OnReceiveDrag", PlayerFrame_OnReceiveDrag) + if UnitFrame_OnEnter and UnitFrame_OnLeave and UnitFrame_OnUpdate then + holder:SetScript("OnEnter", function() + if Unlocked() then return end + UnitFrame_OnEnter() + if this.updateTooltip then this:SetScript("OnUpdate", UnitFrame_OnUpdate) end + end) + holder:SetScript("OnLeave", function() + UnitFrame_OnLeave() + this:SetScript("OnUpdate", nil) + end) + end + return holder + end + + local portrait = Holder("Portrait", 80, 80, portraitLeft, portraitTop) + local name = Holder("Name", width + 8, 23, left, top + 25) + local health = Holder("Health", width + 8, healthHeight + 6, left, top) + local mana = Holder("Power", width + 8, manaHeight + 6, left, top - healthHeight - 8) + ShaguTweaks.StandalonePlayerFrame = { portrait = portrait, name = name, health = health, power = mana } + + -- Split the native artwork before changing its source texture's anchors. + -- Preserve its UV orientation (the player's stock artwork is mirrored). + local art = PlayerFrameTexture + local path = art:GetTexture() + local u, v, unused, bottom, right = art:GetTexCoord() + local artWidth, artHeight = PlayerFrame:GetWidth(), PlayerFrame:GetHeight() + local r, g, b, a = art:GetVertexColor() + local function Crop(holder, x, y, w, h) + local texture = holder:CreateTexture(nil, "OVERLAY") + texture:SetTexture(path) + texture:SetTexCoord(u + (right-u)*x/artWidth, u + (right-u)*(x+w)/artWidth, + v + (bottom-v)*y/artHeight, v + (bottom-v)*(y+h)/artHeight) + texture:SetAllPoints(holder) + texture:SetVertexColor(r, g, b, a) + return texture + end + -- Keep the native level badge on the left. Complete the right side from + -- the clean upper-left quadrant, leaving space for the live resting icon + -- without mirroring a second level badge into the artwork. The native + -- resting/combat icon shares the original left badge's position. + portrait.art = Crop(portrait, 34, 8, 40, 72) + portrait.artRight = Crop(portrait, 74, 8, -40, 36) + portrait.artBottomRight = Crop(portrait, 74, 44, -40, -36) + local function Piece(texture, x, y, height) + texture:ClearAllPoints() + texture:SetPoint("TOPLEFT", portrait, "TOPLEFT", x, y) + texture:SetWidth(40) + texture:SetHeight(height) + end + Piece(portrait.art, 0, 0, 72) + Piece(portrait.artRight, 40, 0, 36) + Piece(portrait.artBottomRight, 40, -36, 36) + + -- The native lower-left quadrant includes a dark rectangular shadow + -- beside the level badge. Use the same clean quadrant as the lower right, + -- then restore only the badge circle, not its surrounding shadow. + portrait.badge = Crop(portrait, 37, 50, 32, 32) + portrait.badge:ClearAllPoints() + portrait.badge:SetPoint("TOPLEFT", portrait, "TOPLEFT", 3, -42) + portrait.badge:SetWidth(32) + portrait.badge:SetHeight(32) + if type(portrait.badge.SetMask) == "function" then + portrait.art:SetTexCoord(u + (right-u)*34/artWidth, u + (right-u)*74/artWidth, + v + (bottom-v)*8/artHeight, v + (bottom-v)*44/artHeight) + Piece(portrait.art, 0, 0, 36) + portrait.artBottomLeft = Crop(portrait, 34, 44, 40, -36) + Piece(portrait.artBottomLeft, 0, -36, 36) + portrait.badge:SetMask("Interface\\CharacterFrame\\TempPortraitAlphaMask") + -- Keep the badge above every ring piece but below the number/state icons. + portrait.badgeOverlay = CreateFrame("Frame", nil, portrait) + portrait.badgeOverlay:SetAllPoints(portrait) + portrait.badgeOverlay:SetFrameLevel(portrait:GetFrameLevel() + 1) + portrait.badge:SetParent(portrait.badgeOverlay) + else + -- Older clients retain the original left half and its native badge. + portrait.badge:Hide() + end + + -- The combined frame texture has a portrait arc at the left ends of its + -- bars. A separate Blizzard nine-slice edge gives clean rounded corners + -- at both normal and Big Health heights, without clipping the bar fill. + local border = { + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 12, tile = true, tileSize = 8, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + } + local color = ShaguTweaks.UIBorder + for _, holder in ipairs({ name, health, mana }) do + holder:SetBackdrop(border) + holder:SetBackdropBorderColor(color and color.r or 1, color and color.g or 1, + color and color.b or 1, 1) + end + + -- Keep status/role indicators with the portrait. Their native event + -- handlers and parent visibility continue running unchanged. + local indicators = { "PlayerHitIndicator", "PlayerPVPIcon", "PlayerPVPIconHitArea", + "PlayerLeaderIcon", "PlayerMasterIcon", "PlayerRestIcon", "PlayerAttackIcon", + "PlayerAttackBackground", "PlayerStatusGlow", "PlayerFrameGroupIndicator" } + local indicatorPoints = {} + for i = 1, table.getn(indicators) do + local region = _G[indicators[i]] + if region and region:GetLeft() and region:GetTop() then + indicatorPoints[i] = { region:GetLeft() - portraitLeft, region:GetTop() - portraitTop } + end + end + -- Snapshot first: some indicators are anchored to other indicators. + for i = 1, table.getn(indicators) do + local point = indicatorPoints[i] + if point then MoveRegion(_G[indicators[i]], portrait, point[1], point[2]) end + end + MoveRegion(PlayerPortrait, portrait, 8, -4) + if _G.PlayerLevelText then + -- Put the native text above the ring and portrait, centered over the + -- native left-hand badge. The stock level-update code keeps owning it. + local levelX, levelY = 19, -58 + if PlayerLevelText.GetCenter then + local x, y = PlayerLevelText:GetCenter() + if x and y then levelX, levelY = x - portraitLeft, y - portraitTop end + end + portrait.levelOverlay = CreateFrame("Frame", nil, portrait) + portrait.levelOverlay:SetAllPoints(portrait) + portrait.levelOverlay:SetFrameLevel(portrait:GetFrameLevel() + 5) + PlayerLevelText:SetParent(portrait.levelOverlay) + PlayerLevelText:ClearAllPoints() + PlayerLevelText:SetPoint("CENTER", portrait.levelOverlay, "TOPLEFT", levelX, levelY) + PlayerLevelText:SetDrawLayer("BACKGROUND") + PlayerLevelText:SetAlpha(1) + PlayerLevelText:SetText(UnitLevel("player")) + PlayerLevelText:Show() + -- Keep the level above the ring but BELOW its native state icons. + -- Their existing Show/Hide logic then covers the number while resting + -- or in combat and reveals it again without adding another event loop. + for _, key in ipairs({ "PlayerRestIcon", "PlayerAttackIcon", "PlayerAttackBackground", "PlayerStatusGlow" }) do + local indicator = _G[key] + if indicator then indicator:SetParent(portrait.levelOverlay) end + end + end + local playerName = PlayerFrame.name or _G.PlayerName + if playerName then + playerName:SetParent(name) + playerName:ClearAllPoints() + playerName:SetPoint("CENTER", name, "CENTER", 0, 0) + end + MoveRegion(_G.PlayerFrameNameBackground, name, 4, -2) + -- Big Health deliberately hides the stock name background. Keep its + -- color source for that module, but draw a separate class strip here. + name.classBackground = name:CreateTexture(nil, "BACKGROUND") + name.classBackground:SetPoint("TOPLEFT", name, "TOPLEFT", 4, -3) + name.classBackground:SetPoint("BOTTOMRIGHT", name, "BOTTOMRIGHT", -4, 3) + local healthTexture = PlayerFrameHealthBar:GetStatusBarTexture() + name.classBackground:SetTexture(healthTexture and healthTexture:GetTexture() + or "Interface\\TargetingFrame\\UI-StatusBar") + local _, class = UnitClass("player") + local palette = ShaguTweaks.ModernClassColors or RAID_CLASS_COLORS + local classColor = palette and palette[class] + local cr, cg, cb = PlayerFrameHealthBar:GetStatusBarColor() + name.classBackground:SetVertexColor(classColor and classColor.r or cr, + classColor and classColor.g or cg, classColor and classColor.b or cb, 1) + if ShaguTweaks_config[T["Hide Player Name"]] == 1 then name:Hide() end + + local function DetachBar(bar, holder, h) + -- Vanilla's UnitFrameManaBar_Update calls UpdateManaType(GetParent()). + -- Keep that parent contract as well as the bar's own unit/events. + holder.healthbar = PlayerFrameHealthBar + holder.manabar = PlayerFrameManaBar + bar:SetParent(holder) + bar:ClearAllPoints() + bar:SetPoint("TOPLEFT", holder, "TOPLEFT", 4, -3) + bar:SetWidth(width) + bar:SetHeight(h) + BindHandle(bar, holder) + local background = bar:CreateTexture(nil, "BACKGROUND") + background:SetTexture(0, 0, 0, .5) + background:SetAllPoints(bar) + if bar.TextString then + bar.TextString:SetParent(bar) + bar.TextString:ClearAllPoints() + bar.TextString:SetPoint("CENTER", bar, "CENTER", 0, 0) + end + end + DetachBar(PlayerFrameHealthBar, health, healthHeight) + DetachBar(PlayerFrameManaBar, mana, manaHeight) + if _G.PlayerFrameAlternatePowerBar then + local alternate = _G.PlayerFrameAlternatePowerBar + alternate:SetParent(mana) + alternate:ClearAllPoints() + alternate:SetPoint("TOPLEFT", mana, "BOTTOMLEFT", 4, -2) + alternate:SetWidth(width) + end + + -- Hide the now-redundant shared backing, without hiding PlayerFrame: its + -- native events, pet frame, menu and status scripts still need to run. + local function Suppress(region) + if not region then return end + local setAlpha = region.SetAlpha + setAlpha(region, 0) + ShaguTweaks.hooksecurefunc(region, "SetAlpha", function(object) setAlpha(object, 0) end) + end + Suppress(PlayerFrameTexture) + Suppress(_G.PlayerFrameBackground) + Suppress(_G.PlayerFrameNameBackground) + PlayerFrame:EnableMouse(false) + + if PlayerStatusTexture then + MoveRegion(PlayerStatusTexture, portrait, 1, 0) + local function CropStatus() + PlayerStatusTexture:SetTexCoord(0, 72/256, 0, 66/128) + PlayerStatusTexture:SetWidth(72) + PlayerStatusTexture:SetHeight(66) + end + if type(PlayerStatusTexture.SetMask) == "function" then + PlayerStatusTexture:SetMask("Interface\\CharacterFrame\\TempPortraitAlphaMask") + end + ShaguTweaks.hooksecurefunc("PlayerFrame_UpdateStatus", CropStatus) + CropStatus() + end + end + + -- Wait until all modules and their one-shot OnUpdates have initialized. + -- This also captures the player's restored layout-cache position. + local ready = CreateFrame("Frame") + ready:RegisterEvent("PLAYER_ENTERING_WORLD") + ready:SetScript("OnEvent", function() + this:UnregisterAllEvents() + this:SetScript("OnUpdate", function() + this:SetScript("OnUpdate", nil) + this:Hide() + if API.timer then + API.Defer(Setup) + else + -- A second frame lets the other modules' one-shot updates finish. + this:SetScript("OnUpdate", function() + this:SetScript("OnUpdate", nil) + Setup() + end) + this:Show() + end + end) + end) +end diff --git a/tests/README.md b/tests/README.md index 23a206d..edf9887 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,6 +7,15 @@ lua5.0 tests/test_ui_rarity.lua . lua5.0 tests/test_ui_slot_edges.lua . lua5.0 tests/test_ui_palette.lua . lua5.0 tests/test_options_search.lua . +lua5.0 tests/test_player_frame_options.lua . +lua5.0 tests/test_standalone_player.lua . +lua5.0 tests/test_actionbar_movers.lua . +lua5.0 tests/test_golden_unit_names.lua . +lua5.0 tests/test_quest_parchment.lua . +lua5.0 tests/test_castbar_mover.lua . +lua5.0 tests/test_bighealth_split.lua . +lua5.0 tests/test_loss_of_control_style.lua . +lua5.0 tests/test_health_text_alignment.lua . ``` These checks execute private helpers extracted from the production source with @@ -21,3 +30,17 @@ reuse, and preservation/application of unsaved settings. They cannot validate client rendering, actual event order, DLL behavior, or FPS. In-game testing remains necessary after changes to these paths. + +Player-frame checks cover portrait-only hiding, modern name textures, retention +of native bar objects/events/text, enlarged power sizing, independent positions, +drag/reset and clicks, and initialization without idle polling. The standalone +layout's artwork crops, tooltip layering, native resting/combat animations, +scale changes and interactions with other addons must still be verified in game. + +Action-bar checks cover the Reduced Actionbar prerequisite, automatic movement integration, outline-only dragging, +dependent bars, saved anchors, native relayout, reset, hidden progress bars and +mouseover bars. Golden name-header checks cover the opaque texture, active +palette, font/text updates and switching between NPC and player targets. Quest/gossip checks cover native parchment crops, tile dimensions, idempotence and removal of the stationery overlay. Actual action +execution, paging, stances, pet transitions and title-strip rendering need game QA. + +Cast-bar checks cover initialization despite an old disabled flag, checkbox removal, saved position protection and reset. diff --git a/tests/test_actionbar_movers.lua b/tests/test_actionbar_movers.lua new file mode 100644 index 0000000..2c038ed --- /dev/null +++ b/tests/test_actionbar_movers.lua @@ -0,0 +1,238 @@ +local root=(arg[1] or '.') .. '/' +local function Check(reduced,actionOption,nativeEvents,timer) + local progressOption=actionOption + local modules,frames,pending={},{},{} + local methods={} + local noop=function() end + local function new(kind,name,parent) + local frame=setmetatable({name=name,parent=parent,scripts={},visible=true,width=200,height=36,scale=1,points={}},{__index=function(t,k) if methods[k] then return methods[k] elseif k~='ShaguTweaksMouseoverController' and string.find(k,'^[A-Z]') then return noop end end}) + table.insert(frames,frame);if name then _G[name]=frame end;return frame + end + function methods:GetName() return self.name end + function methods:GetParent() return self.parent end + function methods:SetPoint(p,relative,rp,x,y) self.points={{p,relative,rp,x or 0,y or 0}} end + function methods:ClearAllPoints() self.points={} end + function methods:GetPoint(i) local p=self.points[i or 1];if p then return unpack(p) end end + function methods:GetNumPoints() return table.getn(self.points) end + function methods:GetLeft() + local p=self.points[1];if not p then return self.left or 0 end + return (p[2] and p[2]~=UIParent and p[2]:GetLeft() or 0)+p[4] + end + function methods:GetTop() + local p=self.points[1];if not p then return self.top or 0 end + return (p[2] and p[2]~=UIParent and p[2]:GetTop() or 0)+p[5] + end + function methods:GetWidth() return self.width end + function methods:GetHeight() return self.height end + function methods:SetWidth(w) self.width=w end + function methods:SetHeight(h) self.height=h end + function methods:SetScale(s) self.scale=s end + function methods:GetEffectiveScale() return self.scale end + function methods:SetAllPoints(f) self.allPoints=f end + function methods:SetScript(k,v) self.scripts[k]=v end + function methods:GetScript(k) return self.scripts[k] end + function methods:IsVisible() return self.visible and (not self.parent or self.parent:IsVisible()) end + function methods:Show() local changed=not self.visible;self.visible=true;if changed and self.scripts.OnShow then local before=this;this=self;self.scripts.OnShow();this=before end end + function methods:Hide() local changed=self.visible;self.visible=false;if changed and self.scripts.OnHide then local before=this;this=self;self.scripts.OnHide();this=before end end + function methods:StartMoving() self.moving=true end + function methods:SetMovable(value) self.movable=value end + function methods:SetUserPlaced(value) + assert(self.movable, 'Frame is not movable or resizable') + self.userPlaced=value + end + function methods:StopMovingOrSizing() self.moving=false end + function methods:CreateFontString() return new('FontString',nil,self) end + function methods:SetText(t) self.text=t end + CreateFrame=new + UIParent=new('Frame','UIParent') + local names={'MainMenuBar','MultiBarBottomLeft','MultiBarBottomRight','MultiBarRight','MultiBarLeft','PetActionBarFrame','ShapeshiftBarFrame','MainMenuExpBar','ReputationWatchBar'} + for i,name in ipairs(names) do new('Frame',name,UIParent):SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',10*i,100+i*20) end + MainMenuExpBar:SetPoint('TOPLEFT',MainMenuBar,'TOPLEFT',0,20) + ReputationWatchBar:SetPoint('TOPLEFT',MainMenuExpBar,'TOPLEFT',0,8) + MultiBarBottomLeft:SetPoint('TOPLEFT',MainMenuExpBar,'TOPLEFT',0,30) + MultiBarBottomRight:SetPoint('TOPLEFT',MultiBarBottomLeft,'TOPLEFT',0,40) + local nativeClick=function() end + ActionButton1=new('Button','ActionButton1',MainMenuBar);ActionButton1:SetScript('OnClick',nativeClick) + ActionButton12=new('Button','ActionButton12',MainMenuBar) + PetActionButton1=new('Button','PetActionButton1',PetActionBarFrame) + PetActionButton10=new('Button','PetActionButton10',PetActionBarFrame) + BonusActionBarFrame=new('Frame','BonusActionBarFrame',MainMenuBar);BonusActionBarFrame:Hide() + MainMenuExpBar:Hide();ReputationWatchBar:Hide() + MultiBarRight:Hide();MultiBarRight.ShaguTweaksMouseoverController={enabled=true} + local originalMainSetPoint=MainMenuBar.SetPoint + local keys=false + ShaguTweaks={mods=modules,T=setmetatable({},{__index=function(t,k) return k end}),API={timer=timer,modifierstate=nativeEvents}} + function ShaguTweaks.GetGlobalEnv() return _G end + function ShaguTweaks:register(m) modules[m.title]=m;return m end + ShaguTweaks.API.IsShiftKeyDown=function() return keys end + ShaguTweaks.API.IsControlKeyDown=ShaguTweaks.API.IsShiftKeyDown + ShaguTweaks.API.IsAltKeyDown=ShaguTweaks.API.IsShiftKeyDown + function ShaguTweaks.API.Defer(callback) table.insert(pending,callback) end + function ShaguTweaks.HookScript(frame,event,callback) + local original=frame:GetScript(event) + frame:SetScript(event,function() if original then original() end;callback() end) + end + local layouts=0 + function UIParent_ManageFramePositions() + layouts=layouts+1 + MainMenuBar:ClearAllPoints();MainMenuBar:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',10,120) + MultiBarBottomLeft:ClearAllPoints();MultiBarBottomLeft:SetPoint('TOPLEFT',MainMenuExpBar,'TOPLEFT',0,30) + end + function ReputationWatchBar_Update() + ReputationWatchBar:ClearAllPoints();ReputationWatchBar:SetPoint('TOPLEFT',MainMenuExpBar,'TOPLEFT',0,8) + end + local config={['Reduced Actionbar Size']=reduced and 1 or 0,ActionBarPositions={}} + ShaguTweaks_config=config + dofile(root..'mods/actionbar-move.lua') + local baseline=table.getn(frames) + assert(not modules['Movable Action Bars']) + if actionOption then ShaguTweaks.EnableActionBarMovers() end + assert(not modules['Movable XP & Reputation Bars']) + if not reduced or (not actionOption and not progressOption) then + assert(table.getn(frames)==baseline and MainMenuBar.SetPoint==originalMainSetPoint) + return + end + local ready=frames[table.getn(frames)] + local function fire(frame,script,button) + local priorThis,priorArg=this,arg1;this,arg1=frame,button + frame:GetScript(script)();this,arg1=priorThis,priorArg + end + fire(ready,'OnEvent');fire(ready,'OnUpdate') + if timer then pending[1]() end + local state=ShaguTweaks.ActionBarMovers + assert(state and table.getn(state.states)==((actionOption and reduced) and 7 or 0)+(progressOption and 2 or 0)) + -- Snap to a neighbour's original relative placement; leave distant drops + -- and hidden targets alone. Coordinates are compared in UIParent units. + local snapTarget=new('Frame',nil,UIParent) + snapTarget:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',100,200) + local snapState={frame=MainMenuBar,snapOffsets={{target={frame=snapTarget},x=4,y=42}}} + local sx,sy=state.Snap(snapState,109,235) + assert(sx==104 and sy==242) + sx,sy=state.Snap(snapState,130,235);assert(sx==130 and sy==235) + snapTarget:Hide();sx,sy=state.Snap(snapState,109,235);assert(sx==109 and sy==235) + snapTarget:Show();snapTarget:SetScale(.5);MainMenuBar:SetScale(.5) + sx,sy=state.Snap(snapState,110,280);assert(sx==108 and sy==284) + MainMenuBar:SetScale(1) + snapTarget:SetScale(1) + local candidate=snapState.snapOffsets[1].target + local oldLevel,oldWatched,oldStatus=UnitLevel,GetWatchedFactionInfo,ReputationWatchStatusBar + candidate.name='MainMenuExpBar' + UnitLevel=function() return 60 end + sx,sy=state.Snap(snapState,109,235);assert(sx==109 and sy==235) + UnitLevel=function() return 40 end + sx,sy=state.Snap(snapState,109,235);assert(sx==104 and sy==242) + candidate.name='ReputationWatchBar' + GetWatchedFactionInfo=function() return nil end + ReputationWatchStatusBar=new('Frame',nil,snapTarget) + sx,sy=state.Snap(snapState,109,235);assert(sx==109 and sy==235) + GetWatchedFactionInfo=function() return 'Stormwind' end + sx,sy=state.Snap(snapState,109,235);assert(sx==104 and sy==242) + ReputationWatchStatusBar:Hide() + sx,sy=state.Snap(snapState,109,235);assert(sx==109 and sy==235) + ReputationWatchStatusBar:Show() + snapTarget.GetAlpha=function() return 0 end + sx,sy=state.Snap(snapState,109,235);assert(sx==109 and sy==235) + UnitLevel,GetWatchedFactionInfo,ReputationWatchStatusBar=oldLevel,oldWatched,oldStatus + assert(table.getn(state.states[1].snapOffsets)>0) + -- Native main-frame top sits 40 units above its buttons. Snapping must + -- leave six units between visible rows, not preserve that artwork gap. + local lower=new('Frame',nil,UIParent);lower:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',100,200) + local upper=new('Frame',nil,UIParent);upper:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',100,300) + local lowerButton=new('Button','SnapLower',lower) + lowerButton.GetLeft=function() return 110 end;lowerButton.GetTop=function() return 160 end + lowerButton.GetRight=function() return 210 end;lowerButton.GetBottom=function() return 124 end + local upperButton=new('Button','SnapUpper',upper) + upperButton.GetLeft=function() return 110 end;upperButton.GetTop=function() return 300 end + upperButton.GetRight=function() return 210 end;upperButton.GetBottom=function() return 264 end + local lowerState={name='MainMenuBar',frame=lower,first='SnapLower',last='SnapLower',snapOffsets={}} + local upperState={name='MultiBarBottomLeft',frame=upper,first='SnapUpper',last='SnapUpper',snapOffsets={}} + lowerState.snapOffsets={{target=upperState,x=0,y=-80}} -- incorrect legacy gap + local allStates=state.states;state.states={lowerState,upperState} + sx,sy=state.Snap(lowerState,103,295);assert(sx==100 and sy==298) + assert((264-(sy-40))==6) + sx,sy=state.Snap(lowerState,100,220);assert(sy==220) -- legacy gap cannot attract + state.states=allStates + assert(not ready:GetScript('OnUpdate')) + if nativeEvents then assert(not state.events:GetScript('OnUpdate')) end + assert(ActionButton1:GetScript('OnClick')==nativeClick) + local target=state.states[1] + assert(not target.marker.visible) + keys=true + fire(state.events,'OnEvent') + assert(target.marker.visible) + local oldX,oldY=target.frame:GetLeft(),target.frame:GetTop() + local oldXPX,oldRepX=MainMenuExpBar:GetLeft(),ReputationWatchBar:GetLeft() + -- Model a compact visible row offset inside a larger native artwork frame. + target.marker:ClearAllPoints();target.marker:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',oldX+25,oldY-18) + target.marker:SetWidth(120);target.marker:SetHeight(32) + ActionButton1:ClearAllPoints();ActionButton1:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',oldX+27,oldY-20) + ActionButton12.GetRight=function() return oldX+143 end + ActionButton12.GetBottom=function() return oldY-48 end + fire(target.marker,'OnDragStart') + assert(state.dragging==target and state.outline.moving) + assert(state.outline:GetWidth()==120 and state.outline:GetHeight()==32) + assert(state.outline:GetLeft()==oldX+25 and state.outline:GetTop()==oldY-18) + assert(target.frame:GetLeft()==oldX and target.frame:GetTop()==oldY) -- drag only the empty outline + state.outline:ClearAllPoints();state.outline:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',355,422) + keys=false;fire(state.events,'OnEvent') + assert(not state.dragging and not state.outline.visible and not state.outline.moving) + assert(target.frame:GetLeft()==330 and target.frame:GetTop()==440) + assert(config.ActionBarPositions[target.name][1]==330) + target.frame:ClearAllPoints();target.frame:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',1,2) + assert(target.frame:GetLeft()==330 and target.frame:GetTop()==440) -- native layout cannot erase a saved anchor + target.clear(target.frame);target.point(target.frame,'TOPLEFT',UIParent,'BOTTOMLEFT',0,0) + local savedEvent=event;event='PLAYER_ENTERING_WORLD';fire(state.events,'OnEvent');event=savedEvent + assert(target.frame:GetLeft()==330 and target.frame:GetTop()==440) + keys=true;fire(state.events,'OnEvent');fire(target.marker,'OnClick','RightButton') + assert(not config.ActionBarPositions[target.name] and layouts>0) + if progressOption then + local count=0 + for _,s in ipairs(state.states) do if s.progress then count=count+1;assert(s.marker.visible) end end + assert(count==2) -- markers are available even at max level/no watched reputation + end + if actionOption and reduced then + for _,s in ipairs(state.states) do if s.name=='MultiBarRight' then assert(s.marker.visible) end end + if progressOption then assert(config.ActionBarPositions.MainMenuExpBar and MainMenuExpBar:GetLeft()==oldXPX and ReputationWatchBar:GetLeft()==oldRepX) end + local pet + for _,s in ipairs(state.states) do if s.name=='PetActionBarFrame' then pet=s end end + assert(pet and pet.marker.visible) + assert(pet.marker:GetPoint(1)=='BOTTOMRIGHT') -- mock retains the last anchor + PetActionBarFrame:Hide();assert(not pet.marker.visible) + PetActionBarFrame:Show();assert(pet.marker.visible) + pet.marker:ClearAllPoints();pet.marker:SetPoint('TOPLEFT',PetActionBarFrame,'TOPLEFT',0,0) + local petX,petY=PetActionBarFrame:GetLeft(),PetActionBarFrame:GetTop() + PetActionButton1:ClearAllPoints();PetActionButton1:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',petX+12,petY-8) + PetActionButton10.GetRight=function() return petX+168 end + PetActionButton10.GetBottom=function() return petY-38 end + fire(pet.marker,'OnDragStart') + assert(state.outline:GetWidth()==160 and state.outline:GetHeight()==34) + assert(state.outline:GetLeft()==petX+10 and state.outline:GetTop()==petY-6) + state.outline:ClearAllPoints();state.outline:SetPoint('TOPLEFT',UIParent,'BOTTOMLEFT',510,594) + fire(pet.marker,'OnDragStop') + assert(PetActionBarFrame:GetLeft()==500 and PetActionBarFrame:GetTop()==600) + assert(config.ActionBarPositions.PetActionBarFrame[1]==500) + fire(pet.marker,'OnClick','RightButton') + assert(not config.ActionBarPositions.PetActionBarFrame) + end +end +Check(false,true,true,true) +Check(true,false,true,true) +Check(true,true,true,true) +Check(true,true,false,false) +print('Action movers: reduced-layout prerequisite, single combined option, outline drag, saved-anchor protection, dependent pinning, reset, hidden progress/mouseover markers and idle setup passed') +-- A detached progress bar must not pull Reduced Actionbar's automatic stack +-- along with it. Execute that module's actual layout callback in isolation. +local f=assert(io.open(root..'mods/reduced-actionbar.lua','r'));local reducedSource=f:read('*a');f:close();assert(loadstring(reducedSource)) +local a=assert(string.find(reducedSource,' local function UpdateActionBarPositions()',1,true)) +local b=assert(string.find(reducedSource,' hooksecurefunc("UIParent_ManageFramePositions"',a,true)) +local widgets={'MainMenuExpBar','ReputationWatchBar','MultiBarBottomLeft','MultiBarBottomRight','PetActionBarFrame','ShapeshiftBarFrame','MainMenuBarArtFrame','MainMenuBar','ActionButton1','CastingBarFrame'} +for _,name in ipairs(widgets) do + _G[name]={visible=true,IsVisible=function(self) return self.visible end,ClearAllPoints=function(self) self.point=nil end,SetPoint=function(self,...) self.point=arg end} +end +local moved={} +ShaguTweaks.ActionBarMovers={IsMoved=function(name) return moved[name] end} +local relayout=assert(loadstring(string.sub(reducedSource,a,b-1)..'return UpdateActionBarPositions'))() +relayout();assert(MultiBarBottomLeft.point[2]==ReputationWatchBar) +moved.ReputationWatchBar=true;relayout();assert(MultiBarBottomLeft.point[2]==MainMenuExpBar) +moved.MainMenuExpBar=true;relayout();assert(MultiBarBottomLeft.point[2]==MainMenuBar) +print('Reduced Actionbar: detached progress bars no longer become automatic action-stack anchors') diff --git a/tests/test_bighealth_split.lua b/tests/test_bighealth_split.lua new file mode 100644 index 0000000..20b80ea --- /dev/null +++ b/tests/test_bighealth_split.lua @@ -0,0 +1,50 @@ +local root=(arg[1] or '.')..'/' +local function Check(player,target) + local mods,frames,hooks={},{},{} + local methods={} + local function obj() return setmetatable({height=12},{__index=methods}) end + function methods:SetTexture(v) self.texture=v end + function methods:SetHeight(v) self.height=v end + function methods:SetPoint() end + function methods:SetVertexColor() end + function methods:GetVertexColor() return .2,.5,.8,1 end + function methods:GetTexCoord() return 0,0,0,1,1,0,1,1 end + function methods:GetWidth() return 256 end + function methods:SetWidth() end + function methods:SetTexCoord() end + function methods:Hide() self.hidden=true end + function methods:SetScript(k,v) self[k]=v end + function methods:RegisterEvent() end + function methods:UnregisterAllEvents() end + function methods:SetStatusBarColor() end + for _,name in ipairs({'PlayerFrameTexture','PlayerStatusTexture','PlayerFrameHealthBar','PlayerFrameNameBackground','TargetFrameTexture','TargetFrameHealthBar','TargetFrameNameBackground'}) do _G[name]=obj() end + ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end}),UITint={r=.5,g=.5,b=.5,a=1}} + function ShaguTweaks:register(m) mods[m.title]=m;return m end + hooksecurefunc=function(object,key,fn) if type(object)=='string' then hooks[object]=key end end + GetAddOnInfo=function() return nil end + IsResting=function() return false end + UnitClassification=function() return 'elite' end + TargetFrame_CheckFaction=function() end + CreateFrame=function() local f=obj();table.insert(frames,f);return f end + dofile(root..'mods/unitframes-bighealth.lua') + assert(not mods['Unit Frame Big Health']) + if player then mods['Unit Frame Big Health for Player']:enable() end + if target then mods['Unit Frame Big Health for Target']:enable() end + for _,f in ipairs(frames) do if f.OnUpdate then this=f;f.OnUpdate() end end + assert(PlayerFrameHealthBar.height==(player and 30 or 12)) + assert(TargetFrameHealthBar.height==(target and 30 or 12)) + assert((PlayerFrameNameBackground.hidden or false)==player) + assert((TargetFrameNameBackground.hidden or false)==target) + assert((hooks.PlayerFrame_UpdateStatus~=nil)==player) + assert((hooks.TargetFrame_CheckClassification~=nil)==target) +end +Check(false,false);Check(true,false);Check(false,true);Check(true,true) +local f=assert(io.open(root..'main.lua'));local s=f:read('*a');f:close();assert(loadstring(s)) +local a=assert(string.find(s,' local oldBigHealth =',1,true)) +local b=assert(string.find(s,' -- initialize overwrites',a,true)) +local migrate=assert(loadstring(string.sub(s,a,b-1))) +ShaguTweaks_config={['Unit Frame Big Health']=1};migrate() +assert(ShaguTweaks_config['Unit Frame Big Health for Player']==1 and ShaguTweaks_config['Unit Frame Big Health for Target']==1) +ShaguTweaks_config={['Unit Frame Big Health']=1,['Unit Frame Big Health for Player']=0};migrate() +assert(ShaguTweaks_config['Unit Frame Big Health for Player']==0 and not ShaguTweaks_config['Unit Frame Big Health']) +print('Big Health: all four option combinations, independent artwork/hooks and old-setting migration passed') diff --git a/tests/test_castbar_mover.lua b/tests/test_castbar_mover.lua new file mode 100644 index 0000000..a653bdc --- /dev/null +++ b/tests/test_castbar_mover.lua @@ -0,0 +1,30 @@ +local root=(arg[1] or '.')..'/' +local methods={} +local function object() + return setmetatable({scripts={}}, {__index=function(t,k) return methods[k] or function() end end}) +end +function methods:SetScript(k,v) self.scripts[k]=v end +function methods:CreateTexture() return object() end +function methods:CreateFontString() return object() end +function methods:SetPoint(...) self.point=arg end +function methods:ClearAllPoints() self.point=nil end +UIParent=object();CastingBarFrame=object() +CreateFrame=function(_,name) local f=object();if name then _G[name]=f end;return f end +ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end}),GetGlobalEnv=function() return _G end} +function ShaguTweaks:register() error('Cast-bar checkbox must not be registered') end +ShaguTweaks_config={['Movable Cast Bar']=0,CastBarPosition={123,234}} +local f=assert(io.open(root..'main.lua'));local source=f:read('*a');f:close();assert(loadstring(source)) +local a=assert(string.find(source,' if expansion == "vanilla" and ShaguTweaks.EnableCastBarMover then',1,true)) +local b=assert(string.find(source,'\n end',a,true)) +local initialize=assert(loadstring('local expansion="vanilla"\n'..string.sub(source,a,b+5))) +dofile(root..'mods/castbar-move.lua') +initialize() +assert(ShaguTweaksCastBarMover) +assert(CastingBarFrame.point[4]==123 and CastingBarFrame.point[5]==234) +CastingBarFrame:SetPoint('BOTTOM',UIParent,'BOTTOM',1,2) +assert(CastingBarFrame.point[4]==123) +local mover=ShaguTweaksCastBarMover +initialize();assert(ShaguTweaksCastBarMover==mover) +arg1='RightButton';mover.scripts.OnMouseUp() +assert(not ShaguTweaks_config.CastBarPosition and CastingBarFrame.point[5]==60) +print('Always-on cast-bar mover: old disabled flag ignored, no checkbox, saved position, idempotent initialization and reset passed') diff --git a/tests/test_golden_unit_names.lua b/tests/test_golden_unit_names.lua new file mode 100644 index 0000000..c669096 --- /dev/null +++ b/tests/test_golden_unit_names.lua @@ -0,0 +1,82 @@ +local root=(arg[1] or '.') .. '/' +local f=assert(io.open(root..'mods/dark-ui-elements.lua','r'));local source=f:read('*a');f:close();assert(loadstring(source)) +local made=0 +local methods={} +local function object() + return setmetatable({level=7},{__index=methods}) +end +function methods:SetWidth(v) self.width=v end +function methods:SetHeight(v) self.height=v end +function methods:SetFrameLevel(v) self.level=v end +function methods:GetFrameLevel() return self.level end +function methods:SetPoint(...) self.point=arg end +function methods:SetTexture(v) self.path=v end +function methods:SetTexCoord(...) self.coords=arg end +function methods:SetAllPoints(v) self.allPoints=v end +function methods:SetVertexColor(...) self.color=arg end +function methods:SetAlpha(v) self.alpha=v end +function methods:GetAlpha() return self.alpha or 1 end +function methods:Show() self.visible=true end +function methods:Hide() self.visible=false end +function methods:RegisterEvent() end +function methods:SetScript(event,fn) self[event]=fn end +function methods:SetFontObject(v) self.font=v end +function methods:GetFontObject() return 'DefaultGoldFont' end +function methods:GetJustifyH() return 'CENTER' end +function methods:SetJustifyH(v) self.justify=v end +function methods:SetText(v) self.text=v end +function methods:GetText() return self.text end +function methods:CreateTexture() return object() end +function methods:CreateFontString() return object() end +function CreateFrame() made=made+1;return object() end +local a=assert(string.find(source,'local function RedrawTitle',1,true)) +local b=assert(string.find(source,'local function ShadeTitleBar',a,true)) +local c=assert(string.find(source,'local function GoldenUnitNameBar',b,true)) +local d=assert(string.find(source,'golden.enable =',c,true)) +local prefix='local tint={r=1,g=.78,b=.4}; local golden={config={["goldmode.bar"]={r=.8,g=.63,b=.62}}}; ' +local apply=assert(loadstring(prefix..string.sub(source,a,b-1)..string.sub(source,c,d-1)..'return GoldenUnitNameBar'))() +local parent=object();local title=object();title.text='First target' +function title:GetParent() return parent end +function title:GetPoint() return 'CENTER',parent,'CENTER',0,10 end +local target={name=title} +local isPlayer,exists=false,true +function UnitExists() return exists end +function UnitIsPlayer() return isPlayer end +ShaguTweaks={HookScript=function(frame,event,fn) frame[event]=fn end} +apply(target) +local bar=target.stGoldenNameBar +assert(bar and bar.width==119 and bar.height==14 and bar.level>parent.level) +assert(bar.texture.path=='Interface\\PaperDollInfoFrame\\UI-Character-General-TopLeft') +assert(bar.texture.color[4]==1 and math.abs(bar.texture.color[2]-.78*.63)<.00001) +assert(bar.title.font=='DefaultGoldFont' and title.alpha==0 and bar.title.text=='First target') +title:SetText('Next target');assert(bar.title.text=='Next target') +local count=made;apply(target);assert(made==count) +assert(bar.visible) +isPlayer=true;bar.OnEvent();assert(not bar.visible and title.alpha==1) +title:SetText('Player target');assert(title:GetText()=='Player target') +isPlayer=false;bar.OnEvent();assert(bar.visible and title.alpha==0) +exists=false;bar.OnEvent();assert(not bar.visible and title.alpha==1) +exists=true;target.OnShow();assert(bar.visible and title.alpha==0) +-- Exercise actual enable gating while stubbing only unrelated window work. +T=setmetatable({},{__index=function(t,k) return k end}) +golden={config={['goldmode.color']={},['goldmode.border']={},['goldmode.bar']={}}} +ShaguTweaks={};PlayerFrame={};TargetFrame={} +local seen,applied={},0 +function Apply() applied=applied+1 end +function ShadeTitleBars() end +function RestoreMailParchment() end +function GoldenUnitNameBar(frame) seen[frame]=true end +assert(loadstring(string.sub(source,d)))() +ShaguTweaks_config={['Standalone Player Frame']=1} +golden:enable();assert(seen[TargetFrame] and not seen[PlayerFrame]) +ShaguTweaks_config={};seen={} +golden:enable();assert(seen[TargetFrame] and not seen[PlayerFrame]) +ShaguTweaks_config={['Unit Frame Big Health for Player']=1};seen={} +golden:enable();assert(seen[TargetFrame] and not seen[PlayerFrame]) +ShaguTweaks_config={['Unit Frame Big Health for Target']=1};seen={} +golden:enable();assert(not next(seen)) +ShaguTweaks_config={['Unit Frame Big Health for Player']=1,['Unit Frame Big Health for Target']=1};seen={} +golden:enable();assert(not next(seen)) +ShaguTweaks_config={['Darkened UI']=1};seen={} +golden:enable();assert(not next(seen) and applied==5) +print('Golden name headers: opaque native title texture, palette match, live text, font retention, idempotence, standalone exemption and Darkened UI precedence passed') diff --git a/tests/test_health_text_alignment.lua b/tests/test_health_text_alignment.lua new file mode 100644 index 0000000..8aec674 --- /dev/null +++ b/tests/test_health_text_alignment.lua @@ -0,0 +1,28 @@ +local root=(arg[1] or '.')..'/' +local f=assert(io.open(root..'mods/health-numbers.lua'));local s=f:read('*a');f:close();assert(loadstring(s)) +local a=assert(string.find(s,' local function AnchorHealth',1,true)) +local b=assert(string.find(s,' local function MatchNameStyle',a,true)) +local anchor=assert(loadstring(string.sub(s,a,b-1)..'return AnchorHealth'))() +a=assert(string.find(s,' local playerHealthY =',1,true)) +b=assert(string.find(s,' AnchorHealth(self.valueTexts.playerHealth',a,true)) +local y=assert(loadstring(string.sub(s,a,b-1)..'return playerHealthY,targetHealthY')) +T=setmetatable({},{__index=function(t,k) return k end}) +local height=30 +PlayerFrameHealthBar={GetHeight=function() return height end} +TargetFrameHealthBar={GetHeight=function() return 30 end} +local function text() + return {ClearAllPoints=function() end,SetJustifyH=function(self,v) self.justify=v end, + SetPoint=function(self,...) self.point=arg end} +end +for _,standalone in ipairs({0,1}) do + ShaguTweaks_config={['Standalone Player Frame']=standalone} + for _,h in ipairs({12,30}) do + height=h;local py,ty=y() + assert(py==((standalone==1 or h==12) and .5 or -7) and ty==-7) + local value=text();value.percentText=text() + anchor(value,PlayerFrameHealthBar,py) + assert(value.point[1]=='RIGHT' and value.point[4]==-4 and value.point[5]==py) + assert(value.percentText.point[1]=='LEFT' and value.percentText.point[4]==4 and value.percentText.point[5]==py) + end +end +print('Standalone health numbers: vertically centered, left/right insets retained, grouped/target offsets unchanged') diff --git a/tests/test_loss_of_control_style.lua b/tests/test_loss_of_control_style.lua new file mode 100644 index 0000000..2fa7787 --- /dev/null +++ b/tests/test_loss_of_control_style.lua @@ -0,0 +1,54 @@ +local root=(arg[1] or '.')..'/' +local made,frames=0,{} +local methods={} +local function new(name) + made=made+1 + local f=setmetatable({scripts={},events={},shown=true},{__index=function(t,k) return methods[k] or function() end end}) + if name then _G[name]=f end + table.insert(frames,f);return f +end +function methods:SetBackdrop(v) self.backdrop=v end +function methods:SetBackdropBorderColor(...) self.rim=arg end +function methods:SetVertexColor(...) self.tint=arg end +function methods:SetTexture(v) self.texture=v end +function methods:SetWidth(v) self.width=v end +function methods:SetFontObject(v) self.font=v end +function methods:SetText(v) self.text=v end +function methods:SetScript(k,v) self.scripts[k]=v end +function methods:RegisterEvent(k) self.events[k]=true end +function methods:UnregisterAllEvents() self.events={} end +function methods:IsShown() return self.shown end +function methods:Show() self.shown=true end +function methods:Hide() self.shown=false end +function methods:CreateTexture() return new() end +function methods:CreateFontString() return new() end +CreateFrame=function(_,name) return new(name) end +UIParent=new();GameFontNormalLarge='NormalLarge';GameFontHighlightSmall='HighlightSmall' +SlashCmdList={};ShaguTweaks_config={} +local module +ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end}),GetGlobalEnv=function() return _G end} +function ShaguTweaks:register(m) module=m;return m end +dofile(root..'mods/loss-of-control.lua') +local baseline=made;module:enable();assert(made==baseline) -- unavailable API stays dormant +local data={locType='STUN',timeRemaining=4,duration=8,iconTexture='stun',displayText='Hammer of Justice'} +C_LossOfControl={GetActiveLossOfControlDataCount=function() return data and 1 or 0 end,GetActiveLossOfControlData=function() return data end} +module:enable() +local f=ShaguTweaksLossOfControl +assert(f.backdrop.edgeFile=='Interface\\Tooltips\\UI-Tooltip-Border') +assert(f.border.backdrop.edgeFile==f.backdrop.edgeFile) +assert(f.rim[1]==.6 and f.border.rim[1]==.6 and f.icon.tint[1]==1) +assert(f.bar.texture=='Interface\\TargetingFrame\\UI-StatusBar' and f.bar.width==107) +assert(f.title.font==GameFontNormalLarge and f.detail.text=='Hammer of Justice') +local driver +for _,frame in ipairs(frames) do if frame.events.LOSS_OF_CONTROL_UPDATE then driver=frame end end +local count=made +for _,color in ipairs({{r=.2,g=.2,b=.2,a=1},{r=.8,g=.62,b=.32,a=1}}) do + ShaguTweaks.UIBorder=color + event='LOSS_OF_CONTROL_UPDATE';driver.scripts.OnEvent() + assert(f.rim[1]==color.r and f.rim[2]==color.g and f.border.rim[3]==color.b) + assert(f.icon.tint[1]==1 and f.bar.tint[1]==1 and f.bar.tint[2]==.2) + assert(made==count) -- palette application never allocates additional widgets +end +data=nil;driver.scripts.OnEvent();assert(not f.shown) +module:disable();assert(not next(driver.events)) +print('Loss of Control: tooltip rims, classic fonts/status fill, default/dark/gold palettes, native icon color, API guard and hide/disable passed') diff --git a/tests/test_player_frame_options.lua b/tests/test_player_frame_options.lua new file mode 100644 index 0000000..d50a4f7 --- /dev/null +++ b/tests/test_player_frame_options.lua @@ -0,0 +1,41 @@ +local root=(arg[1] or '.') .. '/' +assert(loadfile(root..'mods/unitframes-classcolor.lua')) +local modules={} +ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end})} +function ShaguTweaks.GetGlobalEnv() return _G end +function ShaguTweaks:register(module) modules[module.title]=module; return module end +function ShaguTweaks.hooksecurefunc(object,name,callback) + if type(object)~='table' then return end + local original=object[name]; object[name]=function(self,a,b,c,d) original(self,a,b,c,d); callback(self,a,b,c,d) end +end +PlayerPortrait={alpha=1}; function PlayerPortrait:SetAlpha(v) self.alpha=v end +local health={visible=true}; local mana={visible=true} +dofile(root..'mods/unitframes-hide-player-portrait.lua') +modules['Hide Player Portrait']:enable() +assert(PlayerPortrait.alpha==0 and health.visible and mana.visible) +PlayerPortrait:SetAlpha(1); assert(PlayerPortrait.alpha==0) +MAX_PARTY_MEMBERS=0 +local deferred +function CreateFrame() return {SetScript=function(self,event,callback) if callback then deferred={frame=self,callback=callback} end end,Hide=function() end} end +function UnitClass() return 'Warlock','WARLOCK' end +function TargetFrame_CheckFaction() end +local function texture() + return {SetTexture=function(self,p) self.path=p end,SetVertexColor=function(self,r,g,b,a) self.r=r end} +end +PlayerFrameNameBackground=texture(); TargetFrameNameBackground=texture() +PlayerFrameHealthBar={SetStatusBarTexture=function(self,p) self.path=p end,GetStatusBarColor=function() return 1,1,1,1 end,SetStatusBarColor=function() end} +dofile(root..'mods/unitframes-classcolor.lua') +modules['Modern Class Colors']:enable() +this=deferred.frame; deferred.callback(); this=nil +local expected='Interface\\AddOns\\ShaguTweaks\\img\\BlizzardRetailBar' +assert(PlayerFrameNameBackground.path==expected and TargetFrameNameBackground.path==expected and PlayerFrameHealthBar.path==expected) +print('Lua 5.0: portrait-only alpha lock and matching modern name/health artwork passed') +-- Hiding the player name never affects the grouped player frame. +dofile(root..'mods/unitframes-hide-player-name.lua') +local nameHidden=false +ShaguTweaks.StandalonePlayerFrame={name={Hide=function() nameHidden=true end}} +ShaguTweaks_config={['Standalone Player Frame']=0} +modules['Hide Player Name']:enable();assert(not nameHidden) +ShaguTweaks_config['Standalone Player Frame']=1 +modules['Hide Player Name']:enable();assert(nameHidden) +print('Hide Player Name: standalone-only guard passed') diff --git a/tests/test_quest_parchment.lua b/tests/test_quest_parchment.lua new file mode 100644 index 0000000..bb467b2 --- /dev/null +++ b/tests/test_quest_parchment.lua @@ -0,0 +1,38 @@ +local root=(arg[1] or '.')..'/' +local f=assert(io.open(root..'mods/dark-ui-elements.lua','r')) +local source=f:read('*a');f:close();assert(loadstring(source)) +local a=assert(string.find(source,'local function RestoreQuestParchment',1,true)) +local b=assert(string.find(source,'local function DarkenFrame',a,true)) +local restore=assert(loadstring(string.sub(source,a,b-1)..'return RestoreQuestParchment'))() +local methods={} +function methods:GetTexture() return self.path end +function methods:SetTexture(v) self.path=v end +function methods:GetWidth() return self.width end +function methods:GetHeight() return self.height end +function methods:SetWidth(v) self.width=v end +function methods:SetHeight(v) self.height=v end +function methods:SetPoint(...) self.point=arg end +function methods:SetTexCoord(...) self.uv=arg end +function methods:SetVertexColor(...) self.color=arg end +local function texture(path,w,h) return setmetatable({path=path,width=w,height=h},{__index=methods}) end +for _,name in ipairs({'GossipFrameGreetingPanel','QuestFrameGreetingPanel','QuestFrameDetailPanel','QuestFrameProgressPanel','QuestFrameRewardPanel'}) do + local regions={} + for _,suffix in ipairs({'TopLeft','TopRight','BotLeft','BotRight'}) do + table.insert(regions,texture('Interface\\QuestFrame\\UI-QuestGreeting-'..suffix,string.find(suffix,'Left') and 256 or 128,256)) + end + table.insert(regions,texture('Interface\\QuestFrame\\UI-Quest-BotLeftPatch',128,64)) + local panel={GetName=function() return name end,GetRegions=function() return unpack(regions) end} + function panel:CreateTexture(_,layer) assert(layer=='ARTWORK');local t=texture();table.insert(regions,t);return t end + restore(panel);assert(table.getn(regions)==9) + local left,right=regions[1].stQuestPaper,regions[2].stQuestPaper + assert(left.width==234 and left.height==182 and left.color[1]==1) + assert(right.width==67 and right.uv[2]==67/128) + assert(regions[4].stQuestPaper.height==148) + assert(not regions[5].stQuestPaper) -- the bottom patch is not a 256px tile + restore(panel);assert(table.getn(regions)==9) +end +assert(not string.find(source,'["^GossipFrameGreetingPanel$"]',1,true)) +assert(not string.find(source,'["^QuestFrame(.+)Panel$"]',1,true)) +local f=assert(io.open(root..'mods/reduced-actionbar.lua','r'));local reduced=f:read('*a');f:close() +assert(loadstring(reduced));assert(string.find(reduced,'ShaguTweaks.EnableActionBarMovers()',1,true)) +print('Native quest/gossip parchment: four original crops, right-tile dimensions, no stationery overlay, idempotence; Reduced Actionbar enables movement') diff --git a/tests/test_standalone_player.lua b/tests/test_standalone_player.lua new file mode 100644 index 0000000..b985248 --- /dev/null +++ b/tests/test_standalone_player.lua @@ -0,0 +1,156 @@ +local root=(arg[1] or '.') .. '/' +local function Check(big,timer,hidden,hideName,maskAvailable) + local modules,frames,pending={},{},{} + local methods={} + local function noop() end + local function new(kind,name,parent) + local object=setmetatable({kind=kind or 'Frame',name=name,parent=parent,scripts={},width=119,height=12,left=106,top=550,scale=1,alpha=1,visible=true},{__index=function(t,k) if methods[k] then return methods[k] elseif string.find(k,'^[A-Z]') then return noop end end}) + if kind=='Texture' and maskAvailable==false then object.SetMask=false end + table.insert(frames,object); if name then _G[name]=object end; return object + end + function methods:GetName() return self.name end + function methods:GetObjectType() return self.kind end + function methods:SetParent(parent) self.parent=parent end + function methods:GetParent() return self.parent end + function methods:SetWidth(w) self.width=w end + function methods:SetHeight(h) self.height=h end + function methods:GetWidth() return self.width end + function methods:GetHeight() return self.height end + function methods:GetLeft() return self.left end + function methods:GetTop() return self.top end + function methods:GetCenter() return self.centerX,self.centerY end + function methods:ClearAllPoints() self.points={} end + function methods:SetPoint(p,relative,rp,x,y) self.points={p,relative,rp,x,y}; if rp=='BOTTOMLEFT' then self.left=x;self.top=y end end + function methods:SetAllPoints(relative) self.allPoints=relative end + function methods:SetScale(scale) self.scale=scale end + function methods:GetEffectiveScale() return self.scale end + function methods:SetFrameLevel(level) self.level=level end + function methods:GetFrameLevel() return self.level or 1 end + function methods:GetFrameStrata() return 'BACKGROUND' end + function methods:SetScript(script,callback) self.scripts[script]=callback end + function methods:GetScript(script) return self.scripts[script] end + function methods:CreateTexture() return new('Texture',nil,self) end + function methods:SetMask(path) self.mask=path end + function methods:SetTexture(path) self.path=path end + function methods:SetBackdrop(backdrop) self.backdrop=backdrop end + function methods:GetTexture() return self.path end + function methods:GetStatusBarTexture() return {GetTexture=function() return 'NativeHealthTexture' end} end + function methods:GetStatusBarColor() return .53,.53,.93,1 end + function methods:SetText(text) self.text=text end + function methods:SetTexCoord(a,b,c,d) self.uv={a,b,c,d} end + function methods:GetTexCoord() return 1,0,1,.78125,.09375,0,.09375,.78125 end + function methods:SetVertexColor(r,g,b,a) self.color={r,g,b,a} end + function methods:GetVertexColor() return 1,.78,.4,1 end + function methods:SetDrawLayer(layer) self.layer=layer end + function methods:SetAlpha(alpha) self.alpha=alpha end + function methods:EnableMouse(enabled) self.mouse=enabled end + function methods:StartMoving() self.moving=true end + function methods:StopMovingOrSizing() self.moving=false end + function methods:Hide() self.visible=false end + function methods:Show() self.visible=true end + CreateFrame=new + local keys=false + ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end}),API={timer=timer}} + function ShaguTweaks.GetGlobalEnv() return _G end + function ShaguTweaks:register(module) modules[module.title]=module;return module end + ShaguTweaks.API.IsShiftKeyDown=function() return keys end + ShaguTweaks.API.IsControlKeyDown=ShaguTweaks.API.IsShiftKeyDown + ShaguTweaks.API.IsAltKeyDown=ShaguTweaks.API.IsShiftKeyDown + function ShaguTweaks.API.Defer(callback) table.insert(pending,callback) end + function ShaguTweaks.hooksecurefunc(object,name,callback) + if type(object)=='string' then callback=name;name=object;object=_G end + local original=object[name] or noop + object[name]=function(a,b,c,d) original(a,b,c,d);callback(a,b,c,d) end + end + local oldThis,oldArg + local function fire(frame,script,button) + oldThis,oldArg=this,arg1;this,arg1=frame,button; local callback=frame:GetScript(script); if callback then callback() end; this,arg1=oldThis,oldArg + end + UIParent=new('Frame','UIParent');PlayerFrame=new('Button','PlayerFrame',UIParent) + PlayerFrame.width=232;PlayerFrame.height=100 + function UnitClass() return 'Warlock','WARLOCK' end + function UnitLevel() return 60 end + ShaguTweaks.ModernClassColors={WARLOCK={r=.53,g=.53,b=.93}} + PlayerLevelText=new('FontString','PlayerLevelText',PlayerFrame) + PlayerLevelText.centerX=117;PlayerLevelText.centerY=496 + PlayerFrame.name=new('FontString','PlayerName',PlayerFrame) + PlayerPortrait=new('Texture','PlayerPortrait',PlayerFrame);PlayerPortrait.alpha=hidden and 0 or 1 + PlayerRestIcon=new('Texture','PlayerRestIcon',PlayerFrame);PlayerRestIcon.width=31 + PlayerRestIcon.left=101;PlayerRestIcon.top=513 + PlayerStatusGlow=new('Frame','PlayerStatusGlow',PlayerFrame);PlayerStatusGlow.width=32 + PlayerStatusGlow.left=101;PlayerStatusGlow.top=513 + PlayerFrameTexture=new('Texture','PlayerFrameTexture',PlayerFrame);PlayerFrameTexture.path=big and 'BigHealthArt' or 'StockArt' + PlayerFrameBackground=new('Texture','PlayerFrameBackground',PlayerFrame) + PlayerFrameNameBackground=new('Texture','PlayerFrameNameBackground',PlayerFrame) + PlayerFrameHealthBar=new('StatusBar','PlayerFrameHealthBar',PlayerFrame) + PlayerFrameManaBar=new('StatusBar','PlayerFrameManaBar',PlayerFrame) + PlayerFrameHealthBar.height=big and 30 or 12 + PlayerFrameHealthBar.TextString=new('FontString',nil,PlayerFrame) + PlayerFrameManaBar.TextString=new('FontString',nil,PlayerFrame) + local healthEvent=function() end + PlayerFrameHealthBar:SetScript('OnEvent',healthEvent) + local numbers=new('FontString',nil,PlayerFrameHealthBar) + PlayerStatusTexture=new('Texture','PlayerStatusTexture',PlayerFrame) + PlayerFrameDropDown={} + PlayerFrame_OnReceiveDrag=noop + PlayerFrame_UpdateStatus=noop + local clicked,menu + function PlayerFrame_OnClick(button) clicked=button end + function SpellIsTargeting() return false end + function ToggleDropDownMenu(a,b,c,anchor) menu=anchor end + ShaguTweaks_config={['Standalone Player Frame']=1,['Hide Player Name']=hideName and 1 or 0,['Unit Frame Big Health for Player']=big and 1 or 0,StandalonePlayerFrame={Health={x=420,y=520}}} + local originalHealth,originalMana,originalPortrait=PlayerFrameHealthBar,PlayerFrameManaBar,PlayerPortrait + dofile(root..'mods/unitframes-hide-player-name.lua') + modules['Hide Player Name']:enable() + dofile(root..'mods/unitframes-standalone-player.lua') + assert(not ShaguTweaks.StandalonePlayerFrame) + modules['Standalone Player Frame']:enable() + local ready=frames[table.getn(frames)] + fire(ready,'OnEvent');fire(ready,'OnUpdate') + if timer then assert(table.getn(pending)==1);pending[1]() else fire(ready,'OnUpdate') end + local layout=ShaguTweaks.StandalonePlayerFrame + assert(layout.name.visible==(not hideName)) + assert(layout.health.backdrop.edgeFile=='Interface\\Tooltips\\UI-Tooltip-Border') + assert(not layout.health.art and not layout.power.art) + assert(layout.portrait.art.width==40 and layout.portrait.art.height==(maskAvailable==false and 72 or 36) and layout.portrait.artRight.height==36) + assert(layout.portrait.artBottomRight.height==36) + if maskAvailable==false then + assert(not layout.portrait.badge.visible and not layout.portrait.artBottomLeft) + else + assert(layout.portrait.artBottomLeft.height==36) + assert(layout.portrait.badge.mask=='Interface\\CharacterFrame\\TempPortraitAlphaMask') + assert(layout.portrait.badge:GetParent()==layout.portrait.badgeOverlay) + assert(layout.portrait.badgeOverlay:GetFrameLevel()>layout.portrait:GetFrameLevel()) + assert(layout.portrait.levelOverlay:GetFrameLevel()>layout.portrait.badgeOverlay:GetFrameLevel()) + end + assert(PlayerLevelText:GetParent()==layout.portrait.levelOverlay and PlayerLevelText.text==60) + assert(PlayerLevelText.points[4]==19 and PlayerLevelText.points[5]==-58) + assert(PlayerRestIcon.points[4]==3 and PlayerStatusGlow.points[4]==3) + assert(PlayerLevelText.layer=='BACKGROUND' and PlayerRestIcon:GetParent()==layout.portrait.levelOverlay) + assert(layout.portrait.levelOverlay:GetFrameLevel()>layout.portrait:GetFrameLevel()) + PlayerLevelText:SetText(61);assert(PlayerLevelText.text==61) + assert(layout.name.classBackground.path=='NativeHealthTexture') + assert(layout.name.classBackground.color[1]==.53 and layout.name.classBackground.color[3]==.93) + assert(PlayerFrameNameBackground.alpha==0 and layout.name.classBackground.visible) + assert(layout and originalHealth==PlayerFrameHealthBar and originalMana==PlayerFrameManaBar and originalPortrait==PlayerPortrait) + assert(PlayerFrameHealthBar:GetParent()==layout.health and PlayerFrameManaBar:GetParent()==layout.power) + assert(layout.power.unit=="player" and layout.power.manabar==PlayerFrameManaBar) + assert(PlayerPortrait:GetParent()==layout.portrait and PlayerPortrait.alpha==(hidden and 0 or 1)) + assert(numbers:GetParent()==PlayerFrameHealthBar and PlayerFrameHealthBar:GetScript('OnEvent')==healthEvent) + assert(PlayerFrame.visible and PlayerFrame.mouse==false and PlayerFrameTexture.alpha==0) + assert(layout.health.left==420 and layout.health.top==520) + assert(PlayerFrameManaBar.height==(big and 30 or 12)) + assert(not ready:GetScript('OnUpdate')) + fire(layout.name,'OnClick','LeftButton');assert(clicked=='LeftButton') + fire(PlayerFrameHealthBar,'OnMouseUp','RightButton');assert(menu==layout.health:GetName()) + keys=true + fire(PlayerFrameManaBar,'OnDragStart');assert(layout.power.moving) + layout.power.left=300;layout.power.top=400 + fire(PlayerFrameManaBar,'OnDragStop');assert(not layout.power.moving and ShaguTweaks_config.StandalonePlayerFrame.Power.x==300) + fire(layout.power,'OnMouseDown','RightButton');assert(not ShaguTweaks_config.StandalonePlayerFrame.Power) + assert(layout.power.left==layout.power.defaultPosition.x) + PlayerFrameTexture:SetAlpha(1);assert(PlayerFrameTexture.alpha==0) + PlayerFrame_UpdateStatus();assert(PlayerStatusTexture.width==72) +end +Check(false,true,false,false);Check(true,true,true,true);Check(false,false,false,true);Check(true,false,false,false);Check(false,true,false,false,false) +print('Standalone player: native object/event/text retention, big power sizing, portrait hiding, saved positions, drag/reset, click handling and idle initialization passed') diff --git a/tests/test_ui_palette.lua b/tests/test_ui_palette.lua index 9808912..ed5248d 100644 --- a/tests/test_ui_palette.lua +++ b/tests/test_ui_palette.lua @@ -25,3 +25,39 @@ frame:SetBackdropBorderColor(0,1,0,.6) frame:SetBackdrop({edgeFile='replacement'}) assert(frame.r==0 and frame.g==1 and frame.b==0 and frame.a==.6) print('Rarity preservation across backdrop resets passed') +-- Loading the palette module with both options disabled must not install +-- border hooks, create frames, or alter TrinketMenu's own gray frame border. +local paletteModules={} +ShaguTweaks={T=setmetatable({},{__index=function(t,k) return k end})} +function ShaguTweaks.GetGlobalEnv() return _G end +function ShaguTweaks.GetExpansion() return 'vanilla' end +function ShaguTweaks:register(module) paletteModules[module.title]=module; return module end +QuestLogDetailScrollChildFrame={GetWidth=function() return 100 end,GetHeight=function() return 100 end} +local writes=0 +local originalSetter=function() writes=writes+1 end +TrinketMenu_MainFrame={SetBackdropBorderColor=originalSetter} +function CreateFrame() error('Disabled palettes must not create a frame') end +function ShaguTweaks.HookAddonOrVariable() error('Disabled palettes must not install addon hooks') end +ShaguTweaks_config={['Golden UI']=0,['Darkened UI']=0} +dofile((arg[1] or '.')..'/mods/dark-ui-elements.lua') +assert(paletteModules['Golden UI'] and paletteModules['Darkened UI']) +assert(not ShaguTweaks.UIBorder and not ShaguTweaks.UITint) +assert(writes==0 and TrinketMenu_MainFrame.SetBackdropBorderColor==originalSetter) +print('Default UI: disabled palettes leave TrinketMenu border setters and colors untouched') +-- An enabled palette must also skip TrinketMenu, including anonymous children +-- reached by recursive scanning and generic minimap recoloring. +local start=assert(string.find(source,'local function IsExcludedFrame',1,true)) +local finish=assert(string.find(source,'local function LockAddonFrames',start,true)) +local prefix='local tint={r=1,g=.78,b=.4,a=1}; local edge={r=.8,g=.62,b=.32,a=1}; ' +local helpers=assert(loadstring(prefix..string.sub(source,start,finish-1)..'return {DarkenFrame,LockTree,FindAddonFrames,TintMinimapButton}'))() +local probe={GetName=function() return 'TrinketMenu_MainFrame' end} +function probe:GetChildren() error('TrinketMenu descendants must not be scanned') end +function probe:GetRegions() error('TrinketMenu textures must not be scanned') end +function probe:SetBackdropBorderColor() error('TrinketMenu border must not be recolored') end +local parent={GetName=function() return 'UIParent' end,GetChildren=function() return probe end} +helpers[1](parent) +helpers[2](probe) +helpers[3](parent) +helpers[4](probe) +assert(not matches('TrinketMenu_MainFrame')) +print('Active palettes: TrinketMenu recursive, persistent-hook and minimap recoloring paths are excluded')