AnySCT 0.2.0

This commit is contained in:
2026-10-15 13:52:38 +02:00
commit 521342f1b6
20 changed files with 4399 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
## Interface: 11200
## Title: AnySCT
## Notes: Blizzard-pet-style floating combat text for any units you pick: tanks, their pets and totems, mobs, enemy players.
## Author: ShempError
## Version: 0.2.0
## X-Website: https://octowow.st/git/ShempError/AnySCT
## OptionalDeps: pfUI, CT_RaidAssist, oRA, TWThreat
## SavedVariables: AnySCTDB
parse.lua
plates.lua
core.lua
menu.lua
widgets.lua
sct.lua
minimap.lua
options.lua
+44
View File
@@ -0,0 +1,44 @@
# Changelog
All notable changes to AnySCT are documented here.
## 0.2.0 — 2026-10-15
### Changed
- **Every hit is shown**: the default line limits are now 16 per mob (was 4) and 60 on screen (was 16), sliders up to 30 / 100, so a burst of about 10 hits per second is no longer recycled. Saved limits that still equal the old defaults are raised; limits you chose yourself stay. A tall stack is kept below the top screen edge.
- "Tank" is now **targetSCT** in every visible string (menu entry "Mark as targetSCT", options, chat
output, minimap tooltip). Nothing changed in behaviour. Saved settings are migrated on first load;
`/asct tank <name>` still works as an alias of `add`.
- The options window is regrouped by function into four tabs (targetSCTs, Text, Colours, Placement) with
section headers, and **every control's tooltip now also shows when you hover its label**.
### Added
- **targetSCT name** option (Never / Only with several targetSCTs / Always): the unit's name, or the
owner's for pets and totems, first on the line under the number (`Name - Spell - Victim`).
- **Own colour per targetSCT** (off by default): each listed unit gets its own colour, picked from a
palette so that colours differ well (the first is the client's pet orange). Pick your own with the
swatch in the list; **Auto colours** (or `/asct colours`) reassigns them. Saved with the list.
- **Experimental GO / WAIT label** from the server's threat data (like TWThreat): shows over your own
elite or boss target how much threat lead the targetSCT has over you. Options: enable, required buffer
(0-7500), and a Threat switch per list row. `/asct threat` prints its state. Optional *Record threat
trace* (off by default) writes the received data to `<WoW>\imports\anysct_threat.txt` for bug reports.
### Fixed
- nampower builds that deliver spell damage and miss events without the `NP_Enable...` CVars are detected through `GetNampowerVersion`, so spell events no longer fall back to chat rows. An opt-in event ledger (trace option or `/asct debug`) writes `<WoW>\imports\anysct_events.txt`.
- The options window sits above world nameplates (frame strata DIALOG).
### Removed
- The hit-count GO / WAIT indicator (`goAfterHits`); the saved option is dropped.
## 0.1.0 — 2026-10-14
First release.
- A short list of units (right-click menu **Mark as tank**, `/asct add`, options window, or imported
CT_RaidAssist / oRA main tanks); saved per character.
- Their melee, special attacks and misses float over the **victim** in the client's own world-text style;
pets and totems of a listed unit are shown under the owner.
- Heals received by a listed unit (green `+amount`, optional healer name, HoT ticks, your own heals).
- Placement over the victim's nameplate (clamped to the screen edge) or at a movable fixed anchor.
- Sources: nampower events when present, otherwise the chat log; SuperWoW GUIDs when present.
- Options window with tabs, colour pickers and a minimap button; zero idle cost.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ShempError
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.
+317
View File
@@ -0,0 +1,317 @@
# AnySCT
Blizzard-pet-style floating combat text for **any units you pick**, for TurtleWoW / WoW 1.12.1 (Vanilla).
Put a tank, a pet, a totem, a boss or an enemy player on a short list (a *targetSCT*). Every hit, crit,
miss, dodge, parry and heal of those units then floats over the **victim** in the same style as the
client's own pet damage text. Use it to watch what your tank does before you open up, to follow a boss
or an enemy player, or to see a pet's damage on the mob it hit.
AnySCT shows only the events of the units on its list. It never touches Blizzard's CombatText,
`SHOW_COMBAT_TEXT` or another scrolling-combat-text addon, and it never shows your own events (if you
are on the list, your own SCT addon stays the only one that shows your damage).
<img src="screenshots/hero-miss-resist-wait.png" alt="Miss and Resist floating over training dummies, with a WAIT label under the dummy" width="100%">
*A listed tank hitting the training dummies in Orgrimmar, seen from another character: an orange melee
"Miss", a gold spell "Resist", and the experimental WAIT label under the dummy being attacked.*
## What's new — 0.2.0 · 2026-10-15
- **targetSCT.** What used to be called "tank" is now *targetSCT* everywhere (menu, options, commands
keep working). Each targetSCT can show its **name** next to its numbers (Never / only with several /
always) and gets its **own colour**, picked automatically so they are easy to tell apart.
- **Every hit is shown.** The default limits are 16 lines per mob and 60 on screen, so a burst of about
ten hits per second is no longer recycled.
- **nampower spell events are used on every nampower build**: builds that deliver spell damage and miss
events without the `NP_Enable...` CVars were treated as "no events" and fell back to chat rows.
- **Regrouped options with tooltips on every label**, four tabs: targetSCTs, Text, Colours, Placement.
The window now sits above world nameplates.
- **Experimental GO / WAIT label** over your own elite target: how much threat lead the targetSCT has
over you, from the server's threat data (needs a group; see [GO / WAIT](#go--wait-threat-label-experimental)).
**Version history** — details in [CHANGELOG.md](CHANGELOG.md):
- **0.2.0** (2026-10-15) — targetSCT rename, name option, per-targetSCT colours, regrouped options with
label tooltips, higher line limits, experimental threat-based GO / WAIT.
- **0.1.0** (2026-10-14) — first release: list, victim-routed text in the client's own style, nameplate
placement, heals, options and minimap button.
## What you see
Everything floats over the **victim** of the event, never over the source:
- **Melee in orange, special attacks in gold, heals in green.** Numbers use the client's own damage font,
no outline, a black shadow. Ordinary hits rise and fade; **crits pop** at a larger size and stay in place.
- **Words for what did not land**: Miss, Dodge, Parry, Block, Resist, Immune, ... in the same style.
- **Every hit is shown**: up to 16 lines per mob and 60 on screen by default, so a fast burst is not
recycled early (both are sliders on the Text tab).
- Pets and totems of a listed unit are shown under their owner; Fire Nova, traps and other area damage
land on the mob they hit.
<img src="screenshots/crit-block-go.png" alt="A large crit number over a dummy, a grey Block and a GO label" width="100%">
*A crit "366" popping over the Heroic Training Dummy, a "Block" in grey, and a GO label partly covered by
the crit.*
## GO / WAIT threat label (experimental)
Off by default. Shows under the nameplate of **your own target** how much threat lead the targetSCT has
over you, so you can see when you can open without pulling aggro: `GO +4210` (green) or
`WAIT 1250 / 1500` (orange; your buffer / the buffer you asked for).
<table>
<tr>
<td align="center" width="50%">
<a href="screenshots/wait.png"><img src="screenshots/wait.png" width="420" alt="WAIT label under the Heroic Training Dummy, a Dodge over the Expert Training Dummy"></a><br>
<sub><b>WAIT</b> — "WAIT 1211 / 1500": the buffer is still below the required 1500, a "Dodge" floats above</sub>
</td>
<td align="center" width="50%">
<a href="screenshots/go.png"><img src="screenshots/go.png" width="420" alt="GO label under the Heroic Training Dummy, a Dodge over the Expert Training Dummy"></a><br>
<sub><b>GO</b> — "GO +3225": the lead is large enough, opening is safe</sub>
</td>
</tr>
</table>
- **Data:** the server's threat answers, the same ones TWThreat uses. It works only in a party or raid,
only on your own target, and only while that target is an elite or boss in combat. AnySCT listens
passively to answers that are already coming in (for example for TWThreat) and sends its own request at
most once a second, only when no usable answer arrived in the last moment. Nothing is sent and nothing
runs otherwise.
- **Rule:** `buffer = threat(targetSCT) * f - threat(you)`, with f = 1.1 if you fight the mob in melee, 1.3
if you do not (1.1 if you are not in the answer). That is your distance to the server's pull threshold
(110 % melee, 130 % ranged). GO when the targetSCT holds aggro and the buffer is at least the required
buffer, otherwise WAIT. With several enabled units the one holding aggro is used; without a fresh answer
(3 s) the label hides.
- **Options** (targetSCTs tab): *Enable GO / WAIT (experimental)*; *Required threat buffer* (0-7500 in steps
of 100, default 1500; 0 means GO as soon as the targetSCT holds aggro and you are below the pull
threshold; a typical raid burst over 3 s is a few thousand threat, Execute crits can exceed 10000, so
raise it to match your own burst); a *Threat* switch per list row (on for players, off for pets, totems,
mobs and hostile players).
- **Experimental:** the packet format is taken from the TWThreat addon and has not been confirmed on every
server build. If it does not work for you, switch on *Record threat trace* (Placement tab, Debug) and
send `<WoW>\imports\anysct_threat.txt` with your bug report (needs SuperWoW). The trace is off by default
and nothing is written unless you switch it on.
## Features
- Numbers and words (Miss, Dodge, Parry, Block, Immune, ...) in the client's own world-text style: the
client damage font, no outline, black shadow, orange for melee, gold for special attacks, crits pop in
place, other texts rise and fade.
- Text floats over the **victim's nameplate**, clamped to the screen edge when the plate is off screen,
or at a fixed anchor you can move. Fire Nova, traps, pets and totems land on the mob they hit.
- **Pets and totems** of a listed unit are shown under the owner (see Requirements for how the owner is
found).
- **Heals** received by a listed unit float over it as green `+amount` (optionally with the healer's name,
HoT ticks and your own heals).
- **Names**: the targetSCT's name on the line under the number (`Name - Spell - Victim`).
- **Colours**: one colour per targetSCT (automatic palette, or pick your own), class colours, or a colour
scheme per kind of hit.
- Minimap button, full options window, nothing is polled when idle.
## Requirements
AnySCT runs on a plain 1.12.1 client with the default UI and needs no other addon. Optional helpers make
it more exact:
| optional | what it adds |
|---|---|
| **SuperWoW** | Exact unit GUIDs: nameplates are matched by GUID instead of by name, pets and totems are traced to their owner, units outside your group can be learned by GUID. Also needed for the threat trace file. |
| **nampower** | Melee, spell and heal events with GUIDs instead of chat-log parsing: more reliable, and it works for units whose chat rows are not visible. AnySCT switches the needed CVars on automatically. |
| **CT_RaidAssist / oRA** | Only needed to import their main tank list as targetSCTs (option, on). Without them you add targetSCTs by hand. |
| **TWThreat** | Not needed: the GO / WAIT label asks the server itself. If TWThreat runs, its answers are reused, so fewer requests go out. |
| **pfUI** | Its damage font and nameplates are honoured. |
Without SuperWoW and nampower everything still works from the chat log. **Enemy nameplates must be on
(V key)** for the text to float over the mob; without a plate it falls back to the fixed anchor.
## Install
1. Copy the folder `AnySCT` into `<WoW>\Interface\AddOns\`. The folder name must be exactly `AnySCT`.
2. Start the client (new addon files need a restart, not just `/reload`).
## Usage
Nothing is shown until a unit is listed:
- Right-click a unit (target frame, party or raid frames, NPC targets) and choose **Mark as targetSCT**
(**Unmark targetSCT** removes it), or
- type `/asct add <name>` (no name: your target), or
- use the list in the options window (`/asct options`, or right-click the minimap button).
Imported CT_RaidAssist / oRA main tanks are added automatically (option). The list is saved per
character.
### Commands (`/anysct`, alias `/asct`)
| command | what |
|---|---|
| `add <name>` | add a unit (no name: your target) |
| `remove <name>` | remove a unit |
| `list` | print the list |
| `clear` | empty the list |
| `colours` | give every listed unit a new automatic colour |
| `place clamp/anchoroff/anchor` | where the text appears |
| `unlock` / `lock` | move the fixed anchor (drag the box; a demo stream plays) |
| `demo` | sample text at your target's nameplate, to compare with your own numbers |
| `threat` | one line with the state of the GO / WAIT label |
| `debug` | toggle the event / drop trace in the chat frame |
| `mem` | memory self-test (idle and a demo burst) |
| `options` | open the options window |
| `reset` | reset all options (the list is kept) |
## Options
`/asct options`, or right-click the minimap button. The window is draggable, closes with ESC and sits above
world nameplates. Every control has a tooltip, and the tooltip also shows when you hover its label. **Defaults**
(bottom row) restores every setting below; the targetSCT list and its colours are kept.
### Tab 1 — targetSCTs
<img src="screenshots/options-targetscts.png" alt="Options window, targetSCTs tab" width="100%">
*General, What to show and the GO / WAIT section on the left; the targetSCT list with a colour swatch and a
Threat switch per row, the list buttons and the main-tank import on the right.*
| control | what it does | default |
|---|---|---|
| Enable AnySCT | master switch | on |
| Only while the targetSCT fights | hide text while the listed unit itself is not in combat (shown if its state cannot be read) | off |
| Extend combat log range (200) | sets the combat-log range CVars to 200 on login so raid targetSCTs outside your party are not silent | on |
| Show minimap button | left click unlocks / locks the anchor, right click opens the options | on |
| Show special attacks | also show Heroic Strike, Stormstrike and other ability rows (gold) | on |
| Include totems | show totem damage whose owner is a listed targetSCT | on |
| Guess totem owner | when no owner is known, assume the only listed shaman; can misattribute | off |
| Show heals on targetSCTs | heals received by a listed unit float over it as green `+amount` | on |
| Show HoT ticks | also heal-over-time ticks (chat rows only, can be busy) | off |
| Include my own heals | your own heals on listed units too | on |
| Enable GO / WAIT (experimental) | the threat label described above | off |
| Required threat buffer | slider 0-7500 in steps of 100 | 1500 |
| targetSCT list | one row per unit: colour swatch (click to pick), **Threat** switch (does the unit feed the GO / WAIT label); click a row to select it | empty |
| Add / Remove / Clear | add the typed name (empty box: your target), remove the selected row, empty the list | |
| Auto colours | give every unit a new automatic colour; replaces hand-picked colours | |
| Import CT_RA / oRA main tanks | add the main tanks of those addons that are in your group | on |
### Tab 2 — Text
<img src="screenshots/options-text.png" alt="Options window, Text tab" width="100%">
*Text content (name modes, spell name, healer name, glancing marks) above, Size & motion (font, direction and
six sliders including the line limits) below.*
| control | what it does | default |
|---|---|---|
| targetSCT name | the source's name (the owner's for pets and totems) first on the line under the number: Never / Only with several targetSCTs / Always | Only with several |
| Victim name | the victim's name under the text: Never / Anchor only / Always | Never |
| Show spell name | ability name under the number for special attacks | off |
| Show healer name | healer's name under heal numbers | on |
| Mark glancing / crushing | add the word after the number | off |
| Font | Client (the damage font, honours pfUI) / FrizQT / Skurri / Arial Narrow | Client |
| Direction | text rises (Up) or sinks (Down) | Up |
| Number size | factor on the engine size, 0.5-3 | 1.00 |
| Word size | same for Miss / Dodge / Parry ... | 1.00 |
| Rise (px) | how far a normal text rises, 0-300; crits stay in place | 120 |
| Jitter (px) | random horizontal offset per line, 0-40 | 0 |
| Max lines | lines on screen over all mobs, 3-100; the oldest is recycled at the limit | 60 |
| Max lines per mob | lines over one mob, 1-30; lines live 1.5 s, so 16 shows every hit of a burst of about 10 per second | 16 |
### Tab 3 — Colours
<img src="screenshots/options-colours.png" alt="Options window, Colours tab" width="100%">
*Per-targetSCT colours, the text colour switches, and the three colour swatches with Reset colours.*
| control | what it does | default |
|---|---|---|
| Own colour per targetSCT | every listed unit gets its own colour (pets and totems use the owner's) | off |
| Class colours | numbers and words take the source's class colour | off |
| Colour coding | crit yellow, miss / dodge / parry red, block blue, absorb / resist / immune purple; off: everything orange | off |
| Melee colour | colour picker with live preview | orange `FF8400` |
| Spell colour | colour of special attacks | gold `FFDE00` |
| Heal colour | colour of heal numbers | green |
| Reset colours | back to the three defaults | |
Precedence: heal colour, then the unit's own colour, then class colour, colour coding and the melee / spell
colours. The colour picker opens in front of the options window.
### Tab 4 — Placement
<img src="screenshots/options-placement.png" alt="Options window, Placement tab" width="100%">
*Placement and no-nameplate behaviour, plate offsets and range, the fixed anchor with Unlock / Reset / Demo, the
list label, and the Debug section.*
| control | what it does | default |
|---|---|---|
| Placement | Nameplate, clamp (over the victim's plate, held at the screen edge) / Nameplate, else anchor / Always anchor | Nameplate, clamp |
| No nameplate | when the victim has no visible plate: Anchor / Hide | Anchor |
| Plate X offset | horizontal shift from the plate centre, -150..150 | 40 |
| Plate Y offset | vertical shift from the plate's top edge, -100..100 | 0 |
| Edge margin (px) | distance to the screen edge at which a plate counts as off screen, 0-200 | 40 |
| Nameplate range | sets the nameplate range CVars on login (SuperWoW / nampower), 0-80; 0 leaves them alone | 0 |
| Anchor X / Anchor Y | position of the fixed anchor from the screen centre, -800..800 / -500..500 | 0 / 120 |
| Unlock anchor / Lock anchor | drag the anchor box; a demo stream plays while unlocked | |
| Reset anchor | back to screen centre, 120 px up | |
| Demo | hit, hit, crit, miss, dodge, parry, block at your target's plate (anchor without a target) | |
| Show targetSCT list label | a `targetSCTs: ...` line above the anchor | off |
| Record threat trace | writes the received threat data to `<WoW>\imports\anysct_threat.txt` (needs SuperWoW), for bug reports | off |
Enemy nameplates must be on (V key) for text over the mob.
## Where the text appears
The text always floats over the **victim** of the event, not over the source. Placement modes: over the
nameplate clamped to the screen edge (default), over the nameplate with the fixed anchor as fallback,
or always the fixed anchor. At the fixed anchor the victim's name is always added because the position no
longer identifies the mob. If no plate addon or plate is found the text simply uses the fixed anchor.
## Colours and names
With **Own colour per targetSCT** (Colours tab) every listed unit gets its own colour, saved with the list.
New units take the palette colour that is farthest from the colours already in use; the first is the
client's pet orange. Click the swatch in the list to change one (kept until you press **Auto colours**).
Precedence: heal colour, then the unit's own colour, then class colour, colour scheme and the melee /
spell colours. The **targetSCT name** option (Never / Only with several targetSCTs / Always) puts the
unit's name first on the line under the number; for pets and totems it is the owner's name.
## Sources of events
- **nampower** (preferred): `AUTO_ATTACK_OTHER` for melee, `SPELL_DAMAGE_EVENT_OTHER` and
`SPELL_MISS_OTHER` for spells, `SPELL_HEAL_BY_OTHER` for heals; matched by GUID.
- **Chat log** (fallback, and for units without a known GUID): only the channels that are needed are
registered. Option "Extend combat log range" (default on) sets the combat-log range to 200 on login
because raid members outside your party are otherwise silent beyond roughly 50 yards; the server still
caps the real range.
### CVars this addon sets
On login and when the matching option changes. They are **not restored** when you disable the addon.
- `NP_EnableAutoAttackEvents`, `NP_EnableSpellDamageEvents`, `NP_EnableSpellMissEvents`,
`NP_EnableSpellHealEvents` = 1 (only when nampower exposes them)
- `CombatLogRangeFriendlyPlayers`, `CombatLogRangeParty` = 200 (option "Extend combat log range")
- `NameplateRange`, `NP_NameplateDistance` (only when "Nameplate range" is above 0)
## Cost
Idle cost is zero: the animation frame has an `OnUpdate` only while a line is on screen, the combat tick
runs only in combat, and the threat ticker only while its conditions hold. FontStrings are pooled and
created on first use. `/asct mem` measures it in game.
## Limitations and known unknowns
- If you are the targetSCT yourself you see nothing from AnySCT (by design).
- DoT ticks are not shown.
- Attacks on the player by a listed unit come through the chat channels only; whether nampower reports
them is not known.
- With the chat path (no GUIDs), several same-named mobs cannot be told apart for victim routing.
- A totem is attributed only to a known owner (SuperWoW owner suffix, pet tokens). The option "Guess
totem owner" (off) assumes the only listed shaman and can be wrong.
- The text parameters are measured engine values and may differ slightly from the client's own text on
your setup; sizes, rise and offsets can be tuned in the options.
- The GO / WAIT label is experimental, see [GO / WAIT](#go--wait-threat-label-experimental).
- Localised clients: the client's GlobalStrings are read live.
## License
MIT, see [LICENSE](LICENSE).
+1641
View File
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
-- AnySCT menu.lua -- "Mark as targetSCT" in the unit right-click menus (target, party and raid frames;
-- pfUI uses the same Blizzard UnitPopup dropdowns). Hooks the 1.12 UnitPopup system:
-- UnitPopupButtons / UnitPopupMenus register the button, inserted before CANCEL
-- UnitPopup_ShowMenu sets the label: Mark as targetSCT / Unmark targetSCT
-- UnitPopup_HideButtons hides the entry for hostile units and non-players
-- UnitPopup_OnClick handles our value, everything else goes to the original
-- Everything is guarded so a missing menu table or function never errors.
AnySCT_Menu = {}
local M = AnySCT_Menu
local KEY = "ANYSCT_TOGGLE"
-- Menus the client opens for a unit: PLAYER (other players, incl. hostile), PARTY, RAID, FRIEND list.
-- NPC targets open "RAID_TARGET_ICON" (the title is the icon list); the key is added to it at
-- level 1 only and removed again for the nested icon submenu of the player menus.
local MENUS = { "PLAYER", "PARTY", "RAID", "FRIEND" }
M.KEY = KEY
local origShow, origHide, origClick
-- The name the menu is about: captured when the menu opened (ShowMenu wrapper), else the unit's
-- current name. Never dd.name: for NPC targets that is the menu title.
local function nameOf(dd)
if type(dd) ~= "table" then return nil end
if dd.anyName then return dd.anyName end
if dd.unit then return UnitName(dd.unit) end
return nil
end
local function listed(name)
return name ~= nil and AnySCT_Core.state.byName[name] ~= nil
end
function M.Label(name)
if listed(name) then return "Unmark targetSCT" end
return "Mark as targetSCT"
end
-- true when the entry must not be offered: no name, or the player themself (own events are never shown)
function M.Hidden(dd)
if type(dd) ~= "table" then return true end
local n = nameOf(dd)
if n == nil then return true end
return n == UnitName("player")
end
-- Add (or remove) the key in the NPC menu depending on the menu level.
local function syncIconMenu(which)
local menu = UnitPopupMenus["RAID_TARGET_ICON"]
if type(menu) ~= "table" then return end
local level = UIDROPDOWNMENU_MENU_LEVEL or 1
local at
for i = 1, table.getn(menu) do
if menu[i] == KEY then at = i end
end
if level == 1 and which == "RAID_TARGET_ICON" then
if not at then table.insert(menu, KEY) end
elseif at then
table.remove(menu, at)
end
end
-- Mark / unmark. unit (optional) lets the list learn the GUID of a unit outside the group.
function M.Toggle(name, unit)
local C = AnySCT_Core
if C.state.byName[name] then
C.RemoveSource(name)
C.Print(name .. " is no longer a targetSCT")
else
-- what is known about the unit decides the default of the threat switch (pets, mobs and hostile
-- players off; players on)
local info
if unit and UnitIsPlayer and UnitCanAttack then
info = { player = UnitIsPlayer(unit) and true or false, friendly = not UnitCanAttack("player", unit) }
end
C.AddSource(name, "manual", nil, info)
if unit and C.SeeUnit then C.SeeUnit(name, unit) end
-- no token from the menu (or it did not resolve): look at target / targettarget / mouseover
if C.ProbeLoose then C.ProbeLoose() end
C.Print(name .. " marked as targetSCT")
end
end
local function insertKey(menu)
if type(menu) ~= "table" then return end
local n = table.getn(menu)
local at = n + 1
for i = 1, n do
if menu[i] == KEY then return end
if menu[i] == "CANCEL" then at = i; break end
end
table.insert(menu, at, KEY)
end
-- Installs the hooks once. Returns true when the UnitPopup tables exist.
function M.Install()
if M.installed then return true end
if type(UnitPopupButtons) ~= "table" or type(UnitPopupMenus) ~= "table" then return false end
UnitPopupButtons[KEY] = { text = "Mark as targetSCT", dist = 0 }
for i = 1, table.getn(MENUS) do insertKey(UnitPopupMenus[MENUS[i]]) end
if type(UnitPopup_ShowMenu) == "function" then
origShow = UnitPopup_ShowMenu
UnitPopup_ShowMenu = function(dropdownMenu, which, unit, name, userData)
syncIconMenu(which)
local n = unit and UnitName(unit) or ((not unit) and name) or nil
if type(dropdownMenu) == "table" then dropdownMenu.anyName = n end
UnitPopupButtons[KEY].text = M.Label(n)
return origShow(dropdownMenu, which, unit, name, userData)
end
end
if type(UnitPopup_HideButtons) == "function" then
origHide = UnitPopup_HideButtons
UnitPopup_HideButtons = function()
origHide()
local dd = getglobal(UIDROPDOWNMENU_INIT_MENU)
if type(dd) ~= "table" or type(UnitPopupShown) ~= "table" then return end
local menu = UnitPopupMenus[dd.which]
if type(menu) ~= "table" then return end
for i = 1, table.getn(menu) do
if menu[i] == KEY and M.Hidden(dd) then UnitPopupShown[i] = 0 end
end
end
end
if type(UnitPopup_OnClick) == "function" then
origClick = UnitPopup_OnClick
UnitPopup_OnClick = function()
if this and this.value == KEY then
local dd = getglobal(UIDROPDOWNMENU_INIT_MENU)
local n = nameOf(dd)
if n then M.Toggle(n, dd.unit) end
return
end
return origClick()
end
end
M.installed = true
return true
end
+94
View File
@@ -0,0 +1,94 @@
-- AnySCT minimap.lua -- minimap button.
-- Left click: unlock / lock the text anchor (unlocking also plays the demo).
-- Right click: options. Drag: move around the minimap. Can be disabled in the options.
--
-- pfUI-safe recipe: parent = Minimap, frame name contains "Minimap", strata HIGH, level 9.
-- pfUI may collect the button into its own panel and then owns its position; that is fine.
AnySCT_Minimap = {}
local M = AnySCT_Minimap
local button = nil
local function placeButton(angleDeg)
if not button then return end
local radius = (Minimap:GetWidth() / 2) + 5
local rad = (angleDeg or 0) * math.pi / 180
button:ClearAllPoints()
button:SetPoint("CENTER", Minimap, "CENTER", math.cos(rad) * radius, math.sin(rad) * radius)
end
if CreateFrame then
local function buildButton()
if button then return end
local btn = CreateFrame("Button", "AnySCTMinimapButton", Minimap)
btn:SetWidth(33)
btn:SetHeight(33)
btn:SetFrameStrata("HIGH")
btn:SetFrameLevel(9)
local icon = btn:CreateTexture("AnySCTMinimapIcon", "ARTWORK")
icon:SetTexture("Interface\\Icons\\Ability_Warrior_DefensiveStance")
icon:SetWidth(20)
icon:SetHeight(20)
icon:SetPoint("CENTER", btn, "CENTER", 0, 0)
icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
local border = btn:CreateTexture("AnySCTMinimapBorder", "OVERLAY")
border:SetTexture("Interface\\Minimap\\MiniMap-TrackingBorder")
border:SetWidth(54)
border:SetHeight(54)
border:SetPoint("TOPLEFT", btn, "TOPLEFT", 0, 0)
btn:SetHighlightTexture("Interface\\Minimap\\UI-Minimap-ZoomButton-Highlight")
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
btn:RegisterForDrag("LeftButton")
-- the drag OnUpdate exists only while dragging
local function dragUpdate()
local mx, my = Minimap:GetCenter()
local cx, cy = GetCursorPosition()
local scale = Minimap:GetEffectiveScale()
cx, cy = cx / scale, cy / scale
local angle = math.atan2(cy - my, cx - mx) * 180 / math.pi
AnySCT_Core.SetOpt("mmAngle", angle)
placeButton(angle)
end
btn:SetScript("OnDragStart", function() this:SetScript("OnUpdate", dragUpdate) end)
btn:SetScript("OnDragStop", function() this:SetScript("OnUpdate", nil) end)
btn:SetScript("OnClick", function()
if arg1 == "RightButton" then
if AnySCT_Options then AnySCT_Options.toggle() end
elseif AnySCT_SCT then
AnySCT_SCT.ToggleMove()
end
end)
btn:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_LEFT")
GameTooltip:SetText("AnySCT", 1, 1, 1)
local st = AnySCT_Core.state
local n = table.getn(st.order)
GameTooltip:AddLine("targetSCTs: " .. ((n > 0) and table.concat(st.order, ", ") or "none"), 0.9, 0.9, 0.9)
GameTooltip:AddLine("Left click: unlock / lock the text anchor", 0.8, 0.8, 0.8)
GameTooltip:AddLine("Right click: options", 0.8, 0.8, 0.8)
GameTooltip:AddLine("Drag: move around the minimap", 0.8, 0.8, 0.8)
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function() GameTooltip:Hide() end)
button = btn
end
-- Honours the "Minimap button" option; called on login and after the option changes.
function M.Update()
local C = AnySCT_Core
if not C.Opt("minimap") then
if button then button:Hide() end
return
end
buildButton()
placeButton(C.Opt("mmAngle"))
button:Show()
end
end
+773
View File
@@ -0,0 +1,773 @@
-- AnySCT options.lua -- options panel: every setting is a control with a tooltip, and the tooltip
-- also shows when hovering the control's label (checkbox / colour-swatch labels are part of the hit
-- rect, slider labels sit in an extended hit rect, dropdown labels get a named hover frame).
-- ESC closes it, dragging the panel moves it. Table-driven: adding a setting is one line.
-- Layout: fixed 520x460 panel with four bottom-edge tabs, grouped under section headers:
-- targetSCTs : General | What to show | GO / WAIT indicator (left) targetSCT list (right)
-- Text : Text content | Size & motion
-- Colours : Per-targetSCT colours | Text colours
-- Placement : Placement | Fixed anchor
-- Defaults + Close on every tab. Choices are Blizzard dropdowns (UIDropDownMenuTemplate).
AnySCT_Options = {}
local O = AnySCT_Options
if CreateFrame then
local W = AnySCT_W
local C = AnySCT_Core
local frame
local checks, sliders, cycles, rows = {}, {}, {}, {}
local moveButton, editBox
local selected = nil
-- { option key, label, tooltip }
local CHECKS = {
{ "enabled", "Enable AnySCT", "Master switch. Off: no text and no GO / WAIT counting." },
{ "onlyWhileSourceFights", "Only while the targetSCT fights",
"Hide text while the targetSCT (or other listed source) itself is not in combat. Off: always show. If the source's combat state cannot be read, the text is shown." },
{ "showHeals", "Show heals on targetSCTs",
"Heals received by a listed targetSCT float over it as +amount with the healer's name. Needs the targetSCT to be listed; healers may be anyone. Uses nampower heal events when present, else the buff chat channels." },
{ "showHots", "Show HoT ticks",
"Also show heal-over-time ticks (Renew, Rejuvenation, ...). Chat rows only; can be busy in a raid. Off by default." },
{ "showOwnHeals", "Include my own heals",
"Show your own heals on listed targetSCTs too. The game never floats heals, so nothing is shown twice." },
{ "healShowHealer", "Show healer name",
"Print the healer's name under the heal number." },
{ "colourize", "Colour coding",
"Off: everything is orange like Blizzard's pet text. On: crit yellow, miss / dodge / parry red, block blue, absorb / resist / immune purple. A per-targetSCT colour (Colours tab) overrides this." },
{ "markGlancing", "Mark glancing / crushing",
"Add the word glancing or crushing after the number (Blizzard style). Off: shown as a plain number." },
{ "showSpells", "Show special attacks",
"Also show Heroic Strike, Stormstrike and other ability rows (gold, like pet spells). Off unregisters those chat channels." },
{ "guessTotemOwner", "Guess totem owner",
"When no owner can be determined, assume a totem belongs to the only shaman on your list. This can misattribute an enemy shaman's totem or your own. Off by default." },
{ "classColours", "Class colours",
"Numbers and words take the class colour of their source (pets and totems: the owner's class). Mobs and unknown sources keep the text colour below. A per-targetSCT colour overrides this. Off by default." },
{ "colorPerTarget", "Own colour per targetSCT",
"Every listed targetSCT gets its own text colour (pets and totems use their owner's). Colours are picked automatically so they differ well; click the swatch in the targetSCT list to change one, or press Auto colours. Heals keep the heal colour. Special attacks use the same colour. Off by default: one orange." },
{ "includeTotems", "Include totems",
"Also show totem damage when its owner is a listed targetSCT. Needs an owner lookup (SuperWoW owner suffix, or the only shaman among the listed targetSCTs). Totems have no nameplate: text appears over the victim." },
{ "showSpellName", "Show spell name",
"Add the ability name (Heroic Strike, Stormstrike, ...) under the number for special attacks. Off by default." },
{ "showLabel", "Show targetSCT list label",
"A 'targetSCTs: ...' line above the fixed anchor. Off by default." },
{ "extendRange", "Extend combat log range (200)",
"Sets CombatLogRangeFriendlyPlayers and CombatLogRangeParty to 200 on login. Without it, raid targetSCTs outside your party are silent beyond roughly 50 yards. The server still caps the real range. On by default." },
{ "minimap", "Show minimap button",
"Left click unlocks / locks the anchor, right click opens this window." },
{ "threatGo", "Enable GO / WAIT (experimental)",
"Experimental. Shows GO or WAIT over your elite / boss target from the server's threat data (the same data TWThreat uses). Works only in a party or raid, only on your own target while it is an elite or boss in combat, and only for targetSCTs whose Threat switch is on. It shows your threat buffer, i.e. how far you are from pulling aggro off the targetSCT, before you pull it. Off by default. Needs no other addon, but also uses TWThreat's answers when TWThreat runs." },
{ "threatTrace", "Record threat trace",
"Writes the received server threat data to <WoW>\imports\anysct_threat.txt (needs SuperWoW). For bug reports." },
{ "importMain", "Import CT_RA / oRA main tanks",
"Imports every entry of the CT_RaidAssist / oRA main tank list that is in your group as a targetSCT." },
}
-- { option key, label, choices { {value, text}, ... }, tooltip }
local CYCLES = {
{ "placement", "Placement",
{ { "clamp", "Nameplate, clamp" }, { "anchorOff", "Nameplate, else anchor" },
{ "anchor", "Always anchor" } },
"Where the text appears. Over the nameplate needs enemy nameplates on (V). Clamp keeps text along the screen edge when the plate is off screen." },
{ "noPlate", "No nameplate",
{ { "anchor", "Anchor" }, { "hide", "Hide" } },
"What to do when the targetSCT's target has no visible nameplate." },
{ "sourceNameMode", "targetSCT name",
{ { "never", "Never" }, { "multi", "Only with several targetSCTs" }, { "always", "Always" } },
"Print the name of the targetSCT that caused the number (for its pets and totems: the owner's name) as the first part of the line under the number, before spell and victim: Source - Spell - Victim. 'Only with several targetSCTs' helps to tell them apart; 'Always' shows it even with a single one. Not shown on heals." },
{ "mobNameMode", "Victim name",
{ { "never", "Never" }, { "anchor", "Anchor only" }, { "always", "Always" } },
"Show the victim's name under the text. Off by default; 'Anchor only' names it when the text falls back to the fixed anchor." },
{ "font", "Font",
{ { "CLIENT", "Client" }, { "FRIZQT", "FrizQT" }, { "SKURRI", "Skurri" },
{ "ARIALN", "Arial Narrow" } },
"Font of the floating text. The client damage font (DAMAGE_TEXT_FONT, honours pfUI) matches the Blizzard text." },
{ "direction", "Direction",
{ { "up", "Up" }, { "down", "Down" } }, "Whether the text rises or sinks." },
}
-- { key, label, min, max, step, format, tooltip }. posX / posY edit the fixed anchor.
local SLIDERS = {
{ "sizeFactor", "Number size", 0.5, 3, 0.05, "%.2f",
"Size factor of damage numbers. Size = 0.018333 x screen diagonal x factor (crits 0.0275): 23 px at 1024x768, 54 px at 2560x1440." },
{ "wordFactor", "Word size", 0.5, 3, 0.05, "%.2f", "Size factor of Miss / Dodge / Parry / ..." },
{ "riseHeight", "Rise (px)", 0, 300, 5, nil,
"How far a normal text rises (crits stay in place). The engine rises 2 yards; the screen distance depends on the camera." },
{ "jitter", "Jitter (px)", 0, 40, 1, nil, "Random horizontal offset per line. 0 like the engine; lines stack instead." },
{ "maxLines", "Max lines", 3, 100, 1, nil, "Maximum number of lines on screen over all mobs; when it is reached the oldest line is recycled." },
{ "maxLinesPerTarget", "Max lines per mob", 1, 30, 1, nil, "Maximum number of lines over one mob. Lines live 1.5 s, so 16 shows every hit of a burst of about 10 per second; lower values recycle the oldest line early." },
{ "threatGoLead", "Required threat buffer", 0, 75, 1, nil,
"Threat points you must still be below the pull threshold (110 % melee / 130 % ranged of the tank's threat) before the label says GO. Slider: 0-7500 in steps of 100. Default 1500 suits fresh level 60 and levelling characters. A typical raid burst over 3 s is a few thousand threat; Execute crits can exceed 10000 - raise it to match your own burst. 0 = GO as soon as the targetSCT holds aggro and you are below the pull threshold.",
"lead" },
{ "xOffset", "Plate X offset", -150, 150, 5, nil,
"Horizontal shift from the plate centre. +40 puts targetSCT numbers beside your own floating text." },
{ "yOffset", "Plate Y offset", -100, 100, 5, nil, "Vertical shift from the plate's top edge." },
{ "edgeMargin", "Edge margin (px)", 0, 200, 5, nil,
"Distance to the screen edge at which a plate counts as off screen and the text is clamped." },
{ "posX", "Anchor X", -800, 800, 5, nil, "Horizontal position of the fixed anchor, from the screen centre." },
{ "posY", "Anchor Y", -500, 500, 5, nil, "Vertical position of the fixed anchor, from the screen centre." },
{ "plateRange", "Nameplate range", 0, 80, 5, nil,
"Sets the SuperWoW NameplateRange and nampower NP_NameplateDistance CVars on login. 0 leaves them untouched (client default about 41-45 yards)." },
}
local function getVal(key)
if key == "threatGoLead" then return C.PosFromLead(C.Opt(key) or 1500) end
if key == "posX" then return (C.Opt("pos") or {}).x or 0 end
if key == "posY" then return (C.Opt("pos") or {}).y or 0 end
return C.Opt(key) or 0
end
local function setVal(key, v)
if key == "threatGoLead" then
C.SetOpt(key, C.LeadFromPos(v))
elseif key == "posX" or key == "posY" then
local p = C.Opt("pos") or {}
local x, y = p.x or 0, p.y or 0
if key == "posX" then x = v else y = v end
C.SetOpt("pos", { point = "CENTER", x = x, y = y })
else
C.SetOpt(key, v)
end
end
local function choiceText(d)
local cur = C.Opt(d[1])
for i = 1, table.getn(d[3]) do
if d[3][i][1] == cur then return d[3][i][2], cur end
end
return d[3][1][2], d[3][1][1]
end
local swatches = {}
local DEFAULT_COLOURS = { colorMelee = { 1.0, 0.518, 0.0 }, colorSpell = { 1.0, 0.871, 0.0 }, colorHeal = { 0.1, 1.0, 0.1 } }
-- Blizzard's colour picker, raised above this panel (it opens behind it otherwise), live preview.
-- onChange(r, g, b) applies a colour; onCancel() restores the colour from before.
local function showPicker(r, g, b, onChange, onCancel)
local cp = ColorPickerFrame
cp.hasOpacity = false
cp.opacityFunc = nil
cp.func = nil
cp:SetColorRGB(r, g, b)
cp.func = function()
local nr, ng, nb = ColorPickerFrame:GetColorRGB()
onChange(nr, ng, nb)
O.refresh()
end
cp.cancelFunc = function()
onCancel()
O.refresh()
end
-- Strata only: SetFrameLevel on a parent does not move its children and would bury the picker's
-- own Okay/Cancel buttons. The options panel is at DIALOG, so FULLSCREEN_DIALOG is always above.
cp:SetFrameStrata("FULLSCREEN_DIALOG")
if cp:IsShown() then cp:Hide() end
cp:Show()
end
local function openPicker(key)
local cur = C.Opt(key) or DEFAULT_COLOURS[key]
local prev = { cur[1], cur[2], cur[3] }
showPicker(cur[1], cur[2], cur[3],
function(r, g, b) C.SetOpt(key, { r, g, b }) end,
function() C.SetOpt(key, prev) end)
end
-- Colour of one listed targetSCT (list row swatch); cancel restores the colour from before.
local function openEntryPicker(name)
local e = C.state.byName[name]
if not e or not e.hex then return end
local prev = e.hex
local r, g, b = C.HexToRgb(prev)
showPicker(r, g, b,
function(nr, ng, nb) C.SetColour(name, C.RgbToHex(nr, ng, nb)) end,
function() C.SetColour(name, prev) end)
end
O.openPicker = openPicker
O.openEntryPicker = openEntryPicker
local function refresh()
if not frame then return end
for i = 1, table.getn(swatches) do
local c = C.Opt(swatches[i].asctKey) or DEFAULT_COLOURS[swatches[i].asctKey]
swatches[i].tex:SetTexture(c[1], c[2], c[3])
end
for i = 1, table.getn(checks) do
checks[i]:SetChecked(C.Opt(checks[i].asctKey) and 1 or nil)
end
for i = 1, table.getn(sliders) do
local sl = sliders[i]
local v = getVal(sl.asctKey)
sl.asctBusy = true
sl:SetValue(v)
sl.asctBusy = false
local txt = sl.asctFmt and string.format(sl.asctFmt, v) or tostring(v)
if sl.asctMap then txt = tostring(C.LeadFromPos(v)) end
getglobal(sl:GetName() .. "Text"):SetText(sl.asctLabel .. ": " .. txt)
end
for i = 1, table.getn(cycles) do
local txt, val = choiceText(CYCLES[i])
UIDropDownMenu_SetSelectedValue(cycles[i], val)
UIDropDownMenu_SetText(txt, cycles[i])
end
local mv = AnySCT_SCT and AnySCT_SCT.IsMoveMode and AnySCT_SCT.IsMoveMode()
moveButton:SetText(mv and "Lock anchor" or "Unlock anchor")
local st = C.state
for i = 1, table.getn(rows) do
local name = st.order[i]
local row = rows[i]
if name then
local e = st.byName[name]
row.asctName = name
row.text:SetText(e.src == "main" and (name .. " (import)") or name)
if selected == name then row.text:SetTextColor(1, 0.82, 0) else row.text:SetTextColor(0.8, 0.8, 0.8) end
local rgb = e.rgb or { 1, 1, 1 }
row.sw.tex:SetTexture(rgb[1], rgb[2], rgb[3])
row.th:SetChecked(e.threatOn and 1 or nil)
row:Show()
else
row.asctName = nil
row:Hide()
end
end
end
O.refresh = refresh
-- Fixed panel geometry (UI scale 1): 520 x 460, four tabs hanging below the bottom edge, 16 px margins.
local PANEL_W, PANEL_H = 520, 460
local COLA, COLB = 20, 270 -- two content columns, 230-240 wide each
local TOP = -48 -- first content row, below the title
local TAB_NAMES = { "targetSCTs", "Text", "Colours", "Placement" }
local TAB_TIPS = {
"The targetSCT list, what is shown, and general switches.",
"What the lines say, plus fonts, sizes and timing of the text.",
"Per-targetSCT colours, class colours and the colour scheme.",
"Where the text appears, the fixed anchor and the nameplate offsets.",
}
local NUM_TABS = 4
local pages, tabs = {}, {}
-- which control lives on which tab (by option key)
local function indexOf(list, key)
for i = 1, table.getn(list) do
if list[i][1] == key then return i end
end
return nil
end
-- Tabs are anchored to the panel one by one (x accumulated from their widths) so the inactive ones
-- can sit higher than the active one, as in the Group Finder (owner: ~5 px).
local TAB_Y_ACTIVE, TAB_Y_INACTIVE, TAB_X0, TAB_GAP = 4, 9, 13, -17
local function placeTabs(active)
if not frame then return end
local x = TAB_X0
for i = 1, NUM_TABS do
local t = tabs[i]
if t then
t:ClearAllPoints()
t:SetPoint("TOPLEFT", frame, "BOTTOMLEFT", x, (i == active) and TAB_Y_ACTIVE or TAB_Y_INACTIVE)
local w = t:GetWidth()
if type(w) ~= "number" then w = 0 end
x = x + w + TAB_GAP
end
end
end
local function selectTab(n)
if not pages[n] then n = 1 end
placeTabs(n)
for i = 1, NUM_TABS do
if i == n then pages[i]:Show() else pages[i]:Hide() end
end
if frame and PanelTemplates_SetTab then PanelTemplates_SetTab(frame, n) end
if type(AnySCTDB) == "table" then AnySCTDB.optionsTab = n end
end
local function build()
if frame then return frame end
local f = CreateFrame("Frame", "AnySCTOptionsFrame", UIParent)
f:SetWidth(PANEL_W)
f:SetHeight(PANEL_H)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
-- DIALOG sits above the HIGH strata where world nameplates (and AnySCT's own text) are drawn, so no
-- plate shows through the panel; the colour picker (FULLSCREEN_DIALOG) stays above the panel.
f:SetFrameStrata("DIALOG")
if f.SetToplevel then f:SetToplevel(true) end
-- Raised so the tabs (panel level - 1) stay at a valid level >= 0 and sit behind the dialog border.
if f.SetFrameLevel then f:SetFrameLevel(10) end
f:SetBackdrop(W.PANEL_BACKDROP)
f:SetMovable(true)
f:EnableMouse(true)
f:SetClampedToScreen(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
f:SetScript("OnShow", function() refresh() end)
local title = W.MakeText(f, "AnySCTOptionsTitle", 14, W.COLORS.title)
title:SetPoint("TOPLEFT", f, "TOPLEFT", 22, -20)
title:SetText("AnySCT")
local closeX = CreateFrame("Button", "AnySCTOptionsCloseX", f, "UIPanelCloseButton")
closeX:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -8)
-- Hidden font string with the button font: buttons are sized to their text + 24 px.
local measure = f:CreateFontString("AnySCTOptionsMeasure", "ARTWORK", "GameFontNormal")
measure:Hide()
local function fitWidth(a, b)
measure:SetText(a)
local w = measure:GetStringWidth() or 0
if b then
measure:SetText(b)
local w2 = measure:GetStringWidth() or 0
if w2 > w then w = w2 end
end
return math.floor(w + 0.5) + 24
end
-- One content frame per tab, same size as the panel so coordinates are panel coordinates.
for i = 1, NUM_TABS do
local pg = CreateFrame("Frame", "AnySCTOptionsPage" .. i, f)
pg:SetWidth(PANEL_W)
pg:SetHeight(PANEL_H)
pg:SetPoint("TOPLEFT", f, "TOPLEFT", 0, 0)
pg:Hide()
pages[i] = pg
end
-- Classic bottom-edge tabs (CharacterFrameTabButtonTemplate) hanging below the panel. The names must
-- be <frame name> .. "Tab" .. i: PanelTemplates_SetTab finds them that way.
f.numTabs = NUM_TABS
if PanelTemplates_SetNumTabs then PanelTemplates_SetNumTabs(f, NUM_TABS) end
for i = 1, NUM_TABS do
local t = CreateFrame("Button", "AnySCTOptionsFrameTab" .. i, f, "CharacterFrameTabButtonTemplate")
t:SetText(TAB_NAMES[i])
-- Group Finder (LFT.xml) look: the template's top 10% of art (the lip) is cropped, so the visible
-- tab starts flat under the border; tab 2 chains LEFT->RIGHT -17 there (-16 in the stock frames).
local nm = "AnySCTOptionsFrameTab" .. i
local l, m, r = getglobal(nm .. "Left"), getglobal(nm .. "Middle"), getglobal(nm .. "Right")
if l and l.SetTexCoord then l:SetTexCoord(0.0, 0.15625, 0.1, 1.0) end
if m and m.SetTexCoord then m:SetTexCoord(0.15625, 0.84375, 0.1, 1.0) end
if r and r.SetTexCoord then r:SetTexCoord(0.84375, 1.0, 0.1, 1.0) end
-- Like the Group Finder (LFT.xml): tabs drawn ABOVE the frame, the lip cropped away by the
-- texcoords, the tab top sitting just under the border's bevel at the frame's outer edge.
if t.SetFrameLevel and f.GetFrameLevel then t:SetFrameLevel(f:GetFrameLevel() + 1) end
-- Position comes from placeTabs (called by selectTab) once the widths are known.
t:SetPoint("TOPLEFT", f, "BOTTOMLEFT", TAB_X0, TAB_Y_INACTIVE)
if PanelTemplates_TabResize then PanelTemplates_TabResize(0, t) end
local hl = getglobal(nm .. "HighlightTexture")
if hl and hl.SetWidth and t.GetTextWidth then hl:SetWidth((t:GetTextWidth() or 0) + 30) end
t.asctTab = i
t:SetScript("OnClick", function() selectTab(this.asctTab) end)
W.AttachTooltip(t, TAB_NAMES[i], TAB_TIPS[i])
tabs[i] = t
end
local function header(page, name, text, x, y)
local h = W.MakeText(page, name, 12, W.COLORS.title)
h:SetPoint("TOPLEFT", f, "TOPLEFT", x, y)
h:SetText(text)
end
-- UIPanelButtonTemplate, default font; width fitted to label (alt = longer toggle text).
local function makeButton(page, name, label, alt, px, py, tipTitle, tipBody, onClick)
local b = CreateFrame("Button", name, page, "UIPanelButtonTemplate")
b:SetWidth(fitWidth(label, alt))
b:SetHeight(22)
b:SetPoint("TOPLEFT", f, "TOPLEFT", px, py)
b:SetText(label)
b:SetScript("OnClick", onClick)
W.AttachTooltip(b, tipTitle, tipBody)
return b
end
-- The label belongs to the hit rect: hovering it shows the tooltip, clicking it toggles.
local function extendRight(btn, fs)
if btn.SetHitRectInsets and fs and fs.GetStringWidth then
btn:SetHitRectInsets(0, -((fs:GetStringWidth() or 0) + 4), 0, 0)
end
end
local function makeCheck(page, key, px, py)
local i = indexOf(CHECKS, key)
local d = CHECKS[i]
local name = "AnySCTOptionsCheck" .. i
local cb = CreateFrame("CheckButton", name, page, "UICheckButtonTemplate")
cb:SetWidth(24)
cb:SetHeight(24)
cb:SetPoint("TOPLEFT", f, "TOPLEFT", px, py)
local label = getglobal(name .. "Text")
label:SetText(d[2])
extendRight(cb, label)
cb.asctKey = d[1]
cb:SetScript("OnClick", function() C.SetOpt(this.asctKey, this:GetChecked() == 1) end)
W.AttachTooltip(cb, d[2], d[3])
checks[i] = cb
end
local function makeSlider(page, key, px, py)
local i = indexOf(SLIDERS, key)
local d = SLIDERS[i]
local name = "AnySCTOptionsSlider" .. i
local sl = CreateFrame("Slider", name, page, "OptionsSliderTemplate")
sl:SetWidth(200)
sl:SetHeight(16)
sl:SetMinMaxValues(d[3], d[4])
sl:SetValueStep(d[5])
sl:SetPoint("TOPLEFT", f, "TOPLEFT", px, py)
getglobal(name .. "Low"):SetText(d[6] and string.format(d[6], d[3]) or tostring(d[3]))
getglobal(name .. "High"):SetText(d[6] and string.format(d[6], d[4]) or tostring(d[4]))
sl.asctKey, sl.asctLabel, sl.asctStep, sl.asctFmt, sl.asctMap = d[1], d[2], d[5], d[6], d[8]
if d[8] then getglobal(name .. "High"):SetText("7500") end
sl:SetScript("OnValueChanged", function()
if this.asctBusy then return end
local v = math.floor(this:GetValue() / this.asctStep + 0.5) * this.asctStep
local txt = this.asctFmt and string.format(this.asctFmt, v) or tostring(v)
if this.asctFmt then v = tonumber(txt) end
if this.asctMap then txt = tostring(C.LeadFromPos(v)) end
setVal(this.asctKey, v)
getglobal(this:GetName() .. "Text"):SetText(this.asctLabel .. ": " .. txt)
end)
W.AttachTooltip(sl, d[2], d[7])
-- the value label sits above the bar: let it count as part of the slider
if sl.SetHitRectInsets then sl:SetHitRectInsets(0, 0, -16, 0) end
sliders[i] = sl
end
-- Label + Blizzard dropdown on one row (label in a named hover frame carrying the same tooltip).
local function makeCycle(page, key, px, py)
local i = indexOf(CYCLES, key)
local d = CYCLES[i]
local lbl = W.MakeText(page, "AnySCTOptionsDropLabel" .. i, 12, W.COLORS.ink)
lbl:SetPoint("TOPLEFT", f, "TOPLEFT", px, py - 9)
lbl:SetText(d[2])
local name = "AnySCTOptionsDrop" .. i
local dd = CreateFrame("Frame", name, page, "UIDropDownMenuTemplate")
dd:SetPoint("TOPLEFT", f, "TOPLEFT", px + 104, py + 1)
dd.asctIndex = i
UIDropDownMenu_Initialize(dd, function()
local def = CYCLES[dd.asctIndex]
local cur = C.Opt(def[1])
for j = 1, table.getn(def[3]) do
local info = {}
info.text = def[3][j][2]
info.value = def[3][j][1]
info.checked = (def[3][j][1] == cur)
info.func = function()
C.SetOpt(CYCLES[dd.asctIndex][1], this.value)
refresh()
end
UIDropDownMenu_AddButton(info)
end
end)
UIDropDownMenu_SetWidth(170, dd)
local lines = ""
for j = 1, table.getn(d[3]) do lines = lines .. " " .. d[3][j][2] .. "\n" end
local body = d[4] .. "\nChoices:\n" .. lines
W.AttachTooltip(dd, d[2], body)
local hover = CreateFrame("Frame", "AnySCTOptionsDropHover" .. i, page)
hover:SetWidth(100)
hover:SetHeight(16)
hover:SetPoint("TOPLEFT", f, "TOPLEFT", px, py - 7)
hover:EnableMouse(true)
W.AttachTooltip(hover, d[2], body)
local ddBtn = getglobal(name .. "Button")
if ddBtn then
-- The arrow button covers the dropdown; give it the tooltip but keep Blizzard's handlers.
local oldEnter = ddBtn:GetScript("OnEnter")
local oldLeave = ddBtn:GetScript("OnLeave")
ddBtn.asctTitle, ddBtn.asctBody = d[2], body
ddBtn:SetScript("OnEnter", function()
if oldEnter then oldEnter() end
if GameTooltip then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(this.asctTitle, 1, 1, 1)
GameTooltip:AddLine(this.asctBody, 0.8, 0.8, 0.8, 1)
GameTooltip:Show()
end
end)
ddBtn:SetScript("OnLeave", function()
if oldLeave then oldLeave() end
if GameTooltip then GameTooltip:Hide() end
end)
end
cycles[i] = dd
end
-- Colour swatch with its label; the label is part of the swatch's hit rect.
local function makeSwatch(page, key, label, tip, px, py)
local b = CreateFrame("Button", "AnySCTOptionsSwatch" .. key, page)
b:SetWidth(26)
b:SetHeight(18)
b:SetPoint("TOPLEFT", f, "TOPLEFT", px, py)
local border = b:CreateTexture(nil, "BACKGROUND")
border:SetTexture(1, 1, 1)
border:SetAllPoints(b)
b.tex = b:CreateTexture(nil, "ARTWORK")
b.tex:SetPoint("TOPLEFT", b, "TOPLEFT", 2, -2)
b.tex:SetPoint("BOTTOMRIGHT", b, "BOTTOMRIGHT", -2, 2)
b.asctKey = key
b:SetScript("OnClick", function() openPicker(this.asctKey) end)
W.AttachTooltip(b, label, tip)
local t = W.MakeText(page, "AnySCTOptionsSwatchText" .. key, 12, W.COLORS.ink)
t:SetPoint("LEFT", b, "RIGHT", 6, 0)
t:SetText(label)
extendRight(b, t)
table.insert(swatches, b)
end
------------------------------------------------------------------ tab 1: targetSCTs
local p1 = pages[1]
header(p1, "AnySCTOptionsHdrGeneral", "General", COLA, TOP)
local y = TOP - 18
local GENERAL = { "enabled", "onlyWhileSourceFights", "extendRange", "minimap" }
for i = 1, table.getn(GENERAL) do
makeCheck(p1, GENERAL[i], COLA, y)
y = y - 21
end
y = y - 8
header(p1, "AnySCTOptionsHdrShow", "What to show", COLA, y)
y = y - 18
local SHOW = { "showSpells", "includeTotems", "guessTotemOwner", "showHeals", "showHots", "showOwnHeals" }
for i = 1, table.getn(SHOW) do
makeCheck(p1, SHOW[i], COLA, y)
y = y - 21
end
y = y - 8
header(p1, "AnySCTOptionsHdrGo", "GO / WAIT indicator (experimental)", COLA, y)
makeCheck(p1, "threatGo", COLA, y - 18)
makeSlider(p1, "threatGoLead", COLA + 10, y - 62)
header(p1, "AnySCTOptionsHdrList", "targetSCTs", COLB, TOP)
local ty = TOP - 18
local box = CreateFrame("Frame", "AnySCTOptionsListBox", p1)
box:SetWidth(230)
box:SetHeight(140)
box:SetPoint("TOPLEFT", f, "TOPLEFT", COLB, ty)
box:SetBackdrop(W.SOLID_BACKDROP)
box:SetBackdropColor(0, 0, 0, 0.6)
box:SetBackdropBorderColor(0.7, 0.7, 0.7, 1)
for i = 1, 8 do
local row = CreateFrame("Button", "AnySCTOptionsRow" .. i, box)
row:SetWidth(214)
row:SetHeight(16)
row:SetPoint("TOPLEFT", box, "TOPLEFT", 8, -8 - (i - 1) * 16)
row.text = row:CreateFontString("AnySCTOptionsRowText" .. i, "ARTWORK", "GameFontHighlightSmall")
row.text:SetPoint("LEFT", row, "LEFT", 28, 0)
row.text:SetWidth(128)
row.text:SetJustifyH("LEFT")
row:SetScript("OnClick", function()
selected = this.asctName
refresh()
end)
W.AttachTooltip(row, "targetSCT", "Click to select, then Remove. Entries marked import are managed by the source addon; remove one to drop it for this session.")
local sw = CreateFrame("Button", "AnySCTOptionsRowSwatch" .. i, row)
sw:SetWidth(22)
sw:SetHeight(12)
sw:SetPoint("LEFT", row, "LEFT", 0, 0)
local border = sw:CreateTexture(nil, "BACKGROUND")
border:SetTexture(1, 1, 1)
border:SetAllPoints(sw)
sw.tex = sw:CreateTexture(nil, "ARTWORK")
sw.tex:SetPoint("TOPLEFT", sw, "TOPLEFT", 1, -1)
sw.tex:SetPoint("BOTTOMRIGHT", sw, "BOTTOMRIGHT", -1, 1)
sw.asctRow = row
sw:SetScript("OnClick", function()
local nm = this.asctRow.asctName
if nm then openEntryPicker(nm) end
end)
W.AttachTooltip(sw, "targetSCT colour",
"Click to pick this targetSCT's colour. It is used when 'Own colour per targetSCT' (Colours tab) is on, and kept until you press Auto colours.")
row.sw = sw
-- per-targetSCT threat switch (the label is part of the hit rect)
local th = CreateFrame("CheckButton", "AnySCTOptionsRowThreat" .. i, row, "UICheckButtonTemplate")
th:SetWidth(16)
th:SetHeight(16)
th:SetPoint("LEFT", row, "LEFT", 158, 0)
local thLabel = getglobal("AnySCTOptionsRowThreat" .. i .. "Text")
thLabel:SetText("Threat")
extendRight(th, thLabel)
th.asctRow = row
th:SetScript("OnClick", function()
local nm = this.asctRow.asctName
if nm then C.SetThreatOn(nm, this:GetChecked() == 1) end
end)
W.AttachTooltip(th, "Threat (experimental)",
"Use this targetSCT for the GO / WAIT threat label. On by default for players, off for pets, totems, mobs and hostile players. Only matters when 'Enable GO / WAIT' is on.")
row.th = th
rows[i] = row
end
ty = ty - 140 - 4
editBox = CreateFrame("EditBox", "AnySCTOptionsAddBox", p1, "InputBoxTemplate")
editBox:SetWidth(150)
editBox:SetHeight(20)
editBox:SetPoint("TOPLEFT", f, "TOPLEFT", COLB + 8, ty)
editBox:SetAutoFocus(false)
editBox:SetMaxLetters(32)
editBox:SetScript("OnEscapePressed", function() this:ClearFocus() end)
editBox:SetScript("OnEnterPressed", function()
if this:GetText() ~= "" then C.Slash("add " .. this:GetText()) end
this:SetText("")
this:ClearFocus()
refresh()
end)
W.AttachTooltip(editBox, "Add a targetSCT", "Type a player name and press Enter, or click Add.")
ty = ty - 24
local bx = COLB
local bAdd = makeButton(p1, "AnySCTOptionsAdd", "Add", nil, bx, ty, "Add targetSCT",
"Add the typed name (or your target if the box is empty) to the list.",
function()
local t = editBox:GetText()
C.Slash("add " .. t)
editBox:SetText("")
refresh()
end)
bx = bx + bAdd:GetWidth() + 6
local bRem = makeButton(p1, "AnySCTOptionsRemove", "Remove", nil, bx, ty, "Remove targetSCT",
"Remove the selected targetSCT.", function()
if selected then C.RemoveSource(selected) selected = nil end
refresh()
end)
bx = bx + bRem:GetWidth() + 6
makeButton(p1, "AnySCTOptionsClearList", "Clear", nil, bx, ty, "Clear list",
"Remove every targetSCT, including the saved manual list.", function()
C.ClearSources()
selected = nil
refresh()
end)
ty = ty - 28
makeButton(p1, "AnySCTOptionsAutoColours", "Auto colours", nil, COLB, ty, "Auto colours",
"Give every listed targetSCT a new colour from the built-in palette, chosen so that they are easy to tell apart. Replaces colours you picked by hand.",
function()
C.AutoColours()
refresh()
end)
ty = ty - 30
makeCheck(p1, "importMain", COLB, ty)
------------------------------------------------------------------ tab 2: Text
local p2 = pages[2]
header(p2, "AnySCTOptionsHdrContent", "Text content", COLA, TOP)
local y2 = TOP - 18
makeCycle(p2, "sourceNameMode", COLA, y2)
y2 = y2 - 30
makeCycle(p2, "mobNameMode", COLA, y2)
y2 = y2 - 30
local CONTENT = { "showSpellName", "healShowHealer", "markGlancing" }
for i = 1, table.getn(CONTENT) do
makeCheck(p2, CONTENT[i], COLA, y2)
y2 = y2 - 21
end
y2 = y2 - 8
header(p2, "AnySCTOptionsHdrSize", "Size & motion", COLA, y2)
y2 = y2 - 18
makeCycle(p2, "font", COLA, y2)
y2 = y2 - 30
makeCycle(p2, "direction", COLA, y2)
y2 = y2 - 48
local LOOK = { "sizeFactor", "wordFactor", "riseHeight", "jitter", "maxLines", "maxLinesPerTarget" }
for i = 1, table.getn(LOOK) do
local col = COLA + 10
if math.mod(i, 2) == 0 then col = COLB + 10 end
makeSlider(p2, LOOK[i], col, y2)
if math.mod(i, 2) == 0 then y2 = y2 - 46 end
end
------------------------------------------------------------------ tab 3: Colours
local p3 = pages[3]
header(p3, "AnySCTOptionsHdrPerTarget", "Per-targetSCT colours", COLA, TOP)
local y3 = TOP - 18
makeCheck(p3, "colorPerTarget", COLA, y3)
y3 = y3 - 40
header(p3, "AnySCTOptionsHdrTextColours", "Text colours", COLA, y3)
y3 = y3 - 18
makeCheck(p3, "classColours", COLA, y3)
makeCheck(p3, "colourize", COLB, y3)
y3 = y3 - 32
makeSwatch(p3, "colorMelee", "Melee colour", "Colour of damage numbers and words (melee). Opens the colour picker; changes preview live.", COLA, y3)
makeSwatch(p3, "colorSpell", "Spell colour", "Colour of special attacks (Heroic Strike, ...).", COLA + 130, y3)
makeButton(p3, "AnySCTOptionsResetColours", "Reset colours", nil, COLA + 270, y3 - 2,
"Reset colours", "Orange FF8400 for melee, gold FFDE00 for spells (Blizzard defaults), green for heals.",
function()
C.SetOpt("colorMelee", { 1.0, 0.518, 0.0 })
C.SetOpt("colorSpell", { 1.0, 0.871, 0.0 })
C.SetOpt("colorHeal", { 0.1, 1.0, 0.1 })
refresh()
end)
y3 = y3 - 30
makeSwatch(p3, "colorHeal", "Heal colour", "Colour of heal numbers on listed targetSCTs.", COLA, y3)
------------------------------------------------------------------ tab 4: Placement
local p4 = pages[4]
header(p4, "AnySCTOptionsHdrPlacement", "Placement", COLA, TOP)
local y4 = TOP - 18
makeCycle(p4, "placement", COLA, y4)
y4 = y4 - 30
makeCycle(p4, "noPlate", COLA, y4)
y4 = y4 - 46
local PLATE = { "xOffset", "yOffset", "edgeMargin", "plateRange" }
for i = 1, table.getn(PLATE) do
local col = COLA + 10
if math.mod(i, 2) == 0 then col = COLB + 10 end
makeSlider(p4, PLATE[i], col, y4)
if math.mod(i, 2) == 0 then y4 = y4 - 46 end
end
y4 = y4 - 4
header(p4, "AnySCTOptionsHdrAnchor", "Fixed anchor", COLA, y4)
y4 = y4 - 34
makeSlider(p4, "posX", COLA + 10, y4)
makeSlider(p4, "posY", COLB + 10, y4)
y4 = y4 - 38
local ax = COLA
moveButton = makeButton(p4, "AnySCTOptionsMove", "Unlock anchor", "Lock anchor", ax, y4,
"Move the fixed anchor", "Unlock to drag the anchor box and play a demo stream; lock when done.",
function()
if AnySCT_SCT then AnySCT_SCT.ToggleMove() end
refresh()
end)
ax = ax + moveButton:GetWidth() + 6
local bPos = makeButton(p4, "AnySCTOptionsResetPos", "Reset anchor", nil, ax, y4,
"Reset anchor", "Put the fixed anchor back to the default spot (screen centre, 120 px up).",
function()
local p = AnySCT_DEFAULTS.pos
C.SetOpt("pos", { point = p.point, x = p.x, y = p.y })
refresh()
end)
ax = ax + bPos:GetWidth() + 6
makeButton(p4, "AnySCTOptionsDemo", "Demo", nil, ax, y4, "Demo",
"Play hit, hit, crit, miss, dodge, parry, block at your target's nameplate (fixed anchor without a target) to compare with your own numbers.",
function() if AnySCT_SCT then AnySCT_SCT.Demo() end end)
y4 = y4 - 28
makeCheck(p4, "showLabel", COLA, y4)
y4 = y4 - 26
header(p4, "AnySCTOptionsHdrDebug", "Debug", COLA, y4)
makeCheck(p4, "threatTrace", COLA, y4 - 18)
------------------------------------------------------------------ bottom row (all tabs)
local yb = -(PANEL_H - 16 - 22)
local bClose = makeButton(f, "AnySCTOptionsClose", "Close", nil, 0, yb, "Close", "Close this window (ESC works too).",
function() f:Hide() end)
local bDef = makeButton(f, "AnySCTOptionsReset", "Defaults", nil, 0, yb, "Defaults",
"Restore every setting to its default (the targetSCT list and its colours are kept).",
function() C.Slash("reset") refresh() end)
local rx = PANEL_W - 16 - bClose:GetWidth()
bClose:SetPoint("TOPLEFT", f, "TOPLEFT", rx, yb)
bDef:SetPoint("TOPLEFT", f, "TOPLEFT", rx - 6 - bDef:GetWidth(), yb)
table.insert(UISpecialFrames, "AnySCTOptionsFrame")
frame = f
local last = 1
if type(AnySCTDB) == "table" and type(AnySCTDB.optionsTab) == "number" then last = AnySCTDB.optionsTab end
selectTab(last)
f:Hide()
return f
end
function O.toggle()
local f = build()
if f:IsShown() then f:Hide() else f:Show() end
end
end
+346
View File
@@ -0,0 +1,346 @@
-- AnySCT parse.lua -- pure functions, no WoW API (only an optional getglobal for live
-- GlobalStrings), so everything here runs offline under Lua 5.0.
--
-- Three entry points:
-- AnySCT_ParseMelee(msg) chat row of a melee swing -> result table or nil
-- AnySCT_ParseSpell(msg) chat row of a special attack -> result table or nil
-- AnySCT_DecodeAutoAttack nampower AUTO_ATTACK_OTHER numbers -> result table or nil
--
-- Result table: { kind, amount, blocked, absorbed, resisted, attacker, victim, spell, school }
-- kind is one of hit crit glancing crushing miss dodge parry block evade immune deflect absorb resist.
AnySCT_Parse = {}
local P = AnySCT_Parse
-- Live GlobalString if the client has one, else the enUS literal.
local function gs(name, default)
local v
if getglobal then v = getglobal(name) end
if type(v) == "string" then return v end
return default
end
-- printf-style GlobalString -> Lua 5.0 pattern (no anchors). %s -> (.-), %d -> (%d+); literal text
-- is escaped. Positional placeholders (%1$s) are read in textual order. Second result: the
-- placeholder kinds in order.
function AnySCT_GsToPattern(template)
local parts = {}
local kinds = {}
local i, len = 1, string.len(template)
while i <= len do
local c = string.sub(template, i, i)
if c == "%" then
local rest = string.sub(template, i)
local _, e, _, t = string.find(rest, "^%%(%d*)%$?([sdfg])")
if e then
if t == "s" then
table.insert(parts, "(.-)")
else
table.insert(parts, "(%d+)")
end
table.insert(kinds, t)
i = i + e
else
-- literal percent ("%%") or a stray one
local n = string.sub(template, i + 1, i + 1)
table.insert(parts, "%%")
if n == "%" then i = i + 2 else i = i + 1 end
end
else
if string.find(c, "[%(%)%.%+%-%*%?%[%]%^%$]") then
table.insert(parts, "%" .. c)
else
table.insert(parts, c)
end
i = i + 1
end
end
return table.concat(parts, ""), kinds
end
-- ---- form tables ----------------------------------------------------------------------------
-- { globalName, enUS literal, kind, field names in placeholder order }. Order matters: the more
-- specific string (CRIT / SCHOOL) must be tried before the plain one.
local F_ATT = { "attacker", "victim", "amount", "school" }
local F_ATN = { "attacker", "victim", "amount" }
local F_AV = { "attacker", "victim" }
local F_SP = { "attacker", "spell", "victim", "amount", "school" }
local F_SPN = { "attacker", "spell", "victim", "amount" }
local F_SPV = { "attacker", "spell", "victim" }
local F_A = { "attacker" }
local F_SPA = { "attacker", "spell" }
local MELEE_DEFS = {
{ "COMBATHITCRITSCHOOLOTHEROTHER", "%s crits %s for %d %s damage.", "crit", F_ATT },
{ "COMBATHITCRITOTHEROTHER", "%s crits %s for %d.", "crit", F_ATN },
{ "COMBATHITSCHOOLOTHEROTHER", "%s hits %s for %d %s damage.", "hit", F_ATT },
{ "COMBATHITOTHEROTHER", "%s hits %s for %d.", "hit", F_ATN },
{ "MISSEDOTHEROTHER", "%s misses %s.", "miss", F_AV },
{ "VSBLOCKOTHEROTHER", "%s attacks. %s blocks.", "block", F_AV },
{ "VSDODGEOTHEROTHER", "%s attacks. %s dodges.", "dodge", F_AV },
{ "VSPARRYOTHEROTHER", "%s attacks. %s parries.", "parry", F_AV },
{ "VSIMMUNEOTHEROTHER", "%s attacks but %s is immune.", "immune", F_AV },
{ "VSEVADEOTHEROTHER", "%s attacks. %s evades.", "evade", F_AV },
{ "VSDEFLECTOTHEROTHER", "%s attacks. %s deflects.", "deflect", F_AV },
{ "VSABSORBOTHEROTHER", "%s attacks. %s absorbs all the damage.", "absorb", F_AV },
{ "VSRESISTOTHEROTHER", "%s attacks. %s resists all the damage.", "resist", F_AV },
-- attacks on the player: the victim is "you" (set from the 5th entry)
{ "VSDODGEOTHERSELF", "%s attacks. You dodge.", "dodge", F_A, true },
{ "VSPARRYOTHERSELF", "%s attacks. You parry.", "parry", F_A, true },
{ "VSBLOCKOTHERSELF", "%s attacks. You block.", "block", F_A, true },
{ "VSIMMUNEOTHERSELF", "%s attacks but you are immune.", "immune", F_A, true },
{ "VSEVADEOTHERSELF", "%s attacks. You evade.", "evade", F_A, true },
{ "VSDEFLECTOTHERSELF", "%s attacks. You deflect.", "deflect", F_A, true },
{ "VSABSORBOTHERSELF", "%s attacks. You absorb all the damage.", "absorb", F_A, true },
{ "VSRESISTOTHERSELF", "%s attacks. You resist all the damage.", "resist", F_A, true },
}
local SPELL_DEFS = {
{ "SPELLLOGCRITSCHOOLOTHEROTHER", "%s's %s crits %s for %d %s damage.", "crit", F_SP },
{ "SPELLLOGCRITOTHEROTHER", "%s's %s crits %s for %d.", "crit", F_SPN },
{ "SPELLLOGSCHOOLOTHEROTHER", "%s's %s hits %s for %d %s damage.", "hit", F_SP },
{ "SPELLLOGOTHEROTHER", "%s's %s hits %s for %d.", "hit", F_SPN },
{ "SPELLLOGABSORBOTHEROTHER", "%s's %s is absorbed by %s.", "absorb", F_SPV },
{ "SPELLMISSOTHEROTHER", "%s's %s missed %s.", "miss", F_SPV },
{ "SPELLDODGEDOTHEROTHER", "%s's %s was dodged by %s.", "dodge", F_SPV },
{ "SPELLPARRIEDOTHEROTHER", "%s's %s was parried by %s.", "parry", F_SPV },
{ "SPELLBLOCKEDOTHEROTHER", "%s's %s was blocked by %s.", "block", F_SPV },
{ "SPELLRESISTOTHEROTHER", "%s's %s was resisted by %s.", "resist", F_SPV },
{ "SPELLIMMUNEOTHEROTHER", "%s's %s fails. %s is immune.", "immune", F_SPV },
{ "SPELLEVADEDOTHEROTHER", "%s's %s was evaded by %s.", "evade", F_SPV },
{ "SPELLMISSOTHERSELF", "%s's %s misses you.", "miss", F_SPA, true },
{ "SPELLDODGEDOTHERSELF", "%s's %s was dodged.", "dodge", F_SPA, true },
{ "SPELLPARRIEDOTHERSELF", "%s's %s was parried.", "parry", F_SPA, true },
{ "SPELLBLOCKEDOTHERSELF", "%s's %s was blocked.", "block", F_SPA, true },
{ "SPELLRESISTOTHERSELF", "%s's %s was resisted.", "resist", F_SPA, true },
{ "SPELLEVADEDOTHERSELF", "%s's %s was evaded.", "evade", F_SPA, true },
{ "SPELLDEFLECTEDOTHERSELF", "%s's %s was deflected.", "deflect", F_SPA, true },
{ "SPELLIMMUNEOTHERSELF", "%s's %s failed. You are immune.", "immune", F_SPA, true },
{ "SPELLLOGABSORBOTHERSELF", "You absorb %s's %s.", "absorb", F_SPA, true },
}
-- Heal rows (CHAT_MSG_SPELL_*_BUFF and the PERIODIC_*_BUFFS channels). Result: kind "heal", crit,
-- periodic, attacker = healer ("you" for the player), victim, spell, amount.
local F_HEAL = { "attacker", "spell", "victim", "amount" }
local F_HEALS = { "spell", "victim", "amount" } -- "Your Spell heals Victim for N."
local F_HOT = { "victim", "amount", "attacker", "spell" }
local F_HOTS = { "victim", "amount", "spell" } -- "Victim gains N health from your Spell."
local HEAL_DEFS = {
{ "HEALEDCRITOTHEROTHER", "%s's %s critically heals %s for %d.", "healcrit", F_HEAL },
{ "HEALEDOTHEROTHER", "%s's %s heals %s for %d.", "heal", F_HEAL },
{ "HEALEDCRITSELFOTHER", "Your %s critically heals %s for %d.", "healcrit", F_HEALS, nil, true },
{ "HEALEDSELFOTHER", "Your %s heals %s for %d.", "heal", F_HEALS, nil, true },
{ "PERIODICAURAHEALOTHEROTHER", "%s gains %d health from %s's %s.", "hot", F_HOT },
{ "PERIODICAURAHEALSELFOTHER", "%s gains %d health from your %s.", "hot", F_HOTS, nil, true },
}
-- trailers appended to the end of a row; the third entry names what they set on the result
local TRAILER_DEFS = {
{ "ABSORB_TRAILER", " (%d absorbed)", "absorbed", {} },
{ "BLOCK_TRAILER", " (%d blocked)", "blocked", {} },
{ "RESIST_TRAILER", " (%d resisted)", "resisted", {} },
{ "GLANCING_TRAILER", " (glancing)", "glancing", {} },
{ "CRUSHING_TRAILER", " (crushing)", "crushing", {} },
}
local meleeForms, spellForms, healForms, trailers
local function compile(defs, anchored)
local out = {}
for i = 1, table.getn(defs) do
local d = defs[i]
local pat = AnySCT_GsToPattern(gs(d[1], d[2]))
if anchored then pat = "^" .. pat .. "$" else pat = pat .. "$" end
out[i] = { pat = pat, kind = d[3], key = d[3], fields = d[4], nf = table.getn(d[4]), selfVictim = d[5],
selfAttacker = d[6] }
end
return out
end
-- Rebuild all compiled patterns (call again if the GlobalStrings change, e.g. in tests).
function AnySCT_Parse_Rebuild()
meleeForms = compile(MELEE_DEFS, true)
spellForms = compile(SPELL_DEFS, true)
healForms = compile(HEAL_DEFS, true)
trailers = compile(TRAILER_DEFS, false)
end
-- Strip trailers from the end of msg (repeatedly: they can stack). Returns the stripped message
-- and an info table { absorbed=, blocked=, resisted=, glancing=true, crushing=true }.
local info = {} -- reused: one parse at a time, read before the next call
local caps = {} -- reused capture buffer
local function stripTrailers(msg)
if not trailers then AnySCT_Parse_Rebuild() end
info.absorbed, info.blocked, info.resisted, info.glancing, info.crushing = nil, nil, nil, nil, nil
local changed = true
while changed do
changed = false
for i = 1, table.getn(trailers) do
local t = trailers[i]
local s, _, n = string.find(msg, t.pat)
if s then
msg = string.sub(msg, 1, s - 1)
if n then info[t.key] = (info[t.key] or 0) + tonumber(n) else info[t.key] = true end
changed = true
end
end
end
return msg, info
end
AnySCT_StripTrailers = stripTrailers
local function matchForms(forms, msg)
for i = 1, table.getn(forms) do
local f = forms[i]
local s, _, c1, c2, c3, c4, c5 = string.find(msg, f.pat)
if s then
caps[1], caps[2], caps[3], caps[4], caps[5] = c1, c2, c3, c4, c5
local r = { kind = f.kind }
for j = 1, f.nf do r[f.fields[j]] = caps[j] end
if r.amount then r.amount = tonumber(r.amount) end
if f.selfVictim then r.victim = "you" end
if f.selfAttacker then r.attacker = "you" end
return r
end
end
return nil
end
-- Fold trailer info into the result. Same precedence as the auto-attack decoder:
-- crit > crushing > glancing > block > hit. A plain "hit" is promoted; a crit stays a crit.
local function applyMods(r, info)
r.absorbed = (type(info.absorbed) == "number") and info.absorbed or 0
r.blocked = (type(info.blocked) == "number") and info.blocked or 0
r.resisted = (type(info.resisted) == "number") and info.resisted or 0
if not r.amount then r.amount = 0 end
if r.kind == "hit" then
if info.crushing then r.kind = "crushing"
elseif info.glancing then r.kind = "glancing"
elseif r.blocked > 0 then r.kind = "block" end
end
return r
end
function AnySCT_ParseMelee(msg)
if type(msg) ~= "string" then return nil end
if not meleeForms then AnySCT_Parse_Rebuild() end
local body, info = stripTrailers(msg)
local r = matchForms(meleeForms, body)
if not r then return nil end
-- "Tank's Heroic Strike hits Boss for 5." also fits the melee shape; that is a spell row
if string.find(r.attacker, "'s ", 1, true) then return nil end
return applyMods(r, info)
end
function AnySCT_ParseSpell(msg)
if type(msg) ~= "string" then return nil end
if not spellForms then AnySCT_Parse_Rebuild() end
local body, info = stripTrailers(msg)
local r = matchForms(spellForms, body)
if not r then return nil end
return applyMods(r, info)
end
function AnySCT_ParseHeal(msg)
if type(msg) ~= "string" then return nil end
if not healForms then AnySCT_Parse_Rebuild() end
local r = matchForms(healForms, msg)
if not r then return nil end
if r.kind == "healcrit" then r.kind = "heal"; r.crit = true
elseif r.kind == "hot" then r.kind = "heal"; r.periodic = true end
r.absorbed, r.blocked, r.resisted = 0, 0, 0
return r
end
-- nampower SPELL_HEAL_BY_OTHER: the crit flag is not documented for this client, so heals decode as
-- non-crit unless the caller passes an explicit crit argument (true or 1).
function AnySCT_DecodeHeal(amount, crit)
local r = { kind = "heal", amount = tonumber(amount) or 0, absorbed = 0, blocked = 0, resisted = 0 }
if crit == true or crit == 1 then r.crit = true end
return r
end
-- ---- nampower AUTO_ATTACK_OTHER decoder ---------------------------------------------------------
-- hitInfo flags (server UnitDefines.h): MISS 0x10, ABSORB 0x20, RESIST 0x40, CRITICALHIT 0x80,
-- GLANCING 0x4000, CRUSHING 0x8000. Lua 5.0 has no bit ops, so test a flag with floor and mod.
local function bitset(v, flag)
return math.mod(math.floor(v / flag), 2) == 1
end
P.bitset = bitset
local F_MISS, F_ABSORB, F_RESIST, F_CRIT, F_GLANCING, F_CRUSHING = 16, 32, 64, 128, 16384, 32768
-- victimState -> kind for the states that mean "no damage dealt"
local STATE_KIND = { [2] = "dodge", [3] = "parry", [6] = "evade", [7] = "immune", [8] = "deflect" }
-- victimState 0 unaffected, 1 normal, 2 dodge, 3 parry, 4 interrupt (ignored -> nil), 5 block
-- (partial: damage goes through), 6 evade, 7 immune, 8 deflect.
-- Precedence for damaging hits: crit > crushing > glancing > block > hit.
function AnySCT_DecodeAutoAttack(damage, hitInfo, victimState, blocked, absorbed, resisted)
damage = tonumber(damage) or 0
hitInfo = tonumber(hitInfo) or 0
victimState = tonumber(victimState) or 1
blocked = tonumber(blocked) or 0
absorbed = tonumber(absorbed) or 0
resisted = tonumber(resisted) or 0
local r = { kind = "hit", amount = damage, blocked = blocked, absorbed = absorbed, resisted = resisted }
if victimState == 4 then return nil end
local sk = STATE_KIND[victimState]
if sk then
r.kind = sk
r.amount = 0
return r
end
if bitset(hitInfo, F_MISS) then
r.kind = "miss"
r.amount = 0
return r
end
if damage <= 0 then
if absorbed > 0 or bitset(hitInfo, F_ABSORB) then r.kind = "absorb"
elseif resisted > 0 or bitset(hitInfo, F_RESIST) then r.kind = "resist"
elseif victimState == 5 then r.kind = "block"
elseif victimState == 0 then r.kind = "miss"
end
return r
end
if bitset(hitInfo, F_CRIT) then r.kind = "crit"
elseif bitset(hitInfo, F_CRUSHING) then r.kind = "crushing"
elseif bitset(hitInfo, F_GLANCING) then r.kind = "glancing"
elseif victimState == 5 then r.kind = "block"
end
return r
end
-- ---- nampower spell events ---------------------------------------------------------------------
-- SPELL_DAMAGE_EVENT_OTHER: amount, mitigation "absorb,block,resist" (comma separated string),
-- hitInfo (SpellHitType, crit = 0x02). Argument layout as documented for nampower; spell hits seen in game with nampower 4.6.2.
function AnySCT_DecodeSpellDamage(amount, mitigation, hitInfo)
amount = tonumber(amount) or 0
hitInfo = tonumber(hitInfo) or 0
local absorbed, blocked, resisted = 0, 0, 0
if type(mitigation) == "string" then
local _, _, a, b, r = string.find(mitigation, "^%s*(-?%d*)%s*,%s*(-?%d*)%s*,%s*(-?%d*)%s*$")
absorbed, blocked, resisted = tonumber(a) or 0, tonumber(b) or 0, tonumber(r) or 0
end
local r = { kind = "hit", amount = amount, absorbed = absorbed, blocked = blocked, resisted = resisted }
if amount <= 0 then
if absorbed > 0 then r.kind = "absorb"
elseif resisted > 0 then r.kind = "resist"
elseif blocked > 0 then r.kind = "block" end
elseif bitset(hitInfo, 2) then
r.kind = "crit"
end
return r
end
-- SPELL_MISS_OTHER missType: the vanilla SpellMissInfo enum (unverified for this client).
local MISS_KIND = { [1] = "miss", [2] = "resist", [3] = "dodge", [4] = "parry", [5] = "block",
[6] = "evade", [7] = "immune", [8] = "immune", [9] = "deflect", [10] = "absorb" }
function AnySCT_DecodeSpellMiss(missType)
local k = MISS_KIND[tonumber(missType) or 0]
if not k then return nil end
return { kind = k, amount = 0, absorbed = 0, blocked = 0, resisted = 0 }
end
+267
View File
@@ -0,0 +1,267 @@
-- AnySCT plates.lua -- nameplate lookup and screen placement maths.
--
-- There is no world-to-screen API in the 1.12 client, so the only way to float text over a mob is
-- to ride its nameplate. This file finds the plate frame of a unit, and decides WHERE a line goes:
-- mode 1 plate found, centre on screen -> spawn at the plate
-- mode 2 plate found, centre off screen / in the edge margin -> clamp to the screen
-- mode 3 no usable plate (or placement says so) -> fixed anchor
-- mode 0 no plate and the user chose to hide
--
-- Plate detection (verified for TurtleWoW): child of WorldFrame, object type "Button", first region
-- a Texture "Interface\\Tooltips\\Nameplate-Border". Region order: border, glow, name, level,
-- levelicon, raidicon; children: healthbar, castbar. The GUID comes from plate:GetName(1) under
-- SuperWoW (>= 1.0) / nampower (>= 2.28); it is accepted only if it looks like "0x...".
AnySCT_Plates = {}
local P = AnySCT_Plates
P.plates = {} -- every plate frame seen so far
P.nPlates = 0
P.byGuid = {} -- normalised guid -> visible plate (rebuilt each scan)
P.guidCapable = false -- true once any visible plate returned a GUID
P.lastCount = 0
P.lastScan = -1
P.gen = 0 -- bumped by every scan; lines re-check their plate only when it changes
P.SCAN_INTERVAL = 0.2
local function now()
if GetTime then return GetTime() end
return 0
end
-- Forget every cached plate (tests; also usable if the plate frames are ever rebuilt).
function P.Reset()
P.plates = {}
P.nPlates = 0
P.byGuid = {}
P.guidCapable = false
P.lastCount = 0
P.lastScan = -1
P.gen = 0
end
-- ---- pure clamp maths --------------------------------------------------------------------------
-- Clamp a point into [margin, size - margin] on both axes. Third result: true if it moved.
-- If the screen is smaller than twice the margin the axis collapses to its centre.
local function clampAxis(v, size, margin)
local lo, hi = margin, size - margin
if hi < lo then return size / 2 end
if v < lo then return lo end
if v > hi then return hi end
return v
end
function AnySCT_ClampToScreen(x, y, width, height, margin)
margin = margin or 0
local cx, cy = clampAxis(x, width, margin), clampAxis(y, height, margin)
return cx, cy, (cx ~= x or cy ~= y)
end
-- ---- detection ----------------------------------------------------------------------------------
function P.IsPlate(f)
if not f or not f.GetObjectType or f:GetObjectType() ~= "Button" then return false end
local r = f:GetRegions()
if not r or not r.GetObjectType or r:GetObjectType() ~= "Texture" then return false end
return r:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
end
-- The GUID string of a plate, or nil.
function P.PlateGuid(plate)
local ok, g = pcall(plate.GetName, plate, 1)
if ok and type(g) == "string" and string.find(g, "^0x") then return g end
return nil
end
local function addPlate(f)
P.nPlates = P.nPlates + 1
P.plates[P.nPlates] = f
-- cache what the name fallback needs (regions: border, glow, name, ...)
local r1, r2, r3 = f:GetRegions()
f.asctNameRegion = r3
f.asctBar = f:GetChildren()
end
-- Scan for new plates (only when WorldFrame's child count changed) and rebuild the GUID map.
-- Throttled to SCAN_INTERVAL unless force.
function P.Scan(force)
local t = now()
if not force and t - P.lastScan < P.SCAN_INTERVAL then return end
P.lastScan = t
local n = WorldFrame:GetNumChildren()
if n ~= P.lastCount then
local kids = { WorldFrame:GetChildren() }
for i = P.lastCount + 1, n do
local f = kids[i]
if f and P.IsPlate(f) then addPlate(f) end
end
P.lastCount = n
end
local map = P.byGuid
for k in pairs(map) do map[k] = nil end
for i = 1, P.nPlates do
local pl = P.plates[i]
if pl:IsVisible() then
local g = P.PlateGuid(pl)
if g then
P.guidCapable = true
map[AnySCT_Core.NormGuid(g)] = pl
end
end
end
P.gen = P.gen + 1
end
-- Health fraction of a plate's bar, or nil.
local function barFraction(plate)
local bar = plate.asctBar
if not bar or not bar.GetValue then return nil end
local lo, hi = bar:GetMinMaxValues()
if not hi or hi <= lo then return nil end
return (bar:GetValue() - lo) / (hi - lo)
end
-- Debug dump of a failed lookup: victim GUID, normalised form, scan-map size and up to 10 visible
-- plates with name and GUID. Only called with /asct debug on.
local function dumpMiss(guid, name)
local C = AnySCT_Core
local ng = guid and C.NormGuid(guid) or "-"
local n = 0
for _ in pairs(P.byGuid) do n = n + 1 end
C.Dbg("plate miss: guid=" .. tostring(guid) .. " norm=" .. tostring(ng) .. " name=" .. tostring(name)
.. " mapped=" .. n .. " plates=" .. P.nPlates)
local shown = 0
for i = 1, P.nPlates do
local pl = P.plates[i]
if shown < 10 and pl:IsVisible() then
shown = shown + 1
local g = P.PlateGuid(pl)
local nm = pl.asctNameRegion and pl.asctNameRegion.GetText and pl.asctNameRegion:GetText() or "?"
C.Dbg(" plate '" .. tostring(nm) .. "' guid=" .. tostring(g) .. " norm=" .. tostring(g and C.NormGuid(g)))
end
end
end
-- Find the plate of a unit. guid: SuperWoW GUID string or nil; name: unit name; unit: token for the
-- health comparison of the name fallback. Returns a visible plate or nil.
-- Chain, never stopping early: scan map by GUID -> one forced rescan -> UnitNameplate(guid) ->
-- plate name (event name, else UnitName(guid)) -> nil.
function P.Find(guid, name, unit)
P.Scan(false)
if guid then
local ng = AnySCT_Core.NormGuid(guid)
local pl = P.byGuid[ng]
if not pl and now() - P.lastScan > 0.05 then
P.Scan(true)
pl = P.byGuid[ng]
end
if pl and pl:IsVisible() then return pl end
if UnitNameplate then
local ok, f = pcall(UnitNameplate, guid)
if ok and type(f) == "table" and f.IsVisible and f:IsVisible() then return f end
end
if not name and UnitName then
local ok, n = pcall(UnitName, guid)
if ok and type(n) == "string" then name = n end
end
end
if not name then
if AnySCT_Core.debug then dumpMiss(guid, name) end
return nil
end
local want
if unit and UnitHealth and UnitHealthMax then
local mx = UnitHealthMax(unit)
if mx and mx > 0 then want = UnitHealth(unit) / mx end
end
local best = P.FindByName(name, want)
if not best and now() - P.lastScan > 0.05 then
P.Scan(true) -- the plate may be new since the last scan: one forced rescan on a miss
best = P.FindByName(name, want)
end
if not best and AnySCT_Core.debug then dumpMiss(guid, name) end
return best
end
-- Visible plate whose name text is `name`; with several, the one whose health is closest to `want`.
function P.FindByName(name, want)
local best, bestDiff
for i = 1, P.nPlates do
local pl = P.plates[i]
if pl:IsVisible() and pl.asctNameRegion and pl.asctNameRegion.GetText
and pl.asctNameRegion:GetText() == name then
local diff = 0
if want then
local fr = barFraction(pl)
if fr then diff = want - fr; if diff < 0 then diff = -diff end end
end
if not best or diff < bestDiff then best, bestDiff = pl, diff end
end
end
return best
end
-- ---- placement -----------------------------------------------------------------------------------
-- Spawn point of a plate in UIParent coordinates (horizontal centre, TOP edge, like the Blizzard
-- pet text) plus the screen size, or nil when the plate has no position.
function P.PlatePosition(plate)
if not plate then return nil end
local cx, cy = plate:GetCenter()
if not cx then return nil end
local top = plate.GetTop and plate:GetTop() or nil
if top then cy = top end
local ps = plate:GetEffectiveScale()
local us = UIParent:GetEffectiveScale()
if ps and us and us > 0 then
cx = cx * ps / us
cy = cy * ps / us
end
return cx, cy, UIParent:GetWidth(), UIParent:GetHeight()
end
-- Decide the display mode. opt needs: placement ("clamp" | "anchorOff" | "anchor"), noPlate
-- ("anchor" | "hide"), edgeMargin. Returns mode, x, y, screenW, screenH (x, y only for mode 1/2).
function P.Select(plate, opt)
if opt.placement == "anchor" then return 3 end
local cx, cy, w, h = P.PlatePosition(plate)
if not cx then
if opt.noPlate == "hide" then return 0 end
return 3
end
local m = opt.edgeMargin or 0
if cx >= m and cx <= w - m and cy >= m and cy <= h - m then return 1, cx, cy, w, h end
if opt.placement == "anchorOff" then return 3 end
return 2, cx, cy, w, h
end
AnySCT_SelectMode = P.Select
-- Does the plate still show the mob? Uses the last scan only (no per-frame GetName).
function P.PlateMatches(plate, guidNorm)
if not plate:IsVisible() then return false end
if guidNorm and P.guidCapable then
local mapped = P.byGuid[guidNorm]
if mapped then return mapped == plate end
-- the mob's GUID is not exposed (plate was found by name): keep following unless the plate
-- now shows a different, GUID-exposing mob
for k, pl in pairs(P.byGuid) do
if pl == plate then return false end
end
end
return true
end
-- Per-frame follow of a line (fields: plate, guidNorm, gen, valid, lastX, lastY, lastW, lastH).
-- The identity check runs only when a new scan generation exists; between scans a frame costs a
-- visibility test and a position read, no allocation.
function P.FollowLine(s)
if s.gen ~= P.gen then
s.gen = P.gen
s.valid = P.PlateMatches(s.plate, s.guidNorm)
end
if s.valid and s.plate:IsVisible() then
local px, py, w, h = P.PlatePosition(s.plate)
if px then s.lastX, s.lastY, s.lastW, s.lastH = px, py, w, h end
end
end
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+671
View File
@@ -0,0 +1,671 @@
-- AnySCT sct.lua -- floating combat text for the listed targetSCTs, styled after the client's own
-- world damage numbers.
--
-- Where a line appears is decided by AnySCT_Plates.Select (plates.lua): over the mob's nameplate
-- (top centre), clamped to the screen when the plate is off screen, or at the fixed anchor
-- AnySCTAnchor. Plate lines follow the plate every tick; anchor lines scroll from the anchor.
--
-- Engine: a fixed FontString pool and ONE shared OnUpdate that exists only while a line is in
-- flight (idle cost zero). Nothing is allocated per tick. This file only draws its own frames and
-- never touches Blizzard's CombatText, SHOW_COMBAT_TEXT or another SCT addon.
AnySCT_SCT = {}
local S = AnySCT_SCT
-- ONE table for every visual / motion parameter; the animation code reads only from here.
-- Values are measured engine parameters of the 1.12.1 client's own world text; colours and size match
-- the client's pet text. Retune by changing values here.
AnySCT_STYLE = {
font = "CLIENT", -- key of S.FONTS, or "CLIENT" = global DAMAGE_TEXT_FONT (pfUI honoured)
lifetime = 1.5, -- seconds, every text
fadeIn = 0.15, -- alpha 0 -> 1 over this many seconds
fadeOutNumber = 0.76, -- fade-out start (seconds); alpha reaches 0 at the lifetime
fadeOutCrit = 1.0,
fadeOutWord = 1.0, -- Miss / Dodge / Parry / Block ...
fadeOutAbsorb = 0.09, -- "Absorb" is a quick flicker
riseHeight = 120, -- px a normal text rises (engine: 2 world units; screen distance varies)
critRiseFactor = 0, -- crits do not rise, they pop in place
sizeScale = 0.018333, -- text pixel size = scale x screen diagonal (1024x768 -> 23 px)
critSizeScale = 0.0275, -- crits (1024x768 -> 35 px)
shadow = 0.002, -- shadow offset, fraction of the viewport per axis (down-right)
spread = 0, -- random horizontal px per line (the engine stacks instead)
maxLines = 60, -- total lines on screen (several mobs with bursts)
maxPerTarget = 16, -- lines per mob: lifetime 1.5 s x 9 hits/s = 14 live lines, none recycled early
colorMelee = { 1.0, 0.518, 0.0 }, -- 0xFFFF8400
colorSpell = { 1.0, 0.871, 0.0 }, -- 0xFFFFDE00
-- crit size factor keyframes {fraction of the lifetime, factor on the crit size}; linear in
-- between, last value held. {{0,1},{1,1}} turns the pop off.
critKeyframes = { { 0, 0.1 }, { 0.1, 2.0 }, { 0.2, 1.0 }, { 1, 1.0 } },
colorHeal = { 0.1, 1.0, 0.1 }, -- heals on a listed targetSCT
glancingScale = 0.75, -- only with the colour scheme on
}
local STYLE = AnySCT_STYLE
-- Option defaults follow the style table (core.lua loads first and owns the defaults table).
if AnySCT_DEFAULTS then
AnySCT_DEFAULTS.colorMelee = STYLE.colorMelee
AnySCT_DEFAULTS.colorSpell = STYLE.colorSpell
AnySCT_DEFAULTS.colorHeal = STYLE.colorHeal
AnySCT_DEFAULTS.riseHeight = STYLE.riseHeight
AnySCT_DEFAULTS.jitter = STYLE.spread
AnySCT_DEFAULTS.maxLines = STYLE.maxLines
AnySCT_DEFAULTS.maxLinesPerTarget = STYLE.maxPerTarget
AnySCT_DEFAULTS.font = STYLE.font
end
-- Piecewise-linear curve value at t. Pure, allocation-free.
function S.KeyScale(kf, t)
local n = table.getn(kf)
if t <= kf[1][1] then return kf[1][2] end
for i = 2, n do
local b = kf[i]
if t <= b[1] then
local a = kf[i - 1]
local span = b[1] - a[1]
if span <= 0 then return b[2] end
return a[2] + (b[2] - a[2]) * ((t - a[1]) / span)
end
end
return kf[n][2]
end
function S.CurveActive(kf)
for i = 1, table.getn(kf) do
if kf[i][2] ~= 1 then return true end
end
return false
end
function S.CurveAnimated(kf)
for i = 2, table.getn(kf) do
if kf[i][2] ~= kf[1][2] then return true end
end
return false
end
-- Text pixel size in UI units: scale x diagonal x factor. (pixels = units x effective scale, and
-- the diagonal in pixels = the UI-unit diagonal x effective scale, so the scale cancels.)
function S.TextSize(scale, factor, sw, sh)
return scale * factor * math.sqrt(sw * sw + sh * sh)
end
S.COLORS = {
heal = STYLE.colorHeal,
pet = STYLE.colorMelee,
spell = STYLE.colorSpell,
hit = { 1.00, 1.00, 1.00 },
crit = { 1.00, 0.90, 0.15 },
glancing = { 0.62, 0.62, 0.62 },
crushing = { 1.00, 0.55, 0.10 },
miss = { 1.00, 0.25, 0.25 },
dodge = { 1.00, 0.25, 0.25 },
parry = { 1.00, 0.25, 0.25 },
block = { 0.35, 0.55, 1.00 },
absorb = { 0.75, 0.40, 1.00 },
resist = { 0.75, 0.40, 1.00 },
immune = { 0.75, 0.40, 1.00 },
evade = { 0.75, 0.40, 1.00 },
deflect = { 0.75, 0.40, 1.00 },
}
-- Text colour r, g, b. Precedence:
-- 1. heal colour (ckey "heal")
-- 2. the listed source's own colour (option colorPerTarget on and res.srcRgb set); the same colour
-- for numbers, words and spells, whatever the colour scheme says
-- 3. ckey "pet" / "spell" (colour scheme off): the class colour of the source when the option is on
-- and the class is known (pets and totems carry their owner's), else the configured melee /
-- spell colour
-- 4. every other key: a per-kind colour of the colourize scheme.
function S.PickColour(res, opt, ckey)
if ckey == "heal" then
local h = opt.colorHeal or S.COLORS.heal
return h[1], h[2], h[3]
end
if opt.colorPerTarget and res.srcRgb then
local c = res.srcRgb
return c[1], c[2], c[3]
end
if ckey == "pet" or ckey == "spell" then
if opt.classColours and res.class and RAID_CLASS_COLORS then
local rc = RAID_CLASS_COLORS[res.class]
if rc then return rc.r, rc.g, rc.b end
end
local c = (ckey == "spell") and opt.colorSpell or opt.colorMelee
if c then return c[1], c[2], c[3] end
end
local c = S.COLORS[ckey] or S.COLORS.pet
return c[1], c[2], c[3]
end
-- Pure: GO / WAIT label text. srcName / mobName are optional prefixes.
-- GO +4210 (buffer = threat distance to pulling aggro)
-- WAIT 1250 / 3000 (buffer / required buffer)
function S.ThreatText(status, buffer, need, srcName, mobName)
local b = math.floor((buffer or 0) + 0.5)
local t
if status == "GO" then
t = "GO +" .. b
else
t = "WAIT " .. b .. " / " .. math.floor((need or 0) + 0.5)
end
if srcName then t = srcName .. " " .. t end
if mobName then t = mobName .. ": " .. t end
return t
end
S.WORDS = {
miss = "Miss", dodge = "Dodge", parry = "Parry", block = "Block", absorb = "Absorb",
resist = "Resist", immune = "Immune", evade = "Evade", deflect = "Deflect",
}
S.FONTS = {
FRIZQT = "Fonts\\FRIZQT__.TTF",
SKURRI = "Fonts\\skurri.ttf",
ARIALN = "Fonts\\ARIALN.TTF",
}
function S.FontPath(choice)
if choice == "CLIENT" then
if type(DAMAGE_TEXT_FONT) == "string" then return DAMAGE_TEXT_FONT end
return S.FONTS.FRIZQT
end
return S.FONTS[choice] or S.FONTS.FRIZQT
end
-- Pure: result + options + display mode -> text, colour key, isWord, size multiplier, suffix.
-- Numbers are bare. Mode 3 (fixed anchor) always names the mob: the position no longer identifies it.
function S.Format(res, opt, mode)
local kind = res.kind
local word = S.WORDS[kind]
local amount = res.amount or 0
local text
local isWord = false
if word and (amount <= 0 or opt.colourize) then
isWord = true
if amount > 0 then text = word .. " " .. amount else text = word end
else
text = tostring(amount)
end
if opt.markGlancing and (kind == "glancing" or kind == "crushing") then
text = text .. " " .. kind
end
local mul = 1
if kind == "glancing" and opt.colourize then mul = STYLE.glancingScale end
local suffix
if kind == "heal" then
text = "+" .. text
if opt.healShowHealer and res.healer then suffix = res.healer end
elseif opt.showSpellName and res.spell then
suffix = res.spell
end
-- the listed source's name comes first: Source - Spell - Victim (never on heals: the text is over it)
if kind ~= "heal" and res.source then
local sm = opt.sourceNameMode
if sm == "always" or (sm == "multi" and res.srcMulti) then
local nm = res.source
if res.srcPet then nm = nm .. " (" .. res.srcPet .. ")" end
if suffix then suffix = nm .. " - " .. suffix else suffix = nm end
end
end
if res.mobName and (opt.mobNameMode == "always" or (mode == 3 and opt.mobNameMode == "anchor")) then
if suffix then suffix = suffix .. " - " .. res.mobName else suffix = res.mobName end
end
local colourKey = (res.spell or res.spellId) and "spell" or "pet"
if kind == "heal" then colourKey = "heal"
elseif opt.colourize then colourKey = kind end
return text, colourKey, isWord, mul, suffix
end
if CreateFrame then
local W = AnySCT_W
local C = AnySCT_Core
local P = AnySCT_Plates
local POOL_MAX = 100 -- slot tables only; FontStrings are created on first use (ensureFs)
local critAnimated = S.CurveAnimated(STYLE.critKeyframes)
local critActive = S.CurveActive(STYLE.critKeyframes)
local GAP = 3 -- px between the plate top and the text
local anchor, float, listLabel
local moveMode = false
local activeCount = 0
local slots = {}
local freeStack, freeTop = {}, 0
for i = 1, POOL_MAX do
slots[i] = { active = false, fs = nil, sfx = nil, t = 0, dur = 1.5, rise = 120, dir = 1,
dx = 0, push = 0, base = 24, crit = false, key = nil, fadeOut = 1,
plate = nil, guidNorm = nil, lastX = nil, lastY = nil, lastW = 0, lastH = 0 }
freeTop = freeTop + 1
freeStack[freeTop] = i
end
local function opt(key) return C.Opt(key) end
local function setFont(fs, size)
fs:SetFont(S.FontPath(opt("font")), size) -- no outline: the engine text uses a shadow
end
local function applyMoveVisuals()
if moveMode then
anchor:EnableMouse(true)
anchor:SetBackdropColor(0.05, 0.08, 0.12, 0.8)
anchor:SetBackdropBorderColor(0.9, 0.78, 0.49, 1)
else
anchor:EnableMouse(false)
anchor:SetBackdropColor(0, 0, 0, 0)
anchor:SetBackdropBorderColor(0, 0, 0, 0)
end
end
local function placeAnchor()
local pos = opt("pos") or AnySCT_DEFAULTS.pos
anchor:ClearAllPoints()
anchor:SetPoint(pos.point or "CENTER", UIParent, pos.point or "CENTER", pos.x, pos.y)
end
local onUpdate, followUpdate
local function build()
if anchor then return anchor end
-- container of every floating line; SetClampedToScreen is a second safety net
float = CreateFrame("Frame", "AnySCTFloat", UIParent)
float:SetAllPoints(UIParent)
float:SetFrameStrata("HIGH")
float:EnableMouse(false)
float:SetClampedToScreen(true)
local a = CreateFrame("Frame", "AnySCTAnchor", UIParent)
a:SetWidth(160)
a:SetHeight(30)
a:SetFrameStrata("HIGH")
a:SetFrameLevel(10)
a:SetMovable(true)
a:EnableMouse(false)
a:SetClampedToScreen(true)
a:RegisterForDrag("LeftButton")
a:SetBackdrop(W.SOLID_BACKDROP)
a:SetScript("OnDragStart", function()
if moveMode then this:StartMoving() end
end)
a:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
local cx, cy = this:GetCenter()
C.SetOpt("pos", { point = "CENTER", x = cx - UIParent:GetWidth() / 2, y = cy - UIParent:GetHeight() / 2 })
end)
anchor = a
listLabel = a:CreateFontString("AnySCTLabel", "OVERLAY")
W.ApplyFont(listLabel, 11, "OUTLINE")
listLabel:SetPoint("BOTTOM", a, "TOP", 0, 2)
placeAnchor()
a:Show()
applyMoveVisuals()
S.UpdateLabel()
return a
end
S.Build = build
function S.UpdateLabel()
if not anchor then return end
if not opt("showLabel") then listLabel:Hide() return end
listLabel:Show()
local st = C.state
if table.getn(st.order) == 0 then
listLabel:SetText("targetSCTs: none")
listLabel:SetTextColor(0.55, 0.55, 0.55)
else
listLabel:SetText("targetSCTs: " .. table.concat(st.order, ", "))
listLabel:SetTextColor(1, 1, 1)
end
end
-- ---- pool ---------------------------------------------------------------------------------
local function ensureFs(i)
local s = slots[i]
if s.fs then return end
s.fs = float:CreateFontString("AnySCTNum" .. i, "OVERLAY")
s.sfx = float:CreateFontString("AnySCTSfx" .. i, "OVERLAY")
end
-- Free slot; at a cap (total maxLines, or maxLinesPerTarget lines of one mob) the oldest
-- matching line is recycled. key identifies the mob (guid, name, or "anchor").
local function acquire(key)
local maxLines = opt("maxLines") or STYLE.maxLines
if maxLines > POOL_MAX then maxLines = POOL_MAX end
local perTarget = opt("maxLinesPerTarget") or STYLE.maxPerTarget
local same = 0
for i = 1, POOL_MAX do
if slots[i].active and slots[i].key == key then same = same + 1 end
end
if same < perTarget and freeTop > 0 and activeCount < maxLines then
local i = freeStack[freeTop]
freeStack[freeTop] = nil
freeTop = freeTop - 1
activeCount = activeCount + 1
return i
end
local oldest, oldestT = 1, -1
for i = 1, POOL_MAX do
local sl = slots[i]
if sl.active and (same < perTarget or sl.key == key) and sl.t > oldestT then
oldestT = sl.t; oldest = i
end
end
return oldest
end
local function release(i)
freeTop = freeTop + 1
freeStack[freeTop] = i
activeCount = activeCount - 1
end
onUpdate = function()
local dt = arg1
local any = false
local margin = opt("edgeMargin") or 40
local xo, yo = opt("xOffset") or 0, opt("yOffset") or 0
local needScan = false
for i = 1, POOL_MAX do
local s = slots[i]
if s.active then
s.t = s.t + dt
if s.t >= s.dur then
s.active = false
s.fs:Hide()
s.sfx:Hide()
release(i)
else
any = true
local t = s.t
local a = 1
if t < STYLE.fadeIn then a = t / STYLE.fadeIn end
if t > s.fadeOut then a = a * (s.dur - t) / (s.dur - s.fadeOut) end
local rise = s.rise
if s.crit then
rise = rise * STYLE.critRiseFactor
if critAnimated then
s.fs:SetTextHeight(s.base * S.KeyScale(STYLE.critKeyframes, t / s.dur))
end
end
local off = (t / s.dur * rise + s.push) * s.dir
s.fs:ClearAllPoints()
if s.plate then
needScan = true
P.FollowLine(s)
local x, y = s.lastX + xo + s.dx, s.lastY + GAP + yo + off
if not (s.lastX >= margin and s.lastX <= s.lastW - margin
and s.lastY >= margin and s.lastY <= s.lastH - margin) then
x, y = AnySCT_ClampToScreen(x, y, s.lastW, s.lastH, margin)
end
-- a tall stack of a burst never leaves the screen at the top
if y > s.lastH - margin then y = s.lastH - margin end
s.fs:SetPoint("BOTTOM", UIParent, "BOTTOMLEFT", x, y)
else
s.fs:SetPoint("BOTTOM", anchor, "CENTER", s.dx, off)
end
s.fs:SetAlpha(a)
s.fs:SetShadowColor(0, 0, 0, a)
s.sfx:SetAlpha(a)
end
end
end
if needScan then P.Scan(false) end -- 0.2 s cadence, keeps the scan generation moving
if not any then float:SetScript("OnUpdate", nil) end
end
-- Spawn one line. mode/plate/px.. come from P.Select (mode 3 = fixed anchor).
local function spawn(res, mode, plate, px, py, pw, ph)
local o = C.Opts()
local key = res.mobGuid or res.mobName or "anchor"
if mode == 3 then key = "anchor" end
local i = acquire(key)
local s = slots[i]
C.Ledger(res, s.active and "shown+recycled" or "shown")
s.key = key
ensureFs(i)
local text, ckey, isWord, mul, suffix = S.Format(res, o, mode)
local fs, sfx = s.fs, s.sfx
local sw, sh = GetScreenWidth(), GetScreenHeight()
s.crit = (res.kind == "crit") or (res.crit and true or false)
local scale = s.crit and STYLE.critSizeScale or STYLE.sizeScale
local factor = isWord and (o.wordFactor or 1) or (o.sizeFactor or 1)
local base = S.TextSize(scale, factor, sw, sh)
s.base = base * mul
-- older lines of the same mob move one text height away
for j = 1, POOL_MAX do
if slots[j].active and slots[j].key == key then slots[j].push = slots[j].push + s.base end
end
s.active = true
s.t = 0
s.push = 0
s.dur = STYLE.lifetime
s.rise = o.riseHeight or STYLE.riseHeight
s.dir = (o.direction == "down") and -1 or 1
local jt = o.jitter or 0
if jt > 0 then s.dx = math.random(-jt, jt) else s.dx = 0 end
if isWord then
s.fadeOut = (res.kind == "absorb") and STYLE.fadeOutAbsorb or STYLE.fadeOutWord
elseif s.crit then
s.fadeOut = STYLE.fadeOutCrit
else
s.fadeOut = STYLE.fadeOutNumber
end
if mode == 3 then
s.plate, s.guidNorm, s.valid = nil, nil, false
else
s.plate = plate
s.guidNorm = res.mobGuid and C.NormGuid(res.mobGuid) or nil
s.lastX, s.lastY, s.lastW, s.lastH = px, py, pw, ph
s.gen, s.valid = P.gen, true
end
local dxs, dys = STYLE.shadow * sw, STYLE.shadow * sh
setFont(fs, s.base)
if s.crit and critActive then
fs:SetTextHeight(s.base * S.KeyScale(STYLE.critKeyframes, 0))
else
fs:SetTextHeight(s.base)
end
fs:SetShadowOffset(dxs, -dys)
local cr, cg, cb = S.PickColour(res, o, ckey)
fs:SetTextColor(cr, cg, cb)
fs:SetText(text)
fs:SetAlpha(0)
fs:SetShadowColor(0, 0, 0, 0)
fs:Show()
if suffix then
sfx:SetFont(S.FontPath(o.font), math.max(10, math.floor(s.base * 0.3)))
sfx:SetShadowOffset(dxs, -dys)
sfx:SetShadowColor(0, 0, 0, 1)
sfx:SetTextColor(cr, cg, cb)
sfx:SetText(suffix)
sfx:ClearAllPoints()
sfx:SetPoint("TOP", fs, "BOTTOM", 0, 0)
sfx:SetAlpha(0)
sfx:Show()
else
sfx:Hide()
end
float:SetScript("OnUpdate", onUpdate)
end
function S.Show(res)
if not res then return end
build()
local o = C.Opts()
local plate
if o.placement ~= "anchor" and (res.mobGuid or res.mobName) then
plate = P.Find(res.mobGuid, res.mobName, res.mobUnit)
end
local mode, px, py, pw, ph = P.Select(plate, o)
if C.debug then
C.Dbg("place " .. tostring(res.mobName) .. ": " .. (plate and "plate found" or "no plate")
.. ", mode " .. tostring(mode) .. (mode == 0 and " (hidden by option)" or ""))
end
if mode == 0 then
C.Ledger(res, "dropped:placement")
return
end
spawn(res, mode, plate, px, py, pw, ph)
end
function S.OptionChanged(key)
if not anchor then return end
if key == "all" or key == "pos" then placeAnchor() end
S.UpdateLabel()
end
-- ---- GO / WAIT threat label ------------------------------------------------------------------
-- ONE label over the nameplate of the PLAYER's target. core.lua decides status and numbers
-- (C.ThreatDecide); this only draws. The follow OnUpdate is attached only while the label is shown.
local TL = { fs = nil, shown = false, plate = nil, status = nil, text = nil, lastText = nil,
lastMode = 0, lastName = nil, mobName = nil, guid = nil }
local followFrame
local findAcc = 0
-- status nil hides the label.
function S.SetThreat(status, buffer, need, srcName)
if not anchor then return end
if not status then
if TL.shown then
TL.shown = false
if TL.fs then TL.fs:Hide() end
end
if followFrame then followFrame:SetScript("OnUpdate", nil) end
return
end
if not TL.fs then
TL.fs = float:CreateFontString("AnySCTThreatLabel", "OVERLAY")
TL.fs:SetFont(S.FontPath(opt("font")), 14, "OUTLINE")
end
TL.status = status
TL.text = S.ThreatText(status, buffer, need, srcName, nil)
TL.shown = true
findAcc = 1 -- look the plate up on the next frame
if not followFrame then followFrame = CreateFrame("Frame", "AnySCTFollowFrame", UIParent) end
followFrame:SetScript("OnUpdate", followUpdate)
end
followUpdate = function()
if not TL.shown then return end
findAcc = findAcc + arg1
local o = C.Opts()
if findAcc >= 0.5 then
findAcc = 0
TL.plate = nil
TL.mobName = UnitName("target")
TL.guid = C.ResolveGuid("target")
if o.placement ~= "anchor" then TL.plate = P.Find(TL.guid, TL.mobName, "target") end
end
local margin = o.edgeMargin or 40
local lift = S.TextSize(STYLE.sizeScale, o.sizeFactor or 1, GetScreenWidth(), GetScreenHeight()) + 6
local mode, px, py, pw, ph = P.Select(TL.plate, o)
if mode == 0 then TL.fs:Hide() return end
local name = (mode == 3) and TL.mobName or nil
if TL.text ~= TL.lastText or mode ~= TL.lastMode or name ~= TL.lastName then
TL.lastText, TL.lastMode, TL.lastName = TL.text, mode, name
TL.fs:SetText(name and (name .. ": " .. TL.text) or TL.text)
if TL.status == "GO" then TL.fs:SetTextColor(0.2, 1, 0.2) else TL.fs:SetTextColor(1, 0.6, 0.1) end
end
TL.fs:ClearAllPoints()
if mode == 3 then
TL.fs:SetPoint("BOTTOM", anchor, "TOP", 0, 18)
else
local x, y = px, py + lift
if mode == 2 then x, y = AnySCT_ClampToScreen(x, y, pw, ph, margin) end
TL.fs:SetPoint("BOTTOM", UIParent, "BOTTOMLEFT", x, y)
end
TL.fs:Show()
end
-- ---- demo / move / memory ------------------------------------------------------------------
-- Fixed sequence for side-by-side comparison with the player's own real numbers.
local DEMO = {
{ kind = "hit", amount = 72 }, { kind = "hit", amount = 66 }, { kind = "crit", amount = 141 },
{ kind = "miss", amount = 0 }, { kind = "dodge", amount = 0 }, { kind = "parry", amount = 0 },
{ kind = "block", amount = 0 },
}
local demoIdx, demoLeft, demoAcc, demoTicker, demoAtAnchor = 0, 0, 0, nil, false
-- /asct demo plays at the nameplate of the current target (the same placement rules as real
-- events); without a target, or with atAnchor, at the fixed anchor.
function S.Demo(atAnchor)
build()
demoLeft = table.getn(DEMO)
demoIdx = 0
demoAcc = 0
demoAtAnchor = atAnchor and true or false
if not demoTicker then demoTicker = CreateFrame("Frame", "AnySCTDemoFrame", UIParent) end
demoTicker:SetScript("OnUpdate", function()
demoAcc = demoAcc + arg1
if demoAcc < 0.45 then return end
demoAcc = 0
if demoLeft <= 0 then this:SetScript("OnUpdate", nil) return end
demoLeft = demoLeft - 1
demoIdx = demoIdx + 1
local res = DEMO[demoIdx]
res.mobGuid, res.mobName, res.mobUnit = nil, nil, nil
if not demoAtAnchor and UnitExists("target") then
res.mobGuid = C.ResolveGuid("target")
res.mobName = UnitName("target")
res.mobUnit = "target"
S.Show(res)
else
spawn(res, 3)
end
end)
end
function S.FontStringCount()
local n = 1 -- the targetSCT list label
for i = 1, POOL_MAX do
if slots[i].fs then n = n + 2 end
end
if TL.fs then n = n + 1 end
return n
end
-- /asct mem: gcinfo() is KB in use. Phase 1: 10 s idle. Phase 2: a demo burst.
local memFrame, memPhase, memT, memStart = nil, 0, 0, 0
function S.Mem()
build()
if not memFrame then memFrame = CreateFrame("Frame", "AnySCTMemFrame", UIParent) end
C.Print("memory now " .. gcinfo() .. " KB, " .. S.FontStringCount() .. " FontStrings; measuring 10 s idle ...")
memPhase, memT, memStart = 1, 0, gcinfo()
memFrame:SetScript("OnUpdate", function()
memT = memT + arg1
if memPhase == 1 and memT >= 10 then
C.Print("idle 10 s: " .. (gcinfo() - memStart) .. " KB change; starting demo burst ...")
memPhase, memT, memStart = 2, 0, gcinfo()
S.Demo(true)
elseif memPhase == 2 and memT >= 7 then
C.Print("demo burst: " .. (gcinfo() - memStart) .. " KB change; now " .. gcinfo()
.. " KB, " .. S.FontStringCount() .. " FontStrings")
memPhase = 0
this:SetScript("OnUpdate", nil)
end
end)
end
function S.SetMoveMode(on)
build()
moveMode = on and true or false
applyMoveVisuals()
if moveMode then
S.Demo(true)
C.Print("anchor unlocked: drag the box, then /anysct lock")
else
C.Print("anchor locked")
end
if AnySCT_Options and AnySCT_Options.refresh then AnySCT_Options.refresh() end
end
function S.IsMoveMode() return moveMode end
function S.ToggleMove() S.SetMoveMode(not moveMode) end
end
+70
View File
@@ -0,0 +1,70 @@
-- AnySCT widgets.lua -- small UI helpers shared by the minimap button and the options panel.
AnySCT_W = {}
local W = AnySCT_W
W.FONT = "Fonts\\FRIZQT__.TTF"
W.COLORS = {
title = { r = 0.90, g = 0.78, b = 0.49 },
ink = { r = 0.79, g = 0.80, b = 0.83 },
dim = { r = 0.53, g = 0.55, b = 0.60 },
}
W.PANEL_BACKDROP = {
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 },
}
W.SOLID_BACKDROP = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
}
-- Keep our pixel size; take pfUI's font family when pfUI is loaded.
function W.ApplyFont(fs, size, flags)
size = size or 10
if pfUI and pfUI.font_default then
fs:SetFont(pfUI.font_default, size, flags)
else
fs:SetFont(W.FONT, size, flags)
end
end
function W.ApplyShadow(fs, on)
if on then
fs:SetShadowColor(0, 0, 0, 0.9)
fs:SetShadowOffset(1, -1)
else
fs:SetShadowColor(0, 0, 0, 0)
fs:SetShadowOffset(0, 0)
end
end
function W.MakeText(parent, name, size, color, layer)
local fs = parent:CreateFontString(name, layer or "ARTWORK")
W.ApplyFont(fs, size or 11)
local c = color or W.COLORS.ink
fs:SetTextColor(c.r, c.g, c.b)
return fs
end
-- Tooltip on any frame (title line + wrapped body).
function W.AttachTooltip(frame, title, body)
frame.asctTitle = title
frame.asctBody = body
frame:SetScript("OnEnter", function()
if not GameTooltip then return end
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetText(this.asctTitle or "", 1, 1, 1)
if this.asctBody then GameTooltip:AddLine(this.asctBody, 0.8, 0.8, 0.8, 1) end
GameTooltip:Show()
end)
frame:SetScript("OnLeave", function()
if GameTooltip then GameTooltip:Hide() end
end)
end