AnySCT 0.2.0
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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).
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 976 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 460 KiB |
|
After Width: | Height: | Size: 464 KiB |
|
After Width: | Height: | Size: 468 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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
|
||||||
@@ -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
|
||||||