Release 3.8: player frames, movable bars and UI polish

This commit is contained in:
2026-11-04 12:47:05 -04:00
parent 19bc58620b
commit e557dbe058
35 changed files with 1913 additions and 926 deletions
+31
View File
@@ -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
+4
View File
@@ -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
+10
View File
@@ -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.
+25 -15
View File
@@ -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)*
+5 -5
View File
@@ -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
+2 -1
View File
@@ -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")
-4
View File
@@ -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 <Shift> and <Ctrl> are pressed together."] = nil,
+15
View File
@@ -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.
+413
View File
@@ -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
+1 -16
View File
@@ -8,14 +8,6 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Movable Cast Bar"],
description = T["Hold <Ctrl>, <Shift> and <Alt> 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
+99 -4
View File
@@ -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
+3
View File
@@ -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)
+40 -20
View File
@@ -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
+1
View File
@@ -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
-402
View File
@@ -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
-84
View File
@@ -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
-57
View File
@@ -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
+8 -2
View File
@@ -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
-266
View File
@@ -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
+65 -48
View File
@@ -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
+6 -2
View File
@@ -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
+17
View File
@@ -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
+21
View File
@@ -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
+371
View File
@@ -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
+23
View File
@@ -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.
+238
View File
@@ -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')
+50
View File
@@ -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')
+30
View File
@@ -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')
+82
View File
@@ -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')
+28
View File
@@ -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')
+54
View File
@@ -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')
+41
View File
@@ -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')
+38
View File
@@ -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')
+156
View File
@@ -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')
+36
View File
@@ -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')