33 Commits
Author SHA1 Message Date
OEVG 6fc566feaf Release 3.8.7: unit frame colors and theme fixes 2026-11-08 19:32:00 -04:00
OEVG e43d93a524 Release 3.8.6: module fixes and native timer optimizations 2026-11-06 17:20:18 -04:00
OEVG b9912256bf Refresh README screenshot URL to bypass stale image caches 2026-11-06 11:44:50 -04:00
OEVG 40762969a1 Update Advanced Options screenshot for 3.8.5 2026-11-06 11:42:16 -04:00
OEVG 9101a0b32f Release 3.8.5: UI polish and initialization cleanup 2026-11-06 11:40:25 -04:00
OEVG a81ca54e0c Merge remote README updates with 3.8.4 release 2026-11-06 00:35:14 -04:00
OEVG 224f6db154 Release 3.8.4: pale-blue proc and reactive spell highlights 2026-11-06 00:34:09 -04:00
OEVG 4f0c80c58e Update README.md 2026-11-05 16:35:08 +00:00
OEVG eb9faafafd Update README.md 2026-11-05 16:32:49 +00:00
OEVG 662de4fced Update README.md 2026-11-05 16:12:13 +00:00
OEVG f393a827c7 Update README.md 2026-11-05 16:07:44 +00:00
OEVG ffa1ceb83b Update README.md 2026-11-05 16:06:32 +00:00
OEVG 8b62382660 Refine resource predictions and attached druid mana display 2026-11-05 11:50:27 -04:00
OEVG 97ff7a7d84 Document Advanced Options with a screenshot 2026-11-05 11:28:28 -04:00
OEVG 5db1aa9c41 Release 3.8.3: personal resource display and player-frame refinements 2026-11-05 11:27:15 -04:00
OEVG d51d2556ef Release 3.8.2: healing predictions and native API improvements 2026-11-04 21:39:57 -04:00
OEVG 48ee0747c3 Update Docs/EXTRAS_MERGE.md 2026-11-04 18:28:05 +00:00
OEVG dfe8cea692 Release 3.8.1: streamline ClassicAPI support and fix action bars 2026-11-04 14:03:16 -04:00
OEVG 7da9684025 Update Docs/EXTRAS_MERGE.md 2026-11-04 16:53:29 +00:00
OEVG e557dbe058 Release 3.8: player frames, movable bars and UI polish 2026-11-04 12:47:05 -04:00
OEVG 19bc58620b Release 3.7.4: add options search and polish UI compatibility 2026-11-03 17:27:54 -04:00
OEVGandClaude Opus 5.5 0784e2e5bb Golden UI: Forever-brown window title bars; mail inbox gets the send page's stationery (3.7.3 hotfix)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-03 08:14:46 +00:00
OEVGandClaude Opus 5.5 767b458aa6 Add Golden UI, a gold recolor sharing Darkened UI's code (3.7.3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-03 06:04:52 +00:00
OEVGandClaude Sonnet 5.5 9ef84b50fe Swing Timer: move each bar on its own; dragging the cast bar pins the swing bars (3.7.2 hotfix)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:43:45 +00:00
OEVGandClaude Sonnet 5.5 28da226764 Cast bar move markers: use the Swing Timer's classic cast bar look
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:32:32 +00:00
OEVG b47b2ec06e Update Docs/EXTRAS_MERGE.md 2026-11-02 22:27:22 +00:00
OEVGandClaude Sonnet 5.5 8e47666644 Add Loss of Control Alert, Totem Timers, Movable Cast Bar and movable Enemy Castbars (3.7.2), Docs/EXTRAS_MERGE.md for the Extras merge
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:23:04 +00:00
OEVGandClaude Sonnet 5.5 338135be34 Remove Key-Down Casting and Alt Self-Cast in favour of ClassicAPI's ActionButtonUseKeyDown (3.7.1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 22:02:50 +00:00
OEVGandClaude Opus 5.5 2c58ce2aa3 Merge the ShaguTweaks Extras ClassicAPI modules into ShaguTweaks (3.7)
Bring in 27 module files from Dusk/ShaguTweaks-Extras-ClassicAPI
(8e14bb4): action bar, chat, loot, macro, merchant (Buy Em All),
minimap & world map, tooltip and unit frame modules, with their
textures. Titles are unchanged, so saved toggles carry over.

- Categories: Chat -> Chat & Social, World Map -> Minimap & World Map,
  Merchant & Auction -> Merchant; Macro is a new category.
- Texture paths point at ShaguTweaks\img.
- Not merged: Raid Frames, Bag Item Click and Bag Search Bar (Bagshui
  covers them), Auction Alt-Buy (aux covers it).
- Chat note when the old ShaguTweaks-extras addon is still enabled.
- English translation keys, notices, license copy, asset provenance,
  README, changelog and version 3.7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 21:52:53 +00:00
OEVGandClaude Sonnet 5.5 b2c30df0e1 README: list ShaguTweaks Extras ClassicAPI as an optional requirement
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 21:04:26 +00:00
OEVGandClaude Sonnet 5.5 3c5f36b67a XP Bar Text: draw with a shadow instead of an outline so digits no longer touch
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 21:03:27 +00:00
OEVGandClaude Opus 5.5 6de8e93496 README: client-mod badges
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 14:54:06 +00:00
OEVGandClaude Opus 5.5 00c6903a81 Changelog: number each entry as a version, 3.0 to 3.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwusM4i1CYaHQAYixJ7
2026-11-02 04:59:03 +00:00
151 changed files with 13616 additions and 5404 deletions
+94 -66
View File
@@ -1,109 +1,137 @@
# Changelog # Changelog
Changes in this fork (OEVG/ShaguTweaks-ClassicAPI), newest first. Entries are dated and list the commit. Version numbers start at 3.0; the version is in `ShaguTweaks.toc` (`## Version`). Changes in OEVG/ShaguTweaks-ClassicAPI, newest first. Entries summarize released behavior; development iterations and test notes belong in `Docs/` and `tests/`.
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. Based on [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI) at `4391564` (2026-09-08). Earlier history is available upstream.
## Version 3.6 — 2026-11-02 ## 3.8.7 — 2026-11-08 — Unit frames and minimap buttons
Version 3.6 is the addon as of this entry, with every change listed below. - Add optional owner class colors for pet health bars, including targeted pets with identifiable owners.
- Add an option to remove the class-colored background behind your player name, including on the detached name frame.
- Apply Darkened and Golden UI tints to the Outfitter, TrinketMenu and Atlas-CFM minimap buttons.
- Preserve the Spellbook's original parchment and torn edges in Darkened and Golden UI.
## 2026-11-02 — Map window, Transports button and Loot History ## 3.8.6 — 2026-11-06 — Module fixes and native timers
### Added - Prevent recursive merchant-click hooks when Buy Em All runs alongside another merchant addon.
- Keep the viewed world map unchanged when hovering the Turtle clock.
- Refresh reagent counts when conditional macros change spells, and apply Minimap Clean's hidden state to late addon buttons.
- Keep the dungeon-finder minimap button's size and position stable when dragging it with Enlarged Minimap enabled.
- Stop Sell Junk's completion check when sales stall, and refresh rarity borders/glows when previously uncached item data arrives.
- Use native ClassicAPI cooldown numbers when native countdowns are enabled, retaining the existing appearance and legacy fallback; remove unused static spell/debuff locale data.
- Use native target cast-bar animation and player countdown text where supported, and limit power/portrait styling updates to affected unit frames.
- Use native totem/control-alert timers where supported, and reduce repeated resource/map styling writes and chat-cache allocations.
- Keep swing-timer and cast-bar preview borders above their fills and match them to the selected UI theme.
- Remove AoE Loot and retire its saved setting. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for AutoAreaLoot.
- **Loot History** (Loot, on): `/loothistory` opens a window with the recent group-loot rolls. Each item shows who won it with which roll; click it to see everyone's need, greed or pass and their rolls. A Clear button empties the list. Ported from brues-code/pfUI's loot history and drawn with the classic dialog look. Needs ClassicAPI's `C_LootHistory`, which keeps the last 128 rolled items through `/reload`. ## 3.8.5 — 2026-11-06 — UI polish and initialization cleanup
- **WorldMap Window:** when [OctoBus](https://github.com/Phridgey/OctoBus) is installed, a **Transports** button left of the Continent dropdown shows or hides the OctoBus window, like a left-click on its minimap icon. While the map is open, the OctoBus window is drawn above it.
### Fixed - Replace the theme checkboxes with one **UI:** dropdown below Search: Default, Darkened or Golden, preserving existing choices.
- Add a version button beside Defaults in Advanced Options, opening a copyable Gitea project link.
- Match equipped-item comparison backgrounds to the source tooltip in Darkened UI.
- Preserve native mail, quest-log and NPC quest/gossip parchment colors and torn edges in both themes, including empty quest-log artwork. Restore Golden UI's darker NPC title strip.
- Keep item-rarity borders and formatted Golden titles consistent when windows update or are recolored again.
- Consolidate deferred initialization, batch duplicate delayed recolors and bound the texture-classification cache while preserving frame exclusions.
- **WorldMap Window:** the map keeps the size picked with Ctrl+Mousewheel after closing and reopening it, and after a reload. It used to go back to the default size every time it opened. ## 3.8.4 — 2026-11-06 — Action-bar proc highlights
- **WorldMap Window:** the title bar drags the window too. On Turtle WoW the title is drawn above the window's edge, so only the dropdown row could be dragged before.
## 2026-11-01 — Swing Timer and Wand Timer - Add **Proc & Reactive Spell Highlights** (Action Bar, off): event-driven pale light-blue glows for supported spells and macros, with native pulsing animation where available. Central proc alerts remain with dedicated addons such as Proc Doc.
### Added ## 3.8.3 — 2026-11-05 — Personal Resource Display
- **Swing Timer** (Action Bar, off by default): main-hand and off-hand swing timers styled like the classic cast bar, with the weapon's icon on the left. They sit just above the cast bar; hold Ctrl+Shift+Alt to drag them, right-click to reset. A bar turns gray while the target is out of reach. - Replace **Standalone Player Frame** with **Personal Resource Display**: independent, movable and resizable copies of player health and power, leaving the original player frame intact. Existing health/power positions migrate.
- **Swing Timer queue colors:** the main-hand bar shows a queued Heroic Strike in light blue and Cleave in green (warriors), and Maul in orange (druids). It goes back to gold when the swing goes off, is refused, or is cancelled. - Add **Hide Player Frame Resources** to keep only the clean portrait and independently movable class-colored name. **Hide Player Name** now requires this option and also hides the raid group number. Resource hiding and the personal display can be used independently.
- **Wand Timer** (Action Bar, off by default): the time until your next wand shot in the same style. It also times Auto Shot for bows, guns and crossbows, and turns red during the last half second, when moving would cancel the shot. - Share healing predictions and Energy & Mana Tick timing with the copied bars, including resizing, and retain the active UI border palette.
- Both are ported from brues-code/pfUI's swing timer and need ClassicAPI v1.15.10 or newer (`PLAYER_SWING`). - Show a small attached mana bar for druids using Personal Resource Display while shapeshifted.
- Remove **Hide Player Portrait** and retire its saved setting.
- Condense the changelog into release summaries, removing intermediate testing history.
## 2026-11-01 — Hide Pet Unit Frame and XP numbers ## 3.8.2 — 2026-11-04 — Healing predictions and native API improvements
### Added - Add **Healing Predictions** for player and target: incoming direct heals and upcoming HoT ticks, with Big Health/standalone support. Reuse compatible Puppeteer data when available; otherwise use an observed-heal estimator and vanilla HealComm. See [prediction details and limitations](Docs/HEALING_PREDICTION_REVIEW.md).
- Use native ClassicAPI range subscriptions for **Range Color**, with balanced button subscriptions and an older-version fallback.
- Improve **Energy & Mana Tick** handling of power changes, mana-costing casts and regeneration timing; use native spark animation when available.
- Use ClassicAPI's macro icons and command parsing, retaining compatibility for legacy custom hints and other macro addons.
- Use native equipment comparisons where available, including full item links, paired equipment slots and stat differences; keep comparisons stable during Bagshui tooltip refreshes.
- Route health/power number updates separately and use native duration timers for swing/wand displays where supported.
- Remove redundant cast tracking and consolidate movement/startup helpers. Add a shared regression-test runner. See the [cast tracking review](Docs/CAST_TRACKING_REVIEW.md).
- Prevent caster-bar icons showing through empty druid form slots. Clarify that Auto Stance enters forms; ClassicAPI Auto Cancel Form leaves them.
- **Hide Pet Unit Frame** (Unit Frames, off by default): hides the pet frame below the player frame for pet classes. ## 3.8.1 — 2026-11-04 — ClassicAPI baseline and legacy cleanup
### Changed - Require **ClassicAPI 1.18.10+**, with a startup check and update message for unsupported runtimes.
- Remove **Auto Dismount** and its unused support code in favor of ClassicAPI's settings; retain Auto Stance for entering required forms.
- Respect **Always Show Action Bars** for empty main/bonus-bar slots.
- Fix disappearing Big Health fills after health updates in standalone mode.
- Remove the unused debuff-timing library and legacy per-frame nameplate discovery; retain optional-mod and missing-data compatibility checks.
- **XP Bar Text:** hovering the experience bar shows the current and needed XP, like the reputation bar (`1555/3000`), followed by the percentage and the rested bonus. ## 3.8 — 2026-11-04 — Player frames, movable bars and UI polish
## 2026-11-01 — World map colors and per-frame checks - Add **Standalone Player Frame**, detaching the native portrait, name, health and power into movable pieces, with clean rounded artwork, a class-colored name and saved positions. Add **Hide Player Portrait** and standalone-only **Hide Player Name**.
- Split **Big Health** into independent player and target options, migrating existing settings. Standalone power sizing follows the player setting.
- Include action, pet, stance, XP and reputation bar movement in **Reduced Actionbar Size**. Ctrl+Shift+Alt dragging supports saved positions, reset and snapping to visible bars.
- Make cast-bar movement always available and remove its checkbox.
- Give **Loss of Control Alert** classic artwork and borders that follow the active UI mode.
- Preserve native quest/gossip parchment colors and torn edges in Golden/Darkened UI. Golden UI NPC name headers use opaque title artwork; player names retain class styling.
- Extend Modern Class Colors to name backgrounds. Leave TrinketMenu styling to TrinketMenu.
- Remove Debuff Timer and the three nameplate options to avoid overlapping dedicated addons. See [alternatives and migration notes](Docs/EXTRAS_MERGE.md#removed-later).
### Fixed ## 3.7.4 — 2026-11-03 — Advanced Options search
- **WorldMap Class Colors:** raid members on the world map and battlefield minimap show their own class color. In a raid the map packs its icons (the first icon is the first raid member on that map, not `raid1`), so icons used to show other players' colors, such as a priest in paladin pink. Battleground team members outside your raid are colored from the player cache. - Add case-insensitive search by option name, description, category and slider label, retaining unsaved changes while filtering.
### Changed ## 3.7.3 — 2026-11-03 — Golden UI
- **WorldMap Class Colors:** runs only while the world map or battlefield minimap is open. - Add **Golden UI**, with configurable gold frame/border colors and brown title bars while retaining default text colors. Darkened UI takes precedence when both are selected.
- **Unit Frame Class Portraits:** the target-of-target check runs five times a second instead of every frame. A new target of target still gets its class icon at once. - Share theme handling across native windows and supported addons, including aux/AuxCraft, Bagshui, Outfitter, VCB and RollFor. Preserve parchment, item icons, slot interiors and existing quality colors.
- **Enlarged Minimap:** the check that keeps the buff rows clear of the bigger minimap runs only while the default buff frame is shown. With a buff addon that hides it, such as VCB, the check stops. - Extend **Item Rarity Borders** to outgoing mail/TurtleMail attachments and both sides of trade windows, refreshing on item changes and clearing empty slots. **Loot Quality** remains specific to the loot window.
- Center Tooltip Details health text within its bar.
## 2026-11-01 — Performance (`1ae8398`) ## 3.7.2 — 2026-11-02 — Loss of Control Alert, Totem Timers and movable cast bar
### Changed - Add **Loss of Control Alert** and **Totem Timers**, using ClassicAPI events with animation only while active. Both support modifier dragging and reset.
- Add **Movable Cast Bar** and independent movement for swing, wand and enemy cast bars, with saved positions.
- Link the [Extras migration guide](Docs/EXTRAS_MERGE.md) from the README.
- **Player cache:** the shared cache (`ShaguTweaks_player_cache`) now expires once per session. Players with a known class are kept 180 days after they were last seen, players whose class was never learned 90 days, and the cache holds at most 10,000 players (the oldest sightings go first). Before this, most entries never expired and one account's file reached 4.1 MB. See [Docs/PERFORMANCE.md](Docs/PERFORMANCE.md). ## 3.7.1 — 2026-11-02 — Key-Down Casting removed
- **Player cache:** every sighting (mouseover, target, nameplate, party, raid, guild, friends, `/who`, chat) refreshes a player's age. Guild members and friends are refreshed each session, so they do not expire.
- **Player cache:** a player's GUID is saved only while the class is unknown. Such players are resolved later with `GetPlayerInfoByGUID`.
- **Chat:** a speaker whose class is known and who spoke within the hour no longer triggers a GUID lookup.
- **Guild roster:** a full rescan runs at most once a minute unless the member count changed.
- **Range Color:** the range check runs every 0.2 seconds per button. It used to run on every frame for every ranged button.
- **Range Color:** Blizzard's own range check works again, so hotkey numbers turn red out of range, as in the default UI.
### Removed - Remove **Key-Down Casting** and its dependent **Alt Self-Cast** option in favor of ClassicAPI's native settings. Use `/console ActionButtonUseKeyDown 1` for key-down activation and the native SELFCAST modified click for self-casting.
- Social Colors' separate 180-day cache cleanup. Expiry now lives in `libs/libunitscan.lua` and runs whether or not Social Colors is enabled. ## 3.7 — 2026-11-02 — Extras modules built in
### Upgrade note - Merge Dusk's Extras action-bar, chat, loot, macro, merchant, map, tooltip and abbreviated-name modules, preserving their saved choices. Warn when the separate Extras addon remains enabled.
- Leave raid frames, bag helpers and auction Alt-Buy to dedicated addons. See [the module list](Docs/EXTRAS_MERGE.md).
- Improve XP text legibility with a shadow instead of an outline.
- The first login after updating can remove many old cache entries. Class colors and chat levels come back as soon as those players are seen again. ## 3.6 — 2026-11-02 — Map window, Transports button and Loot History
## 2026-10-31 — ClassicAPI v1.18.13 review and tidy-up (`e4c7741`) - Add **Loot History**, using ClassicAPI's stored group-roll results, and an OctoBus **Transports** button on the world map.
- Preserve map scale across reopening/reload and allow dragging from its title bar.
### Fixed ## 3.5 — 2026-11-01 — Swing Timer and Wand Timer
- **Debuff Timer:** native cooldown frames hide ClassicAPI's own countdown numbers, so the timer text is not doubled when the `countdownForCooldowns` CVar is on. - Add classic-style main/off-hand swing timers, queued-attack colors and ranged/wand timing with Hunter wind-up indication, based on brues-code/pfUI.
### Changed ## 3.4 — 2026-11-01 — Hide Pet Unit Frame and XP numbers
- README rewritten: one feature list, module names and default states matching the in-game options, the recommended ClassicAPI version, and links to the docs. - Add **Hide Pet Unit Frame** and show current/required XP, percentage and rested bonus on hover.
- [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md) records the review of ClassicAPI v1.17.7 to v1.18.13. ClassicAPI v1.18.10 or newer is recommended for its nameplate crash fix.
- `.gitignore` covers editor and merge leftovers.
### Removed ## 3.3 — 2026-11-01 — World map colors and per-frame checks
- The Russian `## Notes-ruRU` line in `ShaguTweaks.toc`. - Correct class colors for packed world-map raid icons and battleground teammates.
- Limit map, target-of-target portrait and enlarged-minimap checks to relevant visibility/update intervals.
## 2026-10-30 — OEVG customization (`48d45f8`, `d705a9b`) ## 3.2 — 2026-11-01 — Performance
### Added - Bound and expire the shared player cache, refresh sighting ages and avoid redundant GUID/guild lookups. See [cache details](Docs/PERFORMANCE.md).
- Throttle Range Color checks and retain Blizzard's native out-of-range hotkey coloring.
- **Modern Class Colors:** an optional palette for all nine Vanilla classes, including blue shamans, with class-tinted health artwork and precolored rage, energy and focus bars. Mana uses the light-blue retail fill on every supported frame, including the druid's mana bar in bear and cat form. See [Docs/MODERN_MANA.md](Docs/MODERN_MANA.md). ## 3.1 — 2026-10-31 — ClassicAPI review and tidy-up
- **White Unit Frame Numbers:** health, percentage and power numbers in white instead of gold.
- **Unit Frame Big Health:** the resting glow is limited to the portrait.
### Changed - Prevent duplicate Debuff Timer countdown text with ClassicAPI's native countdown enabled.
- Refresh feature documentation, compatibility notes and repository ignore rules.
- **Real Health Numbers:** percentage on the left and current health on the right. The numbers copy the character name's font, size and shadow. Unknown target health stays percentage-only. ## 3.0 — 2026-10-30 — OEVG customization
- **Movable Unit Frames:** hold Ctrl+Shift+Alt to drag.
- **Tooltip Details:** guild ranks and the leader marker are restored, with optional modern class colors and health texture. Cached colors no longer leak between tooltips.
- **Range Color:** spells without a range no longer use the legacy fallback, and wrong friendly or hostile targets and stale red or no-mana tints are handled.
- **ClassicAPI integration:** native cooldown frames, filtered unit events, corrected chat identity and payload handling, and fewer duplicate player-cache records when ClassicAPI's own player cache is on. See [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md).
### Removed - Add **Modern Class Colors** and modern health/power textures, including druid mana, plus independent **White Unit Frame Numbers**.
- Place health percentage left and current health right with matching name-font styling. Limit Big Health's resting glow to the portrait.
- All non-English locale and translation files. The addon loads English data only.
- The **Unit Frame Health Colors** module (`mods/health-color.lua`).
+4
View File
@@ -33,6 +33,10 @@ hashes.
| `img/UI-TargetingFrame.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/UI-TargetingFrame.tga` | `c3c56c28ff81f99325a8aa5a8d8c5cd4a9c56a95` | | `img/UI-TargetingFrame.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/UI-TargetingFrame.tga` | `c3c56c28ff81f99325a8aa5a8d8c5cd4a9c56a95` |
| `img/circleparty.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleparty.tga` | `9634f9ffb07ae4fb047223f83c299a8ba1906dff` | | `img/circleparty.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleparty.tga` | `9634f9ffb07ae4fb047223f83c299a8ba1906dff` |
| `img/circleraid.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleraid.tga` | `c8a5c3e4c572ebd4af426d878bdc8db21d758691` | | `img/circleraid.tga` | exact-upstream-git-blob-match | `shagu/ShaguTweaks: img/circleraid.tga` | `c8a5c3e4c572ebd4af426d878bdc8db21d758691` |
| `img/df-gryphon-beta.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-gryphon-beta.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `bb4262fe7bf811fa7fff958f7f29269b69038280` |
| `img/df-gryphon.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-gryphon.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `6c7e965613222b0313dc1d56d9aa81414a828eef` |
| `img/df-wyvern.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/df-wyvern.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `4414fedcec732ea3096c30598ba7ada25a6af585` |
| `img/xpbar.tga` | exact-upstream-git-blob-match | `paokkerkir/ShaguTweaks-extras: img/xpbar.tga` (via Dusk/ShaguTweaks-Extras-ClassicAPI) | `de80c16875cd7d9fa96baf99bd6ae2e1514595f7` |
## Screenshot manifest ## Screenshot manifest
+46
View File
@@ -0,0 +1,46 @@
# Cast tracking review (3.8.2)
## Data ownership
The Improved Castbar and healing estimator query the normalized ClassicAPI cast
surface directly. Enemy Castbars also queries ClassicAPI first. The old libcast
copy of ClassicAPI cast events, keyed by unit name, had no built-in display
consumer. Name keys also cannot distinguish two enemies with the same name.
Removed the native-event mirror and legacy localized combat-text parser from
libcast. Ordinary non-Hunter initialization now registers no cast events or
casting hooks. Native unit queries do not create cached Lua records.
SuperWoW still supplies timed GUID records when ClassicAPI cannot supply complete
timing. Remote channels require ClassicAPI's live channel state and matching
spell ID before display, so an old timed record cannot resurrect a stopped or
different channel. A cache update refreshes the current target immediately.
The SuperWoW listener is created only when Super WoW Compatibility and Enemy
Castbars are enabled. It ignores the local player and complete native records.
It retains at most 128 remote records, prunes expired records on incoming events
at most every half second, and expires records when read. There is no cache
cleanup OnUpdate loop. Expired records may remain until another event/query,
but the cache remains bounded and expired entries cannot be displayed.
Hunter-only Aimed Shot/Multi-Shot custom timing is retained for compatibility
through ShaguTweaks' legacy query wrappers. Those hooks are no longer installed
for other classes. The built-in Improved Castbar continues to use ClassicAPI
directly; this cleanup does not add custom Hunter casts to that display.
## Validation and limits
`tests/test_cast_tracking.lua` covers native query ownership, duplicate enable
guards, disabled consumers, complete native records, missing channel timing,
live channel identity, delayed failures, expiry, cache limits, and Hunter
custom-cast entry points. These are mocked Lua 5.0 checks, not client/DLL tests.
Before release, test player casts/channels and interruptions; enemy casts and
interruptions; switching targets during a channel; and Enemy Castbars with
Super WoW Compatibility both enabled and disabled. If a Hunter is available,
also test Aimed Shot and Multi-Shot through spellbook, macro and action buttons.
This reduces redundant Lua handlers and records. No FPS, memory or CPU gain has
been measured. Compare the same encounter/options using ClassicAPI's
GetAddOnCPUUsage/GetAddOnMemoryUsage with script profiling enabled; do not rely
on GetFrameCPUUsage or infer a frame-rate gain from code size.
+208
View File
@@ -0,0 +1,208 @@
# ClassicAPI / pfUI overlap review — 3.8.2
Reviewed 2026-11-04 against the current, uncommitted ShaguTweaks build.
This is a source review and implementation shortlist, not a performance result
or a claim that every finding has been reproduced in game. No addon behavior
or recoloring was changed during this review.
Implementation follow-up: the energy/mana, macro ownership and native equipment
comparison work below has now been applied and has three new mocked regression
suites (25 total). The user confirmed the final energy/mana, macro and comparison
paths work in game, including the Bagshui comparison ownership fix. The energy adaptation retains
the original five-second mana sweep and two-second regen baseline; it adopts
phase rejection/native movement, not pfUI's separate configurable FSR bar or its
server-specific agility/regen-aura modifier calculation. Theme candidates remain
unimplemented.
## Sources and coverage
- [ClassicAPI API.md, commit c0b403ba05a917176fa680cbfc96c619c98d46d6](https://github.com/brues-code/ClassicAPI/blob/c0b403ba05a917176fa680cbfc96c619c98d46d6/docs/API.md).
Downloaded the pinned document after resolving current master. Reviewed its
system/function catalog and the contracts relevant to this addon. Its roughly
1.15 MB includes APIs for systems this addon does not implement.
- [brues-code/pfUI, commit 560343bd746e00898606cf756bdfe533a4823110](https://github.com/brues-code/pfUI/tree/560343bd746e00898606cf756bdfe533a4823110).
Fetched origin and verified the clean reference checkout matches current
master. Indexed its 90 module/API Lua files and examined the implementations
overlapping ShaguTweaks, including native timing, events, macros, tooltips,
inventories, maps, unit frames, skinning, and helper APIs.
- Indexed ShaguTweaks' 83 module and seven library files, plus its API/helpers,
loader, compatibility files and current TOC. Read the relevant implementations
and traced consumers for the findings below. API-symbol inventories are only
discovery aids: a wrapper can already use a native API elsewhere.
| Overlap | Result |
| --- | --- |
| Action bars, range, reagents and macros | Range subscriptions already adopted; macro ownership is the next substantial cleanup opportunity. |
| Player/target frames, health/power, portraits | Routing already separated; energy tick correctness and portrait event routing remain candidates. |
| Cast, swing, loss-of-control, cooldown, totem and roll displays | Swing native timing adopted; several other displays still animate/count down in Lua. |
| Item/tooltip/vendor, bags and mail/trade rarity | Many native item/tooltip APIs already used; equipment comparison can be simplified. Preserve working rarity behavior. |
| Chat, social/player cache | Native identity and cache interfaces already integrated; no justification to delete the Lua cache wholesale. |
| Minimap, map and coordinate helpers | Mostly visibility-gated/throttled; many updates genuinely depend on cursor/unit movement. Profile before further changes. |
| Golden/Dark UI and addon compatibility | Optional candidates listed separately below. Keep the tested implementation intact. |
| Auction replacement, equipment manager, full raid/unit-frame replacement, Glue/login, TTS and similar systems | Not equivalent to current addon features; do not import simply because pfUI or ClassicAPI offers them. |
## Recommended correctness and ownership work
### 1. Energy/mana tick: strongest next candidate
Current source: `mods/unitframes-energy-tick.lua:21` and its event/update handlers.
Reference: pfUI `modules/energytick.lua`.
Three concrete issues are visible in the source:
- The unit filter runs before event dispatch. ClassicAPI's
PLAYER_ENTERING_WORLD now carries `true` in arg1 on a fresh login; the filter
rejects it as not equal to `"player"`, skipping initialization. Reload's arg1
is nil, so this need not reproduce the same way after /reload.
- `lastMana` is seeded on world entry but not on UNIT_DISPLAYPOWER. Changing
forms can compare an energy value with the previous mana sample or vice versa.
- Any mana loss sets a five-second sweep. A drain such as Mana Burn does not
establish the same five-second-rule condition as paying for a spell. Any
sufficiently large gain can also reset the regen sweep, including a potion
or unrelated mana proc. The spark's initial width/height are captured once,
so later standalone/Big Health relayout also deserves testing.
pfUI separates power-mode samples, spell-cost/FSR tracking and the periodic regen
phase. It also uses an invisible native status-bar duration to move the spark.
Adapt the model to the existing player mana bar rather than importing pfUI's
unit-frame layout or assuming every Octo/Turtle regen rule matches its comments.
Benefit: correctness first, plus removal of the spark's Lua per-frame movement.
Risk: medium for a full rewrite; low for the login-payload/unit-filter fix alone.
Test mana spending, drains, potions/procs, channels, full mana, form switching,
and Big Health/standalone layouts. Event-filter initialization can be fixed
independently of the broader native-timer conversion.
### 2. Native macro ownership instead of repeated manual scans
Current sources: `mods/macro-icons.lua:18`, `:47`, `:113`;
`mods/macro-tweaks.lua:17` and its slash registrations.
Current ClassicAPI handles native macro conditions, #showtooltip/#show, spell
and item icons, range, cooldown, usability, count and current castsequence step.
It also exposes C_Macro.GetMacroIcon and C_Macro.SetMacroDisplay. The current
Macro Icons module rebuilds a 36-slot macro cache and scans visible action bars
on several events, parses bodies, resolves spellbook slots and may rewrite icons.
There is a concrete behavior difference: ClassicAPI preserves a manually chosen
macro icon and substitutes spell/item artwork only for the question-mark icon.
Our resolver can replace that chosen artwork with GetSpellTexture when the
specified spell resolves. This is a source-level difference, not a reproduced
user complaint. Conditional/item macros already fall back to GetActionTexture
when our parser cannot resolve them; do not claim every conditional macro fails.
Proposed migration: let native #showtooltip/#show own ordinary macros. Retain
the nonstandard `--showtooltip` / `/pfcast` behavior explicitly if desired,
using SetMacroDisplay where appropriate. Review the global SendChatMessage
metadata filter and old /equip, /use, focus and attack aliases against the
installed native command surface. Finding a native macro command does not prove
that an equivalent interactive chat alias exists in SlashCmdList; test both.
Benefit: less parsing/allocation and fewer competing owners; better consistency
with native macro behavior. Risk: medium, due to existing custom syntax, chosen
icons, macro addons and older supported DLLs. Feature/version detection and
tests must precede deletion. Do not remove Macro Tweaks as one undifferentiated
block. pfUI's current `macroicons.lua` is an icon-picker UI, not the same feature.
### 3. Equip Compare can delegate comparison construction
Current source: `mods/equip-compare.lua:21`, `:53`, `:129`.
Reference: pfUI `modules/eqcompare.lua`.
pfUI calls GameTooltip:SetHyperlinkCompareItem for comparison offsets 1 and 2.
ShaguTweaks still maintains an equipment-type-to-slot table and manually shifts
tooltip lines to insert CURRENTLY_EQUIPPED. A guarded native path could replace
much of that construction while retaining our Shift condition, positioning,
font/color style and third-party tooltip behavior.
Benefit: smaller code and native handling of comparison slots/item instances;
likely little FPS effect. Risk: medium visual/compatibility risk. Test rings,
trinkets, dual wield, two-hand weapons, equipped enchants/random suffixes, bags,
links, vendors and simultaneous tooltip addons. Preserve the actual item link,
not just its item ID, when comparing.
## Native runtime opportunities to prioritize after profiling
| Feature | Current work | Native/pfUI approach | Benefit and risk |
| --- | --- | --- | --- |
| Cooldown Numbers | One visible helper OnUpdate per active cooldown, formatting at 0.1 s, plus alpha synchronization. | pfUI configures native Cooldown countdown fonts/formatters. For stock 1.12 Model indicators, retain the original swipe and consider a duration-text binding for our overlay. | Potentially useful with many cooldowns; medium compatibility risk. Stock Models are not native Cooldown frames, so this is not a blanket method substitution. Preserve GCD suppression, TimeConvert formatting, noCooldownCount/pfCooldownType and other addons' ownership; do not enable a global CVar merely to style ours. |
| Loss of Control | While shown, reselects effects, rebuilds metadata and reapplies theme/text/width every 0.05 s. | pfUI reselects on LOC events plus visible-only UNIT_AURA_DELTA and SPELL_UPDATE_COOLDOWN; native duration/countdown handles display timing. | Reduces repeated aura/list scans and formatting during CC. Medium correctness risk: refreshed CC or an already-locked school can extend without a LOC diff event; expiration/selection also need explicit handling. Keep priority and classic styling. |
| Player/target cast displays | Already discover casts on events; remaining Lua work animates text/fill/spark/alpha while visible. | pfUI uses duration objects, native fill-edge anchors and text bindings. | Conditional saving during casts. Medium risk from pushback, channel updates, fade/preview and SuperWoW fallback. Native cast-duration objects are snapshots, not automatically updated: refresh on delay/channel-update events. Remote channels whose start was unseen can still lack native timing. |
| Totem Timers | Four-slot timed text/color/width updates while totems are up. | GetTotemDuration and duration bindings; scheduled 10/5-second color thresholds and expiry; pfUI uses native cooldown displays. | Shaman-only saving; lower priority for the user's druid. Preserve our bar/tooltip/click design rather than copying pfUI's panel layout. Replacements/destruction/preview must cancel old bindings/timers. |
| Class Portraits | Every portrait event refreshes six frames; target-of-target adds a 0.2 s GUID check through the stock updater. | pfUI's chained frames use UNIT_TARGET subscriptions. Update the relevant portrait on unit events and handle same-name target changes by GUID. | Small conditional saving and clearer ownership. Preserve native portrait refresh hooks: stock code can overwrite custom artwork in the same frame. Validate actual event order before removing the safety path. |
| Group roll bars | One shared visible-only Lua loop queries remaining time for every active roll each frame. | Native durations for the fill with event-driven reset/cancel, as other pfUI displays do. | Low priority: already consolidated and runs only during rolls. Native snapshot timing must account for actual roll changes; profile first. |
No table above promises a measured CPU or frame-rate improvement. Native work
still costs client time, even when it disappears from Lua profiling.
## Golden/Dark UI: optional findings only — do not change now
The current implementation is a compatibility recolorer. pfUI replaces or skins
many widgets with its own artwork/backdrops; copying its skin code would change
the look and bypass the fixes that made parchment, slots, rarity and addon borders
correct. There is no documented single ClassicAPI switch that replaces our theme.
| Candidate | What could improve | Risk | Expected benefit |
| --- | --- | --- | --- |
| Read-only theme inspection | A diagnostic using GetDebugName/GetSourceLocation and region/owner inspection could identify the actual texture/addon behind a missed border, reducing trial and error. It need not change any colors. | Low; keep it on demand. | Maintenance/debugging only, no FPS gain. |
| Coalesce repeated deferred recolors | RecolorOnShow currently applies immediately and schedules another pass. Merge multiple pending requests for the same frame while retaining the immediate and trailing behavior. | Moderate: timing relative to row/frame creation is essential. | Probably small; mainly window creation/show bursts. Profile before considering. |
| Texture-path classification/cache and traversal scratch | Recolor traversals create region/child arrays and repeatedly classify texture paths. A bounded path-only classification cache or carefully scoped reusable arrays could avoid some garbage. Name-based exclusions must remain separate; recursive scratch cannot be shared at the same depth, and reused arrays need correct Lua 5.0 lengths. | Low-to-moderate for narrow path caching; moderate for traversal changes. | Small/unmeasured and mostly startup/window-open work. Existing path/region markers and dynamic texture changes must remain correct. |
| Targeted addon lifecycle hooks | pfUI's skins commonly use addon-load or widget-construction hooks. These might narrow the global CreateFrame wrapper in our recolorer. | High compatibility risk: anonymous or late-created Bagshui/aux/VCB/RollFor widgets can be missed. | Unmeasured; avoid unless the wrapper is actually expensive. |
| Explicit native-menu styling | pfUI styles MenuVariants' default/context menu mixins and selection controls. If a current ClassicAPI-created menu is visibly unthemed, a targeted adapter could extend Golden/Dark coverage. | Moderate; pooled/reused controls and checked/disabled states need testing. | Visual consistency only. This is a potential coverage extension, not a confirmed silver-border defect in the user's build. |
Retain all parchment/slot-edge markers, neutral-color checks, rarity preservation,
TrinketMenu exclusions, default-UI guards and the trailing recolor timing. No
recommendation here requires reopening the working recoloring before profiling.
## Small cleanup and deliberate non-changes
- An internal source-reference scan found no production consumer outside api.lua
for these wrappers: GetNamePlateForUnit, GetDebuffDataByIndex, UnitDebuff,
GetDebuffType, SetOverrideBindingClick, ClearOverrideBindings, GetMapOverlays
and UnitInRange. These are cleanup candidates, not proven safe deletions:
exposed ShaguTweaks APIs may have external consumers. Runtime benefit is
negligible; capability flags may still be needed by other helpers.
- Macro migration could shrink libspell/libtipscan further. Currently the macro
module uses GetSpellIndex and Hunter emulation uses GetSpellInfo. Both
libraries' scanners are lazy. Do not delete them before migrating these users
and deciding how to preserve their public interfaces.
- Keep libnameplate: Golden/Dark compatibility registers its callbacks for
ShaguPlates. It is dormant without consumers and is not an unused library.
- Native HookScript/hooksecurefunc can simplify selected hook sites, but replacing
our helper globally is unsafe without an argument/order audit. Our helper
supports prepend and returns a fixed legacy argument shape; native script
handlers can receive explicit self/event arguments. Styling locks deliberately
replace methods to intercept colors, which a simple posthook does not replace.
- Keep Auto Stance; ClassicAPI Auto Cancel Form handles the opposite operation.
pfUI's older autoshift/error handler is not a better replacement for ours.
- Keep observed/shared healing prediction. The reviewed API has healing bonus
and spell metadata, not a complete authoritative incoming-heal total. Raw DBC
effects alone cannot safely replace the estimator or the Puppeteer source.
- Keep independent Range Color subscriptions and the existing native merchant,
item-cache, loot-history, gossip, map-overlay and player-cache integrations.
Many of the obvious pfUI native implementations are already represented here.
- A cursor-following tooltip still needs movement observation; pfUI also uses a
visible-only follower. World-map coordinates must track moving cursors/units.
Replacing every visible OnUpdate with a ticker is not inherently faster.
- ClassicAPI now documents syntax transpilation, extra Lua library functions and
a raised upvalue limit. That does not supersede this repository's Lua 5.0
coding/testing rules. Do not modernize syntax merely for stylistic cleanup.
- Do not add pfUI's bags, auction/raid-frame replacements, equipment manager,
alternate heal model or other large features solely for parity. Existing
dedicated addons and the user's chosen Blizzard styling remain the design.
## Suggested order at the time of review
1. Energy-tick initialization/form-sample fixes; then decide whether to adopt its
native sweep and more accurate regen/FSR model.
2. Native macro ownership migration with a clear policy for the two custom
syntaxes and chosen macro icons.
3. Native equipment comparison if that behavior change is wanted.
4. Profile the current build to select among cooldown, LOC, cast, portrait and
other runtime candidates. Keep theme candidates documented unless a real
defect or measured bottleneck justifies their risk.
The existing 22 regression suites passed before this documentation-only review.
The later implementation follow-up noted above added three suites (25 total)
for energy/mana, macros and native equipment comparison. Other recommendations
remain unimplemented and have no measured performance benefit.
+68 -19
View File
@@ -1,16 +1,58 @@
# ClassicAPI integration updates # ClassicAPI integration updates
These changes use newer ClassicAPI features while retaining older fallback paths. ## Current integration (3.8.3, 2026-11-05)
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
bundled `!!!ClassicAPI` addon together, following its installation
instructions; a DLL update needs a full client restart.
The integration code below was written against v1.17.6. The v1.17.7 to v1.18.13 The [ClassicAPI/pfUI overlap review](CLASSICAPI_PFUI_REVIEW_382.md) pins current
review found nothing that needed a rewrite; see source revisions and records optional recoloring improvements. Its energy/mana
[Reviewed since v1.17.6](#reviewed-since-v1176). tick, macro ownership and native equipment comparison recommendations have
subsequently been implemented; other candidates remain recommendations.
## Changes ClassicAPI is required, with a minimum loaded DLL version of 1.18.10 and core
timer/nameplate API checks. Install the DLL and its bundled `!!!ClassicAPI` addon
together and fully restart the client after a DLL update. Optional SuperWoW
support is detected separately. Newer APIs are detected per feature rather than
assumed available from the minimum version.
- Range Color uses native action-range subscriptions/change events when present;
older supported APIs retain a throttled fallback.
- Healing Predictions shares compatible OEVG Puppeteer data when available;
otherwise it uses a lazy ClassicAPI event/observed-heal estimator with live HoT
aura timing. See [the estimator review](HEALING_PREDICTION_REVIEW.md).
- Personal Resource Display mirrors the native player bars without a second
health/power event driver; its health copy shares the existing prediction
estimator. Resource hiding is independent, and Hide Player Name requires it.
The former standalone setting/positions migrate; Hide Player Portrait is retired.
- Castbars query ClassicAPI directly. The unused name-keyed cast mirror and
combat-text parser are removed; SuperWoW supplements missing enemy timing with
a bounded GUID cache. See [the cast review](CAST_TRACKING_REVIEW.md).
- Swing/wand fill and text use native duration objects/bindings when supported,
with cancellable boundary timers and no Lua per-frame driver. Older APIs retain
the throttled path. The implementation follows brues-code/pfUI at `560343b`.
- Health and power events refresh only their respective number overlays;
identity changes/startup retain complete refreshes. Modifier-key checks and
the repeated delayed layout initialization use shared API helpers.
- Energy/mana ticks handle login/form payloads and use cast-cost-driven FSR with
a separately observed regen phase and native spark movement where supported.
Ordinary macro icons are left to ClassicAPI; only custom legacy hints need
our adapter. Missing interactive command aliases use native macro parsing.
- Native equipment comparison builds equipped headers/slot selection/stat
differences from full item links, retaining our Shift and positioning rules.
- Legacy debuff timing and nameplate discovery have been removed. Auto Dismount
is provided by ClassicAPI; ShaguTweaks Auto Stance remains distinct from Auto
Cancel Form. Theme recoloring remains unchanged by the latest cleanup.
These focused reviews do not constitute a complete audit of all changes between
ClassicAPI releases. Current automated checks and their limits are listed below.
## Historical review (2026-10-31)
The following sections record the integration written against v1.17.6 and the
subsequent v1.17.7–v1.18.13 review. They describe that snapshot, not the current
feature list. Debuff Timer was removed in 3.8 in favor of
[TargetFrameBuff](https://octowow.st/git/OEVG/TargetFrameBuff), and its unused
library was removed in 3.8.1. See [migration notes](EXTRAS_MERGE.md#removed-later).
### Changes at that time
- **Health correctness:** `UnitHealthMissing` returns a deficit in the same units - **Health correctness:** `UnitHealthMissing` returns a deficit in the same units
as the health getters. It can be a percentage, so it is no longer used to infer as the health getters. It can be a percentage, so it is no longer used to infer
@@ -34,7 +76,7 @@ review found nothing that needed a rewrite; see
intact. With persistence off or native storage unavailable, the original Lua intact. With persistence off or native storage unavailable, the original Lua
cache behavior remains. The addon never enables native persistence itself. cache behavior remains. The addon never enables native persistence itself.
## Reviewed since v1.17.6 ### Reviewed since v1.17.6
156 ClassicAPI commits (v1.17.7 to v1.18.13) were read against the modules in 156 ClassicAPI commits (v1.17.7 to v1.18.13) were read against the modules in
this addon. this addon.
@@ -76,7 +118,7 @@ this addon.
- `UNIT_TARGET`, `UnitFacing`, `wipe`, the `Cooldown` countdown formatter and - `UNIT_TARGET`, `UnitFacing`, `wipe`, the `Cooldown` countdown formatter and
the other new APIs: no module here needs them. the other new APIs: no module here needs them.
Nothing from this review was tested in game. At the time of that historical review, its changes had not been tested in game.
## Measuring performance ## Measuring performance
@@ -104,14 +146,21 @@ turned on by this addon.
## Verification ## Verification
Lua 5.1 syntax checks cover all addon Lua files. Mocked regression checks cover: Changed/new Lua files are parse-checked with a real Lua 5.0 interpreter. The
unknown percentage health, valid cached estimates, direct health, native and current 25 mocked regression suites cover UI styling/rarity, options search,
fallback unit-event registration, sixth-return chat identity and name mismatch, unit-frame layouts, action-bar behavior/movement, runtime guards, range-event
native chat payload with corrupted globals, duplicate filtering, native/legacy ownership, cast tracking, healing prediction, shared startup/modifier helpers,
cooldown creation, and persistence enabled/disabled while retaining levels and health/power routing, native/fallback swing and energy timers, macro ownership
history. Existing health overlays and English-only load-list checks also pass. and equipment comparison. Run them together with
These checks do not replace in-game rendering, event-order and combat testing; `sh tests/run_all.sh ../tools/lua5.0/lua5.0` from the addon root; see
no memory/FPS improvement is claimed without a live measurement. [tests/README.md](../tests/README.md).
These checks do not replace in-game rendering, DLL event-order and combat tests.
The user confirmed the range/healing/cast changes and the final energy/mana,
macro and equipment-comparison paths in game, including stable Bagshui comparisons
after the ownership recovery fix. Automated coverage does not establish every
possible addon/client combination. No memory/FPS
improvement is claimed without a live measurement.
## Primary references ## Primary references
+78
View File
@@ -0,0 +1,78 @@
# ShaguTweaks Extras ClassicAPI: what moved where
Since version 3.7 the modules of Dusk's [ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) are part of this addon, so you no longer need to install it. This page lists where every module went, what was left out and why.
Copied from Extras commit `8e14bb4` (2026-09-18). Credits and licenses are in `THIRD_PARTY_NOTICES.md`.
## What to do when updating
From 3.8.6, Cooldown Numbers uses ClassicAPI's native countdown text when
`countdownForCooldowns` is enabled. It preserves ShaguTweaks' colors, rounding
and duration threshold. The addon leaves that global CVar unchanged; when it is
off, or the required native APIs are unavailable, the existing Lua renderer
continues to work. Legacy Model indicators retain their sweep and use a native
child for numbers when supported.
For custom-module authors: 3.8.6 removes unused `ShaguTweaks.L` data sections
`bagtypes`, `itemtypes`, `hunterpaging`, `critters`, `dyndebuffs`, `judgements`,
`interrupts`, `resurrections`, `spells`, `debuffs` and `totems`. The `class` and
`customcast` sections remain. Custom modules using the removed tables must
supply their own data or use client APIs.
1. Install this addon (3.7 or newer).
2. Disable or delete the `ShaguTweaks-extras` folder in `Interface\AddOns`. While it stays enabled, its older copies replace the built-in ones, and a message in chat reminds you at login.
3. Your on/off choices carry over, because the modules kept their names.
## Where the modules went
Open the settings with **Esc → Advanced Options**.
| Module | Now in category |
|---|---|
| Dragonflight Gryphons | Action Bar |
| Floating Actionbar | Action Bar |
| Hide Macro Text | Action Bar |
| Mouseover Right, Mouseover Right 2 | Action Bar |
| Reagent Counter | Action Bar |
| Center Vertical Actionbar | Action Bar |
| Show Bags, Show Micro Menu | Action Bar |
| Center Text Input Box | Chat & Social |
| Block NPC Spam | Chat & Social |
| Enable Text Shadow | Chat & Social |
| Chat Timestamps | Chat & Social |
| World Chat Hider | Chat & Social |
| Loot Quality | Loot |
| Macro Icons, Macro Tweaks | **Macro** (new category) |
| Buy Em All | **Merchant** (new category) |
| MiniMap Framerate & Latency | Minimap & World Map |
| Reveal World Map | Minimap & World Map |
| Cursor Tooltip, Hide Combat Tooltip, Metric Range | Tooltip & Items |
| Unit Frame Abbreviated Names | Unit Frames |
The textures these modules use (the gryphons and the XP bar) moved to this addon's `img` folder.
## Left out, and why
| Module | Reason |
|---|---|
| **Raid Frames**, with all its options (Show Aggro Indicators, Show Combat Feedback, Use Compact Layout, Show Dispel Indicators, Show Group Headers, Show Healing Predictions, Hide Party Frames, Use As Party Frames) | Not wanted in this fork. I use Puppeteer for Raid/Party/Focus frames. |
| **Bag Item Click** and **Bag Search Bar** | [Bagshui](https://octowow.st/git/OEVG/Bagshui) already has a search box and sends items to trade, the auction house and mail on right-click. |
| **Auction Alt-Buy** | [aux](https://octowow.st/git/Dusk/aux-addon-ClassicAPI) already buys out or cancels with Alt+click on a row. |
## Removed later
| 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. |
| **Auto Dismount** | 3.8.1 | ClassicAPI provides Auto Dismount, Auto Stand and Auto Cancel Form. |
| **AoE Loot** | 3.8.6 | Removed in favor of a dedicated area-looting addon. Use [AutoAreaLoot](https://github.com/rotaruandrei93/AutoAreaLoot) for this functionality; check its README for required dependencies. The old ShaguTweaks setting is retired automatically. |
| **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.
- [ShaguPlates](https://github.com/Dinkleberrrg/ShaguPlates) for extensive customization.
+70
View File
@@ -0,0 +1,70 @@
# Healing prediction review — 2026-11-04
## Reviewed versions
- ShaguTweaks: local 3.8.2 test build based on `dfe8cea` (3.8.1).
- OEVG's private [Puppeteer](https://octowow.st/git/OEVG/Puppeteer):
`ee273a93ef7f6d3f60376b601e27a9ec2f49595f`. Fetched `origin/main` and verified
the clean local checkout matched it. This review does not use upstream
OldManAlpha Puppeteer as a substitute.
## Implemented in ShaguTweaks
- Prefer Puppeteer's public `PTHealPredict.GetIncomingHealing(guid)` and
`HookUpdates(callback)` when present and player GUID formats match. Use the
provider's total once, including its HoTs. Do not add its direct-heal subtotal
or ShaguTweaks estimates to it.
- Do not start ShaguTweaks' estimator event listener, combat-log processing,
HealComm reception/broadcasting or expiry polling while this shared provider
is active. A guarded callback is registered once per provider; repeated
enable/disable cycles do not accumulate listeners. Puppeteer owns its data
and its roster of tracked units; this integration does not modify either.
- Without that provider, keep direct cast estimates separate from HoT tick
samples. Learn periodic amounts only for the two displayed units, by caster
GUID and spell ID. Observe native aura timing/caster identity and removal;
unknown amounts or unknown timing are not invented.
- The displayed HoT contribution is **one upcoming tick per active HoT**,
matching Puppeteer's current total. It is not the sum of every future tick.
Rejuvenation and Regrowth can coexist; Regrowth's direct cast is not
confused with its periodic amount. Stopping a direct cast does not remove HoTs.
- Preserve learned values across form changes. Real gear/talent/level changes
invalidate the standalone player's own estimates. Reuse native positional
aura getters where possible; scan on events rather than on every frame.
- Expiry work stops when no prediction remains. Explicit Lua 5.0 array-length
bookkeeping prevents stale entries after repeated HoT expiry/refresh cycles.
## Improvements identified in private Puppeteer (not modified here)
1. **Caster-specific HoT identity.** `AddHot` stores
`IncomingHots[target][spellName]`; `UpdateCacheHot` and `RemoveHoT` use the
same name-only identity. If a server permits same-name HoTs from multiple
casters, the later one overwrites the earlier entry. The ClassicAPI aura
removal handler also supplies no caster identity to `RemoveHoT`. Prefer
caster GUID plus spell ID/aura instance, with tests for refresh/remove ordering.
2. **Native expiry rather than a fixed emergency timeout.** The normal path
removes a HoT on an aura-removal event, but the cleanup fallback drops all
HoTs after 25 seconds. Actual aura expiry would improve correctness when
events are missed and avoid treating every effect as the same duration.
3. **Persistent cache validity.** `PTHealCache` and `PTPlayerHealCache` retain
estimates across sessions; observed values converge gradually. A gear,
talent, level or build signature for the local player's estimates would
prevent an old build's values being used until enough new samples arrive.
Avoid invalidating on form-only spellbook/inventory notifications.
4. **Callback removal API.** `HookUpdates` appends a callback but exposes no
corresponding removal function. A token/unsubscribe API would allow addon
integrations to detach cleanly. ShaguTweaks currently registers once and
guards disabled callbacks.
5. **Clarify HoT semantics.** `GetIncomingHealing` adds each HoT's stored tick
amount, not all remaining ticks. The UI/help should make this explicit;
changing to total remaining healing would be a separate behavior change.
## Validation and limits
Lua 5.0 mocks exercise direct/periodic separation, multiple casters,
refresh/removal/expiry, gear changes, shared totals, provider absence/GUID
mismatch, late activation and disabled callbacks. Neither estimator promises
server-authoritative heal amounts. Unknown/cold caches, healing modifiers,
critical heals and remote visibility can affect estimates. Standalone reception
supports explicit-duration vanilla HealComm direct-heal messages; compact/group
protocols are not implemented. Native rendering, real provider event ordering,
and CPU/memory costs still require in-game verification.
+19 -5
View File
@@ -42,11 +42,25 @@ The separate 180-day cleanup in Social Colors is removed. It ran only every
## Range Color ## Range Color
The module shared Blizzard's `rangeTimer` and tested it against 0.2 seconds. 3.8.2 uses native action-range subscriptions and change events when available.
Once the timer had reset, that test was always true, so the full range check Visibility and paging maintain balanced references. Older supported APIs retain
ran on every frame for every ranged action button. It now keeps its own the independent 0.2-second timer introduced by the earlier fix; Blizzard's own
0.2-second timer. Blizzard's own range check runs again, so the hotkey text range timer remains untouched.
turns red out of range, as in the default UI.
## Health/power numbers and swing timers (3.8.2)
Health events refresh only health text, and power events refresh only power
text, including the alternate druid power display. Target/pet identity changes
and startup still refresh both.
Swing/wand timers use native duration objects and countdown bindings when
available. Cancellable timers cover due/grace and Hunter warning boundaries;
range and action-state events update colors. The native path has no Lua
OnUpdate driver. Older APIs retain a throttled fallback, with numeric-tenth
comparison before formatting text. Current recoloring behavior is unchanged.
These are reductions in Lua work, not measured FPS claims. Follow the
[final 3.8.2 validation/measurement plan](PERFORMANCE_TEST_382.md) for A/B results.
## Map and portrait checks ## Map and portrait checks
+93
View File
@@ -0,0 +1,93 @@
# 3.8.2 final validation and measurement
Only the player can run these checks in the game. No performance gain has been
measured by the automated tests. The live game folder was not modified.
## First verify behavior
- Real Health Numbers: take damage, heal, spend/regenerate power, switch targets
and summon/dismiss a pet. Druid forms should show the appropriate resource and
update the separate mana display. Check Big Health and standalone layouts.
- Swing/Wand Timer: attack, stop, move out of range, change targets, and change
weapons. Dual-wield bars and wand Shoot should reset and stop correctly.
- Druid Maul or Warrior Heroic Strike/Cleave: queue and cancel, checking colors.
Hunter Auto Shot: check the last-half-second red warning if a Hunter is available.
- Hold Ctrl+Shift+Alt during a swing: preview labels remain stable, countdowns
resume on release, and drag/reset/saved positions still work after reload.
- Energy & Mana Tick: test a fresh login, Cat/Bear/caster transitions, spending
mana, channel completion/cancellation, a potion and a drain if practical.
Verify the spark after the first observed gain, its five-second cast-cost
sweep, and resizing with Big Health/standalone bars.
- Macros: test bare/explicit/conditional #showtooltip, item macros, castsequence,
chosen icons and any legacy --showtooltip or /pfcast display hints you use.
Edit/delete a custom hint and reload; check for stale overrides. Check missing
/equip, /use, focus and attack aliases without replacing another addon's aliases.
- Shift-compare rings, trinkets and weapons from bags/vendors/chat links; check
one empty paired slot, enchanted/random-suffix items, Shift release, hover
changes and tooltip closure. Compare with other tooltip addons enabled.
## Find the remaining bottlenecks
Profile the current build first. The purpose is to identify expensive workloads
and then narrow them down to modules/functions, not to prove the recent changes
made the addon faster. Older builds are not required for this investigation.
Keep the same character, location, target, addon list, UI settings and frame-rate
limit. Enable Real Health Numbers and the applicable Swing/Wand Timer in both.
Warm up for two minutes after loading; do not include loading or option changes
in a measured run. Keep GentleGC and other client mods configured identically.
Enable profiling, then reload:
```
/console scriptProfile 1
/reload
```
Start each sample with:
```
/script ResetCPUUsage(); STPerfStart=GetTime()
```
Perform the chosen scenario for about 60 seconds, then print average addon Lua
CPU milliseconds per elapsed second:
```
/script UpdateAddOnCPUUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.3f ms/s over %.1f s",GetAddOnCPUUsage("ShaguTweaks")/(GetTime()-STPerfStart),GetTime()-STPerfStart))
```
Scenarios: (1) idle with no windows open; (2) steady melee/wand attacks;
(3) repeated damage/healing and power changes, including druid form changes;
(4) repeated map, inventory, mail and auction-window interactions. Record which
windows were used and how many times. Repeat each three times on the current
build and compare medians. Repeating comparable activity matters more than
identical duration alone.
These addon totals are the first triage step, not module/function attribution.
For workloads with meaningful cost, follow with focused function CPU profiling
and, where practical, repeat the same scenario with one relevant option disabled
(reload after changing options). That separates module cost from unrelated
activity and tells us where a code change might be worthwhile. Respect the
decision to preserve the working theme recoloring behavior.
At the same point after warm-up and after a fixed test session, sample Lua memory:
```
/script UpdateAddOnMemoryUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.1f KiB",GetAddOnMemoryUsage("ShaguTweaks")))
```
Memory includes uncollected objects; compare at equivalent GentleGC collection
points. Do not force collections during CPU samples. Native textures/DLL memory
and total garbage production are not represented by this single memory number.
Send the build, scenario, enabled options, three CPU results and memory readings.
Those results guide the next targeted investigation. Lower addon Lua
CPU does not by itself prove higher FPS: native timer work moves out of Lua,
profiling adds overhead, and other addons/client rendering may dominate. Do not
use GetFrameCPUUsage for this comparison. Afterward, turn profiling off:
```
/console scriptProfile 0
/reload
```
+119
View File
@@ -0,0 +1,119 @@
# 3.8.3 profiling: current bottlenecks and theme cost
Only the player can run these checks in the game. No performance gain has been
measured by the automated tests. The live game folder was not modified.
## Focused theme comparison
First measure the current configuration, then repeat with both recoloring options
off, Golden UI only, and Darkened UI only. Reload after each setting change.
Use the same addons, character, location, FPS limit and GentleGC configuration.
For every mode, warm up for two minutes and open the same windows once before
sampling. Then take three 60-second idle samples and three 60-second samples
opening/closing the same map, bags, mail and auction windows the same number of
times. Record CPU ms/s, memory before/after, and any hitches. Measure initial
window creation separately from repeated openings if only the first opening hitches.
| Mode | Scenario | CPU ms/s, three runs | Memory before/after | Hitches |
| --- | --- | --- | --- | --- |
| Both themes off | Idle / windows | | | |
| Golden UI only | Idle / windows | | | |
| Darkened UI only | Idle / windows | | | |
The reported 6.2 MB versus 6.9 MB is approximately 0.7 MB extra retained Lua
memory. These two snapshots do not measure allocation rate, CPU cost or a leak.
After several identical window-opening cycles and natural GC cycles, check
whether memory settles or keeps rising. Do not manually collect during CPU runs.
Also profile the current build in combat/forms/healing as below. Theme comparisons
isolate one option's cost; they do not establish gains over an older release.
## First verify behavior
- Real Health Numbers: take damage, heal, spend/regenerate power, switch targets
and summon/dismiss a pet. Druid forms should show the appropriate resource and
update the separate mana display. Check Big Health and Personal Resource Display layouts.
- Swing/Wand Timer: attack, stop, move out of range, change targets, and change
weapons. Dual-wield bars and wand Shoot should reset and stop correctly.
- Druid Maul or Warrior Heroic Strike/Cleave: queue and cancel, checking colors.
Hunter Auto Shot: check the last-half-second red warning if a Hunter is available.
- Hold Ctrl+Shift+Alt during a swing: preview labels remain stable, countdowns
resume on release, and drag/reset/saved positions still work after reload.
- Energy & Mana Tick: test a fresh login, Cat/Bear/caster transitions, spending
mana, channel completion/cancellation, a potion and a drain if practical.
Verify the spark after the first observed gain, its five-second cast-cost
sweep, and resizing with Big Health and Personal Resource Display bars.
- Macros: test bare/explicit/conditional #showtooltip, item macros, castsequence,
chosen icons and any legacy --showtooltip or /pfcast display hints you use.
Edit/delete a custom hint and reload; check for stale overrides. Check missing
/equip, /use, focus and attack aliases without replacing another addon's aliases.
- Shift-compare rings, trinkets and weapons from bags/vendors/chat links; check
one empty paired slot, enchanted/random-suffix items, Shift release, hover
changes and tooltip closure. Compare with other tooltip addons enabled.
## Find the remaining bottlenecks
Profile the current build first. The purpose is to identify expensive workloads
and then narrow them down to modules/functions, not to prove the recent changes
made the addon faster. Older builds are not required for this investigation.
Keep the same character, location, target, addon list, UI settings and frame-rate
limit. Enable Real Health Numbers and the applicable Swing/Wand Timer in both.
Warm up for two minutes after loading; do not include loading or option changes
in a measured run. Keep GentleGC and other client mods configured identically.
Enable profiling, then reload:
```
/console scriptProfile 1
/reload
```
Start each sample with:
```
/script ResetCPUUsage(); STPerfStart=GetTime()
```
Perform the chosen scenario for about 60 seconds, then print average addon Lua
CPU milliseconds per elapsed second:
```
/script UpdateAddOnCPUUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.3f ms/s over %.1f s",GetAddOnCPUUsage("ShaguTweaks")/(GetTime()-STPerfStart),GetTime()-STPerfStart))
```
Scenarios: (1) idle with no windows open; (2) steady melee/wand attacks;
(3) repeated damage/healing and power changes, including druid form changes;
(4) repeated map, inventory, mail and auction-window interactions. Record which
windows were used and how many times. Repeat each three times on the current
build and compare medians. Repeating comparable activity matters more than
identical duration alone.
These addon totals are the first triage step, not module/function attribution.
For workloads with meaningful cost, follow with focused function CPU profiling
and, where practical, repeat the same scenario with one relevant option disabled
(reload after changing options). That separates module cost from unrelated
activity and tells us where a code change might be worthwhile. Respect the
decision to preserve the working theme recoloring behavior.
At the same point after warm-up and after a fixed test session, sample Lua memory:
```
/script UpdateAddOnMemoryUsage(); DEFAULT_CHAT_FRAME:AddMessage(string.format("ShaguTweaks: %.1f KiB",GetAddOnMemoryUsage("ShaguTweaks")))
```
Memory includes uncollected objects; compare at equivalent GentleGC collection
points. Do not force collections during CPU samples. Native textures/DLL memory
and total garbage production are not represented by this single memory number.
Send the build, scenario, enabled options, three CPU results and memory readings.
Those results guide the next targeted investigation. Lower addon Lua
CPU does not by itself prove higher FPS: native timer work moves out of Lua,
profiling adds overhead, and other addons/client rendering may dominate. Do not
use GetFrameCPUUsage for this comparison. Afterward, turn profiling off:
```
/console scriptProfile 0
/reload
```
+52
View File
@@ -0,0 +1,52 @@
# UI option hotfix review — 2026-11-03
Scope: the 3.7.3 mail, trade, Golden UI/Darkened UI, aux/AuxCraft and Bagshui
compatibility changes. This is a focused review, not an audit of every module.
## Changes from the user-tested build
- Coalesce full outgoing-mail refreshes into one pending callback. Use the
existing feature-detected `API.Defer` bridge to ClassicAPI's native
`C_Timer.After(0, callback)`; retain the addon queue on older builds.
Skip the callback's work if the mail window has closed. Individual TurtleMail
attachments still update synchronously when their textures change.
- Remove the redundant full trade-update hook. Blizzard's full update already
calls both individually hooked slot-update functions for all seven slots.
- Preserve marked rarity colors and alpha when another addon reapplies a
backdrop; common-white rarity borders remain white under either palette.
- Keep the slot-art matcher limited to string texture paths.
- Remove a redundant boolean expression and expand a compressed refresh block.
The temporary diagnostic slash command was already removed.
## Performance assessment
The reviewed paths add no continuous OnUpdate driver. Work runs on item changes,
window shows, frame creation, or one-shot callbacks. The native timer is used for
scheduling, not recurring polling. New callbacks reuse their functions.
The palettes still traverse the frame tree at initialization and wrap frame
creation and border setters for compatible windows. This has a real startup and
UI-creation cost. Rarity updates also perform item lookups and border operations.
Four narrow texture strips are allocated once per slot background that needs
recoloring and then reused. The inbox also uses four parchment crops. Extra
regions consume memory and rendering work even though they need no Lua polling.
Removing the redundant trade hook avoids a second 14-slot rarity pass during a
full trade refresh. Coalescing mail refresh requests avoids duplicate work within
a scheduling interval. Neither improvement is an in-game benchmark.
No FPS, memory, GC or CPU measurements were taken in game. Lua mocks do not model
client rendering or DLL behavior. Use ClassicAPI's per-addon CPU/memory counters
with script profiling enabled for comparable fixed-duration scenarios if a
slowdown is suspected; do not use GetFrameCPUUsage for this comparison.
## Validation and remaining limits
Lua 5.0 checks are documented in `tests/README.md`. They cover qualities 0–5,
mail add/remove ordering, different items on each trade side, cleared slots,
option independence, slot reuse, palette persistence and timer fallback.
OEVG confirmed the preceding build's UI behavior in game. These cleanup changes
still need a short in-game smoke test: mail attachment add/remove, both trade
columns, Bagshui empty slots, and reopening aux/AuxCraft under each palette.
Keep version 3.7.3; no live game files were modified.
+3
View File
@@ -11,6 +11,9 @@ apply every listed license to the ShaguTweaks-ClassicAPI repository as a whole.
- `ShaguTweaks-mods-MIT.txt` — MIT license for the adapted - `ShaguTweaks-mods-MIT.txt` — MIT license for the adapted
TokensWorth/ShaguTweaks-mods material documented in TokensWorth/ShaguTweaks-mods material documented in
`THIRD_PARTY_NOTICES.md`. `THIRD_PARTY_NOTICES.md`.
- `ShaguTweaks-extras-MIT.txt` — MIT license of ShaguTweaks-extras, for the
modules merged from ShaguTweaks-Extras-ClassicAPI documented in
`THIRD_PARTY_NOTICES.md`.
- `pfUI-MIT.txt` — MIT license for the Swing Timer and Wand Timer ported from - `pfUI-MIT.txt` — MIT license for the Swing Timer and Wand Timer ported from
brues-code/pfUI, documented in `THIRD_PARTY_NOTICES.md`. brues-code/pfUI, documented in `THIRD_PARTY_NOTICES.md`.
- `ClassicAPI-GPL-3.0.txt` — verbatim license document from the external - `ClassicAPI-GPL-3.0.txt` — verbatim license document from the external
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 Eric Mauser (Shagu)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+115 -27
View File
@@ -1,19 +1,22 @@
# 🧩 ShaguTweaks — ClassicAPI # 🧩 ShaguTweaks — ClassicAPI
A lightweight fork of [ShaguTweaks](https://github.com/shagu/ShaguTweaks) for **Turtle WoW-like servers**, focused on **stability, compatibility and performance** while keeping the original ShaguTweaks experience. [![ClassicAPI](https://img.shields.io/badge/ClassicAPI-Required-purple.svg)](https://github.com/brues-code/ClassicAPI)
[![SuperWoW](https://img.shields.io/badge/SuperWoW-Optional-yellow.svg)](https://github.com/balakethelock/SuperWoW)
This is an English-only customization of [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI), based on ShaguTweaks, for the Vanilla client environment with ClassicAPI. English-only fork of [Dusk/ShaguTweaks-ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-ClassicAPI), based on [ShaguTweaks](https://github.com/shagu/ShaguTweaks), for the **Turtle WoW-like servers**.
> This fork requires ClassicAPI and targets **Turtle WoW-like server/client environments**. Focused on **stability, compatibility, customization, and performance** while keeping the original ShaguTweaks experience.
**Version 3.6.** What changed in each update: see the [changelog](CHANGELOG.md). **Version 3.8.7.** 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. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for removed modules.
## 🔌 Requirements ## 🔌 Requirements
- **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required.** Install both the DLL and its bundled `!!!ClassicAPI` addon, and restart the client after a DLL update. v1.18.10 or newer is recommended (it includes a crash fix for addons that restyle nameplates). The integration was last reviewed against v1.18.13; see [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md).
- **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional / recommended.** Only used as an additional fallback for some cast/GUID information.
ClassicAPI is the main API layer. - **[ClassicAPI](https://github.com/brues-code/ClassicAPI) — required.** Install both the DLL and its bundled `!!!ClassicAPI` addon, and restart the client after a DLL update. v1.18.10 or newer is required (it includes a crash fix for addons that restyle nameplates). The addon checks the loaded DLL version and core APIs before enabling tweaks, and feature-detects newer range and duration-timer APIs. See [current integration notes and the historical review](Docs/CLASSICAPI_UPDATES.md). ClassicAPI is the main API layer.
- **[SuperWoW](https://github.com/balakethelock/SuperWoW) — optional and recommended.** Supplements missing enemy cast timing when Enemy Castbars and Super WoW Compatibility are enabled. Complete ClassicAPI records are used directly without a second cache. See the [cast tracking review](Docs/CAST_TRACKING_REVIEW.md).
- **[ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) — no longer used.** Its modules are built in since 3.7 (see [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md)). Disable or remove it; while it is enabled, its copies replace the built-in ones.
## 📦 Installation ## 📦 Installation
@@ -25,21 +28,57 @@ ClassicAPI is the main API layer.
Settings: **Esc → Advanced Options**. Settings: **Esc → Advanced Options**.
## 📷 Screenshots
Personal Resource Display, independently positioned player name and movable action bars with Golden UI.
![Personal Resource Display and movable action bars](screenshots/personal-resource-display.png)
Golden UI world map with the OctoBus Transports panel.
![Golden UI world map with OctoBus integration](screenshots/golden-world-map.png)
Modifier-key previews for movable action bars, cast bars and personal resources.
![Movable action bars and cast-bar previews](screenshots/movable-bars-preview.png)
Advanced Options with search and grouped settings.
![Advanced Options menu with search and UI mode selection](screenshots/advanced-options-3.8.5.png)
## ✨ What's different in this fork ## ✨ What's different in this fork
**Proc & Reactive Spell Highlights** (Action Bar, off): pale light-blue action-button glows for supported procs and reactive spells, including macros. Uses ClassicAPI glow events and native animation where available, with a static fallback. Central artwork is left to Proc Doc or other alert addons. Requires ClassicAPI spell-overlay support; older supported DLLs leave this feature inactive.
**Unit frames** **Unit frames**
- **Hide Player Name** (off): hides the name and raid group number while **Hide Player Frame Resources** is enabled. Its checkbox is unavailable without that prerequisite.
- **Unit Frame Energy & Mana Tick** (off): uses native spark movement when available, retains the five-second mana sweep for mana-costing spells, and observes the two-second regen phase separately. Form changes reset power samples; drains and mid-period gains do not falsely start/reset the cast-cost display. The spark also appears on the Personal Resource Display power bar, sharing timing and resizing with either bar.
- **Healing Predictions** (off): a green segment shows incoming direct heals plus one upcoming tick per active HoT, capped at maximum health. Reuses compatible Puppeteer prediction data when available, including its HoTs, without starting a second estimator. Otherwise ClassicAPI supplies cast/aura/combat events: own direct heals need an observed non-critical heal per spell rank each session, and HoTs such as Rejuvenation/Regrowth need an observed tick amount and identified live aura. Form changes preserve estimates; actual gear, talent or level changes invalidate own standalone samples. Other direct heals can arrive through explicit-duration vanilla HealComm messages. Unknown amounts/timing and percentage-only target health are not projected. Supports Big Health and Personal Resource Display. See the [estimator review](Docs/HEALING_PREDICTION_REVIEW.md).
- **Personal Resource Display** (off): independent copies of your health and power bars, leaving the original player frame intact. Hold **Ctrl+Shift+Alt** to drag either bar or resize it with its bottom-right grip; right-click with those keys held to reset that bar. Positions and sizes are saved per character. Big Health for Player gives both bars the enlarged height. Copied bars share native values, textures and colors, plus Healing Predictions when enabled. Replaces Standalone Player Frame and migrates its health/power positions.
- **Hide Player Frame Resources** (off): hides the original health and power bars, leaving a clean rounded portrait and a class-colored name. Independent of Personal Resource Display; use either or both. Ctrl+Shift+Alt-drag moves the name independently; right-click with those keys held resets it beside the portrait. Its position is saved per character. **Movable Unit Frames** moves the portrait; an undetached name follows it. The portrait keeps its native left level badge and resting/combat icons.
- **Modern Class Colors:** optional palette for all nine Vanilla classes, including blue shamans. Listed directly below **Unit Frame Class Colors**. - **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. - **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. - **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. - **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. The player option also class-colors the player health bar and the Personal Resource Display health bar when enabled. Both are presentation-only and do not force numeric health/power text. 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. - **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. - **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)
- **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`.
**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** **Swing Timer and Wand Timer**
- **Swing Timer:** main-hand and off-hand swing timers styled like the classic cast bar, with the weapon's icon on the left. They sit just above the cast bar; hold **Ctrl+Shift+Alt** to drag them and right-click to reset. Current ClassicAPI animates the fill and countdown through native duration objects, following pfUI's approach, without a Lua per-frame driver. Older supported APIs keep a throttled fallback. Styling, movement, range/queued-attack colors and Hunter warning timing remain ShaguTweaks' own.
- **Swing Timer:** main-hand and off-hand swing timers styled like the classic cast bar, with the weapon's icon on the left. They sit just above the cast bar until you move them; hold **Ctrl+Shift+Alt** to drag each bar (main hand, off hand and the ranged/wand bar) on its own, and right-click a bar to put just that one back.
- **Wand Timer:** the time until your next wand shot in the same style. It also times Auto Shot for bows, guns and crossbows, turning red during the last half second, when moving would cancel the shot. - **Wand Timer:** the time until your next wand shot in the same style. It also times Auto Shot for bows, guns and crossbows, turning red during the last half second, when moving would cancel the shot.
- The timers follow the server's real attack timers through ClassicAPI's `PLAYER_SWING` event, including parry haste and on-next-swing abilities. A bar turns gray while the target is out of reach. - The timers follow the server's real attack timers through ClassicAPI's `PLAYER_SWING` event, including parry haste and on-next-swing abilities. A bar turns gray while the target is out of reach.
- The main-hand bar shows a queued **Heroic Strike** in light blue and **Cleave** in green (warriors), and **Maul** in orange (druids). - The main-hand bar shows a queued **Heroic Strike** in light blue and **Cleave** in green (warriors), and **Maul** in orange (druids).
@@ -47,19 +86,27 @@ Settings: **Esc → Advanced Options**.
**Tooltips, items and action bars** **Tooltips, items and action bars**
- **Equip Compare:** Shift shows native equipped-item comparisons from full item links, including paired slots and stat differences, while retaining our placement. Older APIs keep the previous comparison path.
- **Macro Icons:** current ClassicAPI handles ordinary directives, conditions and chosen icons. This option adds compatibility for legacy `--showtooltip` and `/pfcast` display hints; only conditional custom hints need periodic re-evaluation. **Macro Tweaks** supplies missing interactive command aliases through the native parser and preserves command-history filtering. Other macro parsers/registered aliases keep ownership.
- **Tooltip Details:** restored guild ranks and leader marker; optional modern class colors and health texture. NPC interaction and tooltip-clear handling prevent cached colors leaking between tooltips. - **Tooltip Details:** restored guild ranks and leader marker; optional modern class colors and health texture. NPC interaction and tooltip-clear handling prevent cached colors leaking between tooltips.
- **Item Rarity Borders** cover merchant, quest, mail, trade, crafting and loot frames, and **Item Rarity Glows** add an independent glow using the same shared rarity engine. - **Item Rarity Borders** cover merchant, quest, mail (including every TurtleMail attachment), both sides of trade, crafting and loot frames in every UI mode. **Item Rarity Glows** add an independent glow using the same shared rarity engine. **Loot Quality** colors the loot window itself and does not enable mail or trade borders.
- **Range Color:** self/rangeless spells no longer use a misleading legacy fallback; invalid friendly/hostile target checks and stale red/no-mana tint are handled. No additional AoE-specific exceptions have been added. The range check runs five times a second instead of every frame. - **Range Color:** self/rangeless spells no longer use a misleading legacy fallback; invalid friendly/hostile target checks and stale red/no-mana tint are handled. No additional AoE-specific exceptions have been added. The range check runs five times a second instead of every frame.
- **XP Bar Text:** hovering the experience bar shows current and needed XP like the reputation bar (`1555/3000`), then the percentage and the rested bonus. - **XP Bar Text:** hovering the experience bar shows current and needed XP like the reputation bar (`1555/3000`), then the percentage and the rested bonus.
- **WorldMap Class Colors:** raid members on the world map and battlefield minimap show their own class color. - **WorldMap Class Colors:** raid members on the world map and battlefield minimap show their own class color.
- **WorldMap Window:** the map keeps the size you pick with Ctrl+Mousewheel (saved per character), and the title bar drags the window as well. With [OctoBus](https://github.com/Phridgey/OctoBus) installed, a **Transports** button left of the Continent dropdown shows or hides its transport timers. - **WorldMap Window:** the map keeps the size you pick with Ctrl+Mousewheel (saved per character), and the title bar drags the window as well. With [OctoBus](https://github.com/Phridgey/OctoBus) installed, a **Transports** button left of the Continent dropdown shows or hides its transport timers.
- **Loot History:** `/loothistory` lists recent group-loot rolls with the winner of each item; click an item for everyone's need, greed or pass and their rolls. Ported from [brues-code/pfUI](https://github.com/brues-code/pfUI). Requires ClassicAPI. - **Loot History:** `/loothistory` lists recent group-loot rolls with the winner of each item; click an item for everyone's need, greed or pass and their rolls. Ported from [brues-code/pfUI](https://github.com/brues-code/pfUI). Requires ClassicAPI.
**Built-in Extras modules** — full list of where each one went and what was left out: [Docs/EXTRAS_MERGE.md](Docs/EXTRAS_MERGE.md)
- The modules of Dusk's [ShaguTweaks Extras ClassicAPI](https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI) are part of this addon, each under the matching category: Action Bar, Chat & Social, Loot, Minimap & World Map, Tooltip & Items and Unit Frames, plus two new categories, **Macro** and **Merchant**.
- They keep their names, so your saved on/off choices carry over.
- Left out: **Raid Frames**; **Bag Item Click** and **Bag Search Bar**, which [Bagshui](https://octowow.st/git/OEVG/Bagshui) already covers; and **Auction Alt-Buy**, which [aux](https://octowow.st/git/Dusk/aux-addon-ClassicAPI) already does (Alt+click a row to buy out or cancel).
**Performance and stability** **Performance and stability**
- Reduced per-frame work, tooltip scans and repeated UI updates. - 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). - **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 GUID-based handling for same-name enemies.
- Safer hooks and fewer destructive global overrides. - Safer hooks and fewer destructive global overrides.
- **English-only loading:** only English data and translations are included, avoiding unused locale allocations. - **English-only loading:** only English data and translations are included, avoiding unused locale allocations.
@@ -72,8 +119,6 @@ ClassicAPI backs casts, auras, items and unit data: native cooldown support, fil
Modules that use ClassicAPI directly: Modules that use ClassicAPI directly:
- Range Color - Range Color
- AoE Loot
- Auto Dismount
- Chat Tweaks - Chat Tweaks
- Equip Compare - Equip Compare
- Free Slot Count - Free Slot Count
@@ -83,16 +128,22 @@ Modules that use ClassicAPI directly:
- Loot History - Loot History
- Movable Unit Frames - Movable Unit Frames
- Real Health Numbers - Real Health Numbers
- Nameplate Class Colors
- Nameplate Castbar
- No Toggle Auto-Attack - No Toggle Auto-Attack
- Sell Junk - Sell Junk
- Skip Gossip Text - Skip Gossip Text
- Swing Timer - Swing Timer
- Wand Timer - Wand Timer
- Loss of Control Alert
- Totem Timers
- Enemy Castbars - Enemy Castbars
- Tooltip Details - Tooltip Details
- WorldMap Window - WorldMap Window
- Buy Em All
- Macro Icons and Macro Tweaks
- MiniMap Framerate & Latency
- Reagent Counter
- Reveal World Map
- Show Bags and Show Micro Menu
Other modules can also benefit indirectly from ClassicAPI through shared ShaguTweaks libraries and caches. Per-version notes are in [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md). Other modules can also benefit indirectly from ClassicAPI through shared ShaguTweaks libraries and caches. Per-version notes are in [Docs/CLASSICAPI_UPDATES.md](Docs/CLASSICAPI_UPDATES.md).
@@ -102,36 +153,45 @@ Names match the in-game options list. Modules marked *(off)* are disabled until
### Action Bar ### Action Bar
- Auto Dismount - Auto Stance (enters a required stance/form; to cast Moonfire from Cat Form, enable ClassicAPI Auto Cancel Form instead)
- Auto Stance
- Range Color *(off)* - Range Color *(off)*
- Cooldown Numbers - Cooldown Numbers
- Hide Gryphons *(off)* - Hide Gryphons *(off)*
- Improved Castbar *(off)* - Improved Castbar *(off)*
- Cast-bar movement is built in: hold Ctrl+Shift+Alt to drag it, right-click to reset (no checkbox)
- Swing Timer *(off)* - Swing Timer *(off)*
- Wand Timer *(off)* - Wand Timer *(off)*
- No Toggle Auto-Attack - No Toggle Auto-Attack
- Reduced Actionbar Size *(off)* - Reduced Actionbar Size *(off)*
- Show Bags *(off)*
- Show Micro Menu *(off)*
- Reagent Counter *(off)*
- Hide Macro Text *(off)*
- Mouseover Right *(off)*
- Mouseover Right 2 *(off)*
- Center Vertical Actionbar *(off)*
- Floating Actionbar *(off)*
- Dragonflight Gryphons *(off)*
### Unit Frames ### 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)* - Unit Frame Class Colors *(off)*
- Modern Class Colors *(off)* - Modern Class Colors *(off)*
- Unit Frame Class Portraits *(off)* - Unit Frame Class Portraits *(off)*
- Unit Frame Energy & Mana Tick *(off)* - Unit Frame Energy & Mana Tick *(off)*
- Hide Pet Unit Frame *(off)* - Hide Pet Unit Frame *(off)*
- Unit Frame Abbreviated Names *(off)*
- Movable Unit Frames - Movable Unit Frames
- Real Health Numbers *(off)* - Real Health Numbers *(off)*
- White Unit Frame Numbers *(off)* - White Unit Frame Numbers *(off)*
- Enemy Castbars - 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 ### Tooltip & Items
@@ -142,6 +202,9 @@ Names match the in-game options list. Modules marked *(off)* are disabled until
- Sell Junk - Sell Junk
- Tooltip Details - Tooltip Details
- Vendor Values - Vendor Values
- Metric Range
- Cursor Tooltip *(off)*
- Hide Combat Tooltip *(off)*
### Chat & Social ### Chat & Social
@@ -150,6 +213,11 @@ Names match the in-game options list. Modules marked *(off)* are disabled until
- Chat Levels *(off)* - Chat Levels *(off)*
- Chat Spam Filter *(off)* - Chat Spam Filter *(off)*
- Social Colors - Social Colors
- Chat Timestamps *(off)*
- Enable Text Shadow *(off)*
- Center Text Input Box *(off)*
- Block NPC Spam *(off)*
- World Chat Hider *(off)*
### Minimap & World Map ### Minimap & World Map
@@ -162,19 +230,36 @@ Names match the in-game options list. Modules marked *(off)* are disabled until
- WorldMap Class Colors - WorldMap Class Colors
- WorldMap Coordinates - WorldMap Coordinates
- WorldMap Window - WorldMap Window
- Reveal World Map
- MiniMap Framerate & Latency *(off)*
### Loot ### Loot
- AoE Loot *(off)*
- Improved Roll Frames *(off)* - Improved Roll Frames *(off)*
- Loot Cursor - Loot Cursor
- Loot History (`/loothistory`) - Loot History (`/loothistory`)
- Loot Quality *(off)*
### Macro
- Macro Icons
- Macro Tweaks
### Merchant
- Buy Em All
### Interface ### Interface
- Darkened UI *(off)* - **UI:** dropdown below the Advanced Options search bar, with **Default**, **Darkened** and **Golden** (Default initially). Select one mode and press Okay to apply/reload. Existing theme selections carry over; old saves with both enabled retain Darkened priority. Both themed modes recolor supported addon borders and minimap buttons. Golden uses gold borders, tooltips and dialogs like WoW Forever while retaining default text colors. TrinketMenu keeps its own styling.
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)* - Hide Errors *(off)*
- Smaller Error Frame *(off)* - Smaller Error Frame *(off)*
- Loss of Control Alert *(off)*
- Totem Timers *(off, Shaman)*
- XP Bar Text (current/needed XP, percentage and rested bonus on hover) - XP Bar Text (current/needed XP, percentage and rested bonus on hover)
### General ### General
@@ -189,6 +274,7 @@ The following original ShaguTweaks modules are not included as standalone module
- **Chat Links** → integrated into **Chat Tweaks** - **Chat Links** → integrated into **Chat Tweaks**
- **Movable Unit Frames Extended** → integrated into **Movable Unit Frames** - **Movable Unit Frames Extended** → integrated into **Movable Unit Frames**
- **ShaguTweaks Extras ClassicAPI** → its modules are built in, except Raid Frames, Bag Item Click, Bag Search Bar and Auction Alt-Buy
## 📜 Project identity & licensing ## 📜 Project identity & licensing
@@ -215,14 +301,16 @@ Additional maintenance and Turtle WoW work by **paokkerkir**.
ClassicAPI compatibility fork maintained by **Dusk-92**. ClassicAPI compatibility fork maintained by **Dusk-92**.
Special thanks to **Sandrea** for providing **AoELoot**, adapted here as the **AoE Loot** module. The former **AoE Loot** module was removed in 3.8.6. See the [migration guide](Docs/EXTRAS_MERGE.md#removed-later) for the recommended replacement.
Special thanks to **Jesper_92** for reporting issues that helped improve the addon. Special thanks to **Jesper_92** for reporting issues that helped improve the addon.
Additional code and ideas from **pfUI** and **zUI**. Additional code and ideas from **pfUI** and **zUI**.
Extras modules from **Dusk-92**'s ShaguTweaks-Extras-ClassicAPI, a fork of **paokkerkir**'s ShaguTweaks-extras (MIT, Shagu).
Swing Timer, Wand Timer and Loot History ported from **brues-code/pfUI** (MIT, Shagu and contributors). Swing Timer, Wand Timer and Loot History ported from **brues-code/pfUI** (MIT, Shagu and contributors).
Extended Movable Unit Frames support adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`. Extended Movable Unit Frames support and several of the Extras modules adapted from **TokensWorth/ShaguTweaks-mods**, originally released under MIT by **GryllsAddons**. See `THIRD_PARTY_NOTICES.md`.
Released under the original **MIT License**. Released under the original **MIT License**.
+47 -8
View File
@@ -2,7 +2,7 @@
## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI] ## Title: |cffffcc00Shagu|cffffffffTweaks |cffcccccc[ClassicAPI]
## Author: Shagu ## Author: Shagu
## Notes: A collection of tweaks and enhancements to the Default UI ## Notes: A collection of tweaks and enhancements to the Default UI
## Version: 3.6 ## Version: 3.8.7
## Dependencies: !!!ClassicAPI ## Dependencies: !!!ClassicAPI
## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache ## SavedVariables: ShaguTweaks_player_cache, ShaguTweaks_social_cache
## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache ## SavedVariablesPerCharacter: ShaguTweaks_config, ShaguTweaks_cache
@@ -23,7 +23,6 @@ config.lua
libs\libtipscan.lua libs\libtipscan.lua
libs\libspell.lua libs\libspell.lua
libs\libcast.lua libs\libcast.lua
libs\libdebuff.lua
libs\libunitscan.lua libs\libunitscan.lua
libs\libnameplate.lua libs\libnameplate.lua
libs\libpredict.lua libs\libpredict.lua
@@ -43,22 +42,18 @@ mods\minimap-clean.lua
mods\minimap-zoom.lua mods\minimap-zoom.lua
mods\minimap-tracking.lua mods\minimap-tracking.lua
mods\actionbar-range-color.lua mods\actionbar-range-color.lua
mods\actionbar-proc-highlights.lua
mods\free-slots.lua mods\free-slots.lua
mods\xpbar-text.lua mods\xpbar-text.lua
mods\nameplate-scale.lua
mods\sell-junk.lua mods\sell-junk.lua
mods\unitframes-classcolor.lua mods\unitframes-classcolor.lua
mods\vendor-values.lua mods\vendor-values.lua
mods\worldmap-coordinates.lua mods\worldmap-coordinates.lua
mods\worldmap-window.lua mods\worldmap-window.lua
mods\nameplate-classcolor.lua
mods\nameplate-castbar.lua
mods\move-unitframes.lua mods\move-unitframes.lua
mods\target-castbar.lua mods\target-castbar.lua
mods\target-debufftimer.lua
mods\social-colors.lua mods\social-colors.lua
mods\item-colors.lua mods\item-colors.lua
mods\auto-dismount.lua
mods\auto-stance.lua mods\auto-stance.lua
mods\tooltip-details.lua mods\tooltip-details.lua
mods\hide-errors.lua mods\hide-errors.lua
@@ -69,23 +64,67 @@ mods\hide-gryphons.lua
mods\worldmap-colors.lua mods\worldmap-colors.lua
mods\unitframes-bighealth.lua mods\unitframes-bighealth.lua
mods\health-numbers.lua mods\health-numbers.lua
mods\unitframes-healing-prediction.lua
mods\unitframes-classportrait.lua mods\unitframes-classportrait.lua
mods\unitframes-hide-player-name.lua
mods\unitframes-hide-player-resources.lua
mods\unitframes-personal-resource-display.lua
mods\unitframes-energy-tick.lua mods\unitframes-energy-tick.lua
mods\unitframes-hide-pet.lua mods\unitframes-hide-pet.lua
mods\cooldown-numbers.lua mods\cooldown-numbers.lua
mods\turtle-wow.lua mods\turtle-wow.lua
mods\superwow.lua mods\superwow.lua
mods\reduced-actionbar.lua mods\reduced-actionbar.lua
mods\actionbar-move.lua
mods\actionbar-grid.lua
mods\no-toggle.lua mods\no-toggle.lua
mods\aoe-loot.lua
mods\loot-cursor.lua mods\loot-cursor.lua
mods\loot-history.lua mods\loot-history.lua
mods\loss-of-control.lua
mods\totem-timers.lua
mods\improved-roll-frames.lua mods\improved-roll-frames.lua
mods\improved-castbar.lua mods\improved-castbar.lua
mods\castbar-move.lua
mods\swing-timer.lua mods\swing-timer.lua
mods\smaller-errors-frame.lua mods\smaller-errors-frame.lua
mods\skip-gossip.lua mods\skip-gossip.lua
# modules merged from ShaguTweaks Extras ClassicAPI (Dusk)
# action bar
mods\actionbar-df-gryphon.lua
mods\actionbar-right-center.lua
mods\actionbar-reagents.lua
mods\actionbar-float.lua
mods\reduced-actionbar-bags.lua
mods\reduced-actionbar-micromenu.lua
mods\actionbar-mouseover-common.lua
mods\actionbar-mouseover-bar-right.lua
mods\actionbar-mouseover-bar-right2.lua
mods\actionbar-hide-macro.lua
# merchant
mods\buy-em-all.lua
# loot
mods\loot-quality.lua
# tooltip & items
mods\metric-range.lua
mods\cursor-tooltip.lua
mods\hide-combat-tooltip.lua
# unit frames
mods\unitframes-abbrev-names.lua
# minimap & world map
mods\minimap-framerate-latency.lua
mods\worldmap-reveal.lua
# macro
mods\macro-icons.lua
mods\macro-tweaks.lua
# chat & social
mods\chat-filter-common.lua
mods\chat-npc-spam.lua
mods\world-chat-hider.lua
mods\chat-input-center.lua
mods\chat-text-shadow.lua
mods\chat-timestamps.lua
# compatibility # compatibility
compat-dragonflightui.lua compat-dragonflightui.lua
compat-guda.lua compat-guda.lua
+66 -3
View File
@@ -33,9 +33,60 @@ from `TokensWorth/ShaguTweaks-mods`:
- Copyright (c) 2022 GryllsAddons - Copyright (c) 2022 GryllsAddons
- License: MIT - License: MIT
The following modules, merged from ShaguTweaks-Extras-ClassicAPI, also
contain code adapted from `TokensWorth/ShaguTweaks-mods` (as recorded by that
project):
- `mods/actionbar-mouseover-common.lua`
- `mods/actionbar-mouseover-bar-right.lua`
- `mods/actionbar-mouseover-bar-right2.lua`
- `mods/actionbar-hide-macro.lua`
- `mods/unitframes-abbrev-names.lua`
- `mods/cursor-tooltip.lua`
- `mods/hide-combat-tooltip.lua`
- `mods/minimap-framerate-latency.lua`
- `mods/chat-npc-spam.lua`
- `mods/world-chat-hider.lua`
- `mods/loot-quality.lua`
The preserved license text is stored in The preserved license text is stored in
`LICENSES/ShaguTweaks-mods-MIT.txt`. `LICENSES/ShaguTweaks-mods-MIT.txt`.
## ShaguTweaks-Extras-ClassicAPI
These modules were merged from Dusk-92's ShaguTweaks-Extras-ClassicAPI
(commit `8e14bb4`, 2026-09-18), itself a fork of paokkerkir's
ShaguTweaks-extras:
- Action bar: `actionbar-df-gryphon.lua`, `actionbar-right-center.lua`,
`actionbar-reagents.lua`, `actionbar-float.lua`,
`reduced-actionbar-bags.lua`, `reduced-actionbar-micromenu.lua`,
`actionbar-mouseover-common.lua`, `actionbar-mouseover-bar-right.lua`,
`actionbar-mouseover-bar-right2.lua`, `actionbar-hide-macro.lua`
- Merchant: `buy-em-all.lua`
- Loot: `loot-quality.lua`
- Tooltip & items: `metric-range.lua`, `cursor-tooltip.lua`,
`hide-combat-tooltip.lua`
- Unit frames: `unitframes-abbrev-names.lua`
- Minimap & world map: `minimap-framerate-latency.lua`, `worldmap-reveal.lua`
- Macro: `macro-icons.lua`, `macro-tweaks.lua`
- Chat & social: `chat-filter-common.lua`, `chat-npc-spam.lua`,
`world-chat-hider.lua`, `chat-input-center.lua`, `chat-text-shadow.lua`,
`chat-timestamps.lua`
Their option categories, texture paths and one chat message were changed for
the merge; the rest of the code is unchanged. The textures
`img/df-gryphon.tga`, `img/df-gryphon-beta.tga`, `img/df-wyvern.tga` and
`img/xpbar.tga` come from the same tree (see `Docs/ASSET_PROVENANCE.md`).
- Sources: https://octowow.st/git/Dusk/ShaguTweaks-Extras-ClassicAPI and
https://github.com/paokkerkir/ShaguTweaks-extras
- Copyright (c) 2025 Eric Mauser (Shagu)
- License: MIT
The preserved license text is stored in
`LICENSES/ShaguTweaks-extras-MIT.txt`.
## brues-code/pfUI ## brues-code/pfUI
The Swing Timer and Wand Timer in `mods/swing-timer.lua` are ported from The Swing Timer and Wand Timer in `mods/swing-timer.lua` are ported from
@@ -66,12 +117,13 @@ ClassicAPI binaries or source are distributed as part of this addon.
## AoELoot ## AoELoot
`mods/aoe-loot.lua` is adapted from the standalone AoELoot addon provided to Before 3.8.6, `mods/aoe-loot.lua` was adapted from the standalone AoELoot
the project by Sandrea: addon provided to the project by Sandrea. The module has since been removed;
this historical attribution remains for earlier releases:
- Original addon title: `AoELoot` - Original addon title: `AoELoot`
- Original metadata credit: Sandrea / ChatGPT - Original metadata credit: Sandrea / ChatGPT
- Adapted module: `mods/aoe-loot.lua` - Former adapted module: `mods/aoe-loot.lua`
The supplied package did not include an upstream URL or a separate license The supplied package did not include an upstream URL or a separate license
file. This notice records the known provenance and preserves attribution; it file. This notice records the known provenance and preserves attribution; it
@@ -175,3 +227,14 @@ Matching atlas metadata and conversion details are recorded in `Docs/MODERN_MANA
### Frame-specific resource exports ### Frame-specific resource exports
Additional img/ModernPower*Class.tga, *Target.tga, *Small.tga and *Party.tga variants use the matching Blizzard atlas crops documented in Docs/MODERN_MANA.md. Player/target/small variants come from FileDataID 4642466; party variants come from FileDataID 4681512, interface/hud/uipartyframe.blp. Both were retrieved for build 12.1.5.70448 from wago.tools. All resource exports now use bilinear resizing to compact power-of-two TGA dimensions. These remain Blizzard game artwork; no MIT relicensing is asserted. Additional img/ModernPower*Class.tga, *Target.tga, *Small.tga and *Party.tga variants use the matching Blizzard atlas crops documented in Docs/MODERN_MANA.md. Player/target/small variants come from FileDataID 4642466; party variants come from FileDataID 4681512, interface/hud/uipartyframe.blp. Both were retrieved for build 12.1.5.70448 from wago.tools. All resource exports now use bilinear resizing to compact power-of-two TGA dimensions. These remain Blizzard game artwork; no MIT relicensing is asserted.
## Additional ClassicAPI implementation reference (3.8.2)
brues-code/pfUI at commit 560343bd746e00898606cf756bdfe533a4823110 was reviewed for native action-range subscription ownership and the separation of incoming-heal estimation from frame overlays. Source: https://github.com/brues-code/pfUI . The focused ShaguTweaks implementations retain their own frame and module architecture.
The swing/wand timer also adapts that commit's `modules/swingtimer.lua` native duration/text-binding, fill-edge anchoring and cancellable boundary-timer approach (MIT, Shagu and contributors). ShaguTweaks retains its classic artwork, movement, colors, grace behavior and older-API fallback.
Energy/mana tick phase locking and native fill-edge spark movement were also
adapted from that commit's `modules/energytick.lua`; native equipment comparison
was informed by `modules/eqcompare.lua` (MIT, Shagu and contributors). Existing
ShaguTweaks presentation and older-API paths remain separate.
+83 -54
View File
@@ -7,6 +7,16 @@ ShaguTweaks.API = ShaguTweaks.API or {}
local API = ShaguTweaks.API local API = ShaguTweaks.API
API.classicapi_version = tonumber(_G.CLASSIC_API_VERSION) or 0 API.classicapi_version = tonumber(_G.CLASSIC_API_VERSION) or 0
API.minimum_version = 11810
API.minimum_version_text = "1.18.10"
API.IsSupportedRuntime = function()
-- The addon dependency checks the companion addon, not the loaded DLL.
return (tonumber(_G.CLASSIC_API_VERSION) or 0) >= API.minimum_version
and type(_G.C_Timer) == "table" and type(_G.C_Timer.After) == "function"
and type(_G.C_EventUtils) == "table" and type(_G.C_EventUtils.IsEventValid) == "function"
and type(_G.C_NamePlate) == "table" and type(_G.C_NamePlate.GetNamePlateForUnit) == "function"
and _G.C_EventUtils.IsEventValid("NAME_PLATE_UNIT_ADDED")
end
API.classicapi = type(_G.C_NamePlate) == "table" API.classicapi = type(_G.C_NamePlate) == "table"
or type(_G.C_Spell) == "table" or type(_G.C_Spell) == "table"
or type(_G.C_UnitAuras) == "table" or type(_G.C_UnitAuras) == "table"
@@ -50,9 +60,6 @@ API.containeritems = type(_G.C_Container) == "table"
API.containeriteminfo = type(_G.C_Container) == "table" API.containeriteminfo = type(_G.C_Container) == "table"
and type(_G.C_Container.GetContainerItemInfo) == "function" and type(_G.C_Container.GetContainerItemInfo) == "function"
API.containeropenable = type(_G.C_Container) == "table"
and type(_G.C_Container.IsContainerItemOpenable) == "function"
API.items = type(_G.C_Item) == "table" API.items = type(_G.C_Item) == "table"
API.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function" API.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function"
API.iteminfoinstant = API.items API.iteminfoinstant = API.items
@@ -80,6 +87,19 @@ API.eventutils = type(_G.C_EventUtils) == "table"
API.timer = type(_G.C_Timer) == "table" API.timer = type(_G.C_Timer) == "table"
and type(_G.C_Timer.After) == "function" and type(_G.C_Timer.After) == "function"
API.durationtimers = type(_G.C_DurationUtil) == "table"
and type(_G.C_DurationUtil.CreateDuration) == "function"
and type(_G.C_DurationUtil.CreateDurationTextBinding) == "function"
and type(_G.C_StringUtil) == "table"
and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function"
and type(_G.C_Timer) == "table" and type(_G.C_Timer.NewTimer) == "function"
and type(_G.Enum) == "table" and type(_G.Enum.StatusBarInterpolation) == "table"
and type(_G.Enum.StatusBarTimerDirection) == "table"
and type(_G.Enum.NumericRuleFormatRounding) == "table"
and _G.Enum.StatusBarInterpolation.Immediate ~= nil
and _G.Enum.StatusBarTimerDirection.ElapsedTime ~= nil
and _G.Enum.NumericRuleFormatRounding.Down ~= nil
API.regionmouseover = _G.UIParent API.regionmouseover = _G.UIParent
and type(_G.UIParent.IsMouseOver) == "function" and type(_G.UIParent.IsMouseOver) == "function"
@@ -122,19 +142,9 @@ API.loothistoryevents = API.loothistory and API.eventutils
and _G.C_EventUtils.IsEventValid("LOOT_HISTORY_FULL_UPDATE") and _G.C_EventUtils.IsEventValid("LOOT_HISTORY_FULL_UPDATE")
API.lootrollitemid = type(_G.GetLootRollItemID) == "function" API.lootrollitemid = type(_G.GetLootRollItemID) == "function"
API.aoeloot = type(_G.C_Loot) == "table"
and type(_G.C_Loot.GetNearbyLootableUnits) == "function"
and type(_G.C_Loot.IsScanInProgress) == "function"
and type(_G.C_Loot.LootAllCorpses) == "function"
API.overridebindings = type(_G.SetOverrideBindingClick) == "function" API.overridebindings = type(_G.SetOverrideBindingClick) == "function"
and type(_G.ClearOverrideBindings) == "function" and type(_G.ClearOverrideBindings) == "function"
API.playerstate = type(_G.IsMounted) == "function"
and type(_G.Dismount) == "function"
and type(_G.GetShapeshiftFormID) == "function"
and type(_G.CancelShapeshiftForm) == "function"
API.unitguid = type(_G.UnitGUID) == "function" API.unitguid = type(_G.UnitGUID) == "function"
API.unittoken = type(_G.UnitTokenFromGUID) == "function" API.unittoken = type(_G.UnitTokenFromGUID) == "function"
API.tooltipunit = _G.GameTooltip API.tooltipunit = _G.GameTooltip
@@ -150,6 +160,10 @@ API.tooltipsetitem = API.tooltipitem
API.unitrange = type(_G.UnitInRange) == "function" API.unitrange = type(_G.UnitInRange) == "function"
API.macrospell = type(_G.GetMacroSpell) == "function" API.macrospell = type(_G.GetMacroSpell) == "function"
API.macrodisplay = type(_G.C_Macro) == "table"
and type(_G.C_Macro.SetMacroDisplay) == "function"
and type(_G.C_Macro.GetMacroIcon) == "function"
API.nativemacrocommands = API.macrodisplay and type(_G.RunMacroText) == "function"
API.attackverbs = type(_G.StartAttack) == "function" API.attackverbs = type(_G.StartAttack) == "function"
and type(_G.StopAttack) == "function" and type(_G.StopAttack) == "function"
API.focus = type(_G.FocusUnit) == "function" API.focus = type(_G.FocusUnit) == "function"
@@ -225,6 +239,24 @@ API.GetChannelInfo = function(unit)
isTradeSkill, notInterruptible, spellID, true isTradeSkill, notInterruptible, spellID, true
end end
-- Native cast durations are snapshots: fetch again after pushback/channel
-- updates. Synthetic/SuperWoW timing uses a separate reusable object, never
-- mutating a duration returned by the engine.
API.GetCastDuration = function(unit, channel, startTime, endTime, fallback)
if not API.durationtimers then return end
local query = channel and _G.UnitChannelDuration or _G.UnitCastingDuration
if type(query) == "function" then
local duration = query(unit)
if duration and math.abs(duration:GetStartTime() - startTime) < .001
and math.abs(duration:GetEndTime() - endTime) < .001 then
return duration, fallback
end
end
fallback = fallback or _G.C_DurationUtil.CreateDuration()
fallback:SetTimeSpan(startTime, endTime)
return fallback, fallback
end
API.IsSpellInRange = function(spell, unit) API.IsSpellInRange = function(spell, unit)
if API.spellrange then if API.spellrange then
return _G.C_Spell.IsSpellInRange(spell, unit) return _G.C_Spell.IsSpellInRange(spell, unit)
@@ -373,6 +405,38 @@ API.Defer = function(callback)
end end
end end
-- Layout consumers must wait for world entry, then for other modules' first
-- OnUpdates, before restoring/detaching frames. Each request is one-shot and
-- releases its scripts when finished. The standalone layout needs a second
-- update without native timers; the action-bar layout only needs the first.
-- Schedule during module enable, before the first PLAYER_ENTERING_WORLD.
API.AfterWorldReady = function(callback, extraFrame)
local ready = CreateFrame("Frame")
local function Finish()
ready:SetScript("OnUpdate", nil)
ready:Hide()
callback()
end
ready:RegisterEvent("PLAYER_ENTERING_WORLD")
ready:SetScript("OnEvent", function()
ready:UnregisterAllEvents()
ready:SetScript("OnEvent", nil)
ready:SetScript("OnUpdate", function()
ready:SetScript("OnUpdate", nil)
ready:Hide()
if API.timer then
API.Defer(callback)
elseif extraFrame then
ready:SetScript("OnUpdate", Finish)
ready:Show()
else
callback()
end
end)
end)
return ready
end
API.IsMouseOver = function(region, topOffset, bottomOffset, leftOffset, rightOffset) API.IsMouseOver = function(region, topOffset, bottomOffset, leftOffset, rightOffset)
if API.regionmouseover and region then if API.regionmouseover and region then
return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset) return region:IsMouseOver(topOffset, bottomOffset, leftOffset, rightOffset)
@@ -388,30 +452,6 @@ API.GetContainerNumFreeSlots = function(bag)
return 0, 0 return 0, 0
end end
API.IsContainerItemOpenable = function(bag, slot)
if API.containeropenable then
return _G.C_Container.IsContainerItemOpenable(bag, slot)
end
end
API.GetNearbyLootableUnits = function()
if API.aoeloot then
return _G.C_Loot.GetNearbyLootableUnits()
end
end
API.IsLootScanInProgress = function()
return API.aoeloot and _G.C_Loot.IsScanInProgress() or false
end
API.LootAllCorpses = function(maxCount)
if API.aoeloot then
if maxCount then return _G.C_Loot.LootAllCorpses(maxCount) end
return _G.C_Loot.LootAllCorpses()
end
return false
end
-- ClassicAPI updates its own left/right modifier bitmap before firing -- ClassicAPI updates its own left/right modifier bitmap before firing
-- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key -- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key
-- state when the richer ClassicAPI functions are available. -- state when the richer ClassicAPI functions are available.
@@ -451,6 +491,12 @@ API.IsAltKeyDown = function()
return false return false
end end
-- All frame movers/previews use the same three-key chord. Individual key
-- queries above retain ClassicAPI's left/right state and vanilla fallback.
API.AreMoveModifiersDown = function()
return API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown()
end
-- Direct item IDs avoid a class of Turtle WoW custom-item failures caused by -- Direct item IDs avoid a class of Turtle WoW custom-item failures caused by
-- relying exclusively on legacy item-link parsing. -- relying exclusively on legacy item-link parsing.
local function GetItemIDFromLink(link) local function GetItemIDFromLink(link)
@@ -756,23 +802,6 @@ API.SellAllJunkItems = function()
end end
end end
API.IsMounted = function()
return API.playerstate and _G.IsMounted() or false
end
API.Dismount = function()
if API.playerstate then return _G.Dismount() end
end
API.GetShapeshiftFormID = function()
if API.playerstate then return _G.GetShapeshiftFormID() end
return 0
end
API.CancelShapeshiftForm = function()
if API.playerstate then return _G.CancelShapeshiftForm() end
end
API.UnitGUID = function(unit) API.UnitGUID = function(unit)
if API.unitguid then if API.unitguid then
return _G.UnitGUID(unit) return _G.UnitGUID(unit)
+6 -1
View File
@@ -98,11 +98,16 @@ Block("MiniMap Clock")
Block("MiniMap Tweaks") Block("MiniMap Tweaks")
Block("MiniMap Square") Block("MiniMap Square")
Block("Movable Unit Frames") Block("Movable Unit Frames")
Block("Personal Resource Display")
Block("Hide Player Frame Resources")
Block("Hide Player Name")
-- DragonflightUI replaces the stock player/target bars and provides its own -- DragonflightUI replaces the stock player/target bars and provides its own
-- health/power text. Keep the real ShaguTweaks modules disabled there even -- health/power text. Keep the real ShaguTweaks modules disabled there even
-- though Real Health Numbers is once again a genuine core module. -- though Real Health Numbers is once again a genuine core module.
Block("Real Health Numbers") Block("Real Health Numbers")
Block("Unit Frame Big Health") Block("Healing Predictions")
Block("Unit Frame Big Health for Player")
Block("Unit Frame Big Health for Target")
Block("Reduced Actionbar Size") Block("Reduced Actionbar Size")
Block("Unit Frame Class Colors") Block("Unit Frame Class Colors")
Block("Unit Frame Class Portraits") Block("Unit Frame Class Portraits")
+216 -11
View File
@@ -4,6 +4,19 @@ local GetExpansion = ShaguTweaks.GetExpansion
local mod = math.mod or mod local mod = math.mod or mod
local current_config = {} local current_config = {}
local darkKey, goldKey = T["Darkened UI"], T["Golden UI"]
local modeKey = T["UI:"]
local modeLabels = { T["Default"], T["Darkened"], T["Golden"] }
local modeOption = { title = modeKey, description = T["UI mode: Default, Darkened UI or Golden UI."], category = T["Interface"] }
local function SelectedMode()
return current_config[darkKey] == 1 and 2 or current_config[goldKey] == 1 and 3 or 1
end
local function SelectMode(mode, dropdown)
current_config[darkKey] = mode == 2 and 1 or 0
current_config[goldKey] = mode == 3 and 1 or 0
UIDropDownMenu_SetSelectedID(dropdown, mode)
UIDropDownMenu_SetText(modeLabels[mode], dropdown)
end
local max_width = 500 local max_width = 500
local max_height = 680 local max_height = 680
@@ -12,6 +25,7 @@ settings:Hide()
table.insert(UISpecialFrames, "AdvancedSettingsGUI") table.insert(UISpecialFrames, "AdvancedSettingsGUI")
settings:SetScript("OnHide", function() settings:SetScript("OnHide", function()
if settings.search then settings.search:ClearFocus() end
ShowUIPanel(GameMenuFrame) ShowUIPanel(GameMenuFrame)
UpdateMicroButtons() UpdateMicroButtons()
end) end)
@@ -34,10 +48,14 @@ settings:SetBackdrop({
settings.scrollframe = CreateFrame('ScrollFrame', 'AdvancedSettingsGUIScrollframe', settings, 'UIPanelScrollFrameTemplate') settings.scrollframe = CreateFrame('ScrollFrame', 'AdvancedSettingsGUIScrollframe', settings, 'UIPanelScrollFrameTemplate')
settings.scrollframe:SetWidth(max_width - 50) settings.scrollframe:SetWidth(max_width - 50)
settings.scrollframe:SetPoint('CENTER', settings, -16, 15) settings.scrollframe:SetPoint('CENTER', settings, -16, -21)
settings.scrollframe:Hide() settings.scrollframe:Hide()
settings.container = CreateFrame("Frame", "AdvancedSettingsGUIContainer", settings) settings.container = CreateFrame("Frame", "AdvancedSettingsGUIContainer", settings)
settings.empty = settings.container:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
settings.empty:SetPoint("TOP", settings.container, "TOP", 0, -24)
settings.empty:SetText(T["No matching options."])
settings.empty:Hide()
settings.title = CreateFrame("Frame", "AdvancedSettingsGUITtitle", settings) settings.title = CreateFrame("Frame", "AdvancedSettingsGUITtitle", settings)
settings.title:SetPoint("TOP", settings, "TOP", 0, 12) settings.title:SetPoint("TOP", settings, "TOP", 0, 12)
@@ -109,25 +127,181 @@ settings.defaults:SetScript("OnClick", function()
settings:defaults() settings:defaults()
end) end)
settings.version = CreateFrame("Button", "AdvancedSettingsGUIVersion", settings, "GameMenuButtonTemplate")
settings.version:SetWidth(80)
settings.version:SetPoint("LEFT", settings.defaults, "RIGHT", 4, 0)
settings.version:SetText("v" .. (GetAddOnMetadata("ShaguTweaks", "Version") or "unknown"))
settings.projectLink = CreateFrame("Frame", "ShaguTweaksProjectLink", settings)
local projectLink = settings.projectLink
projectLink:SetWidth(420)
projectLink:SetHeight(110)
projectLink:SetPoint("CENTER", settings, "CENTER", 0, 0)
projectLink:SetFrameStrata("DIALOG")
projectLink:SetFrameLevel(settings:GetFrameLevel() + 10)
projectLink:EnableMouse(true)
projectLink:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
})
projectLink:Hide()
table.insert(UISpecialFrames, "ShaguTweaksProjectLink")
projectLink.label = projectLink:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
projectLink.label:SetPoint("TOP", projectLink, "TOP", 0, -16)
projectLink.label:SetText(T["Press Ctrl+C to copy the link."])
projectLink.edit = CreateFrame("EditBox", "ShaguTweaksProjectLinkEdit", projectLink, "InputBoxTemplate")
projectLink.edit:SetWidth(370)
projectLink.edit:SetHeight(24)
projectLink.edit:SetPoint("TOP", projectLink, "TOP", 0, -33)
projectLink.edit:SetAutoFocus(false)
projectLink.edit:SetScript("OnEscapePressed", function() projectLink:Hide() end)
projectLink.edit:SetScript("OnEnterPressed", function() projectLink:Hide() end)
projectLink:SetScript("OnHide", function() projectLink.edit:ClearFocus() end)
projectLink.close = CreateFrame("Button", nil, projectLink, "GameMenuButtonTemplate")
projectLink.close:SetWidth(90)
projectLink.close:SetPoint("BOTTOM", projectLink, "BOTTOM", 0, 12)
projectLink.close:SetText(CLOSE)
projectLink.close:SetScript("OnClick", function() projectLink:Hide() end)
settings.version:SetScript("OnClick", function()
local color = ShaguTweaks.UIBorder
if color then projectLink:SetBackdropBorderColor(color.r, color.g, color.b, 1) end
projectLink:Show()
projectLink.edit:SetText("https://octowow.st/git/OEVG/ShaguTweaks-ClassicAPI")
projectLink.edit:SetFocus()
projectLink.edit:HighlightText()
end)
local searchIndex = {}
local function MatchesSearch(module, title, category, query)
if query == "" then return true end
if not searchIndex[module] then
local text = title .. " " .. category .. " " .. (module.description or "")
if module.sliders then
for _, slider in ipairs(module.sliders) do
text = text .. " " .. (slider.label or slider.key)
end
end
searchIndex[module] = string.lower(text)
end
return string.find(searchIndex[module], query, 1, true) ~= nil
end
settings.searchLabel = settings:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
settings.searchLabel:SetPoint("TOPLEFT", settings, "TOPLEFT", 22, -36)
settings.searchLabel:SetText(T["Search:"])
settings.search = CreateFrame("EditBox", "AdvancedSettingsGUISearch", settings, "InputBoxTemplate")
settings.search:SetPoint("TOPLEFT", settings, "TOPLEFT", 78, -30)
settings.search:SetWidth(max_width - 139)
settings.search:SetHeight(24)
settings.search:SetAutoFocus(false)
settings.search:SetText("")
settings.search:SetScript("OnEscapePressed", function() this:ClearFocus() end)
settings.search:SetScript("OnEnterPressed", function() this:ClearFocus() end)
settings.search:SetScript("OnTextChanged", function()
settings.query = string.lower(string.gsub(string.gsub(this:GetText() or "", "^%s+", ""), "%s+$", ""))
settings.scrollframe:SetVerticalScroll(0)
local scrollbar = _G.AdvancedSettingsGUIScrollframeScrollBar
if scrollbar then scrollbar:SetValue(0) end
settings:load()
end)
settings.searchClear = CreateFrame("Button", "AdvancedSettingsGUISearchClear", settings, "UIPanelButtonTemplate")
settings.searchClear:SetPoint("LEFT", settings.search, "RIGHT", 8, 0)
settings.searchClear:SetWidth(24)
settings.searchClear:SetHeight(22)
settings.searchClear:SetText("X")
settings.searchClear:SetScript("OnClick", function()
settings.search:SetText("")
settings.search:ClearFocus()
end)
settings.UpdateOptionAvailability = function(self)
if not settings.entries then return end
for title, button in pairs(settings.entries) do
local module = ShaguTweaks.mods[title]
if module and module.requires then
local available = true
for _, required in ipairs(module.requires) do
if current_config[required] ~= 1 then available = false end
end
button.dependencyMet = available
if available then button:Enable() else button:Disable() end
local label = _G["AdvancedSettingsGUI" .. title .. "Text"]
if label then
if available then label:SetTextColor(1, .82, 0)
else label:SetTextColor(.5, .5, .5) end
end
end
end
end
settings.load = function(self) settings.load = function(self)
-- never use more than 3/4 of the screen size -- never use more than 3/4 of the screen size
max_height = math.min(UIParent:GetHeight()/UIParent:GetScale()*0.75, 680) max_height = math.min(UIParent:GetHeight()/UIParent:GetScale()*0.75, 680)
-- update window sizing according to screen -- update window sizing according to screen
settings:SetHeight(max_height) settings:SetHeight(max_height)
settings.scrollframe:SetHeight(max_height - 80) settings.scrollframe:SetHeight(max_height - 152)
settings.container:SetHeight(max_height - 30) settings.container:SetHeight(max_height - 30)
settings.entries = settings.entries or {} settings.entries = settings.entries or {}
-- Reuse widgets, hiding old results before building the filtered layout.
for _, button in pairs(settings.entries) do button:Hide() end
for _, panel in pairs(settings.category or {}) do
panel:Hide()
if panel.button then panel.button:Hide() end
if panel.title then panel.title:Hide() end
end
local expansion = ShaguTweaks:GetExpansion() local expansion = ShaguTweaks:GetExpansion()
-- sort all configs into categories -- sort all configs into categories
local gui = {} local gui = {}
for title, module in pairs(ShaguTweaks.mods) do for title, module in pairs(ShaguTweaks.mods) do
if module.expansions[expansion] then if title ~= darkKey and title ~= goldKey and module.expansions[expansion] then
local category = module.category or T["General"] local category = module.category or T["General"]
gui[category] = gui[category] or {} if MatchesSearch(module, title, category, settings.query or "") then
gui[category][title] = module gui[category] = gui[category] or {}
gui[category][title] = module
end
end
end
local hasMode = expansion == "vanilla" and (ShaguTweaks.mods[darkKey] or ShaguTweaks.mods[goldKey])
if hasMode then
local holder = settings.entries["ui-mode"]
if not holder then
holder = CreateFrame("Frame", nil, settings)
holder:SetWidth(160); holder:SetHeight(32)
holder.label = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
holder.label:SetPoint("LEFT", holder, "LEFT", 0, 0)
holder.label:SetText(modeKey)
local dropdown = CreateFrame("Frame", "AdvancedSettingsGUIUIMode", holder, "UIDropDownMenuTemplate")
dropdown:SetPoint("LEFT", holder, "LEFT", 22, -2)
UIDropDownMenu_SetWidth(90, dropdown)
UIDropDownMenu_Initialize(dropdown, function()
for i, label in ipairs(modeLabels) do
local choice = i
local info = { text = label, checked = SelectedMode() == i,
func = function() SelectMode(choice, dropdown) end }
UIDropDownMenu_AddButton(info)
end
end)
settings.modeDropdown = dropdown
settings.entries["ui-mode"] = holder
holder:SetScript("OnHide", function() if CloseDropDownMenus then CloseDropDownMenus() end end)
end
holder:ClearAllPoints()
holder:SetPoint("TOP", settings, "TOP", 0, -56)
holder:Show()
UIDropDownMenu_SetSelectedID(settings.modeDropdown, SelectedMode())
UIDropDownMenu_SetText(modeLabels[SelectedMode()], settings.modeDropdown)
local color = ShaguTweaks.UIBorder
if color then
for _, suffix in ipairs({"Left", "Middle", "Right"}) do
local texture = _G["AdvancedSettingsGUIUIMode" .. suffix]
if texture then texture:SetVertexColor(color.r, color.g, color.b, 1) end
end
end end
end end
@@ -149,6 +323,9 @@ settings.load = function(self)
-- add category background -- add category background
settings.category = settings.category or {} settings.category = settings.category or {}
settings.category[category] = settings.category[category] or CreateFrame("Frame", nil, settings.container) settings.category[category] = settings.category[category] or CreateFrame("Frame", nil, settings.container)
settings.category[category]:Show()
settings.category[category]:ClearAllPoints()
settings.category[category].buttons = {}
if not previous then if not previous then
settings.category[category]:SetPoint("TOPLEFT", settings.container, "TOPLEFT", spacing, -spacing - topspace) settings.category[category]:SetPoint("TOPLEFT", settings.container, "TOPLEFT", spacing, -spacing - topspace)
@@ -196,6 +373,7 @@ settings.load = function(self)
end end
settings.category[category].button = settings.category[category].button or CreateFrame("Button", nil, settings.container) settings.category[category].button = settings.category[category].button or CreateFrame("Button", nil, settings.container)
settings.category[category].button:Show()
settings.category[category].button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 0, entrysize-4) settings.category[category].button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 0, entrysize-4)
settings.category[category].button:SetWidth(22) settings.category[category].button:SetWidth(22)
settings.category[category].button:SetHeight(22) settings.category[category].button:SetHeight(22)
@@ -205,6 +383,7 @@ settings.load = function(self)
end) end)
settings.category[category].title = settings.category[category].title or CreateFrame("Button", nil, settings.container) settings.category[category].title = settings.category[category].title or CreateFrame("Button", nil, settings.container)
settings.category[category].title:Show()
settings.category[category].title:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 22, entrysize-4) settings.category[category].title:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", 22, entrysize-4)
settings.category[category].title:SetWidth(200) settings.category[category].title:SetWidth(200)
settings.category[category].title:SetHeight(entrysize) settings.category[category].title:SetHeight(entrysize)
@@ -225,7 +404,12 @@ settings.load = function(self)
settings.category[category]:SetBackdropBorderColor(.2,.2,.2,1) settings.category[category]:SetBackdropBorderColor(.2,.2,.2,1)
else else
settings.category[category]:SetBackdropColor(.2,.2,.2,1) settings.category[category]:SetBackdropColor(.2,.2,.2,1)
settings.category[category]:SetBackdropBorderColor(.5,.5,.5,1) local border = ShaguTweaks.GoldMode and ShaguTweaks.UIBorder
if border then
settings.category[category]:SetBackdropBorderColor(border.r, border.g, border.b, 1)
else
settings.category[category]:SetBackdropBorderColor(.5,.5,.5,1)
end
end end
-- add category title -- add category title
@@ -260,6 +444,8 @@ settings.load = function(self)
settings.category[category].buttons[button] = true settings.category[category].buttons[button] = true
button.title = title button.title = title
button:Show()
button:ClearAllPoints()
button:SetChecked(current_config[title] == 1 and true or nil) button:SetChecked(current_config[title] == 1 and true or nil)
button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", mod(entry, 2) == 1 and 17 or 17+200, math.ceil(entry/2-1)*-entrysize-spacing/2) button:SetPoint("TOPLEFT", settings.category[category], "TOPLEFT", mod(entry, 2) == 1 and 17 or 17+200, math.ceil(entry/2-1)*-entrysize-spacing/2)
@@ -280,11 +466,16 @@ settings.load = function(self)
end) end)
button:SetScript("OnClick", function() button:SetScript("OnClick", function()
if this.dependencyMet == false then
this:SetChecked(current_config[this.title] == 1 and true or nil)
return
end
if this:GetChecked() then if this:GetChecked() then
current_config[this.title] = 1 current_config[this.title] = 1
else else
current_config[this.title] = 0 current_config[this.title] = 0
end end
settings:UpdateOptionAvailability()
end) end)
text:SetText(title) text:SetText(title)
entry = entry + 1 entry = entry + 1
@@ -303,6 +494,8 @@ settings.load = function(self)
end end
local slider = settings.entries[sliderKey] local slider = settings.entries[sliderKey]
slider:Show()
slider:ClearAllPoints()
settings.category[category].buttons[slider] = true settings.category[category].buttons[slider] = true
local yOffset = -(height + spacing / 2 + 16 + (sidx - 1) * sliderHeight) local yOffset = -(height + spacing / 2 + 16 + (sidx - 1) * sliderHeight)
@@ -345,22 +538,32 @@ settings.load = function(self)
required_height = required_height + height + spacing required_height = required_height + height + spacing
end end
settings:UpdateOptionAvailability()
-- set container size to required height -- set container size to required height
if not next(gui) then
if hasMode and MatchesSearch(modeOption, modeKey, modeOption.category, settings.query or "" ) then
settings.empty:Hide()
else settings.empty:Show() end
required_height = 64
else
settings.empty:Hide()
end
settings.container:SetHeight(required_height) settings.container:SetHeight(required_height)
if required_height < max_height then if required_height + 132 <= max_height then
-- reduce base frame if possible -- reduce base frame if possible
settings:SetHeight(required_height + 60) settings:SetHeight(required_height + 132)
settings.container:SetParent(settings) settings.container:SetParent(settings)
settings.container:ClearAllPoints() settings.container:ClearAllPoints()
settings.container:SetPoint("CENTER", settings, 0, 20) settings.container:SetPoint("TOPLEFT", settings, "TOPLEFT", 10, -86)
settings.container:SetWidth(max_width - 20) settings.container:SetWidth(max_width - 20)
settings.scrollframe:Hide() settings.scrollframe:Hide()
elseif required_height > max_height then else
-- set up scrollframe when needed -- set up scrollframe when needed
settings.container:SetParent(settings.scrollframe) settings.container:SetParent(settings.scrollframe)
settings.container:SetHeight(settings.scrollframe:GetHeight()) settings.container:ClearAllPoints()
settings.container:SetPoint("TOPLEFT", settings.scrollframe, "TOPLEFT", 0, 0)
settings.container:SetWidth(settings.scrollframe:GetWidth() + 20) settings.container:SetWidth(settings.scrollframe:GetWidth() + 20)
settings.scrollframe:SetScrollChild(settings.container) settings.scrollframe:SetScrollChild(settings.container)
@@ -392,6 +595,8 @@ settings:SetScript("OnShow", function()
for k, v in pairs(ShaguTweaks_config) do for k, v in pairs(ShaguTweaks_config) do
current_config[k] = v current_config[k] = v
end end
-- Existing saves may contain both old toggles; retain Darkened precedence.
if current_config[darkKey] == 1 then current_config[goldKey] = 0 end
-- copy overwrites into a separate table so Cancel discards slider changes -- copy overwrites into a separate table so Cancel discards slider changes
current_config.overwrites = {} current_config.overwrites = {}
for k, v in pairs(ShaguTweaks_config.overwrites or {}) do for k, v in pairs(ShaguTweaks_config.overwrites or {}) do
+50 -11
View File
@@ -76,18 +76,57 @@ ShaguTweaks.HookScript = function(f, script, func)
end) end)
end end
ShaguTweaks.HookAddonOrVariable = function(addon, func) -- One event listener for deferred addon/global initialization. Deliberately
local lurker = CreateFrame("Frame", nil) -- retain the old three triggers rather than invoking already-ready hooks inline.
lurker.func = func do
lurker:RegisterEvent("ADDON_LOADED") local listener, draining
lurker:RegisterEvent("VARIABLES_LOADED") local pending, ready, checked = {}, {}, {}
lurker:RegisterEvent("PLAYER_ENTERING_WORLD") local function Drain()
lurker:SetScript("OnEvent",function() if draining then return end
if IsAddOnLoaded(addon) or _G[addon] then draining = true
this:func() local previousThis, previousEvent = this, event
this:UnregisterAllEvents() local a1,a2,a3,a4,a5,a6,a7,a8,a9 = arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9
local remaining = 0
local count = table.getn(pending)
for i = 1, count do
local entry = pending[i]
local available = checked[entry.name]
if available == nil then
available = (IsAddOnLoaded(entry.name) or _G[entry.name]) and true or false
checked[entry.name] = available
end
if available then table.insert(ready, entry)
else remaining = remaining + 1; pending[remaining] = entry end
end end
end) for i = count, remaining + 1, -1 do pending[i] = nil end
table.setn(pending, remaining) -- Lua 5.0 caches lengths used by table.insert.
for name in pairs(checked) do checked[name] = nil end
-- Dispatch only the snapshot: callbacks may register more work/load addons.
for i = 1, table.getn(ready) do
local callback = ready[i].callback
ready[i] = nil
this, event = listener, previousEvent
arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9 = a1,a2,a3,a4,a5,a6,a7,a8,a9
listener.func = callback
local ok, failure = pcall(callback, listener)
listener.func = nil
if not ok then pcall(geterrorhandler(), failure) end
end
table.setn(ready, 0)
this, event = previousThis, previousEvent
arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9 = a1,a2,a3,a4,a5,a6,a7,a8,a9
draining = nil
if not next(pending) then listener:UnregisterAllEvents() end
end
ShaguTweaks.HookAddonOrVariable = function(addon, func)
if not listener then listener = CreateFrame("Frame", nil); listener:SetScript("OnEvent", Drain) end
if not next(pending) then
listener:RegisterEvent("ADDON_LOADED")
listener:RegisterEvent("VARIABLES_LOADED")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
end
table.insert(pending, { name = addon, callback = func })
end
end end
ShaguTweaks.hooksecurefunc = function(tbl, name, func, prepend) ShaguTweaks.hooksecurefunc = function(tbl, name, func, prepend)
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
+54 -371
View File
@@ -1,14 +1,10 @@
local _G = ShaguTweaks.GetGlobalEnv() local _G = ShaguTweaks.GetGlobalEnv()
local L = ShaguTweaks.L local L = ShaguTweaks.L
local API = ShaguTweaks.API local API = ShaguTweaks.API
local GetExpansion = ShaguTweaks.GetExpansion
local libtipscan = ShaguTweaks.libtipscan
local libspell = ShaguTweaks.libspell local libspell = ShaguTweaks.libspell
local libtipscan = ShaguTweaks.libtipscan
local hooksecurefunc = ShaguTweaks.hooksecurefunc local hooksecurefunc = ShaguTweaks.hooksecurefunc
local cmatch = ShaguTweaks.cmatch if ShaguTweaks.GetExpansion() ~= "vanilla" then return end
-- return instantly if we're not on a vanilla client
if GetExpansion() ~= "vanilla" then return end
local valid_units = {} local valid_units = {}
valid_units["pet"] = true valid_units["pet"] = true
@@ -32,372 +28,68 @@ for i=1,4 do valid_units["partypet" .. i .. "target"] = true end
for i=1,40 do valid_units["raid" .. i .. "target"] = true end for i=1,40 do valid_units["raid" .. i .. "target"] = true end
for i=1,40 do valid_units["raidpet" .. i .. "target"] = true end for i=1,40 do valid_units["raidpet" .. i .. "target"] = true end
local lastcasttex, lastrank, _ valid_units.focus = true
local scanner valid_units.focustarget = true
local function GetScanner()
if not scanner then
scanner = libtipscan:GetScanner("libcast")
end
return scanner
end
local libcast = CreateFrame("Frame", "ShaguTweaksEnemyCast") local libcast = CreateFrame("Frame", "ShaguTweaksEnemyCast")
ShaguTweaks.libcast = libcast
local player = UnitName("player") local player = UnitName("player")
local _, playerClass = UnitClass("player") local _, playerClass = UnitClass("player")
local useClassicCasts = API and API.casts local useClassicCasts = API and API.casts
local needsCustomHunter = useClassicCasts and playerClass == "HUNTER" local needsCustomHunter = useClassicCasts and playerClass == "HUNTER"
local lastrank
local scanner
local function GetScanner()
if not scanner then scanner = libtipscan:GetScanner("libcast") end
return scanner
end
libcast.db = {}
if needsCustomHunter then libcast.db[player] = {} end
ShaguTweaks.UnitChannelInfo = _G.UnitChannelInfo or function(unit) function libcast:Prune(now)
-- ClassicAPI is authoritative for real unit tokens. Fall through to the for key, cast in pairs(self.db) do
-- legacy database only for the player's custom Hunter cast emulation or for if key ~= player and (not cast.start or not cast.casttime or cast.start + cast.casttime/1000 <= now) then self.db[key] = nil end
-- name/GUID keys maintained for compatibility.
if valid_units[unit] and useClassicCasts then
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill =
API.GetChannelInfo(unit)
if cast then
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
if unit ~= "player" then return end
end end
end
unit = valid_units[unit] and UnitName(unit) or unit local function ReadCached(key, channel)
local cast = key and libcast.db[key]
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill if not cast or not cast.cast or not cast.start or not cast.casttime then return end
local db = libcast.db[unit] if cast.start + cast.casttime/1000 <= GetTime() then
if key == player then cast.cast = nil else libcast.db[key] = nil end
if db and db.cast and db.start + db.casttime / 1000 > GetTime() then return
if not db.channel then return end
cast = db.cast
nameSubtext = db.rank
text = ""
texture = db.icon
startTime = db.start * 1000
endTime = startTime + db.casttime
isTradeSkill = nil
elseif db then
db.cast = nil
db.rank = nil
db.start = nil
db.casttime = nil
db.icon = nil
db.channel = nil
end end
if (cast.channel and true or false) ~= channel then return end
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill return cast.cast, cast.rank or "", "", cast.icon, cast.start*1000,
cast.start*1000 + cast.casttime, nil
end
ShaguTweaks.UnitCastingInfo = function(unit)
if valid_units[unit] then
local name, rank, text, icon, startTime, endTime, trade = API.GetCastInfo(unit)
if name then return name, rank, text, icon, startTime, endTime, trade end
-- A missing native cast is authoritative. Only the local Hunter's custom
-- emulation may fill a native gap; never consult a name-keyed remote cache.
if unit ~= "player" or not needsCustomHunter then return end
unit = player
end
return ReadCached(unit, false)
end
ShaguTweaks.UnitChannelInfo = function(unit)
if valid_units[unit] then
return API.GetChannelInfo(unit)
end
return ReadCached(unit, true)
end end
ShaguTweaks.UnitCastingInfo = _G.UnitCastingInfo or function(unit) -- No ClassicAPI remote event mirror or combat-text parser. Official castbars
if valid_units[unit] and useClassicCasts then -- query ClassicAPI directly; optional SuperWoW owns timed GUID records only.
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = if needsCustomHunter then
API.GetCastInfo(unit)
if cast then
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
if unit ~= "player" then return end
end
unit = valid_units[unit] and UnitName(unit) or unit
local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
local db = libcast.db[unit]
if db and db.cast and db.start + db.casttime / 1000 > GetTime() then
if db.channel then return end
cast = db.cast
nameSubtext = db.rank or ""
text = ""
texture = db.icon
startTime = db.start * 1000
endTime = startTime + db.casttime
isTradeSkill = nil
elseif db then
db.cast = nil
db.rank = nil
db.start = nil
db.casttime = nil
db.icon = nil
db.channel = nil
end
return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill
end
function libcast:AddAction(mob, spell, channel)
if not mob or not spell then return nil end
local data = L["spells"][spell]
if data and data.t then
local casttime = data.t
local icon = data.icon and string.format("%s%s", "Interface\\Icons\\", data.icon) or nil
-- add cast action to the database
if not self.db[mob] then self.db[mob] = {} end
self.db[mob].cast = spell
self.db[mob].rank = nil
self.db[mob].start = GetTime()
self.db[mob].casttime = casttime
self.db[mob].icon = icon
self.db[mob].channel = channel
return true
end
return nil
end
function libcast:RemoveAction(mob, spell)
if self.db[mob] and ( L["interrupts"][spell] ~= nil or spell == "INTERRUPT" ) then
-- remove cast action to the database
self.db[mob].cast = nil
self.db[mob].rank = nil
self.db[mob].start = nil
self.db[mob].casttime = nil
self.db[mob].icon = nil
self.db[mob].channel = nil
return true
end
return nil
end
-- main data
libcast.db = { [player] = {} }
-- Current ClassicAPI can maintain the legacy name-keyed libcast cache from
-- structured unit spell events. This preserves the public ShaguTweaks
-- UnitCastingInfo/UnitChannelInfo fallback without parsing every combat-text
-- line. Older ClassicAPI builds keep the historical parser below.
local classicRemoteEvents = API and API.casts and API.eventutils
and _G.C_EventUtils
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_START")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_STOP")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_CHANNEL_START")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_CHANNEL_STOP")
if classicRemoteEvents then
local remoteEvents = {
"UNIT_SPELLCAST_START",
"UNIT_SPELLCAST_STOP",
"UNIT_SPELLCAST_FAILED",
"UNIT_SPELLCAST_INTERRUPTED",
"UNIT_SPELLCAST_DELAYED",
"UNIT_SPELLCAST_CHANNEL_START",
"UNIT_SPELLCAST_CHANNEL_STOP",
"UNIT_SPELLCAST_CHANNEL_UPDATE",
}
for index = 1, table.getn(remoteEvents) do
local eventName = remoteEvents[index]
if _G.C_EventUtils.IsEventValid(eventName) then
libcast:RegisterEvent(eventName)
end
end
else
libcast:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF")
libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS")
libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE")
libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF")
end
-- Full legacy player tracking is only needed when ClassicAPI cast data is
-- unavailable. On current ClassicAPI, only Hunters keep the stop/fail events
-- required to end the Aimed Shot / Multi-Shot custom cast emulation.
if not useClassicCasts then
libcast:RegisterEvent("SPELLCAST_START")
libcast:RegisterEvent("SPELLCAST_STOP") libcast:RegisterEvent("SPELLCAST_STOP")
libcast:RegisterEvent("SPELLCAST_FAILED") libcast:RegisterEvent("SPELLCAST_FAILED")
libcast:RegisterEvent("SPELLCAST_INTERRUPTED") libcast:RegisterEvent("SPELLCAST_INTERRUPTED")
libcast:RegisterEvent("SPELLCAST_DELAYED") libcast:SetScript("OnEvent", function()
libcast:RegisterEvent("SPELLCAST_CHANNEL_START") local cast = libcast.db[player]
libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP") if cast then cast.cast = nil end
libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE") lastrank = nil
elseif needsCustomHunter then end)
libcast:RegisterEvent("SPELLCAST_STOP")
libcast:RegisterEvent("SPELLCAST_FAILED")
libcast:RegisterEvent("SPELLCAST_INTERRUPTED")
end
local function ClearClassicRemote(unit)
if not unit or unit == "player" then return end
local name = UnitName(unit)
if not name then return end
local cast = libcast.db[name]
if cast then
cast.cast = nil
cast.rank = nil
cast.start = nil
cast.casttime = nil
cast.icon = nil
cast.channel = nil
end
end
local function UpdateClassicRemote(unit, channel)
if not unit or unit == "player" then return end
local name = UnitName(unit)
if not name then return end
local spell, rank, text, icon, startTime, endTime
if channel then
spell, rank, text, icon, startTime, endTime = API.GetChannelInfo(unit)
else
spell, rank, text, icon, startTime, endTime = API.GetCastInfo(unit)
end
if not spell or not startTime or not endTime then
ClearClassicRemote(unit)
return
end
local cast = libcast.db[name]
if not cast then
cast = {}
libcast.db[name] = cast
end
cast.cast = spell
cast.rank = rank
cast.start = startTime / 1000
cast.casttime = endTime - startTime
cast.icon = icon
cast.channel = channel and true or nil
end
libcast:SetScript("OnEvent", function()
-- Keep the public legacy cache current from ClassicAPI without combat text.
if classicRemoteEvents and strfind(event, "UNIT_SPELLCAST_", 1) then
if arg1 == "player" then return end
if event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" then
UpdateClassicRemote(arg1, false)
elseif event == "UNIT_SPELLCAST_CHANNEL_START" or event == "UNIT_SPELLCAST_CHANNEL_UPDATE" then
UpdateClassicRemote(arg1, true)
elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_FAILED"
or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_CHANNEL_STOP"
then
ClearClassicRemote(arg1)
end
return
end
-- Fill database with player casts
if event == "SPELLCAST_START" then
local icon = L["spells"][arg1] and L["spells"][arg1].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg1].icon) or lastcasttex
-- add cast action to the database
this.db[player].cast = arg1
this.db[player].rank = lastrank
this.db[player].start = GetTime()
this.db[player].casttime = arg2
this.db[player].icon = icon
this.db[player].channel = nil
if not L["spells"][arg1] or not L["spells"][arg1].icon or not L["spells"][arg1].t then
L["spells"][arg1] = L["spells"][arg1] or { }
L["spells"][arg1].icon = L["spells"][arg1].icon or icon
L["spells"][arg1].t = L["spells"][arg1].t or arg2
end
lastcasttex, lastrank = nil, nil
elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
if this.db[player] and not this.db[player].channel then
-- remove cast action to the database
this.db[player].cast = nil
this.db[player].rank = nil
this.db[player].rank = nil
this.db[player].start = nil
this.db[player].casttime = nil
this.db[player].icon = nil
this.db[player].channel = nil
else
lastcasttex, lastrank = nil, nil
end
elseif event == "SPELLCAST_DELAYED" then
if this.db[player].cast then
this.db[player].start = this.db[player].start + arg1/1000
end
elseif event == "SPELLCAST_CHANNEL_START" then
-- add cast action to the database
this.db[player].cast = arg2
this.db[player].rank = lastrank
this.db[player].start = GetTime()
this.db[player].casttime = arg1
this.db[player].icon = L["spells"][arg2] and L["spells"][arg2].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg2].icon) or lastcasttex
this.db[player].channel = true
lastcasttex, lastrank = nil, nil
elseif event == "SPELLCAST_CHANNEL_STOP" then
if this.db[player] and this.db[player].channel then
-- remove cast action to the database
this.db[player].cast = nil
this.db[player].rank = nil
this.db[player].start = nil
this.db[player].casttime = nil
this.db[player].icon = nil
this.db[player].channel = nil
end
elseif event == "SPELLCAST_CHANNEL_UPDATE" then
if this.db[player].cast then
this.db[player].start = -this.db[player].casttime/1000 + GetTime() + arg1/1000
end
-- Fill database with environmental casts
elseif arg1 then
local mob, spell, _
-- (.+) begins to cast (.+).
mob, spell = cmatch(arg1, SPELLCASTOTHERSTART)
if libcast:AddAction(mob, spell) then return end
-- (.+) begins to perform (.+).
mob, spell = cmatch(arg1, SPELLPERFORMOTHERSTART)
if libcast:AddAction(mob, spell) then return end
-- (.+) gains (.+).
mob, spell = cmatch(arg1, AURAADDEDOTHERHELPFUL)
if libcast:RemoveAction(mob, spell) then return end
-- (.+) is afflicted by (.+).
mob, spell = cmatch(arg1, AURAADDEDOTHERHARMFUL)
if libcast:RemoveAction(mob, spell) then return end
-- Your (.+) hits (.+) for (%d+).
spell, mob = cmatch(arg1, SPELLLOGSELFOTHER)
if libcast:RemoveAction(mob, spell) then return end
-- Your (.+) crits (.+) for (%d+).
spell, mob = cmatch(arg1, SPELLLOGCRITSELFOTHER)
if libcast:RemoveAction(mob, spell) then return end
-- (.+)'s (.+) %a hits (.+) for (%d+).
_, spell, mob = cmatch(arg1, SPELLLOGOTHEROTHER)
if libcast:RemoveAction(mob, spell) then return end
-- (.+)'s (.+) %a crits (.+) for (%d+).
_, spell, mob = cmatch(arg1, SPELLLOGCRITOTHEROTHER)
if libcast:RemoveAction(mob, spell) then return end
-- You interrupt (.+)'s (.+).
mob, spell = cmatch(arg1, SPELLINTERRUPTSELFOTHER)
if libcast:RemoveAction(mob, spell) then return end
-- (.+) interrupts (.+)'s (.+).
_, mob, spell = cmatch(arg1, SPELLINTERRUPTOTHEROTHER)
if libcast:RemoveAction(mob, spell) then return end
end
end)
--[[ Custom Casts --[[ Custom Casts
Enable Castbars for spells that don't have a castbar by default Enable Castbars for spells that don't have a castbar by default
(e.g Multi-Shot and Aimed Shot) (e.g Multi-Shot and Aimed Shot)
@@ -508,15 +200,10 @@ local function CastCustom(spell)
end end
end end
if not useClassicCasts or needsCustomHunter then
hooksecurefunc("UseContainerItem", function(id, index)
lastcasttex = GetContainerItemInfo(id, index)
end)
hooksecurefunc("CastSpell", function(id, bookType) hooksecurefunc("CastSpell", function(id, bookType)
local spellName, rank = GetSpellName(id, bookType) local spellName, rank = GetSpellName(id, bookType)
if not GetCustomCast(spellName) then return end
lastrank = rank lastrank = rank
lastcasttex = GetSpellTexture(id, bookType)
if GetSpellCooldown(id, bookType) ~= 0 then if GetSpellCooldown(id, bookType) ~= 0 then
CastCustom(spellName) CastCustom(spellName)
@@ -555,12 +242,11 @@ hooksecurefunc("UseAction", function(slot, target, button)
-- CastSpellByName. Do not guess it from the macro's first cast line. -- CastSpellByName. Do not guess it from the macro's first cast line.
return return
elseif actionType == "item" then elseif actionType == "item" then
lastcasttex = GetActionTexture(slot)
lastrank = nil lastrank = nil
return return
elseif actionType == "spell" and actionID then elseif actionType == "spell" and actionID then
local spellName, rank, icon = API.GetSpellInfo(actionID) local spellName, rank = API.GetSpellInfo(actionID)
lastcasttex = icon or GetActionTexture(slot) if not GetCustomCast(spellName) then return end
lastrank = rank lastrank = rank
if not IsCurrentAction(slot) then return end if not IsCurrentAction(slot) then return end
CastCustom(spellName) CastCustom(spellName)
@@ -575,10 +261,7 @@ hooksecurefunc("UseAction", function(slot, target, button)
tip:SetAction(slot) tip:SetAction(slot)
local spellName, rank = tip:Line(1) local spellName, rank = tip:Line(1)
lastcasttex = GetActionTexture(slot)
lastrank = rank lastrank = rank
CastCustom(spellName) CastCustom(spellName)
end) end)
end end
ShaguTweaks.libcast = libcast
-378
View File
@@ -1,378 +0,0 @@
local _G = ShaguTweaks.GetGlobalEnv()
local L = ShaguTweaks.L
local API = ShaguTweaks.API
local GetExpansion = ShaguTweaks.GetExpansion
local libtipscan = ShaguTweaks.libtipscan
local libspell = ShaguTweaks.libspell
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local QueueFunction = ShaguTweaks.QueueFunction
local cmatch = ShaguTweaks.cmatch
-- return instantly if we're not on a vanilla client
if GetExpansion() ~= "vanilla" then return end
-- fix a typo (missing $) in ruRU capture index
if GetLocale() == "ruRU" then
SPELLREFLECTSELFOTHER = gsub(SPELLREFLECTSELFOTHER, "%%2s", "%%2%$s")
end
local libdebuff = CreateFrame("Frame", "ShaguTweaksDebuffsScanner", UIParent)
local scanner
local function GetScanner()
if not scanner then
scanner = libtipscan:GetScanner("libdebuff")
end
return scanner
end
local _, class = UnitClass("player")
local lastspell
local useClassicAuraTiming = API and API.aurapositional and API.UnitDebuff
function libdebuff:GetDuration(effect, rank)
if L["debuffs"][effect] then
local rank = rank and tonumber((string.gsub(rank, RANK .. " ", ""))) or 0
local rank = L["debuffs"][effect][rank] and rank or libdebuff:GetMaxRank(effect)
local duration = L["debuffs"][effect][rank]
if effect == L["dyndebuffs"]["Rupture"] then
-- Rupture: +2 sec per combo point
duration = duration + GetComboPoints()*2
elseif effect == L["dyndebuffs"]["Kidney Shot"] then
-- Kidney Shot: +1 sec per combo point
duration = duration + GetComboPoints()*1
elseif effect == L["dyndebuffs"]["Demoralizing Shout"] then
-- Booming Voice: 10% per talent
local _,_,_,_,count = GetTalentInfo(2,1)
if count and count > 0 then duration = duration + ( duration / 100 * (count*10)) end
elseif effect == L["dyndebuffs"]["Shadow Word: Pain"] then
-- Improved Shadow Word: Pain: +3s per talent
local _,_,_,_,count = GetTalentInfo(3,4)
if count and count > 0 then duration = duration + count * 3 end
elseif effect == L["dyndebuffs"]["Frostbolt"] then
-- Permafrost: +1s per talent
local _,_,_,_,count = GetTalentInfo(3,7)
if count and count > 0 then duration = duration + count end
elseif effect == L["dyndebuffs"]["Gouge"] then
-- Improved Gouge: +.5s per talent
local _,_,_,_,count = GetTalentInfo(2,1)
if count and count > 0 then duration = duration + (count*.5) end
end
return duration
else
return 0
end
end
function libdebuff:UpdateDuration(unit, unitlevel, effect, duration)
if not unit or not effect or not duration then return end
unitlevel = unitlevel or 0
if libdebuff.objects[unit] and libdebuff.objects[unit][unitlevel] and libdebuff.objects[unit][unitlevel][effect] then
libdebuff.objects[unit][unitlevel][effect].duration = duration
end
end
function libdebuff:GetMaxRank(effect)
local max = 0
for id in pairs(L["debuffs"][effect]) do
if id > max then max = id end
end
return max
end
function libdebuff:UpdateUnits()
TargetDebuffButton_Update()
end
function libdebuff:AddPending(unit, unitlevel, effect, duration)
if not unit then return end
if not L["debuffs"][effect] then return end
duration = duration or libdebuff:GetDuration(effect)
if duration > 0 and libdebuff.pending[3] ~= effect then
libdebuff.pending[1] = unit
libdebuff.pending[2] = unitlevel or 0
libdebuff.pending[3] = effect
libdebuff.pending[4] = duration
end
end
function libdebuff:RemovePending()
libdebuff.pending[1] = nil
libdebuff.pending[2] = nil
libdebuff.pending[3] = nil
libdebuff.pending[4] = nil
end
function libdebuff:PersistPending(effect)
if not libdebuff.pending[3] then return end
if libdebuff.pending[3] == effect or ( effect == nil and libdebuff.pending[3] ) then
libdebuff:AddEffect(libdebuff.pending[1], libdebuff.pending[2], libdebuff.pending[3], libdebuff.pending[4])
libdebuff:RemovePending()
end
end
function libdebuff:RevertLastAction()
lastspell.start = lastspell.start_old
lastspell.start_old = nil
libdebuff:UpdateUnits()
end
function libdebuff:AddEffect(unit, unitlevel, effect, duration, deferUpdate)
if not unit or not effect then return end
unitlevel = unitlevel or 0
if not libdebuff.objects[unit] then libdebuff.objects[unit] = {} end
if not libdebuff.objects[unit][unitlevel] then libdebuff.objects[unit][unitlevel] = {} end
if not libdebuff.objects[unit][unitlevel][effect] then libdebuff.objects[unit][unitlevel][effect] = {} end
-- save current effect as lastspell
lastspell = libdebuff.objects[unit][unitlevel][effect]
libdebuff.objects[unit][unitlevel][effect].effect = effect
libdebuff.objects[unit][unitlevel][effect].start_old = libdebuff.objects[unit][unitlevel][effect].start
libdebuff.objects[unit][unitlevel][effect].start = GetTime()
libdebuff.objects[unit][unitlevel][effect].duration = duration or libdebuff:GetDuration(effect)
if not deferUpdate then
libdebuff:UpdateUnits()
end
end
-- ClassicAPI exposes aura identity and timing directly. The old periodic
-- combat-text parser only exists to reconstruct off-target applications on
-- older ClassicAPI builds; current ClassicAPI does not register those noisy
-- events at all.
if not useClassicAuraTiming then
libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE")
libdebuff:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE")
libdebuff:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER")
libdebuff:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
libdebuff:RegisterEvent("SPELLCAST_STOP")
end
-- Target changes and aura updates are still useful for the rare fallback case
-- where ClassicAPI knows an aura's duration but could not observe its original
-- application and therefore has no expirationTime.
libdebuff:RegisterEvent("PLAYER_TARGET_CHANGED")
libdebuff:RegisterEvent("UNIT_AURA")
-- register seal handler
if class == "PALADIN" then
libdebuff:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS")
end
-- Remove Pending
libdebuff.rp = { SPELLIMMUNESELFOTHER, IMMUNEDAMAGECLASSSELFOTHER,
SPELLMISSSELFOTHER, SPELLRESISTSELFOTHER, SPELLEVADEDSELFOTHER,
SPELLDODGEDSELFOTHER, SPELLDEFLECTEDSELFOTHER, SPELLREFLECTSELFOTHER,
SPELLPARRIEDSELFOTHER, SPELLLOGABSORBSELFOTHER }
libdebuff.objects = {}
libdebuff.pending = {}
-- Gather Data by Events
libdebuff:SetScript("OnEvent", function()
-- paladin seal refresh
if event == "CHAT_MSG_COMBAT_SELF_HITS" then
local hit = cmatch(arg1, COMBATHITSELFOTHER)
local crit = cmatch(arg1, COMBATHITCRITSELFOTHER)
if hit or crit then
for seal in L["judgements"] do
local name = UnitName("target")
local level = UnitLevel("target")
if name and libdebuff.objects[name] then
if level and libdebuff.objects[name][level] and libdebuff.objects[name][level][seal] then
libdebuff:AddEffect(name, level, seal)
elseif libdebuff.objects[name][0] and libdebuff.objects[name][0][seal] then
libdebuff:AddEffect(name, 0, seal)
end
end
end
end
-- Add Combat Log
elseif event == "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE" or event == "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE" then
local unit, effect = cmatch(arg1, AURAADDEDOTHERHARMFUL)
if unit and effect then
local unitlevel = UnitName("target") == unit and UnitLevel("target") or 0
if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then
libdebuff:AddEffect(unit, unitlevel, effect)
end
end
-- Add Missing Buffs by Iteration
elseif ( event == "UNIT_AURA" and arg1 == "target" ) or event == "PLAYER_TARGET_CHANGED" then
local unit = UnitName("target")
if not unit then return end
local unitlevel = UnitLevel("target") or 0
local changed = false
for i=1, 16 do
local effect, rank, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff("target", i)
-- abort when no further debuff was found
if not texture then break end
if effect and effect ~= "" and (not timeleft or timeleft < 0) then
-- Exact ClassicAPI timing needs no legacy cache at all. Only construct
-- an estimate when expirationTime is genuinely unavailable.
if not libdebuff.objects[unit] or not libdebuff.objects[unit][unitlevel] or not libdebuff.objects[unit][unitlevel][effect] then
local fallbackDuration = duration and duration > 0 and duration or nil
libdebuff:AddEffect(unit, unitlevel, effect, fallbackDuration, true)
changed = true
end
end
end
if changed then
libdebuff:UpdateUnits()
end
-- Update Pending Spells
elseif event == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" or event == "CHAT_MSG_SPELL_SELF_DAMAGE" then
-- Most combat messages cannot affect libdebuff. Avoid running the whole
-- miss/resist/immune pattern set unless there is state to repair.
if not libdebuff.pending[3] and not (lastspell and lastspell.start_old) then
return
end
for _, msg in pairs(libdebuff.rp) do
local effect = cmatch(arg1, msg)
if effect and libdebuff.pending[3] == effect then
-- instant removal of the pending spell
libdebuff:RemovePending()
return
elseif effect and lastspell and lastspell.start_old and lastspell.effect == effect then
-- late removal of debuffs (e.g hunter arrows as they hit late)
libdebuff:RevertLastAction()
return
end
end
elseif event == "SPELLCAST_STOP" then
if libdebuff.pending[3] then
QueueFunction(libdebuff.PersistPending)
end
end
end)
-- Gather Data by User Actions.
-- Current ClassicAPI gets exact application timing from C_UnitAuras, so these
-- legacy prediction hooks are installed only when that timing surface is absent.
if not useClassicAuraTiming then
hooksecurefunc("CastSpell", function(id, bookType)
local effect, rank = GetSpellName(id, bookType)
if not effect or not L["debuffs"][effect] then return end
local duration = libdebuff:GetDuration(effect, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration)
end)
hooksecurefunc("CastSpellByName", function(effect, target)
if not effect then return end
-- Strip an explicit rank before checking the duration database. Reject
-- ordinary damage/heal casts before doing any spell metadata work.
local _, _, baseEffect, explicitRank = string.find(effect, "(.+)%((.+)%)")
local name = baseEffect or effect
if not L["debuffs"][name] then return end
if libdebuff.pending[3] == name then return end
local rank = explicitRank
if not rank then
_, rank = libspell.GetSpellInfo(name)
end
local duration = libdebuff:GetDuration(name, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), name, duration)
end)
hooksecurefunc("UseAction", function(slot, target, button)
-- Macros are intentionally left to CastSpellByName: conditional macros can
-- select a different spell than their first /cast line.
if API and API.actioninfo and API.GetActionInfo then
local actionType, actionID = API.GetActionInfo(slot)
if actionType == "macro" or actionType == "item" then return end
if actionType == "spell" and actionID then
if not IsCurrentAction(slot) then return end
local effect, rank = API.GetSpellInfo(actionID)
if not effect or not L["debuffs"][effect] then return end
local duration = libdebuff:GetDuration(effect, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration)
return
elseif actionType then
return
end
end
if GetActionText(slot) or not IsCurrentAction(slot) then return end
local tip = GetScanner()
tip:SetAction(slot)
local effect, rank = tip:Line(1)
if not effect or not L["debuffs"][effect] then return end
local duration = libdebuff:GetDuration(effect, rank)
libdebuff:AddPending(UnitName("target"), UnitLevel("target"), effect, duration)
end)
end
function libdebuff:UnitDebuff(unit, id)
local unitname = UnitName(unit)
local unitlevel = UnitLevel(unit)
local effect, texture, stacks, dtype
local duration, timeleft = nil, -1
local rank = nil -- no backport
-- ClassicAPI returns positional aura metadata without allocating a table.
-- When it observed the application, use its exact duration/expiration
-- immediately and bypass the legacy name/level estimate entirely.
if useClassicAuraTiming then
local expirationTime
effect, texture, stacks, dtype, duration, expirationTime =
API.UnitDebuff(unit, id)
if texture and duration and duration > 0
and expirationTime and expirationTime > 0
then
timeleft = expirationTime - GetTime()
if timeleft < 0 then timeleft = 0 end
return effect, rank, texture, stacks, dtype, duration, timeleft
end
end
-- Plain Vanilla / older ClassicAPI compatibility. Also acts as a defensive
-- fallback if a positional API unexpectedly failed to resolve a visible aura.
if not texture then
texture, stacks, dtype = UnitDebuff(unit, id)
end
if texture and (not effect or effect == "") then
local tip = GetScanner()
tip:SetUnitDebuff(unit, id)
effect = tip:Line(1) or ""
end
if libdebuff.objects[unitname] and libdebuff.objects[unitname][unitlevel] and libdebuff.objects[unitname][unitlevel][effect] then
-- clean up cache
if libdebuff.objects[unitname][unitlevel][effect].duration and libdebuff.objects[unitname][unitlevel][effect].duration + libdebuff.objects[unitname][unitlevel][effect].start < GetTime() then
libdebuff.objects[unitname][unitlevel][effect] = nil
else
duration = libdebuff.objects[unitname][unitlevel][effect].duration
timeleft = duration + libdebuff.objects[unitname][unitlevel][effect].start - GetTime()
end
-- no level data
elseif libdebuff.objects[unitname] and libdebuff.objects[unitname][0] and libdebuff.objects[unitname][0][effect] then
-- clean up cache
if libdebuff.objects[unitname][0][effect].duration and libdebuff.objects[unitname][0][effect].duration + libdebuff.objects[unitname][0][effect].start < GetTime() then
libdebuff.objects[unitname][0][effect] = nil
else
duration = libdebuff.objects[unitname][0][effect].duration
timeleft = duration + libdebuff.objects[unitname][0][effect].start - GetTime()
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft
end
ShaguTweaks.libdebuff = libdebuff
+2 -13
View File
@@ -22,7 +22,6 @@ local function IsNamePlate(frame)
end end
local registry = {} local registry = {}
local lastParentCount = -1
local libnameplate = CreateFrame("Frame", nil, UIParent) local libnameplate = CreateFrame("Frame", nil, UIParent)
ShaguTweaks.libnameplate = libnameplate ShaguTweaks.libnameplate = libnameplate
libnameplate.OnInit = {} libnameplate.OnInit = {}
@@ -78,13 +77,6 @@ local function DiscoverNameplates()
InitializePlate(children[index]) InitializePlate(children[index])
end end
lastParentCount = count
end
local function ScanNameplatesLegacy()
local count = WorldFrame:GetNumChildren()
if count == lastParentCount then return end
DiscoverNameplates()
end end
local eventDriver = CreateFrame("Frame") local eventDriver = CreateFrame("Frame")
@@ -110,10 +102,9 @@ function libnameplate:HasConsumers()
end end
function libnameplate:Enable() function libnameplate:Enable()
if self.enabled then return end if self.enabled or not API.IsSupportedRuntime() or not HasClassicNameplateEvents() then return end
self.enabled = true self.enabled = true
if HasClassicNameplateEvents() then
-- Never enumerate WorldFrame children synchronously from the ClassicAPI -- Never enumerate WorldFrame children synchronously from the ClassicAPI
-- nameplate event path. Defer discovery to the next rendered frame. -- nameplate event path. Defer discovery to the next rendered frame.
ScheduleDiscovery() ScheduleDiscovery()
@@ -124,9 +115,7 @@ function libnameplate:Enable()
end) end)
self:SetScript("OnUpdate", nil) self:SetScript("OnUpdate", nil)
else
self:SetScript("OnUpdate", ScanNameplatesLegacy)
end
end end
function libnameplate:EnableIfNeeded() function libnameplate:EnableIfNeeded()
+259 -564
View File
@@ -1,592 +1,287 @@
-- ClassicAPI supplies cast timing, not incoming heal amounts. Learn our own
-- direct heals and receive explicit-duration vanilla HealComm announcements.
local _G = ShaguTweaks.GetGlobalEnv() local _G = ShaguTweaks.GetGlobalEnv()
local L = ShaguTweaks.L
local API = ShaguTweaks.API local API = ShaguTweaks.API
local GetExpansion = ShaguTweaks.GetExpansion local P = { pending = {}, enabled = false }
local libtipscan = ShaguTweaks.libtipscan ShaguTweaks.libpredict = P
local libspell = ShaguTweaks.libspell local learned, driver, player, sentGUID, sentTarget, sentSpell, activeGUID
local hooksecurefunc = ShaguTweaks.hooksecurefunc local elapsed = 0
local strsplit = ShaguTweaks.strsplit local equipment, equipmentReady = {}, false
local cmatch = ShaguTweaks.cmatch local sharedHooks = {}
local hotAmounts, hotUnits = {}, {}
local senttarget local trackedUnits = {"player", "target"}
local heals, ress, events = {}, {}, {} local function HotTotal(unit)
local state = hotUnits[unit]
local PRAYER_OF_HEALING if not state or state.guid ~= UnitGUID(unit) then return 0 end
do -- Prayer of Healing local total, now = 0, GetTime()
local locales = { for _, hot in ipairs(state.entries) do if hot.expires > now then total = total + hot.amount end end
["deDE"] = "Gebet der Heilung", return total
["enUS"] = "Prayer of Healing",
["esES"] = "Rezo de curación",
["frFR"] = "Prière de soins",
["koKR"] = "치유의 기원",
["ruRU"] = "Молитва исцеления",
["zhCN"] = "治疗祷言",
}
PRAYER_OF_HEALING = locales[GetLocale()] or locales["enUS"]
end end
local function HotPending()
local libpredict = CreateFrame("Frame")
libpredict.enabled = false
libpredict:SetScript("OnEvent", function()
if event == "CHAT_MSG_ADDON" and (arg1 == "HealComm" or arg1 == "CTRA") then
this:ParseChatMessage(arg4, arg2, arg1)
elseif event == "UNIT_HEALTH" then
local name = UnitName(arg1)
if ress[name] and not UnitIsDeadOrGhost(arg1) then
ress[UnitName(arg1)] = nil
end
end
end)
local function UpdateTimeouts()
local now = GetTime() local now = GetTime()
local pending for _, state in pairs(hotUnits) do
for _, hot in ipairs(state.entries) do if hot.expires > now then return true end end
for timestamp in pairs(events) do end
if now >= timestamp then return false
events[timestamp] = nil end
else local function EquipmentChanged()
pending = true if not _G.GetInventoryItemLink then return false end
local changed = false
for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot)
if equipment[slot] ~= link then changed = true end
equipment[slot] = link
end
local ready = equipmentReady
equipmentReady = true
return ready and changed
end
local function Notify(target) if P.changed then P.changed(target) end end
local function Expire()
elapsed = elapsed + (arg1 or 0)
if elapsed < .1 then return end
elapsed = 0
local now, any = GetTime(), false
for target, senders in pairs(P.pending) do
local changed, remaining = false, false
for sender, heal in pairs(senders) do
if heal.expires <= now then senders[sender] = nil; changed = true
else remaining = true; any = true end
end
if not remaining then P.pending[target] = nil end
if changed then Notify(target) end
end
for _, state in pairs(hotUnits) do
for i = table.getn(state.entries), 1, -1 do
if state.entries[i].expires <= now then
table.remove(state.entries, i)
Notify(state.name)
else any = true end
end end
end end
if not any then driver:SetScript("OnUpdate", nil) end
-- No prediction timeout is pending: completely remove OnUpdate until the
-- next AddEvent() instead of waking this library on every rendered frame.
if not pending then
libpredict.timeoutActive = nil
libpredict:SetScript("OnUpdate", nil)
end
end end
function P:HealStop(sender)
function libpredict:ParseComm(sender, msg) if not sender then return end
local msgtype, target, heal, time for target, senders in pairs(self.pending) do
if senders[sender] then
if msg == "Healstop" or msg == "GrpHealstop" then senders[sender] = nil
msgtype = "Stop" if not next(senders) then self.pending[target] = nil end
elseif msg == "Resurrection/stop/" then Notify(target)
msgtype = "RessStop" end
elseif msg then end
local msgobj = {strsplit("/", msg)} if driver and not next(self.pending) and not HotPending() then driver:SetScript("OnUpdate", nil) end
end
if msgobj and msgobj[1] and msgobj[2] then function P:Heal(sender, target, amount, milliseconds)
-- legacy healcomm object amount, milliseconds = tonumber(amount), tonumber(milliseconds)
if msgobj[1] == "GrpHealdelay" or msgobj[1] == "Healdelay" then if not self.enabled or self.external or not sender or not target or target == "" or not amount or not milliseconds
msgtype, time = "Delay", msgobj[2] or amount <= 0 or amount > 10000000 or milliseconds <= 0 or milliseconds > 600000 then return end
self:HealStop(sender)
local senders = self.pending[target]
if not senders then senders = {}; self.pending[target] = senders end
senders[sender] = { amount = amount, expires = GetTime() + milliseconds / 1000 }
Notify(target)
elapsed = 0
driver:SetScript("OnUpdate", Expire)
end
function P:UnitGetIncomingHeals(unit)
if not self.enabled or not UnitExists(unit) or UnitIsDeadOrGhost(unit) then return 0 end
if self.external then
local guid = UnitGUID(unit)
local amount = guid and self.external.GetIncomingHealing(guid)
return type(amount) == "number" and amount > 0 and amount or 0
end
local senders = self.pending[UnitName(unit)]
local sum, now = HotTotal(unit), GetTime()
if senders then
for _, heal in pairs(senders) do if heal.expires > now then sum = sum + heal.amount end end
end
return sum
end
local function RefreshHots(unit)
if unit ~= "player" and unit ~= "target" then return end
local state = hotUnits[unit]
if not state then state = {entries={}}; hotUnits[unit] = state end
state.guid, state.name = UnitGUID(unit), UnitName(unit)
local auras, count, now = _G.C_UnitAuras, 0, GetTime()
if auras and UnitExists(unit) and not UnitIsDeadOrGhost(unit) and UnitCanAssist("player", unit) then
for index = 1, 48 do
local name, duration, expires, source, spell, guid
if type(auras.UnitBuff) == "function" then
local n, _, _, _, d, e, s, _, _, id = auras.UnitBuff(unit, index)
name, duration, expires, source, spell = n, d, e, s, id
guid = source and UnitGUID(source)
end end
if (not name or not guid) and type(auras.GetAuraDataByIndex) == "function" then
if msgobj[1] and msgobj[1] == "Resurrection" and msgobj[2] then local aura = auras.GetAuraDataByIndex(unit, index, "HELPFUL")
msgtype, target = "Ress", msgobj[2] if aura then
end name, duration, expires, spell = aura.name, aura.duration, aura.expirationTime, aura.spellId
guid = aura.sourceGUID or (aura.sourceUnit and UnitGUID(aura.sourceUnit))
if msgobj[1] == "Heal" and msgobj[2] then
msgtype, target, heal, time = "Heal", msgobj[2], msgobj[3], msgobj[4]
end
if msgobj[1] == "GrpHeal" and msgobj[2] then
msgtype, target, heal, time = "Heal", {}, msgobj[2], msgobj[3]
for i=4,8 do
if msgobj[i] then table.insert(target, msgobj[i]) end
end end
end end
elseif select and UnitCastingInfo then if not name then break end
-- latest healcomm local byCaster = guid and hotAmounts[guid]
msgtype = tonumber(string.sub(msg, 1, 3)) local amount = byCaster and byCaster[spell]
if not msgtype then return end if amount and expires and expires > now then
count = count + 1
if msgtype == 0 then local hot = state.entries[count] or {}
msgtype = "Heal" state.entries[count] = hot
heal = tonumber(string.sub(msg, 4, 8)) hot.amount, hot.expires = amount, expires
target = string.sub(msg,9, -1)
local starttime = select(5, UnitCastingInfo(sender))
local endtime = select(6, UnitCastingInfo(sender))
if not starttime or not endtime then return end
time = endtime - starttime
elseif msgtype == 1 then
msgtype = "Stop"
elseif msgtype == 2 then
msgtype = "Heal"
heal = tonumber(string.sub(msg,4, 8))
target = {strsplit(":", string.sub(msg,9, -1))}
local starttime = select(5, UnitCastingInfo(sender))
local endtime = select(6, UnitCastingInfo(sender))
if not starttime or not endtime then return end
time = endtime - starttime
end end
end end
end end
for index = table.getn(state.entries), count+1, -1 do state.entries[index] = nil end
return msgtype, target, heal, time -- Lua 5.0 caches the length used by table.remove; direct slot reuse must
-- update it explicitly, or an expired/refreshed HoT can leave stale entries.
table.setn(state.entries, count)
Notify(state.name)
if count > 0 then elapsed = 0; driver:SetScript("OnUpdate", Expire) end
end end
local function TryShared()
function libpredict:ParseChatMessage(sender, msg, comm) local shared = _G.PTHealPredict
local msgtype, target, heal, time if type(shared) ~= "table" or type(shared.GetIncomingHealing) ~= "function"
or type(shared.HookUpdates) ~= "function" then return false end
if comm == "HealComm" then -- Puppeteer uses SuperWoW GUIDs; never hand it incompatible identifiers.
msgtype, target, heal, time = libpredict:ParseComm(sender, msg) local _, guid = UnitExists("player")
elseif comm == "CTRA" then if not guid or guid ~= UnitGUID("player") then return false end
local _, _, cmd, ctratarget = string.find(msg, "(%a+)%s?([^#]*)") P.external, P.source = shared, "Puppeteer"
if cmd == "RES" and ctratarget and ctratarget ~= "" and ctratarget ~= UNKNOWN then learned, activeGUID, sentGUID, sentTarget, sentSpell = nil, nil, nil, nil, nil
msgtype = "Ress" if driver then driver:UnregisterAllEvents(); driver:SetScript("OnUpdate", nil) end
target = ctratarget for name in pairs(P.pending) do P.pending[name] = nil end
elseif cmd == "RESNO" then hotUnits, hotAmounts = {}, {}
msgtype = "RessStop" if not sharedHooks[shared] then
end shared.HookUpdates(function(changedGUID)
end if not P.enabled or P.external ~= shared then return end
for _, unit in ipairs(trackedUnits) do
if msgtype == "Stop" and sender then if UnitGUID(unit) == changedGUID then Notify(UnitName(unit)) end
libpredict:HealStop(sender)
return
elseif msgtype == "RessStop" and sender then
libpredict:RessStop(sender)
return
elseif msgtype == "Delay" and time then
libpredict:HealDelay(sender, time)
elseif msgtype == "Heal" and target and heal and time then
if type(target) == "table" then
for _, name in pairs(target) do
libpredict:Heal(sender, name, heal, time)
end end
else end)
libpredict:Heal(sender, target, heal, time) sharedHooks[shared] = true
end
Notify(UnitName("player")); Notify(UnitName("target"))
return true
end
local function Broadcast(message)
if _G.AceLibrary and AceLibrary.HasInstance and AceLibrary:HasInstance("HealComm-1.0") then return end
local channel = GetNumRaidMembers() > 0 and "RAID" or GetNumPartyMembers() > 0 and "PARTY"
if channel then SendAddonMessage("HealComm", message, channel) end
end
local function StopOwn(guid)
if guid and activeGUID and guid ~= activeGUID then return end
P:HealStop(player)
if activeGUID then Broadcast("Healstop") end
activeGUID = nil
end
local function RefreshOwn()
if not sentSpell or not learned[sentSpell] then return end
local name, _, _, startTime, endTime = API.UnitCastingInfo("player")
if not name or not startTime or not endTime then return end
local remaining = endTime - GetTime() * 1000
if remaining <= 0 then return end
activeGUID = sentGUID
P:Heal(player, sentTarget or player, learned[sentSpell], remaining)
Broadcast("Heal/" .. (sentTarget or player) .. "/" .. learned[sentSpell] .. "/" .. math.floor(remaining) .. "/")
end
function P:ParseComm(sender, message)
if type(sender) ~= "string" or type(message) ~= "string" then return end
if message == "Healstop" or message == "GrpHealstop" or message == "001F" then self:HealStop(sender); return end
local _, _, target, amount, duration = string.find(message, "^Heal/([^/]+)/(%d+)/(%d+)/$")
if target then self:Heal(sender, target, amount, duration); return end
local _, _, delay = string.find(message, "^Healdelay/(%d+)/$")
delay = tonumber(delay)
if delay and delay <= 60000 then
for name, senders in pairs(self.pending) do
if senders[sender] then senders[sender].expires = senders[sender].expires + delay/1000; Notify(name) end
end end
elseif msgtype == "Ress" then
libpredict:Ress(sender, target)
end end
end end
local function Reset()
function libpredict:AddEvent(time, target) for target in pairs(P.pending) do P.pending[target] = nil; Notify(target) end
events[time] = events[time] or {} for unit, state in pairs(hotUnits) do hotUnits[unit] = nil; Notify(state.name) end
table.insert(events[time], target) if driver then driver:SetScript("OnUpdate", nil) end
activeGUID, sentGUID, sentTarget, sentSpell = nil, nil, nil, nil
if not self.timeoutActive then
self.timeoutActive = true
self:SetScript("OnUpdate", UpdateTimeouts)
end
end end
local function OnEvent()
function libpredict:Heal(sender, target, amount, duration) if event == "ADDON_LOADED" then
if not sender or not target or not amount or not duration then TryShared()
return elseif event == "UNIT_AURA" then
end RefreshHots(arg1)
elseif event == "PLAYER_TARGET_CHANGED" then
local timeout = duration/1000 + GetTime() RefreshHots("target")
heals[target] = heals[target] or {} elseif event == "CHAT_MSG_ADDON" then
heals[target][sender] = { amount, timeout } if arg1 == "HealComm" and (arg3 == "PARTY" or arg3 == "RAID" or arg3 == "BATTLEGROUND") then P:ParseComm(arg4, arg2) end
libpredict:AddEvent(timeout, target) elseif event == "COMBAT_LOG_EVENT_UNFILTERED" then
end if arg2 == "SPELL_PERIODIC_HEAL" and arg4 and arg8 and arg12 and tonumber(arg15)
and tonumber(arg15) > 0 and arg18 ~= true and arg18 ~= 1 then
function libpredict:HealStop(sender) -- Learn only ticks landing on one of the two displayed units. Keep
for ttarget, t in pairs(heals) do -- direct and periodic amounts separate (Regrowth uses both).
for tsender in pairs(heals[ttarget]) do if arg8 == UnitGUID("player") or arg8 == UnitGUID("target") then
if sender == tsender then local amounts = hotAmounts[arg4] or {}
heals[ttarget][tsender] = nil hotAmounts[arg4] = amounts
amounts[arg12] = tonumber(arg15)
if arg8 == UnitGUID("player") then RefreshHots("player") end
if arg8 == UnitGUID("target") then RefreshHots("target") end
end end
end
end
end
function libpredict:HealDelay(sender, delay)
local delay = delay/1000
for target, t in pairs(heals) do
for tsender, amount in pairs(heals[target]) do
if sender == tsender then
amount[2] = amount[2] + delay
libpredict:AddEvent(amount[2], target)
end
end
end
end
function libpredict:Ress(sender, target)
ress[target] = ress[target] or {}
ress[target][sender] = true
end
function libpredict:RessStop(sender)
for ttarget, t in pairs(ress) do
for tsender in pairs(ress[ttarget]) do
if sender == tsender then
ress[ttarget][tsender] = nil
end
end
end
end
function libpredict:UnitGetIncomingHeals(unit)
if not unit or not UnitName(unit) then return 0 end
if UnitIsDeadOrGhost(unit) then return 0 end
local name = UnitName(unit)
local sumheal = 0
if not heals[name] then
return sumheal
else
for sender, amount in pairs(heals[name]) do
if amount[2] <= GetTime() then
heals[name][sender] = nil
else
sumheal = sumheal + amount[1]
end
end
end
return sumheal
end
function libpredict:UnitHasIncomingResurrection(unit)
if not unit or not UnitName(unit) then return nil end
local name = UnitName(unit)
if not ress[name] then
return nil
else
for sender, val in pairs(ress[name]) do
if val == true then
return val
end
end
end
return nil
end
local spell_queue = { "DUMMY", "DUMMYRank 9", "TARGET" }
local realm = GetRealmName()
local player = UnitName("player")
local cache, gear_string = {}, ""
local resetcache = CreateFrame("Frame")
resetcache:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" then
-- load and initialize previous caches of spell amounts
ShaguTweaks_cache = ShaguTweaks_cache or {}
ShaguTweaks_cache["prediction"] = ShaguTweaks_cache["prediction"] or {}
ShaguTweaks_cache["prediction"][realm] = ShaguTweaks_cache["prediction"][realm] or {}
ShaguTweaks_cache["prediction"][realm][player] = ShaguTweaks_cache["prediction"][realm][player] or {}
ShaguTweaks_cache["prediction"][realm][player]["heals"] = ShaguTweaks_cache["prediction"][realm][player]["heals"] or {}
cache = ShaguTweaks_cache["prediction"][realm][player]["heals"]
return
end
if event == "UNIT_INVENTORY_CHANGED" then
-- skip non-player events
if arg1 and arg1 ~= "player" then return end
local gear = ""
for id = 1, 18 do
gear = gear .. (GetInventoryItemLink("player",id) or "")
end
-- abort when inventory didn't change
if gear == gear_string then return end
gear_string = gear
end
-- flag all cached heals for renewal
for k in pairs(cache) do
if type(cache[k]) == "number" or type(cache[k]) == "string" then
-- migrate old data
local oldval = cache[k]
cache[k] = { [1] = oldval }
end
-- flag for reset
cache[k][2] = true
end
end)
local function UpdateCache(spell, heal, crit)
if not spell or not heal then return end
local heal = tonumber(heal)
if not cache[spell] or cache[spell][2] then
-- skills or equipment changed, save whatever is detected
cache[spell] = cache[spell] or {}
cache[spell][1] = crit and heal*2/3 or heal
cache[spell][2] = crit
elseif not crit and cache[spell][1] < heal then
-- safe the best heal we can get
cache[spell][1] = heal
cache[spell][2] = nil
end
end
-- Gather Data by User Actions. Hooks are installed lazily by libpredict:Enable()
-- so disabling healing predictions leaves no spell hook on the hot path.
local scanner
local hooksInstalled
local useClassicSenderEvents
local function TrackCastSpell(id, bookType)
if not libpredict.sender.enabled then return end
local effect, rank = libspell.GetSpellInfo(id, bookType)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
end
local function TrackCastSpellByName(effect, target)
if not libpredict.sender.enabled then return end
local effect, rank = libspell.GetSpellInfo(effect)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
end
local function TrackUseAction(slot, target, selfcast)
if not libpredict.sender.enabled then return end
-- Conditional macros are resolved by the later CastSpellByName hook. For a
-- direct spell action, ClassicAPI gives us the spellID without a tooltip.
if API and API.actioninfo and API.GetActionInfo then
local actionType, actionID = API.GetActionInfo(slot)
if actionType == "macro" or actionType == "item" then return end
if actionType == "spell" and actionID then
if not IsCurrentAction(slot) then return end
local effect, rank = API.GetSpellInfo(actionID)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
return
elseif actionType then
return return
end end
if arg2 == "SPELL_HEAL" and arg4 == UnitGUID("player") and arg12 and tonumber(arg15)
and tonumber(arg15) > 0 and arg18 ~= true and arg18 ~= 1 then learned[arg12] = tonumber(arg15) end
elseif event == "PLAYER_ENTERING_WORLD" then
if TryShared() then return end
player = UnitName("player"); Reset()
if EquipmentChanged() then for spell in pairs(learned) do learned[spell] = nil end end
RefreshHots("player"); RefreshHots("target")
elseif event == "UNIT_INVENTORY_CHANGED" or event == "PLAYER_LEVEL_UP" or event == "CHARACTER_POINTS_CHANGED" then
if event ~= "UNIT_INVENTORY_CHANGED" or (arg1 == "player" and EquipmentChanged()) then
for spell in pairs(learned) do learned[spell] = nil end
local guid = UnitGUID("player")
if guid then hotAmounts[guid] = nil end
StopOwn()
RefreshHots("player"); RefreshHots("target")
end
elseif arg1 == "player" then
if event == "UNIT_SPELLCAST_SENT" then
sentTarget = arg2 and arg2 ~= "" and (UnitExists(arg2) and UnitName(arg2) or arg2) or player
sentGUID, sentSpell = arg3, arg4
elseif event == "UNIT_SPELLCAST_START" or event == "UNIT_SPELLCAST_DELAYED" then
if arg2 == sentGUID then RefreshOwn() end
elseif event == "UNIT_SPELLCAST_STOP" or event == "UNIT_SPELLCAST_FAILED"
or event == "UNIT_SPELLCAST_INTERRUPTED" or event == "UNIT_SPELLCAST_SUCCEEDED" then StopOwn(arg2) end
end end
if GetActionText(slot) or not IsCurrentAction(slot) then return end
scanner = scanner or libtipscan:GetScanner("prediction")
scanner:SetAction(slot)
local effect, rank = scanner:Line(1)
if not effect then return end
spell_queue[1] = effect
spell_queue[2] = effect.. ( rank or "" )
spell_queue[3] = selfcast and UnitName("player") or UnitName("target") and UnitCanAssist("player", "target") and UnitName("target") or UnitName("player")
end end
function P:Enable()
local function InstallSenderHooks()
if hooksInstalled then return end
hooksecurefunc("CastSpell", TrackCastSpell)
hooksecurefunc("CastSpellByName", TrackCastSpellByName)
hooksecurefunc("UseAction", TrackUseAction)
hooksInstalled = true
end
libpredict.sender = CreateFrame("Frame", "ShaguTweaksPredictionSender", UIParent)
libpredict.sender.enabled = false
libpredict.sender.SendHealCommMsg = function(self, msg)
SendAddonMessage("HealComm", msg, "RAID")
SendAddonMessage("HealComm", msg, "BATTLEGROUND")
end
libpredict.sender.SendResCommMsg = function(self, msg)
SendAddonMessage("CTRA", msg, "RAID")
SendAddonMessage("CTRA", msg, "BATTLEGROUND")
end
libpredict.sender:SetScript("OnEvent", function()
if event == "CHAT_MSG_SPELL_SELF_BUFF" then -- vanilla
local spell, _, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
return
end
local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
return
end
local spell, heal = cmatch(arg1, HEALEDCRITSELFOTHER) -- "Your %s critically heals %s for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
return
end
local spell, _, heal = cmatch(arg1, HEALEDCRITSELFSELF) -- "Your %s critically heals you for %d."
if spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
return
end
elseif event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_HEAL" and arg4 == player then -- tbc
local spell, heal, crit = arg10, arg12, arg13
if spell and heal and crit then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal, true) end
elseif spell and heal then
if spell == spell_queue[1] then UpdateCache(spell_queue[2], heal) end
end
elseif event == "UNIT_SPELLCAST_SENT" then
if arg1 ~= "player" then return end
if GetExpansion() == "vanilla" and useClassicSenderEvents then
-- ClassicAPI already resolved the actual outgoing spell and target:
-- unit, target, castGUID, spellID, spellName, rank. Use it directly so
-- prediction tracking needs no CastSpell/UseAction hooks at all.
local target = arg2 and arg2 ~= "" and arg2 or UnitName("player")
local spell = arg5
local rank = arg6 or ""
senttarget = target
if spell then
spell_queue[1] = spell
spell_queue[2] = spell .. rank
spell_queue[3] = target
end
else
-- Native TBC layout keeps the target in arg4.
local castTarget = arg4
senttarget = castTarget and castTarget ~= "" and castTarget or nil
end
elseif strfind(event, "SPELLCAST_START", 1) then
local spell, time = arg1, arg2
if strfind(event, "UNIT_", 1) then
if arg1 ~= "player" then return end
if GetExpansion() == "vanilla" and API and API.casts then
local spellname, _, _, _, starttime, endtime = API.GetCastInfo("player")
spell = spellname
time = starttime and endtime and endtime - starttime or nil
else
local spellname, _, _, _, starttime, endtime = UnitCastingInfo("player")
spell = spellname
time = starttime and endtime and endtime - starttime or nil
end
end
if not spell or not time then return end
if spell_queue[1] == spell and cache[spell_queue[2]] then
local sender = player
local target = senttarget or spell_queue[3]
local amount = cache[spell_queue[2]][1]
local casttime = time
if spell == PRAYER_OF_HEALING then
target = sender
for i=1,4 do
if CheckInteractDistance("party"..i, 4) then
libpredict:Heal(player, UnitName("party"..i), amount, casttime)
if GetExpansion() == "vanilla" then -- vanilla
libpredict.sender:SendHealCommMsg("Heal/" .. UnitName("party"..i) .. "/" .. amount .. "/" .. casttime .. "/")
else -- tbc
libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), UnitName("party"..i)))
end
libpredict.sender.healing = true
end
end
end
libpredict:Heal(player, target, amount, casttime)
if GetExpansion() == "vanilla" then -- vanilla
libpredict.sender:SendHealCommMsg("Heal/" .. target .. "/" .. amount .. "/" .. casttime .. "/")
else -- tbc
libpredict.sender:SendHealCommMsg(string.format("002%05d%s", math.min(amount, 99999), target))
end
libpredict.sender.healing = true
elseif spell_queue[1] == spell and L["resurrections"][spell] then
local target = senttarget or spell_queue[3]
libpredict:Ress(player, target)
libpredict.sender:SendHealCommMsg("Resurrection/" .. target .. "/start/")
libpredict.sender:SendResCommMsg("RES " .. target)
libpredict.sender.resurrecting = true
end
elseif strfind(event, "SPELLCAST_FAILED", 1) or strfind(event, "SPELLCAST_INTERRUPTED", 1) then
if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end
if libpredict.sender.healing then
libpredict:HealStop(player)
if GetExpansion() == "vanilla" then -- vanilla
libpredict.sender:SendHealCommMsg("HealStop")
else -- tbc
libpredict.sender:SendHealCommMsg("001F")
end
libpredict.sender.healing = nil
elseif libpredict.sender.resurrecting then
local target = senttarget or spell_queue[3]
libpredict:RessStop(player)
libpredict.sender:SendHealCommMsg("Resurrection/stop/")
libpredict.sender:SendResCommMsg("RESNO " .. target)
libpredict.sender.resurrecting = nil
end
senttarget = nil
elseif event == "SPELLCAST_DELAYED" then
if libpredict.sender.healing then
libpredict:HealDelay(player, arg1)
libpredict.sender:SendHealCommMsg("Healdelay/" .. arg1 .. "/")
end
elseif strfind(event, "SPELLCAST_STOP", 1) then
if strfind(event, "UNIT_", 1) and arg1 ~= "player" then return end
libpredict:HealStop(player)
senttarget = nil
end
end)
function libpredict:Enable()
if self.enabled then return end if self.enabled then return end
self.enabled = true self.enabled = true
self.sender.enabled = true if TryShared() then return end
self.source = "ClassicAPI + HealComm"
-- incoming HealComm / resurrection tracking player = UnitName("player")
self:RegisterEvent("UNIT_HEALTH") learned = {} -- Session-only: actual gear/talent/level changes invalidate estimates.
self:RegisterEvent("CHAT_MSG_ADDON") if EquipmentChanged() then
self:RegisterEvent("PLAYER_TARGET_CHANGED") local guid = UnitGUID("player")
if guid then hotAmounts[guid] = nil end
-- prediction cache lifecycle
resetcache:RegisterEvent("PLAYER_ENTERING_WORLD")
resetcache:RegisterEvent("SKILL_LINES_CHANGED")
resetcache:RegisterEvent("UNIT_INVENTORY_CHANGED")
if GetExpansion() == "vanilla" then
-- ClassicAPI supplies structured player spellcast events on 1.12. Use
-- those instead of registering both event families and processing each cast
-- twice. Keep the native SPELLCAST_DELAYED event because it carries the
-- exact delay delta consumed by HealDelay().
useClassicSenderEvents = API and API.eventutils and _G.C_EventUtils
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_START")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_STOP")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_FAILED")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_FAILED_QUIET")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_INTERRUPTED")
and _G.C_EventUtils.IsEventValid("UNIT_SPELLCAST_SENT")
if useClassicSenderEvents then
self.sender:RegisterEvent("UNIT_SPELLCAST_START")
self.sender:RegisterEvent("UNIT_SPELLCAST_STOP")
self.sender:RegisterEvent("UNIT_SPELLCAST_FAILED")
self.sender:RegisterEvent("UNIT_SPELLCAST_FAILED_QUIET")
self.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
self.sender:RegisterEvent("UNIT_SPELLCAST_SENT")
else
self.sender:RegisterEvent("SPELLCAST_START")
self.sender:RegisterEvent("SPELLCAST_STOP")
self.sender:RegisterEvent("SPELLCAST_FAILED")
self.sender:RegisterEvent("SPELLCAST_INTERRUPTED")
end
self.sender:RegisterEvent("SPELLCAST_DELAYED")
self.sender:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF")
else
-- Native TBC event layout and combat log.
self.sender:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
self.sender:RegisterEvent("UNIT_SPELLCAST_START")
self.sender:RegisterEvent("UNIT_SPELLCAST_STOP")
self.sender:RegisterEvent("UNIT_SPELLCAST_FAILED")
self.sender:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED")
self.sender:RegisterEvent("UNIT_SPELLCAST_SENT")
end
-- force cache updates
self.sender:RegisterEvent("UNIT_INVENTORY_CHANGED")
self.sender:RegisterEvent("SKILL_LINES_CHANGED")
if not useClassicSenderEvents then
InstallSenderHooks()
end end
driver = driver or CreateFrame("Frame")
driver:SetScript("OnEvent", OnEvent)
driver:RegisterEvent("PLAYER_ENTERING_WORLD")
driver:RegisterEvent("ADDON_LOADED")
driver:RegisterEvent("PLAYER_TARGET_CHANGED")
API.RegisterUnitEvent(driver,"UNIT_AURA","player","target")
driver:RegisterEvent("CHAT_MSG_ADDON")
driver:RegisterEvent("UNIT_INVENTORY_CHANGED")
-- Form changes can produce SPELLS_CHANGED without changing any heal amount.
-- New spell ranks have separate spell IDs and learn independently.
driver:RegisterEvent("PLAYER_LEVEL_UP")
driver:RegisterEvent("CHARACTER_POINTS_CHANGED")
local native = {"UNIT_SPELLCAST_SENT", "UNIT_SPELLCAST_START", "UNIT_SPELLCAST_DELAYED",
"UNIT_SPELLCAST_STOP", "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_FAILED", "UNIT_SPELLCAST_INTERRUPTED",
"COMBAT_LOG_EVENT_UNFILTERED"}
for _, name in ipairs(native) do if _G.C_EventUtils.IsEventValid(name) then driver:RegisterEvent(name) end end
elapsed = 0
RefreshHots("player"); RefreshHots("target")
end
function P:Disable()
self.enabled = false
self.external = nil
if driver then driver:UnregisterAllEvents() end
Reset()
end end
ShaguTweaks.libpredict = libpredict
File diff suppressed because it is too large Load Diff
+86 -8
View File
@@ -8,8 +8,6 @@ ShaguTweaks_translation["enUS"] = {
["Adds coordinates to the bottom of the World Map."] = nil, ["Adds coordinates to the bottom of the World Map."] = nil,
["Advanced Options"] = nil, ["Advanced Options"] = nil,
["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil, ["Allows to scroll using the mouse wheel, enables sticky chat channels and repeats message on arrow up."] = nil,
["Auto Dismount"] = nil,
["Automatically dismounts whenever a spell is casted."] = nil,
["Automatically switch to the required warrior or druid stance on spell cast."] = nil, ["Automatically switch to the required warrior or druid stance on spell cast."] = nil,
["Auto Stance"] = nil, ["Auto Stance"] = nil,
["Blue Shaman Class Colors"] = nil, ["Blue Shaman Class Colors"] = nil,
@@ -23,7 +21,8 @@ ShaguTweaks_translation["enUS"] = {
["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil, ["Copy website URLs from the chat, transforms CLINKs into real items and handles quest and player links."] = nil,
["Cursor"] = nil, ["Cursor"] = nil,
["Darkened UI"] = nil, ["Darkened UI"] = nil,
["Debuff Timer"] = 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,
["Display health, class color, guild name, guild rank and current target on unit tooltips."] = 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, ["Display the remaining duration as text on every cooldown."] = nil,
["Draw the mini map in a squared shape instead of a round one."] = nil, ["Draw the mini map in a squared shape instead of a round one."] = nil,
@@ -47,9 +46,6 @@ ShaguTweaks_translation["enUS"] = {
["MiniMap Tweaks"] = nil, ["MiniMap Tweaks"] = nil,
["Movable Unit Frames"] = nil, ["Movable Unit Frames"] = nil,
["N/A"] = nil, ["N/A"] = nil,
["Nameplate Castbar"] = nil,
["Nameplate Class Colors"] = nil,
["Nameplate Scale"] = nil,
["Nameplates"] = nil, ["Nameplates"] = nil,
["Player"] = nil, ["Player"] = nil,
["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil, ["Player and Target unit frames can be moved while <Shift> and <Ctrl> are pressed together."] = nil,
@@ -64,7 +60,7 @@ ShaguTweaks_translation["enUS"] = {
["Show class colors in Who, Guild, Friends and Chat."] = nil, ["Show class colors in Who, Guild, Friends and Chat."] = nil,
["Show debuff durations on the target unit frame."] = nil, ["Show debuff durations on the target unit frame."] = nil,
["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil, ["Show item rarity as the border color on bags, bank, character and inspect frames."] = nil,
["Shows an enemy castbar on target unit frame."] = nil, ["Shows an enemy castbar on target unit frame. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."] = nil,
["Shows current and needed XP, percentage and rested bonus while hovering over the experience bar."] = nil, ["Shows current and needed XP, percentage and rested bonus while hovering over the experience bar."] = nil,
["Shows currently equipped items on tooltips while the shift key is pressed."] = nil, ["Shows currently equipped items on tooltips while the shift key is pressed."] = nil,
["Shows the vendor sell values on all item tooltips."] = nil, ["Shows the vendor sell values on all item tooltips."] = nil,
@@ -90,7 +86,7 @@ ShaguTweaks_translation["enUS"] = {
["Main Hand"] = nil, ["Main Hand"] = nil,
["Off Hand"] = nil, ["Off Hand"] = nil,
["Ranged"] = nil, ["Ranged"] = nil,
["Shows main-hand and off-hand swing timers above the cast bar, with the weapon's icon. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."] = nil, ["Shows main-hand and off-hand swing timers above the cast bar, with the weapon's icon. Hold <Ctrl>, <Shift> and <Alt> to move each bar on its own; right-click a bar to reset it."] = nil,
["Shows the time until your next wand shot (or Auto Shot) above the cast bar, with the weapon's icon."] = nil, ["Shows the time until your next wand shot (or Auto Shot) above the cast bar, with the weapon's icon."] = nil,
["Transports"] = nil, ["Transports"] = nil,
["Shows or hides the OctoBus transport timers."] = nil, ["Shows or hides the OctoBus transport timers."] = nil,
@@ -101,4 +97,86 @@ ShaguTweaks_translation["enUS"] = {
["Clear"] = nil, ["Clear"] = nil,
["No loot rolls yet."] = nil, ["No loot rolls yet."] = nil,
["Loot History requires ClassicAPI."] = nil, ["Loot History requires ClassicAPI."] = nil,
["Enemy Castbar"] = nil,
["Movable Cast Bar"] = nil,
["Hold <Ctrl>, <Shift> and <Alt> to move the cast bar; right-click to reset."] = nil,
["Cast Bar"] = nil,
["Loss of Control Alert"] = nil,
["Shows a large icon and countdown when you are stunned, feared, silenced, rooted, disarmed or locked out of a spell school. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."] = nil,
["Stunned"] = nil,
["Feared"] = nil,
["Confused"] = nil,
["Charmed"] = nil,
["Possessed"] = nil,
["Silenced"] = nil,
["Pacified"] = nil,
["Pacified and Silenced"] = nil,
["Disarmed"] = nil,
["Rooted"] = nil,
["Interrupted"] = nil,
["Drag to move, right-click to reset"] = nil,
["Totem Timers"] = nil,
["Shamans: shows the totems you have out as icons with the time left. Left-click targets a totem. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."] = nil,
["Dragonflight Gryphons"] = nil,
["Replaces actionbar gryphons with the dragonflight version."] = nil,
["Action Bar"] = nil,
["Floating Actionbar"] = nil,
["Removes all background textures and lets the actionbar float."] = nil,
["Hide Macro Text"] = nil,
["Hides macro names on action buttons."] = nil,
["Mouseover Right"] = nil,
["Hide the right actionbar and show it on mouseover."] = nil,
["Mouseover Right 2"] = nil,
["Hide the second right actionbar and show it on mouseover."] = nil,
["Reagent Counter"] = nil,
["Shows a reagent counter on action buttons."] = nil,
["Center Vertical Actionbar"] = nil,
["Center the vertical actionbar on the right side."] = nil,
["Buy Em All"] = nil,
["Shift-click vendor items to choose stack, maximum, or a custom purchase amount."] = nil,
["Merchant"] = nil,
["Center Text Input Box"] = nil,
["Move the chat input box to the center of the screen."] = nil,
["Chat & Social"] = nil,
["Block NPC Spam"] = nil,
["Blocks known repetitive NPC say/yell spam messages."] = nil,
["Enable Text Shadow"] = nil,
["Enable text shadow in all chat frames."] = nil,
["Chat Timestamps"] = nil,
["Add timestamps to chat messages."] = nil,
["Cursor Tooltip"] = nil,
["Attaches default tooltips to the cursor."] = nil,
["Hide Combat Tooltip"] = nil,
["Hides the tooltip in combat. Hold Shift to show it temporarily."] = nil,
["Loot Quality"] = nil,
["Colors the loot frame using the highest item quality in the current loot window."] = nil,
["Loot"] = nil,
["Macro Icons"] = nil,
["Detect showtooltip and spells in macros to use them on action buttons."] = nil,
["Macro"] = nil,
["Macro Tweaks"] = nil,
["Add /equip, /use and modern ClassicAPI macro commands, remove #showtooltip from chat and hide macro commands from history."] = nil,
["Metric Range"] = nil,
["Displays tooltip range units in metres instead of yards."] = nil,
["MiniMap Framerate & Latency"] = nil,
["Adds a small framerate & latency display to the mini map."] = nil,
["Minimap & World Map"] = nil,
["Framerate"] = nil,
["High"] = nil,
["fps"] = nil,
["Low"] = nil,
["Latency"] = nil,
["ms"] = nil,
["Show Bags"] = nil,
["Shows bag and keyring buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the bag bar."] = nil,
["Show Micro Menu"] = nil,
["Shows micro menu buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the micro menu."] = nil,
["Unit Frame Abbreviated Names"] = nil,
["Abbreviates long target and target-of-target names."] = nil,
["World Chat Hider"] = nil,
["Hides World channel messages while inside an instance without leaving the channel."] = nil,
["Reveal World Map"] = nil,
["Reveals unexplored world map areas and shows exploration hints."] = nil,
["Reveal Unexplored"] = nil,
["Exploration Point"] = nil,
} }
+56
View File
@@ -43,19 +43,70 @@ ShaguTweaks:RegisterEvent("VARIABLES_LOADED")
ShaguTweaks:SetScript("OnEvent", function() ShaguTweaks:SetScript("OnEvent", function()
-- flag all external modules as unofficial -- flag all external modules as unofficial
if event == "ADDON_LOADED" then if event == "ADDON_LOADED" then
-- Most Extras ClassicAPI modules are built in now. With the old addon
-- still enabled, its copies replace the built-in ones (same titles).
if arg1 == "ShaguTweaks-extras" then
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00ShaguTweaks:|r the Extras modules are now built in. Disable the |cffffcc00ShaguTweaks Extras ClassicAPI|r addon so they only run once.")
end
ShaguTweaks.provider = nil ShaguTweaks.provider = nil
official = false official = false
return return
end end
-- load current expansion -- load current expansion
if not ShaguTweaks.API.IsSupportedRuntime() then
DEFAULT_CHAT_FRAME:AddMessage("|cffff6060ShaguTweaks:|r ClassicAPI " .. ShaguTweaks.API.minimum_version_text
.. "+ is required. Update both its DLL and !!!ClassicAPI addon, then restart the client. Tweaks have not been enabled.")
return
end
local expansion = ShaguTweaks:GetExpansion() local expansion = ShaguTweaks:GetExpansion()
-- initialize empty config -- initialize empty config
if not ShaguTweaks_config then ShaguTweaks_config = {} end if not ShaguTweaks_config then ShaguTweaks_config = {} end
local prediction = ShaguTweaks.T["Healing Predictions"]
for _, oldName in ipairs({"Enable Healing Prediction", "Enable Healing Prediction on Unit Frames"}) do
local oldPrediction = ShaguTweaks.T[oldName]
if ShaguTweaks_config[prediction] == nil then ShaguTweaks_config[prediction] = ShaguTweaks_config[oldPrediction] end
ShaguTweaks_config[oldPrediction] = nil
end
-- Retire the removed duplicate toggle; ClassicAPI owns these preferences.
ShaguTweaks_config[ShaguTweaks.T["Auto Dismount"]] = nil
-- Area looting is delegated to a dedicated addon from 3.8.6 onward.
ShaguTweaks_config[ShaguTweaks.T["AoE Loot"]] = nil
-- Gold numbers belong to Real Health Numbers; retire the duplicate toggle. -- Gold numbers belong to Real Health Numbers; retire the duplicate toggle.
ShaguTweaks_config["Unit Frame Health Colors"] = nil ShaguTweaks_config["Unit Frame Health Colors"] = nil
-- The portrait-only hiding option has been removed.
ShaguTweaks_config[ShaguTweaks.T["Hide Player Portrait"]] = 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
-- Replace detached native bars with copies; hiding the originals is opt-in.
local oldStandalone = ShaguTweaks.T["Standalone Player Frame"]
local resourceDisplay = ShaguTweaks.T["Personal Resource Display"]
if ShaguTweaks_config[resourceDisplay] == nil then
ShaguTweaks_config[resourceDisplay] = ShaguTweaks_config[oldStandalone]
end
if ShaguTweaks_config.StandalonePlayerFrame then
local positions = ShaguTweaks_config.PersonalResourceDisplay or {}
for _, key in ipairs({"Health", "Power"}) do
if not positions[key] then positions[key] = ShaguTweaks_config.StandalonePlayerFrame[key] end
end
ShaguTweaks_config.PersonalResourceDisplay = positions
end
ShaguTweaks_config[oldStandalone] = nil
ShaguTweaks_config.StandalonePlayerFrame = nil
-- initialize overwrites -- initialize overwrites
ShaguTweaks_config.overwrites = ShaguTweaks_config.overwrites or {} ShaguTweaks_config.overwrites = ShaguTweaks_config.overwrites or {}
@@ -88,6 +139,11 @@ ShaguTweaks:SetScript("OnEvent", function()
end end
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 -- Libraries that can stay dormant until a module actually consumes them
-- get one final activation pass after every enabled module has registered -- get one final activation pass after every enabled module has registered
-- its callbacks. -- its callbacks.
+40
View File
@@ -0,0 +1,40 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Dragonflight Gryphons"],
description = T["Replaces actionbar gryphons with the dragonflight version."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Action Bar"],
config = {
["dragonfly.gryphon"] = "retail",
},
enabled = nil,
})
module.enable = function(self)
local gryphon = CreateFrame("Frame", nil, MainMenuBarArtFrame)
gryphon:SetFrameStrata("HIGH")
gryphon:RegisterEvent("PLAYER_ENTERING_WORLD")
gryphon:SetScript("OnEvent", function()
-- replace original gryphons by dragonflight versions
if module.config["dragonfly.gryphon"] == "retail" and UnitFactionGroup("player") == "Horde" then
-- retail horde
MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-wyvern")
MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-wyvern")
elseif module.config["dragonfly.gryphon"] == "retail" and UnitFactionGroup("player") == "Alliance" then
-- retail alliance
MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon")
MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon")
else
-- beta artwork
MainMenuBarLeftEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon-beta")
MainMenuBarRightEndCap:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\df-gryphon-beta")
end
-- move gryphons above action buttons
MainMenuBarLeftEndCap:SetParent(this)
MainMenuBarRightEndCap:SetParent(this)
end)
end
+107
View File
@@ -0,0 +1,107 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Floating Actionbar"],
description = T["Removes all background textures and lets the actionbar float."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Action Bar"],
enabled = nil,
})
local texture_removals = {
MainMenuXPBarTexture0, MainMenuXPBarTexture1, MainMenuXPBarTexture2, MainMenuXPBarTexture3,
ReputationXPBarTexture0, ReputationXPBarTexture1, ReputationXPBarTexture2, ReputationXPBarTexture3,
ReputationWatchBarTexture0, ReputationWatchBarTexture1, ReputationWatchBarTexture2, ReputationWatchBarTexture3,
MainMenuBarTexture0, MainMenuBarTexture1, MainMenuBarTexture2, MainMenuBarTexture3,
BonusActionBarTexture1, BonusActionBarTexture0, BonusActionBarTexture2
}
local actionbars = {
"Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight",
"MultiBarLeft", "MultiBarRight", "Shapeshift"
}
module.enable = function(self)
MainMenuBar:ClearAllPoints()
MainMenuBar:SetPoint("BOTTOM", 0, 8)
-- align actionbutton textures and add border
for _, prefix in ipairs(actionbars) do
for i = 1, NUM_ACTIONBAR_BUTTONS do
local button = _G[prefix .. "Button" .. i]
local texture = _G[prefix.."Button"..i.."NormalTexture"]
if button and texture then
texture:SetWidth(60)
texture:SetHeight(60)
texture:SetPoint("CENTER", 0, 0)
ShaguTweaks.AddBorder(button, 3, { r=.7, g=.7, b=.7, a=1 })
end
end
end
ShaguTweaks.AddBorder(MainMenuBarPerformanceBarFrameButton, { -12, -0.5, -8, 4.5 }, { r=.7, g=.7, b=.7, a=1 })
-- replace reputation bar texture
ReputationWatchStatusBar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
ReputationWatchStatusBarBackground:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
ReputationWatchStatusBarBackground:SetVertexColor(0, 0, 0, .5)
-- replace experience bar texture
MainMenuExpBar:SetStatusBarTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
local _, _, _, _, _, background = MainMenuExpBar:GetRegions()
if background then
background:SetTexture("Interface\\AddOns\\ShaguTweaks\\img\\xpbar")
background:SetVertexColor(0, 0, 0, .5)
end
-- center the experience bar when Floating Actionbar is used on its own.
-- Reduced Actionbar applies its own matching offset, so avoid shifting it twice.
local function UpdateExperiencePosition()
local reducedEnabled = ShaguTweaks_config
and ShaguTweaks_config[T["Reduced Actionbar Size"]] == 1
if reducedEnabled then return end
MainMenuExpBar:ClearAllPoints()
if ReputationWatchBar:IsShown() then
MainMenuExpBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 0)
else
MainMenuExpBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 3)
end
end
-- keep the custom reputation position without replacing Blizzard's updater
local function UpdateReputationPosition()
UpdateExperiencePosition()
ReputationWatchBar:ClearAllPoints()
if MainMenuExpBar:IsShown() then
ReputationWatchBar:SetPoint("BOTTOM", MainMenuBar, "TOP", 0, -7)
else
ReputationWatchBar:SetPoint("TOP", MainMenuBar, "TOP", 0, 2)
end
end
hooksecurefunc("ReputationWatchBar_Update", UpdateReputationPosition)
-- Blizzard can restore the default XP bar anchor after login/reloads and
-- actionbar visibility changes, so re-apply the fixed floating offset.
hooksecurefunc("UIParent_ManageFramePositions", UpdateExperiencePosition)
UpdateReputationPosition()
-- hide max level top frame
MainMenuBarMaxLevelBar:SetAlpha(0)
-- remove textures
for _, texture in ipairs(texture_removals) do
if texture then
texture:SetTexture()
texture:Hide()
end
end
end
+62
View File
@@ -0,0 +1,62 @@
-- Extend the native extra-bar preference to main/bonus action buttons.
-- Own exactly one grid reference; drag-and-drop owns its separate reference.
local _G = ShaguTweaks.GetGlobalEnv()
local function BonusVisible()
return _G.BonusActionBarFrame and BonusActionBarFrame:IsVisible()
end
local function HideCoveredButton(button)
if ShaguTweaks.API.IsSupportedRuntime() and BonusVisible() then button:Hide() end
end
local function RefreshBaseButtons()
if not ShaguTweaks.API.IsSupportedRuntime() then return end
local previous = this
for i = 1, 12 do
local button = _G["ActionButton" .. i]
if button then
if BonusVisible() then button:Hide()
else
-- Let the native updater restore filled/empty slot visibility using
-- the existing grid references. Never change the contents of a slot.
this = button
ActionButton_Update()
end
end
end
this = previous
end
local function UpdateGrid()
if not ShaguTweaks.API.IsSupportedRuntime() then return end
local show = _G.ALWAYS_SHOW_MULTIBARS == 1 or _G.ALWAYS_SHOW_MULTIBARS == "1"
for _, prefix in ipairs({"ActionButton", "BonusActionButton"}) do
for i = 1, 12 do
local button = _G[prefix .. i]
if button then
if show and not button.stAlwaysShowGrid then
button.stAlwaysShowGrid = true
ActionButton_ShowGrid(button)
elseif not show and button.stAlwaysShowGrid then
button.stAlwaysShowGrid = nil
ActionButton_HideGrid(button)
end
end
end
end
RefreshBaseButtons()
end
ShaguTweaks.hooksecurefunc("MultiActionBar_UpdateGridVisibility", UpdateGrid)
-- Floating artwork no longer masks the caster bar below the form bar.
-- Native button refreshes and dragging can call Show again: keep covered
-- buttons hidden until the actual bonus frame closes (including animations).
for i = 1, 12 do
local button = _G["ActionButton" .. i]
if button then ShaguTweaks.hooksecurefunc(button, "Show", HideCoveredButton) end
end
if _G.BonusActionBarFrame then
ShaguTweaks.HookScript(BonusActionBarFrame, "OnShow", RefreshBaseButtons)
ShaguTweaks.HookScript(BonusActionBarFrame, "OnHide", RefreshBaseButtons)
end
local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:SetScript("OnEvent", function()
if ShaguTweaks.API.IsSupportedRuntime() then UpdateGrid() end
end)
+34
View File
@@ -0,0 +1,34 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Hide Macro Text"],
description = T["Hides macro names on action buttons."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
local function HideMacroText(button)
if not button or not button.GetName then return end
local name = button:GetName()
local text = name and _G[name .. "Name"]
if text then text:SetAlpha(0) end
end
for i = 1, 24 do
HideMacroText(_G["BonusActionButton" .. i])
end
for i = 1, 12 do
HideMacroText(_G["ActionButton" .. i])
HideMacroText(_G["MultiBarRightButton" .. i])
HideMacroText(_G["MultiBarLeftButton" .. i])
HideMacroText(_G["MultiBarBottomLeftButton" .. i])
HideMacroText(_G["MultiBarBottomRightButton" .. i])
end
end
+15
View File
@@ -0,0 +1,15 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Mouseover Right"],
description = T["Hide the right actionbar and show it on mouseover."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
ShaguTweaks.CreateMouseoverActionBar(MultiBarRight, "SHOW_MULTI_ACTIONBAR_3")
end
+15
View File
@@ -0,0 +1,15 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Mouseover Right 2"],
description = T["Hide the second right actionbar and show it on mouseover."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
ShaguTweaks.CreateMouseoverActionBar(MultiBarLeft, "SHOW_MULTI_ACTIONBAR_4")
end
+138
View File
@@ -0,0 +1,138 @@
-- Shared mouseover actionbar helper.
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
if not ShaguTweaks.CreateMouseoverActionBar then
ShaguTweaks.CreateMouseoverActionBar = function(bar, visibilityFlag)
if not bar or not visibilityFlag then return end
if bar.ShaguTweaksMouseoverController then return end
local controller = CreateFrame("Frame", nil, UIParent)
local hotspot = CreateFrame("Frame", nil, UIParent)
controller.bar = bar
controller.hotspot = hotspot
controller.visibilityFlag = visibilityFlag
controller.enabled = false
controller.elapsed = 0
controller.idle = 0
bar.ShaguTweaksMouseoverController = controller
-- One invisible reveal area replaces the original per-button overlay
-- frames. It only accepts mouse input while the actionbar is hidden.
hotspot:SetAllPoints(bar)
hotspot:SetFrameStrata("DIALOG")
hotspot:EnableMouse(false)
hotspot:Hide()
local function ReadEnabled()
return _G[visibilityFlag] and true or false
end
local function StopWatching()
controller:SetScript("OnUpdate", nil)
controller.elapsed = 0
controller.idle = 0
end
local function StartWatching()
if not controller.enabled or not bar:IsShown() then return end
hotspot:EnableMouse(false)
controller.elapsed = 0
controller.idle = 0
controller:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + (arg1 or 0)
if this.elapsed < .10 then return end
local step = this.elapsed
this.elapsed = 0
if not this.enabled then
StopWatching()
hotspot:EnableMouse(false)
hotspot:Hide()
return
end
if not bar:IsShown() then
StopWatching()
hotspot:Show()
hotspot:EnableMouse(true)
return
end
if MouseIsOver(bar) then
this.idle = 0
return
end
this.idle = this.idle + step
if this.idle >= 2 then
StopWatching()
bar:Hide()
end
end)
end
local function Sync()
controller.enabled = ReadEnabled()
if not controller.enabled then
StopWatching()
hotspot:EnableMouse(false)
hotspot:Hide()
return
end
hotspot:Show()
if bar:IsShown() then
StartWatching()
else
hotspot:EnableMouse(true)
end
end
hotspot:SetScript("OnEnter", function()
if not controller.enabled then return end
hotspot:EnableMouse(false)
bar:Show()
-- The bar's OnShow script starts the watcher. Avoid a second redundant
-- StartWatching() call here.
end)
-- Keep existing scripts intact while observing native/UI-addon visibility
-- changes. These wrappers are installed once per bar.
local oldOnShow = bar:GetScript("OnShow")
bar:SetScript("OnShow", function()
if oldOnShow then oldOnShow() end
if controller.enabled then StartWatching() end
end)
local oldOnHide = bar:GetScript("OnHide")
bar:SetScript("OnHide", function()
if oldOnHide then oldOnHide() end
StopWatching()
if controller.enabled then
hotspot:Show()
hotspot:EnableMouse(true)
else
hotspot:EnableMouse(false)
hotspot:Hide()
end
end)
controller:RegisterEvent("PLAYER_ENTERING_WORLD")
controller:RegisterEvent("CVAR_UPDATE")
controller:SetScript("OnEvent", Sync)
Sync()
end
end
+402
View File
@@ -0,0 +1,402 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local S = { states = {} }
local Unlocked = ShaguTweaks.API.AreMoveModifiersDown
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
API.AfterWorldReady(Setup)
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
+104
View File
@@ -0,0 +1,104 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Proc & Reactive Spell Highlights"],
description = T["Highlights action buttons for active procs and reactive spells using ClassicAPI. Supports spells and macros; central proc artwork is left to other addons."],
expansions = { ["vanilla"] = true }, category = T["Action Bar"], enabled = nil,
})
module.enable = function(self)
if self.states then return end
local query = _G.C_SpellActivationOverlay and _G.C_SpellActivationOverlay.IsSpellOverlayed or _G.IsSpellOverlayed
local valid = _G.C_EventUtils and _G.C_EventUtils.IsEventValid
if type(query) ~= "function" or not API.GetActionInfo or not valid
or not valid("SPELL_ACTIVATION_OVERLAY_GLOW_SHOW")
or not valid("SPELL_ACTIVATION_OVERLAY_GLOW_HIDE") then return end
local states, byButton = {}, {}
self.states = states
local function Paint(state, active)
if active then
if not state.glow:IsShown() then
state.glow:Show()
if state.pulse then state.pulse:Play() end
end
else
if state.pulse then state.pulse:Stop() end
state.glow:Hide()
end
end
local function Sync(state)
local spell
if state.button:IsVisible() then
local slot = ActionButton_GetPagedID(state.button)
if slot then
local kind, id = API.GetActionInfo(slot)
if kind == "spell" then spell = id
elseif kind == "macro" and _G.GetMacroSpell then
local name, rank, spellID = _G.GetMacroSpell(id)
spell = spellID
end
end
end
state.spell = spell
Paint(state, spell and query(spell))
end
for _, prefix in ipairs({"ActionButton", "BonusActionButton", "MultiBarBottomLeftButton",
"MultiBarBottomRightButton", "MultiBarRightButton", "MultiBarLeftButton"}) do
for i = 1, 12 do
local button = _G[prefix .. i]
if button then
local glow = CreateFrame("Frame", nil, button)
glow:SetAllPoints(button)
glow:SetFrameLevel(button:GetFrameLevel() + 5)
glow:SetBackdrop({ edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 12 })
glow:SetBackdropBorderColor(.7, .9, 1, 1)
-- Additive light stands out against both dark and gold button rims.
-- Use the client's existing action-button glow artwork.
local halo = glow:CreateTexture(nil, "OVERLAY")
halo:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
halo:SetBlendMode("ADD")
halo:SetVertexColor(.55, .85, 1, 1)
halo:SetPoint("TOPLEFT", button, "TOPLEFT", -8, 8)
halo:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 8, -8)
glow:EnableMouse(false)
glow:Hide()
local state = { button = button, glow = glow, halo = halo }
if type(glow.CreateAnimationGroup) == "function" then
local pulse = glow:CreateAnimationGroup()
local fade = pulse:CreateAnimation("Alpha")
if type(fade.SetFromAlpha) == "function" and type(fade.SetToAlpha) == "function" then
pulse:SetLooping("BOUNCE")
fade:SetFromAlpha(1); fade:SetToAlpha(.7)
fade:SetDuration(.4); fade:SetSmoothing("IN_OUT")
state.pulse = pulse
end
end
table.insert(states, state); byButton[button] = state
local function Changed() Sync(state) end
ShaguTweaks.HookScript(button, "OnShow", Changed)
ShaguTweaks.HookScript(button, "OnHide", Changed)
Sync(state)
end
end
end
ShaguTweaks.hooksecurefunc("ActionButton_Update", function()
local state = byButton[this]
if state then Sync(state) end
end)
local events = CreateFrame("Frame")
self.events = events
for _, name in ipairs({"SPELL_ACTIVATION_OVERLAY_GLOW_SHOW", "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE",
"PLAYER_ENTERING_WORLD", "ACTIONBAR_SLOT_CHANGED", "ACTIONBAR_PAGE_CHANGED",
"UPDATE_BONUS_ACTIONBAR", "UPDATE_MACROS", "ACTIONBAR_UPDATE_STATE",
"PLAYER_TARGET_CHANGED", "MODIFIER_STATE_CHANGED", "SPELLS_CHANGED"}) do
if valid(name) then events:RegisterEvent(name) end
end
events:SetScript("OnEvent", function()
local proc = event == "SPELL_ACTIVATION_OVERLAY_GLOW_SHOW" or event == "SPELL_ACTIVATION_OVERLAY_GLOW_HIDE"
for _, state in ipairs(states) do
if proc then
if state.spell == arg1 then Paint(state, state.button:IsVisible() and query(arg1)) end
else Sync(state) end
end
end)
end
+86
View File
@@ -52,7 +52,93 @@ end
local RANGE_INTERVAL = TOOLTIP_UPDATE_TIME or 0.2 local RANGE_INTERVAL = TOOLTIP_UPDATE_TIME or 0.2
-- Newer ClassicAPI owns the range sampling; Lua reacts only to slot changes.
local function EnableNativeRange()
local actionbar = _G.C_ActionBar
if not actionbar or type(actionbar.EnableActionRangeCheck) ~= "function"
or not API.eventutils or not _G.C_EventUtils.IsEventValid("ACTION_RANGE_CHECK_UPDATE") then return false end
local states, byButton, ranges = {}, {}, {}
local function Paint(state)
local button = state.button
local slot = state.slot
if not slot then return end
local icon = state.icon
if not icon then return end
local result = ranges[slot]
-- Keep invalid-faction spells neutral, as the old module did.
local kind, id = API.GetActionInfo(slot)
local spells = _G.C_Spell
if result == 0 and kind == "spell" and spells then
if (spells.IsSpellHelpful and spells.IsSpellHelpful(id) and not UnitCanAssist("player", "target"))
or (spells.IsSpellHarmful and spells.IsSpellHarmful(id) and not UnitCanAttack("player", "target")) then result = nil end
end
if result == 0 then
icon:SetVertexColor(button.r or .85, button.g or .18, button.b or .18, button.a or 1)
else
local usable, noMana = IsUsableAction(slot)
if usable and usable ~= 0 then icon:SetVertexColor(1, 1, 1, 1)
elseif noMana and noMana ~= 0 then icon:SetVertexColor(.5, .5, 1, 1)
else icon:SetVertexColor(.4, .4, .4, 1) end
end
end
local function Sync(state)
local slot = state.button:IsVisible() and ActionButton_GetPagedID(state.button) or nil
if slot and (slot < 1 or slot > 120) then slot = nil end
if slot ~= state.slot then
local old = state.slot
state.slot = slot
if old then actionbar.EnableActionRangeCheck(old, false) end
if slot then
ranges[slot] = nil
actionbar.EnableActionRangeCheck(slot, true)
end
end
Paint(state)
end
local function ButtonChanged()
local state = byButton[this]
if state then Sync(state) end
end
local function AllChanged()
for _, state in ipairs(states) do Sync(state) end
end
local listener = CreateFrame("Frame")
listener:RegisterEvent("ACTION_RANGE_CHECK_UPDATE")
listener:RegisterEvent("PLAYER_ENTERING_WORLD")
listener:RegisterEvent("PLAYER_TARGET_CHANGED")
listener:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
listener:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
listener:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
listener:SetScript("OnEvent", function()
if event == "ACTION_RANGE_CHECK_UPDATE" then
ranges[arg1] = arg3 and NormalizeRange(arg2) or nil
for _, state in ipairs(states) do if state.slot == arg1 then Paint(state) end end
else
-- Unchanged native results do not fire again: retain their cached state.
AllChanged()
end
end)
for _, prefix in ipairs({"ActionButton", "BonusActionButton", "MultiBarBottomLeftButton",
"MultiBarBottomRightButton", "MultiBarLeftButton", "MultiBarRightButton"}) do
for i = 1, 12 do
local button = _G[prefix .. i]
if button then
local state = {button = button, icon = _G[prefix .. i .. "Icon"]}
byButton[button] = state
table.insert(states, state)
ShaguTweaks.HookScript(button, "OnShow", ButtonChanged)
ShaguTweaks.HookScript(button, "OnHide", ButtonChanged)
end
end
end
hooksecurefunc("ActionButton_Update", ButtonChanged)
hooksecurefunc("ActionButton_UpdateUsable", ButtonChanged)
AllChanged()
return true
end
module.enable = function(self) module.enable = function(self)
if EnableNativeRange() then return end
hooksecurefunc("ActionButton_OnUpdate", function(elapsed) hooksecurefunc("ActionButton_OnUpdate", function(elapsed)
-- Blizzard sets rangeTimer only for actions that have a range. Use our -- Blizzard sets rangeTimer only for actions that have a range. Use our
-- own countdown instead of sharing it: decrementing rangeTimer here and -- own countdown instead of sharing it: decrementing rangeTimer here and
+167
View File
@@ -0,0 +1,167 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Reagent Counter"],
description = T["Shows a reagent counter on action buttons."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
if self.counter then return end
local reagent_slots = { }
local reagent_counts = { }
local macro_slots = { }
local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight" }
local reagentcounter = CreateFrame("Frame", "ShaguTweaksReagentCount", UIParent)
self.counter = reagentcounter
reagentcounter:RegisterEvent("PLAYER_ENTERING_WORLD")
reagentcounter:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
reagentcounter:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
reagentcounter:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
reagentcounter:RegisterEvent("UPDATE_MACROS")
reagentcounter:RegisterEvent("SPELLS_CHANGED")
reagentcounter:RegisterEvent("BAG_UPDATE")
local macroEvents = { "MODIFIER_STATE_CHANGED", "PLAYER_TARGET_CHANGED",
"UPDATE_MOUSEOVER_UNIT", "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED",
"UPDATE_SHAPESHIFT_FORM", "UPDATE_SHAPESHIFT_FORMS", "SPELL_UPDATE_COOLDOWN",
"UNIT_AURA", "UNIT_DISPLAYPOWER" }
local macroEventSet = {}
local watchingMacros = false
local function WatchMacros()
local needed = next(macro_slots) ~= nil
if needed == watchingMacros then return end
watchingMacros = needed
for _, name in ipairs(macroEvents) do
if needed and API.eventutils and _G.C_EventUtils.IsEventValid(name) then
macroEventSet[name] = true
if name == "UNIT_AURA" or name == "UNIT_DISPLAYPOWER" then
API.RegisterUnitEvent(reagentcounter, name, "player")
else reagentcounter:RegisterEvent(name) end
elseif not needed then
reagentcounter:UnregisterEvent(name)
macroEventSet[name] = nil
end
end
end
-- Hidden frames still receive events, but do not execute OnUpdate.
-- Wake for one frame only when work is pending.
reagentcounter:Hide()
reagentcounter:SetScript("OnEvent", function()
if (event == "UNIT_AURA" or event == "UNIT_DISPLAYPOWER") and arg1 ~= "player" then return end
this.pending = true
if macroEventSet[event] then
this.macros = true
elseif event ~= "BAG_UPDATE" then
this.rescan = true
end
this:Show()
end)
reagentcounter:SetScript("OnUpdate", function()
if not this.pending then
this:Hide()
return
end
-- Rebuild the action-slot -> reagent mapping only when the action layout
-- changes. ClassicAPI resolves the spell/macro directly, so no tooltip
-- scan or localized reagent-name parsing is required.
if this.rescan then
for slot = 1, 120 do
reagentcounter.ScanSlot(slot)
end
WatchMacros()
elseif this.macros then
-- A native macro can resolve to another spell without being edited.
-- Re-evaluate only known macro actions, not all 120 action slots.
for slot in pairs(macro_slots) do reagentcounter.ScanSlot(slot) end
WatchMacros()
end
-- Count only reagents referenced by active actions. ClassicAPI reads the
-- inventory directly, avoiding a Lua bag scan for every reagent.
for itemID in pairs(reagent_counts) do reagent_counts[itemID] = nil end
for _, itemID in pairs(reagent_slots) do
if reagent_counts[itemID] == nil then
reagent_counts[itemID] = API.GetItemCount(itemID, false, false) or 0
end
end
-- update all actionbar buttons
for _, prefix in ipairs(bars) do
for i = 1, NUM_ACTIONBAR_BUTTONS do
local button = _G[prefix .. "Button" .. i]
if button then
local text = _G[button:GetName().."Count"]
local slot = ActionButton_GetPagedID(button)
if text then
local itemID = reagent_slots[slot]
if itemID then
text:SetText(reagent_counts[itemID] or 0)
elseif not IsConsumableAction(slot) then
text:SetText()
end
end
end
end
end
this.pending = nil
this.rescan = nil
this.macros = nil
this:Hide()
end)
reagentcounter.ScanSlot = function(slot)
reagent_slots[slot] = nil
macro_slots[slot] = nil
if not HasAction(slot) then return end
local actionType, actionID = API.GetActionInfo(slot)
local spellID
if actionType == "spell" then
spellID = actionID
elseif actionType == "macro" and actionID then
macro_slots[slot] = actionID
local _, _, macroSpellID = API.GetMacroSpell(actionID)
spellID = macroSpellID
end
if not spellID then return end
local reagents = API.GetSpellReagents(spellID)
local reagent = reagents and reagents[1]
if not reagent or not reagent.itemID then return end
-- The action button only has one count field. Preserve the historical
-- behavior by showing the first reagent's owned stack count.
reagent_slots[slot] = reagent.itemID
end
-- The stock UI clears the Count fontstring on non-consumable actions whenever
-- ActionButton_UpdateCount() runs. Re-apply our reagent value afterwards so
-- login/reload refreshes cannot erase it a moment later.
ShaguTweaks.hooksecurefunc("ActionButton_UpdateCount", function()
local button = this
if not button or not button.GetName then return end
local slot = ActionButton_GetPagedID(button)
local itemID = slot and reagent_slots[slot]
if not itemID then return end
local text = _G[button:GetName().."Count"]
if text then
text:SetText(reagent_counts[itemID] or 0)
end
end)
end
+17
View File
@@ -0,0 +1,17 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local rgbhex = ShaguTweaks.rgbhex
local module = ShaguTweaks:register({
title = T["Center Vertical Actionbar"],
description = T["Center the vertical actionbar on the right side."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Action Bar"],
enabled = nil,
})
module.enable = function(self)
MultiBarRight:ClearAllPoints()
MultiBarRight:SetPoint("RIGHT", 0, -16)
end
-121
View File
@@ -1,121 +0,0 @@
-- Adapted from AoELoot by Sandrea / ChatGPT for ShaguTweaks-ClassicAPI.
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["AoE Loot"],
description = T["Automatically loots all nearby corpses using ClassicAPI without confirmation prompts. Leaves Master Loot untouched."],
expansions = { ["vanilla"] = true },
category = T["Loot"],
enabled = nil,
})
module.enable = function(self)
-- AoE Loot relies entirely on ClassicAPI's native corpse walker,
-- container classification and timer. Do not keep partial legacy behavior
-- when those capabilities are unavailable.
if not API or not API.aoeloot or not API.containeropenable or not API.timer then return end
local frame = CreateFrame("Frame", "ShaguTweaksAoELoot")
local pending = false
local containerLootDeadline = 0
local takeoverGeneration = 0
local TAKEOVER_DELAY = 0.3
local function CancelTakeover()
takeoverGeneration = takeoverGeneration + 1
pending = false
end
local function TrackContainerUse(bag, slot)
local isOpenable, canOpen = API.IsContainerItemOpenable(bag, slot)
if isOpenable and canOpen then
-- LOOT_OPENED is also fired by clams, lockboxes and similar bag items.
-- Mark that session before UseContainerItem runs so it remains under the
-- normal client handling and the consumed container can leave the bag.
containerLootDeadline = GetTime() + 3
end
end
-- Use the ShaguTweaks hook explicitly because its prepend mode records the
-- bag item before UseContainerItem can synchronously fire LOOT_OPENED.
ShaguTweaks.hooksecurefunc("UseContainerItem", TrackContainerUse, true)
local function IsMasterLootActive()
if not GetLootMethod then return false end
return GetLootMethod() == "master"
end
local function CanStartAoELoot()
if IsMasterLootActive() then return false end
if API.IsLootScanInProgress() then return false end
return true
end
local function HasNearbyLootableCorpse()
local units = API.GetNearbyLootableUnits()
return type(units) == "table" and table.getn(units) > 0
end
local function StartAoELoot(generation)
if generation ~= takeoverGeneration or not pending then return end
pending = false
if CanStartAoELoot() then
API.LootAllCorpses()
end
end
local function TakeOverLootSession()
-- AoE Loot owns corpse sessions while enabled. Close the normal client
-- window immediately so Vanilla autoloot, SuperAPI SetAutoloot modes and
-- quickloot-patched clients cannot keep control of the session.
takeoverGeneration = takeoverGeneration + 1
local generation = takeoverGeneration
pending = true
CloseLoot()
-- Native autoloot may already have emitted loot packets before LOOT_OPENED
-- reaches Lua. ClassicAPI's own loot test uses the same 0.3s settling
-- window between loot operations; after it expires the native corpse walker
-- becomes the sole owner of all remaining nearby corpse loot.
C_Timer.After(TAKEOVER_DELAY, function()
StartAoELoot(generation)
end)
end
frame:RegisterEvent("LOOT_OPENED")
frame:SetScript("OnEvent", function()
if event ~= "LOOT_OPENED" then return end
-- Inventory containers use the same loot event as corpses. Let the normal
-- client finish them instead of treating them as an AoE-loot trigger.
if containerLootDeadline > 0 then
local isContainerLoot = GetTime() <= containerLootDeadline
containerLootDeadline = 0
if isContainerLoot then
CancelTakeover()
return
end
end
-- The master looter must keep the normal window to inspect and assign loot.
if IsMasterLootActive() then
CancelTakeover()
return
end
-- Chests, fishing nodes and other non-corpse sources also emit LOOT_OPENED.
-- Only take over when ClassicAPI can see at least one nearby lootable unit.
if not HasNearbyLootableCorpse() then return end
-- A queued takeover already owns this interaction. ClassicAPI suppresses
-- its own LOOT_OPENED/LOOT_CLOSED events while walking corpses, so no
-- additional session needs to be stacked here.
if pending then return end
TakeOverLootSession()
end)
end
-121
View File
@@ -1,121 +0,0 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Auto Dismount"],
description = T["Automatically dismounts whenever a spell is casted."],
expansions = { ["vanilla"] = true },
category = T["Action Bar"],
enabled = true,
})
module.enable = function(self)
local dismount = CreateFrame("Frame")
ShaguTweaks.dismount = dismount
-- Keep the legacy detection data as a compatibility fallback for custom
-- mounts/forms that ClassicAPI does not identify as one of the known forms.
dismount.strings = {
-- deDE
"^Erhöht Tempo um (.+)%%",
-- enUS
"^Increases speed by (.+)%%",
-- esES
"^Aumenta la velocidad en un (.+)%%",
-- frFR
"^Augmente la vitesse de (.+)%%",
-- ruRU
"^Скорость увеличена на (.+)%%",
-- koKR
"^이동 속도 (.+)%%만큼 증가",
-- zhCN
"^速度提高(.+)%%",
-- turtle-wow
"speed based on", "Slow and steady...", "Riding",
"Lento y constante...", "Aumenta la velocidad según tu habilidad de Montar.",
"根据您的骑行技能提高速度。", "根据骑术技能提高速度。", "又慢又稳......",
}
-- shapeshift buff icons (legacy/custom fallback)
dismount.shapeshifts = {
"ability_racial_bearform", "ability_druid_catform", "ability_druid_travelform",
"spell_nature_forceofnature", "ability_druid_aquaticform", "spell_nature_spiritwolf"
}
-- ClassicAPI SpellShapeshiftForm.dbc IDs matching the forms the original
-- module intentionally cancelled. Do not include Moonkin/Shadowform/Stealth.
local removableForms = {
[1] = true, -- Cat
[2] = true, -- Tree (vanilla DBC)
[3] = true, -- Travel
[4] = true, -- Aquatic
[5] = true, -- Bear
[8] = true, -- Dire Bear
[9] = true, -- Tree of Life (Turtle WoW)
[11] = true, -- Swift Travel (Turtle WoW)
[16] = true, -- Ghost Wolf
}
-- errors that indicate mount/shapeshift
dismount.errors = { SPELL_FAILED_NOT_MOUNTED, ERR_ATTACK_MOUNTED, ERR_TAXIPLAYERALREADYMOUNTED,
SPELL_FAILED_NOT_SHAPESHIFT, SPELL_FAILED_NO_ITEMS_WHILE_SHAPESHIFTED, SPELL_NOT_SHAPESHIFTED,
SPELL_NOT_SHAPESHIFTED_NOSPACE, ERR_CANT_INTERACT_SHAPESHIFTED, ERR_NOT_WHILE_SHAPESHIFTED,
ERR_NO_ITEMS_WHILE_SHAPESHIFTED, ERR_TAXIPLAYERSHAPESHIFTED,ERR_MOUNT_SHAPESHIFTED }
dismount.scanner = ShaguTweaks.libtipscan:GetScanner("dismount")
local function LegacyCancel()
for i=0, 31 do
-- detect mounts based on tooltip text
dismount.scanner:SetPlayerBuff(i)
for _, str in pairs(dismount.strings) do
if dismount.scanner:Find(str) then
CancelPlayerBuff(i)
return true
end
end
-- detect shapeshift based on texture
local buff = GetPlayerBuffTexture(i)
if buff then
for _, bufftype in pairs(dismount.shapeshifts) do
if string.find(string.lower(buff), bufftype) then
CancelPlayerBuff(i)
return true
end
end
end
end
end
dismount:RegisterEvent("UI_ERROR_MESSAGE")
dismount:SetScript("OnEvent", function()
-- stand up
if arg1 == SPELL_FAILED_NOT_STANDING then
SitOrStand()
return
end
for _, errorstring in pairs(dismount.errors) do
if arg1 == errorstring then
-- ClassicAPI reads the real mounted/form state directly, avoiding a
-- 32-buff tooltip scan in the normal case.
if API.IsMounted() then
API.Dismount()
return
end
local formID = API.GetShapeshiftFormID()
if removableForms[formID] then
API.CancelShapeshiftForm()
return
end
-- Preserve compatibility with custom private-server auras/forms.
LegacyCancel()
return
end
end
end)
end
+1 -1
View File
@@ -3,7 +3,7 @@ local cmatch = ShaguTweaks.cmatch
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Auto Stance"], title = T["Auto Stance"],
description = T["Automatically switch to the required warrior or druid stance on spell cast."], description = T["Automatically switch to the warrior stance or druid form required by a spell. To leave a form for spells such as Moonfire, enable ClassicAPI Auto Cancel Form."],
expansions = { ["vanilla"] = true }, expansions = { ["vanilla"] = true },
category = T["Action Bar"], category = T["Action Bar"],
enabled = true, enabled = true,
+669
View File
@@ -0,0 +1,669 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local HookScript = ShaguTweaks.HookScript
local floor = math.floor
local ceil = math.ceil
local min = math.min
local mod = math.mod or mod
STBuyEmAll = STBuyEmAll or {}
local Buy = STBuyEmAll
local strings = {
enUS = {
["Max"] = "Max",
["Stack"] = "Stack",
["Are you sure you want to buy\n %d × %s?"] = "Are you sure you want to buy\n %d × %s?",
["Stack purchase"] = "Stack purchase",
["Stack size"] = "Stack size",
["Partial stack"] = "Partial stack",
["Maximum purchase"] = "Maximum purchase",
["You can fit"] = "You can fit",
["You can afford"] = "You can afford",
["Vendor has"] = "Vendor has",
},
deDE = {
["Are you sure you want to buy\n %d × %s?"] = "Bist du sicher das du\n %d × %s kaufen willst?",
["Stack size"] = "Stack größe",
["Maximum purchase"] = "Größt möglicher Einkauf",
["You can fit"] = "Du hast Platz für",
["You can afford"] = "Du kannst dir leisten",
["Vendor has"] = "Der Verkäufer hat",
},
frFR = {
["Stack"] = "Pile",
["Are you sure you want to buy\n %d × %s?"] = "Voulez-vous vraiment acheter\n %d × %s?",
["Stack size"] = "Taille de pile",
["Maximum purchase"] = "Achat maximum",
["You can fit"] = "Vous pouvez transporter",
["You can afford"] = "Vous pouvez payer",
["Vendor has"] = "Le marchand a",
},
}
local locale = strings[GetLocale()] or strings.enUS
local function L(key)
return locale[key] or strings.enUS[key] or key
end
BUYEMALL_MAX = L("Max")
BUYEMALL_STACK = L("Stack")
local module = ShaguTweaks:register({
title = T["Buy Em All"],
description = T["Shift-click vendor items to choose stack, maximum, or a custom purchase amount."],
expansions = { ["vanilla"] = true },
category = T["Merchant"],
enabled = true,
})
local function FamilyMatches(itemFamily, bagFamily)
bagFamily = tonumber(bagFamily) or 0
itemFamily = tonumber(itemFamily) or 0
if bagFamily == 0 then return true end
if itemFamily == 0 then return false end
-- Bag families are power-of-two bit flags. Avoid requiring an external bit
-- library on 1.12 while still supporting Turtle custom specialty families.
return mod(floor(itemFamily / bagFamily), 2) == 1
end
function Buy:CountPurchaseItem()
if not self.purchaseItemID then return nil end
if API and API.GetItemCount then
return API.GetItemCount(self.purchaseItemID, false, false)
end
return nil
end
function Buy:StopPurchaseQueue()
if self.purchaseFrame then self.purchaseFrame:Hide() end
self.purchaseItemIndex = nil
self.purchaseItemID = nil
self.purchasePreset = 1
self.purchaseLoops = 0
self.purchaseAmount = 0
self.purchaseLeftover = 0
self.purchaseElapsed = 0
self.purchaseWaiting = nil
self.purchaseExpectedCount = nil
self.purchasePendingKind = nil
end
function Buy:AbortPurchaseQueue()
self:StopPurchaseQueue()
DEFAULT_CHAT_FRAME:AddMessage("Buy Em All: purchase queue stopped because the previous stack was not confirmed. No further stacks were sent.")
end
function Buy:ConfirmPendingPurchase()
if not self.purchaseWaiting or not self.purchaseExpectedCount then return false end
local count = self:CountPurchaseItem()
if count == nil or count < self.purchaseExpectedCount then return false end
if self.purchasePendingKind == "loop" then
self.purchaseLoops = self.purchaseLoops - 1
elseif self.purchasePendingKind == "leftover" then
self.purchaseLeftover = 0
end
self.purchaseWaiting = nil
self.purchaseExpectedCount = nil
self.purchasePendingKind = nil
self.purchaseElapsed = 0
return true
end
function Buy:Purchase_OnEvent()
self:ConfirmPendingPurchase()
end
function Buy:SendPurchase(quantity, kind)
local before = self:CountPurchaseItem()
if before == nil then
self:AbortPurchaseQueue()
return
end
self.purchaseWaiting = true
self.purchasePendingKind = kind
self.purchaseExpectedCount = before + quantity * (self.purchasePreset or 1)
self.purchaseElapsed = 0
BuyMerchantItem(self.purchaseItemIndex, quantity)
end
function Buy:Purchase_OnUpdate(elapsed)
self.purchaseElapsed = (self.purchaseElapsed or 0) + (elapsed or 0)
if self.purchaseWaiting then
if self.purchaseElapsed >= self.purchaseTimeout then
if not self:ConfirmPendingPurchase() then self:AbortPurchaseQueue() end
end
return
end
if self.purchaseElapsed < self.purchaseDelay then return end
self.purchaseElapsed = 0
if self.purchaseLoops and self.purchaseLoops > 0 then
self:SendPurchase(self.purchaseAmount, "loop")
return
end
if self.purchaseLeftover and self.purchaseLeftover > 0 then
self:SendPurchase(self.purchaseLeftover, "leftover")
return
end
self:StopPurchaseQueue()
end
function Buy:DoPurchase(amount)
if not amount or amount <= 0 or not self.itemIndex or not self.preset or not self.stack then return end
if STBuyEmAllFrame then STBuyEmAllFrame:Hide() end
local numLoops, purchaseAmount, leftover
if self.preset > 1 then
numLoops = floor(amount / self.preset)
purchaseAmount = 1
leftover = 0
else
numLoops = floor(amount / self.stack)
purchaseAmount = self.stack
leftover = mod(amount, self.stack)
end
self:StopPurchaseQueue()
self.purchaseItemIndex = self.itemIndex
self.purchaseItemID = self.itemID
self.purchasePreset = self.preset
self.purchaseLoops = numLoops
self.purchaseAmount = purchaseAmount
self.purchaseLeftover = leftover
self.purchaseElapsed = self.purchaseDelay
if self.purchaseLoops > 0 or self.purchaseLeftover > 0 then
self.purchaseFrame:Show()
end
end
function Buy:DoConfirmation(amount)
self.confirmAmount = amount
local dialog = StaticPopup_Show("STBUYEMALL_CONFIRM", amount, self.itemName)
if dialog then dialog.data = amount end
end
function Buy:VerifyPurchase(amount)
amount = amount or self.split
if not amount or amount <= 0 or not self.preset or self.preset <= 0 then return end
amount = ceil(amount / self.preset) * self.preset
if amount > self.max then amount = self.max end
if amount > self.stack and amount > self.defaultStack then
self:DoConfirmation(amount)
else
self:DoPurchase(amount)
end
end
function Buy:SetStackClick()
if not self.stack or self.stack <= 0 then
self.stackClick = self.max or self.split or 1
return
end
local targetRemainder = self.partialFit or 0
if targetRemainder == 0 then targetRemainder = self.stack end
local currentRemainder = mod(self.split, self.stack)
local increase = targetRemainder - currentRemainder
if increase <= 0 then increase = increase + self.stack end
self.stackClick = self.split + increase
end
function Buy:UpdateDisplay(amount)
if not self.preset or not self.price or not self.split then return end
local purchase = ceil((amount or self.split) / self.preset)
local cost = purchase * self.price
if STBuyEmAllMoneyFrame then
MoneyFrame_Update("STBuyEmAllMoneyFrame", cost)
end
if not amount and not self.isUpdating then
self.isUpdating = true
STBuyEmAllText:SetText(self.split)
STBuyEmAllLeftButton:Enable()
STBuyEmAllRightButton:Enable()
if self.split >= self.max then
STBuyEmAllRightButton:Disable()
elseif self.split <= self.preset then
STBuyEmAllLeftButton:Disable()
end
self:SetStackClick()
STBuyEmAllStackButton:Enable()
if not self.stackClick or self.max < self.stackClick then
STBuyEmAllStackButton:Disable()
end
self.isUpdating = nil
end
end
function Buy:Show(anchor)
self.typing = 0
STBuyEmAllLeftButton:Disable()
STBuyEmAllRightButton:Enable()
self:SetStackClick()
STBuyEmAllStackButton:Enable()
if not self.stackClick or self.max < self.stackClick then STBuyEmAllStackButton:Disable() end
STBuyEmAllFrame:ClearAllPoints()
STBuyEmAllFrame:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 0, 4)
STBuyEmAllFrame:Show()
self:UpdateDisplay()
end
function Buy:OnClick()
if this == STBuyEmAllOkayButton then
self:VerifyPurchase()
elseif this == STBuyEmAllCancelButton then
STBuyEmAllFrame:Hide()
elseif this == STBuyEmAllStackButton then
if self.stackClick and self.stackClick <= self.max then
self.split = self.stackClick
self:UpdateDisplay()
if this:IsEnabled() == 1 then self:OnEnter() else GameTooltip:Hide() end
end
elseif this == STBuyEmAllMaxButton then
self.split = self.max
self:UpdateDisplay()
end
end
function Buy:OnChar()
local digit = tonumber(arg1)
if digit == nil then return end
if self.typing == 0 then
self.typing = 1
self.split = 0
end
local split = (self.split * 10) + digit
if split <= self.max then
self.split = split
end
if self.split <= 0 then self.split = 1 end
self:UpdateDisplay()
end
function Buy:OnKeyDown()
if arg1 == "BACKSPACE" or arg1 == "DELETE" then
if self.typing == 0 or self.split == 1 then return end
self.split = floor(self.split / 10)
if self.split <= 1 then
self.split = 1
self.typing = 0
end
self:UpdateDisplay()
elseif arg1 == "ENTER" then
self:VerifyPurchase()
elseif arg1 == "ESCAPE" then
STBuyEmAllFrame:Hide()
elseif arg1 == "LEFT" or arg1 == "DOWN" then
self:Left_Click()
elseif arg1 == "RIGHT" or arg1 == "UP" then
self:Right_Click()
end
end
function Buy:Left_Click()
if self.split <= self.preset then return end
local decrease = mod(self.split, self.preset)
decrease = decrease == 0 and self.preset or decrease
self.split = self.split - decrease
self:UpdateDisplay()
end
function Buy:Right_Click()
local increase = self.preset - mod(self.split, self.preset)
if self.split + increase > self.max then return end
self.split = self.split + increase
self:UpdateDisplay()
end
Buy.lines = {
stack = {
label = L("Stack purchase"),
field = "stackClick",
{ label = L("Stack size"), field = "stack" },
{ label = L("Partial stack"), field = "partialFit" },
},
max = {
label = L("Maximum purchase"),
field = "max",
{ label = L("You can fit"), field = "fit" },
{ label = L("You can afford"), field = "afford" },
{
label = L("Vendor has"),
field = "available",
Hide = function()
return not Buy.available or Buy.available <= 1
end,
},
},
}
function Buy:CreateTooltip(lines)
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:SetText(lines.label .. "|cFFFFFFFF - |r" .. GREEN_FONT_COLOR_CODE .. (lines.amount or 0) .. "|r")
for _, line in ipairs(lines) do
if not (line.Hide and line.Hide()) then
local color = line.amount == lines.amount and GREEN_FONT_COLOR or HIGHLIGHT_FONT_COLOR
GameTooltip:AddDoubleLine(line.label, line.amount or 0, 1, 1, 1, color.r, color.g, color.b)
end
end
GameTooltip:Show()
end
function Buy:OnEnter()
local lines = self.lines[this == STBuyEmAllMaxButton and "max" or "stack"]
lines.amount = self[lines.field]
for _, line in ipairs(lines) do line.amount = self[line.field] end
self:CreateTooltip(lines)
self:UpdateDisplay(lines.amount)
end
function Buy:OnLeave()
self:UpdateDisplay()
GameTooltip:Hide()
end
function Buy:OnHide()
StaticPopup_Hide("STBUYEMALL_CONFIRM")
end
function Buy:FreeBagSpace(itemID)
if not itemID then return 0, 1 end
local stackSize = API.GetItemMaxStackSizeByID(itemID) or 1
if stackSize < 1 then stackSize = 1 end
local itemFamily = API.GetItemFamily(itemID) or 0
local capacity = 0
for bag = 0, 4 do
local freeSlots, bagFamily = API.GetContainerNumFreeSlots(bag)
if FamilyMatches(itemFamily, bagFamily) then
capacity = capacity + (freeSlots or 0) * stackSize
end
local numSlots = GetContainerNumSlots(bag)
for slot = 1, numSlots do
local bagItemID = API.GetContainerItemID(bag, slot)
if bagItemID == itemID then
local itemCount = API.GetContainerItemStackCount(bag, slot) or 0
if itemCount < stackSize then
capacity = capacity + stackSize - itemCount
end
end
end
end
return capacity, stackSize
end
function Buy:GetMerchantData(index)
local info = API.GetMerchantItemInfo and API.GetMerchantItemInfo(index)
if info and info.itemID then
local name = API.GetItemNameByID(info.itemID)
if not name then
name = GetMerchantItemInfo(index)
end
return name, info.itemID, info.price or 0, info.stackCount or 1,
info.numAvailable or -1
end
local name, _, price, quantity, numAvailable = GetMerchantItemInfo(index)
local itemID = API.GetMerchantItemID and API.GetMerchantItemID(index)
return name, itemID, price, quantity, numAvailable
end
function Buy:HandleMerchantClick(anchor, button, ignoreModifiers)
if not anchor or not anchor.GetID then return false end
if MerchantFrame.selectedTab ~= 1
or not API.IsShiftKeyDown()
or API.IsControlKeyDown()
or ignoreModifiers
or (ChatFrameEditBox:IsVisible() and button == "LeftButton") then
return false
end
self.itemIndex = anchor:GetID()
local name, itemID, price, quantity, numAvailable = self:GetMerchantData(self.itemIndex)
if not name or not itemID or not price or not quantity or quantity < 1 then return false end
self.preset = quantity
self.price = price
self.itemName = name
self.itemID = itemID
self.available = numAvailable
local bagCapacity, stack = self:FreeBagSpace(itemID)
self.stack = stack
self.fit = floor(bagCapacity / quantity) * quantity
if price > 0 then
self.afford = floor(GetMoney() / price) * quantity
else
self.afford = self.fit
end
self.max = min(self.fit, self.afford)
if numAvailable and numAvailable > -1 then
self.max = min(self.max, numAvailable * quantity)
end
if self.max <= 0 then return true end
if self.max == 1 then
-- Let this invocation's predecessor buy one without opening StackSplit.
return false, true
end
self.defaultStack = quantity
if self.defaultStack > self.max then self.defaultStack = self.max end
self.split = self.defaultStack
self.partialFit = mod(self.fit, stack)
self:SetStackClick()
self:Show(anchor)
return true
end
function Buy:CreateUI()
if STBuyEmAllFrame then return end
local frame = CreateFrame("Frame", "STBuyEmAllFrame", MerchantFrame)
frame:SetWidth(210)
frame:SetHeight(110)
frame:SetFrameStrata("DIALOG")
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
frame:SetBackdropColor(0, 0, 0, .9)
frame:EnableKeyboard(true)
frame:SetScript("OnChar", function() Buy:OnChar() end)
frame:SetScript("OnKeyDown", function() Buy:OnKeyDown() end)
frame:SetScript("OnHide", function() Buy:OnHide() end)
frame:Hide()
local title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall")
title:SetPoint("TOP", frame, "TOP", 0, -10)
title:SetText(T["Buy Em All"])
local amount = frame:CreateFontString("STBuyEmAllText", "ARTWORK", "GameFontHighlight")
amount:SetWidth(50)
amount:SetPoint("TOP", frame, "TOP", 0, -29)
amount:SetText("1")
local left = CreateFrame("Button", "STBuyEmAllLeftButton", frame, "UIPanelButtonTemplate")
left:SetWidth(28)
left:SetHeight(22)
left:SetPoint("RIGHT", amount, "LEFT", -4, 0)
left:SetText("-")
left:SetScript("OnClick", function() Buy:Left_Click() end)
local right = CreateFrame("Button", "STBuyEmAllRightButton", frame, "UIPanelButtonTemplate")
right:SetWidth(28)
right:SetHeight(22)
right:SetPoint("LEFT", amount, "RIGHT", 4, 0)
right:SetText("+")
right:SetScript("OnClick", function() Buy:Right_Click() end)
local stack = CreateFrame("Button", "STBuyEmAllStackButton", frame, "UIPanelButtonTemplate")
stack:SetWidth(58)
stack:SetHeight(22)
stack:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -50)
stack:SetText(BUYEMALL_STACK)
stack:SetScript("OnClick", function() Buy:OnClick() end)
stack:SetScript("OnEnter", function() Buy:OnEnter() end)
stack:SetScript("OnLeave", function() Buy:OnLeave() end)
local maxButton = CreateFrame("Button", "STBuyEmAllMaxButton", frame, "UIPanelButtonTemplate")
maxButton:SetWidth(58)
maxButton:SetHeight(22)
maxButton:SetPoint("LEFT", stack, "RIGHT", 4, 0)
maxButton:SetText(BUYEMALL_MAX)
maxButton:SetScript("OnClick", function() Buy:OnClick() end)
maxButton:SetScript("OnEnter", function() Buy:OnEnter() end)
maxButton:SetScript("OnLeave", function() Buy:OnLeave() end)
local okay = CreateFrame("Button", "STBuyEmAllOkayButton", frame, "UIPanelButtonTemplate")
okay:SetWidth(58)
okay:SetHeight(22)
okay:SetPoint("LEFT", maxButton, "RIGHT", 4, 0)
okay:SetText(OKAY)
okay:SetScript("OnClick", function() Buy:OnClick() end)
local cancel = CreateFrame("Button", "STBuyEmAllCancelButton", frame, "UIPanelButtonTemplate")
cancel:SetWidth(58)
cancel:SetHeight(22)
cancel:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -78)
cancel:SetText(CANCEL)
cancel:SetScript("OnClick", function() Buy:OnClick() end)
local money = CreateFrame("Frame", "STBuyEmAllMoneyFrame", frame, "SmallMoneyFrameTemplate")
money:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 12, 11)
MoneyTypeInfo["STBUYEMALL"] = {
UpdateFunc = function() return 0 end,
showSmallerCoins = "Backpack",
fixedWidth = 1,
collapse = 1,
}
money.info = MoneyTypeInfo["STBUYEMALL"]
money.moneyType = "STBUYEMALL"
money.small = 1
end
module.enable = function(self)
if IsAddOnLoaded("BuyEmAll") then return end
local required = {
"GetItemCount", "GetItemMaxStackSizeByID", "GetItemFamily",
"GetContainerNumFreeSlots", "GetContainerItemID",
"GetContainerItemStackCount", "GetMerchantItemInfo",
"GetMerchantItemID", "IsShiftKeyDown", "IsControlKeyDown",
}
for _, name in ipairs(required) do
if not API or type(API[name]) ~= "function" then
DEFAULT_CHAT_FRAME:AddMessage("|cffff5555ShaguTweaks:|r Buy Em All requires ClassicAPI.")
return
end
end
Buy:CreateUI()
Buy.purchaseDelay = 0.05
Buy.purchaseTimeout = 3.0
Buy.purchaseFrame = Buy.purchaseFrame or CreateFrame("Frame", "STBuyEmAllPurchaseQueue", UIParent)
Buy.purchaseFrame:Hide()
Buy.purchaseFrame:RegisterEvent("BAG_UPDATE")
Buy.purchaseFrame:RegisterEvent("MERCHANT_UPDATE")
Buy.purchaseFrame:SetScript("OnEvent", function()
Buy:Purchase_OnEvent()
end)
Buy.purchaseFrame:SetScript("OnUpdate", function()
Buy:Purchase_OnUpdate(arg1)
end)
StaticPopupDialogs["STBUYEMALL_CONFIRM"] = {
text = L("Are you sure you want to buy\n %d × %s?"),
button1 = YES,
button2 = NO,
OnAccept = function()
Buy:DoPurchase(Buy.confirmAmount)
end,
timeout = 0,
hideOnEscape = true,
}
HookScript(MerchantFrame, "OnHide", function()
STBuyEmAllFrame:Hide()
Buy:StopPurchaseQueue()
end)
local installed, dispatching
local function DispatchClick(original, button, ignoreModifiers)
local handled, single = Buy:HandleMerchantClick(this, button, ignoreModifiers)
if handled then return end
return original(single and "LeftButton" or button, single and 1 or ignoreModifiers)
end
local function InstallHook()
local current = _G.MerchantItemButton_OnClick
if type(current) ~= "function" then return end
if current == installed then return end
-- A later addon may wrap our previous hook. Each installation must retain
-- its own predecessor; a mutable shared fallback would form a cycle.
local original = current
installed = function(button, ignoreModifiers)
if dispatching then return original(button, ignoreModifiers) end
dispatching = true
local ok, r1, r2, r3, r4 = pcall(DispatchClick, original, button, ignoreModifiers)
dispatching = nil
if not ok then error(r1) end
return r1, r2, r3, r4
end
_G.MerchantItemButton_OnClick = installed
end
InstallHook()
local frame = CreateFrame("Frame", nil, UIParent)
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("MERCHANT_SHOW")
frame:SetScript("OnEvent", InstallHook)
end
+141
View File
@@ -0,0 +1,141 @@
-- Movable Cast Bar
--
-- Hold Ctrl+Shift+Alt to show a marker over the player's cast bar and drag
-- it anywhere; right-click the marker to put it back. The position is saved
-- per character. The Swing Timer, Wand Timer and the Improved Castbar icon
-- are anchored to the cast bar, so they follow it.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local POSITION_KEY = "CastBarPosition"
-- One table for state: Lua 5.0 allows only 32 upvalues per function.
local S = { preview = false }
local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
local function SavedPosition()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
if pos and pos[1] and pos[2] then return pos end
end
-- Put the bar at the saved spot. Goes through the original SetPoint because
-- the wrapper below ignores everybody else while a saved spot exists.
local function Apply()
local pos = SavedPosition()
if not pos then return end
CastingBarFrame:ClearAllPoints()
S.orig(CastingBarFrame, "BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
end
-- Back to the stock spot; Reduced Actionbar re-anchors it on the next layout pass.
local function Reset()
if ShaguTweaks_config then ShaguTweaks_config[POSITION_KEY] = nil end
CastingBarFrame:ClearAllPoints()
S.orig(CastingBarFrame, "BOTTOM", UIParent, "BOTTOM", 0, 60)
if _G.UIParent_ManageFramePositions then _G.UIParent_ManageFramePositions() end
end
local function SetPreview(on)
if S.preview == on then return end
S.preview = on
if on then
local tint = ShaguTweaks.UITint
S.mover.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1,
tint and tint.b or 1, tint and tint.a or 1)
S.mover:Show()
else
CastingBarFrame:StopMovingOrSizing()
S.mover:Hide()
end
end
-- Same look as the Swing Timer preview bars: the classic cast bar with its
-- border and spark, full, in the cast bar's gold.
local function CreateMover()
local m = CreateFrame("StatusBar", "ShaguTweaksCastBarMover", UIParent)
m:SetFrameStrata("DIALOG")
m:SetAllPoints(CastingBarFrame)
m:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
m:SetStatusBarColor(1, .7, 0)
m:SetMinMaxValues(0, 1)
m:SetValue(1)
m:EnableMouse(true)
m:Hide()
m.bg = m:CreateTexture(nil, "BACKGROUND")
m.bg:SetAllPoints(m)
m.bg:SetTexture(0, 0, 0, .5)
-- Keep the rim above the StatusBar's ARTWORK fill during preview/dragging.
m.border = m:CreateTexture(nil, "OVERLAY")
m.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border")
m.border:SetWidth(256)
m.border:SetHeight(64)
m.border:SetPoint("TOP", m, "TOP", 0, 28)
m.spark = m:CreateTexture(nil, "OVERLAY")
m.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
m.spark:SetBlendMode("ADD")
m.spark:SetWidth(32)
m.spark:SetHeight(32)
m.spark:SetPoint("CENTER", m, "RIGHT", 0, 2)
m.text = m:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
m.text:SetPoint("CENTER", m, "CENTER", 0, 3)
m.text:SetText(T["Cast Bar"])
m:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then
-- the swing bars default to sitting on top of the cast bar: pin them
-- first so only the cast bar moves
if ShaguTweaks.FreezeSwingBars then ShaguTweaks.FreezeSwingBars() end
CastingBarFrame:StartMoving()
end
end)
m:SetScript("OnMouseUp", function()
CastingBarFrame:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
Reset()
else
ShaguTweaks_config[POSITION_KEY] = { CastingBarFrame:GetLeft(), CastingBarFrame:GetBottom() }
Apply()
end
end)
S.mover = m
end
ShaguTweaks.EnableCastBarMover = function()
if not CastingBarFrame then return end
if not S.mover then
S.orig = CastingBarFrame.SetPoint
CreateMover()
CastingBarFrame:SetMovable(true)
CastingBarFrame:SetClampedToScreen(true)
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetPreview(ModifiersDown())
else
Apply()
end
end)
end
-- Reduced Actionbar and others re-anchor the bar on every layout pass.
-- While a saved spot exists their SetPoint calls are ignored.
CastingBarFrame.SetPoint = function(frame, a, b, c, d, e)
if SavedPosition() then return end
return S.orig(frame, a, b, c, d, e)
end
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
Apply()
end
+68
View File
@@ -0,0 +1,68 @@
-- Shared chat-filter dispatcher for the chat filter modules (formerly in Extras).
-- Installs one ChatFrame_OnEvent wrapper only when a filter is enabled.
local Filter = ShaguTweaks.ExtrasChatFilter
if not Filter then
Filter = {
handlers = {},
order = {},
known = {},
installed = false,
installer = nil,
}
ShaguTweaks.ExtrasChatFilter = Filter
end
function Filter:Install()
if self.installed then return end
self.installed = true
self.original = ChatFrame_OnEvent
ChatFrame_OnEvent = function(event)
for i = 1, table.getn(Filter.order) do
local current = Filter.handlers[Filter.order[i]]
if current and current(event) then
return
end
end
if Filter.original then
return Filter.original(event)
end
end
end
function Filter:ScheduleInstall()
if self.installed or self.installer then return end
-- ShaguTweaks enables modules during VARIABLES_LOADED in unspecified table
-- order. Install on PLAYER_ENTERING_WORLD so this dispatcher becomes the
-- outer wrapper after the core Chat Spam Filter regardless of module order.
-- That prevents locally hidden World/NPC messages from entering its cache.
self.installer = CreateFrame("Frame")
self.installer:RegisterEvent("PLAYER_ENTERING_WORLD")
self.installer:SetScript("OnEvent", function()
Filter:Install()
this:UnregisterAllEvents()
this:Hide()
Filter.installer = nil
end)
end
function Filter:Register(key, handler)
if type(key) ~= "string" or type(handler) ~= "function" then return end
if not self.known[key] then
self.known[key] = true
self.order[table.getn(self.order) + 1] = key
end
self.handlers[key] = handler
self:ScheduleInstall()
end
function Filter:Unregister(key)
self.handlers[key] = nil
end
+45
View File
@@ -0,0 +1,45 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Center Text Input Box"],
description = T["Move the chat input box to the center of the screen."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
category = T["Chat & Social"],
maintainer = "@shagu (GitHub)",
enabled = nil,
})
local dodge_frames = {
MainMenuBarArtFrame, MultiBarBottomLeft, MultiBarBottomRight, PetActionBarFrame, ShapeshiftBarFrame
}
module.enable = function(self)
ChatFrameEditBox:SetWidth(300)
local function UpdateInputPosition()
local top = 0
for _, frame in ipairs(dodge_frames) do
if frame and frame:IsVisible() and frame:GetTop() then
top = math.max(top, frame:GetTop())
end
end
ChatFrameEditBox:ClearAllPoints()
ChatFrameEditBox:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, top)
end
-- Other ShaguTweaks modules also post-hook UIParent_ManageFramePositions.
-- Module enable order is intentionally unordered, so install this layout
-- hook at PLAYER_ENTERING_WORLD after the normal module pass. This makes
-- the edit box position use the final actionbar/pet/stance positions.
local installer = CreateFrame("Frame", nil, UIParent)
installer:RegisterEvent("PLAYER_ENTERING_WORLD")
installer:SetScript("OnEvent", function()
hooksecurefunc("UIParent_ManageFramePositions", UpdateInputPosition)
UpdateInputPosition()
this:UnregisterAllEvents()
this:Hide()
end)
end
+92
View File
@@ -0,0 +1,92 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local Filter = ShaguTweaks.ExtrasChatFilter
local module = ShaguTweaks:register({
title = T["Block NPC Spam"],
description = T["Blocks known repetitive NPC say/yell spam messages."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Chat & Social"],
enabled = nil,
})
local blockedEvents = {
["CHAT_MSG_MONSTER_SAY"] = true,
["CHAT_MSG_MONSTER_YELL"] = true,
}
local blockedNPCs = {}
local npcNames = {
-- enUS
"Tansy Sparkpen",
"Fara Boltbreaker",
"Shellcoin Promoter",
-- ruRU
"Зазывала ярмарки Новолуния",
"Томас Миллер",
"Уильям",
"Донна",
"Жюстина Демалье",
"Мерлис Малаган",
"Лиана Пирс",
"Сюзанна",
"Джейни Аншип",
}
for _, name in pairs(npcNames) do
blockedNPCs[name] = true
blockedNPCs[string.lower(name)] = true
end
local blockedPhrases = {
{ "shellcoin", "invest" },
{ "shells", "trade" },
{ "shells", "money" },
}
local function IsBlockedNPC(sender)
if not sender then return false end
return blockedNPCs[sender] or blockedNPCs[string.lower(sender)] or false
end
local function IsBlockedPhrase(message)
if not message then return false end
local normalized = string.lower(string.gsub(message, "[^A-Za-z0-9]", ""))
for _, phrase in pairs(blockedPhrases) do
local matched = true
for _, word in pairs(phrase) do
if not string.find(normalized, word, 1, true) then
matched = false
break
end
end
if matched then return true end
end
return false
end
local function FilterNPCSpam(event)
if not blockedEvents[event] then return false end
if not arg1 or not arg2 then return false end
return IsBlockedNPC(arg2) or IsBlockedPhrase(arg1)
end
module.enable = function(self)
if Filter then
Filter:Register("BlockNPCSpam", FilterNPCSpam)
end
end
module.disable = function(self)
if Filter then
Filter:Unregister("BlockNPCSpam")
end
end
+7 -17
View File
@@ -16,11 +16,11 @@ local COOLDOWN = 70
-- how often (in seconds) stale entries may be pruned when chat traffic arrives -- how often (in seconds) stale entries may be pruned when chat traffic arrives
local CLEAN_INTERVAL = 30 local CLEAN_INTERVAL = 30
-- double-buffered cache of [frame] = {message = last_seen_time}, keyed per -- Cache of [frame] = {message = last_seen_time}, keyed per
-- chat frame so that a message shown in one tab (e.g. "General") doesn't -- chat frame so that a message shown in one tab (e.g. "General") doesn't
-- get treated as a duplicate when it also needs to show in another tab -- get treated as a duplicate when it also needs to show in another tab
-- (e.g. a custom "World" tab) -- (e.g. a custom "World" tab)
local cache = { {}, {}, INDEX = 1 } local cache = {}
local last_cleanup = 0 local last_cleanup = 0
local canFilterEmotes = API and API.guildmembership and API.friendmembership local canFilterEmotes = API and API.guildmembership and API.friendmembership
@@ -37,21 +37,12 @@ local function PruneCache(now)
if now - last_cleanup < CLEAN_INTERVAL then return end if now - last_cleanup < CLEAN_INTERVAL then return end
last_cleanup = now last_cleanup = now
local index = cache.INDEX for frame, frameCache in pairs(cache) do
local newindex = (index == 1) and 2 or 1
cache[newindex] = {}
for frame, frameCache in pairs(cache[index]) do
for msg, seen_at in pairs(frameCache) do for msg, seen_at in pairs(frameCache) do
if (seen_at + COOLDOWN) > now then if (seen_at + COOLDOWN) <= now then frameCache[msg] = nil end
cache[newindex][frame] = cache[newindex][frame] or {}
cache[newindex][frame][msg] = seen_at
end
end end
if not next(frameCache) then cache[frame] = nil end
end end
cache[index] = {}
cache.INDEX = newindex
end end
-- returns true (and records the message) if this exact text was already seen -- returns true (and records the message) if this exact text was already seen
@@ -60,11 +51,10 @@ local function IsDuplicate(frame, msg)
local now = GetTime() local now = GetTime()
PruneCache(now) PruneCache(now)
local index = cache.INDEX local frameCache = cache[frame]
local frameCache = cache[index][frame]
if not frameCache then if not frameCache then
frameCache = {} frameCache = {}
cache[index][frame] = frameCache cache[frame] = frameCache
end end
local seen_at = frameCache[msg] local seen_at = frameCache[msg]
+19
View File
@@ -0,0 +1,19 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local rgbhex = ShaguTweaks.rgbhex
local module = ShaguTweaks:register({
title = T["Enable Text Shadow"],
description = T["Enable text shadow in all chat frames."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
category = T["Chat & Social"],
maintainer = "@shagu (GitHub)",
enabled = nil,
})
module.enable = function(self)
for i=1,NUM_CHAT_WINDOWS do
local font, size = _G["ChatFrame"..i]:GetFont()
_G["ChatFrame"..i]:SetFont(font, size, "OUTLINE")
end
end
+57
View File
@@ -0,0 +1,57 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local rgbhex = ShaguTweaks.rgbhex
local module = ShaguTweaks:register({
title = T["Chat Timestamps"],
description = T["Add timestamps to chat messages."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
category = T["Chat & Social"],
maintainer = "@shagu (GitHub)",
enabled = false,
config = {
["chat.timestamp.bracket"] = "[]",
["chat.timestamp.format"] = 24,
["chat.timestamp.color"] = { r = .8, g = .8, b = .8, a = 1},
}
})
module.enable = function(self)
-- config shortcuts
local bracket = self.config["chat.timestamp.bracket"]
local clock = self.config["chat.timestamp.format"]
local rgb = self.config["chat.timestamp.color"]
-- parse config
local left = string.sub(bracket, 1, 1) or ""
local right = string.sub(bracket, 2, 2) or ""
local format = clock == 24 and "%H:%M:%S" or "%I:%M:%S %p"
local color = rgbhex({ rgb.r, rgb.g, rgb.b, rgb.a })
local function InstallTimestampHooks()
for i = 1, NUM_CHAT_WINDOWS do
local frame = _G["ChatFrame"..i]
if frame and not frame.ShaguTweaksExtrasTimestampAddMessage then
frame.ShaguTweaksExtrasTimestampAddMessage = frame.AddMessage
frame.AddMessage = function(self, msg, a1, a2, a3, a4, a5)
if not msg then return end
msg = color .. left .. date(format) .. right .. "|r " .. msg
self:ShaguTweaksExtrasTimestampAddMessage(msg, a1, a2, a3, a4, a5)
end
end
end
end
-- Install after the normal ShaguTweaks module pass. Chat Tweaks in the
-- main fork also wraps AddMessage, and its module order is intentionally
-- unordered. Waiting for PLAYER_ENTERING_WORLD makes timestamps the outer
-- wrapper every time, so history consistently stores the displayed time.
local installer = CreateFrame("Frame", nil, UIParent)
installer:RegisterEvent("PLAYER_ENTERING_WORLD")
installer:SetScript("OnEvent", function()
InstallTimestampHooks()
this:UnregisterAllEvents()
this:Hide()
end)
end
+127 -6
View File
@@ -18,6 +18,90 @@ local module = ShaguTweaks:register({
-- by one complete clock cycle. -- by one complete clock cycle.
local TIMER_WRAP_SECONDS = (2 ^ 32) / 1000 local TIMER_WRAP_SECONDS = (2 ^ 32) / 1000
local formatter
local nativeCooldowns = setmetatable({}, { __mode = "k" })
local cvarWatcher
local SetCooldown
local function FontSize(parent)
local size = parent:GetHeight() or 0
size = size > 0 and size * .64 or 12
return size > 14 and 14 or size
end
local function NativeNumbersEnabled()
return type(_G.GetCVar) == "function" and _G.GetCVar("countdownForCooldowns") == "1"
end
local function CanUseNative(cooldown)
return type(cooldown.GetCountdownFontString) == "function"
and type(cooldown.SetCountdownFormatter) == "function"
and type(cooldown.SetMinimumCountdownDuration) == "function"
and type(cooldown.SetHideCountdownNumbers) == "function"
and _G.C_StringUtil and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function"
and _G.Enum and _G.Enum.NumericRuleFormatRounding
end
local function NativeTarget(cooldown, parent)
if CanUseNative(cooldown) then return cooldown end
if cooldown.stCountdownNativeFrame ~= nil then return cooldown.stCountdownNativeFrame or nil end
if not ShaguTweaks.API.nativecooldowns or not _G.C_StringUtil
or type(_G.C_StringUtil.CreateNumericRuleFormatter) ~= "function" then return end
-- Stock Vanilla indicators can still be Models. Keep their sweep untouched
-- and use an engine-driven child solely for numbers, as the old overlay did.
local ok, child = pcall(CreateFrame, "Cooldown", nil, cooldown)
if not ok or not child then
cooldown.stCountdownNativeFrame = false
return
end
if not CanUseNative(child) then
child:Hide()
cooldown.stCountdownNativeFrame = false
return
end
child.stCountdownHelper = true
child:SetAllPoints(cooldown)
child:SetFrameLevel(parent:GetFrameLevel() + 1)
child:SetDrawSwipe(false)
child:SetDrawEdge(false)
child:SetDrawBling(false)
cooldown.stCountdownNativeFrame = child
return child
end
local function GetFormatter()
if formatter then return formatter end
local up = _G.Enum.NumericRuleFormatRounding.Up
formatter = _G.C_StringUtil.CreateNumericRuleFormatter()
-- Match TimeConvert, including its colors before rounding at 5/10 seconds.
formatter:SetBreakpoints({
{ threshold = 0, step = 1, rounding = up, format = "|cffff5555%d" },
{ threshold = 5, step = 1, rounding = up, format = "|cffffff55%d" },
{ threshold = 10, step = 1, rounding = up, format = "|cffffffff%d" },
{ threshold = 60, format = "|cffffffff%dm", components = { { div = 60, step = 1, rounding = up } } },
{ threshold = 3600, format = "|cffffffff%dh", components = { { div = 3600, step = 1, rounding = up } } },
{ threshold = 86400, format = "|cffffffff%dd", components = { { div = 86400, step = 1, rounding = up } } },
})
return formatter
end
local function RememberNative(cooldown, start, duration, enable)
nativeCooldowns[cooldown] = true
cooldown.stCountdownStart, cooldown.stCountdownDuration, cooldown.stCountdownEnable = start, duration, enable
if cvarWatcher then return end
cvarWatcher = CreateFrame("Frame")
cvarWatcher.enabled = NativeNumbersEnabled()
cvarWatcher:RegisterEvent("CVAR_UPDATE")
cvarWatcher:SetScript("OnEvent", function()
local enabled = NativeNumbersEnabled()
if enabled == cvarWatcher.enabled then return end
cvarWatcher.enabled = enabled
for frame in pairs(nativeCooldowns) do
SetCooldown(frame, frame.stCountdownStart, frame.stCountdownDuration, frame.stCountdownEnable)
end
end)
end
local function CooldownOnUpdate() local function CooldownOnUpdate()
-- hide frames without parent -- hide frames without parent
local parent = this:GetParent() local parent = this:GetParent()
@@ -73,9 +157,7 @@ local function CreateCoolDown(cooldown)
cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(nil, "OVERLAY") cooldown.cooldowntext.text = cooldown.cooldowntext:CreateFontString(nil, "OVERLAY")
-- detect dynamic font size -- detect dynamic font size
local size = parent:GetHeight() or 0 local size = FontSize(parent)
size = size > 0 and size * .64 or 12
size = size > 14 and 14 or size
-- set fonts -- set fonts
cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE") cooldown.cooldowntext.text:SetFont(STANDARD_TEXT_FONT, size, "OUTLINE")
@@ -83,16 +165,55 @@ local function CreateCoolDown(cooldown)
cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate) cooldown.cooldowntext:SetScript("OnUpdate", CooldownOnUpdate)
end end
local function SetCooldown(this, start, duration, enable) SetCooldown = function(this, start, duration, enable)
if this.stCountdownHelper then return end
-- Let OmniCC and ShaguPlates own their cooldown text. Also hide an overlay -- Let OmniCC and ShaguPlates own their cooldown text. Also hide an overlay
-- that may have been created before either compatibility flag was applied. -- that may have been created before either compatibility flag was applied.
if this.noCooldownCount or this.pfCooldownType then if this.noCooldownCount or this.pfCooldownType or this.readable then
if this.cooldowntext then if this.cooldowntext then
this.cooldowntext:Hide() this.cooldowntext:Hide()
end end
if this.stCountdownNativeFrame then this.stCountdownNativeFrame:Hide() end
if (this.stNativeCountdown or this.noCooldownCount)
and type(this.SetHideCountdownNumbers) == "function" then
this:SetHideCountdownNumbers(true)
end
return return
end end
local parent = this:GetParent()
if parent and (CanUseNative(this) or (ShaguTweaks.API.nativecooldowns
and _G.C_StringUtil and type(_G.C_StringUtil.CreateNumericRuleFormatter) == "function")) then
RememberNative(this, start, duration, enable)
if NativeNumbersEnabled() then
local target = NativeTarget(this, parent)
local text = target and target:GetCountdownFontString()
if text then
if this.cooldowntext then this.cooldowntext:Hide() end
if target == this then this.stNativeCountdown = true end
target:SetCountdownFormatter(GetFormatter())
-- Native threshold is strictly greater-than; retain duration >= 2s.
target:SetMinimumCountdownDuration(1999)
local active = start and start > 0 and duration
and duration >= 2 and (not enable or enable > 0)
target:SetHideCountdownNumbers(not active)
if target ~= this then
if active then target:SetCooldown(start, duration) else target:Hide() end
end
text:SetFont(STANDARD_TEXT_FONT, FontSize(parent), "OUTLINE")
text:ClearAllPoints()
text:SetPoint("CENTER", target, "CENTER", 0, 0)
text:SetTextColor(1, 1, 1, 1)
return
end
end
end
-- Never switch on the global CVar: it also controls unrelated addons.
-- Older builds and CVar-off clients keep the Lua renderer.
if this.stCountdownNativeFrame then this.stCountdownNativeFrame:Hide() end
if type(this.SetHideCountdownNumbers) == "function" then this:SetHideCountdownNumbers(true) end
-- don't draw global cooldowns -- don't draw global cooldowns
if not duration or duration < 2 then if not duration or duration < 2 then
-- hide if already existing -- hide if already existing
@@ -108,7 +229,7 @@ local function SetCooldown(this, start, duration, enable)
end end
if this.cooldowntext then if this.cooldowntext then
if start > 0 and duration > 0 and (not enable or enable > 0) then if start and start > 0 and duration > 0 and (not enable or enable > 0) then
this.cooldowntext.elapsed = 0 this.cooldowntext.elapsed = 0
this.cooldowntext:Show() this.cooldowntext:Show()
else else
+99
View File
@@ -0,0 +1,99 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Cursor Tooltip"],
description = T["Attaches default tooltips to the cursor."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Tooltip & Items"],
enabled = nil,
})
module.enable = function(self)
if ShaguTweaks.CursorTooltipInstalled then return end
ShaguTweaks.CursorTooltipInstalled = true
-- Keep the native helper for non-GameTooltip callers. The feature only
-- needs to move the normal world/UI GameTooltip and should not unexpectedly
-- change other tooltip frames that may reuse the same helper.
local originalDefaultAnchor = ShaguTweaks.CursorTooltipOriginalDefaultAnchor
or _G.GameTooltip_SetDefaultAnchor
ShaguTweaks.CursorTooltipOriginalDefaultAnchor = originalDefaultAnchor
local cursor = CreateFrame("Frame", nil, UIParent)
cursor:SetWidth(1)
cursor:SetHeight(1)
cursor:Hide()
local following = false
local lastX, lastY, lastScale
local function UpdateCursor(force)
local scale = UIParent:GetEffectiveScale()
if not scale or scale == 0 then scale = UIParent:GetScale() end
if not scale or scale == 0 then scale = 1 end
local x, y = GetCursorPosition()
-- GetCursorPosition is cheap and required for following the pointer, but
-- avoid a frame-layout operation while the mouse is completely still.
if not force and x == lastX and y == lastY and scale == lastScale then
return
end
lastX, lastY, lastScale = x, y, scale
-- Reusing the same CENTER anchor updates it in place; ClearAllPoints every
-- rendered frame is unnecessary.
cursor:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x / scale, y / scale)
end
cursor:SetScript("OnUpdate", function()
if not following or not GameTooltip:IsShown() then
this:Hide()
return
end
UpdateCursor(false)
end)
function _G.GameTooltip_SetDefaultAnchor(tooltip, parent)
if tooltip ~= GameTooltip then
return originalDefaultAnchor(tooltip, parent)
end
-- Cursor Tooltip intentionally uses ANCHOR_CURSOR without marking the
-- tooltip as a default anchored tooltip. This matches the upstream mod's
-- lifecycle and makes world-unit tooltips disappear immediately when the
-- mouseover unit is lost, while keeping the custom cursor positioning.
tooltip:SetOwner(parent or UIParent, "ANCHOR_CURSOR")
tooltip:ClearAllPoints()
-- Position once before Show() so there is no first-frame jump from the
-- cursor frame's default origin.
UpdateCursor(true)
tooltip:SetPoint("BOTTOMLEFT", cursor, "TOPRIGHT", 12, 12)
tooltip:SetClampedToScreen(true)
following = true
end
-- Default anchoring normally happens before GameTooltip:Show(). Start the
-- per-frame cursor tracker only once the tooltip is actually visible.
hooksecurefunc(GameTooltip, "Show", function()
if not following then return end
UpdateCursor(true)
cursor:Show()
end)
hooksecurefunc(GameTooltip, "Hide", function()
following = false
cursor:Hide()
end)
end
+816 -36
View File
@@ -15,6 +15,26 @@ local module = ShaguTweaks:register({
} }
}) })
-- Golden UI runs the same recolor pass with a gold palette (like the WoW
-- Forever client): textures are tinted gold, borders and tooltip edges get a
-- softer gold, and backgrounds keep their own colors.
local golden = ShaguTweaks:register({
title = T["Golden UI"],
description = T["Turns the borders of the interface, tooltips and dialogs gold, like the WoW Forever client. Darkened UI takes priority when both are enabled."],
expansions = { ["vanilla"] = true },
category = T["Interface"],
enabled = nil,
config = {
["goldmode.color"] = { r = 1, g = .78, b = .4, a = 1 },
["goldmode.border"] = { r = .8, g = .62, b = .32, a = 1 },
["goldmode.bar"] = { r = .8, g = .63, b = .62, a = 1 },
}
})
-- Active palette, set by whichever of the two modules is enabled:
-- tint goes on textures, edge on backdrop borders and added button borders.
local tint, edge
local blacklist = { local blacklist = {
["Solid Texture"] = true, ["Solid Texture"] = true,
["WHITE8X8"] = true, ["WHITE8X8"] = true,
@@ -53,11 +73,7 @@ local regionskips = {
} }
local backgrounds = { local backgrounds = {
["^SpellBookFrame$"] = { 325, 355, 17, -74 },
["^ItemTextFrame$"] = { 300, 355, 24, -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 = { local borders = {
@@ -81,6 +97,9 @@ local borders = {
["ChatFrame(.+)Button"] = -2, ["ChatFrame(.+)Button"] = -2,
["PetFrameHappiness"] = 2, ["PetFrameHappiness"] = 2,
["MicroButton"] = { -21, 0, 0, 0 }, ["MicroButton"] = { -21, 0, 0, 0 },
-- VCB's buff and weapon-buff buttons, like the Blizzard buff buttons
["^VCB_BF_BUFF_BUTTON%d+$"] = 3,
["^VCB_BF_WEAPON_BUTTON%d+$"] = 3,
} }
local addonframes = { local addonframes = {
@@ -97,6 +116,7 @@ local addonframes = {
["Blizzard_RaidUI"] = { "ReadyCheckFrame" }, ["Blizzard_RaidUI"] = { "ReadyCheckFrame" },
["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" }, ["Blizzard_TradeSkillUI"] = { "TradeSkillFrame" },
["Blizzard_TrainerUI"] = { "ClassTrainerFrame" }, ["Blizzard_TrainerUI"] = { "ClassTrainerFrame" },
["Artisan"] = { "ArtisanFrame" },
} }
-- Turtle WoW exposes these extra inspect pages. Keep them out of the generic -- Turtle WoW exposes these extra inspect pages. Keep them out of the generic
@@ -117,21 +137,69 @@ if GetExpansion() == "tbc" then
borders["MicroButton"] = { -21, 0, 0, 0 } borders["MicroButton"] = { -21, 0, 0, 0 }
end end
local function IsBlacklisted(texture) -- Frame art that ships inside an addon (the "AddOns" entry above skips
local name = texture:GetName() -- addon textures, which are mostly icons) but should follow the palette.
local texture = texture:GetTexture() local whitelist = {
if not texture then return true end "outfitter\\textures\\outfitterframe",
"outfitter\\textures\\scrollbartrench",
-- Artisan swaps in its own lower half of the profession window
"artisan\\textures\\bottomleft",
"artisan\\textures\\bottomright",
-- RollFor's classic loot window
"rollfor\\assets\\og-loot-frame",
}
-- Addon windows that build their rows while opening: recolored every time
-- they are shown (and once more on the next frame, after the rows exist).
local showframes = {
["RollForLootFrameHeader"] = true,
}
local PATH_CACHE_LIMIT = 256
local pathCache, pathKeys, pathCursor = {}, {}, 0
local function ClassifyPath(path)
local result = pathCache[path]
local capture = ShaguTweaks.RecolorCapture
if capture then
if result then capture.cacheHits = capture.cacheHits + 1
else capture.cacheMisses = capture.cacheMisses + 1 end
end
if result then return result end
local lower = string.lower(path)
result = 3 -- Candidate: name exclusions must still be evaluated per region.
if string.find(lower, "interface\\questframe\\ui-questlog-empty-", 1, true)
or string.find(lower, "interface\\stationery\\", 1, true)
or string.find(lower, "interface\\mailframe\\mailitemborder", 1, true) then
result = 2 -- Paper/insets always remain native.
else
for i = 1, table.getn(whitelist) do
if string.find(lower, whitelist[i], 1, true) then result = 1; break end
end
if result == 3 then
for entry in pairs(blacklist) do
if string.find(path, entry, 1) then result = 2; break end
end
end
end
pathCursor = math.mod(pathCursor, PATH_CACHE_LIMIT) + 1
local previous = pathKeys[pathCursor]
if previous then pathCache[previous] = nil end
pathKeys[pathCursor], pathCache[path] = path, result
return result
end
local function IsBlacklisted(texture)
local path = texture:GetTexture()
if type(path) ~= "string" then return true end
local name = texture:GetName()
if name and string.find(name, "StationeryBackground", 1, true) then return true end
local result = ClassifyPath(path)
if result == 1 then return nil end -- Explicit artwork whitelist wins over names.
if result == 2 then return true end
if name then if name then
for entry in pairs(blacklist) do for entry in pairs(blacklist) do
if string.find(name, entry, 1) then return true end if string.find(name, entry, 1) then return true end
end end
end end
for entry in pairs(blacklist) do
if string.find(texture, entry, 1) then return true end
end
return nil return nil
end end
@@ -140,50 +208,257 @@ local function AddSpecialBackground(frame, w, h, x, y)
frame.Material:SetTexture("Interface\\Stationery\\StationeryTest1") frame.Material:SetTexture("Interface\\Stationery\\StationeryTest1")
frame.Material:SetWidth(w) frame.Material:SetWidth(w)
frame.Material:SetHeight(h) frame.Material:SetHeight(h)
frame.Material:SetPoint("TOPLEFT", frame, x, y) frame.Material:SetPoint("TOPLEFT", frame, "TOPLEFT", x, y)
frame.Material:SetVertexColor(.8, .8, .8) frame.Material:SetVertexColor(.8, .8, .8)
end end
local function DarkenFrame(frame, r, g, b, a) local function IsSlotArtwork(path)
-- dont't do anything if disabled if type(path) ~= "string" then return end
if not ShaguTweaks.DarkMode then return end path = string.lower(path)
return string.find(path, "interface\\buttons\\ui-slot-background", 1, true)
or string.find(path, "bagshui\\images\\itemslot\\emptyslot", 1, true)
end
-- Leave the original artwork white. Four narrow copies color only its rim.
-- Anchor to the owning button (not a Texture region), so pooled Bagshui
-- buttons carry every strip with them when resized, moved or hidden.
local function TintSlotBorder(texture, button)
local path = texture:GetTexture()
local strips = texture.stSlotEdges
if not IsSlotArtwork(path) then
if strips then
for i = 1, 4 do strips[i]:Hide() end
end
return
end
if not strips then
strips = {}
texture.stSlotEdges = strips
for i = 1, 4 do
strips[i] = button:CreateTexture(nil, "BORDER")
strips[i].stSlotEdge = true
end
strips[1]:SetPoint("TOPLEFT", button, "TOPLEFT", -1, 1)
strips[1]:SetPoint("TOPRIGHT", button, "TOPRIGHT", 1, 1)
strips[1]:SetHeight(3)
strips[2]:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", -1, -1)
strips[2]:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, -1)
strips[2]:SetHeight(3)
strips[3]:SetPoint("TOPLEFT", button, "TOPLEFT", -1, -2)
strips[3]:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", -1, 2)
strips[3]:SetWidth(3)
strips[4]:SetPoint("TOPRIGHT", button, "TOPRIGHT", 1, -2)
strips[4]:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, 2)
strips[4]:SetWidth(3)
end
local left, top, bottomLeft, bottom, right = texture:GetTexCoord()
local w, h = button:GetWidth() + 2, button:GetHeight() + 2
if w <= 6 or h <= 6 then w, h = 39, 39 end
local dx, dy = (right - left) * 3 / w, (bottom - top) * 3 / h
texture:SetVertexColor(1, 1, 1, 1)
strips[1]:SetTexCoord(left, right, top, top + dy)
strips[2]:SetTexCoord(left, right, bottom - dy, bottom)
strips[3]:SetTexCoord(left, left + dx, top + dy, bottom - dy)
strips[4]:SetTexCoord(right - dx, right, top + dy, bottom - dy)
for i = 1, 4 do
strips[i]:SetTexture(path)
strips[i]:SetVertexColor(edge.r, edge.g, edge.b, 1)
strips[i]:SetAlpha(texture:GetAlpha())
strips[i]:Show()
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
-- The Spellbook's metal frame and torn paper share four 256px-high tiles.
local spellbookPaper = {
topleft = {22, 74, 256, 256, 256},
topright = {0, 74, 85, 256, 128},
botleft = {22, 0, 256, 175, 256},
botright = {0, 0, 85, 175, 128},
}
local function RestoreSpellbookParchment(frame, source)
if source.stSpellbookPaper then return end
local path = source:GetTexture()
if type(path) ~= "string" then return end
local _, _, side = string.find(string.lower(path), "^interface\\spellbook\\ui%-spellbookpanel%-(%a+)$")
local crop = spellbookPaper[side]
if not crop then return end
local width, height = source:GetWidth(), source:GetHeight()
if not width or width <= 0 or not height or height <= 0 then return end
-- Keep the art below the paper and the page number above it.
source:SetDrawLayer("BORDER")
if _G.SpellBookPageText then _G.SpellBookPageText:SetDrawLayer("OVERLAY") end
local paper = frame:CreateTexture(nil, "ARTWORK")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
paper:SetVertexColor(1, 1, 1, 1)
local x1, y1, x2, y2, tileWidth = unpack(crop)
local sx, sy = width / tileWidth, height / 256
paper:SetTexCoord(x1 / tileWidth, x2 / tileWidth, y1 / 256, y2 / 256)
paper:SetWidth((x2 - x1) * sx)
paper:SetHeight((y2 - y1) * sy)
paper:SetPoint("TOPLEFT", source, "TOPLEFT", x1 * sx, -y1 * sy)
source.stSpellbookPaper = paper
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
-- Native paper starts below the header and includes the torn lower
-- edge through row 159. Stopping at 148 leaves a tinted bottom band.
local y1, y2 = top and 82 or 0, top and 256 or 160
-- 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 RestoreQuestLogParchment(frame)
if not frame.GetName or frame:GetName() ~= "QuestLogFrame" then return end
for _, source in pairs({frame:GetRegions()}) do
local path = source.GetTexture and source:GetTexture()
if type(path) == "string" and not source.stQuestLogCrops and not source.stOriginalQuestPaper then
local lower = string.lower(path)
local crops, tileWidth
-- Turtle's two native tiles, verified against its shipped artwork.
-- Keep the list surface and both parchment halves; exclude metal rims.
if string.find(lower, "interface\\questframe\\ui-questlog-left", 1, true)
and string.find(lower, "ui%-questlog%-left$") then
crops, tileWidth = {{19,74,320,436},{348,74,512,456}}, 512
elseif string.find(lower, "interface\\questframe\\ui-questlog-right", 1, true)
and string.find(lower, "ui%-questlog%-right$") then
crops, tileWidth = {{0,74,136,456}}, 256
end
if crops then
local w,h = source:GetWidth(), source:GetHeight()
if w and h and w > 0 and h > 0 then
source.stQuestLogCrops = {}
for _, rect in ipairs(crops) do
local paper = frame:CreateTexture(nil, "OVERLAY")
paper.stOriginalQuestPaper = true
paper:SetTexture(path)
paper:SetTexCoord(rect[1]/tileWidth,rect[3]/tileWidth,rect[2]/512,rect[4]/512)
paper:SetWidth((rect[3]-rect[1])*w/tileWidth)
paper:SetHeight((rect[4]-rect[2])*h/512)
paper:SetPoint("TOPLEFT",source,"TOPLEFT",rect[1]*w/tileWidth,-rect[2]*h/512)
paper:SetVertexColor(1,1,1,1)
table.insert(source.stQuestLogCrops,paper)
end
end
end
end
end
end
local function DarkenFrame(frame, r, g, b, a, scan)
-- 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
local capture = ShaguTweaks.RecolorCapture
if capture and frame then
if not scan then
scan = capture.rootIndex[frame]
if not scan then
scan = { name = frame.GetName and frame:GetName() or tostring(frame), calls = 0, frames = 0, regions = 0, tables = 0 }
capture.rootIndex[frame] = scan
table.insert(capture.roots, scan)
end
scan.calls = scan.calls + 1
end
scan.frames = scan.frames + 1
capture.frameVisits = capture.frameVisits + 1
if capture.seen[frame] then capture.repeatVisits = capture.repeatVisits + 1
else capture.seen[frame] = true; capture.uniqueFrames = capture.uniqueFrames + 1 end
end
-- set defaults -- set defaults
if not r and not g and not b then if not r and not g and not b then
r, g, b, a = module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, module.config["darkmode.color"].a r, g, b, a = tint.r, tint.g, tint.b, tint.a
end end
-- iterate through all subframes -- iterate through all subframes
if frame and frame.GetChildren then if frame and frame.GetChildren then
if capture then scan.tables = scan.tables + 1 end
for _, frame in pairs({frame:GetChildren()}) do for _, frame in pairs({frame:GetChildren()}) do
DarkenFrame(frame, r, g, b, a) DarkenFrame(frame, r, g, b, a, scan)
end end
end end
-- set vertex on all regions -- set vertex on all regions
if frame and frame.GetRegions then if frame and frame.GetRegions then
if capture then scan.tables = scan.tables + 1 end
-- read name -- read name
local name = frame.GetName and frame:GetName() local name = frame.GetName and frame:GetName()
-- set a dark backdrop border color everywhere -- set the palette's backdrop border color everywhere
frame:SetBackdropBorderColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, module.config["darkmode.color"].a) if not frame.stPreserveRarityColor then
if capture then capture.borderWrites = capture.borderWrites + 1 end
frame:SetBackdropBorderColor(edge.r, edge.g, edge.b, edge.a)
end
-- add special backgrounds to quests and such -- add special backgrounds to quests and such
for pattern, inset in pairs(backgrounds) do for pattern, inset in pairs(backgrounds) do
if name and string.find(name, pattern) then AddSpecialBackground(frame, inset[1], inset[2], inset[3], inset[4]) end if name and string.find(name, pattern) then
AddSpecialBackground(frame, inset[1], inset[2], inset[3], inset[4])
end
end end
-- add black borders around specified buttons -- add black borders around specified buttons
for pattern, inset in pairs(borders) do for pattern, inset in pairs(borders) do
if name and string.find(name, pattern) then AddBorder(frame, inset, module.config["darkmode.color"]) end if name and string.find(name, pattern) then AddBorder(frame, inset, edge) end
end end
-- scan through all regions (textures) -- scan through all regions (textures)
for id, region in pairs({frame:GetRegions()}) do for id, region in pairs({frame:GetRegions()}) do
if capture then scan.regions = scan.regions + 1 end
if region.SetVertexColor and region:GetObjectType() == "Texture" then if region.SetVertexColor and region:GetObjectType() == "Texture" then
if region:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then if region:GetTexture() and string.find(region:GetTexture(), "UI%-Panel%-Button%-Up") then
-- monochrome buttons -- monochrome buttons
-- region:SetDesaturated(true) -- 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
TintSlotBorder(region, frame)
elseif name == "InboxFrame" then
-- The inbox artwork includes its parchment. Tinting it turns the
-- paper orange; covering it with stationery leaves a straight seam.
-- Keep these regions stock; child buttons and the mail header still
-- receive the palette through their own frames.
elseif name and id and regionskips[name] and regionskips[name][id] then elseif name and id and regionskips[name] and regionskips[name][id] then
-- skip special regions -- skip special regions
elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then elseif region.GetBlendMode and region:GetBlendMode() == "ADD" then
@@ -191,18 +466,251 @@ local function DarkenFrame(frame, r, g, b, a)
elseif IsBlacklisted(region) then elseif IsBlacklisted(region) then
-- skip blacklisted texture names -- skip blacklisted texture names
else else
if capture then capture.textureWrites = capture.textureWrites + 1 end
region:SetVertexColor(r,g,b,a) region:SetVertexColor(r,g,b,a)
if name == "SpellBookFrame" then RestoreSpellbookParchment(frame, region) end
end end
end end
end end
end end
if frame and frame.GetRegions then RestoreQuestParchment(frame) end
if frame and frame.GetRegions then RestoreQuestLogParchment(frame) end
end end
-- register darken frame to global -- register darken frame to global
ShaguTweaks.DarkenFrame = DarkenFrame ShaguTweaks.DarkenFrame = DarkenFrame
module.enable = function(self) -- Other addons that draw their own tooltip-style borders. Their frames (and
ShaguTweaks.DarkMode = true -- everything inside them) keep following the palette even when the addon
-- sets its border color again later, or builds a window only when opened.
local ADDON_PREFIXES = {
"ShaguDPS", "Bagshui", "Outfitter", "SoulstoneTracker", "SSWarlock",
"SSTClassic", "SSTracker", "OctoBus", "RollFor", "VCB_",
"aux_frame", "AuxCraft", "AuxDragOutline",
-- Filled mail slots get rarity borders after their item texture changes.
"MailAttachment", "SendMailPackageButton",
}
local function IsAddonFrameName(name)
if not name then return end
for i = 1, table.getn(ADDON_PREFIXES) do
if string.find(name, ADDON_PREFIXES[i], 1, true) == 1 then return true end
end
end
-- Only light grey/white/silver borders follow the palette. Colored borders
-- (item quality, hover highlights, class or theme colors) and dark ones stay
-- as the addon set them.
local function IsLightNeutral(r, g, b)
r, g, b = tonumber(r) or 0, tonumber(g) or 0, tonumber(b) or 0
local hi, lo = math.max(r, g, b), math.min(r, g, b)
return hi - lo < .1 and hi >= .35
end
local function LockBorder(frame)
if not frame or frame.stBorderLock or not frame.SetBackdropBorderColor then return end
frame.stBorderLock = true
local setColor, setBackdrop = frame.SetBackdropBorderColor, frame.SetBackdrop
frame.SetBackdropBorderColor = function(self, r, g, b, a)
if not self.stPreserveRarityColor and IsLightNeutral(r, g, b) then
setColor(self, edge.r, edge.g, edge.b, a or 1)
else
setColor(self, r, g, b, a)
end
end
-- a new backdrop starts with a white border
if setBackdrop then
frame.SetBackdrop = function(self, backdrop)
local r, g, b, a
if self.stPreserveRarityColor and self.GetBackdropBorderColor then
r, g, b, a = self:GetBackdropBorderColor()
end
setBackdrop(self, backdrop)
if backdrop and backdrop.edgeFile then
if r then
setColor(self, r, g, b, a)
else
setColor(self, edge.r, edge.g, edge.b, 1)
end
end
end
end
-- run the current color through the filter once
if frame.GetBackdropBorderColor then
local ok, r, g, b, a = pcall(frame.GetBackdropBorderColor, frame)
if ok and r then frame:SetBackdropBorderColor(r, g, b, a) end
end
end
-- Border textures (VCB's buff and weapon borders): same rule, on the
-- texture's vertex color.
local function LockTextureBorder(texture)
if texture.stBorderLock or not texture.SetVertexColor then return end
texture.stBorderLock = true
local setColor = texture.SetVertexColor
texture.SetVertexColor = function(self, r, g, b, a)
if IsLightNeutral(r, g, b) then
setColor(self, edge.r, edge.g, edge.b, a or 1)
else
setColor(self, r, g, b, a)
end
end
if texture.GetVertexColor then
local ok, r, g, b, a = pcall(texture.GetVertexColor, texture)
if ok and r then texture:SetVertexColor(r, g, b, a) end
end
end
local function LockTree(frame)
if not frame then return end
if IsExcludedFrame(frame) then return end
local capture = ShaguTweaks.RecolorCapture
if capture then capture.lockVisits = capture.lockVisits + 1 end
LockBorder(frame)
if frame.GetRegions then
if capture then capture.lockTables = capture.lockTables + 1 end
local regions = { frame:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
local name = region.GetName and region:GetName()
if name and string.find(name, "Border$") and region.GetObjectType and region:GetObjectType() == "Texture" then
LockTextureBorder(region)
end
end
end
if frame.GetChildren then
if capture then capture.lockTables = capture.lockTables + 1 end
local children = { frame:GetChildren() }
for i = 1, table.getn(children) do LockTree(children[i]) end
end
end
-- find the addon frames that exist already, anywhere below UIParent
local function FindAddonFrames(frame)
local capture = ShaguTweaks.RecolorCapture
if capture then
capture.discoveryVisits = capture.discoveryVisits + 1
capture.discoveryTables = capture.discoveryTables + 1
end
local children = { frame:GetChildren() }
for i = 1, table.getn(children) do
local child = children[i]
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)
end
end
end
-- These buttons need explicit discovery when created by XML or nested below
-- a minimap child frame. Outfitter and TrinketMenu bake the rim into their art.
local addonMinimapButtons = {
OutfitterMinimapButton = true,
TrinketMenu_IconFrame = true,
AtlasCFMMinimapButton = true,
}
local function TintMinimapButton(frame)
if not tint or not frame or not frame.GetRegions then return end
local name = frame.GetName and frame:GetName()
if name == "OutfitterMinimapButton" or name == "TrinketMenu_IconFrame" then
local normal = frame:GetNormalTexture()
local pushed = frame:GetPushedTexture()
if normal then normal:SetVertexColor(tint.r, tint.g, tint.b, 1) end
if pushed then pushed:SetVertexColor(tint.r, tint.g, tint.b, 1) end
return
end
if IsExcludedFrame(frame) then return end
local regions = { frame:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
local texture = region.GetTexture and region:GetTexture()
if texture and string.find(string.lower(texture), "minimap%-trackingborder") then
region:SetVertexColor(tint.r, tint.g, tint.b, 1)
end
end
end
local function TintLoadedMinimapButton(name)
HookAddonOrVariable(name, function() TintMinimapButton(_G[name]) end)
end
local function RecolorOnShow(frame)
if not frame or frame.stRecolorOnShow then return end
frame.stRecolorOnShow = true
if golden.rollforLoot and frame:GetName() == "RollForLootFrameHeader" then golden.rollforLoot(frame) end
local pending
local function Trailing()
pending = nil
DarkenFrame(frame)
end
ShaguTweaks.HookScript(frame, "OnShow", function()
DarkenFrame(frame)
if not pending and _G.C_Timer and _G.C_Timer.After then
pending = true
_G.C_Timer.After(0, Trailing)
end
end)
if frame:IsShown() then DarkenFrame(frame) end
end
local function LockAddonFrames()
FindAddonFrames(UIParent)
for name in pairs(showframes) do RecolorOnShow(_G[name]) end
if Minimap then
local buttons = { Minimap:GetChildren() }
for i = 1, table.getn(buttons) do TintMinimapButton(buttons[i]) end
end
for name in pairs(addonMinimapButtons) do
local button = _G[name]
if not button then
TintLoadedMinimapButton(name)
elseif button:GetParent() ~= Minimap then
TintMinimapButton(button)
end
end
-- frames those addons create later: named after the addon, or created
-- inside one of their frames (templates bring their children along)
local create = CreateFrame
CreateFrame = function(a1, a2, a3, a4, a5, a6)
local frame = create(a1, a2, a3, a4, a5, a6)
if frame and (IsAddonFrameName(a2) or (type(a3) == "table" and a3.stBorderLock)) then
LockTree(frame)
end
if frame and type(a2) == "string" and showframes[a2] then RecolorOnShow(frame) end
-- buttons that get an extra border (VCB buffs made after login)
if frame and type(a2) == "string" then
for pattern, inset in pairs(borders) do
if string.find(a2, pattern) then AddBorder(frame, inset, edge) end
end
end
-- minimap buttons get their border texture right after they are made
if frame and ((a3 and a3 == Minimap) or addonMinimapButtons[a2]) then
if _G.C_Timer and _G.C_Timer.After then
_G.C_Timer.After(0, function() TintMinimapButton(frame) end)
end
end
return frame
end
end
local function Apply(tintColor, edgeColor, darkBackgrounds)
if tint then return end
tint, edge = tintColor, edgeColor
ShaguTweaks.UITint = tint
ShaguTweaks.UIBorder = edge
local name, original, r, g, b local name, original, r, g, b
local hookBuffButton_Update = BuffButton_Update local hookBuffButton_Update = BuffButton_Update
@@ -220,23 +728,45 @@ module.enable = function(self)
elseif not original and _G[name] then elseif not original and _G[name] then
-- tbc buff buttons don't have borders, so we -- tbc buff buttons don't have borders, so we
-- need to manually add a dark one. -- need to manually add a dark one.
AddBorder(_G[name], 2, module.config["darkmode.color"]) AddBorder(_G[name], 2, edge)
end end
end end
TOOLTIP_DEFAULT_COLOR.r = module.config["darkmode.color"].r TOOLTIP_DEFAULT_COLOR.r = edge.r
TOOLTIP_DEFAULT_COLOR.g = module.config["darkmode.color"].g TOOLTIP_DEFAULT_COLOR.g = edge.g
TOOLTIP_DEFAULT_COLOR.b = module.config["darkmode.color"].b TOOLTIP_DEFAULT_COLOR.b = edge.b
TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = module.config["darkmode.color"].r -- Darkened UI also greys the tooltip background; Golden UI keeps it.
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = module.config["darkmode.color"].g if darkBackgrounds then
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = module.config["darkmode.color"].b TOOLTIP_DEFAULT_BACKGROUND_COLOR.r = tint.r
TOOLTIP_DEFAULT_BACKGROUND_COLOR.g = tint.g
TOOLTIP_DEFAULT_BACKGROUND_COLOR.b = tint.b
end
DarkenFrame(UIParent) DarkenFrame(UIParent)
DarkenFrame(WorldMapFrame) DarkenFrame(WorldMapFrame)
DarkenFrame(DropDownList1) DarkenFrame(DropDownList1)
DarkenFrame(DropDownList2) DarkenFrame(DropDownList2)
DarkenFrame(DropDownList3) DarkenFrame(DropDownList3)
LockAddonFrames()
-- Bagshui restores its empty-slot artwork to white whenever items are
-- assigned. Reapply only to that background, leaving icons, quality
-- borders and user-configured solid background colors alone.
HookAddonOrVariable("Bagshui", function()
local ui = _G.Bagshui and _G.Bagshui.prototypes and _G.Bagshui.prototypes.Ui
if not ui or not ui.UpdateItemButtonColorsAndBadges then return end
ShaguTweaks.hooksecurefunc(ui, "UpdateItemButtonColorsAndBadges", function(self, button)
local data = button and button.bagshuiData
local background = data and data.buttonComponents and data.buttonComponents.background
if background then TintSlotBorder(background, button) end
end)
end)
-- the loot window gets its art set again when it opens
if LootFrame then
ShaguTweaks.HookScript(LootFrame, "OnShow", function() DarkenFrame(LootFrame) end)
end
-- align all actionbutton textures -- align all actionbutton textures
local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" } local bars = { "Action", "BonusAction", "MultiBarBottomLeft", "MultiBarBottomRight", "MultiBarLeft", "MultiBarRight", "Shapeshift" }
@@ -259,7 +789,7 @@ module.enable = function(self)
if button[func] then if button[func] then
local tex = button[func](button) local tex = button[func](button)
if tex then if tex then
tex:SetVertexColor(module.config["darkmode.color"].r+.2, module.config["darkmode.color"].g+.2, module.config["darkmode.color"].b+.2, 1) tex:SetVertexColor(math.min(tint.r+.2, 1), math.min(tint.g+.2, 1), math.min(tint.b+.2, 1), 1)
end end
end end
end end
@@ -268,12 +798,12 @@ module.enable = function(self)
HookAddonOrVariable("Blizzard_AuctionUI", function() HookAddonOrVariable("Blizzard_AuctionUI", function()
for i = 1, 15 do for i = 1, 15 do
local tex = _G["AuctionFilterButton"..i]:GetNormalTexture() local tex = _G["AuctionFilterButton"..i]:GetNormalTexture()
tex:SetVertexColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, 1) tex:SetVertexColor(tint.r, tint.g, tint.b, 1)
end end
for i = 1, 8 do for i = 1, 8 do
_G["BrowseButton"..i.."Left"]:SetVertexColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, 1) _G["BrowseButton"..i.."Left"]:SetVertexColor(tint.r, tint.g, tint.b, 1)
_G["BrowseButton"..i.."Right"]:SetVertexColor(module.config["darkmode.color"].r, module.config["darkmode.color"].g, module.config["darkmode.color"].b, 1) _G["BrowseButton"..i.."Right"]:SetVertexColor(tint.r, tint.g, tint.b, 1)
end end
end) end)
@@ -413,3 +943,253 @@ module.enable = function(self)
table.insert(ShaguTweaks.libnameplate.OnShow, ReapplyShaguDarkMode) table.insert(ShaguTweaks.libnameplate.OnShow, ReapplyShaguDarkMode)
end end
end end
-- Title bars: the dark strips the window titles sit on. Golden UI
-- multiplies them toward WoW Forever's red-brown with a MOD layer. Measured
-- from the window art: rows below the frame's top edge, and how far the strip
-- runs from the left (after the portrait) and from the right (before the
-- close button).
local STRIP_A = { 72, -16, -60, -34 } -- character, spellbook, mail, quest log art
local STRIP_B = { 72, -15, -62, -33 } -- merchant, friends art
local function Shade(parent, layer, color)
local shade = parent:CreateTexture(nil, layer)
shade:SetTexture(1, 1, 1, 1)
shade:SetVertexColor(tonumber(color.r) or .8, tonumber(color.g) or .63, tonumber(color.b) or .62, 1)
shade:SetBlendMode("MOD")
return shade
end
local function PlaceStrip(shade, frame, strip)
shade:ClearAllPoints()
shade:SetPoint("TOPLEFT", frame, "TOPLEFT", strip[1], strip[2])
shade:SetPoint("BOTTOMRIGHT", frame, "TOPRIGHT", strip[3], strip[4])
end
-- The shade has to sit above the window art, which would darken a title
-- that is drawn in the same frame. Those titles are drawn again on top and
-- the original is kept, invisible, for its position and text.
local function RedrawTitle(holder, title)
if not title then return end
local copy = holder:CreateFontString(nil, "OVERLAY", "GameFontNormal")
local font = title.GetFontObject and title:GetFontObject()
if font then copy:SetFontObject(font) end
copy:SetAllPoints(title)
if title.GetJustifyH then copy:SetJustifyH(title:GetJustifyH()) end
copy:SetText(title:GetText())
title:SetAlpha(0)
local setText = title.SetText
title.SetText = function(self, text)
setText(self, text)
copy:SetText(self:GetText())
end
local setFormattedText = title.SetFormattedText
if type(setFormattedText) == "function" then
title.SetFormattedText = function(self, ...)
setFormattedText(self, unpack(arg))
copy:SetText(self:GetText())
end
end
return copy
end
local function ShadeTitleBar(frame, strip, title, color, level)
if not frame or frame.stTitleBar then return end
local bar = CreateFrame("Frame", nil, frame)
bar:SetAllPoints(frame)
if level then bar:SetFrameLevel(level) end
frame.stTitleBar = bar
bar.shade = Shade(bar, "BACKGROUND", color)
PlaceStrip(bar.shade, frame, strip)
bar.title = RedrawTitle(bar, title)
return bar
end
-- frames whose titles live in child frames (or above the art layer): the
-- shade is a plain texture in the window itself, nothing to redraw
local function ShadeStripInFrame(frame, strip, layer, color)
if not frame or frame.stTitleShade then return end
frame.stTitleShade = Shade(frame, layer, color)
PlaceStrip(frame.stTitleShade, frame, strip)
end
-- MailFrame's four art tiles contain both the metal frame and the paper.
-- Restore cropped copies of the original paper above the tinted tiles, below
-- the inbox's child frames. Parent them to InboxFrame so they hide on the
-- Send Mail and TurtleMail Log tabs. Matching the original pixels preserves the torn
-- edges and tile seams without replacing the parchment with stationery.
local function RestoreMailParchment()
if not InboxFrame then return end
if not InboxFrame.stParchmentRetryHook then
InboxFrame.stParchmentRetryHook = true
ShaguTweaks.HookScript(InboxFrame, "OnShow", RestoreMailParchment)
end
if InboxFrame.stOriginalParchment then return end
InboxFrame.stParchmentTiles = InboxFrame.stParchmentTiles or {}
local copies = InboxFrame.stParchmentTiles
local complete = true
local tiles = {
{ "MailFrameTopLeft", 22, 74, 256, 256 },
{ "MailFrameTopRight", 0, 74, 80, 256 },
{ "MailFrameBotLeft", 22, 0, 256, 170 },
{ "MailFrameBotRight", 0, 0, 80, 170 },
}
for i = 1, table.getn(tiles) do
local tile = tiles[i]
local source = _G[tile[1]]
if not copies[i] and source and source:GetTexture() then
local w, h = source:GetWidth(), source:GetHeight()
if w and h and w > 0 and h > 0 then
local paper = InboxFrame:CreateTexture(nil, "BACKGROUND")
paper:SetTexture(source:GetTexture())
paper:SetVertexColor(1, 1, 1, 1)
paper:SetTexCoord(tile[2] / w, tile[4] / w, tile[3] / h, tile[5] / h)
paper:SetWidth(tile[4] - tile[2])
paper:SetHeight(tile[5] - tile[3])
paper:SetPoint("TOPLEFT", source, "TOPLEFT", tile[2], -tile[3])
copies[i] = paper
end
end
if not copies[i] then complete = false end
end
InboxFrame.stOriginalParchment = complete or nil
end
module.enable = function(self)
ShaguTweaks.DarkMode = true
Apply(module.config["darkmode.color"], module.config["darkmode.color"], true)
RestoreMailParchment()
end
local function ShadeNpcTitleBar(frame, nameFrame, panels, color)
if not frame or not nameFrame or frame.stTitleBar then return end
local level = frame:GetFrameLevel() + 1
for _, panel in pairs(panels) do
if panel then level = math.max(level, panel:GetFrameLevel() + 1) end
end
-- NPC names already live in a separate frame. Keep that native title above
-- both the panel artwork and shade, without creating another text copy.
if nameFrame:GetFrameLevel() <= level then nameFrame:SetFrameLevel(level + 1) end
ShadeTitleBar(frame, {72,-22,-60,-39}, nil, color, level)
end
local function ShadeTitleBars(color)
-- world map: its strip is 3 to 21 px below the top of its art (384 above
-- the frame center), from the left corner to the window buttons
if WorldMapFrame and WorldMapFrameTitle and not WorldMapFrame.stTitleBar then
local bar = ShadeTitleBar(WorldMapFrame, { 0, 0, 0, 0 }, WorldMapFrameTitle, color)
bar.shade:ClearAllPoints()
bar.shade:SetPoint("TOPLEFT", WorldMapFrame, "CENTER", -510, 381)
bar.shade:SetPoint("BOTTOMRIGHT", WorldMapFrame, "CENTER", 455, 363)
-- the stone art is lighter around the seam of its first two pieces
-- (measured in 8 px columns): darken those columns a bit more so the
-- bar comes out even
local even = {
{ -272, .81 }, { -264, .76 },
{ -256, .66 }, { -248, .52 }, { -240, .49 }, { -232, .47 }, { -224, .53 }, { -216, .54 },
{ -208, .52 }, { -200, .58 }, { -192, .69 }, { -184, .76 }, { -176, .68 }, { -168, .68 },
{ -160, .69 }, { -152, .71 }, { -144, .78 }, { -136, .86 },
}
for i = 1, table.getn(even) do
local x, f = even[i][1], even[i][2]
local fix = Shade(bar, "BACKGROUND", { r = f, g = f, b = f })
fix:SetPoint("TOPLEFT", WorldMapFrame, "CENTER", x, 381)
fix:SetPoint("BOTTOMRIGHT", WorldMapFrame, "CENTER", x + 8, 363)
end
end
ShadeTitleBar(MerchantFrame, STRIP_B, MerchantNameText, color)
ShadeTitleBar(SpellBookFrame, STRIP_A, SpellBookTitleText, color)
ShadeTitleBar(FriendsFrame, STRIP_B, FriendsFrameTitleText, color)
ShadeNpcTitleBar(QuestFrame, _G.QuestNpcNameFrame,
{ _G.QuestFrameGreetingPanel, _G.QuestFrameDetailPanel, _G.QuestFrameProgressPanel, _G.QuestFrameRewardPanel }, color)
ShadeNpcTitleBar(GossipFrame, _G.GossipNpcNameFrame, { _G.GossipFrameGreetingPanel }, color)
-- the quest log is wider: its strip ends before the close button
ShadeTitleBar(QuestLogFrame, { 72, -16, -121, -34 }, QuestLogTitleText, color)
-- mail: the art is on the BORDER layer, the titles are on the page frames
ShadeStripInFrame(MailFrame, STRIP_A, "ARTWORK", color)
-- character: the art is on the tab pages (PaperDollFrame and the others),
-- the name on CharacterNameFrame. The shade goes between the two.
if CharacterFrame and PaperDollFrame and CharacterNameFrame then
local level = PaperDollFrame:GetFrameLevel() + 1
if CharacterNameFrame:GetFrameLevel() <= level then CharacterNameFrame:SetFrameLevel(level + 1) end
ShadeTitleBar(CharacterFrame, STRIP_A, nil, color, level)
end
end
-- RollFor's classic loot window: the art is on the BACKGROUND layer and the
-- "Items" title on ARTWORK, so the shade fits between them. The strip runs
-- 12 to 30 px down, from the portrait to 25 px before the right edge (the
-- window width changes with the item names).
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
ShaguTweaks.GoldMode = true
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
end
-- Function references for the optional companion; no profiling starts here.
ShaguTweaks.RecolorProfileFunctions = {
DarkenFrame = DarkenFrame, LockTree = LockTree,
FindAddonFrames = FindAddonFrames, ClassifyPath = ClassifyPath,
}
+177
View File
@@ -10,11 +10,186 @@ local module = ShaguTweaks:register({
enabled = true, enabled = true,
}) })
local function MatchBackground(compare, source)
if not ShaguTweaks_config or ShaguTweaks_config[T["Darkened UI"]] ~= 1
or not source.GetBackdropColor or not compare.SetBackdropColor then return end
local r,g,b,a = source:GetBackdropColor()
if not r then return end
local cr,cg,cb,ca = compare:GetBackdropColor()
if cr ~= r or cg ~= g or cb ~= b or ca ~= a then compare:SetBackdropColor(r,g,b,a) end
end
local function EnableNativeCompare(self)
if not API.tooltipsetitem or not API.modifierstate or not API.timer
or not ShoppingTooltip1 or not ShoppingTooltip2
or type(ShoppingTooltip1.SetHyperlinkCompareItem) ~= "function"
or type(ShoppingTooltip2.SetHyperlinkCompareItem) ~= "function" then return false end
local active, activeLink, activeOwner, activeRevision
local firstShown, secondShown
local revision, building = 0, false
local tracked, order = {}, {}
local function Hide(tooltip)
if tooltip and tooltip ~= active then return end
active = nil
activeLink, activeOwner, activeRevision = nil, nil, nil
firstShown, secondShown = nil, nil
ShoppingTooltip1:Hide(); ShoppingTooltip2:Hide()
end
local function Restore(tooltip, link, owner)
if not API.IsShiftKeyDown() or active ~= tooltip or activeLink ~= link
or activeOwner ~= owner or activeRevision ~= revision then return false end
-- Hide clears native ownership. Show alone can briefly display the cached
-- text but leave an unowned tooltip that the client then discards/fades.
-- Rebind to the actual hovered button (not the source tooltip) and refill
-- in the same frame only when the native owner/content was lost.
building = true
for i = 1, 2 do
local compare = i == 1 and ShoppingTooltip1 or ShoppingTooltip2
if (i == 1 and firstShown) or (i == 2 and secondShown) then
if not compare:IsShown() or compare:GetOwner() ~= (owner or UIParent) then
local point, relative, relativePoint, x, y = compare:GetPoint()
compare:SetOwner(owner or UIParent, "ANCHOR_NONE")
if point then compare:SetPoint(point, relative, relativePoint, x, y) end
if compare:SetHyperlinkCompareItem(link, i, true, tooltip) then compare:Show() end
end
MatchBackground(compare, tooltip)
end
end
building = false
return true
end
local function Show(tooltip)
if building then return end
if not tooltip:IsShown() then Hide(tooltip); return end
if not API.IsShiftKeyDown() then Hide(); return end
local _, link = tooltip:GetItem()
if not link then Hide(tooltip); return end
local owner = tooltip:GetOwner()
-- Native/tooltips addons can rebuild the hovered item repeatedly. Its
-- comparison does not change unless the item, owner or equipped data did.
if Restore(tooltip, link, owner) then return end
building = true
Hide()
local x = GetCursorPosition() / UIParent:GetEffectiveScale()
local left = x >= GetScreenWidth() / 2
local point, parent = tooltip:GetPoint()
if parent == UIParent and point == "TOPRIGHT" then left = true end
local anchor, relative = left and "TOPRIGHT" or "TOPLEFT", left and "TOPLEFT" or "TOPRIGHT"
local previous = tooltip
for i = 1, 2 do
local compare = i == 1 and ShoppingTooltip1 or ShoppingTooltip2
compare:SetOwner(owner or UIParent, "ANCHOR_NONE")
compare:ClearAllPoints()
compare:SetPoint(anchor, previous, relative, 0, 0)
-- Preserve the full item link (enchants/random suffixes). Native code
-- selects filled ring/weapon slots and supplies the equipped header.
if compare:SetHyperlinkCompareItem(link, i, true, tooltip) then
compare:Show()
MatchBackground(compare, tooltip)
previous = compare
active = tooltip
else compare:Hide() end
end
if active then
activeLink, activeOwner, activeRevision = link, owner, revision
firstShown, secondShown = ShoppingTooltip1:IsShown(), ShoppingTooltip2:IsShown()
end
building = false
end
local function Schedule(tooltip)
local state = tracked[tooltip]
if building or not state or state.pending then return end
state.pending = true
API.Defer(state.refresh)
end
-- A comparison may be hidden by native/foreign tooltip code independently
-- of its source. Repair on that event; no recurring polling driver.
local function ComparisonHidden()
if active and not building then Schedule(active) end
end
ShoppingTooltip1:HookScript("OnHide", ComparisonHidden)
ShoppingTooltip2:HookScript("OnHide", ComparisonHidden)
local function SourceUpdated(tooltip)
if building then return end
if tooltip:IsShown() then
local _, link = tooltip:GetItem()
if link then Restore(tooltip, link, tooltip:GetOwner()) end
end
Schedule(tooltip)
end
local function Track(tooltip)
if not tooltip or tracked[tooltip] or type(tooltip.GetItem) ~= "function" then return end
local state = {}
tracked[tooltip] = state
table.insert(order, tooltip)
state.refresh = function()
state.pending = nil
Show(tooltip)
end
local updated = function() SourceUpdated(tooltip) end
local settled = function() Schedule(tooltip) end
tooltip:HookScript("OnTooltipSetItem", updated)
-- Clearing is part of a normal item rebuild. Inspect the settled content
-- next frame instead of hiding a still-valid comparison immediately.
tooltip:HookScript("OnTooltipCleared", settled)
tooltip:HookScript("OnHide", settled)
tooltip:HookScript("OnShow", updated)
end
ShoppingTooltip1:SetClampedToScreen(true)
ShoppingTooltip2:SetClampedToScreen(true)
Track(GameTooltip)
Track(ItemRefTooltip)
ShaguTweaks.HookAddonOrVariable("AtlasLoot", function()
Track(AtlasLootTooltip); Track(AtlasLootTooltip2)
end)
local function VisibleItemTooltip()
for _, tooltip in ipairs(order) do
if tooltip:IsShown() then
local _, link = tooltip:GetItem()
if link then return tooltip end
end
end
end
local modifier = CreateFrame("Frame")
self.nativeModifier = modifier
modifier:RegisterEvent("MODIFIER_STATE_CHANGED")
local utils = _G.C_EventUtils
if utils and utils.IsEventValid and utils.IsEventValid("PLAYER_EQUIPMENT_CHANGED") then
modifier:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
else modifier:RegisterEvent("UNIT_INVENTORY_CHANGED") end
if utils and utils.IsEventValid and utils.IsEventValid("GET_ITEM_INFO_RECEIVED") then
modifier:RegisterEvent("GET_ITEM_INFO_RECEIVED")
end
modifier:SetScript("OnEvent", function()
if event == "PLAYER_EQUIPMENT_CHANGED" or event == "UNIT_INVENTORY_CHANGED"
or event == "GET_ITEM_INFO_RECEIVED" then
if event == "UNIT_INVENTORY_CHANGED" and arg1 ~= "player" then return end
revision = revision + 1
if not API.IsShiftKeyDown() then return end
if active then Schedule(active)
else
local tooltip = VisibleItemTooltip()
if tooltip then Schedule(tooltip) end
end
return
end
if arg1 ~= "LSHIFT" and arg1 ~= "RSHIFT" then return end
if not API.IsShiftKeyDown() then Hide(); return end
if active and active:IsShown() then Show(active); return end
local tooltip = VisibleItemTooltip()
if tooltip then Show(tooltip) end
end)
return true
end
module.enable = function(self) module.enable = function(self)
if self.installed then return end
if EnableNativeCompare(self) then self.installed = true; return end
if not API.tooltipsetitem or not API.modifierstate or not API.timer if not API.tooltipsetitem or not API.modifierstate or not API.timer
or not API.iteminventorytype or not API.iteminventoryslotkey then or not API.iteminventorytype or not API.iteminventoryslotkey then
return return
end end
self.installed = true
local sides = { "Left", "Right" } local sides = { "Left", "Right" }
@@ -129,6 +304,7 @@ module.enable = function(self)
ShoppingTooltip1:SetInventoryItem("player", slotID) ShoppingTooltip1:SetInventoryItem("player", slotID)
ShoppingTooltip1:Show() ShoppingTooltip1:Show()
AddHeader(ShoppingTooltip1) AddHeader(ShoppingTooltip1)
MatchBackground(ShoppingTooltip1, tooltip)
if slotNames[2] then if slotNames[2] then
local secondSlotID = GetInventorySlotInfo(slotNames[2]) local secondSlotID = GetInventorySlotInfo(slotNames[2])
@@ -142,6 +318,7 @@ module.enable = function(self)
ShoppingTooltip2:SetInventoryItem("player", secondSlotID) ShoppingTooltip2:SetInventoryItem("player", secondSlotID)
ShoppingTooltip2:Show() ShoppingTooltip2:Show()
AddHeader(ShoppingTooltip2) AddHeader(ShoppingTooltip2)
MatchBackground(ShoppingTooltip2, tooltip)
end end
activeTooltip = tooltip activeTooltip = tooltip
+1 -1
View File
@@ -1,5 +1,5 @@
local _G = ShaguTweaks.GetGlobalEnv() local _G = ShaguTweaks.GetGlobalEnv()
local L, T = ShaguTweaks.L, ShaguTweaks.T local T = ShaguTweaks.T
local API = ShaguTweaks.API local API = ShaguTweaks.API
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
+13 -7
View File
@@ -143,6 +143,7 @@ module.enable = function(self)
if bar then bar:SetStatusBarTexture(texture) end if bar then bar:SetStatusBarTexture(texture) end
end end
local playerHealthY = PlayerFrameHealthBar:GetHeight() >= 20 and -7 or 0.5 local playerHealthY = PlayerFrameHealthBar:GetHeight() >= 20 and -7 or 0.5
-- The native Big Health frame reserves the top for the player's name.
local targetHealthY = TargetFrameHealthBar:GetHeight() >= 20 and -7 or 0.5 local targetHealthY = TargetFrameHealthBar:GetHeight() >= 20 and -7 or 0.5
AnchorHealth(self.valueTexts.playerHealth, PlayerFrameHealthBar, playerHealthY) AnchorHealth(self.valueTexts.playerHealth, PlayerFrameHealthBar, playerHealthY)
@@ -297,6 +298,10 @@ module.enable = function(self)
self.valueEvents = self.valueEvents or CreateFrame("Frame") self.valueEvents = self.valueEvents or CreateFrame("Frame")
local events = self.valueEvents local events = self.valueEvents
local healthTexts = { player = self.valueTexts.playerHealth,
target = self.valueTexts.targetHealth, pet = self.valueTexts.petHealth }
local powerTexts = { player = self.valueTexts.playerMana,
target = self.valueTexts.targetMana, pet = self.valueTexts.petMana }
events:RegisterEvent("PLAYER_ENTERING_WORLD") events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("PLAYER_TARGET_CHANGED") events:RegisterEvent("PLAYER_TARGET_CHANGED")
@@ -326,13 +331,14 @@ module.enable = function(self)
UpdateTarget() UpdateTarget()
elseif event == "PET_UI_UPDATE" then elseif event == "PET_UI_UPDATE" then
UpdatePet() UpdatePet()
elseif arg1 == "player" then elseif arg1 == "player" or arg1 == "target" or arg1 == "pet" then
UpdatePlayer() local unit = arg1
UpdateAlternatePower() if event == "UNIT_HEALTH" or event == "UNIT_MAXHEALTH" then
elseif arg1 == "target" then UpdateHealth(healthTexts[unit], unit, unit == "target")
UpdateTarget() else
elseif arg1 == "pet" then UpdatePower(powerTexts[unit], unit)
UpdatePet() if unit == "player" then UpdateAlternatePower() end
end
end end
end) end)
+146
View File
@@ -0,0 +1,146 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Hide Combat Tooltip"],
description = T["Hides the tooltip in combat. Hold Shift to show it temporarily."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Tooltip & Items"],
enabled = nil,
})
module.enable = function(self)
if ShaguTweaks.HideCombatTooltipInstalled then return end
ShaguTweaks.HideCombatTooltipInstalled = true
local controller = CreateFrame("Frame", nil, UIParent)
local inCombat = UnitAffectingCombat("player") and true or false
local shiftDown = API.IsShiftKeyDown() and true or false
local guardActive = false
local elapsed = 0
local function Apply()
if not inCombat then
if GameTooltip:GetAlpha() ~= 1 then
GameTooltip:SetAlpha(1)
end
return
end
local alpha = shiftDown and 1 or 0
if GameTooltip:GetAlpha() ~= alpha then
GameTooltip:SetAlpha(alpha)
end
end
local function GuardTick()
elapsed = elapsed + (arg1 or 0)
if elapsed < .05 then return end
elapsed = 0
-- ClassicAPI provides modifier events, so normal clients don't need to
-- query Shift here. This fallback is only for old environments.
if not API.modifierstate then
shiftDown = API.IsShiftKeyDown() and true or false
end
Apply()
end
local function StartGuard()
if guardActive or not inCombat or not GameTooltip:IsShown() then return end
guardActive = true
elapsed = 0
controller:SetScript("OnUpdate", GuardTick)
end
local function StopGuard()
if not guardActive then return end
guardActive = false
elapsed = 0
controller:SetScript("OnUpdate", nil)
end
local function RefreshShift()
shiftDown = API.IsShiftKeyDown() and true or false
if inCombat and GameTooltip:IsShown() then
Apply()
StartGuard()
end
end
local function EnterCombat()
inCombat = true
shiftDown = API.IsShiftKeyDown() and true or false
if GameTooltip:IsShown() then
Apply()
StartGuard()
end
end
local function LeaveCombat()
inCombat = false
StopGuard()
Apply()
end
-- Vanilla/Turtle can restore GameTooltip alpha internally while updating a
-- visible unit tooltip. A purely event-driven implementation therefore does
-- not stay hidden reliably. The guard below exists only while BOTH combat
-- and an actual GameTooltip are active, and is throttled to 20 Hz instead of
-- running the full upstream logic every rendered frame.
local oldOnShow = GameTooltip:GetScript("OnShow")
GameTooltip:SetScript("OnShow", function()
if oldOnShow then oldOnShow() end
if inCombat then
shiftDown = API.IsShiftKeyDown() and true or false
Apply()
StartGuard()
else
Apply()
end
end)
local oldOnHide = GameTooltip:GetScript("OnHide")
GameTooltip:SetScript("OnHide", function()
if oldOnHide then oldOnHide() end
StopGuard()
end)
controller:RegisterEvent("PLAYER_ENTERING_WORLD")
controller:RegisterEvent("PLAYER_REGEN_DISABLED")
controller:RegisterEvent("PLAYER_REGEN_ENABLED")
if API.modifierstate then
controller:RegisterEvent("MODIFIER_STATE_CHANGED")
end
controller:SetScript("OnEvent", function()
if event == "PLAYER_REGEN_DISABLED" then
EnterCombat()
elseif event == "PLAYER_REGEN_ENABLED" then
LeaveCombat()
elseif event == "MODIFIER_STATE_CHANGED" then
RefreshShift()
elseif event == "PLAYER_ENTERING_WORLD" then
if UnitAffectingCombat("player") then
EnterCombat()
else
LeaveCombat()
end
end
end)
if inCombat then
EnterCombat()
else
LeaveCombat()
end
end
+30 -5
View File
@@ -84,6 +84,19 @@ module.enable = function(self)
end end
local timerFormat = "%.1f"..T["s"] local timerFormat = "%.1f"..T["s"]
local nativeText = API.durationtimers and type(API.GetCastDuration) == "function"
if nativeText then
local formatter = _G.C_StringUtil.CreateNumericRuleFormatter()
formatter:AddBreakpoint({ threshold = 0, format = timerFormat })
castbar.timerBinding = _G.C_DurationUtil.CreateDurationTextBinding()
castbar.timerBinding:SetFontString(castbar.timerText)
castbar.timerBinding:SetFormatter(formatter)
castbar.timerBinding:SetExpiredText(string.format(timerFormat, 0))
ShaguTweaks.HookScript(CastingBarFrame, "OnHide", function()
castbar.timerBinding:Disable()
castbar:Hide()
end)
end
local function UpdateTimer() local function UpdateTimer()
local alpha = CastingBarFrame:GetAlpha() local alpha = CastingBarFrame:GetAlpha()
@@ -136,6 +149,7 @@ module.enable = function(self)
castbar.startTime = nil castbar.startTime = nil
castbar.endTime = nil castbar.endTime = nil
castbar.isChannel = nil castbar.isChannel = nil
if castbar.timerBinding then castbar.timerBinding:Disable() end
if CastingBarFrame:GetAlpha() == 0 then if CastingBarFrame:GetAlpha() == 0 then
castbar:Hide() castbar:Hide()
end end
@@ -157,24 +171,35 @@ module.enable = function(self)
castbar.elapsed = 0 castbar.elapsed = 0
castbar:Show() castbar:Show()
UpdateTimer() if nativeText then
local duration
duration, castbar.fallbackDuration = API.GetCastDuration("player", isChannel,
castbar.startTime, castbar.endTime, castbar.fallbackDuration)
castbar.timerBinding:SetDuration(duration)
castbar.timerBinding:Enable()
else
UpdateTimer()
end
end end
-- The cast/channel query runs only on ClassicAPI state-change events. -- The cast/channel query runs only on ClassicAPI state-change events.
-- While visible, OnUpdate only animates the remaining-time text and alpha. -- Native text inherits the player's normal fade. Only older builds need
-- an OnUpdate to animate the remaining-time text and alpha.
castbar.elapsed = 0 castbar.elapsed = 0
castbar:SetScript("OnUpdate", function() if not nativeText then castbar:SetScript("OnUpdate", function()
this.elapsed = (this.elapsed or 0) + arg1 this.elapsed = (this.elapsed or 0) + arg1
if this.elapsed < .05 then return end if this.elapsed < .05 then return end
this.elapsed = 0 this.elapsed = 0
UpdateTimer() UpdateTimer()
end) end) end
if nativeText then ShaguTweaks.HookScript(CastingBarFrame, "OnShow", RefreshCast) end
local events = CreateFrame("Frame", nil, UIParent) local events = CreateFrame("Frame", nil, UIParent)
events:RegisterEvent("PLAYER_ENTERING_WORLD") events:RegisterEvent("PLAYER_ENTERING_WORLD")
for index = 1, table.getn(classicEvents) do for index = 1, table.getn(classicEvents) do
events:RegisterEvent(classicEvents[index]) API.RegisterUnitEvent(events, classicEvents[index], "player")
end end
events:SetScript("OnEvent", function() events:SetScript("OnEvent", function()
+195 -14
View File
@@ -62,6 +62,9 @@ local weaponOptions = {
glowMin = 2, glowMin = 2,
} }
local transparentOptions = { borderMin = 2, glowMin = 2, defaultAlpha = 0 } local transparentOptions = { borderMin = 2, glowMin = 2, defaultAlpha = 0 }
local mailTradeOptions = {
borderMin = 0, glowMin = 2, defaultAlpha = 0, quest = false, mailTrade = true,
}
local icon14Options = { local icon14Options = {
borderInset = 2, glowInset = 14, borderMin = 2, glowMin = 2, borderInset = 2, glowInset = 14, borderMin = 2, glowMin = 2,
defaultAlpha = 0, defaultAlpha = 0,
@@ -77,6 +80,68 @@ local function Quality(itemID)
return API.GetItemQualityByID(itemID) return API.GetItemQualityByID(itemID)
end end
-- Cold metadata needs one retry when ClassicAPI's cache reply arrives. Store
-- identities only: the refresh rereads live slots instead of repainting a
-- recycled button with an old item's reply. No polling while waiting.
local pendingItems = {}
local itemEvents, itemRefreshQueued
local function StopItemEventsIfIdle()
if itemEvents and not next(pendingItems) then
itemEvents:UnregisterEvent("GET_ITEM_INFO_RECEIVED")
end
end
local function RetryItemColors()
itemRefreshQueued = nil
if Engine.borders or Engine.glows then
if Engine.RefreshAll then Engine:RefreshAll() end
end
end
local function TrackColdItem(button, itemID, quality)
if not itemID or quality ~= nil or not (Engine.borders or Engine.glows)
or not button.IsVisible or not button:IsVisible() then
if pendingItems[button] then
pendingItems[button] = nil
StopItemEventsIfIdle()
end
return
end
if not API.eventutils or not _G.C_EventUtils
or not _G.C_EventUtils.IsEventValid("GET_ITEM_INFO_RECEIVED") then return end
if not itemEvents then
itemEvents = CreateFrame("Frame")
itemEvents:SetScript("OnEvent", function()
local refresh
for slot, id in pairs(pendingItems) do
if not slot:IsVisible() then
pendingItems[slot] = nil
elseif id == arg1 then
pendingItems[slot] = nil
if arg2 and arg2 ~= 0 then refresh = true end
end
end
StopItemEventsIfIdle()
if refresh and not itemRefreshQueued then
itemRefreshQueued = true
if API.timer then API.Defer(RetryItemColors)
else ShaguTweaks.QueueFunction(RetryItemColors) end
end
end)
end
if not button.stItemCacheHideHook then
button.stItemCacheHideHook = true
HookScript(button, "OnHide", function()
pendingItems[button] = nil
StopItemEventsIfIdle()
end)
end
pendingItems[button] = itemID
itemEvents:RegisterEvent("GET_ITEM_INFO_RECEIVED")
end
local function IsQuestItem(itemID) local function IsQuestItem(itemID)
if not itemID then return false end if not itemID then return false end
local _, _, _, _, _, classID = API.GetItemInfoInstant(itemID) local _, _, _, _, _, classID = API.GetItemInfoInstant(itemID)
@@ -110,12 +175,14 @@ local function EnsureGlow(button, inset)
return texture return texture
end end
local function SetVisualColor(button, itemID, opts) local function SetVisualColor(button, itemID, opts, knownQuality)
if not button then return end if not button then return end
opts = opts or emptyOptions opts = opts or emptyOptions
local default = opts.default or defaultColor local default = opts.default or defaultColor
local quality = Quality(itemID) local quality = knownQuality
if quality == nil then quality = Quality(itemID) end
TrackColdItem(button, itemID, quality)
local quest = opts.quest ~= false and IsQuestItem(itemID) local quest = opts.quest ~= false and IsQuestItem(itemID)
local r, g, b local r, g, b
local hasRarity = false local hasRarity = false
@@ -131,12 +198,17 @@ local function SetVisualColor(button, itemID, opts)
if Engine.borders then if Engine.borders then
local border = AddBorder(button, opts.borderInset or 3, default) local border = AddBorder(button, opts.borderInset or 3, default)
if border then if border then
-- Common items must stay white even under a global UI palette.
border.stPreserveRarityColor = opts.mailTrade or nil
if hasRarity then if hasRarity then
border:SetBackdropBorderColor(r, g, b, 1) border:SetBackdropBorderColor(r, g, b, 1)
else else
border:SetBackdropBorderColor(default.r, default.g, default.b, opts.defaultAlpha or 1) border:SetBackdropBorderColor(default.r, default.g, default.b, opts.defaultAlpha or 1)
end end
end end
elseif opts.mailTrade and button.ShaguTweaks_border then
button.ShaguTweaks_border.stPreserveRarityColor = nil
button.ShaguTweaks_border:SetBackdropBorderColor(default.r, default.g, default.b, 0)
end end
if Engine.glows then if Engine.glows then
@@ -350,16 +422,104 @@ function Engine:Install()
end end
end end
local function RefreshSendMail()
local opts = mailTradeOptions
local button = _G.SendMailPackageButton
if button then
local itemID
if type(_G.GetSendMailItemLink) == "function" then
itemID = API.GetItemIDFromLink(_G.GetSendMailItemLink())
end
local quality
if type(_G.GetSendMailItem) == "function" then
local name, texture, count, value = _G.GetSendMailItem()
if name then quality = value end
end
SetVisualColor(button, itemID, opts, quality)
end
-- TurtleMail stores each attachment's source bag and slot on its button.
-- Stop at the first missing button; no dependency on its private constants.
local i = 1
while _G["MailAttachment" .. i] do
button = _G["MailAttachment" .. i]
local item = button.item
local itemID = item and API.GetContainerItemID(item[1], item[2])
SetVisualColor(button, itemID, opts)
i = i + 1
end
end
-- Share one callback across a burst of show/item events. Direct TurtleMail
-- attachment hooks below still color the individual slot immediately.
local sendMailRefreshPending
local function RunSendMailRefresh()
sendMailRefreshPending = nil
if MailFrame and MailFrame:IsShown() then RefreshSendMail() end
end
local function QueueSendMailRefresh()
if sendMailRefreshPending then return end
sendMailRefreshPending = true
if API.timer then
API.Defer(RunSendMailRefresh)
else
ShaguTweaks.QueueFunction(RunSendMailRefresh)
end
end
local function PaintTradeSlot(button, name, link)
local opts = mailTradeOptions
-- The live client returns valid trade links but no GetItemInfo(link)
-- quality. Resolve their IDs through the same capability-aware quality
-- lookup used successfully for mail attachments.
local itemID = name and link and API.GetItemIDFromLink(link)
SetVisualColor(button, itemID, opts)
end
local function RefreshPlayerTradeSlot(index)
local button = _G["TradePlayerItem" .. index .. "ItemButton"]
if button then
local name = GetTradePlayerItemInfo(index)
local link = name and GetTradePlayerItemLink(index)
PaintTradeSlot(button, name, link)
end
end
local function RefreshTargetTradeSlot(index)
local button = _G["TradeRecipientItem" .. index .. "ItemButton"]
if button then
local name = GetTradeTargetItemInfo(index)
local link = name and GetTradeTargetItemLink(index)
PaintTradeSlot(button, name, link)
end
end
local function RefreshTrade() local function RefreshTrade()
for i = 1, 7 do for i = 1, 7 do
local player = _G["TradePlayerItem" .. i .. "ItemButton"] RefreshPlayerTradeSlot(i)
local target = _G["TradeRecipientItem" .. i .. "ItemButton"] RefreshTargetTradeSlot(i)
if player then end
SetVisualColor(player, API.GetTradePlayerItemID(i), transparentOptions) end
end
if target then -- TurtleMail replaces SendMailFrame_Update at PLAYER_LOGIN, after our
SetVisualColor(target, API.GetTradeTargetItemID(i), transparentOptions) -- global hook may have been installed. Its buttons always set their normal
-- texture when adding/removing an attachment, so hook that stable boundary.
local function RefreshAttachmentTexture(button, texture)
local opts = mailTradeOptions
local item = texture and button.item
local itemID = item and API.GetContainerItemID(item[1], item[2])
SetVisualColor(button, itemID, opts)
end
local function HookAttachmentTextures()
local i = 1
while _G["MailAttachment" .. i] do
local button = _G["MailAttachment" .. i]
if not button.stRarityTextureHook then
button.stRarityTextureHook = true
hooksecurefunc(button, "SetNormalTexture", RefreshAttachmentTexture)
end end
i = i + 1
end end
end end
@@ -425,6 +585,7 @@ function Engine:Install()
self.RefreshAll = function() self.RefreshAll = function()
RefreshPaperdoll() RefreshPaperdoll()
if InspectFrame and InspectFrame:IsShown() then RefreshInspect() end
RefreshWeapons() RefreshWeapons()
RefreshBags() RefreshBags()
if BankFrame and BankFrame:IsShown() then RefreshBank() end if BankFrame and BankFrame:IsShown() then RefreshBank() end
@@ -432,6 +593,10 @@ function Engine:Install()
if QuestFrame and QuestFrame:IsShown() then RefreshQuest() end if QuestFrame and QuestFrame:IsShown() then RefreshQuest() end
if QuestLogFrame and QuestLogFrame:IsShown() then RefreshQuestLog() end if QuestLogFrame and QuestLogFrame:IsShown() then RefreshQuestLog() end
if TradeFrame and TradeFrame:IsShown() then RefreshTrade() end if TradeFrame and TradeFrame:IsShown() then RefreshTrade() end
if MailFrame and MailFrame:IsShown() then
RefreshMail()
RefreshSendMail()
end
if LootFrame and LootFrame:IsShown() then RefreshLoot() end if LootFrame and LootFrame:IsShown() then RefreshLoot() end
if TradeSkillFrame and TradeSkillFrame:IsShown() then RefreshTradeSkill() end if TradeSkillFrame and TradeSkillFrame:IsShown() then RefreshTradeSkill() end
if CraftFrame and CraftFrame:IsShown() then RefreshCraft() end if CraftFrame and CraftFrame:IsShown() then RefreshCraft() end
@@ -484,6 +649,7 @@ function Engine:Install()
if MailFrame then if MailFrame then
HookScript(MailFrame, "OnShow", function() HookScript(MailFrame, "OnShow", function()
ShaguTweaks.QueueFunction(RefreshMail) ShaguTweaks.QueueFunction(RefreshMail)
QueueSendMailRefresh()
end) end)
end end
@@ -492,12 +658,27 @@ function Engine:Install()
-- Blizzard has updated the reused mail buttons. -- Blizzard has updated the reused mail buttons.
hooksecurefunc("InboxFrame_Update", RefreshMail) hooksecurefunc("InboxFrame_Update", RefreshMail)
hooksecurefunc("OpenMail_Update", RefreshMail) hooksecurefunc("OpenMail_Update", RefreshMail)
HookAttachmentTextures()
HookAddonOrVariable("TurtleMail", HookAttachmentTextures)
hooksecurefunc("SendMailFrame_Update", QueueSendMailRefresh)
if SendMailFrame then
HookScript(SendMailFrame, "OnShow", QueueSendMailRefresh)
end
local trade = CreateFrame("Frame", nil, TradeFrame) -- Run after Blizzard updates the corresponding visible column, rather than
trade:RegisterEvent("TRADE_SHOW") -- racing TradeFrame's handler on the same TRADE_* event.
trade:RegisterEvent("TRADE_PLAYER_ITEM_CHANGED") hooksecurefunc("TradeFrame_UpdatePlayerItem", RefreshPlayerTradeSlot)
trade:RegisterEvent("TRADE_TARGET_ITEM_CHANGED") hooksecurefunc("TradeFrame_UpdateTargetItem", RefreshTargetTradeSlot)
trade:SetScript("OnEvent", RefreshTrade) -- TradeFrame_Update already calls both per-slot update functions for all
-- seven slots; hooking it too would repeat the entire rarity pass.
if TradeFrame then
HookScript(TradeFrame, "OnHide", function()
for i = 1, 7 do
PaintTradeSlot(_G["TradePlayerItem" .. i .. "ItemButton"], nil)
PaintTradeSlot(_G["TradeRecipientItem" .. i .. "ItemButton"], nil)
end
end)
end
HookAddonOrVariable("Blizzard_TradeSkillUI", function() HookAddonOrVariable("Blizzard_TradeSkillUI", function()
hooksecurefunc("TradeSkillFrame_Update", RefreshTradeSkill) hooksecurefunc("TradeSkillFrame_Update", RefreshTradeSkill)
+134
View File
@@ -0,0 +1,134 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Loot Quality"],
description = T["Colors the loot frame using the highest item quality in the current loot window."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Loot"],
enabled = nil,
})
local MIN_QUALITY = 2 -- uncommon / green
local function FindLootTitle()
local regions = { LootFrame:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
-- Some Vanilla/Turtle regions expose GetText without being FontStrings.
-- Only keep a region that can actually be recolored.
if region
and type(region.GetText) == "function"
and type(region.SetTextColor) == "function"
and region:GetText() == ITEMS then
return region
end
end
end
module.enable = function(self)
if not self.border then
self.border = LootFrame:CreateTexture(nil, "OVERLAY")
self.border:SetTexture("Interface\\LootFrame\\UI-LootPanel")
self.border:SetPoint("TOPLEFT", LootFrame, "TOPLEFT", -4, 3)
self.border:SetPoint("BOTTOMRIGHT", LootFrame, "BOTTOMRIGHT", 5, -3)
self.border:SetDrawLayer("ARTWORK")
self.highlight = LootFrame:CreateTexture(nil, "OVERLAY")
self.highlight:SetTexture("Interface\\LootFrame\\UI-LootPanel")
self.highlight:SetAllPoints(self.border)
self.highlight:SetDrawLayer("ARTWORK")
self.highlight:SetBlendMode("ADD")
end
self.titleRegion = self.titleRegion or FindLootTitle()
if self.titleRegion
and not self.titleColor
and type(self.titleRegion.GetTextColor) == "function" then
local r, g, b, a = self.titleRegion:GetTextColor()
self.titleColor = { r or 1, g or .82, b or 0, a or 1 }
end
local function RestoreTitle()
if not self.titleRegion or type(self.titleRegion.SetTextColor) ~= "function" then return end
local color = self.titleColor
if color then
self.titleRegion:SetTextColor(color[1], color[2], color[3], color[4])
else
self.titleRegion:SetTextColor(1, .82, 0)
end
end
local function ResetVisuals()
self.border:Hide()
self.highlight:Hide()
RestoreTitle()
end
local function UpdateQuality()
local highestQuality = 0
local count = GetNumLootItems() or 0
for i = 1, count do
if LootSlotIsItem(i) then
local _, _, _, quality = GetLootSlotInfo(i)
quality = tonumber(quality) or 0
if quality > highestQuality then
highestQuality = quality
end
end
end
local color = ITEM_QUALITY_COLORS and ITEM_QUALITY_COLORS[highestQuality]
if highestQuality < MIN_QUALITY or not color then
ResetVisuals()
return
end
-- Common (white) items get the Darkened/Golden UI border color instead
-- of white; every other quality keeps its color.
local r, g, b = color.r, color.g, color.b
local edge = ShaguTweaks.UIBorder
if edge and r > .9 and g > .9 and b > .9 then r, g, b = edge.r, edge.g, edge.b end
self.border:SetVertexColor(r, g, b)
self.highlight:SetVertexColor(r, g, b)
self.border:Show()
self.highlight:Show()
if self.titleRegion and type(self.titleRegion.SetTextColor) == "function" then
self.titleRegion:SetTextColor(color.r, color.g, color.b)
end
end
self.ResetVisuals = ResetVisuals
self.events = self.events or CreateFrame("Frame")
self.events:UnregisterAllEvents()
self.events:RegisterEvent("LOOT_OPENED")
self.events:RegisterEvent("LOOT_SLOT_CLEARED")
self.events:RegisterEvent("LOOT_CLOSED")
self.events:SetScript("OnEvent", function()
if event == "LOOT_CLOSED" then
ResetVisuals()
else
UpdateQuality()
end
end)
ResetVisuals()
end
module.disable = function(self)
if self.events then
self.events:UnregisterAllEvents()
end
if self.ResetVisuals then
self.ResetVisuals()
end
end
+400
View File
@@ -0,0 +1,400 @@
-- Loss of Control Alert
--
-- Shows what is stopping you (stun, fear, silence, school lockout, ...) as a
-- large icon with a countdown in the middle of the screen, like retail.
-- Everything comes from ClassicAPI's C_LossOfControl, which collects the
-- crowd-control debuffs on the player and the school lockout after an
-- interrupt, so nothing here reads the combat log or scans buffs.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Loss of Control Alert"],
description = T["Shows a large icon and countdown when you are stunned, feared, silenced, rooted, disarmed or locked out of a spell school. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."],
expansions = { ["vanilla"] = true },
category = T["Interface"],
enabled = false,
})
local ICON = 56
local WIDTH = 300
local BAR_WIDTH = WIDTH - ICON - 30
local THROTTLE = .05
local FALLBACK_ICON = "Interface\\Icons\\Ability_Kick"
local POSITION_KEY = "LossOfControlPosition"
-- Most disabling first. Anything not listed ranks below the list.
local PRIORITY = {
"STUN", "FEAR", "CONFUSE", "CHARM", "POSSESS", "PACIFYSILENCE",
"SILENCE", "PACIFY", "DISARM", "ROOT", "SCHOOL_INTERRUPT",
}
local LABELS = {
STUN = T["Stunned"], FEAR = T["Feared"], CONFUSE = T["Confused"],
CHARM = T["Charmed"], POSSESS = T["Possessed"], SILENCE = T["Silenced"],
PACIFY = T["Pacified"], PACIFYSILENCE = T["Pacified and Silenced"],
DISARM = T["Disarmed"], ROOT = T["Rooted"], SCHOOL_INTERRUPT = T["Interrupted"],
}
local RANK = {}
for i = 1, table.getn(PRIORITY) do RANK[PRIORITY[i]] = i end
-- Border/bar color per kind of effect: hard control red, silences purple,
-- the rest orange, school lockouts blue.
local COLORS = {
STUN = { 1, .2, .2 }, FEAR = { 1, .2, .2 }, CONFUSE = { 1, .2, .2 },
CHARM = { 1, .2, .2 }, POSSESS = { 1, .2, .2 },
SILENCE = { .7, .4, 1 }, PACIFYSILENCE = { .7, .4, 1 }, PACIFY = { .7, .4, 1 },
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 }
local Refresh
local function StopTiming()
if S.expiry then S.expiry:Cancel(); S.expiry = nil end
if S.schoolWake then S.schoolWake:Cancel(); S.schoolWake = nil end
if S.binding then S.binding:Disable(); S.frame.bar:SetMinMaxValues(0, 1) end
end
local function Supported()
local c = _G.C_LossOfControl
return type(c) == "table"
and type(c.GetActiveLossOfControlDataCount) == "function"
and type(c.GetActiveLossOfControlData) == "function"
end
local function FormatTime(seconds)
if seconds >= 10 then return string.format("%d", math.floor(seconds + .5)) end
return string.format("%.1f", seconds)
end
-- The effect to show: highest priority, the longest one among equals.
-- Returns nil when nothing is active.
local function PickEffect()
local best, bestRank, bestLeft
local count = _G.C_LossOfControl.GetActiveLossOfControlDataCount() or 0
for i = 1, count do
local data = _G.C_LossOfControl.GetActiveLossOfControlData(i)
if data and not (data.duration and data.duration > 0
and data.timeRemaining and data.timeRemaining <= 0) then
local rank = RANK[data.locType] or (table.getn(PRIORITY) + 1)
local left = data.timeRemaining or 0
if not best or rank < bestRank or (rank == bestRank and left > bestLeft) then
best, bestRank, bestLeft = data, rank, left
end
end
end
return best
end
local function SchoolName(data)
if data.locType ~= "SCHOOL_INTERRUPT" then return nil end
local spell = _G.C_Spell
if type(spell) == "table" and type(spell.GetSchoolString) == "function" and data.lockoutSchool and data.lockoutSchool ~= 0 then
return spell.GetSchoolString(data.lockoutSchool)
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
if S.themeR ~= r or S.themeG ~= g or S.themeB ~= b or S.themeA ~= a then
S.themeR, S.themeG, S.themeB, S.themeA = r, g, b, a
f:SetBackdropBorderColor(r, g, b, a)
f.border:SetBackdropBorderColor(r, g, b, a)
f.icon:SetVertexColor(1, 1, 1, 1)
end
end
local function SetBarColor(r, g, b)
if S.native then S.frame.bar:SetStatusBarColor(r, g, b)
else S.frame.bar:SetVertexColor(r, g, b) end
end
local function Apply(data)
local f = S.frame
local color = COLORS[data.locType] or DEFAULT_COLOR
local icon = (data.iconTexture and data.iconTexture ~= "") and data.iconTexture or FALLBACK_ICON
if S.icon ~= icon then S.icon = icon; f.icon:SetTexture(icon) end
ApplyTheme()
if S.effectColor ~= color then S.effectColor = color; SetBarColor(color[1], color[2], color[3]) end
local title = LABELS[data.locType] or data.displayText or ""
if S.title ~= title then S.title = title; f.title:SetText(title) end
-- spell name under the label; school lockouts name the locked school
local detail = SchoolName(data)
if not detail and data.locType ~= "SCHOOL_INTERRUPT" then detail = data.displayText end
detail = detail or ""
if S.detail ~= detail then S.detail = detail; f.detail:SetText(detail) end
f.data = data
end
local function UpdateTime()
local f = S.frame
local data = f.data
if not data then return end
local left = data.timeRemaining
if left and left > 0 then
f.time:SetText(FormatTime(left))
local duration = data.duration
if duration and duration > 0 then
f.bar:SetWidth(math.max(1, BAR_WIDTH * math.min(left / duration, 1)))
f.bar:Show()
else
f.bar:Hide()
end
else
f.time:SetText("")
f.bar:Hide()
end
end
-- Re-select on lifecycle events and timed expiry. Native school lockouts also
-- check scalar state for silent extensions; older builds retain their poller.
Refresh = function()
local f = S.frame
if S.unlocked then return end
local data = PickEffect()
if data then
Apply(data)
if S.native then
StopTiming()
local left, total = data.timeRemaining, data.duration
if left and left > 0 then
S.duration:SetTimeFromEnd(GetTime() + left, total and total > 0 and total or left)
S.binding:SetDuration(S.duration)
S.binding:Enable()
S.expiry = _G.C_Timer.NewTimer(left, S.expire)
if total and total > 0 then
f.bar:SetTimerDuration(S.duration, _G.Enum.StatusBarInterpolation.Immediate,
_G.Enum.StatusBarTimerDirection.RemainingTime)
f.bar:Show()
else f.bar:Hide() end
else
f.time:SetText("")
f.bar:Hide()
end
if data.locType == "SCHOOL_INTERRUPT" then
if type(_G.C_LossOfControl.GetSchoolLockout) == "function" then
local mask, remaining = _G.C_LossOfControl.GetSchoolLockout()
S.schoolMask, S.schoolEnd = mask, GetTime() + (remaining or 0)
end
S.schoolWake = _G.C_Timer.NewTimer(.2, S.checkSchool)
end
else UpdateTime() end
if not f:IsShown() then f:Show() end
else
StopTiming()
f.data = nil
f:Hide()
end
end
local function OnUpdate()
S.elapsed = S.elapsed + (arg1 or 0)
if S.elapsed < THROTTLE then return end
S.elapsed = 0
Refresh()
end
local function RestorePosition()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
S.frame:ClearAllPoints()
if pos and pos[1] and pos[2] then
S.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
else
S.frame:SetPoint("CENTER", UIParent, "CENTER", 0, 160)
end
end
local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
-- Hold Ctrl+Shift+Alt to show a preview and drag the alert.
local function SetUnlocked(on)
if S.unlocked == on then return end
local f = S.frame
S.unlocked = on
f:EnableMouse(on)
if on then
StopTiming()
S.icon, S.effectColor, S.title, S.detail = nil, nil, nil, nil
f.data = nil
f.icon:SetTexture(FALLBACK_ICON)
ApplyTheme()
SetBarColor(1, .2, .2)
if S.native then f.bar:SetValue(1) end
f.bar:SetWidth(BAR_WIDTH)
f.bar:Show()
f.title:SetText(T["Stunned"])
f.detail:SetText(T["Drag to move, right-click to reset"])
f.time:SetText("8.0")
f:Show()
else
f:StopMovingOrSizing()
f:Hide()
Refresh()
end
end
local function CreateAlert()
if S.frame then return end
local f = CreateFrame("Frame", "ShaguTweaksLossOfControl", UIParent)
f:SetWidth(WIDTH)
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)
f:EnableMouse(false)
f:Hide()
S.frame = f
f.icon = f:CreateTexture(nil, "ARTWORK")
f.icon:SetWidth(ICON)
f.icon:SetHeight(ICON)
f.icon:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -10)
f.icon:SetTexCoord(.08, .92, .08, .92)
-- 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:SetFontObject(GameFontNormalLarge)
f.title:SetPoint("TOPLEFT", f.icon, "TOPRIGHT", 10, -2)
f.title:SetJustifyH("LEFT")
f.title:SetWidth(WIDTH - ICON - 80)
f.detail = f:CreateFontString(nil, "OVERLAY")
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", 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
S.native = API.durationtimers and _G.Enum.StatusBarTimerDirection.RemainingTime ~= nil
if S.native then
f.bar = CreateFrame("StatusBar", nil, f)
if type(f.bar.SetTimerDuration) ~= "function" then S.native = false; f.bar:Hide() end
end
if S.native then
f.bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
f.bar:SetWidth(BAR_WIDTH)
S.duration = _G.C_DurationUtil.CreateDuration()
local formatter = _G.C_StringUtil.CreateNumericRuleFormatter()
formatter:SetBreakpoints({{ threshold = 0, format = "%.1f" },
{ threshold = 10, step = 1, format = "%d" }})
S.binding = _G.C_DurationUtil.CreateDurationTextBinding()
S.binding:SetFontString(f.time)
S.binding:SetFormatter(formatter)
S.binding:SetExpiredText("")
S.binding:SetUpdateInterval(.05)
S.expire = function() S.expiry = nil; Refresh() end
-- Re-locking an active school can extend it without any native event.
-- Check cheap scalar state only while a school lockout is displayed.
S.checkSchool = function()
S.schoolWake = nil
if S.unlocked or not f.data then return end
local query = _G.C_LossOfControl.GetSchoolLockout
if type(query) ~= "function" then Refresh(); return end
local mask, remaining = query()
local finish = GetTime() + (remaining or 0)
if mask ~= S.schoolMask or math.abs(finish - S.schoolEnd) > .05 then
Refresh()
else S.schoolWake = _G.C_Timer.NewTimer(.2, S.checkSchool) end
end
else
f.bar = f:CreateTexture(nil, "ARTWORK")
f.bar:SetTexture("Interface\\TargetingFrame\\UI-StatusBar")
end
f.bar:SetHeight(4)
f.bar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", ICON + 20, 10)
f:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then this:StartMoving() end
end)
f:SetScript("OnMouseUp", function()
this:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
ShaguTweaks_config[POSITION_KEY] = nil
else
ShaguTweaks_config[POSITION_KEY] = { this:GetLeft(), this:GetBottom() }
end
RestorePosition()
end)
-- Only older builds need a Lua animation/selection loop while shown.
if not S.native then f:SetScript("OnUpdate", OnUpdate) end
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetUnlocked(ModifiersDown())
else
Refresh()
end
end)
ApplyTheme()
RestorePosition()
end
module.enable = function(self)
if not Supported() then return end
CreateAlert()
S.events:RegisterEvent("LOSS_OF_CONTROL_ADDED")
S.events:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
SLASH_SHAGULOSSOFCONTROL1 = "/lossofcontrol"
SlashCmdList["SHAGULOSSOFCONTROL"] = function(msg)
msg = string.lower(msg or "")
if msg == "reset" then
ShaguTweaks_config = ShaguTweaks_config or {}
ShaguTweaks_config[POSITION_KEY] = nil
RestorePosition()
end
end
Refresh()
end
module.disable = function(self)
if not S.frame then return end
if S.unlocked then SetUnlocked(false) end
S.events:UnregisterAllEvents()
StopTiming()
S.frame:Hide()
end
+243
View File
@@ -0,0 +1,243 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local libspell = ShaguTweaks.libspell
local module = ShaguTweaks:register({
title = T["Macro Icons"],
description = T["Adds legacy --showtooltip and /pfcast icon hints. Current ClassicAPI handles ordinary macro icons and preserves manually chosen icons."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Macro"],
enabled = true,
})
local function EnableNativeIcons(self)
if not API.macrodisplay then return false end
-- Another macro parser owns its custom syntax and display decisions.
if _G.CleveRoids or (type(_G.IsAddOnLoaded) == "function"
and _G.IsAddOnLoaded("SuperCleveRoidMacros")) then return true end
local entries, owned = {}, {}
local driver = CreateFrame("Frame", "ShaguTweaksMacroIcons", UIParent)
self.nativeDriver = driver
local ticker
local customEvents = { "MODIFIER_STATE_CHANGED", "PLAYER_TARGET_CHANGED",
"UPDATE_MOUSEOVER_UNIT", "PLAYER_REGEN_DISABLED", "PLAYER_REGEN_ENABLED" }
local function Release()
if ticker then ticker:Cancel(); ticker = nil end
for id in pairs(owned) do _G.C_Macro.SetMacroDisplay(id, nil); owned[id] = nil end
entries = {}
end
local function Refresh()
for id, entry in pairs(entries) do
local value, unit = entry.value, nil
if entry.conditional and type(_G.SecureCmdOptionParse) == "function" then
value, unit = _G.SecureCmdOptionParse(value, true)
value = value or false
end
if not entry.published or entry.lastValue ~= value or entry.lastUnit ~= unit then
local resolved = _G.C_Macro.SetMacroDisplay(id, value, unit)
entry.lastValue, entry.lastUnit = value, unit
entry.published = value == false or resolved
end
end
end
local function Rebuild()
if driver.rebuilding then return end
driver.rebuilding = true
Release()
local maximum = math.max(_G.MAX_MACROS or 36,
(_G.MAX_ACCOUNT_MACROS or 0) + (_G.MAX_CHARACTER_MACROS or 0))
local conditional = false
for id = 1, maximum do
local name, _, body = GetMacroInfo(id)
if name and body then
local explicit, hint, pfcast
for line in string.gfind(body, "[^\n]+") do
local _, _, directive = string.find(line, "^%s*(#[^%s]+)")
if directive == "#showtooltip" or directive == "#show" then explicit = true end
local _, _, value = string.find(line, "%-%-showtooltip%s+(.+)")
hint = hint or value
if not pfcast then _, _, pfcast = string.find(line, "^%s*/pfcast%s+(.+)") end
end
-- Native directives always win. Normal macros need no Lua icon writes,
-- action-bar scans or spellbook lookups at all.
local value = not explicit and (hint or pfcast)
if value then
value = string.gsub(value, "%s+$", "")
local hasConditions = string.find(value, "[", 1, true) ~= nil
entries[id] = { value = value, conditional = hasConditions }
owned[id] = true
conditional = conditional or hasConditions
end
end
end
Refresh()
-- Only conditional custom hints need periodic re-evaluation (e.g. stance
-- or cooldown conditions). Ordinary native macros have no Lua poller.
if conditional and _G.C_Timer and type(_G.C_Timer.NewTicker) == "function" then
ticker = _G.C_Timer.NewTicker(.2, Refresh)
end
for _, name in ipairs(customEvents) do
if conditional then driver:RegisterEvent(name) else driver:UnregisterEvent(name) end
end
if next(entries) then
driver:RegisterEvent("SPELLS_CHANGED")
if _G.C_EventUtils and type(_G.C_EventUtils.IsEventValid) == "function"
and _G.C_EventUtils.IsEventValid("GET_ITEM_INFO_RECEIVED") then
driver:RegisterEvent("GET_ITEM_INFO_RECEIVED")
end
else
driver:UnregisterEvent("SPELLS_CHANGED")
driver:UnregisterEvent("GET_ITEM_INFO_RECEIVED")
end
driver.rebuilding = nil
end
driver:RegisterEvent("PLAYER_ENTERING_WORLD")
driver:RegisterEvent("UPDATE_MACROS")
driver:RegisterEvent("PLAYER_LOGOUT")
driver:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" or event == "UPDATE_MACROS" then Rebuild()
elseif event == "PLAYER_LOGOUT" then driver:UnregisterAllEvents(); Release()
elseif event == "SPELLS_CHANGED" then
for _, entry in pairs(entries) do entry.published = nil end
Refresh()
else Refresh() end
end)
Rebuild()
return true
end
module.enable = function(self)
if self.installed then return end
self.installed = true
if EnableNativeIcons(self) then return end
local gfind = string.gmatch or string.gfind
local function BuildMacroCache()
local cache = {}
for macroSlot = 1, 36 do
local name, _, body = GetMacroInfo(macroSlot)
if name and body then
cache[name] = { id = macroSlot, body = body }
end
end
return cache
end
local function GetMacroData(actionSlot, cache)
local actionType, id = API.GetActionInfo(actionSlot)
if actionType == "macro" and id then
local _, _, body = GetMacroInfo(id)
if body then return id, body end
end
local macroName = GetActionText(actionSlot)
local entry = macroName and cache[macroName]
if entry then return entry.id, entry.body end
end
local function ParseSpellText(match)
if not match then return end
local _, _, spell, rank = string.find(match, "(.+)%((.+)%)")
return spell or match, rank
end
local function ResolveSpell(body, macroSlot)
if not body then return end
local customTooltip
local fallback
for line in gfind(body, "[^%\n]+") do
-- Explicit #showtooltip always wins.
local _, _, match = string.find(line, "^#showtooltip%s+(.+)")
if match then
return ParseSpellText(match)
end
-- Preserve the historical ShaguTweaks custom override.
if not customTooltip then
_, _, customTooltip = string.find(line, "%-%-showtooltip%s+(.+)")
end
-- Keep text parsing as a fallback for old/stale macro caches and /pfcast.
if not fallback then
_, _, fallback = string.find(line, "^/cast%s+(.+)")
end
if not fallback then
_, _, fallback = string.find(line, "^/pfcast%s+(.+)")
end
if not fallback then
_, _, fallback = string.find(line, "CastSpellByName%(%\"(.+)%\"%)")
end
end
if customTooltip then
return ParseSpellText(customTooltip)
end
-- ClassicAPI already resolves and caches the macro's primary spell.
-- This is the correct modern behavior for a bare "#showtooltip" line and
-- also covers /castsequence, CastSpellByName and CastSpellNoToggle.
if macroSlot then
local spell, rank = API.GetMacroSpell(macroSlot)
if spell then
return spell, rank ~= "" and rank or nil
end
end
return ParseSpellText(fallback)
end
local function ButtonMacroScan(bar, macroCache)
if not bar:IsVisible() then return end
local prefix = bar:GetName()
prefix = bar == MainMenuBar and "Action" or prefix
prefix = bar == BonusActionBarFrame and "BonusAction" or prefix
for index = 1, 12 do
local button = _G[prefix.."Button"..index]
local icon = _G[prefix.."Button"..index.."Icon"]
if not button then break end
local actionSlot = ActionButton_GetPagedID(button)
local texture = GetActionTexture(actionSlot)
button.spellslot, button.booktype = nil, nil
local macroSlot, body = GetMacroData(actionSlot, macroCache)
local spell, rank = ResolveSpell(body, macroSlot)
if spell then
button.spellslot, button.booktype = libspell.GetSpellIndex(spell, rank)
if button.spellslot and button.booktype then
texture = GetSpellTexture(button.spellslot, button.booktype) or texture
end
end
if icon and texture and texture ~= icon:GetTexture() then
icon:SetTexture(texture)
end
end
end
local bars = {
MainMenuBar, BonusActionBarFrame, MultiBarRight, MultiBarLeft,
MultiBarBottomRight, MultiBarBottomLeft
}
local macroicons = CreateFrame("Frame", "ShaguTweaksMacroIcons", UIParent)
macroicons:RegisterEvent("PLAYER_ENTERING_WORLD")
macroicons:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
macroicons:RegisterEvent("ACTIONBAR_PAGE_CHANGED")
macroicons:RegisterEvent("UPDATE_BONUS_ACTIONBAR")
macroicons:RegisterEvent("ACTIONBAR_SHOWGRID")
macroicons:RegisterEvent("UPDATE_MACROS")
macroicons:SetScript("OnEvent", function()
local macroCache = BuildMacroCache()
for _, bar in ipairs(bars) do
ButtonMacroScan(bar, macroCache)
end
end)
end
+171
View File
@@ -0,0 +1,171 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Macro Tweaks"],
description = T["Adds missing interactive /equip, /use, attack and focus aliases using ClassicAPI's native parser when available, and keeps executed macro commands out of chat history."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
maintainer = "@shagu (GitHub)",
category = T["Macro"],
enabled = true,
})
module.enable = function(self)
if self.installed then return end
self.installed = true
if not API.nativemacrocommands then
-- Only older APIs need interception; current ClassicAPI owns metadata.
local hookSendChatMessage = _G.SendChatMessage
local function IsShowTooltipMetadata(msg)
if not msg then return false end
local line = string.gsub(msg, "^%s+", "")
line = string.gsub(line, "%s+$", "")
return line == "#showtooltip"
or string.find(line, "^#showtooltip%s+") ~= nil
end
function _G.SendChatMessage(msg, a1, a2, a3, a4, a5, a6, a7, a8)
if IsShowTooltipMetadata(msg) then return end
return hookSendChatMessage(msg, a1, a2, a3, a4, a5, a6, a7, a8)
end
end
-- do not write executed macro commands into chat input history
if not ChatFrameEditBox._AddHistoryLine then
local userinput
ChatFrameEditBox._AddHistoryLine = ChatFrameEditBox.AddHistoryLine
ChatFrameEditBox.AddHistoryLine = function(self, text)
if not userinput and text then
if string.find(text, "^/run%s*") then return end
if string.find(text, "^/script%s*") then return end
if string.find(text, "^/cast%s*") then return end
if string.find(text, "^/startattack%s*") then return end
if string.find(text, "^/stopattack%s*") then return end
if string.find(text, "^/focus%s*") then return end
if string.find(text, "^/clearfocus%s*") then return end
end
ChatFrameEditBox._AddHistoryLine(self, text)
end
local OnEnter = ChatFrameEditBox:GetScript("OnEnterPressed")
ChatFrameEditBox:SetScript("OnEnterPressed", function(a1,a2,a3,a4)
userinput = true
if OnEnter then OnEnter(a1,a2,a3,a4) end
userinput = nil
end)
end
local function GetBagItemName(bag, slot)
local itemID = API.GetContainerItemID(bag, slot)
local name = itemID and API.GetItemNameByID(itemID)
if name then return name end
local itemLink = API.GetContainerItemLink(bag, slot)
local linkedID = itemLink and API.GetItemIDFromLink(itemLink)
local itemName = linkedID and API.GetItemInfo(linkedID)
return itemName
end
local function FindItem(item)
local wanted = string.lower(item or "")
for bag = 4, 0, -1 do
for slot = 1, GetContainerNumSlots(bag) do
local query = GetBagItemName(bag, slot)
if query and string.lower(query) == wanted then
return bag, slot
end
end
end
end
-- ClassicAPI exposes several modern macro primitives as Lua functions but
-- Vanilla does not register their familiar slash-command equivalents.
--
-- Use ShaguTweaks-specific command IDs instead of generic STARTATTACK/FOCUS
-- keys. A SlashCmdList key can exist without the corresponding /command
-- alias, so testing only SlashCmdList.STARTATTACK can silently skip a valid
-- registration and leave Vanilla reporting "Type '/help'...".
local function TrimUnit(msg)
if not msg then return nil end
local _, _, unit = string.find(msg, "^%s*(.-)%s*$")
return unit ~= "" and unit or nil
end
local function IsSlashRegistered(command)
command = string.lower(command)
for key, value in pairs(_G) do
if type(key) == "string"
and string.find(key, "^SLASH_")
and type(value) == "string"
and string.lower(value) == command then
return true
end
end
return false
end
local function RegisterSlashAlias(id, command, handler)
if IsSlashRegistered(command) then return end
_G["SLASH_" .. id .. "1"] = command
if API.nativemacrocommands then
-- These commands are native macro primitives even if no interactive
-- chat alias exists. Preserve conditions, item links and native targeting.
_G.SlashCmdList[id] = function(msg)
_G.RunMacroText(command .. " " .. (msg or ""))
end
else
_G.SlashCmdList[id] = handler
end
end
RegisterSlashAlias("SHAGUTWEAKS_STARTATTACK", "/startattack", function(msg)
API.StartAttack(TrimUnit(msg))
end)
RegisterSlashAlias("SHAGUTWEAKS_STOPATTACK", "/stopattack", function()
API.StopAttack()
end)
RegisterSlashAlias("SHAGUTWEAKS_FOCUS", "/focus", function(msg)
API.FocusUnit(TrimUnit(msg))
end)
RegisterSlashAlias("SHAGUTWEAKS_CLEARFOCUS", "/clearfocus", function()
API.ClearFocus()
end)
local function EquipOrUse(msg)
if not msg or msg == "" then return end
local bag, slot
local _, _, parsedBag, parsedSlot = string.find(msg, "^(%d+)%s+(%d+)$")
if parsedBag and parsedSlot then
bag, slot = parsedBag, parsedSlot
else
local _, _, inventorySlot = string.find(msg, "^(%d+)$")
if inventorySlot then
slot = inventorySlot
else
bag, slot = FindItem(msg)
end
end
bag = bag and tonumber(bag) or nil
slot = slot and tonumber(slot) or nil
if bag and slot then
UseContainerItem(bag, slot)
elseif slot then
UseInventoryItem(slot)
end
end
-- Keep the original Macro Tweaks conveniences, but do not steal /equip or
-- /use from another addon that already registered either command.
RegisterSlashAlias("SHAGUTWEAKS_EQUIP", "/equip", EquipOrUse)
RegisterSlashAlias("SHAGUTWEAKS_USE", "/use", EquipOrUse)
end
+78
View File
@@ -0,0 +1,78 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local HookScript = ShaguTweaks.HookScript
local module = ShaguTweaks:register({
title = T["Metric Range"],
description = T["Displays tooltip range units in metres instead of yards."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Tooltip & Items"],
enabled = true,
})
module.enable = function(self)
-- Avoid double processing while the old standalone addon is still enabled.
if IsAddOnLoaded("MetricRange") then return end
local function ConvertText(text)
if type(text) ~= "string" then return text end
-- Intentionally keep the original behavior: only replace the displayed
-- unit label. Do not numerically convert the range value.
text = string.gsub(text, " yards", " metres")
text = string.gsub(text, " yard", " metre")
text = string.gsub(text, " yd", " m")
return text
end
local function ConvertTooltip(tooltip)
if not tooltip or not tooltip.GetName then return end
local name = tooltip:GetName()
if not name then return end
-- GameTooltip-style frames expose NumLines() in Vanilla. Only inspect the
-- lines that actually exist instead of probing 30 left/right font strings
-- on every tooltip show. Keep the historical 30-line ceiling as fallback.
local lines = 30
if tooltip.NumLines then
lines = math.min(tooltip:NumLines() or 0, 30)
end
for i = 1, lines do
local left = _G[name .. "TextLeft" .. i]
if left then
local old = left:GetText()
if old then
local new = ConvertText(old)
if new ~= old then left:SetText(new) end
end
end
local right = _G[name .. "TextRight" .. i]
if right then
local old = right:GetText()
if old then
local new = ConvertText(old)
if new ~= old then right:SetText(new) end
end
end
end
end
local function HookTooltip(tooltip)
if not tooltip then return end
HookScript(tooltip, "OnShow", function()
ConvertTooltip(tooltip)
end)
end
HookTooltip(GameTooltip)
HookTooltip(ItemRefTooltip)
HookTooltip(ShoppingTooltip1)
HookTooltip(ShoppingTooltip2)
HookTooltip(ShoppingTooltip3)
if GameTooltip and GameTooltip:IsVisible() then ConvertTooltip(GameTooltip) end
if ItemRefTooltip and ItemRefTooltip:IsVisible() then ConvertTooltip(ItemRefTooltip) end
end
+12 -4
View File
@@ -35,7 +35,8 @@ local ignoreList = {
"GameTimeFrame", "GameTimeFrame",
"MinimapToggleButton", "MinimapToggleButton",
"TWMinimapShopFrame", "TWMinimapShopFrame",
"LFT_Minimap" "LFT_Minimap",
"LFTMinimapButton",
} }
local function ShouldIgnore(name) local function ShouldIgnore(name)
@@ -99,6 +100,7 @@ local function RefreshButtonCache()
buttonCache[table.getn(buttonCache) + 1] = child buttonCache[table.getn(buttonCache) + 1] = child
end end
end end
return true
end end
local function SetButtonsAlpha(alpha) local function SetButtonsAlpha(alpha)
@@ -128,15 +130,20 @@ module.enable = function(self)
self.frame = self.frame or CreateFrame("Frame") self.frame = self.frame or CreateFrame("Frame")
-- Prime the cache once. Future scans only happen when the minimap child count -- Addons can replace a child without changing the final child count.
-- changes, e.g. when an addon creates a button after login. self.frame:RegisterEvent("ADDON_LOADED")
RefreshButtonCache() self.frame:SetScript("OnEvent", function() cachedChildCount = -1 end)
SetButtonsAlpha(1)
self.frame:SetScript("OnUpdate", function() self.frame:SetScript("OnUpdate", function()
self.elapsed = self.elapsed + arg1 self.elapsed = self.elapsed + arg1
if self.elapsed < THROTTLE then return end if self.elapsed < THROTTLE then return end
self.elapsed = 0 self.elapsed = 0
-- Only the native child count is queried while the cache is unchanged.
-- Newly discovered buttons inherit the current state even while hidden.
if RefreshButtonCache() then SetButtonsAlpha(self.isShown and 1 or 0) end
local isOver = API.IsMouseOver(Minimap, MARGIN, -MARGIN, -MARGIN, MARGIN) local isOver = API.IsMouseOver(Minimap, MARGIN, -MARGIN, -MARGIN, MARGIN)
if isOver then if isOver then
@@ -161,6 +168,7 @@ end
module.disable = function(self) module.disable = function(self)
if self.frame then if self.frame then
self.frame:SetScript("OnUpdate", nil) self.frame:SetScript("OnUpdate", nil)
self.frame:UnregisterEvent("ADDON_LOADED")
end end
-- Restore all detected addon buttons when the module is disabled. -- Restore all detected addon buttons when the module is disabled.
+162
View File
@@ -0,0 +1,162 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local floor = math.floor
local module = ShaguTweaks:register({
title = T["MiniMap Framerate & Latency"],
description = T["Adds a small framerate & latency display to the mini map."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Minimap & World Map"],
enabled = nil,
})
local TARGET_FPS = 60
local LOW_LATENCY = tonumber(_G.PERFORMANCEBAR_LOW_LATENCY) or 300
local MEDIUM_LATENCY = tonumber(_G.PERFORMANCEBAR_MEDIUM_LATENCY) or 600
local function CreateStatusFrame(width)
local frame = CreateFrame("Frame", nil, Minimap)
frame:SetFrameLevel(64)
frame:SetWidth(width)
frame:SetHeight(23)
frame:SetClampedToScreen(true)
frame:EnableMouse(true)
frame:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 8,
edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
frame:SetBackdropBorderColor(.9, .8, .5, 1)
frame:SetBackdropColor(.4, .4, .4, 1)
frame.text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
frame.text:SetFont(STANDARD_TEXT_FONT, 12, "OUTLINE")
frame.text:SetAllPoints(frame)
frame.text:SetFontObject(GameFontWhite)
return frame
end
local function SetFPSColor(text, fps)
local perc = fps / TARGET_FPS
if perc < 0 then perc = 0 elseif perc > 1 then perc = 1 end
local r, g
if perc <= .5 then
r = 1
g = perc * 2
else
r = (1 - perc) * 2
g = 1
end
text:SetTextColor(r, g, 0, 1)
end
local function SetLatencyColor(text, latency)
if latency > MEDIUM_LATENCY then
text:SetTextColor(1, 0, 0, 1)
elseif latency > LOW_LATENCY then
text:SetTextColor(1, 1, 0, 1)
else
text:SetTextColor(0, 1, 0, 1)
end
end
module.enable = function(self)
if self.ticker then
self.ticker:Cancel()
self.ticker = nil
end
-- Extras is ClassicAPI-first. Do not restore the original permanent
-- OnUpdate implementation when the native scheduler is unavailable.
if type(_G.C_Timer) ~= "table" or type(_G.C_Timer.NewTicker) ~= "function" then
return
end
if not self.fpsFrame then
self.fpsFrame = CreateStatusFrame(49)
self.latencyFrame = CreateStatusFrame(51)
end
local fpsFrame = self.fpsFrame
local latencyFrame = self.latencyFrame
local lowFPS, highFPS
local lowMS, highMS
-- Follow the existing ShaguTweaks MiniMap Clock instead of independently
-- anchoring to MinimapCluster. MiniMap Square and mover integrations already
-- reposition that clock, so these counters follow automatically regardless
-- of module initialization order.
fpsFrame:ClearAllPoints()
latencyFrame:ClearAllPoints()
if _G.MinimapClock then
fpsFrame:SetPoint("RIGHT", _G.MinimapClock, "LEFT", 2, 0)
latencyFrame:SetPoint("LEFT", _G.MinimapClock, "RIGHT", -2, 0)
else
fpsFrame:SetPoint("BOTTOM", Minimap, "BOTTOM", -28, -25)
latencyFrame:SetPoint("BOTTOM", Minimap, "BOTTOM", 28, -25)
end
local function Update()
local fps = floor(GetFramerate() or 0)
local _, _, latency = GetNetStats()
latency = floor(latency or 0)
if fps > 0 and (not lowFPS or fps < lowFPS) then lowFPS = fps end
if not highFPS or fps > highFPS then highFPS = fps end
if latency > 0 and (not lowMS or latency < lowMS) then lowMS = latency end
if not highMS or latency > highMS then highMS = latency end
fpsFrame.text:SetText(tostring(fps))
latencyFrame.text:SetText(tostring(latency))
SetFPSColor(fpsFrame.text, fps)
SetLatencyColor(latencyFrame.text, latency)
end
fpsFrame:SetScript("OnEnter", function()
GameTooltip:ClearLines()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:AddLine(T["Framerate"])
GameTooltip:AddDoubleLine(T["High"], tostring(highFPS or 0) .. " " .. T["fps"], 1,1,1,1,1,1)
GameTooltip:AddDoubleLine(T["Low"], tostring(lowFPS or 0) .. " " .. T["fps"], 1,1,1,1,1,1)
GameTooltip:Show()
end)
latencyFrame:SetScript("OnEnter", function()
GameTooltip:ClearLines()
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMLEFT")
GameTooltip:AddLine(T["Latency"])
GameTooltip:AddDoubleLine(T["High"], tostring(highMS or 0) .. " " .. T["ms"], 1,1,1,1,1,1)
GameTooltip:AddDoubleLine(T["Low"], tostring(lowMS or 0) .. " " .. T["ms"], 1,1,1,1,1,1)
GameTooltip:Show()
end)
fpsFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
latencyFrame:SetScript("OnLeave", function() GameTooltip:Hide() end)
fpsFrame:Show()
latencyFrame:Show()
Update()
-- One native ClassicAPI ticker services both displays. Lua only wakes once
-- per second instead of running two permanent OnUpdate handlers every frame.
self.ticker = _G.C_Timer.NewTicker(1, Update)
end
module.disable = function(self)
if self.ticker then
self.ticker:Cancel()
self.ticker = nil
end
if self.fpsFrame then self.fpsFrame:Hide() end
if self.latencyFrame then self.latencyFrame:Hide() end
end
+27
View File
@@ -1,4 +1,30 @@
local T = ShaguTweaks.T local T = ShaguTweaks.T
local _G = ShaguTweaks.GetGlobalEnv()
local function FixLFTButtonDrag()
local button = _G.LFTMinimapButton
if not button or button.stEnlargedDragFix then return end
local stop = button:GetScript("OnDragStop")
if type(stop) ~= "function" then return end
button.stEnlargedDragFix = true
button:SetScript("OnDragStop", function(...)
local parent = button:GetParent()
local left, top = button:GetLeft(), button:GetTop()
local scale = button:GetScale()
stop(unpack(arg))
-- Turtle's handler detaches to UIParent without converting coordinates or
-- inherited scale. Keep the native parent/local scale (also across reloads)
-- and preserve the dropped screen position. Native reset remains untouched.
if parent == Minimap and button:GetParent() == UIParent then
button:SetParent(parent)
button:SetScale(scale)
if left and top then
button:ClearAllPoints()
button:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", left, top)
end
end
end)
end
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Enlarged Minimap"], title = T["Enlarged Minimap"],
@@ -121,6 +147,7 @@ module.enable = function(self)
end end
MinimapCluster:SetScale(scale) MinimapCluster:SetScale(scale)
FixLFTButtonDrag()
ShaguTweaks.ScheduleMinimapClamp() ShaguTweaks.ScheduleMinimapClamp()
-- BuffFrame is anchored to UIParent TOPRIGHT/TOPRIGHT x=-205 y=-13 by vanilla. -- BuffFrame is anchored to UIParent TOPRIGHT/TOPRIGHT x=-205 y=-13 by vanilla.
+1 -1
View File
@@ -631,7 +631,7 @@ module.enable = function(self)
end end
local function UpdateLockState() local function UpdateLockState()
if API.IsShiftKeyDown() and API.IsControlKeyDown() and API.IsAltKeyDown() then if API.AreMoveModifiersDown() then
UnlockAll() UnlockAll()
else else
LockAll() LockAll()
-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
+126
View File
@@ -0,0 +1,126 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Show Bags"],
description = T["Shows bag and keyring buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the bag bar."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
maintainer = "@shagu (GitHub)",
enabled = nil,
config = {
["panelbag.scale"] = 1,
}
})
module.enable = function(self)
-- The ShaguTweaks loader initializes module config while iterating an
-- unordered table. On a fresh install the Reduced Actionbar key may not have
-- been visited yet, so fall back to that module's declared default.
local reducedTitle = T["Reduced Actionbar Size"]
local reducedEnabled = ShaguTweaks_config and ShaguTweaks_config[reducedTitle]
if reducedEnabled == nil then
local reducedModule = ShaguTweaks.mods and ShaguTweaks.mods[reducedTitle]
reducedEnabled = reducedModule and reducedModule.enabled and 1 or 0
end
if reducedEnabled ~= 1 then return end
-- Moving the bag bar is driven by ClassicAPI modifier events and native
-- Region:IsMouseOver state. Do not keep a permanent polling fallback here.
if not API.modifierstate or not API.regionmouseover then return end
local frames = {
KeyRingButton, CharacterBag3Slot, CharacterBag2Slot, CharacterBag1Slot,
CharacterBag0Slot, MainMenuBarBackpackButton,
}
local bagframe = CreateFrame("Button", "ShaguTweaksReducedActionBarBags", UIParent)
bagframe:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -8, 48)
bagframe:SetWidth(180)
bagframe:SetHeight(42)
bagframe:SetScale(module.config["panelbag.scale"])
bagframe:SetFrameStrata("MEDIUM")
bagframe:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
bagframe:SetBackdropBorderColor(.9,.8,.5,1)
bagframe:SetBackdropColor(.4,.4,.4,1)
bagframe:SetClampedToScreen(true)
bagframe:SetMovable(true)
bagframe:EnableMouse(true)
bagframe:RegisterForDrag("LeftButton")
bagframe:SetUserPlaced(true)
bagframe:RegisterEvent("PLAYER_ENTERING_WORLD")
local function UpdateMoveMode()
local active = API.IsMouseOver(bagframe)
and API.IsShiftKeyDown()
and API.IsControlKeyDown()
if active then
if bagframe.mousedisabled then return end
bagframe.mousedisabled = true
for _, frame in ipairs(frames) do
frame:EnableMouse(0)
end
else
if not bagframe.mousedisabled then return end
bagframe.mousedisabled = false
for _, frame in ipairs(frames) do
frame:EnableMouse(1)
end
end
end
bagframe:SetScript("OnEnter", UpdateMoveMode)
bagframe:SetScript("OnLeave", UpdateMoveMode)
local modifier = CreateFrame("Frame", nil, bagframe)
modifier:RegisterEvent("MODIFIER_STATE_CHANGED")
modifier:SetScript("OnEvent", UpdateMoveMode)
bagframe:SetScript("OnDragStart", function()
local shift = API.IsShiftKeyDown()
local control = API.IsControlKeyDown()
if not shift or not control then return end
this:StartMoving()
end)
bagframe:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
end)
bagframe:SetScript("OnEvent", function()
if this.initialized then return end
this.initialized = true
ShaguTweaks.DarkenFrame(bagframe)
for id, frame in ipairs(frames) do
local anchor = frames[id-1] or bagframe
frame:ClearAllPoints()
frame:SetPoint("LEFT", anchor, id == 1 and "LEFT" or "RIGHT", id == 1 and 5 or 2, 0)
frame:SetParent(bagframe)
frame:SetScale(.8)
frame.Show = nil
frame:Show()
-- Child bag buttons normally own the mouse. Re-evaluate move mode when
-- entering or leaving one so Ctrl+Shift already held before mouseover is
-- handled without polling and without replacing their tooltip scripts.
ShaguTweaks.HookScript(frame, "OnEnter", UpdateMoveMode)
ShaguTweaks.HookScript(frame, "OnLeave", UpdateMoveMode)
end
UpdateMoveMode()
end)
end
+126
View File
@@ -0,0 +1,126 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Show Micro Menu"],
description = T["Shows micro menu buttons when using the reduced actionbar layout. Hold Ctrl+Shift to move the micro menu."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Action Bar"],
maintainer = "@shagu (GitHub)",
enabled = nil,
config = {
["panelmicro.scale"] = 1,
}
})
module.enable = function(self)
-- The ShaguTweaks loader initializes module config while iterating an
-- unordered table. On a fresh install the Reduced Actionbar key may not have
-- been visited yet, so fall back to that module's declared default.
local reducedTitle = T["Reduced Actionbar Size"]
local reducedEnabled = ShaguTweaks_config and ShaguTweaks_config[reducedTitle]
if reducedEnabled == nil then
local reducedModule = ShaguTweaks.mods and ShaguTweaks.mods[reducedTitle]
reducedEnabled = reducedModule and reducedModule.enabled and 1 or 0
end
if reducedEnabled ~= 1 then return end
-- Moving the micro menu is driven by ClassicAPI modifier events and native
-- Region:IsMouseOver state. Do not keep a permanent polling fallback here.
if not API.modifierstate or not API.regionmouseover then return end
local frames = {
CharacterMicroButton, SpellbookMicroButton, TalentMicroButton,
QuestLogMicroButton, MainMenuMicroButton, SocialsMicroButton,
WorldMapMicroButton, HelpMicroButton
}
local microframe = CreateFrame("Button", "ShaguTweaksReducedActionBarMicroMenu", UIParent)
microframe:SetPoint("BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -8, 8)
microframe:SetWidth(225)
microframe:SetHeight(44)
microframe:SetScale(module.config["panelmicro.scale"])
microframe:SetFrameStrata("MEDIUM")
microframe:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
microframe:SetBackdropBorderColor(.9,.8,.5,1)
microframe:SetBackdropColor(.4,.4,.4,1)
microframe:SetClampedToScreen(true)
microframe:SetMovable(true)
microframe:EnableMouse(true)
microframe:RegisterForDrag("LeftButton")
microframe:SetUserPlaced(true)
microframe:RegisterEvent("PLAYER_ENTERING_WORLD")
local function UpdateMoveMode()
local active = API.IsMouseOver(microframe)
and API.IsShiftKeyDown()
and API.IsControlKeyDown()
if active then
if microframe.mousedisabled then return end
microframe.mousedisabled = true
for _, frame in ipairs(frames) do
frame:EnableMouse(0)
end
else
if not microframe.mousedisabled then return end
microframe.mousedisabled = false
for _, frame in ipairs(frames) do
frame:EnableMouse(1)
end
end
end
microframe:SetScript("OnEnter", UpdateMoveMode)
microframe:SetScript("OnLeave", UpdateMoveMode)
local modifier = CreateFrame("Frame", nil, microframe)
modifier:RegisterEvent("MODIFIER_STATE_CHANGED")
modifier:SetScript("OnEvent", UpdateMoveMode)
microframe:SetScript("OnDragStart", function()
local shift = API.IsShiftKeyDown()
local control = API.IsControlKeyDown()
if not shift or not control then return end
this:StartMoving()
end)
microframe:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
end)
microframe:SetScript("OnEvent", function()
if this.initialized then return end
this.initialized = true
ShaguTweaks.DarkenFrame(microframe)
for id, frame in ipairs(frames) do
local anchor = frames[id-1] or microframe
frame:ClearAllPoints()
frame:SetPoint("LEFT", anchor, id == 1 and "LEFT" or "RIGHT", id == 1 and 3.5 or -2, id==1 and 10 or 0)
frame:SetParent(microframe)
frame.Show = nil
frame:Show()
-- Child micro buttons normally own the mouse. Re-evaluate move mode when
-- entering or leaving one so Ctrl+Shift already held before mouseover is
-- handled without polling and without replacing their tooltip scripts.
ShaguTweaks.HookScript(frame, "OnEnter", UpdateMoveMode)
ShaguTweaks.HookScript(frame, "OnLeave", UpdateMoveMode)
end
UpdateMoveMode()
end)
end
+8 -2
View File
@@ -4,7 +4,7 @@ local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Reduced Actionbar Size"], 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 }, expansions = { ["vanilla"] = true },
category = T["Action Bar"], category = T["Action Bar"],
enabled = nil, enabled = nil,
@@ -126,6 +126,11 @@ module.enable = function(self)
local function UpdateActionBarPositions() local function UpdateActionBarPositions()
local expVisible = MainMenuExpBar:IsVisible() local expVisible = MainMenuExpBar:IsVisible()
local reputationVisible = ReputationWatchBar: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 bottomLeftVisible = MultiBarBottomLeft:IsVisible()
local bottomRightVisible = MultiBarBottomRight:IsVisible() local bottomRightVisible = MultiBarBottomRight:IsVisible()
local shapeshiftVisible = ShapeshiftBarFrame:IsVisible() local shapeshiftVisible = ShapeshiftBarFrame:IsVisible()
@@ -134,7 +139,7 @@ module.enable = function(self)
-- move top actionbar if xp or reputation is tracked -- move top actionbar if xp or reputation is tracked
MultiBarBottomLeft:ClearAllPoints() MultiBarBottomLeft:ClearAllPoints()
if expVisible or reputationVisible then 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) MultiBarBottomLeft:SetPoint("BOTTOM", anchor, "TOP", 2, 3)
else else
MultiBarBottomLeft:SetPoint("BOTTOM", MainMenuBar, "TOP", 2, -3) MultiBarBottomLeft:SetPoint("BOTTOM", MainMenuBar, "TOP", 2, -3)
@@ -185,4 +190,5 @@ module.enable = function(self)
for i = 1, 5 do for i = 1, 5 do
_G["ContainerFrame" .. i .. "PortraitButton"]:SetScript("OnClick", ReplaceBag) _G["ContainerFrame" .. i .. "PortraitButton"]:SetScript("OnClick", ReplaceBag)
end end
ShaguTweaks.EnableActionBarMovers()
end end
+17 -6
View File
@@ -35,7 +35,10 @@ module.enable = function(self)
autovendor:SetScript("OnShow", function() autovendor:SetScript("OnShow", function()
this.startMoney = GetMoney() this.startMoney = GetMoney()
this.started = HasGreyItems() this.lastCount = API.GetNumJunkItems()
this.started = this.lastCount > 0
this.elapsed = 0
this.progressUntil = GetTime() + 5
if not this.started then if not this.started then
this:Hide() this:Hide()
@@ -45,6 +48,7 @@ module.enable = function(self)
-- ClassicAPI performs a real merchant sale and drains the queue one item -- ClassicAPI performs a real merchant sale and drains the queue one item
-- per frame, without abusing UseContainerItem or a manual throttle. -- per frame, without abusing UseContainerItem or a manual throttle.
API.SellAllJunkItems() API.SellAllJunkItems()
this.button:Update()
end) end)
autovendor:SetScript("OnHide", function() autovendor:SetScript("OnHide", function()
@@ -57,15 +61,22 @@ module.enable = function(self)
this.started = nil this.started = nil
this.startMoney = nil this.startMoney = nil
this.lastCount, this.elapsed, this.progressUntil = nil, nil, nil
if this.button then this.button:Update() end if this.button then this.button:Update() end
end) end)
-- This frame is visible only while ClassicAPI's short sell queue is active. -- Native code owns sale pacing. Check completion ten times per second and
-- Polling a single engine count here replaces repeated full bag scans. -- stop waiting after five seconds without progress; never resubmit the sale.
autovendor:SetScript("OnUpdate", function() autovendor:SetScript("OnUpdate", function()
if this.started and API.GetNumJunkItems() == 0 then if not this.started then return end
this:Hide() this.elapsed = this.elapsed + arg1
end if this.elapsed < .1 then return end
this.elapsed = 0
local count = API.GetNumJunkItems()
local now = GetTime()
if count < this.lastCount then this.progressUntil = now + 5 end
this.lastCount = count
if count == 0 or now >= this.progressUntil then this:Hide() end
end) end)
autovendor:RegisterEvent("MERCHANT_SHOW") autovendor:RegisterEvent("MERCHANT_SHOW")
+54 -11
View File
@@ -9,6 +9,7 @@
if not GetPlayerBuffID or not CombatLogAdd or not SpellInfo then return end if not GetPlayerBuffID or not CombatLogAdd or not SpellInfo then return end
local T = ShaguTweaks.T local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Super WoW Compatibility"], title = T["Super WoW Compatibility"],
@@ -19,30 +20,59 @@ local module = ShaguTweaks:register({
}) })
module.enable = function(self) module.enable = function(self)
if self.listener then return end
local enemyTitle = T["Enemy Castbars"]
local enabled = ShaguTweaks_config[enemyTitle]
if enabled == nil then
local consumer = ShaguTweaks.mods[enemyTitle]
enabled = consumer and consumer.enabled and 1 or 0
end
if enabled ~= 1 then return end
local libcast = ShaguTweaks.libcast local libcast = ShaguTweaks.libcast
local playerName = UnitName("player")
local lastPrune = -1
local function ClearCast(cast) local function Changed(guid)
cast.cast = nil if libcast.OnRemoteChanged then libcast.OnRemoteChanged(guid) end
cast.rank = nil
cast.start = nil
cast.casttime = nil
cast.icon = nil
cast.channel = nil
cast.spellID = nil
end end
local unitcast = CreateFrame("Frame") local unitcast = CreateFrame("Frame")
self.listener = unitcast
unitcast:RegisterEvent("UNIT_CASTEVENT") unitcast:RegisterEvent("UNIT_CASTEVENT")
unitcast:SetScript("OnEvent", function() unitcast:SetScript("OnEvent", function()
local guid = arg1 local guid = arg1
local event_type = arg3 local event_type = arg3
local spellID = arg4 local spellID = arg4
if not guid then return end if type(guid) ~= "string" or type(spellID) ~= "number" then return end
-- Player timing belongs to ClassicAPI (or Hunter-only local emulation).
if guid == API.UnitGUID("player") then return end
local now = GetTime()
if now - lastPrune >= .5 then libcast:Prune(now); lastPrune = now end
if event_type == "START" or event_type == "CAST" or event_type == "CHANNEL" then if event_type == "START" or event_type == "CAST" or event_type == "CHANNEL" then
-- human readable argument list -- human readable argument list
local timer = arg5 local timer = arg5
if type(timer) ~= "number" or timer ~= timer or timer <= 0 or timer > 600000 then
local cast = libcast.db[guid]
if cast and cast.spellID == spellID then libcast.db[guid] = nil; Changed(guid) end
return
end
-- A complete native record needs no second Lua copy. Keep SuperWoW
-- timing only where the native unit-token query cannot provide it.
local token = API.UnitTokenFromGUID(guid)
if token then
local name, finish
if event_type == "CHANNEL" then
local a, b, c, d, e, f = API.GetChannelInfo(token)
name, finish = a, f
else
local a, b, c, d, e, f = API.GetCastInfo(token)
name, finish = a, f
end
if name and finish and finish > now * 1000 then libcast.db[guid] = nil; return end
end
-- get spell info from spell id -- get spell info from spell id
local spell, icon, _ local spell, icon, _
@@ -55,13 +85,24 @@ module.enable = function(self)
-- add cast action to the database -- add cast action to the database
local cast = libcast.db[guid] local cast = libcast.db[guid]
if not cast then if not cast then
-- Cache only a bounded number of live remote records, even in long
-- sessions with unique NPC GUIDs. No permanent cleanup OnUpdate.
local count, earliest, expiry = 0, nil, nil
for key, entry in pairs(libcast.db) do
if key ~= playerName then
count = count + 1
local ending = entry.start + entry.casttime/1000
if not expiry or ending < expiry then earliest, expiry = key, ending end
end
end
if count >= 128 and earliest then libcast.db[earliest] = nil end
cast = {} cast = {}
libcast.db[guid] = cast libcast.db[guid] = cast
end end
cast.cast = spell cast.cast = spell
cast.rank = nil cast.rank = nil
cast.start = GetTime() cast.start = now
cast.casttime = timer cast.casttime = timer
cast.icon = icon cast.icon = icon
cast.channel = event_type == "CHANNEL" or false cast.channel = event_type == "CHANNEL" or false
@@ -69,13 +110,15 @@ module.enable = function(self)
-- write state variable -- write state variable
ShaguTweaks.superwow_active = true ShaguTweaks.superwow_active = true
Changed(guid)
elseif event_type == "FAIL" then elseif event_type == "FAIL" then
local cast = libcast.db[guid] local cast = libcast.db[guid]
-- Ignore a delayed FAIL for an older spell. Otherwise it can erase a -- Ignore a delayed FAIL for an older spell. Otherwise it can erase a
-- newer cast from the same GUID that already replaced it in the cache. -- newer cast from the same GUID that already replaced it in the cache.
if cast and cast.spellID == spellID then if cast and cast.spellID == spellID then
ClearCast(cast) libcast.db[guid] = nil
Changed(guid)
end end
end end
end) end)
+184 -44
View File
@@ -12,7 +12,7 @@ local API = ShaguTweaks.API or {}
local swing = ShaguTweaks:register({ local swing = ShaguTweaks:register({
title = T["Swing Timer"], title = T["Swing Timer"],
description = T["Shows main-hand and off-hand swing timers above the cast bar, with the weapon's icon. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."], description = T["Shows main-hand and off-hand swing timers above the cast bar, with the weapon's icon. Hold <Ctrl>, <Shift> and <Alt> to move each bar on its own; right-click a bar to reset it."],
expansions = { ["vanilla"] = true }, expansions = { ["vanilla"] = true },
category = T["Action Bar"], category = T["Action Bar"],
enabled = false, enabled = false,
@@ -131,12 +131,7 @@ local function Supported()
return true return true
end end
local function ModifiersDown() local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
local shift = API.IsShiftKeyDown or IsShiftKeyDown
local ctrl = API.IsControlKeyDown or IsControlKeyDown
local alt = API.IsAltKeyDown or IsAltKeyDown
return shift() and ctrl() and alt()
end
local function HasOffhandWeapon() local function HasOffhandWeapon()
local items = _G.C_Item local items = _G.C_Item
@@ -175,15 +170,27 @@ local function Wanted(bar)
return S.melee return S.melee
end end
-- Stack the shown bars upward from the container, in a fixed order. -- Every bar can have its own saved spot ("SwingTimerPosition_<bar>").
local function BarPosition(key)
local pos = ShaguTweaks_config and ShaguTweaks_config["SwingTimerPosition_" .. key]
if pos and pos[1] and pos[2] then return pos end
end
-- Bars without a saved spot stack upward from the container, in a fixed
-- order; bars with one sit where they were put.
local function Layout() local function Layout()
local slot = 0 local slot = 0
for i = 1, 3 do for i = 1, 3 do
local bar = S.bars[ORDER[i]] local bar = S.bars[ORDER[i]]
if bar:IsShown() then if bar:IsShown() then
local pos = BarPosition(bar.key)
bar:ClearAllPoints() bar:ClearAllPoints()
bar:SetPoint("BOTTOM", S.container, "BOTTOM", 0, slot * SPACING) if pos then
slot = slot + 1 bar:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
else
bar:SetPoint("BOTTOM", S.container, "BOTTOM", 0, slot * SPACING)
slot = slot + 1
end
end end
end end
end end
@@ -195,14 +202,47 @@ local function SetProgress(bar, progress, r, g, b)
end end
local function StopBar(bar) local function StopBar(bar)
if bar.wake then bar.wake:Cancel(); bar.wake = nil end
if bar.windupWake then bar.windupWake:Cancel(); bar.windupWake = nil end
if bar.binding then bar.binding:Disable() end
bar.active = false bar.active = false
bar.inWindup = false
bar.timer = 0 bar.timer = 0
bar.graceAt = nil bar.graceAt = nil
if not S.preview then bar:Hide() end if not S.preview then bar:Hide() end
end end
local function Running(bar)
return bar.key == "ranged" and S.autorepeat
or (bar.key ~= "ranged" and S.autoattack and S.combat)
end
local function UpdateColors()
if S.preview then return end
ReconcileQueue()
for i = 1, 3 do
local bar = S.bars[ORDER[i]]
local color = COLORS[bar.key]
if bar.key == "mainhand" and S.queued then color = COLORS[S.queued] end
if bar.key == "ranged" and S.hunter and bar.active and bar.inWindup then color = COLORS.windup end
if S.far[bar.swingType] then color = COLORS.far end
bar:SetStatusBarColor(unpack(color))
end
end
local function StopWaitingBars()
if not S.native then return end
local changed = false
for i = 1, 3 do
local bar = S.bars[ORDER[i]]
if bar.active and bar.graced and not Running(bar) then StopBar(bar); changed = true end
end
if changed then Layout() end
end
local function StartBar(bar, remaining) local function StartBar(bar, remaining)
if not Wanted(bar) or S.preview then return end if not Wanted(bar) or S.preview then return end
if type(remaining) ~= "number" or remaining ~= remaining or remaining < 0 then return end
UpdateSpeeds() UpdateSpeeds()
if bar.speed <= 0 then return end if bar.speed <= 0 then return end
@@ -212,13 +252,29 @@ local function StartBar(bar, remaining)
bar.timer = remaining bar.timer = remaining
bar.graceAt = nil bar.graceAt = nil
bar.active = true bar.active = true
bar.graced = nil
bar.inWindup = bar.key == "ranged" and S.hunter and remaining > 0 and remaining <= AUTOSHOT_WINDUP
UpdateIcon(bar) UpdateIcon(bar)
if S.native then
if bar.wake then bar.wake:Cancel() end
if bar.windupWake then bar.windupWake:Cancel(); bar.windupWake = nil end
bar.duration:SetTimeFromEnd(GetTime() + remaining, bar.max)
bar:SetTimerDuration(bar.duration, _G.Enum.StatusBarInterpolation.Immediate,
_G.Enum.StatusBarTimerDirection.ElapsedTime)
bar.binding:Enable()
bar.wake = _G.C_Timer.NewTimer(remaining, bar.due)
if bar.key == "ranged" and S.hunter and remaining > AUTOSHOT_WINDUP then
bar.windupWake = _G.C_Timer.NewTimer(remaining - AUTOSHOT_WINDUP, bar.windup)
end
UpdateColors()
end
if not bar:IsShown() then if not bar:IsShown() then
bar:Show() bar:Show()
Layout() Layout()
end end
S.driver:Show() if not S.native then S.driver:Show() end
end end
local function ResetAll() local function ResetAll()
@@ -258,10 +314,10 @@ local function Tick(bar, delta, now)
SetProgress(bar, 1 - bar.timer / bar.max, r, g, b) SetProgress(bar, 1 - bar.timer / bar.max, r, g, b)
local text = string.format("%.1f", math.floor(bar.timer * 10) / 10) local tenth = math.floor(bar.timer * 10)
if bar.lastText ~= text then if bar.lastTenth ~= tenth then
bar.lastText = text bar.lastTenth = tenth
bar.text:SetText(text) bar.text:SetText(string.format("%.1f", tenth / 10))
end end
end end
@@ -284,6 +340,14 @@ end
-- Classic cast bar look: dark background, UI-StatusBar fill, the cast bar -- Classic cast bar look: dark background, UI-StatusBar fill, the cast bar
-- border and spark, and an icon frame like Improved Castbar's. -- border and spark, and an icon frame like Improved Castbar's.
local function ApplyBarTheme(bar)
local tint, edge = ShaguTweaks.UITint, ShaguTweaks.UIBorder
bar.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1,
tint and tint.b or 1, tint and tint.a or 1)
bar.icon:SetBackdropBorderColor(edge and edge.r or 1, edge and edge.g or 1,
edge and edge.b or 1, edge and edge.a or 1)
end
local function CreateBar(key) local function CreateBar(key)
local bar = CreateFrame("StatusBar", "ShaguTweaksSwingTimer_" .. key, S.container) local bar = CreateFrame("StatusBar", "ShaguTweaksSwingTimer_" .. key, S.container)
bar:SetWidth(WIDTH) bar:SetWidth(WIDTH)
@@ -297,7 +361,8 @@ local function CreateBar(key)
bar.bg:SetAllPoints(bar) bar.bg:SetAllPoints(bar)
bar.bg:SetTexture(0, 0, 0, .5) bar.bg:SetTexture(0, 0, 0, .5)
bar.border = bar:CreateTexture(nil, "ARTWORK") -- The StatusBar fill uses ARTWORK; keep the rim above it at every progress.
bar.border = bar:CreateTexture(nil, "OVERLAY")
bar.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border") bar.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border")
bar.border:SetWidth(256) bar.border:SetWidth(256)
bar.border:SetHeight(64) bar.border:SetHeight(64)
@@ -313,6 +378,39 @@ local function CreateBar(key)
bar.text = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlight") bar.text = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
bar.text:SetPoint("CENTER", bar, "CENTER", 0, 3) bar.text:SetPoint("CENTER", bar, "CENTER", 0, 3)
-- As in pfUI, native duration objects own the fill and countdown. The
-- spark follows the native fill texture without a Lua position updater.
if S.native and type(bar.SetTimerDuration) ~= "function" then S.native = false end
if S.native then
bar.duration = _G.C_DurationUtil.CreateDuration()
bar.binding = _G.C_DurationUtil.CreateDurationTextBinding()
bar.binding:SetFontString(bar.text)
bar.binding:SetDuration(bar.duration)
bar.binding:SetFormatter(S.formatter)
bar.binding:SetExpiredText("0.0")
bar.binding:Disable()
bar.spark:ClearAllPoints()
bar.spark:SetPoint("CENTER", bar:GetStatusBarTexture(), "RIGHT", 0, 2)
-- Allocate callbacks once per bar, not on every swing reset.
bar.grace = function()
bar.wake = nil
bar.graced = true
if not Running(bar) then StopBar(bar); Layout() end
end
bar.due = function()
bar.inWindup = false
bar.binding:Disable()
if not S.preview then bar.text:SetText("0.0") end
bar.wake = _G.C_Timer.NewTimer(GRACE, bar.grace)
UpdateColors()
end
bar.windup = function()
bar.windupWake = nil
bar.inWindup = true
UpdateColors()
end
end
bar.icon = CreateFrame("Frame", nil, bar) bar.icon = CreateFrame("Frame", nil, bar)
bar.icon:SetPoint("RIGHT", bar, "LEFT", -10, 2) bar.icon:SetPoint("RIGHT", bar, "LEFT", -10, 2)
bar.icon:SetWidth(28) bar.icon:SetWidth(28)
@@ -322,15 +420,34 @@ local function CreateBar(key)
tile = true, tileSize = 8, edgeSize = 12, tile = true, tileSize = 8, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 } insets = { left = 2, right = 2, top = 2, bottom = 2 }
}) })
if ShaguTweaks.DarkMode then
bar.icon:SetBackdropBorderColor(.3, .3, .3, .9)
end
bar.icon.texture = bar.icon:CreateTexture(nil, "BACKGROUND") bar.icon.texture = bar.icon:CreateTexture(nil, "BACKGROUND")
bar.icon.texture:SetPoint("CENTER", 0, 0) bar.icon.texture:SetPoint("CENTER", 0, 0)
bar.icon.texture:SetWidth(24) bar.icon.texture:SetWidth(24)
bar.icon.texture:SetHeight(24) bar.icon.texture:SetHeight(24)
bar.icon.texture:SetTexCoord(.08, .92, .08, .92) bar.icon.texture:SetTexCoord(.08, .92, .08, .92)
ApplyBarTheme(bar)
-- Modules initialize in arbitrary order. Read the final palette when a real
-- timer or mover preview appears, without an idle update or recolor scan.
bar:SetScript("OnShow", function() ApplyBarTheme(this) end)
-- Each bar is dragged on its own while the preview is up.
bar:SetMovable(true)
bar:SetClampedToScreen(true)
bar:EnableMouse(false)
bar:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then this:StartMoving() end
end)
bar:SetScript("OnMouseUp", function()
this:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
ShaguTweaks_config["SwingTimerPosition_" .. this.key] = nil
else
ShaguTweaks_config["SwingTimerPosition_" .. this.key] = { this:GetLeft(), this:GetBottom() }
end
Layout()
end)
bar.key = key bar.key = key
bar.slot = SLOTS[key] bar.slot = SLOTS[key]
@@ -340,31 +457,52 @@ local function CreateBar(key)
return bar return bar
end end
-- Hold Ctrl+Shift+Alt to show every enabled bar and drag them as one. -- Hold Ctrl+Shift+Alt to show every enabled bar and drag each one on its own;
-- right-click a bar to put just that one back.
local function SetPreview(on) local function SetPreview(on)
if S.preview == on then return end if S.preview == on then return end
S.preview = on S.preview = on
S.container:EnableMouse(on)
for i = 1, 3 do for i = 1, 3 do
local bar = S.bars[ORDER[i]] local bar = S.bars[ORDER[i]]
bar:EnableMouse(on)
if not on then bar:StopMovingOrSizing() end
if on and Wanted(bar) then if on and Wanted(bar) then
if bar.binding then bar.binding:Disable(); bar:SetMinMaxValues(0, 1) end
UpdateIcon(bar) UpdateIcon(bar)
local r, g, b = unpack(COLORS[bar.key]) local r, g, b = unpack(COLORS[bar.key])
SetProgress(bar, 1, r, g, b) SetProgress(bar, 1, r, g, b)
bar.text:SetText(PREVIEW[bar.key]) bar.text:SetText(PREVIEW[bar.key])
bar.lastText = nil bar.lastTenth = nil
bar:Show() bar:Show()
elseif S.native and bar.active then
bar.spark:ClearAllPoints()
bar.spark:SetPoint("CENTER", bar:GetStatusBarTexture(), "RIGHT", 0, 2)
bar:SetTimerDuration(bar.duration, _G.Enum.StatusBarInterpolation.Immediate,
_G.Enum.StatusBarTimerDirection.ElapsedTime)
if bar.duration:GetRemainingDuration() > 0 then bar.binding:Enable()
else bar.text:SetText("0.0") end
elseif not bar.active then elseif not bar.active then
bar:Hide() bar:Hide()
end end
end end
if on then Layout()
S.container:SetHeight(HEIGHT + 2 * SPACING) if S.native then UpdateColors() end
else end
S.container:StopMovingOrSizing()
S.container:SetHeight(HEIGHT) -- Keep the bars where they are when something they were stacked on (the cast
-- bar) is dragged: every bar that still follows the default spot is pinned.
ShaguTweaks.FreezeSwingBars = function()
if not S.container then return end
for i = 1, 3 do
local bar = S.bars[ORDER[i]]
local key = "SwingTimerPosition_" .. bar.key
local left, bottom = bar:GetLeft(), bar:GetBottom()
if bar:IsShown() and left and bottom and not BarPosition(bar.key) then
ShaguTweaks_config = ShaguTweaks_config or {}
ShaguTweaks_config[key] = { left, bottom }
end
end end
Layout() Layout()
end end
@@ -387,27 +525,22 @@ local function CreateCore()
S.container = CreateFrame("Frame", "ShaguTweaksSwingTimer", UIParent) S.container = CreateFrame("Frame", "ShaguTweaksSwingTimer", UIParent)
S.container:SetWidth(WIDTH) S.container:SetWidth(WIDTH)
S.container:SetHeight(HEIGHT) S.container:SetHeight(HEIGHT)
S.container:SetMovable(true)
S.container:SetClampedToScreen(true)
S.container:EnableMouse(false) S.container:EnableMouse(false)
S.container:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then this:StartMoving() end S.native = API.durationtimers and true or false
end) if S.native then
S.container:SetScript("OnMouseUp", function() S.formatter = _G.C_StringUtil.CreateNumericRuleFormatter()
this:StopMovingOrSizing() S.formatter:AddBreakpoint({ threshold = 0, step = .1,
if arg1 == "RightButton" then rounding = _G.Enum.NumericRuleFormatRounding.Down, format = "%.1f" })
ShaguTweaks_config["SwingTimerPosition"] = nil end
else
ShaguTweaks_config["SwingTimerPosition"] = { this:GetLeft(), this:GetBottom() }
end
RestorePosition()
end)
for i = 1, 3 do S.bars[ORDER[i]] = CreateBar(ORDER[i]) end for i = 1, 3 do S.bars[ORDER[i]] = CreateBar(ORDER[i]) end
S.driver = CreateFrame("Frame", nil, UIParent) if not S.native then
S.driver:Hide() S.driver = CreateFrame("Frame", nil, UIParent)
S.driver:SetScript("OnUpdate", OnUpdate) S.driver:Hide()
S.driver:SetScript("OnUpdate", OnUpdate)
end
S.events = CreateFrame("Frame") S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function() S.events:SetScript("OnEvent", function()
@@ -464,6 +597,11 @@ local function CreateCore()
elseif event == "ACTIONBAR_SLOT_CHANGED" then elseif event == "ACTIONBAR_SLOT_CHANGED" then
if S.queue then ScanQueueSlots() end if S.queue then ScanQueueSlots() end
elseif event == "ACTIONBAR_UPDATE_STATE" then
-- Native timers do not poll action buttons. This event covers cancelling
-- a queued attack by pressing it again or pressing Escape.
ReconcileQueue()
elseif event == "PLAYER_EQUIPMENT_CHANGED" or event == "UNIT_INVENTORY_CHANGED" then elseif event == "PLAYER_EQUIPMENT_CHANGED" or event == "UNIT_INVENTORY_CHANGED" then
if event == "PLAYER_EQUIPMENT_CHANGED" and arg1 ~= 16 and arg1 ~= 17 and arg1 ~= 18 then return end if event == "PLAYER_EQUIPMENT_CHANGED" and arg1 ~= 16 and arg1 ~= 17 and arg1 ~= 18 then return end
UpdateSpeeds() UpdateSpeeds()
@@ -487,6 +625,7 @@ local function CreateCore()
UpdateSpeeds() UpdateSpeeds()
RestorePosition() RestorePosition()
end end
if S.native then StopWaitingBars(); UpdateColors() end
end) end)
local events = { local events = {
@@ -553,6 +692,7 @@ local function EnableQueueColors()
end end
end end
events:RegisterEvent("ACTIONBAR_SLOT_CHANGED") events:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
events:RegisterEvent("ACTIONBAR_UPDATE_STATE")
end end
swing.enable = function(self) swing.enable = function(self)
+194 -9
View File
@@ -9,7 +9,7 @@ local SuperWoWChannelInfo = ShaguTweaks.UnitChannelInfo
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Enemy Castbars"], title = T["Enemy Castbars"],
description = T["Shows an enemy castbar on target unit frame."], description = T["Shows an enemy castbar on target unit frame. Hold <Ctrl>, <Shift> and <Alt> to move it; right-click to reset."],
expansions = { ["vanilla"] = true }, expansions = { ["vanilla"] = true },
category = T["Unit Frames"], category = T["Unit Frames"],
enabled = true, enabled = true,
@@ -116,7 +116,14 @@ local function QueryCast(unit)
end end
end end
local function UpdatePosition() local POSITION_KEY = "TargetCastBarPosition"
-- Offset of the bar from the bottom of the target frame: the saved spot, or
-- the default that moves below the target of target and the buff rows.
local function GetOffsets()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
if pos and pos[1] and pos[2] then return pos[1], pos[2] end
local targetOfTarget = TargetofTargetFrame and TargetofTargetFrame:IsShown() local targetOfTarget = TargetofTargetFrame and TargetofTargetFrame:IsShown()
local debuff11 = TargetFrameDebuff11 and TargetFrameDebuff11:IsShown() local debuff11 = TargetFrameDebuff11 and TargetFrameDebuff11:IsShown()
local debuff7 = TargetFrameDebuff7 and TargetFrameDebuff7:IsShown() local debuff7 = TargetFrameDebuff7 and TargetFrameDebuff7:IsShown()
@@ -135,22 +142,36 @@ local function UpdatePosition()
y = -24 y = -24
end end
return -12, y
end
local function UpdatePosition()
local x, y = GetOffsets()
-- Buff/debuff rows usually remain unchanged for the whole cast. Keep checking -- Buff/debuff rows usually remain unchanged for the whole cast. Keep checking
-- their cheap visibility state while animating, but only touch the frame -- their cheap visibility state while animating, but only touch the frame
-- anchors when the required position actually changes. -- anchors when the required position actually changes.
if castbar.positionY == y then return end if castbar.positionY == y and castbar.positionX == x then return end
castbar.positionX = x
castbar.positionY = y castbar.positionY = y
castbar:ClearAllPoints() castbar:ClearAllPoints()
castbar:SetPoint("BOTTOM", TargetFrame, "BOTTOM", -12, y) castbar:SetPoint("BOTTOM", TargetFrame, "BOTTOM", x, y)
end end
local function HideCast() local function HideCast()
if castbar.expiry then castbar.expiry:Cancel(); castbar.expiry = nil end
if castbar.nativeProgress then castbar:SetMinMaxValues(0, 1) end
castbar.startTime = nil castbar.startTime = nil
castbar.endTime = nil castbar.endTime = nil
castbar.isChannel = nil castbar.isChannel = nil
castbar:Hide() castbar:Hide()
end end
local function ExpireCast()
castbar.expiry = nil
HideCast()
end
local function UpdateProgress() local function UpdateProgress()
local startTime = castbar.startTime local startTime = castbar.startTime
local endTime = castbar.endTime local endTime = castbar.endTime
@@ -202,6 +223,7 @@ local function RefreshCast()
castbar.startTime = startTime / 1000 castbar.startTime = startTime / 1000
castbar.endTime = endTime / 1000 castbar.endTime = endTime / 1000
castbar.isChannel = isChannel castbar.isChannel = isChannel
if castbar.endTime <= GetTime() then HideCast(); return end
local max = castbar.endTime - castbar.startTime local max = castbar.endTime - castbar.startTime
castbar:SetMinMaxValues(0, max) castbar:SetMinMaxValues(0, max)
@@ -216,9 +238,136 @@ local function RefreshCast()
UpdatePosition() UpdatePosition()
castbar:Show() castbar:Show()
UpdateProgress() if castbar.nativeProgress then
if castbar.expiry then castbar.expiry:Cancel() end
local duration
duration, castbar.fallbackDuration = API.GetCastDuration("target", isChannel,
castbar.startTime, castbar.endTime, castbar.fallbackDuration)
castbar:SetTimerDuration(duration, _G.Enum.StatusBarInterpolation.Immediate,
isChannel and _G.Enum.StatusBarTimerDirection.RemainingTime or _G.Enum.StatusBarTimerDirection.ElapsedTime)
castbar.expiry = _G.C_Timer.NewTimer(math.max(0, castbar.endTime - GetTime()), ExpireCast)
else
UpdateProgress()
end
end end
-- Hold Ctrl+Shift+Alt to show a marker where the bar goes and drag it; the
-- offset from the target frame is saved, so the bar follows the frame when it
-- is moved. Right-click the marker to go back to the automatic position.
-- The marker lives on UIParent because the bar itself hides with the target frame.
local mover
local function PlaceMover()
local x, y = GetOffsets()
mover:SetScale(TargetFrame:GetEffectiveScale() / UIParent:GetEffectiveScale())
mover:ClearAllPoints()
mover:SetPoint("BOTTOM", TargetFrame, "BOTTOM", x, y)
end
local function SaveMoverPosition()
local mx, my = mover:GetCenter()
local tx, ty = TargetFrame:GetCenter()
local bottom, targetBottom = mover:GetBottom(), TargetFrame:GetBottom()
if not (mx and tx and bottom and targetBottom) then return end
local ms, ts = mover:GetEffectiveScale(), TargetFrame:GetEffectiveScale()
ShaguTweaks_config = ShaguTweaks_config or {}
ShaguTweaks_config[POSITION_KEY] = { (mx * ms - tx * ts) / ms, (bottom * ms - targetBottom * ts) / ms }
end
-- Same look as the Swing Timer preview bars: the classic cast bar with its
-- border and spark, full, in the cast bar's gold, with an icon at the left.
local function CreateMover()
local width, height = castbar:GetWidth(), castbar:GetHeight()
local f = width / 195 -- the border and spark art is made for the 195 wide player bar
mover = CreateFrame("StatusBar", "ShaguTweaksTargetCastBarMover", UIParent)
mover:SetFrameStrata("DIALOG")
mover:SetWidth(width)
mover:SetHeight(height)
mover:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
mover:SetStatusBarColor(1, .8, 0)
mover:SetMinMaxValues(0, 1)
mover:SetValue(1)
mover:SetMovable(true)
mover:SetClampedToScreen(true)
mover:EnableMouse(true)
mover:Hide()
mover.bg = mover:CreateTexture(nil, "BACKGROUND")
mover.bg:SetAllPoints(mover)
mover.bg:SetTexture(0, 0, 0, .5)
mover.border = mover:CreateTexture(nil, "OVERLAY")
mover.border:SetTexture("Interface\\CastingBar\\UI-CastingBar-Border")
mover.border:SetWidth(256 * f)
mover.border:SetHeight(64 * f)
mover.border:SetPoint("TOP", mover, "TOP", 0, 28 * f)
mover.spark = mover:CreateTexture(nil, "OVERLAY")
mover.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
mover.spark:SetBlendMode("ADD")
mover.spark:SetWidth(32 * f)
mover.spark:SetHeight(32 * f)
mover.spark:SetPoint("CENTER", mover, "RIGHT", 0, 2 * f)
mover.text = mover:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
mover.text:SetPoint("CENTER", mover, "CENTER", 0, 2)
local font, size = mover.text:GetFont()
if font and size then mover.text:SetFont(font, size - 2, "THINOUTLINE") end
mover.text:SetText(T["Enemy Castbar"])
mover.icon = CreateFrame("Frame", nil, mover)
mover.icon:SetPoint("RIGHT", mover, "LEFT", -2, 0)
mover.icon:SetWidth(20)
mover.icon:SetHeight(20)
mover.icon:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 8, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
mover.icon.texture = mover.icon:CreateTexture(nil, "BACKGROUND")
mover.icon.texture:SetPoint("CENTER", 0, 0)
mover.icon.texture:SetWidth(16)
mover.icon.texture:SetHeight(16)
mover.icon.texture:SetTexture("Interface\\Icons\\Spell_Shadow_ShadowBolt")
mover.icon.texture:SetTexCoord(.08, .92, .08, .92)
mover:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then this:StartMoving() end
end)
mover:SetScript("OnMouseUp", function()
this:StopMovingOrSizing()
if arg1 == "RightButton" then
if ShaguTweaks_config then ShaguTweaks_config[POSITION_KEY] = nil end
else
SaveMoverPosition()
end
castbar.positionX = nil
UpdatePosition()
PlaceMover()
end)
end
local function SetPreview(on)
if (mover and mover:IsShown() and true or false) == on then return end
if on then
if not mover then CreateMover() end
local tint, edge = ShaguTweaks.UITint, ShaguTweaks.UIBorder
mover.border:SetVertexColor(tint and tint.r or 1, tint and tint.g or 1,
tint and tint.b or 1, tint and tint.a or 1)
mover.icon:SetBackdropBorderColor(edge and edge.r or 1, edge and edge.g or 1,
edge and edge.b or 1, edge and edge.a or 1)
PlaceMover()
mover:Show()
elseif mover then
mover:StopMovingOrSizing()
mover:Hide()
end
end
local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
module.enable = function(self) module.enable = function(self)
local castEvents = { local castEvents = {
"UNIT_SPELLCAST_START", "UNIT_SPELLCAST_START",
@@ -245,28 +394,64 @@ module.enable = function(self)
end end
end end
-- Animation-only OnUpdate; hidden frames do not receive OnUpdate ticks. castbar.nativeProgress = API.durationtimers and type(API.GetCastDuration) == "function"
castbar:SetScript("OnUpdate", UpdateProgress) and type(castbar.SetTimerDuration) == "function"
and _G.Enum.StatusBarTimerDirection.RemainingTime ~= nil
if castbar.nativeProgress then
castbar.spark:ClearAllPoints()
castbar.spark:SetPoint("CENTER", castbar:GetStatusBarTexture(), "RIGHT", 0, 0)
else
-- Older ClassicAPI builds retain the animation-only fallback.
castbar:SetScript("OnUpdate", UpdateProgress)
end
local listener = CreateFrame("Frame") local listener = CreateFrame("Frame")
listener:RegisterEvent("PLAYER_TARGET_CHANGED") listener:RegisterEvent("PLAYER_TARGET_CHANGED")
listener:RegisterEvent("MODIFIER_STATE_CHANGED")
for index = 1, table.getn(castEvents) do for index = 1, table.getn(castEvents) do
API.RegisterUnitEvent(listener, castEvents[index], "target") API.RegisterUnitEvent(listener, castEvents[index], "target", "player")
end
-- Layout changes are event-driven on the native animation path. Defer the
-- aura pass so native/third-party buff rows finish laying themselves out.
local positionQueued
local function RunPositionUpdate() positionQueued = nil; UpdatePosition() end
local function QueuePositionUpdate()
if positionQueued or not castbar:IsShown() then return end
positionQueued = true
if API.timer then API.Defer(RunPositionUpdate)
else ShaguTweaks.QueueFunction(RunPositionUpdate) end
end
if castbar.nativeProgress then
API.RegisterUnitEvent(listener, "UNIT_AURA", "target")
if TargetofTargetFrame then
ShaguTweaks.HookScript(TargetofTargetFrame, "OnShow", QueuePositionUpdate)
ShaguTweaks.HookScript(TargetofTargetFrame, "OnHide", QueuePositionUpdate)
end
end end
listener:SetScript("OnEvent", function() listener:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetPreview(ModifiersDown())
return
end
if event == "PLAYER_TARGET_CHANGED" then if event == "PLAYER_TARGET_CHANGED" then
RefreshCast() RefreshCast()
return return
end end
if event == "UNIT_AURA" then QueuePositionUpdate(); return end
-- ClassicAPI fans remote cast events out to every unit token currently -- ClassicAPI fans remote cast events out to every unit token currently
-- resolving to the caster, including "target" when appropriate. -- resolving to the caster, including "target" when appropriate.
if arg1 == "target" then if arg1 == "target" or (arg1 == "player" and UnitIsUnit("target", "player")) then
RefreshCast() RefreshCast()
end end
end) end)
ShaguTweaks.libcast.OnRemoteChanged = function(guid)
if guid == API.UnitGUID("target") then RefreshCast() end
end
RefreshCast() RefreshCast()
end 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
+2 -1
View File
@@ -283,7 +283,8 @@ module.enable = function(self)
"Status", "DIALOG", "GameFontWhite") "Status", "DIALOG", "GameFontWhite")
GameTooltipStatusBar.backdrop.health:SetFont( GameTooltipStatusBar.backdrop.health:SetFont(
STANDARD_TEXT_FONT, 12, "OUTLINE") STANDARD_TEXT_FONT, 12, "OUTLINE")
GameTooltipStatusBar.backdrop.health:SetPoint("TOP", 0, 4) -- centered on the bar: anchored to the top it stuck out over the border
GameTooltipStatusBar.backdrop.health:SetPoint("CENTER", GameTooltipStatusBar, "CENTER", 0, 1)
GameTooltipStatusBar.backdrop.health:SetNonSpaceWrap(false) GameTooltipStatusBar.backdrop.health:SetNonSpaceWrap(false)
-- Preserve the real statusbar method for Blizzard and other addons. A -- Preserve the real statusbar method for Blizzard and other addons. A
+341
View File
@@ -0,0 +1,341 @@
-- Totem Timers (Shaman)
--
-- One icon per active totem (Fire, Earth, Water, Air) with the time left.
-- Left-click targets the totem, hovering shows its tooltip. The data comes
-- from ClassicAPI's totem APIs. Native duration timers animate where available;
-- older builds use a light update only while a totem is up.
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Totem Timers"],
description = T["Shamans: shows the totems you have out as icons with the time left. Left-click targets a totem. Hold <Ctrl>, <Shift> and <Alt> to move them; right-click to reset."],
expansions = { ["vanilla"] = true },
category = T["Interface"],
enabled = false,
})
local SIZE = 34
local GAP = 4
local THROTTLE = .1
local POSITION_KEY = "TotemTimersPosition"
-- slot 1 Fire, 2 Earth, 3 Water, 4 Air
local SLOT_COLORS = {
{ 1, .45, .1 }, -- fire
{ .7, .55, .3 }, -- earth
{ .3, .6, 1 }, -- water
{ .8, .85, 1 }, -- air
}
local PREVIEW_ICONS = {
"Interface\\Icons\\Spell_Fire_SearingTotem",
"Interface\\Icons\\Spell_Nature_StrengthOfEarthTotem02",
"Interface\\Icons\\Spell_Nature_ManaRegenTotem",
"Interface\\Icons\\Spell_Nature_InvisibilityTotem",
}
-- One table for state: Lua 5.0 allows only 32 upvalues per function.
local S = { unlocked = false, elapsed = 0, buttons = {} }
local function Supported()
return type(_G.GetTotemInfo) == "function" and type(_G.GetTotemTimeLeft) == "function"
end
local function FormatTime(seconds)
if seconds >= 60 then
return string.format("%d:%02d", math.floor(seconds / 60), math.floor(seconds - math.floor(seconds / 60) * 60))
end
return string.format("%d", seconds)
end
local function StopButton(b)
if b.expiry then b.expiry:Cancel(); b.expiry = nil end
if b.binding then b.binding:Disable(); b.bar:SetMinMaxValues(0, 1); b.bar:SetValue(1) end
b.active = nil
end
local function Layout()
local shown = 0
for slot = 1, 4 do
local b = S.buttons[slot]
if b:IsShown() then
b:ClearAllPoints()
b:SetPoint("LEFT", S.frame, "LEFT", (slot - 1) * (SIZE + GAP), 0)
shown = shown + 1
end
end
return shown
end
-- Redraw the time and bar of the visible buttons; returns how many are up.
local function UpdateTimes()
local active = 0
for slot = 1, 4 do
local b = S.buttons[slot]
if b:IsShown() and not S.unlocked then
local left = _G.GetTotemTimeLeft(slot) or 0
if left > 0 then
active = active + 1
b.time:SetText(FormatTime(left))
if left <= 5 then
b.time:SetTextColor(1, .2, .2)
elseif left <= 10 then
b.time:SetTextColor(1, .9, .2)
else
b.time:SetTextColor(1, 1, 1)
end
if b.duration and b.duration > 0 then
b.bar:SetWidth(math.max(1, SIZE * math.min(left / b.duration, 1)))
end
else
-- duration ran out, the creature is about to go
b.time:SetText("")
b.bar:SetWidth(1)
end
end
end
return active
end
-- Re-read the four slots. Active totem = start time above 0; "have a totem"
-- is also true when you only carry the totem tool.
local function Refresh()
if S.unlocked then return end
local any = false
for slot = 1, 4 do
local b = S.buttons[slot]
local have, name, start, duration, icon = _G.GetTotemInfo(slot)
local nativeDuration = S.native and _G.GetTotemDuration(slot)
if start and start > 0 and (not nativeDuration or nativeDuration:GetRemainingDuration() > 0) then
any = true
b.duration = duration
if icon and icon ~= "" then b.icon:SetTexture(icon) end
b:Show()
if S.native then
StopButton(b)
b.active = true
b.bar:SetWidth(SIZE)
b.bar:SetTimerDuration(nativeDuration, _G.Enum.StatusBarInterpolation.Immediate,
_G.Enum.StatusBarTimerDirection.RemainingTime)
b.binding:SetDuration(nativeDuration)
b.binding:Enable()
b.expiry = _G.C_Timer.NewTimer(nativeDuration:GetRemainingDuration(), b.expire)
end
else
StopButton(b)
b.duration = nil
b:Hide()
end
end
if any then
Layout()
if not S.native then UpdateTimes() end
if not S.frame:IsShown() then S.frame:Show() end
else
S.frame:Hide()
end
end
local function OnUpdate()
S.elapsed = S.elapsed + (arg1 or 0)
if S.elapsed < THROTTLE then return end
S.elapsed = 0
UpdateTimes()
end
local function RestorePosition()
local pos = ShaguTweaks_config and ShaguTweaks_config[POSITION_KEY]
S.frame:ClearAllPoints()
if pos and pos[1] and pos[2] then
S.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", pos[1], pos[2])
else
S.frame:SetPoint("BOTTOM", UIParent, "BOTTOM", 0, 200)
end
end
local ModifiersDown = ShaguTweaks.API.AreMoveModifiersDown
-- Hold Ctrl+Shift+Alt to show four preview icons and drag them.
local function SetUnlocked(on)
if S.unlocked == on then return end
S.unlocked = on
S.frame:EnableMouse(on)
if on then
for slot = 1, 4 do
local b = S.buttons[slot]
StopButton(b)
b:EnableMouse(false) -- let the drag reach the container
b.icon:SetTexture(PREVIEW_ICONS[slot])
b.time:SetText(FormatTime(60 - slot * 10))
b.time:SetTextColor(1, 1, 1)
b.bar:SetWidth(SIZE)
b:Show()
end
Layout()
S.frame:Show()
else
S.frame:StopMovingOrSizing()
for slot = 1, 4 do S.buttons[slot]:EnableMouse(true); S.buttons[slot]:Hide() end
S.frame:Hide()
Refresh()
end
end
local function CreateButton(slot)
local b = CreateFrame("Button", "ShaguTweaksTotem" .. slot, S.frame)
b:SetWidth(SIZE)
b:SetHeight(SIZE)
b:Hide()
b.border = b:CreateTexture(nil, "BACKGROUND")
b.border:SetTexture("Interface\\Buttons\\WHITE8X8")
b.border:SetPoint("TOPLEFT", b, "TOPLEFT", -1, 1)
b.border:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", 1, -1)
b.border:SetVertexColor(SLOT_COLORS[slot][1], SLOT_COLORS[slot][2], SLOT_COLORS[slot][3])
b.icon = b:CreateTexture(nil, "ARTWORK")
b.icon:SetAllPoints(b)
b.icon:SetTexCoord(.08, .92, .08, .92)
b.time = b:CreateFontString(nil, "OVERLAY")
b.time:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
b.time:SetPoint("CENTER", b, "CENTER", 0, 0)
b.time:SetTextColor(1, 1, 1)
-- remaining time as a thin bar under the icon
b.bar = CreateFrame("StatusBar", nil, b)
b.bar:SetStatusBarTexture("Interface\\Buttons\\WHITE8X8")
b.bar:SetMinMaxValues(0, 1)
b.bar:SetValue(1)
b.bar:SetWidth(SIZE)
b.bar:SetHeight(3)
b.bar:SetPoint("TOPLEFT", b, "BOTTOMLEFT", 0, -2)
b.bar:SetStatusBarColor(SLOT_COLORS[slot][1], SLOT_COLORS[slot][2], SLOT_COLORS[slot][3])
if S.native and type(b.bar.SetTimerDuration) ~= "function" then S.native = false end
if S.native then
b.binding = _G.C_DurationUtil.CreateDurationTextBinding()
b.binding:SetFontString(b.time)
b.binding:SetFormatter(S.formatter)
b.binding:SetExpiredText("")
b.binding:SetUpdateInterval(.1)
b.expire = function()
b.expiry = nil
StopButton(b)
b:Hide()
for i = 1, 4 do if S.buttons[i].active then return end end
if not S.unlocked then S.frame:Hide() end
end
end
b:RegisterForClicks("LeftButtonUp")
b:SetScript("OnClick", function()
if S.unlocked then return end
if type(_G.TargetTotem) == "function" then _G.TargetTotem(slot) end
end)
b:SetScript("OnEnter", function()
if S.unlocked then return end
if GameTooltip.SetTotem then
-- shows itself, no :Show() needed
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetTotem(slot)
end
end)
b:SetScript("OnLeave", function() GameTooltip:Hide() end)
S.buttons[slot] = b
end
local function CreateTimers()
if S.frame then return end
local f = CreateFrame("Frame", "ShaguTweaksTotemTimers", UIParent)
f:SetWidth(4 * SIZE + 3 * GAP)
f:SetHeight(SIZE)
f:SetFrameStrata("MEDIUM")
f:SetMovable(true)
f:SetClampedToScreen(true)
f:EnableMouse(false)
f:Hide()
S.frame = f
S.native = API.durationtimers and type(_G.GetTotemDuration) == "function"
and _G.Enum.StatusBarTimerDirection.RemainingTime ~= nil
if S.native then
local down = _G.Enum.NumericRuleFormatRounding.Down
S.formatter = _G.C_StringUtil.CreateNumericRuleFormatter()
S.formatter:SetBreakpoints({
{ threshold = 0, step = 1, rounding = down, format = "|cffff3333%d" },
{ threshold = 5.000001, step = 1, rounding = down, format = "|cffffe633%d" },
{ threshold = 10.000001, step = 1, rounding = down, format = "|cffffffff%d" },
{ threshold = 60, format = "|cffffffff%d:%02d", components = {
{ div = 60, step = 1, rounding = down }, { mod = 60, step = 1, rounding = down } } },
})
end
for slot = 1, 4 do CreateButton(slot) end
f:SetScript("OnMouseDown", function()
if arg1 == "LeftButton" then this:StartMoving() end
end)
f:SetScript("OnMouseUp", function()
this:StopMovingOrSizing()
ShaguTweaks_config = ShaguTweaks_config or {}
if arg1 == "RightButton" then
ShaguTweaks_config[POSITION_KEY] = nil
else
ShaguTweaks_config[POSITION_KEY] = { this:GetLeft(), this:GetBottom() }
end
RestorePosition()
end)
-- Only older builds need a Lua animation loop while a totem is out.
if not S.native then f:SetScript("OnUpdate", OnUpdate) end
S.events = CreateFrame("Frame")
S.events:SetScript("OnEvent", function()
if event == "MODIFIER_STATE_CHANGED" then
SetUnlocked(ModifiersDown())
else
Refresh()
end
end)
RestorePosition()
end
module.enable = function(self)
local _, class = UnitClass("player")
if class ~= "SHAMAN" or not Supported() then return end
CreateTimers()
S.events:RegisterEvent("PLAYER_TOTEM_UPDATE")
S.events:RegisterEvent("PLAYER_ENTERING_WORLD")
S.events:RegisterEvent("MODIFIER_STATE_CHANGED")
SLASH_SHAGUTOTEMTIMERS1 = "/totemtimers"
SlashCmdList["SHAGUTOTEMTIMERS"] = function(msg)
msg = string.lower(msg or "")
if msg == "reset" then
ShaguTweaks_config = ShaguTweaks_config or {}
ShaguTweaks_config[POSITION_KEY] = nil
RestorePosition()
end
end
Refresh()
end
module.disable = function(self)
if not S.frame then return end
if S.unlocked then SetUnlocked(false) end
S.events:UnregisterAllEvents()
for slot = 1, 4 do StopButton(S.buttons[slot]) end
S.frame:Hide()
end
+27 -56
View File
@@ -5,7 +5,32 @@
if not TargetHPText or not TargetHPPercText then return end if not TargetHPText or not TargetHPPercText then return end
local T = ShaguTweaks.T local T = ShaguTweaks.T
local L = ShaguTweaks.L local _G = ShaguTweaks.GetGlobalEnv()
local function IsFirstContinent()
local maps = _G.C_Map
local first = GetMapContinents()
if first and maps and type(maps.GetBestMapForUnit) == "function"
and type(maps.GetMapInfo) == "function" then
local id = maps.GetBestMapForUnit("player")
for depth = 1, 8 do
if not id or id == 0 then return false end
local info = maps.GetMapInfo(id)
if not info then break end
if info.name == first then return true end
id = info.parentMapID
end
end
-- Older ClassicAPI builds lack the map hierarchy. Restore the viewed map
-- immediately instead of leaving it on the player's zone after clock hover.
local continent, zone = GetCurrentMapContinent(), GetCurrentMapZone()
if continent == nil or zone == nil then return end
SetMapToCurrentZone()
local result = GetCurrentMapContinent() == 1
if continent == 0 then SetMapZoom(0) else SetMapZoom(continent, zone) end
return result
end
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Turtle WoW Compatibility"], title = T["Turtle WoW Compatibility"],
@@ -21,8 +46,7 @@ module.enable = function(self)
local zh, zm = GetGameTime() local zh, zm = GetGameTime()
local sh, sm = zh, zm local sh, sm = zh, zm
SetMapToCurrentZone() if IsFirstContinent() then
if GetCurrentMapContinent() == 1 then
sh = sh + 12 sh = sh + 12
sh = sh >= 24 and sh - 24 or sh sh = sh >= 24 and sh - 24 or sh
end end
@@ -79,57 +103,4 @@ module.enable = function(self)
-- Vendor Values exposes a lazy ClassicAPI-backed SellValueDB compatibility -- Vendor Values exposes a lazy ClassicAPI-backed SellValueDB compatibility
-- table, so do not allocate a second Turtle-specific static price database. -- table, so do not allocate a second Turtle-specific static price database.
-- add druids tree of life and fast travel form to autoshift
if ShaguTweaks.dismount then
table.insert(ShaguTweaks.dismount.shapeshifts, "ability_druid_treeoflife")
table.insert(ShaguTweaks.dismount.shapeshifts, "ability_druid_stagform")
end
-- update debuff durations
L["debuffs"]["Hand of Reckoning"] = {[0]=3.0}
L["debuffs"]['Insect Swarm'] = {[0]=18.0}
L["debuffs"]['Moonfire'] = {[1]=9.0,[2]=18.0,[3]=18.0,[4]=18.0,[5]=18.0,[6]=18.0,[7]=18.0,[8]=18.0,[9]=18.0,[10]=18.0,[0]=18.0}
end
-- Turtle WoW specific libdebuff patches. Only Paladins and Warlocks
-- can trigger these cases, so other classes should not parse every spell-damage
-- chat line for strings they can never produce.
local libdebuff = ShaguTweaks.libdebuff
local _, playerClass = UnitClass("player")
if playerClass == "PALADIN" or playerClass == "WARLOCK" then
local libdebuff_twow = CreateFrame("Frame")
libdebuff_twow:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
libdebuff_twow:SetScript("OnEvent", function()
-- Break early on invalid data
if not arg1 or not arg2 then return end
-- Holy Strike is a spell, but can refresh paladin judgements
-- Credits to @geojak
if playerClass == "PALADIN" and string.find(arg1, "Holy Strike") then
for seal in ShaguTweaks.L["judgements"] do
local name = UnitName("target")
local level = UnitLevel("target")
if name and libdebuff.objects[name] then
if level and libdebuff.objects[name][level] and libdebuff.objects[name][level][seal] then
libdebuff:AddEffect(name, level, seal)
elseif libdebuff.objects[name][0] and libdebuff.objects[name][0][seal] then
libdebuff:AddEffect(name, 0, seal)
end
end
end
end
-- refresh Immolate duration after cast Conflagrate
if playerClass == "WARLOCK" and string.find(arg1, "Conflagrate") then
local name = UnitName("target")
local level = UnitLevel("target")
if name and level and libdebuff.objects[name] and libdebuff.objects[name][level]
and libdebuff.objects[name][level]["Immolate"]
then
local duration = libdebuff.objects[name][level]["Immolate"].duration
libdebuff:UpdateDuration(name, level, "Immolate", duration - 3)
end
end
end)
end end
+147
View File
@@ -0,0 +1,147 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Unit Frame Abbreviated Names"],
description = T["Abbreviates long target and target-of-target names."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Unit Frames"],
enabled = nil,
})
module.enable = function(self)
local maxLength = 15
local cache = {}
local function AbbrevWord(word)
return string.sub(word, 1, 1) .. ". "
end
local function GetShortName(unit)
local name = UnitName(unit)
if not name then
cache[unit] = nil
return
end
local cached = cache[unit]
if cached and cached.raw == name then
return cached.short
end
local short = name
if strlen(short) > maxLength then
short = string.gsub(short, "^(%S+) ", AbbrevWord)
end
if strlen(short) > maxLength then
short = string.gsub(short, "(%S+) ", AbbrevWord)
end
cache[unit] = {
raw = name,
short = short,
}
return short
end
local function GetNameText(frame)
if not frame then return end
if frame.name then return frame.name end
if frame.GetName then
local frameName = frame:GetName()
if frameName then
return _G[frameName .. "Name"]
end
end
end
local function UpdateFrame(frame, unit)
local text = GetNameText(frame)
local name = GetShortName(unit)
if not text or not name then return end
-- Avoid forcing a FontString update when the displayed value is already
-- correct. This matters most for the 250 ms target-of-target fallback.
if not text.GetText or text:GetText() ~= name then
text:SetText(name)
end
end
local function UpdateTarget()
UpdateFrame(TargetFrame, "target")
end
local function UpdateTargetTarget()
if TargetofTargetFrame then
UpdateFrame(TargetofTargetFrame, "targettarget")
end
end
local function EventValid(name)
return API.eventutils
and _G.C_EventUtils
and _G.C_EventUtils.IsEventValid
and _G.C_EventUtils.IsEventValid(name)
end
local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("PLAYER_TARGET_CHANGED")
local hasUnitTarget = EventValid("UNIT_TARGET")
local hasUnitName = EventValid("UNIT_NAME_UPDATE")
if hasUnitTarget then events:RegisterEvent("UNIT_TARGET") end
if hasUnitName then events:RegisterEvent("UNIT_NAME_UPDATE") end
events:SetScript("OnEvent", function()
if event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_TARGET_CHANGED" then
UpdateTarget()
UpdateTargetTarget()
elseif event == "UNIT_TARGET" then
if arg1 == "target" then UpdateTargetTarget() end
elseif event == "UNIT_NAME_UPDATE" then
if arg1 == "target" then
UpdateTarget()
elseif arg1 == "targettarget" then
UpdateTargetTarget()
end
end
end)
-- Vanilla has no UNIT_TARGET event for target-of-target changes. Prefer the
-- ClassicAPI native ticker so Lua only runs at the required 250 ms cadence,
-- instead of entering a Lua OnUpdate handler every rendered frame. Keep the
-- old throttled path only for older environments without C_Timer.NewTicker.
if not hasUnitTarget then
local function UpdateTargetTargetFallback()
if UnitName("targettarget") then
UpdateTargetTarget()
end
end
if _G.C_Timer and type(_G.C_Timer.NewTicker) == "function" then
_G.C_Timer.NewTicker(.25, UpdateTargetTargetFallback)
else
local fallback = CreateFrame("Frame")
fallback.elapsed = 0
fallback:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + (arg1 or 0)
if this.elapsed < .25 then return end
this.elapsed = 0
UpdateTargetTargetFallback()
end)
end
end
UpdateTarget()
UpdateTargetTarget()
end
+74 -62
View File
@@ -1,9 +1,17 @@
local T = ShaguTweaks.T local T = ShaguTweaks.T
local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({ local playerModule = ShaguTweaks:register({
title = T["Unit Frame Big Health"], title = T["Unit Frame Big Health for Player"],
description = T["Increases the healthbar of the player and target unitframe."], description = T["Enlarges the player health bar and colors it by class. Also applies the class color to the Personal Resource Display health bar when enabled."],
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 }, expansions = { ["vanilla"] = true },
category = T["Unit Frames"], category = T["Unit Frames"],
enabled = nil, enabled = nil,
@@ -21,7 +29,7 @@ for i = 1, table.getn(tocs) do
end end
addonpath = addonpath or "Interface\\AddOns\\ShaguTweaks" addonpath = addonpath or "Interface\\AddOns\\ShaguTweaks"
module.enable = function(self) local function Enable(player, target)
local normalTexture = addonpath .. "\\img\\UI-TargetingFrame" local normalTexture = addonpath .. "\\img\\UI-TargetingFrame"
local eliteTexture = addonpath .. "\\img\\UI-TargetingFrame-Elite" local eliteTexture = addonpath .. "\\img\\UI-TargetingFrame-Elite"
local rareTexture = addonpath .. "\\img\\UI-TargetingFrame-Rare" local rareTexture = addonpath .. "\\img\\UI-TargetingFrame-Rare"
@@ -32,16 +40,9 @@ module.enable = function(self)
-- Apply the configured dark tint directly to our replacement textures so the -- Apply the configured dark tint directly to our replacement textures so the
-- result is deterministic without adding any permanent OnUpdate work. -- result is deterministic without adding any permanent OnUpdate work.
local function ApplyDarkModeTextures() local function ApplyDarkModeTextures()
if not ShaguTweaks.DarkMode then return end -- Darkened UI or Golden UI publish their texture tint here, with the
-- /st overwrites already applied.
local darkModule = ShaguTweaks.mods[T["Darkened UI"]] local color = ShaguTweaks.UITint
local color = ShaguTweaks_config
and ShaguTweaks_config.overwrites
and ShaguTweaks_config.overwrites["darkmode.color"]
color = color
or (darkModule and darkModule.config and darkModule.config["darkmode.color"])
if not color then return end if not color then return end
local r = tonumber(color.r) or color.r local r = tonumber(color.r) or color.r
@@ -49,56 +50,60 @@ module.enable = function(self)
local b = tonumber(color.b) or color.b local b = tonumber(color.b) or color.b
local a = tonumber(color.a) or color.a or 1 local a = tonumber(color.a) or color.a or 1
if PlayerFrameTexture then PlayerFrameTexture:SetVertexColor(r, g, b, a) end if player and PlayerFrameTexture then PlayerFrameTexture:SetVertexColor(r, g, b, a) end
if PlayerStatusTexture then PlayerStatusTexture:SetVertexColor(r, g, b, a) end if player and PlayerStatusTexture then PlayerStatusTexture:SetVertexColor(r, g, b, a) end
if TargetFrameTexture then TargetFrameTexture:SetVertexColor(r, g, b, a) end if target and TargetFrameTexture then TargetFrameTexture:SetVertexColor(r, g, b, a) end
end end
-- Big Health is presentation-only. Numeric health/power values are owned by -- Big Health is presentation-only. Numeric health/power values are owned by
-- the independent Real Health Numbers module. -- the independent Real Health Numbers module.
PlayerFrameTexture:SetTexture(normalTexture) if player then
PlayerFrameHealthBar:SetPoint("TOPLEFT", 106, -22) PlayerFrameTexture:SetTexture(normalTexture)
PlayerFrameHealthBar:SetHeight(30) 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. -- The additive status artwork contains both the portrait ring and bar edge.
-- Crop only the latter while resting, shrinking the displayed width by the -- Crop only the latter while resting, shrinking the displayed width by the
-- same ratio so the portrait ring keeps its original size and position. -- same ratio so the portrait ring keeps its original size and position.
-- Native status code continues to own visibility, tint, alpha and pulsing. -- Native status code continues to own visibility, tint, alpha and pulsing.
local statusCoords = {PlayerStatusTexture:GetTexCoord()} local statusCoords = {PlayerStatusTexture:GetTexCoord()}
local statusWidth = PlayerStatusTexture:GetWidth() local statusWidth = PlayerStatusTexture:GetWidth()
local portraitRight = 72 / 256 -- right edge of the portrait in UI-Player-Status local portraitRight = 72 / 256 -- right edge of the portrait in UI-Player-Status
local restingCrop = false local restingCrop = false
local function UpdateRestingGlow() local function UpdateRestingGlow()
local resting = IsResting() local resting = IsResting()
and not UnitAffectingCombat("player") and not UnitAffectingCombat("player")
local left, right = statusCoords[1], statusCoords[5] local left, right = statusCoords[1], statusCoords[5]
if not left or not right or right <= left if not left or not right or right <= left
or portraitRight <= left or portraitRight >= right then return end or portraitRight <= left or portraitRight >= right then return end
if resting and not restingCrop then if resting and not restingCrop then
PlayerStatusTexture:SetTexCoord( PlayerStatusTexture:SetTexCoord(
left, portraitRight, statusCoords[2], statusCoords[4]) left, portraitRight, statusCoords[2], statusCoords[4])
PlayerStatusTexture:SetWidth( PlayerStatusTexture:SetWidth(
statusWidth * (portraitRight - left) / (right - left)) statusWidth * (portraitRight - left) / (right - left))
restingCrop = true restingCrop = true
elseif not resting and restingCrop then elseif not resting and restingCrop then
PlayerStatusTexture:SetTexCoord( PlayerStatusTexture:SetTexCoord(
left, right, statusCoords[2], statusCoords[4]) left, right, statusCoords[2], statusCoords[4])
PlayerStatusTexture:SetWidth(statusWidth) PlayerStatusTexture:SetWidth(statusWidth)
restingCrop = false restingCrop = false
end
end end
hooksecurefunc("PlayerFrame_UpdateStatus", UpdateRestingGlow)
UpdateRestingGlow()
end end
hooksecurefunc("PlayerFrame_UpdateStatus", UpdateRestingGlow)
UpdateRestingGlow()
if target then
TargetFrameTexture:SetTexture(normalTexture) TargetFrameTexture:SetTexture(normalTexture)
TargetFrameHealthBar:SetPoint("TOPRIGHT", -106, -22) TargetFrameHealthBar:SetPoint("TOPRIGHT", -106, -22)
TargetFrameHealthBar:SetHeight(30) TargetFrameHealthBar:SetHeight(30)
end
ApplyDarkModeTextures() ApplyDarkModeTextures()
@@ -131,26 +136,28 @@ module.enable = function(self)
ApplyDarkModeTextures() ApplyDarkModeTextures()
end end
hooksecurefunc("TargetFrame_CheckClassification", UpdateTargetClassificationTexture) if target then hooksecurefunc("TargetFrame_CheckClassification", UpdateTargetClassificationTexture) end
local playerSetStatusBarColor = PlayerFrameHealthBar.SetStatusBarColor local playerSetStatusBarColor = PlayerFrameHealthBar.SetStatusBarColor
local targetSetStatusBarColor = TargetFrameHealthBar.SetStatusBarColor local targetSetStatusBarColor = TargetFrameHealthBar.SetStatusBarColor
local function ApplyPlayerHealthColor() local function ApplyPlayerHealthColor()
if not PlayerFrameNameBackground or not playerSetStatusBarColor then return end if not PlayerFrameNameBackground or not playerSetStatusBarColor then return end
local r, g, b, a = PlayerFrameNameBackground:GetVertexColor() local r, g, b = PlayerFrameNameBackground:GetVertexColor()
playerSetStatusBarColor(PlayerFrameHealthBar, r, g, b, a) -- Resource hiding sets this color-source texture's alpha to zero. Its
-- visibility must not propagate to the actual health fill on updates.
playerSetStatusBarColor(PlayerFrameHealthBar, r, g, b, 1)
end end
local function ApplyTargetHealthColor() local function ApplyTargetHealthColor()
if not TargetFrameNameBackground or not targetSetStatusBarColor then return end if not TargetFrameNameBackground or not targetSetStatusBarColor then return end
local r, g, b, a = TargetFrameNameBackground:GetVertexColor() local r, g, b = TargetFrameNameBackground:GetVertexColor()
targetSetStatusBarColor(TargetFrameHealthBar, r, g, b, a) targetSetStatusBarColor(TargetFrameHealthBar, r, g, b, 1)
end end
-- Keep the Big Health colors without replacing SetStatusBarColor with a -- Keep the Big Health colors without replacing SetStatusBarColor with a
-- no-op. Other addons may still call the original method normally. -- no-op. Other addons may still call the original method normally.
if PlayerFrameNameBackground then if player and PlayerFrameNameBackground then
hooksecurefunc(PlayerFrameHealthBar, "SetStatusBarColor", ApplyPlayerHealthColor) hooksecurefunc(PlayerFrameHealthBar, "SetStatusBarColor", ApplyPlayerHealthColor)
hooksecurefunc(PlayerFrameNameBackground, "Show", function() hooksecurefunc(PlayerFrameNameBackground, "Show", function()
PlayerFrameNameBackground:Hide() PlayerFrameNameBackground:Hide()
@@ -159,7 +166,7 @@ module.enable = function(self)
ApplyPlayerHealthColor() ApplyPlayerHealthColor()
end end
if TargetFrameNameBackground then if target and TargetFrameNameBackground then
hooksecurefunc(TargetFrameHealthBar, "SetStatusBarColor", ApplyTargetHealthColor) hooksecurefunc(TargetFrameHealthBar, "SetStatusBarColor", ApplyTargetHealthColor)
hooksecurefunc(TargetFrameNameBackground, "Show", function() hooksecurefunc(TargetFrameNameBackground, "Show", function()
TargetFrameNameBackground:Hide() TargetFrameNameBackground:Hide()
@@ -168,9 +175,14 @@ module.enable = function(self)
ApplyTargetHealthColor() ApplyTargetHealthColor()
end end
hooksecurefunc("TargetFrame_CheckFaction", ApplyTargetHealthColor) if target then hooksecurefunc("TargetFrame_CheckFaction", ApplyTargetHealthColor) end
UpdateTargetClassificationTexture() if target then
TargetFrame_CheckFaction() UpdateTargetClassificationTexture()
TargetFrame_CheckFaction()
end
end) end)
end end
playerModule.enable = function() Enable(true, false) end
targetModule.enable = function() Enable(false, true) end
+132 -12
View File
@@ -3,6 +3,7 @@ local T = ShaguTweaks.T
local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc local hooksecurefunc = hooksecurefunc or ShaguTweaks.hooksecurefunc
local FALLBACK_CLASS_COLOR = { r = .5, g = .5, b = .5, a = 1 } 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({ local module = ShaguTweaks:register({
title = T["Unit Frame Class Colors"], title = T["Unit Frame Class Colors"],
@@ -18,6 +19,95 @@ local function GetUnitClassColor(unit)
return palette[class] or FALLBACK_CLASS_COLOR return palette[class] or FALLBACK_CLASS_COLOR
end end
local playerNameOption = ShaguTweaks:register({
title = T["Player's Name No Class Color"],
description = T["Removes the class-colored strip behind your player name, including the separate name frame."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
enable = function() end, -- Read by both player name layouts at startup.
})
local petColors = ShaguTweaks:register({
title = T["Pets Use Owner Class Color"],
description = T["Colors pet health bars by their owners' classes, including targeted pets whose owners can be identified."],
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
})
petColors.enable = function()
local refresh = {}
local function AddPetBar(bar, pet, owner)
if not bar then return end
local setColor = bar.SetStatusBarColor
local function Apply()
if not UnitExists(pet) or not UnitExists(owner) then return end
local color = GetUnitClassColor(owner)
setColor(bar, color.r, color.g, color.b, 1)
end
refresh[owner] = Apply
ShaguTweaks.hooksecurefunc(bar, "SetStatusBarColor", Apply)
Apply()
end
AddPetBar(PetFrameHealthBar, "pet", "player")
for id = 1, MAX_PARTY_MEMBERS do
AddPetBar(_G["PartyMemberFrame" .. id .. "PetFrameHealthBar"], "partypet" .. id, "party" .. id)
end
local function GetTargetOwner()
if not UnitExists("target") then return end
if UnitIsUnit("target", "pet") then return "player" end
for id = 1, MAX_PARTY_MEMBERS do
if UnitIsUnit("target", "partypet" .. id) then return "party" .. id end
end
local api = ShaguTweaks.API
if api and api.UnitTokenFromGUID and _G.UnitIsPet and _G.UnitOwnerGUID and _G.UnitIsPet("target") then
local guid = _G.UnitOwnerGUID("target")
local owner = guid and api.UnitTokenFromGUID(guid)
if owner and UnitIsPlayer(owner) then return owner end
end
end
local function RefreshTarget() end
if TargetFrameHealthBar then
local bar = TargetFrameHealthBar
local setColor = bar.SetStatusBarColor
local r, g, b, a = bar:GetStatusBarColor()
local targetColor
RefreshTarget = function()
local owner = GetTargetOwner()
local previous = targetColor
targetColor = owner and GetUnitClassColor(owner)
if targetColor then
setColor(bar, targetColor.r, targetColor.g, targetColor.b, 1)
elseif previous then
setColor(bar, r, g, b, a or 1)
end
end
-- Big Health installs its setter hook later. Calls through the captured
-- setter still reach this hook, so the owner color wins over reaction blue.
ShaguTweaks.hooksecurefunc(bar, "SetStatusBarColor", function(_, red, green, blue, alpha)
r, g, b, a = red, green, blue, alpha
if targetColor then setColor(bar, targetColor.r, targetColor.g, targetColor.b, 1) end
end)
ShaguTweaks.hooksecurefunc("TargetFrame_CheckFaction", RefreshTarget)
RefreshTarget()
end
local events = CreateFrame("Frame")
events:RegisterEvent("UNIT_PET")
events:RegisterEvent("PARTY_MEMBERS_CHANGED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:SetScript("OnEvent", function()
if event == "UNIT_PET" then
local apply = refresh[arg1]
if apply then apply() end
else
for _, apply in pairs(refresh) do apply() end
end
RefreshTarget()
end)
end
local function SetTextColorIfChanged(fontString, r, g, b, a) local function SetTextColorIfChanged(fontString, r, g, b, a)
local cr, cg, cb, ca = fontString:GetTextColor() local cr, cg, cb, ca = fontString:GetTextColor()
if cr ~= r or cg ~= g or cb ~= b or ca ~= a then if cr ~= r or cg ~= g or cb ~= b or ca ~= a then
@@ -67,11 +157,16 @@ module.enable = function(self)
-- add name background to player frame -- add name background to player frame
PlayerFrameNameBackground = PlayerFrame:CreateTexture(nil, "BACKGROUND") 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:SetWidth(119)
PlayerFrameNameBackground:SetHeight(19) PlayerFrameNameBackground:SetHeight(19)
PlayerFrameNameBackground:SetPoint("TOPLEFT", 106, -22) PlayerFrameNameBackground:SetPoint("TOPLEFT", 106, -22)
PlayerFrameNameBackground:SetVertexColor(playerColor.r, playerColor.g, playerColor.b, 1) PlayerFrameNameBackground:SetVertexColor(playerColor.r, playerColor.g, playerColor.b, 1)
-- Big Health still reads this texture's class tint when its artwork is hidden.
if ShaguTweaks_config and ShaguTweaks_config[playerNameOption.title] == 1 then
PlayerFrameNameBackground:Hide()
end
local wait = CreateFrame("Frame") local wait = CreateFrame("Frame")
wait:RegisterEvent("PLAYER_ENTERING_WORLD") wait:RegisterEvent("PLAYER_ENTERING_WORLD")
@@ -119,7 +214,7 @@ local function SetupModernPowerBars()
if not state then return end if not state then return end
local unit = bar.unit or state.unit local unit = bar.unit or state.unit
if not ShaguTweaks.ModernClassColors or not UnitExists(unit) then if not ShaguTweaks.ModernClassColors or not UnitExists(unit) then
state.background:Hide() if state.background:IsShown() then state.background:Hide() end
return return
end end
-- The druid's secondary bar stays mana while the primary bar is rage/energy. -- The druid's secondary bar stays mana while the primary bar is rage/energy.
@@ -130,24 +225,37 @@ local function SetupModernPowerBars()
local suffix = state.family local suffix = state.family
if suffix == "Player" then if suffix == "Player" then
suffix = "" suffix = ""
local _, class = UnitClass(unit)
-- Keep the retail energy variant for combo-point users. -- Keep the retail energy variant for combo-point users.
if powerType == 3 and (class == "ROGUE" or class == "DRUID") then if powerType == 3 then
suffix = "Class" local _, class = UnitClass(unit)
if class == "ROGUE" or class == "DRUID" then suffix = "Class" end
end end
end end
-- Vanilla mana slots are rectangular. Retail target/small-frame fills -- Vanilla mana slots are rectangular. Retail target/small-frame fills
-- have cut-out corners that leave a visible wedge inside those slots. -- have cut-out corners that leave a visible wedge inside those slots.
-- Use the native rectangular mana artwork for every mana frame. -- Use the native rectangular mana artwork for every mana frame.
if powerType == 0 then suffix = "Class" end if powerType == 0 then suffix = "Class" end
bar:SetStatusBarTexture(texture .. suffix) if state.styleTexture ~= texture or state.styleSuffix ~= suffix then
state.background:Show() state.styleTexture, state.styleSuffix = texture, suffix
state.wantedTexture = texture .. suffix
end
local wanted = state.wantedTexture
local current = bar:GetStatusBarTexture()
if not current or current:GetTexture() ~= wanted then bar:SetStatusBarTexture(wanted) end
if not state.background:IsShown() then state.background:Show() end
-- Call the method captured before our hook to avoid recursive updates. -- Call the method captured before our hook to avoid recursive updates.
state.setColor(bar, 1, 1, 1, 1) local r, g, b, a = bar:GetStatusBarColor()
if not r or math.abs(r - 1) > .0001 or math.abs(g - 1) > .0001
or math.abs(b - 1) > .0001 or not a or math.abs(a - 1) > .0001 then
state.setColor(bar, 1, 1, 1, 1)
end
else else
-- Unknown/custom power types retain their original texture and tint. -- Unknown/custom power types retain their original texture and tint.
bar:SetStatusBarTexture(state.originalTexture) local current = bar:GetStatusBarTexture()
state.background:Hide() if not current or current:GetTexture() ~= state.originalTexture then
bar:SetStatusBarTexture(state.originalTexture)
end
if state.background:IsShown() then state.background:Hide() end
end end
end end
@@ -188,7 +296,17 @@ local function SetupModernPowerBars()
end end
-- Vanilla refreshes power type here. Appending preserves the normal frame -- Vanilla refreshes power type here. Appending preserves the normal frame
-- updates, including shapeshifts and targets with a different power type. -- updates, including shapeshifts and targets with a different power type.
hooksecurefunc("UnitFrame_UpdateManaType", RefreshPowerBars) hooksecurefunc("UnitFrame_UpdateManaType", function(frame)
-- Clients use either an explicit unit frame or the legacy `this` context.
if type(frame) ~= "table" then frame = this end
if not frame then return end
if bars[frame] then ApplyPowerStyle(frame); return end
if frame.manabar and bars[frame.manabar] then ApplyPowerStyle(frame.manabar); return end
if not frame.unit then return end
for bar, state in pairs(bars) do
if (bar.unit or state.unit) == frame.unit then ApplyPowerStyle(bar) end
end
end)
RefreshPowerBars() RefreshPowerBars()
return RefreshPowerBars return RefreshPowerBars
end end
@@ -224,9 +342,11 @@ modern.enable = function(self)
RefreshPowerBars() RefreshPowerBars()
for _, prefix in ipairs({"PlayerFrame", "TargetFrame", "PetFrame", "TargetofTarget", "PartyMemberFrame1", "PartyMemberFrame2", "PartyMemberFrame3", "PartyMemberFrame4"}) do for _, prefix in ipairs({"PlayerFrame", "TargetFrame", "PetFrame", "TargetofTarget", "PartyMemberFrame1", "PartyMemberFrame2", "PartyMemberFrame3", "PartyMemberFrame4"}) do
local bar = _G[prefix .. "HealthBar"] 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 end
if not PlayerFrameNameBackground then return 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") local color = GetUnitClassColor("player")
PlayerFrameNameBackground:SetVertexColor(color.r, color.g, color.b, 1) PlayerFrameNameBackground:SetVertexColor(color.r, color.g, color.b, 1)
if TargetFrame_CheckFaction then TargetFrame_CheckFaction() end if TargetFrame_CheckFaction then TargetFrame_CheckFaction() end
+28 -14
View File
@@ -13,6 +13,8 @@ for _, name in pairs(tocs) do
end end
end end
local CLASS_PORTRAIT_TEXTURE = (addonpath or "Interface\\AddOns\\ShaguTweaks") .. "\\img\\UI-Classes-Circles"
local CLASS_ICON_TCOORDS = { local CLASS_ICON_TCOORDS = {
["WARRIOR"] = { 0, 0.25, 0, 0.25 }, ["WARRIOR"] = { 0, 0.25, 0, 0.25 },
["MAGE"] = { 0.25, 0.49609375, 0, 0.25 }, ["MAGE"] = { 0.25, 0.49609375, 0, 0.25 },
@@ -46,33 +48,45 @@ local function UpdatePortraits(frame)
if class then if class then
local iconCoords = CLASS_ICON_TCOORDS[class] local iconCoords = CLASS_ICON_TCOORDS[class]
if iconCoords then if iconCoords then
frame.portrait:SetTexture(addonpath .. "\\img\\UI-Classes-Circles") local wanted = CLASS_PORTRAIT_TEXTURE
frame.portrait:SetTexCoord(unpack(iconCoords)) if frame.portrait:GetTexture() ~= wanted then frame.portrait:SetTexture(wanted) end
local l, t, l2, b, r, t2, r2, b2 = frame.portrait:GetTexCoord()
if l ~= iconCoords[1] or t ~= iconCoords[3] or l2 ~= iconCoords[1]
or b ~= iconCoords[4] or r ~= iconCoords[2] or t2 ~= iconCoords[3]
or r2 ~= iconCoords[2] or b2 ~= iconCoords[4] then
frame.portrait:SetTexCoord(unpack(iconCoords))
end
end end
else else
frame.portrait:SetTexCoord(0, 1, 0, 1) local l, t, l2, b, r, t2, r2, b2 = frame.portrait:GetTexCoord()
if l ~= 0 or t ~= 0 or l2 ~= 0 or b ~= 1 or r ~= 1 or t2 ~= 0 or r2 ~= 1 or b2 ~= 1 then
frame.portrait:SetTexCoord(0, 1, 0, 1)
end
end end
end end
module.enable = function(self) module.enable = function(self)
-- overwrite portraits set by the game -- overwrite portraits set by the game
local original = UnitFrame_Update ShaguTweaks.hooksecurefunc("UnitFrame_Update", function(frame)
UnitFrame_Update = function(arg1, arg2, arg3) UpdatePortraits(type(frame) == "table" and frame.unit and frame or this)
original(arg1, arg2, arg3) end)
UpdatePortraits(this)
end
-- handle portrait update events -- handle portrait update events
local events = CreateFrame("Frame") local events = CreateFrame("Frame")
events:RegisterEvent("PLAYER_ENTERING_WORLD") events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("UNIT_PORTRAIT_UPDATE") events:RegisterEvent("UNIT_PORTRAIT_UPDATE")
local function RefreshFrame(frame)
if not frame or not frame.unit then return end
if event == "PLAYER_ENTERING_WORLD" or frame.unit == arg1
or (arg1 and UnitIsUnit(frame.unit, arg1)) then UpdatePortraits(frame) end
end
events:SetScript("OnEvent", function() events:SetScript("OnEvent", function()
UpdatePortraits(PlayerFrame) RefreshFrame(PlayerFrame)
UpdatePortraits(TargetFrame) RefreshFrame(TargetFrame)
UpdatePortraits(PartyMemberFrame1) RefreshFrame(PartyMemberFrame1)
UpdatePortraits(PartyMemberFrame2) RefreshFrame(PartyMemberFrame2)
UpdatePortraits(PartyMemberFrame3) RefreshFrame(PartyMemberFrame3)
UpdatePortraits(PartyMemberFrame4) RefreshFrame(PartyMemberFrame4)
end) end)
-- Turtle/Vanilla already runs TargetofTarget_OnUpdate for the target-of-target -- Turtle/Vanilla already runs TargetofTarget_OnUpdate for the target-of-target
+232 -78
View File
@@ -1,92 +1,246 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({ local module = ShaguTweaks:register({
title = T["Unit Frame Energy & Mana Tick"], title = T["Unit Frame Energy & Mana Tick"],
description = T["Adds an energy & mana tick to the player frame."], description = T["Adds an energy & mana tick to the player frame and Personal Resource Display."],
expansions = { ["vanilla"] = true }, expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = nil,
category = T["Unit Frames"],
enabled = nil,
}) })
module.enable = function(self) module.enable = function(self)
local energytick = CreateFrame("Frame", nil, PlayerFrameManaBar) if self.tick then return end
energytick:SetAllPoints(PlayerFrameManaBar) local tick = CreateFrame("StatusBar", nil, PlayerFrameManaBar)
energytick:RegisterEvent("PLAYER_ENTERING_WORLD") self.tick = tick
energytick:RegisterEvent("UNIT_DISPLAYPOWER") tick:SetAllPoints(PlayerFrameManaBar)
energytick:RegisterEvent("UNIT_ENERGY") tick:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
energytick:RegisterEvent("UNIT_MANA") tick:SetStatusBarColor(1, 1, 1, 0)
energytick:SetScript("OnEvent", function() tick:SetMinMaxValues(0, 1)
-- This module only tracks the player. Ignore unrelated unit events before tick:Hide()
-- doing any power queries or visibility work. tick.spark = tick:CreateTexture(nil, "OVERLAY")
if arg1 and arg1 ~= "player" then return end tick.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
tick.spark:SetBlendMode("ADD")
if UnitPowerType("player") == 0 then tick.spark:SetPoint("CENTER", tick:GetStatusBarTexture(), "RIGHT", 0, 0)
this.mode = "MANA" tick.spark:Hide()
-- hide if full mana and not in combat local native = API.durationtimers and type(tick.SetTimerDuration) == "function"
if (UnitMana("player") == UnitManaMax("player")) and (not UnitAffectingCombat("player")) then if native then tick.duration = _G.C_DurationUtil.CreateDuration() end
this:Hide() local function CancelWake()
else if tick.wake then tick.wake:Cancel(); tick.wake = nil end
this:Show() end
local function SizeSpark()
local height = PlayerFrameManaBar:GetHeight()
tick.spark:SetHeight(height + 10)
tick.spark:SetWidth(height + 4)
end
-- Observe the regen phase separately from the five-second-rule display.
-- Mid-period gains (potions/procs) do not move an established clock.
local function LockTick(now)
if tick.phase then
local error = math.mod(now - tick.phase, 2)
if error > 1 then error = error - 2 end
if math.abs(error) <= .25 then
if error > .08 then tick.phase = tick.phase + error - .08
elseif error < -.08 then tick.phase = tick.phase + error + .08 end
tick.rejected = nil
return
end end
elseif UnitPowerType("player") == 3 then if not tick.rejected or math.abs(now - tick.rejected - 2) > .25 then
this.mode = "ENERGY" tick.rejected = now
this:Show() return
else
this:Hide()
end
if event == "PLAYER_ENTERING_WORLD" then
this.lastMana = UnitMana("player")
end
if (this.mode == "ENERGY") or ((event == "UNIT_MANA" or event == "UNIT_ENERGY") and arg1 == "player") then
this.currentMana = UnitMana("player")
local diff = 0
if this.lastMana then
diff = this.currentMana - this.lastMana
end end
end
if this.mode == "MANA" and diff < 0 then tick.phase, tick.rejected = now, nil
this.target = 5 end
elseif this.mode == "MANA" and diff > 0 then local Sweep
if this.max ~= 5 and diff > (this.badtick and this.badtick*1.2 or 5) then local function Wrap() tick.wake = nil; Sweep() end
this.target = 2 function Sweep()
else CancelWake()
this.badtick = diff if not tick:IsShown() then return end
local now = GetTime()
local start, duration
if tick.mode == 0 and tick.ruleEnd and tick.ruleEnd > now then
-- Retain the five-second spark appearance for mana-costing casts.
start, duration = tick.ruleEnd - 5, 5
elseif tick.phase then
start = tick.phase + math.floor((now - tick.phase) / 2) * 2
duration = 2
end
if not start then tick.start = nil; tick.spark:Hide(); return end
tick.start, tick.period = start, duration
tick.spark:Show()
if native then
tick.duration:SetTimeFromStart(start, duration)
tick:SetTimerDuration(tick.duration, _G.Enum.StatusBarInterpolation.Immediate,
_G.Enum.StatusBarTimerDirection.ElapsedTime)
tick.wake = _G.C_Timer.NewTimer(math.max(.001, start + duration - now), Wrap)
end
end
local function Visible()
if tick.mode == 3 or (tick.mode == 0 and
(UnitMana("player") < UnitManaMax("player") or UnitAffectingCombat("player"))) then
tick:Show()
Sweep()
else tick:Hide() end
end
tick:SetScript("OnHide", CancelWake)
if not native then
tick:SetScript("OnUpdate", function()
if not tick.start then return end
local now = GetTime()
if now >= tick.start + tick.period then Sweep() end
if tick.start and tick.spark:IsShown() then
tick:SetValue(math.max(0, math.min(1, (now - tick.start) / tick.period)))
end
end)
end
ShaguTweaks.HookScript(PlayerFrameManaBar, "OnSizeChanged", SizeSpark)
ShaguTweaks.HookScript(tick, "OnShow", Sweep)
local function ArmRule(spellID)
if not spellID then return end
if API.GetSpellInfo then
local name, rank, icon, cost, funnel, powerType = API.GetSpellInfo(spellID)
if name then
if powerType == 0 and cost and cost > 0 then
tick.ruleSpell, tick.ruleEnd = spellID, GetTime() + 5
end end
elseif this.mode == "ENERGY" and diff >= 0 then return
this.target = 2
end end
this.lastMana = this.currentMana
end end
local spells = _G.C_Spell
if not spells or type(spells.GetSpellPowerCost) ~= "function" then return end
local costs = spells.GetSpellPowerCost(spellID)
local cost = costs and costs[1]
if cost and cost.type == 0 and cost.cost and cost.cost > 0 then
tick.ruleSpell, tick.ruleEnd = spellID, GetTime() + 5
end
end
tick:SetScript("OnEvent", function()
local world = event == "PLAYER_ENTERING_WORLD"
local display = event == "UNIT_DISPLAYPOWER"
-- ClassicAPI world-entry payloads are booleans, not unit tokens.
if not world and string.find(event, "^UNIT_") and arg1 ~= "player" then return end
local mode = UnitPowerType("player")
if world or display or mode ~= tick.mode then
tick.mode, tick.lastPower = mode, UnitMana("player")
tick.phase, tick.rejected, tick.start = nil, nil, nil
tick.spark:Hide()
end
if event == "UNIT_SPELLCAST_SUCCEEDED" then ArmRule(arg3)
elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
if tick.ruleSpell == arg3 then tick.ruleEnd = GetTime() + 5 end
elseif (event == "UNIT_MANA" and mode == 0) or (event == "UNIT_ENERGY" and mode == 3) then
local power = UnitMana("player")
if tick.lastPower and power > tick.lastPower then LockTick(GetTime()) end
tick.lastPower = power
end
Visible()
if self.RefreshDruidManaTick then self.RefreshDruidManaTick() end
end) end)
tick:RegisterEvent("PLAYER_ENTERING_WORLD")
local pheight, pwidth = PlayerFrameManaBar:GetHeight(), PlayerFrameManaBar:GetWidth() tick:RegisterEvent("PLAYER_REGEN_DISABLED")
energytick:SetScript("OnUpdate", function() tick:RegisterEvent("PLAYER_REGEN_ENABLED")
if this.target then local events = { "UNIT_DISPLAYPOWER", "UNIT_MANA", "UNIT_ENERGY",
this.max = this.target "UNIT_SPELLCAST_SUCCEEDED", "UNIT_SPELLCAST_CHANNEL_STOP" }
this.current = 0 for _, name in ipairs(events) do API.RegisterUnitEvent(tick, name, "player") end
this.target = nil SizeSpark()
-- Mirror the existing sweep; do not create another clock or event listener.
ShaguTweaks.RegisterResourceTickBar = function(parent)
if self.resourceTick then return end
local copy = CreateFrame("StatusBar", nil, parent)
self.resourceTick = copy
copy:SetAllPoints(parent)
copy:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
copy:SetStatusBarColor(1, 1, 1, 0)
copy:SetMinMaxValues(0, 1)
copy.spark = copy:CreateTexture(nil, "OVERLAY")
copy.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
copy.spark:SetBlendMode("ADD")
copy.spark:SetPoint("CENTER", copy:GetStatusBarTexture(), "RIGHT", 0, 0)
local function Resize()
copy.spark:SetHeight(parent:GetHeight() + 10)
copy.spark:SetWidth(parent:GetHeight() + 4)
end end
local function Visibility()
if not this.max then return end if tick:IsShown() then copy:Show() else copy:Hide() end
if tick.spark:IsShown() then copy.spark:Show() else copy.spark:Hide() end
-- OnUpdate already provides the exact elapsed frame time. Accumulating arg1
-- keeps the spark just as smooth without querying GetTime() every frame.
this.current = (this.current or 0) + arg1
if this.current > this.max then
this.max, this.current = 2, 0
end end
ShaguTweaks.HookScript(parent, "OnSizeChanged", Resize)
local pos = (pwidth ~= "-1" and pwidth or width) * (this.current / this.max) ShaguTweaks.HookScript(tick, "OnShow", Visibility)
if not pheight then return end ShaguTweaks.HookScript(tick, "OnHide", Visibility)
this.spark:SetPoint("LEFT", pos-((pheight+5)/2), 0) ShaguTweaks.hooksecurefunc(tick.spark, "Show", Visibility)
end) ShaguTweaks.hooksecurefunc(tick.spark, "Hide", Visibility)
if native then
energytick.spark = energytick:CreateTexture(nil, 'OVERLAY') ShaguTweaks.hooksecurefunc(tick, "SetTimerDuration", function(bar, duration, interpolation, direction)
energytick.spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark") copy:SetTimerDuration(duration, interpolation, direction)
energytick.spark:SetHeight(pheight + 10) end)
energytick.spark:SetWidth(pheight + 4) if tick.start then copy:SetTimerDuration(tick.duration, _G.Enum.StatusBarInterpolation.Immediate,
energytick.spark:SetBlendMode('ADD') _G.Enum.StatusBarTimerDirection.ElapsedTime) end
else
ShaguTweaks.hooksecurefunc(tick, "SetValue", function(bar, value) copy:SetValue(value) end)
copy:SetValue(tick:GetValue() or 0)
end
Resize(); Visibility()
end
local display = ShaguTweaks.PersonalResourceDisplay
if display then ShaguTweaks.RegisterResourceTickBar(display.power.bar) end
ShaguTweaks.RegisterDruidManaTickBar = function(parent, source)
if self.druidTick then return end
local bar = CreateFrame("StatusBar", nil, parent)
self.druidTick = bar
bar:SetAllPoints(parent)
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
bar:SetStatusBarColor(1, 1, 1, 0)
bar:SetMinMaxValues(0, 1)
local spark = bar:CreateTexture(nil, "OVERLAY")
spark:SetTexture("Interface\\CastingBar\\UI-CastingBar-Spark")
spark:SetBlendMode("ADD")
spark:SetPoint("CENTER", bar:GetStatusBarTexture(), "RIGHT", 0, 0)
local function Resize()
spark:SetHeight(parent:GetHeight() + 10); spark:SetWidth(parent:GetHeight() + 4)
end
ShaguTweaks.HookScript(parent, "OnSizeChanged", Resize)
Resize()
local duration = native and _G.C_DurationUtil.CreateDuration()
local phase, last, start, period, wake
local Refresh, Animate
local function Cancel()
if wake then wake:Cancel(); wake = nil end
end
local function Wrap() wake = nil; Refresh() end
Refresh = function()
Cancel()
local now = GetTime()
if not parent:IsVisible() then spark:Hide(); bar:SetScript("OnUpdate", nil); return end
if tick.ruleEnd and tick.ruleEnd > now then start, period = tick.ruleEnd - 5, 5
elseif phase then start, period = phase + math.floor((now-phase)/2)*2, 2
else spark:Hide(); bar:SetScript("OnUpdate", nil); return end
spark:Show()
if native then
duration:SetTimeFromStart(start, period)
bar:SetTimerDuration(duration, _G.Enum.StatusBarInterpolation.Immediate, _G.Enum.StatusBarTimerDirection.ElapsedTime)
wake = _G.C_Timer.NewTimer(math.max(.001, start+period-now), Wrap)
else bar:SetScript("OnUpdate", Animate)
end
end
local function Sample()
local value = source:GetValue()
if last and value > last then phase = GetTime() end
last = value
Refresh()
end
if not native then
Animate = function()
if spark:IsShown() and start then
if GetTime() >= start+period then Refresh() end
bar:SetValue(math.max(0, math.min(1, (GetTime()-start)/period)))
end
end
end
ShaguTweaks.hooksecurefunc(source, "SetValue", Sample)
ShaguTweaks.HookScript(parent, "OnShow", Refresh)
ShaguTweaks.HookScript(parent, "OnHide", function() Cancel(); phase, last = nil, nil; spark:Hide(); bar:SetScript("OnUpdate", nil) end)
self.RefreshDruidManaTick = Refresh
Sample()
end
if display and display.mana then
ShaguTweaks.RegisterDruidManaTickBar(display.mana.bar, PlayerFrameAlternatePowerBar)
end
end end
+89
View File
@@ -0,0 +1,89 @@
local T = ShaguTweaks.T
local P = ShaguTweaks.libpredict
local module = ShaguTweaks:register({
title = T["Healing Predictions"],
description = T["Shows estimated direct heals and upcoming HoT ticks on player and target health bars. Reuses compatible Puppeteer prediction data when available; otherwise uses ClassicAPI events, observed heal amounts and vanilla HealComm. Unknown amounts are not shown."],
expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = nil,
})
local states, events, enabled
local function Refresh(state)
local bar, unit, fill = state.bar, state.unit, state.fill
if not enabled or not bar:IsVisible() or not UnitExists(unit) or UnitIsDeadOrGhost(unit)
or not UnitCanAssist("player", unit) then fill:Hide(); return end
local current, maximum = UnitHealth(unit), UnitHealthMax(unit)
-- A percentage-only target cannot be combined with an absolute heal amount.
if not current or not maximum or maximum <= 0 or current >= maximum
or (unit == "target" and maximum == 100 and not UnitIsUnit(unit, "player")) then fill:Hide(); return end
local amount = math.min(P:UnitGetIncomingHeals(unit), maximum-current)
local width, height = bar:GetWidth(), bar:GetHeight()
if amount <= 0 or width <= 0 or height <= 0 then fill:Hide(); return end
local texture = bar:GetStatusBarTexture()
if bar.personalResourceDisplay then
-- Native status bars may crop transparent texture padding themselves;
-- a plain texture region does not inherit that rendering behavior.
fill:SetTexture(1, 1, 1, 1)
else
fill:SetTexture(texture and texture:GetTexture() or "Interface\\TargetingFrame\\UI-StatusBar")
end
fill:SetVertexColor(0, .85, .4, .6)
fill:ClearAllPoints()
fill:SetPoint("TOPLEFT", bar, "TOPLEFT", width * current/maximum, 0)
fill:SetWidth(width * amount/maximum)
fill:SetHeight(height)
if bar.personalResourceDisplay then fill:SetTexCoord(0, 1, 0, 1)
else fill:SetTexCoord(current/maximum, (current+amount)/maximum, 0, 1) end
fill:Show()
end
local function RefreshAll()
for _, state in ipairs(states) do Refresh(state) end
end
local function AttachBar(unit, bar)
if not bar then return end
for _, state in ipairs(states) do if state.bar == bar then return end end
local state = { unit = unit, bar = bar, fill = bar:CreateTexture(nil,"ARTWORK") }
table.insert(states,state)
state.fill:Hide()
local function Changed() Refresh(state) end
ShaguTweaks.hooksecurefunc(bar,"SetValue",Changed)
ShaguTweaks.hooksecurefunc(bar,"SetMinMaxValues",Changed)
ShaguTweaks.hooksecurefunc(bar,"SetStatusBarTexture",Changed)
ShaguTweaks.hooksecurefunc(bar,"SetWidth",Changed)
ShaguTweaks.hooksecurefunc(bar,"SetHeight",Changed)
ShaguTweaks.HookScript(bar,"OnShow",Changed)
ShaguTweaks.HookScript(bar,"OnSizeChanged",Changed)
ShaguTweaks.HookScript(bar,"OnHide",function() state.fill:Hide() end)
Refresh(state)
end
-- Additional displays share the existing estimator and event driver.
ShaguTweaks.RegisterHealingPredictionBar = function(unit, bar)
if enabled then AttachBar(unit, bar) end
end
module.enable = function()
if enabled then return end
enabled = true
if not states then
states = {}
AttachBar("player", PlayerFrameHealthBar)
AttachBar("target", TargetFrameHealthBar)
P.changed = function(name)
for _, state in ipairs(states) do if UnitName(state.unit)==name then Refresh(state) end end
end
events = CreateFrame("Frame")
events:SetScript("OnEvent",RefreshAll)
end
local display = ShaguTweaks.PersonalResourceDisplay
if display then AttachBar("player", display.health.bar) end
P:Enable()
ShaguTweaks.API.RegisterUnitEvent(events,"UNIT_HEALTH","player","target")
ShaguTweaks.API.RegisterUnitEvent(events,"UNIT_MAXHEALTH","player","target")
events:RegisterEvent("PLAYER_TARGET_CHANGED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
RefreshAll()
end
module.disable = function()
if not enabled then return end
enabled = false
events:UnregisterAllEvents()
P:Disable()
RefreshAll()
end
+25
View File
@@ -0,0 +1,25 @@
local T = ShaguTweaks.T
local module = ShaguTweaks:register({
title = T["Hide Player Name"],
description = T["Hides your player name and raid group number. Requires Hide Player Frame Resources."],
requires = { T["Hide Player Frame Resources"] },
expansions = { ["vanilla"] = true },
category = T["Unit Frames"],
enabled = nil,
})
module.enable = function(self)
if ShaguTweaks_config[T["Hide Player Frame Resources"]] ~= 1 then return end
local layout = ShaguTweaks.PlayerFrameResourcesLayout
if layout and layout.name then layout.name:Hide() end
local group = PlayerFrameGroupIndicator
if group and not self.groupHidden then
self.groupHidden = true
local setAlpha = group.SetAlpha
setAlpha(group, 0)
ShaguTweaks.hooksecurefunc(group, "SetAlpha", function(frame) setAlpha(frame, 0) end)
end
-- Resource-hiding setup also reads the saved setting after all modules load,
-- so this works regardless of their initialization order.
end
+251
View File
@@ -0,0 +1,251 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Hide Player Frame Resources"],
description = T["Hides the original player health and power bars, leaving the clean round portrait and name. Hold Ctrl+Shift+Alt to drag the name independently; right-click with those keys held to reset it. Personal Resource Display is independent."],
expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = nil,
})
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 or not PlayerFrame or not PlayerPortrait or not PlayerFrameTexture then return end
if not PlayerPortrait:GetLeft() or not PlayerFrame:GetLeft() then return end
self.installed = true
local width = PlayerFrameHealthBar:GetWidth()
local portraitLeft, portraitTop = PlayerPortrait:GetLeft() - 8, PlayerPortrait:GetTop() + 4
local function Holder(key, w, h, x, y)
local holder = CreateFrame("Frame", "ShaguTweaksPlayer" .. key, PlayerFrame)
holder:SetWidth(w); holder:SetHeight(h)
holder:SetFrameLevel(PlayerFrame:GetFrameLevel() + 3)
holder:SetPoint("TOPLEFT", PlayerFrame, "TOPLEFT", x - PlayerFrame:GetLeft(), y - PlayerFrame:GetTop())
return holder
end
local portrait = Holder("Portrait", 80, 80, portraitLeft, portraitTop)
local name = Holder("Name", width + 8, 23, portraitLeft + 85, portraitTop - 14)
ShaguTweaks.PlayerFrameResourcesLayout = { portrait = portrait, name = name }
ShaguTweaks_config.PlayerFrameResources = ShaguTweaks_config.PlayerFrameResources or {}
local positions = ShaguTweaks_config.PlayerFrameResources
local nameX, nameY = portraitLeft + 85 - PlayerFrame:GetLeft(), portraitTop - 14 - PlayerFrame:GetTop()
local function PositionName()
name:ClearAllPoints()
if positions.Name then
name:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", positions.Name.x, positions.Name.y)
else
name:SetPoint("TOPLEFT", PlayerFrame, "TOPLEFT", nameX, nameY)
end
end
local dragging, dropped
local function StopName()
if not dragging then return end
name:StopMovingOrSizing()
name:SetUserPlaced(false)
dragging, dropped = nil, true
local x, y = name:GetLeft(), name:GetTop()
if x and y then positions.Name = { x = x, y = y } end
PositionName()
end
name:SetMovable(true)
name:SetClampedToScreen(true)
name:EnableMouse(true)
name:RegisterForDrag("LeftButton")
name:SetScript("OnDragStart", function()
if not API.AreMoveModifiersDown() then return end
dragging, dropped = true, nil
name:StartMoving()
end)
name:SetScript("OnDragStop", StopName)
name:SetScript("OnHide", StopName)
name:SetScript("OnMouseUp", function()
if API.AreMoveModifiersDown() and arg1 == "RightButton" then
StopName()
positions.Name, dropped = nil, nil
PositionName()
elseif dragging then
StopName()
dropped = nil
elseif dropped then
dropped = nil
elseif PlayerFrame_OnClick then
-- Retain native unit clicks even after the name is moved away.
local previous = this
this = PlayerFrame
PlayerFrame_OnClick(arg1)
this = previous
end
end)
PositionName()
-- 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. Give the separate name strip a rounded Blizzard border.
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
name:SetBackdrop(border)
name:SetBackdropBorderColor(color and color.r or 1, color and color.g or 1,
color and color.b or 1, 1)
-- 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
if ShaguTweaks_config[T["Player's Name No Class Color"]] ~= 1 then
-- 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)
end
if ShaguTweaks_config[T["Hide Player Name"]] == 1 then name:Hide() end
-- Keep native bar objects and events alive. Copies may still mirror them.
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)
Suppress(PlayerFrameHealthBar)
Suppress(PlayerFrameManaBar)
Suppress(_G.PlayerFrameAlternatePowerBar)
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
API.AfterWorldReady(Setup, true)
end
@@ -0,0 +1,212 @@
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local module = ShaguTweaks:register({
title = T["Personal Resource Display"],
description = T["Adds independent copies of your health and power bars without changing the player frame. Hold Ctrl+Shift+Alt to drag a bar or its bottom-right resize grip; right-click a bar with those keys held to reset it."],
expansions = { ["vanilla"] = true }, category = T["Unit Frames"], enabled = nil,
})
local border = { edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 12, tile = true, tileSize = 8,
insets = { left = 3, right = 3, top = 3, bottom = 3 } }
local function Position(state, saved)
local point = saved or state.default
state.frame:ClearAllPoints()
state.frame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", point.x, point.y)
state.frame:SetWidth(point.width or state.default.width)
state.frame:SetHeight(point.height or state.default.height)
end
local function Finish(state)
if not state.dragging then return end
state.frame:StopMovingOrSizing()
state.frame:SetUserPlaced(false)
state.dragging = nil
local x, y = state.frame:GetLeft(), state.frame:GetTop()
if x and y then
state.positions[state.key] = { x = x, y = y,
width = state.frame:GetWidth(), height = state.frame:GetHeight() }
Position(state, state.positions[state.key])
end
end
local function Begin()
if not API.AreMoveModifiersDown() then return end
local state = this.resourceState
state.dragging = true
if this == state.grip then state.frame:StartSizing("BOTTOMRIGHT")
else state.frame:StartMoving() end
end
local function Stop() Finish(this.resourceState) end
local function MouseDown()
if arg1 ~= "RightButton" or not API.AreMoveModifiersDown() then return end
local state = this.resourceState
Finish(state)
state.positions[state.key] = nil
Position(state)
end
module.enable = function(self)
if self.scheduled then return end
self.scheduled = true
local function Setup()
if self.states then return end
self.states = {}
ShaguTweaks_config.PersonalResourceDisplay = ShaguTweaks_config.PersonalResourceDisplay or {}
local positions = ShaguTweaks_config.PersonalResourceDisplay
local scale = PlayerFrame:GetEffectiveScale() / UIParent:GetEffectiveScale()
local big = ShaguTweaks_config[T["Unit Frame Big Health for Player"]] == 1
local width = PlayerFrameHealthBar:GetWidth() + 8
local healthHeight = PlayerFrameHealthBar:GetHeight() + 6
local powerHeight = big and healthHeight or PlayerFrameManaBar:GetHeight() + 6
local x, y = UIParent:GetWidth() / scale / 2 - width / 2,
UIParent:GetHeight() / scale / 2 - 120
local function Add(key, source, height, top, attachedTo)
local frame = CreateFrame("Frame", "ShaguTweaksResource" .. key, UIParent)
frame:SetScale(scale)
frame:SetFrameStrata(PlayerFrame:GetFrameStrata())
frame:SetFrameLevel(PlayerFrame:GetFrameLevel() + 3)
frame:SetMovable(true); frame:SetResizable(true); frame:SetClampedToScreen(true)
frame:SetMinResize(64, 18); frame:SetMaxResize(600, 160)
frame:SetBackdrop(border)
local color = ShaguTweaks.UIBorder
frame:SetBackdropBorderColor(color and color.r or 1, color and color.g or 1, color and color.b or 1, 1)
local state = { frame = frame, key = key, positions = positions,
default = { x = x, y = top, width = width, height = height } }
if not attachedTo then table.insert(self.states, state) end
frame.resourceState = state
Position(state, positions[key])
local bar = CreateFrame("StatusBar", nil, frame)
frame.bar = bar
bar.unit = "player"
bar.personalResourceDisplay = true
bar:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -3)
-- Update dimensions explicitly: anchored-child size notifications can
-- precede resolved geometry while the holder is being resized.
local function ResizeBar()
bar:SetWidth(math.max(1, frame:GetWidth() - 8))
bar:SetHeight(math.max(1, frame:GetHeight() - 6))
end
frame:SetScript("OnSizeChanged", ResizeBar)
ResizeBar()
local background = bar:CreateTexture(nil, "BACKGROUND")
background:SetTexture(0, 0, 0, .5); background:SetAllPoints(bar)
local text = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
text:SetPoint(key == "Health" and "RIGHT" or "CENTER", bar,
key == "Health" and "RIGHT" or "CENTER", key == "Health" and -4 or 0, 0)
local percent
if key == "Health" then
percent = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
percent:SetPoint("LEFT", bar, "LEFT", 4, 0)
end
local function TextureAppearance()
local texture = source:GetStatusBarTexture()
local path = texture and texture:GetTexture() or "Interface\\TargetingFrame\\UI-StatusBar"
if state.texturePath ~= path then
state.texturePath = path
bar:SetStatusBarTexture(path)
end
end
local function ColorAppearance()
local r, g, b = source:GetStatusBarColor()
if state.r ~= r or state.g ~= g or state.b ~= b then
state.r, state.g, state.b = r, g, b
bar:SetStatusBarColor(r, g, b, 1)
end
end
local function Values()
local minimum, maximum = source:GetMinMaxValues()
local value = source:GetValue() or 0
maximum = maximum or 0
minimum = minimum or 0
local rangeChanged = state.minimum ~= minimum or state.maximum ~= maximum
if rangeChanged then
state.minimum = minimum
bar:SetMinMaxValues(minimum, maximum > 0 and maximum or 1)
end
if rangeChanged or state.value ~= value then bar:SetValue(value) end
if value ~= state.value or maximum ~= state.maximum then
state.value, state.maximum = value, maximum
text:SetText(ShaguTweaks.Abbreviate(value))
if percent then percent:SetText((maximum > 0 and math.floor(value / maximum * 100 + .5) or 0) .. "%") end
end
end
ShaguTweaks.hooksecurefunc(source, "SetValue", Values)
ShaguTweaks.hooksecurefunc(source, "SetMinMaxValues", Values)
ShaguTweaks.hooksecurefunc(source, "SetStatusBarColor", ColorAppearance)
ShaguTweaks.hooksecurefunc(source, "SetStatusBarTexture", function()
TextureAppearance(); ColorAppearance()
end)
TextureAppearance(); ColorAppearance(); Values()
if attachedTo then
frame:SetParent(attachedTo)
frame:SetScale(1)
frame:ClearAllPoints()
-- Overlap the tooltip rims so the secondary bar joins the power bar.
frame:SetPoint("TOP", attachedTo, "BOTTOM", 0, 5)
-- Keep both the secondary rim and its fill behind the main rim.
frame:SetFrameLevel(math.max(0, attachedTo:GetFrameLevel() - 2))
bar:SetFrameLevel(frame:GetFrameLevel() + 1)
local function ResizeAttached()
frame:SetWidth(attachedTo:GetWidth() * .6)
ResizeBar()
end
ShaguTweaks.HookScript(attachedTo, "OnSizeChanged", ResizeAttached)
ResizeAttached()
return frame
end
frame:RegisterForDrag("LeftButton")
frame:SetScript("OnDragStart", Begin); frame:SetScript("OnDragStop", Stop)
frame:SetScript("OnMouseDown", MouseDown)
frame:SetScript("OnHide", Stop)
local grip = CreateFrame("Button", nil, frame)
state.grip, grip.resourceState = grip, state
grip:SetWidth(12); grip:SetHeight(12); grip:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
grip:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
grip:RegisterForDrag("LeftButton")
grip:SetScript("OnDragStart", Begin); grip:SetScript("OnDragStop", Stop)
grip:SetScript("OnHide", Stop)
return frame
end
local health = Add("Health", PlayerFrameHealthBar, healthHeight, y)
local power = Add("Power", PlayerFrameManaBar, powerHeight, y - healthHeight - 2)
ShaguTweaks.PersonalResourceDisplay = { health = health, power = power }
local _, class = UnitClass("player")
local manaSource = PlayerFrameAlternatePowerBar
if class == "DRUID" and manaSource then
local mana = Add("Mana", manaSource, 18, 0, power)
ShaguTweaks.PersonalResourceDisplay.mana = mana
if ShaguTweaks.RegisterDruidManaTickBar then ShaguTweaks.RegisterDruidManaTickBar(mana.bar, manaSource) end
local function ManaVisibility()
if UnitPowerType("player") ~= 0 and manaSource:IsShown() then mana:Show()
else mana:Hide() end
end
ShaguTweaks.hooksecurefunc(manaSource, "Show", ManaVisibility)
ShaguTweaks.hooksecurefunc(manaSource, "Hide", ManaVisibility)
ShaguTweaks.hooksecurefunc(manaSource, "SetValue", ManaVisibility)
ManaVisibility()
end
if ShaguTweaks.RegisterResourceTickBar then ShaguTweaks.RegisterResourceTickBar(power.bar) end
if ShaguTweaks.RegisterHealingPredictionBar then
ShaguTweaks.RegisterHealingPredictionBar("player", health.bar)
end
local function UpdateHandles()
local unlocked = API.AreMoveModifiersDown()
for _, state in ipairs(self.states) do
if not unlocked then Finish(state) end
state.frame:EnableMouse(unlocked)
if unlocked then state.grip:Show() else state.grip:Hide() end
end
end
local events = CreateFrame("Frame")
self.events = events
if API.modifierstate then
events:RegisterEvent("MODIFIER_STATE_CHANGED")
events:SetScript("OnEvent", UpdateHandles)
else
events.elapsed = 0
events:SetScript("OnUpdate", function()
events.elapsed = events.elapsed + (arg1 or 0)
if events.elapsed >= .1 then events.elapsed = 0; UpdateHandles() end
end)
end
UpdateHandles()
end
API.AfterWorldReady(Setup, true)
end
+59
View File
@@ -0,0 +1,59 @@
-- Adapted from TokensWorth/ShaguTweaks-mods (MIT, original copyright GryllsAddons).
local T = ShaguTweaks.T
local Filter = ShaguTweaks.ExtrasChatFilter
local module = ShaguTweaks:register({
title = T["World Chat Hider"],
description = T["Hides World channel messages while inside an instance without leaving the channel."],
expansions = { ["vanilla"] = true, ["tbc"] = nil },
category = T["Chat & Social"],
enabled = nil,
})
local function IsInsideInstance()
if type(IsInInstance) ~= "function" then return false end
local inside = IsInInstance()
return inside == 1 or inside == true
end
local function IsWorldChannel()
-- Vanilla 1.12 CHAT_MSG_CHANNEL exposes the base channel name in arg9.
-- Custom channel names are not localized, so comparing the actual event is
-- both cheaper and safer than querying GetChannelName("world") every time.
if arg9 and string.lower(arg9) == "world" then
return true
end
-- Conservative fallback for clients that omit arg9: arg4 is the numbered
-- channel label (for example "5. World").
if arg4 then
local full = string.lower(arg4)
if full == "world" then return true end
local _, _, base = string.find(full, "^%d+%.%s*(.+)$")
if base == "world" then return true end
end
return false
end
local function FilterWorldChat(event)
if event ~= "CHAT_MSG_CHANNEL" then return false end
if not IsInsideInstance() then return false end
return IsWorldChannel()
end
module.enable = function(self)
if Filter then
Filter:Register("WorldChatHider", FilterWorldChat)
end
end
module.disable = function(self)
if Filter then
Filter:Unregister("WorldChatHider")
end
end
+17 -12
View File
@@ -22,6 +22,15 @@ end
local partyUnits = { "party1", "party2", "party3", "party4" } local partyUnits = { "party1", "party2", "party3", "party4" }
local UPDATE_INTERVAL = .1 local UPDATE_INTERVAL = .1
local partyNames = {}
local function RefreshPartyNames()
for name in pairs(partyNames) do partyNames[name] = nil end
for i = 1, 4 do
local name = UnitName(partyUnits[i])
if name then partyNames[name] = true end
end
end
local function SetAllPointsOffset(frame, parent, offset) local function SetAllPointsOffset(frame, parent, offset)
frame:SetPoint("TOPLEFT", parent, "TOPLEFT", offset, -offset) frame:SetPoint("TOPLEFT", parent, "TOPLEFT", offset, -offset)
@@ -61,7 +70,7 @@ local function UpdateIcon(frame, fallback)
if not unit and not name then return end if not unit and not name then return end
local icon = _G[frame:GetName() .. "Icon"] local icon = _G[frame:GetName() .. "Icon"]
if icon then icon:SetTexture() end if icon and icon:GetTexture() then icon:SetTexture() end
-- create icon if not yet existing -- create icon if not yet existing
if not frame.texture then if not frame.texture then
@@ -70,13 +79,7 @@ local function UpdateIcon(frame, fallback)
end end
-- check if unit is in same party group -- check if unit is in same party group
local ingroup local ingroup = name and partyNames[name]
for i = 1, 4 do
if UnitName(partyUnits[i]) == name then
ingroup = true
break
end
end
-- update texture according to raid/group state -- update texture according to raid/group state
if ingroup and frame.texture.ingroup ~= "PARTY" then if ingroup and frame.texture.ingroup ~= "PARTY" then
@@ -97,10 +100,10 @@ local function UpdateIcon(frame, fallback)
end end
local color = class and RAID_CLASS_COLORS[class] local color = class and RAID_CLASS_COLORS[class]
if color then local r, g, b = color and color.r or .5, color and color.g or 1, color and color.b or .5
frame.texture:SetVertexColor(color.r, color.g, color.b) if frame.texture.classR ~= r or frame.texture.classG ~= g or frame.texture.classB ~= b then
else frame.texture.classR, frame.texture.classG, frame.texture.classB = r, g, b
frame.texture:SetVertexColor(.5,1,.5) frame.texture:SetVertexColor(r, g, b)
end end
end end
@@ -108,11 +111,13 @@ local function UpdateMapColors()
this.elapsed = (this.elapsed or UPDATE_INTERVAL) + arg1 this.elapsed = (this.elapsed or UPDATE_INTERVAL) + arg1
if this.elapsed < UPDATE_INTERVAL then return end if this.elapsed < UPDATE_INTERVAL then return end
this.elapsed = 0 this.elapsed = 0
local partyRefreshed = false
local buttons = this.buttons local buttons = this.buttons
for i = 1, table.getn(buttons) do for i = 1, table.getn(buttons) do
local frame = _G[buttons[i][1]] local frame = _G[buttons[i][1]]
if frame and frame:IsVisible() then if frame and frame:IsVisible() then
if not partyRefreshed then RefreshPartyNames(); partyRefreshed = true end
UpdateIcon(frame, buttons[i][2]) UpdateIcon(frame, buttons[i][2])
end end
end end
+160
View File
@@ -0,0 +1,160 @@
local _G = ShaguTweaks.GetGlobalEnv()
local T = ShaguTweaks.T
local API = ShaguTweaks.API
local hooksecurefunc = ShaguTweaks.hooksecurefunc
local module = ShaguTweaks:register({
title = T["Reveal World Map"],
description = T["Reveals unexplored world map areas and shows exploration hints."],
expansions = { ["vanilla"] = true, ["tbc"] = false },
category = T["Minimap & World Map"],
maintainer = "@shagu (GitHub)",
enabled = true,
config = {
["map.reveal.marker"] = "on",
}
})
module.enable = function(self)
-- do not load if another map addon is controlling the world map
if Cartographer then return end
if METAMAP_TITLE then return end
local enabled = true
local overlayTextures = {}
local exploreFrames = {}
local mapreveal = {}
mapreveal.onmap = CreateFrame("CheckButton", "shagutweaks_mapreveal_onmap", WorldMapFrame, "UICheckButtonTemplate")
mapreveal.onmap.text = _G["shagutweaks_mapreveal_onmapText"]
mapreveal.onmap:SetWidth(14)
mapreveal.onmap:SetHeight(14)
mapreveal.onmap:SetPoint("TOPLEFT", WorldMapButton, "TOPLEFT", 1, 19)
mapreveal.onmap.text:SetPoint("LEFT", mapreveal.onmap, "RIGHT", 2, 0)
mapreveal.onmap.text:SetText(T["Reveal Unexplored"])
mapreveal.onmap:SetScript("OnShow", function()
this:SetChecked(enabled)
end)
mapreveal.onmap:SetScript("OnClick", function()
enabled = this:GetChecked() and true or false
WorldMapFrame_Update()
end)
local function HideRevealLayer()
for _, texture in ipairs(overlayTextures) do
texture:Hide()
end
for _, frame in ipairs(exploreFrames) do
frame:Hide()
end
end
local function ExploreEnter()
WorldMapTooltip:ClearLines()
WorldMapTooltip:SetOwner(this, "ANCHOR_TOP")
WorldMapTooltip:AddLine(T["Exploration Point"]..":", .3, 1, .8)
WorldMapTooltip:AddLine(this.name or "?", 1, 1, 1)
WorldMapTooltip:Show()
end
local function ExploreLeave()
WorldMapTooltip:Hide()
end
local function GetExploreCenter(overlay)
if overlay.hitRectLeft and overlay.hitRectRight
and overlay.hitRectTop and overlay.hitRectBottom then
return (overlay.hitRectLeft + overlay.hitRectRight) / 2,
(overlay.hitRectTop + overlay.hitRectBottom) / 2
end
if overlay.offsetX and overlay.offsetY
and overlay.textureWidth and overlay.textureHeight then
return overlay.offsetX + overlay.textureWidth / 2,
overlay.offsetY + overlay.textureHeight / 2
end
end
local function UpdateRevealLayer()
HideRevealLayer()
if not enabled then return end
-- ClassicAPI reads WorldMapOverlay.dbc directly from the active client,
-- including custom Turtle-like zones and their real tile layout.
local overlays = API.GetUnexploredMapTextures()
if not overlays then return end
local textureIndex = 0
local exploreIndex = 0
for _, overlay in ipairs(overlays) do
if overlay.tiles then
for _, tile in ipairs(overlay.tiles) do
if tile.file and tile.width and tile.height
and tile.offsetX and tile.offsetY then
textureIndex = textureIndex + 1
local texture = overlayTextures[textureIndex]
if not texture then
texture = WorldMapDetailFrame:CreateTexture(nil, "ARTWORK")
overlayTextures[textureIndex] = texture
end
texture:ClearAllPoints()
texture:SetWidth(tile.width)
texture:SetHeight(tile.height)
texture:SetTexCoord(0, tile.texCoordX or 1, 0, tile.texCoordY or 1)
texture:SetPoint("TOPLEFT", WorldMapDetailFrame, "TOPLEFT", tile.offsetX, -tile.offsetY)
texture:SetTexture(tile.file)
texture:SetVertexColor(.4, .4, .4, 1)
texture:Show()
end
end
end
if self.config["map.reveal.marker"] == "on" then
local x, y = GetExploreCenter(overlay)
if x and y then
exploreIndex = exploreIndex + 1
local explore = exploreFrames[exploreIndex]
if not explore then
explore = CreateFrame("Frame", nil, WorldMapDetailFrame)
explore:SetWidth(16)
explore:SetHeight(16)
explore:EnableMouse(true)
explore:SetFrameLevel(255)
explore:SetScript("OnEnter", ExploreEnter)
explore:SetScript("OnLeave", ExploreLeave)
explore.tex = explore:CreateTexture(nil, "OVERLAY")
explore.tex:SetTexture("Interface\\WorldMap\\WorldMap-MagnifyingGlass")
explore.tex:SetBlendMode("ADD")
explore.tex:SetTexCoord(.08, .92, .08, .92)
explore.tex:SetAllPoints()
exploreFrames[exploreIndex] = explore
end
explore:ClearAllPoints()
explore:SetPoint("CENTER", WorldMapDetailFrame, "TOPLEFT", x, -y)
explore.name = API.GetAreaInfo(overlay.areaID)
or overlay.textureName
or "?"
explore:Show()
end
end
end
end
-- Keep Blizzard's normal explored-map rendering untouched and append only
-- ClassicAPI's unexplored overlays after the native update.
hooksecurefunc("WorldMapFrame_Update", UpdateRevealLayer)
if WorldMapFrame:IsShown() then
UpdateRevealLayer()
end
end
+6 -1
View File
@@ -18,7 +18,12 @@ module.enable = function(self)
self.exp:SetFrameStrata("HIGH") self.exp:SetFrameStrata("HIGH")
self.exp.text = self.exp:CreateFontString(nil, "OVERLAY", "GameFontWhite") self.exp.text = self.exp:CreateFontString(nil, "OVERLAY", "GameFontWhite")
self.exp.text:SetFont("Fonts\\ARIALN.TTF", 14, "OUTLINE") -- Arial Narrow with an outline lets the outlines of neighbouring digits
-- merge, so the numbers looked like they overlapped. A drop shadow keeps
-- the text readable on the bar and leaves the gaps between glyphs open.
self.exp.text:SetFont("Fonts\\ARIALN.TTF", 14)
self.exp.text:SetShadowColor(0, 0, 0, 1)
self.exp.text:SetShadowOffset(1, -1)
self.exp.text:SetPoint("CENTER", MainMenuExpBar, "CENTER", 0, 1) self.exp.text:SetPoint("CENTER", MainMenuExpBar, "CENTER", 0, 1)
self.exp.text:SetJustifyH("CENTER") self.exp.text:SetJustifyH("CENTER")
self.exp.text:SetTextColor(1, 1, 1) self.exp.text:SetTextColor(1, 1, 1)
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Some files were not shown because too many files have changed in this diff Show More