forked from Dusk/ShaguTweaks-ClassicAPI
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc566feaf | ||
|
|
e43d93a524 | ||
|
|
b9912256bf | ||
|
|
40762969a1 | ||
|
|
9101a0b32f | ||
|
|
a81ca54e0c | ||
|
|
224f6db154 | ||
|
|
4f0c80c58e | ||
|
|
eb9faafafd | ||
|
|
662de4fced | ||
|
|
f393a827c7 | ||
|
|
ffa1ceb83b | ||
|
|
8b62382660 | ||
|
|
97ff7a7d84 | ||
|
|
5db1aa9c41 | ||
|
|
d51d2556ef | ||
|
|
48ee0747c3 | ||
|
|
dfe8cea692 | ||
|
|
7da9684025 | ||
|
|
e557dbe058 | ||
|
|
19bc58620b | ||
|
|
0784e2e5bb | ||
|
|
767b458aa6 | ||
|
|
9ef84b50fe | ||
|
|
28da226764 | ||
|
|
b47b2ec06e | ||
|
|
8e47666644 | ||
|
|
338135be34 | ||
|
|
2c58ce2aa3 | ||
|
|
b2c30df0e1 | ||
|
|
3c5f36b67a | ||
|
|
6de8e93496 | ||
|
|
00c6903a81 |
+94
-66
@@ -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`).
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
Executable
+21
@@ -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.
|
||||||
@@ -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.
|
[](https://github.com/brues-code/ClassicAPI)
|
||||||
|
[](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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Golden UI world map with the OctoBus Transports panel.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Modifier-key previews for movable action bars, cast bars and personal resources.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Advanced Options with search and grouped settings.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## ✨ 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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
+54
-371
@@ -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
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Executable
+40
@@ -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
|
||||||
Executable
+107
@@ -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
|
||||||
@@ -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)
|
||||||
Executable
+34
@@ -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
|
||||||
Executable
+15
@@ -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
|
||||||
Executable
+15
@@ -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
|
||||||
Executable
+138
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
Executable
+167
@@ -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
|
||||||
Executable
+17
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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,
|
||||||
|
|||||||
Executable
+669
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+68
@@ -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
|
||||||
Executable
+45
@@ -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
|
||||||
Executable
+92
@@ -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
|
||||||
@@ -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]
|
||||||
|
|||||||
Executable
+19
@@ -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
|
||||||
Executable
+57
@@ -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
@@ -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
|
||||||
|
|||||||
Executable
+99
@@ -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
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Executable
+146
@@ -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
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Executable
+134
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+243
@@ -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
|
||||||
Executable
+171
@@ -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
|
||||||
Executable
+78
@@ -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
@@ -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.
|
||||||
|
|||||||
Executable
+162
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Executable
+126
@@ -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
|
||||||
Executable
+126
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Executable
+147
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Executable
+59
@@ -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
@@ -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
|
||||||
|
|||||||
Executable
+160
@@ -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
@@ -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
Reference in New Issue
Block a user