From 3d4b685a5006089119b8f562aa16661f1864cd8f Mon Sep 17 00:00:00 2001 From: Lasse Nielsen Date: Sun, 1 Mar 2026 21:19:56 +0100 Subject: [PATCH 1/3] fix: correct TalentMap indices for all classes (#21) Verified all TalentMap talent positions (tab:index) against in-game GetTalentInfo output on TBC Anniversary client (1.15.x). Classes corrected (key changes): - Priest: 7 of 8 entries - Warrior: 10 of 11 entries + deferred comments - Rogue: 15 of 17 entries + deferred comments - Paladin: 8 of 9 entries Classes verified correct (docs only): - Mage: all 21 entries already correct - Druid: all 19 entries already correct - Shaman: all 12 entries already correct All files now include grid layout documentation showing the full talent tree ordered by internal talentID, matching the in-game API. Also fixed .busted config pattern from '_spec' to 'test_' to match actual test file naming convention. --- .busted | 2 +- AGENTS.md | 145 ++++++++- Data/TalentMap_Druid.lua | 44 ++- Data/TalentMap_Hunter.lua | 126 +++++--- Data/TalentMap_Mage.lua | 54 +++- Data/TalentMap_Paladin.lua | 90 ++++-- Data/TalentMap_Priest.lua | 96 ++++-- Data/TalentMap_Rogue.lua | 522 ++++++++++++++++----------------- Data/TalentMap_Shaman.lua | 41 ++- Data/TalentMap_Warrior.lua | 212 +++++++------ tests/test_hunter.lua | 58 ++-- tests/test_paladin_talents.lua | 96 +++--- tests/test_priest_auras.lua | 2 +- tests/test_priest_talents.lua | 54 ++-- tests/test_rogue_auras.lua | 6 +- tests/test_rogue_talents.lua | 182 ++++++------ tests/test_warrior_talents.lua | 46 +-- 17 files changed, 1080 insertions(+), 696 deletions(-) diff --git a/.busted b/.busted index 4e642b2..ab3b4ae 100644 --- a/.busted +++ b/.busted @@ -4,7 +4,7 @@ return { }, default = { ROOT = { 'tests' }, - pattern = '_spec', + pattern = 'test_', verbose = true, }, } diff --git a/AGENTS.md b/AGENTS.md index 960d73d..c2a8219 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -166,11 +166,138 @@ task(subagent_type="wowhead-researcher", prompt="Look up all ranks of Fireball f --- -## GitHub Project Board +## Code Style + +### Formatting +- Indent with **4 spaces**, no tabs +- Max line length **120** unless the addon `.luacheckrc` disables it +- Spaces around operators: `local x = 1 + 2` +- No trailing whitespace +- Use plain hyphens (`-`), **never** em or en dashes + +### File Header +Every Lua file starts with: + +```lua +------------------------------------------------------------------------------- +-- FileName.lua +-- Brief description +-- +-- Supported versions: Retail, MoP Classic, TBC Anniversary, Cata, Classic +------------------------------------------------------------------------------- +``` + +### Imports and Scoping +- Use the shared namespace: `local ADDON_NAME, ns = ...` +- Cache WoW API and Lua globals used more than once as locals at the top of the file +- Keep addon logic in locals; only SavedVariables and `SLASH_*` are global +- Use `LibStub` for Ace3 or other embedded libs; never global `require` + +```lua +local ADDON_NAME, ns = ... +local CreateFrame = CreateFrame +local GetTime = GetTime +local LSM = LibStub("LibSharedMedia-3.0") +``` + +### Naming + +| Element | Convention | Example | +|---------|------------|---------| +| Files | PascalCase | `MyAddon_Core.lua` | +| SavedVariables | PascalCase | `MyAddonDB` | +| Local variables | camelCase | `local currentState` | +| Functions (public or local) | PascalCase | `local function UpdateState()` | +| Constants | UPPER_SNAKE | `local MAX_RETRIES = 5` | +| Slash commands | UPPER_SNAKE | `SLASH_MYADDON1` | +| Color codes | UPPER_SNAKE | `local COLOR_RED = "\|cffff0000"` | +| Unused args | underscore prefix | `local _unused` | + +### Types +- Default to plain Lua 5.1 with no annotations +- Only add LuaLS annotations when the file already uses them or for public library APIs +- Keep annotations minimal and accurate; do not introduce new tooling + +### Functions and Structure +- Keep functions under 50 lines; extract helpers when longer +- Prefer early returns over deep nesting +- Prefer composition over inheritance +- Keep logic separated by layer when possible: Core (WoW API), Engine (pure Lua), + Data (tables), Presentation (UI) + +### Error Handling +- Use defensive nil checks for optional APIs +- For version differences, prefer `or` fallbacks over runtime version checks +- Use `pcall` for user callbacks or APIs that may be missing in some versions +- Use `error(msg, 2)` for public library input validation (reports at caller site) + +--- + +## Versioning and File Loading +- Do not gate features with runtime version checks +- Split version-specific code into separate files +- Load with TOC `## Interface` / `## Interface-*` directives or packager comment + directives (`#@retail@`, `#@non-retail@`) + +Packager directives are comments locally, so later files can override earlier ones. + +--- + +## Common Pitfalls +- Missing APIs for a target version -- check `docs/` for the exact client build +- Deprecated globals like `COMBATLOGENABLED` and `COMBATLOGDISABLED` (removed in Cata; + always provide `or` fallbacks) +- Race conditions on `PLAYER_ENTERING_WORLD` -- use a short `C_Timer.After` delay +- Timer leaks -- cancel `C_Timer` or `AceTimer` handles before reusing +- `GetItemInfo` or item data can be nil on first call -- retry with a timer + +--- + +## GitHub Workflow + +### Issues +Create issues using the repo's issue templates (`.github/ISSUE_TEMPLATE/`): +- **Bug reports**: Use `bug-report.yml` template. Title prefix: `[Bug]: ` +- **Feature requests**: Use `feature-request.yml` template. Title prefix: `[Feature]: ` + +Create via CLI: +```bash +gh issue create --repo / --label "bug" --title "[Bug]: " --body "<body matching template fields>" +gh issue create --repo <ORG>/<REPO> --label "enhancement" --title "[Feature]: <title>" --body "<body matching template fields>" +``` + +### Branches +Use conventional branch prefixes: + +| Prefix | Purpose | Example | +|--------|---------|---------| +| `feat/` | New feature | `feat/87-mail-toasts` | +| `fix/` | Bug fix | `fix/99-anchor-zorder` | +| `refactor/` | Code improvement | `refactor/96-listener-utils` | + +Include the issue number in the branch name when linked to an issue. + +### Commits +Use [Conventional Commits](https://www.conventionalcommits.org/): +- `feat: <description> (#issue)` - new feature +- `fix: <description> (#issue)` - bug fix +- `refactor: <description> (#issue)` - code restructuring +- `docs: <description>` - documentation only + +Always use `--no-gpg-sign` (GPG signing not available in CI agent environments). + +### Pull Requests +1. Create PRs via CLI using the repo's `.github/PULL_REQUEST_TEMPLATE.md` format +2. Link to the issue with `Closes #N` in the PR body +3. PRs require passing status checks (luacheck, test) before merge +4. Squash merge only: `gh pr merge <number> --squash` +5. Branches are auto-deleted after merge + +### Project Boards PhDamage uses the **DragonAddons** org-level GitHub project board (#2) for issue tracking and sprint planning. -### Board Columns +#### Board Columns | Column | Purpose | |--------|---------| @@ -181,7 +308,7 @@ PhDamage uses the **DragonAddons** org-level GitHub project board (#2) for issue | In review | PR submitted, awaiting review | | Done | Merged / released | -### Custom Fields +#### Custom Fields | Field | Values / Type | |-------|---------------| @@ -191,7 +318,7 @@ PhDamage uses the **DragonAddons** org-level GitHub project board (#2) for issue | Start date | Date | | Target date | Date | -### Workflow +#### Workflow 1. **Triage** - New issues land in *To triage*. Assign Priority and Size. 2. **Plan** - Move to *Backlog* or *Ready* depending on urgency. @@ -201,6 +328,16 @@ PhDamage uses the **DragonAddons** org-level GitHub project board (#2) for issue --- +## Working Agreement for Agents +- Addon-level AGENTS.md overrides root rules when present +- Do not add new dependencies without discussing trade-offs +- Run luacheck before and after changes +- If only manual tests exist, document what you verified in-game +- Verify changes in the game client when possible +- Keep changes small and focused; prefer composition over inheritance + +--- + ## Communication Style When responding to or commenting on issues, always write in **first-person singular** ("I") diff --git a/Data/TalentMap_Druid.lua b/Data/TalentMap_Druid.lua index 3d6b578..5e24385 100644 --- a/Data/TalentMap_Druid.lua +++ b/Data/TalentMap_Druid.lua @@ -1,10 +1,14 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Druid --- Druid talent modifier definitions for PhDamage +-- TalentMap_Druid.lua +-- Druid talent effects mapped to modifier descriptors for TBC Anniversary +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) -- -- Supported versions: TBC Anniversary ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... + ns.TalentMap = ns.TalentMap or {} local MOD = ns.MOD @@ -14,6 +18,17 @@ local TalentMap = {} ------------------------------------------------------------------------------- -- Balance (Tab 1) +-- 1:1 Nature's Grasp (1) 1:2 Starlight Wrath (5) +-- 1:3 Improved Moonfire (2) 1:4 Nature's Reach (2) +-- 1:5 Brambles (3) 1:6 Moonglow (3) +-- 1:7 Celestial Focus (3) 1:8 Control of Nature (3) +-- 1:9 Insect Swarm (1) 1:10 Nature's Grace (1) +-- 1:11 Moonfury (5) 1:12 Vengeance (5) +-- 1:13 Moonkin Form (1) 1:14 Improved Nature's Grasp (4) +-- 1:15 Lunar Guidance (3) 1:16 Balance of Power (2) +-- 1:17 Dreamstate (3) 1:18 Improved Faerie Fire (3) +-- 1:19 Wrath of Cenarius (5) 1:20 Force of Nature (1) +-- 1:21 Focused Starlight (2) ------------------------------------------------------------------------------- -- Starlight Wrath: -0.1s cast time on Wrath and Starfire per rank @@ -108,7 +123,18 @@ TalentMap["1:21"] = { } ------------------------------------------------------------------------------- --- Feral (Tab 2) +-- Feral Combat (Tab 2) +-- 2:1 Thick Hide (3) 2:2 Feral Aggression (5) +-- 2:3 Ferocity (5) 2:4 Brutal Impact (2) +-- 2:5 Sharpened Claws (3) 2:6 Feral Instinct (3) +-- 2:7 Primal Fury (2) 2:8 Shredding Attacks (2) +-- 2:9 Predatory Strikes (3) 2:10 Feral Charge (1) +-- 2:11 Savage Fury (2) 2:12 Feral Swiftness (2) +-- 2:13 Heart of the Wild (5) 2:14 Leader of the Pack (1) +-- 2:15 Faerie Fire (Feral) (1) 2:16 Nurturing Instinct (2) +-- 2:17 Primal Tenacity (3) 2:18 Survival of the Fittest (3) +-- 2:19 Predatory Instincts (5) 2:20 Mangle (1) +-- 2:21 Improved Leader of the Pack (2) ------------------------------------------------------------------------------- -- Sharpened Claws: +2% melee crit per rank (feral abilities) @@ -144,6 +170,16 @@ TalentMap["2:19"] = { ------------------------------------------------------------------------------- -- Restoration (Tab 3) +-- 3:1 Improved Mark of the Wild (5) 3:2 Furor (5) +-- 3:3 Nature's Focus (5) 3:4 Naturalist (5) +-- 3:5 Improved Regrowth (5) 3:6 Natural Shapeshifter (3) +-- 3:7 Omen of Clarity (1) 3:8 Gift of Nature (5) +-- 3:9 Intensity (3) 3:10 Improved Rejuvenation (3) +-- 3:11 Nature's Swiftness (1) 3:12 Subtlety (5) +-- 3:13 Improved Tranquility (2) 3:14 Tranquil Spirit (5) +-- 3:15 Swiftmend (1) 3:16 Empowered Touch (2) +-- 3:17 Empowered Rejuvenation (5) 3:18 Natural Perfection (3) +-- 3:19 Tree of Life (1) 3:20 Living Spirit (3) ------------------------------------------------------------------------------- -- Naturalist: -0.1s Healing Touch cast time and +2% physical damage per rank diff --git a/Data/TalentMap_Hunter.lua b/Data/TalentMap_Hunter.lua index 8d90a0a..5145831 100644 --- a/Data/TalentMap_Hunter.lua +++ b/Data/TalentMap_Hunter.lua @@ -1,8 +1,9 @@ local ADDON_NAME, ns = ... ------------------------------------------------------------------------------- --- Hunter Talent Modifiers — TBC Anniversary (2.5.5) --- Source of truth: Wowhead TBC Classic +-- Hunter Talent Modifiers - TBC Anniversary (2.5.5) +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) ------------------------------------------------------------------------------- local MOD = ns.MOD @@ -11,12 +12,23 @@ local TalentMap = {} ------------------------------------------------------------------------------- -- Beast Mastery (Tab 1) +-- 1:1 Improved Aspect of the Monkey (3) 1:2 Improved Aspect of the Hawk (5) +-- 1:3 Pathfinding (2) 1:4 Improved Mend Pet (2) +-- 1:5 Bestial Wrath (1) 1:6 Intimidation (1) +-- 1:7 Spirit Bond (2) 1:8 Endurance Training (5) +-- 1:9 Bestial Discipline (2) 1:10 Bestial Swiftness (1) +-- 1:11 Ferocity (5) 1:12 Thick Hide (3) +-- 1:13 Unleashed Fury (5) 1:14 Frenzy (5) +-- 1:15 Focused Fire (2) 1:16 Improved Revive Pet (2) +-- 1:17 Animal Handler (2) 1:18 Ferocious Inspiration (3) +-- 1:19 Catlike Reflexes (3) 1:20 Serpent's Swiftness (5) +-- 1:21 The Beast Within (1) ------------------------------------------------------------------------------- --- Focused Fire: +1/2% all damage when pet is active --- NOTE: Requires pet to be active — always applied when talent is taken +-- Focused Fire: +1/2% all damage when pet is active (Beast Mastery 1:15) +-- NOTE: Requires pet to be active - always applied when talent is taken -- (Pet is almost always out for BM hunters) -TalentMap["1:3"] = { +TalentMap["1:15"] = { name = "Focused Fire", maxRank = 2, effects = { @@ -31,10 +43,20 @@ TalentMap["1:3"] = { ------------------------------------------------------------------------------- -- Marksmanship (Tab 2) +-- 2:1 Improved Concussive Shot (5) 2:2 Efficiency (5) +-- 2:3 Improved Hunter's Mark (5) 2:4 Lethal Shots (5) +-- 2:5 Aimed Shot (1) 2:6 Improved Arcane Shot (5) +-- 2:7 Barrage (3) 2:8 Improved Stings (5) +-- 2:9 Mortal Shots (5) 2:10 Concussive Barrage (3) +-- 2:11 Scatter Shot (1) 2:12 Trueshot Aura (1) +-- 2:13 Ranged Weapon Specialization (5) 2:14 Combat Experience (2) +-- 2:15 Careful Aim (3) 2:16 Master Marksman (5) +-- 2:17 Silencing Shot (1) 2:18 Go for the Throat (2) +-- 2:19 Rapid Killing (2) 2:20 Improved Barrage (3) ------------------------------------------------------------------------------- --- Lethal Shots: +1/2/3/4/5% ranged crit chance -TalentMap["2:2"] = { +-- Lethal Shots: +1/2/3/4/5% ranged crit chance (Marksmanship 2:4) +TalentMap["2:4"] = { name = "Lethal Shots", maxRank = 5, effects = { @@ -46,21 +68,8 @@ TalentMap["2:2"] = { }, } --- Mortal Shots: +6/12/18/24/30% crit damage bonus -TalentMap["2:10"] = { - name = "Mortal Shots", - maxRank = 5, - effects = { - { - type = MOD.CRIT_MULT_BONUS, - value = 0.06, - perRank = true, - }, - }, -} - --- Barrage: +4/8/12% Multi-Shot and Volley damage -TalentMap["2:13"] = { +-- Barrage: +4/8/12% Multi-Shot and Volley damage (Marksmanship 2:7) +TalentMap["2:7"] = { name = "Barrage", maxRank = 3, effects = { @@ -74,43 +83,67 @@ TalentMap["2:13"] = { }, } --- Ranged Weapon Specialization: +1/2/3/4/5% ranged damage -TalentMap["2:15"] = { - name = "Ranged Weapon Specialization", +-- Improved Stings: +10/20/30% Serpent Sting damage (Marksmanship 2:8) +TalentMap["2:8"] = { + name = "Improved Stings", maxRank = 5, effects = { { type = MOD.DAMAGE_MULTIPLIER, - value = 0.01, + value = 0.06, perRank = true, stacking = "additive", + filter = { spellNames = { "Serpent Sting" } }, }, }, } --- Improved Stings: +10/20/30% Serpent Sting damage +-- Mortal Shots: +6/12/18/24/30% crit damage bonus (Marksmanship 2:9) TalentMap["2:9"] = { - name = "Improved Stings", + name = "Mortal Shots", maxRank = 5, effects = { { - type = MOD.DAMAGE_MULTIPLIER, + type = MOD.CRIT_MULT_BONUS, value = 0.06, perRank = true, + }, + }, +} + +-- Ranged Weapon Specialization: +1/2/3/4/5% ranged damage (Marksmanship 2:13) +TalentMap["2:13"] = { + name = "Ranged Weapon Specialization", + maxRank = 5, + effects = { + { + type = MOD.DAMAGE_MULTIPLIER, + value = 0.01, + perRank = true, stacking = "additive", - filter = { spellNames = { "Serpent Sting" } }, }, }, } ------------------------------------------------------------------------------- -- Survival (Tab 3) +-- 3:1 Humanoid Slaying (3) 3:2 Lightning Reflexes (5) +-- 3:3 Entrapment (3) 3:4 Improved Wing Clip (3) +-- 3:5 Clever Traps (2) 3:6 Deterrence (1) +-- 3:7 Improved Feign Death (2) 3:8 Surefooted (3) +-- 3:9 Deflection (5) 3:10 Counterattack (1) +-- 3:11 Killer Instinct (3) 3:12 Trap Mastery (2) +-- 3:13 Wyvern Sting (1) 3:14 Savage Strikes (2) +-- 3:15 Survivalist (5) 3:16 Monster Slaying (3) +-- 3:17 Resourcefulness (3) 3:18 Survival Instincts (2) +-- 3:19 Thrill of the Hunt (3) 3:20 Expose Weakness (3) +-- 3:21 Master Tactician (5) 3:22 Readiness (1) +-- 3:23 Hawk Eye (3) ------------------------------------------------------------------------------- --- Monster Slaying: +1/2/3% damage vs Beasts, Giants, Dragonkin --- NOTE: Requires creatureTypeFilter support in ModifierCalc +-- Humanoid Slaying: +1/2/3% damage vs Humanoids (Survival 3:1) TalentMap["3:1"] = { - name = "Monster Slaying", + name = "Humanoid Slaying", maxRank = 3, effects = { { @@ -118,41 +151,42 @@ TalentMap["3:1"] = { value = 0.01, perRank = true, stacking = "additive", - filter = { creatureTypes = { "Beast", "Giant", "Dragonkin" } }, + filter = { creatureTypes = { "Humanoid" } }, }, }, } --- Humanoid Slaying: +1/2/3% damage vs Humanoids -TalentMap["3:2"] = { - name = "Humanoid Slaying", +-- Surefooted: +1/2/3% hit chance (Survival 3:8) +TalentMap["3:8"] = { + name = "Surefooted", maxRank = 3, effects = { { - type = MOD.DAMAGE_MULTIPLIER, + type = MOD.SPELL_HIT_BONUS, value = 0.01, perRank = true, - stacking = "additive", - filter = { creatureTypes = { "Humanoid" } }, }, }, } --- Surefooted: +1/2/3% hit chance -TalentMap["3:12"] = { - name = "Surefooted", +-- Monster Slaying: +1/2/3% damage vs Beasts, Giants, Dragonkin (Survival 3:16) +-- NOTE: Requires creatureTypeFilter support in ModifierCalc +TalentMap["3:16"] = { + name = "Monster Slaying", maxRank = 3, effects = { { - type = MOD.SPELL_HIT_BONUS, + type = MOD.DAMAGE_MULTIPLIER, value = 0.01, perRank = true, + stacking = "additive", + filter = { creatureTypes = { "Beast", "Giant", "Dragonkin" } }, }, }, } --- Survival Instincts: +2/4% crit chance for all -TalentMap["3:14"] = { +-- Survival Instincts: +2/4% crit chance for all (Survival 3:18) +TalentMap["3:18"] = { name = "Survival Instincts", maxRank = 2, effects = { diff --git a/Data/TalentMap_Mage.lua b/Data/TalentMap_Mage.lua index 29986bb..75d6267 100644 --- a/Data/TalentMap_Mage.lua +++ b/Data/TalentMap_Mage.lua @@ -1,10 +1,14 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Mage --- Mage talent modifier definitions for PhDamage +-- TalentMap_Mage.lua +-- Mage talent effects mapped to modifier descriptors for TBC Anniversary +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) -- -- Supported versions: TBC Anniversary ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... + ns.TalentMap = ns.TalentMap or {} local SCHOOL_FIRE = ns.SCHOOL_FIRE @@ -15,7 +19,19 @@ local MOD = ns.MOD local TalentMap = {} ------------------------------------------------------------------------------- --- Arcane +-- Arcane (Tab 1) +-- 1:1 Arcane Subtlety (2) 1:2 Arcane Concentration (5) +-- 1:3 Arcane Focus (5) 1:4 Arcane Mind (5) +-- 1:5 Wand Specialization (2) 1:6 Improved Arcane Missiles (5) +-- 1:7 Arcane Impact (3) 1:8 Magic Attunement (2) +-- 1:9 Improved Mana Shield (2) 1:10 Arcane Fortitude (1) +-- 1:11 Presence of Mind (1) 1:12 Arcane Power (1) +-- 1:13 Improved Counterspell (2) 1:14 Arcane Instability (3) +-- 1:15 Arcane Meditation (3) 1:16 Magic Absorption (5) +-- 1:17 Improved Blink (2) 1:18 Arcane Potency (3) +-- 1:19 Prismatic Cloak (2) 1:20 Empowered Arcane Missiles (3) +-- 1:21 Mind Mastery (5) 1:22 Slow (1) +-- 1:23 Spell Power (2) ------------------------------------------------------------------------------- -- Arcane Focus: +2% Arcane hit per rank @@ -47,7 +63,7 @@ TalentMap["1:14"] = { }, } --- Empowered Arcane Missiles: +0.75 total SP coefficient per rank (+0.15 per wave × 5 waves) +-- Empowered Arcane Missiles: +0.75 total SP coefficient per rank (+0.15 per wave x 5 waves) TalentMap["1:20"] = { name = "Empowered Arcane Missiles", maxRank = 3, @@ -67,7 +83,18 @@ TalentMap["1:23"] = { } ------------------------------------------------------------------------------- --- Fire +-- Fire (Tab 2) +-- 2:1 Burning Soul (2) 2:2 Molten Shields (2) +-- 2:3 Improved Scorch (3) 2:4 Improved Fireball (5) +-- 2:5 Improved Fire Blast (3) 2:6 Flame Throwing (2) +-- 2:7 Pyroblast (1) 2:8 Impact (5) +-- 2:9 Improved Flamestrike (3) 2:10 Blast Wave (1) +-- 2:11 Critical Mass (3) 2:12 Ignite (5) +-- 2:13 Fire Power (5) 2:14 Combustion (1) +-- 2:15 Incineration (2) 2:16 Master of Elements (3) +-- 2:17 Playing with Fire (3) 2:18 Blazing Speed (2) +-- 2:19 Molten Fury (2) 2:20 Pyromaniac (3) +-- 2:21 Empowered Fireball (5) 2:22 Dragon's Breath (1) ------------------------------------------------------------------------------- -- Improved Fireball: -0.1s cast time per rank @@ -158,7 +185,18 @@ TalentMap["2:21"] = { } ------------------------------------------------------------------------------- --- Frost +-- Frost (Tab 3) +-- 3:1 Improved Frostbolt (5) 3:2 Frostbite (3) +-- 3:3 Piercing Ice (3) 3:4 Improved Frost Nova (2) +-- 3:5 Improved Blizzard (3) 3:6 Improved Cone of Cold (3) +-- 3:7 Permafrost (3) 3:8 Frost Channeling (3) +-- 3:9 Shatter (5) 3:10 Winter's Chill (5) +-- 3:11 Icy Veins (1) 3:12 Frost Warding (2) +-- 3:13 Ice Barrier (1) 3:14 Cold Snap (1) +-- 3:15 Ice Shards (5) 3:16 Arctic Reach (2) +-- 3:17 Elemental Precision (3) 3:18 Frozen Core (3) +-- 3:19 Ice Floes (2) 3:20 Arctic Winds (5) +-- 3:21 Empowered Frostbolt (5) 3:22 Summon Water Elemental (1) ------------------------------------------------------------------------------- -- Improved Frostbolt: -0.1s cast time per rank @@ -181,7 +219,7 @@ TalentMap["3:3"] = { }, } --- Improved Cone of Cold: 15/25/35% damage bonus (non-linear — use table value) +-- Improved Cone of Cold: 15/25/35% damage bonus (non-linear - use table value) TalentMap["3:6"] = { name = "Improved Cone of Cold", maxRank = 3, diff --git a/Data/TalentMap_Paladin.lua b/Data/TalentMap_Paladin.lua index 453d450..175c175 100644 --- a/Data/TalentMap_Paladin.lua +++ b/Data/TalentMap_Paladin.lua @@ -1,11 +1,10 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Paladin --- Paladin talent modifier definitions for PhDamage --- --- Supported versions: TBC Anniversary +-- Paladin Talent Modifiers - TBC Anniversary (2.5.5) +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... -ns.TalentMap = ns.TalentMap or {} local SCHOOL_HOLY = ns.SCHOOL_HOLY local MOD = ns.MOD @@ -14,10 +13,20 @@ local TalentMap = {} ------------------------------------------------------------------------------- -- Holy (Tab 1) +-- 1:1 Spiritual Focus (5) 1:2 Divine Favor (1) +-- 1:3 Aura Mastery (1) 1:4 Improved Lay on Hands (2) +-- 1:5 Healing Light (3) 1:6 Improved Blessing of Wisdom (2) +-- 1:7 Divine Intellect (5) 1:8 Divine Strength (5) +-- 1:9 Illumination (5) 1:10 Improved Seal of Righteousness (5) +-- 1:11 Sanctified Light (3) 1:12 Holy Shock (1) +-- 1:13 Holy Power (5) 1:14 Unyielding Faith (2) +-- 1:15 Pure of Heart (3) 1:16 Purifying Power (2) +-- 1:17 Blessed Life (3) 1:18 Light's Grace (3) +-- 1:19 Holy Guidance (5) 1:20 Divine Illumination (1) ------------------------------------------------------------------------------- --- Healing Light: +4% healing to Holy Light and Flash of Light per rank -TalentMap["1:7"] = { +-- Healing Light: +4% healing to Holy Light and Flash of Light per rank (Holy 1:5) +TalentMap["1:5"] = { name = "Healing Light", maxRank = 3, effects = { @@ -26,8 +35,8 @@ TalentMap["1:7"] = { }, } --- Sanctified Light: +2% crit on Holy Light and Flash of Light per rank -TalentMap["1:14"] = { +-- Sanctified Light: +2% crit on Holy Light and Flash of Light per rank (Holy 1:11) +TalentMap["1:11"] = { name = "Sanctified Light", maxRank = 3, effects = { @@ -36,7 +45,16 @@ TalentMap["1:14"] = { }, } --- Purifying Power: +10% crit on Exorcism and Holy Wrath per rank +-- Holy Power: +1% Holy spell crit per rank (Holy 1:13) +TalentMap["1:13"] = { + name = "Holy Power", + maxRank = 5, + effects = { + { type = MOD.CRIT_BONUS, value = 0.01, perRank = true, filter = { school = SCHOOL_HOLY } }, + }, +} + +-- Purifying Power: +10% crit on Exorcism and Holy Wrath per rank (Holy 1:16) TalentMap["1:16"] = { name = "Purifying Power", maxRank = 2, @@ -46,17 +64,8 @@ TalentMap["1:16"] = { }, } --- Holy Power: +1% Holy spell crit per rank +-- Holy Guidance: +5% of Intellect as spell power per rank (Holy 1:19) TalentMap["1:19"] = { - name = "Holy Power", - maxRank = 5, - effects = { - { type = MOD.CRIT_BONUS, value = 0.01, perRank = true, filter = { school = SCHOOL_HOLY } }, - }, -} - --- Holy Guidance: +5% of Intellect as spell power per rank -TalentMap["1:25"] = { name = "Holy Guidance", maxRank = 5, effects = { @@ -66,10 +75,21 @@ TalentMap["1:25"] = { ------------------------------------------------------------------------------- -- Protection (Tab 2) +-- 2:1 Redoubt (5) 2:2 Improved Devotion Aura (5) +-- 2:3 Toughness (5) 2:4 Shield Specialization (3) +-- 2:5 Guardian's Favor (2) 2:6 Reckoning (5) +-- 2:7 One-Handed Weapon Spec (5) 2:8 Holy Shield (1) +-- 2:9 Blessing of Sanctuary (1) 2:10 Blessing of Kings (1) +-- 2:11 Improved Righteous Fury (3) 2:12 Improved Hammer of Justice (3) +-- 2:13 Improved Concentration Aura (3) 2:14 Anticipation (5) +-- 2:15 Precision (3) 2:16 Stoicism (2) +-- 2:17 Spell Warding (2) 2:18 Sacred Duty (2) +-- 2:19 Ardent Defender (5) 2:20 Combat Expertise (5) +-- 2:21 Avenger's Shield (1) 2:22 Improved Holy Shield (2) ------------------------------------------------------------------------------- --- Precision: +1% spell hit per rank -TalentMap["2:4"] = { +-- Precision: +1% spell hit per rank (Protection 2:15) +TalentMap["2:15"] = { name = "Precision", maxRank = 3, effects = { @@ -77,8 +97,8 @@ TalentMap["2:4"] = { }, } --- Combat Expertise: +1% crit per rank -TalentMap["2:23"] = { +-- Combat Expertise: +1% crit per rank (Protection 2:20) +TalentMap["2:20"] = { name = "Combat Expertise", maxRank = 5, effects = { @@ -88,11 +108,23 @@ TalentMap["2:23"] = { ------------------------------------------------------------------------------- -- Retribution (Tab 3) +-- 3:1 Improved Blessing of Might (5) 3:2 Vengeance (5) +-- 3:3 Deflection (5) 3:4 Improved Retribution Aura (2) +-- 3:5 Benediction (5) 3:6 Sanctity Aura (1) +-- 3:7 Two-Handed Weapon Spec (3) 3:8 Conviction (5) +-- 3:9 Repentance (1) 3:10 Improved Seal of the Crusader (3) +-- 3:11 Seal of Command (1) 3:12 Improved Judgement (2) +-- 3:13 Eye for an Eye (2) 3:14 Vindication (3) +-- 3:15 Pursuit of Justice (3) 3:16 Crusade (3) +-- 3:17 Improved Sanctity Aura (2) 3:18 Divine Purpose (3) +-- 3:19 Sanctified Judgement (3) 3:20 Fanaticism (5) +-- 3:21 Sanctified Seals (3) 3:22 Crusader Strike (1) ------------------------------------------------------------------------------- -- NOTE: Crusade is actually +1%/rank damage to Humanoids, Demons, Undead, and Elementals. -- Simplified to +1%/rank all damage since creature type filtering is not supported. -TalentMap["3:10"] = { +-- Crusade: +1% all damage per rank (Retribution 3:16) +TalentMap["3:16"] = { name = "Crusade", maxRank = 3, effects = { @@ -100,8 +132,8 @@ TalentMap["3:10"] = { }, } --- Sanctified Seals: +1% crit per rank -TalentMap["3:20"] = { +-- Sanctified Seals: +1% crit per rank (Retribution 3:21) +TalentMap["3:21"] = { name = "Sanctified Seals", maxRank = 3, effects = { @@ -109,7 +141,9 @@ TalentMap["3:20"] = { }, } +------------------------------------------------------------------------------- -- Merge into addon namespace with class prefix +------------------------------------------------------------------------------- for key, data in pairs(TalentMap) do ns.TalentMap["PALADIN:" .. key] = data end diff --git a/Data/TalentMap_Priest.lua b/Data/TalentMap_Priest.lua index 3c1d48d..1ef7576 100644 --- a/Data/TalentMap_Priest.lua +++ b/Data/TalentMap_Priest.lua @@ -1,11 +1,10 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Priest --- Priest talent modifier definitions for PhDamage --- --- Supported versions: TBC Anniversary +-- Priest Talent Modifiers - TBC Anniversary (2.5.5) +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... -ns.TalentMap = ns.TalentMap or {} local SCHOOL_HOLY = ns.SCHOOL_HOLY local SCHOOL_SHADOW = ns.SCHOOL_SHADOW @@ -14,10 +13,21 @@ local MOD = ns.MOD local TalentMap = {} ------------------------------------------------------------------------------- --- Discipline +-- Discipline (Tab 1) +-- 1:1 Martyrdom (2) 1:2 Power Infusion (1) +-- 1:3 Mental Agility (5) 1:4 Unbreakable Will (5) +-- 1:5 Improved PW:Shield (3) 1:6 Improved PW:Fortitude (2) +-- 1:7 Wand Specialization (5) 1:8 Improved Inner Fire (3) +-- 1:9 Meditation (3) 1:10 Inner Focus (1) +-- 1:11 Improved Mana Burn (2) 1:12 Divine Spirit (1) +-- 1:13 Silent Resolve (5) 1:14 Mental Strength (5) +-- 1:15 Force of Will (5) 1:16 Absolution (3) +-- 1:17 Improved Divine Spirit (2) 1:18 Focused Power (2) +-- 1:19 Enlightenment (5) 1:20 Reflective Shield (5) +-- 1:21 Pain Suppression (1) 1:22 Focused Will (3) ------------------------------------------------------------------------------- --- Force of Will: +1% spell damage AND +1% spell crit per rank +-- Force of Will: +1% spell damage AND +1% spell crit per rank (Discipline 1:15) TalentMap["1:15"] = { name = "Force of Will", maxRank = 5, @@ -27,8 +37,8 @@ TalentMap["1:15"] = { }, } --- Focused Power: +2% all damage per rank -TalentMap["1:20"] = { +-- Focused Power: +2% all damage per rank (Discipline 1:18) +TalentMap["1:18"] = { name = "Focused Power", maxRank = 2, effects = { @@ -37,11 +47,22 @@ TalentMap["1:20"] = { } ------------------------------------------------------------------------------- --- Holy +-- Holy (Tab 2) +-- 2:1 Inspiration (3) 2:2 Holy Specialization (5) +-- 2:3 Spiritual Guidance (5) 2:4 Searing Light (2) +-- 2:5 Spiritual Healing (5) 2:6 Improved Renew (3) +-- 2:7 Improved Healing (3) 2:8 Healing Focus (2) +-- 2:9 Spell Warding (5) 2:10 Healing Prayers (2) +-- 2:11 Holy Nova (1) 2:12 Divine Fury (5) +-- 2:13 Spirit of Redemption (1) 2:14 Holy Reach (2) +-- 2:15 Blessed Recovery (3) 2:16 Lightwell (1) +-- 2:17 Blessed Resilience (3) 2:18 Surge of Light (2) +-- 2:19 Empowered Healing (5) 2:20 Holy Concentration (3) +-- 2:21 Circle of Healing (1) ------------------------------------------------------------------------------- --- Holy Specialization: +1% holy spell crit per rank -TalentMap["2:3"] = { +-- Holy Specialization: +1% holy spell crit per rank (Holy 2:2) +TalentMap["2:2"] = { name = "Holy Specialization", maxRank = 5, effects = { @@ -49,8 +70,8 @@ TalentMap["2:3"] = { }, } --- Searing Light: +5% Smite and Holy Fire damage per rank -TalentMap["2:13"] = { +-- Searing Light: +5% Smite and Holy Fire damage per rank (Holy 2:4) +TalentMap["2:4"] = { name = "Searing Light", maxRank = 2, effects = { @@ -60,11 +81,32 @@ TalentMap["2:13"] = { } ------------------------------------------------------------------------------- --- Shadow +-- Shadow (Tab 3) +-- 3:1 Shadow Weaving (5) 3:2 Darkness (5) +-- 3:3 Shadow Focus (5) 3:4 Blackout (5) +-- 3:5 Spirit Tap (5) 3:6 Shadow Affinity (3) +-- 3:7 Improved Mind Blast (5) 3:8 Improved SW:Pain (2) +-- 3:9 Improved Fade (2) 3:10 Vampiric Embrace (1) +-- 3:11 Mind Flay (1) 3:12 Shadowform (1) +-- 3:13 Silence (1) 3:14 Improved Psychic Scream (2) +-- 3:15 Shadow Reach (2) 3:16 Improved Vampiric Embrace (2) +-- 3:17 Focused Mind (3) 3:18 Shadow Power (5) +-- 3:19 Vampiric Touch (1) 3:20 Shadow Resilience (2) +-- 3:21 Misery (5) ------------------------------------------------------------------------------- --- Shadow Focus: +2% shadow hit per rank +-- Darkness: +2% shadow damage per rank (Shadow 3:2) TalentMap["3:2"] = { + name = "Darkness", + maxRank = 5, + effects = { + { type = MOD.DAMAGE_MULTIPLIER, value = 0.02, perRank = true, stacking = "additive", + filter = { school = SCHOOL_SHADOW } }, + }, +} + +-- Shadow Focus: +2% shadow hit per rank (Shadow 3:3) +TalentMap["3:3"] = { name = "Shadow Focus", maxRank = 5, effects = { @@ -72,8 +114,8 @@ TalentMap["3:2"] = { }, } --- Improved Shadow Word: Pain: +3% SW:P damage per rank -TalentMap["3:4"] = { +-- Improved Shadow Word: Pain: +3% SW:P damage per rank (Shadow 3:8) +TalentMap["3:8"] = { name = "Improved Shadow Word: Pain", maxRank = 2, effects = { @@ -82,18 +124,8 @@ TalentMap["3:4"] = { }, } --- Darkness: +2% shadow damage per rank -TalentMap["3:15"] = { - name = "Darkness", - maxRank = 5, - effects = { - { type = MOD.DAMAGE_MULTIPLIER, value = 0.02, perRank = true, stacking = "additive", - filter = { school = SCHOOL_SHADOW } }, - }, -} - --- Shadow Power: +20% shadow crit damage bonus per rank (0.10 of base 50% bonus) -TalentMap["3:22"] = { +-- Shadow Power: +20% shadow crit damage bonus per rank (0.10 of base 50% bonus) (Shadow 3:18) +TalentMap["3:18"] = { name = "Shadow Power", maxRank = 5, effects = { @@ -101,7 +133,9 @@ TalentMap["3:22"] = { }, } +------------------------------------------------------------------------------- -- Merge into addon namespace with class prefix +------------------------------------------------------------------------------- for key, data in pairs(TalentMap) do ns.TalentMap["PRIEST:" .. key] = data end diff --git a/Data/TalentMap_Rogue.lua b/Data/TalentMap_Rogue.lua index 9d45560..62c4fe2 100644 --- a/Data/TalentMap_Rogue.lua +++ b/Data/TalentMap_Rogue.lua @@ -1,11 +1,10 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Rogue --- Rogue talent modifier definitions for PhDamage --- --- Supported versions: TBC Anniversary +-- Rogue Talent Modifiers - TBC Anniversary (2.5.5) +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... -ns.TalentMap = ns.TalentMap or {} local MOD = ns.MOD @@ -13,33 +12,37 @@ local TalentMap = {} ------------------------------------------------------------------------------- -- Assassination (Tab 1) --- --- Grid layout (row x col → index): --- 1: Improved Eviscerate (1,1) Remorseless Attacks (1,2) Malice (1,3) --- 4: Ruthlessness (2,1) Murder (2,2) [empty] Puncturing Wounds (2,4) --- 7: Relentless Strikes (3,1) Improved Expose Armor (3,2) Lethality (3,3) --- 10: Vile Poisons (4,2) Improved Poisons (4,3) --- 12: Fleet Footed (5,1) Cold Blood (5,2) Imp Kidney Shot (5,3) Quick Recovery (5,4) --- 16: Seal Fate (6,2) Master Poisoner (6,3) --- 18: Vigor (7,2) Deadened Nerves (7,3) --- 20: Find Weakness (8,3) --- 21: Mutilate (9,2) +-- 1:1 Improved Poisons (5) 1:2 Lethality (5) +-- 1:3 Malice (5) 1:4 Remorseless Attacks (2) +-- 1:5 Ruthlessness (3) 1:6 Murder (2) +-- 1:7 Improved Eviscerate (3) 1:8 Puncturing Wounds (3) +-- 1:9 Improved Expose Armor (2) 1:10 Improved Kidney Shot (3) +-- 1:11 Cold Blood (1) 1:12 Relentless Strikes (1) +-- 1:13 Seal Fate (5) 1:14 Vigor (1) +-- 1:15 Vile Poisons (5) 1:16 Master Poisoner (2) +-- 1:17 Find Weakness (5) 1:18 Mutilate (1) +-- 1:19 Fleet Footed (2) 1:20 Deadened Nerves (5) +-- 1:21 Quick Recovery (2) ------------------------------------------------------------------------------- --- Improved Eviscerate (1:1): +5% Eviscerate damage per rank -TalentMap["1:1"] = { - name = "Improved Eviscerate", - maxRank = 3, +-- Improved Poisons (1:1): +2% chance to apply poisons per rank (2/4/6/8/10%) +-- Application chance modifier - not a damage modifier. +-- TalentMap["1:1"] - Improved Poisons: deferred (proc chance) + +-- Lethality (1:2): +6% crit damage bonus per rank (6/12/18/24/30%) +-- Affects: SS, Gouge, BS, Ghostly Strike, Mutilate, Shiv, Hemorrhage +TalentMap["1:2"] = { + name = "Lethality", + maxRank = 5, effects = { - { type = MOD.DAMAGE_MULTIPLIER, value = 0.05, perRank = true, stacking = "additive", - filter = { spellNames = { "Eviscerate" } } }, + { type = MOD.CRIT_MULT_BONUS, value = 0.06, perRank = true, + filter = { spellNames = { + "Sinister Strike", "Gouge", "Backstab", + "Ghostly Strike", "Mutilate", "Shiv", "Hemorrhage", + } } }, }, } --- Remorseless Attacks (1:2): +20% crit chance on next SS/Hemo/BS/Ambush/GS --- after killing a target that yields XP/honor. Proc-based — deferred to AuraMap. --- TalentMap["1:2"] — Remorseless Attacks: deferred to AuraMap - -- Malice (1:3): +1% crit chance per rank TalentMap["1:3"] = { name = "Malice", @@ -49,12 +52,16 @@ TalentMap["1:3"] = { }, } --- Ruthlessness (1:4): 20% chance per rank to add a combo point after --- finishing move. Proc-based — not expressible as a damage modifier. --- TalentMap["1:4"] — Ruthlessness: deferred (proc-based) +-- Remorseless Attacks (1:4): +20% crit chance on next SS/Hemo/BS/Ambush/GS +-- after killing a target that yields XP/honor. Proc-based - deferred to AuraMap. +-- TalentMap["1:4"] - Remorseless Attacks: deferred to AuraMap --- Murder (1:5): +1% all damage vs Humanoid/Giant/Beast/Dragonkin per rank -TalentMap["1:5"] = { +-- Ruthlessness (1:5): 20% chance per rank to add a combo point after +-- finishing move. Proc-based - not expressible as a damage modifier. +-- TalentMap["1:5"] - Ruthlessness: deferred (proc-based) + +-- Murder (1:6): +1% all damage vs Humanoid/Giant/Beast/Dragonkin per rank +TalentMap["1:6"] = { name = "Murder", maxRank = 2, effects = { @@ -63,8 +70,18 @@ TalentMap["1:5"] = { }, } --- Puncturing Wounds (1:6): +10% BS crit, +5% Mutilate crit per rank -TalentMap["1:6"] = { +-- Improved Eviscerate (1:7): +5% Eviscerate damage per rank +TalentMap["1:7"] = { + name = "Improved Eviscerate", + maxRank = 3, + effects = { + { type = MOD.DAMAGE_MULTIPLIER, value = 0.05, perRank = true, stacking = "additive", + filter = { spellNames = { "Eviscerate" } } }, + }, +} + +-- Puncturing Wounds (1:8): +10% BS crit, +5% Mutilate crit per rank +TalentMap["1:8"] = { name = "Puncturing Wounds", maxRank = 3, effects = { @@ -75,31 +92,35 @@ TalentMap["1:6"] = { }, } --- Relentless Strikes (1:7): Finishing moves have 20% chance per CP to --- restore 25 Energy. Resource gain — not a damage modifier. --- TalentMap["1:7"] — Relentless Strikes: deferred (resource-based) +-- Improved Expose Armor (1:9): Increases effectiveness of Expose Armor. +-- Armor reduction modifier - affects target debuff, not direct damage. +-- TalentMap["1:9"] - Improved Expose Armor: deferred (debuff modifier) --- Improved Expose Armor (1:8): Increases effectiveness of Expose Armor. --- Armor reduction modifier — affects target debuff, not direct damage. --- TalentMap["1:8"] — Improved Expose Armor: deferred (debuff modifier) +-- Improved Kidney Shot (1:10): +3% damage taken from all sources per rank +-- while target is affected by Kidney Shot (3/6/9%) +-- NOTE: This is a debuff on the target during Kidney Shot. Should be +-- handled via AuraMap keyed to the Improved Kidney Shot debuff. +-- TalentMap["1:10"] - Improved Kidney Shot: deferred to AuraMap --- Lethality (1:9): +6% crit damage bonus per rank (6/12/18/24/30%) --- Affects: SS, Gouge, BS, Ghostly Strike, Mutilate, Shiv, Hemorrhage -TalentMap["1:9"] = { - name = "Lethality", - maxRank = 5, - effects = { - { type = MOD.CRIT_MULT_BONUS, value = 0.06, perRank = true, - filter = { spellNames = { - "Sinister Strike", "Gouge", "Backstab", - "Ghostly Strike", "Mutilate", "Shiv", "Hemorrhage", - } } }, - }, -} +-- Cold Blood (1:11): +100% crit chance on next offensive ability (active) +-- NOTE: Activated ability - handled via AuraMap when the Cold Blood buff +-- is active. +-- TalentMap["1:11"] - Cold Blood: deferred to AuraMap --- Vile Poisons (1:10): +4% poison and Envenom damage per rank (4/8/12/16/20%) +-- Relentless Strikes (1:12): Finishing moves have 20% chance per CP to +-- restore 25 Energy. Resource gain - not a damage modifier. +-- TalentMap["1:12"] - Relentless Strikes: deferred (resource-based) + +-- Seal Fate (1:13): Critical strikes from abilities that add combo points +-- have a 20% chance per rank to add an additional combo point. Proc-based. +-- TalentMap["1:13"] - Seal Fate: deferred (proc-based) + +-- Vigor (1:14): +10 maximum Energy (1 rank). Resource pool - no damage effect. +-- TalentMap["1:14"] - Vigor: deferred (resource pool) + +-- Vile Poisons (1:15): +4% poison and Envenom damage per rank (4/8/12/16/20%) -- Also +8% poison dispel resist per rank (not modeled) -TalentMap["1:10"] = { +TalentMap["1:15"] = { name = "Vile Poisons", maxRank = 5, effects = { @@ -110,87 +131,48 @@ TalentMap["1:10"] = { }, } --- Improved Poisons (1:11): +2% chance to apply poisons per rank (2/4/6/8/10%) --- Application chance modifier — not a damage modifier. --- TalentMap["1:11"] — Improved Poisons: deferred (proc chance) - --- Fleet Footed (1:12): +8% run speed, +3% chance to resist movement --- impairing effects per rank. Utility — no damage effect. --- TalentMap["1:12"] — Fleet Footed: deferred (utility) - --- Cold Blood (1:13): +100% crit chance on next offensive ability (active) --- NOTE: Activated ability — handled via AuraMap when the Cold Blood buff --- is active. --- TalentMap["1:13"] — Cold Blood: deferred to AuraMap - --- Improved Kidney Shot (1:14): +3% damage taken from all sources per rank --- while target is affected by Kidney Shot (3/6/9%) --- NOTE: This is a debuff on the target during Kidney Shot. Should be --- handled via AuraMap keyed to the Improved Kidney Shot debuff. --- TalentMap["1:14"] — Improved Kidney Shot: deferred to AuraMap - --- Quick Recovery (1:15): +10% healing received, +40% finishing move energy --- refund on miss per rank. Utility — no direct damage effect. --- TalentMap["1:15"] — Quick Recovery: deferred (utility) - --- Seal Fate (1:16): Critical strikes from abilities that add combo points --- have a 20% chance per rank to add an additional combo point. Proc-based. --- TalentMap["1:16"] — Seal Fate: deferred (proc-based) - --- Master Poisoner (1:17): +2% chance to hit with poison, reduces poison +-- Master Poisoner (1:16): +2% chance to hit with poison, reduces poison -- target's chance to resist spells by 2% per rank. Hit/debuff modifier. --- TalentMap["1:17"] — Master Poisoner: deferred (debuff modifier) - --- Vigor (1:18): +10 maximum Energy (1 rank). Resource pool — no damage effect. --- TalentMap["1:18"] — Vigor: deferred (resource pool) +-- TalentMap["1:16"] - Master Poisoner: deferred (debuff modifier) --- Deadened Nerves (1:19): -1% all damage taken per rank. Defensive. --- TalentMap["1:19"] — Deadened Nerves: deferred (defensive) - --- Find Weakness (1:20): Finishing moves increase damage of all offensive +-- Find Weakness (1:17): Finishing moves increase damage of all offensive -- abilities by 2% per rank for 10 sec (2/4/6/8/10%) -- NOTE: This is a temporary buff triggered by finishing moves. Should be -- handled via AuraMap when the Find Weakness buff is active. --- TalentMap["1:20"] — Find Weakness: deferred to AuraMap - --- Mutilate (1:21): Active ability — damage handled via SpellData. --- TalentMap["1:21"] — Mutilate: handled by SpellData - -------------------------------------------------------------------------------- --- Combat (Tab 2) --- --- Grid layout (row x col → index): --- 1: Improved Gouge (1,1) Improved SS (1,2) Lightning Reflexes (1,3) --- 4: Improved SnD (2,1) Deflection (2,2) Precision (2,3) --- 7: Endurance (3,1) Riposte (3,2) [empty] Improved Sprint (3,4) --- 10: Improved Kick (4,1) Dagger Spec (4,2) DW Spec (4,3) --- 13: Mace Spec (5,1) Blade Flurry (5,2) Sword Spec (5,3) Fist Spec (5,4) --- 17: Blade Twisting (6,1) Weapon Expertise (6,2) Aggression (6,3) --- 20: Vitality (7,1) Adrenaline Rush (7,2) Nerves of Steel (7,3) --- 23: Combat Potency (8,3) --- 24: Surprise Attacks (9,2) -------------------------------------------------------------------------------- +-- TalentMap["1:17"] - Find Weakness: deferred to AuraMap --- Improved Gouge (2:1): Increases Gouge duration. CC utility — no damage. --- TalentMap["2:1"] — Improved Gouge: deferred (CC duration) +-- Mutilate (1:18): Active ability - damage handled via SpellData. +-- TalentMap["1:18"] - Mutilate: handled by SpellData --- Improved Sinister Strike (2:2): -3/-5 Energy cost reduction. --- Energy cost reduction is not a damage modifier — no MOD type for it. --- TalentMap["2:2"] — Improved Sinister Strike: deferred (resource cost) +-- Fleet Footed (1:19): +8% run speed, +3% chance to resist movement +-- impairing effects per rank. Utility - no damage effect. +-- TalentMap["1:19"] - Fleet Footed: deferred (utility) --- Lightning Reflexes (2:3): +1% Dodge per rank. Defensive. --- TalentMap["2:3"] — Lightning Reflexes: deferred (defensive) +-- Deadened Nerves (1:20): -1% all damage taken per rank. Defensive. +-- TalentMap["1:20"] - Deadened Nerves: deferred (defensive) --- Improved Slice and Dice (2:4): +15% SnD duration per rank (15/30/45%) --- NOTE: Duration increase — the attack speed bonus is already captured --- as a buff via AuraMap when SnD is active. --- TalentMap["2:4"] — Improved Slice and Dice: deferred (buff duration) +-- Quick Recovery (1:21): +10% healing received, +40% finishing move energy +-- refund on miss per rank. Utility - no direct damage effect. +-- TalentMap["1:21"] - Quick Recovery: deferred (utility) --- Deflection (2:5): +1% Parry per rank. Defensive. --- TalentMap["2:5"] — Deflection: deferred (defensive) +------------------------------------------------------------------------------- +-- Combat (Tab 2) +-- 2:1 Precision (5) 2:2 Dagger Specialization (5) +-- 2:3 Fist Weapon Specialization (5) 2:4 Mace Specialization (5) +-- 2:5 Lightning Reflexes (5) 2:6 Deflection (5) +-- 2:7 Improved Sinister Strike (2) 2:8 Improved Gouge (3) +-- 2:9 Endurance (2) 2:10 Adrenaline Rush (1) +-- 2:11 Improved Kick (2) 2:12 Dual Wield Specialization (5) +-- 2:13 Improved Sprint (2) 2:14 Blade Flurry (1) +-- 2:15 Sword Specialization (5) 2:16 Riposte (1) +-- 2:17 Aggression (3) 2:18 Weapon Expertise (2) +-- 2:19 Vitality (2) 2:20 Blade Twisting (2) +-- 2:21 Nerves of Steel (2) 2:22 Surprise Attacks (1) +-- 2:23 Combat Potency (5) 2:24 Improved Slice and Dice (3) +------------------------------------------------------------------------------- --- Precision (2:6): +1% melee hit per rank (5 ranks = 5%) -TalentMap["2:6"] = { +-- Precision (2:1): +1% melee hit per rank (5 ranks = 5%) +TalentMap["2:1"] = { name = "Precision", maxRank = 5, effects = { @@ -198,22 +180,9 @@ TalentMap["2:6"] = { }, } --- Endurance (2:7): Reduces cooldown of Sprint/Evasion. Utility. --- TalentMap["2:7"] — Endurance: deferred (cooldown reduction) - --- Riposte (2:8): Active ability — damage handled via SpellData. --- TalentMap["2:8"] — Riposte: handled by SpellData - --- Improved Sprint (2:9): Gives Sprint a chance to remove movement impairing --- effects. Utility. --- TalentMap["2:9"] — Improved Sprint: deferred (utility) - --- Improved Kick (2:10): Gives Kick a chance to silence. CC utility. --- TalentMap["2:10"] — Improved Kick: deferred (CC utility) - --- Dagger Specialization (2:11): +1% crit with Daggers per rank +-- Dagger Specialization (2:2): +1% crit with Daggers per rank -- TODO: Needs weaponType filter support. Currently applies to all abilities. -TalentMap["2:11"] = { +TalentMap["2:2"] = { name = "Dagger Specialization", maxRank = 5, effects = { @@ -221,28 +190,9 @@ TalentMap["2:11"] = { }, } --- Dual Wield Specialization (2:12): +10% OH damage per rank (10/20/30/40/50%) --- TODO: Needs off-hand vs main-hand differentiation in the engine. --- Currently the engine does not model OH vs MH separately. --- TalentMap["2:12"] — Dual Wield Specialization: deferred (OH-specific) - --- Mace Specialization (2:13): Increases expertise with Maces per rank. --- Also has a chance to reduce movement speed. The expertise portion is a --- stat modifier. +-- Fist Weapon Specialization (2:3): +1% crit with Fist Weapons per rank. -- TODO: Needs weaponType filter support. --- TalentMap["2:13"] — Mace Specialization: deferred (weaponType filter) - --- Blade Flurry (2:14): +20% attack speed, hits additional nearby target. --- Active ability — handled via AuraMap when Blade Flurry buff is active. --- TalentMap["2:14"] — Blade Flurry: deferred to AuraMap - --- Sword Specialization (2:15): 1% chance per rank for extra attack on hit. --- Proc-based extra attack — cannot be expressed as a simple damage modifier. --- TalentMap["2:15"] — Sword Specialization: deferred (proc-based) - --- Fist Weapon Specialization (2:16): +1% crit with Fist Weapons per rank. --- TODO: Needs weaponType filter support. -TalentMap["2:16"] = { +TalentMap["2:3"] = { name = "Fist Weapon Specialization", maxRank = 5, effects = { @@ -250,17 +200,57 @@ TalentMap["2:16"] = { }, } --- Blade Twisting (2:17): Sinister Strike/Backstab have 10% chance per rank --- to Daze target. CC utility — no direct damage. --- TalentMap["2:17"] — Blade Twisting: deferred (CC utility) +-- Mace Specialization (2:4): Increases expertise with Maces per rank. +-- Also has a chance to reduce movement speed. The expertise portion is a +-- stat modifier. +-- TODO: Needs weaponType filter support. +-- TalentMap["2:4"] - Mace Specialization: deferred (weaponType filter) --- Weapon Expertise (2:18): +5 expertise per rank (2 ranks = 10) --- Reduces target's dodge and parry chance. Expertise is handled via --- playerState.expertise in CritCalc, not as a TalentMap modifier. --- TalentMap["2:18"] — Weapon Expertise: handled via stats +-- Lightning Reflexes (2:5): +1% Dodge per rank. Defensive. +-- TalentMap["2:5"] - Lightning Reflexes: deferred (defensive) + +-- Deflection (2:6): +1% Parry per rank. Defensive. +-- TalentMap["2:6"] - Deflection: deferred (defensive) + +-- Improved Sinister Strike (2:7): -3/-5 Energy cost reduction. +-- Energy cost reduction is not a damage modifier - no MOD type for it. +-- TalentMap["2:7"] - Improved Sinister Strike: deferred (resource cost) + +-- Improved Gouge (2:8): Increases Gouge duration. CC utility - no damage. +-- TalentMap["2:8"] - Improved Gouge: deferred (CC duration) + +-- Endurance (2:9): Reduces cooldown of Sprint/Evasion. Utility. +-- TalentMap["2:9"] - Endurance: deferred (cooldown reduction) + +-- Adrenaline Rush (2:10): +100% Energy regen for 15s. Active ability. +-- Resource gain - handled via AuraMap when the buff is active. +-- TalentMap["2:10"] - Adrenaline Rush: deferred to AuraMap + +-- Improved Kick (2:11): Gives Kick a chance to silence. CC utility. +-- TalentMap["2:11"] - Improved Kick: deferred (CC utility) + +-- Dual Wield Specialization (2:12): +10% OH damage per rank (10/20/30/40/50%) +-- TODO: Needs off-hand vs main-hand differentiation in the engine. +-- Currently the engine does not model OH vs MH separately. +-- TalentMap["2:12"] - Dual Wield Specialization: deferred (OH-specific) + +-- Improved Sprint (2:13): Gives Sprint a chance to remove movement impairing +-- effects. Utility. +-- TalentMap["2:13"] - Improved Sprint: deferred (utility) + +-- Blade Flurry (2:14): +20% attack speed, hits additional nearby target. +-- Active ability - handled via AuraMap when Blade Flurry buff is active. +-- TalentMap["2:14"] - Blade Flurry: deferred to AuraMap --- Aggression (2:19): +2% SS/BS/Evis damage per rank (2/4/6%) -TalentMap["2:19"] = { +-- Sword Specialization (2:15): 1% chance per rank for extra attack on hit. +-- Proc-based extra attack - cannot be expressed as a simple damage modifier. +-- TalentMap["2:15"] - Sword Specialization: deferred (proc-based) + +-- Riposte (2:16): Active ability - damage handled via SpellData. +-- TalentMap["2:16"] - Riposte: handled by SpellData + +-- Aggression (2:17): +2% SS/BS/Evis damage per rank (2/4/6%) +TalentMap["2:17"] = { name = "Aggression", maxRank = 3, effects = { @@ -269,27 +259,28 @@ TalentMap["2:19"] = { }, } --- Vitality (2:20): +2% Stamina, +1% Agility per rank --- NOTE: Stat multiplier — the Agility increase is already reflected in +-- Weapon Expertise (2:18): +5 expertise per rank (2 ranks = 10) +-- Reduces target's dodge and parry chance. Expertise is handled via +-- playerState.expertise in CritCalc, not as a TalentMap modifier. +-- TalentMap["2:18"] - Weapon Expertise: handled via stats + +-- Vitality (2:19): +2% Stamina, +1% Agility per rank +-- NOTE: Stat multiplier - the Agility increase is already reflected in -- playerState.stats when collected. Could be modeled but would require -- a stat multiplier MOD type. The Agility increase does indirectly -- increase AP and crit, but that's captured by StateCollector. --- TalentMap["2:20"] — Vitality: handled via stats - --- Adrenaline Rush (2:21): +100% Energy regen for 15s. Active ability. --- Resource gain — handled via AuraMap when the buff is active. --- TalentMap["2:21"] — Adrenaline Rush: deferred to AuraMap +-- TalentMap["2:19"] - Vitality: handled via stats --- Nerves of Steel (2:22): Reduces duration of Stun/Fear. Defensive/PvP. --- TalentMap["2:22"] — Nerves of Steel: deferred (defensive) +-- Blade Twisting (2:20): Sinister Strike/Backstab have 10% chance per rank +-- to Daze target. CC utility - no direct damage. +-- TalentMap["2:20"] - Blade Twisting: deferred (CC utility) --- Combat Potency (2:23): 20% chance on OH hit to generate 3 Energy per rank --- (3/6/9/12/15). Proc-based resource gain — not a damage modifier. --- TalentMap["2:23"] — Combat Potency: deferred (proc-based resource) +-- Nerves of Steel (2:21): Reduces duration of Stun/Fear. Defensive/PvP. +-- TalentMap["2:21"] - Nerves of Steel: deferred (defensive) --- Surprise Attacks (2:24): Finishing moves can't be dodged, +10% damage to +-- Surprise Attacks (2:22): Finishing moves can't be dodged, +10% damage to -- SS/BS/Shiv/Gouge (1 rank) -TalentMap["2:24"] = { +TalentMap["2:22"] = { name = "Surprise Attacks", maxRank = 1, effects = { @@ -298,28 +289,52 @@ TalentMap["2:24"] = { }, } +-- Combat Potency (2:23): 20% chance on OH hit to generate 3 Energy per rank +-- (3/6/9/12/15). Proc-based resource gain - not a damage modifier. +-- TalentMap["2:23"] - Combat Potency: deferred (proc-based resource) + +-- Improved Slice and Dice (2:24): +15% SnD duration per rank (15/30/45%) +-- NOTE: Duration increase - the attack speed bonus is already captured +-- as a buff via AuraMap when SnD is active. +-- TalentMap["2:24"] - Improved Slice and Dice: deferred (buff duration) + ------------------------------------------------------------------------------- -- Subtlety (Tab 3) --- --- Grid layout (row x col → index): --- 1: [empty] Master of Deception (1,2) Opportunity (1,3) --- 3: Sleight of Hand (2,1) Dirty Tricks (2,2) Camouflage (2,3) --- 6: Initiative (3,1) Ghostly Strike (3,2) Improved Ambush (3,3) --- 9: Setup (4,1) Elusiveness (4,2) Serrated Blades (4,3) --- 12: Heightened Senses (5,1) Preparation (5,2) Dirty Deeds (5,3) Hemorrhage (5,4) --- 16: Master of Subtlety (6,1) [empty] Deadliness (6,3) --- 18: Enveloping Shadows (7,1) Premeditation (7,2) Cheat Death (7,3) --- 21: Sinister Calling (8,2) --- 22: Shadowstep (9,2) +-- 3:1 Master of Deception (5) 3:2 Camouflage (5) +-- 3:3 Initiative (3) 3:4 Setup (3) +-- 3:5 Elusiveness (2) 3:6 Opportunity (5) +-- 3:7 Dirty Tricks (2) 3:8 Improved Ambush (3) +-- 3:9 Dirty Deeds (2) 3:10 Preparation (1) +-- 3:11 Ghostly Strike (1) 3:12 Premeditation (1) +-- 3:13 Hemorrhage (1) 3:14 Serrated Blades (3) +-- 3:15 Sleight of Hand (2) 3:16 Heightened Senses (2) +-- 3:17 Deadliness (5) 3:18 Enveloping Shadows (3) +-- 3:19 Sinister Calling (5) 3:20 Master of Subtlety (3) +-- 3:21 Shadowstep (1) 3:22 Cheat Death (3) ------------------------------------------------------------------------------- -- Master of Deception (3:1): Reduces chance to be detected in stealth. --- Stealth utility — no damage effect. --- TalentMap["3:1"] — Master of Deception: deferred (stealth utility) +-- Stealth utility - no damage effect. +-- TalentMap["3:1"] - Master of Deception: deferred (stealth utility) + +-- Camouflage (3:2): +5% move speed in stealth, -1 sec stealth cooldown per rank. +-- Stealth utility. +-- TalentMap["3:2"] - Camouflage: deferred (stealth utility) + +-- Initiative (3:3): 25% chance per rank to add combo point on Garrote/Ambush. +-- Proc-based. +-- TalentMap["3:3"] - Initiative: deferred (proc-based) + +-- Setup (3:4): Gives a chance per rank to add combo point when dodging. +-- Proc-based resource gain. +-- TalentMap["3:4"] - Setup: deferred (proc-based) --- Opportunity (3:2): +4% damage from behind with BS/Mutilate/Garrote/Ambush +-- Elusiveness (3:5): Reduces cooldown of Vanish/Blind. Utility. +-- TalentMap["3:5"] - Elusiveness: deferred (cooldown reduction) + +-- Opportunity (3:6): +4% damage from behind with BS/Mutilate/Garrote/Ambush -- per rank (4/8/12/16/20%) -TalentMap["3:2"] = { +TalentMap["3:6"] = { name = "Opportunity", maxRank = 5, effects = { @@ -328,24 +343,9 @@ TalentMap["3:2"] = { }, } --- Sleight of Hand (3:3): -1% chance to be crit, +2% Pickpocket range per rank. --- Defensive/utility. --- TalentMap["3:3"] — Sleight of Hand: deferred (defensive) - --- Dirty Tricks (3:4): Reduces Energy cost of Sap/Blind, increases range. +-- Dirty Tricks (3:7): Reduces Energy cost of Sap/Blind, increases range. -- CC utility. --- TalentMap["3:4"] — Dirty Tricks: deferred (CC utility) - --- Camouflage (3:5): +5% move speed in stealth, -1 sec stealth cooldown per rank. --- Stealth utility. --- TalentMap["3:5"] — Camouflage: deferred (stealth utility) - --- Initiative (3:6): 25% chance per rank to add combo point on Garrote/Ambush. --- Proc-based. --- TalentMap["3:6"] — Initiative: deferred (proc-based) - --- Ghostly Strike (3:7): Active ability — damage handled via SpellData. --- TalentMap["3:7"] — Ghostly Strike: handled by SpellData +-- TalentMap["3:7"] - Dirty Tricks: deferred (CC utility) -- Improved Ambush (3:8): +15% Ambush crit chance per rank (15/30/45%) TalentMap["3:8"] = { @@ -357,16 +357,32 @@ TalentMap["3:8"] = { }, } --- Setup (3:9): Gives a chance per rank to add combo point when dodging. --- Proc-based resource gain. --- TalentMap["3:9"] — Setup: deferred (proc-based) +-- Dirty Deeds (3:9): -10 Energy on Cheap Shot/Garrote per rank, +10% damage +-- to targets below 35% HP per rank (10/20%) +TalentMap["3:9"] = { + name = "Dirty Deeds", + maxRank = 2, + effects = { + { type = MOD.DAMAGE_MULTIPLIER, value = 0.10, perRank = true, stacking = "additive", + filter = { targetHealthBelow = 35 } }, + }, +} + +-- Preparation (3:10): Resets cooldowns of certain abilities. Active ability. +-- TalentMap["3:10"] - Preparation: deferred (cooldown reset) --- Elusiveness (3:10): Reduces cooldown of Vanish/Blind. Utility. --- TalentMap["3:10"] — Elusiveness: deferred (cooldown reduction) +-- Ghostly Strike (3:11): Active ability - damage handled via SpellData. +-- TalentMap["3:11"] - Ghostly Strike: handled by SpellData --- Serrated Blades (3:11): Ignore armor (scales with level) + 10% Rupture +-- Premeditation (3:12): Adds 2 combo points from stealth. Active ability. +-- TalentMap["3:12"] - Premeditation: deferred (utility) + +-- Hemorrhage (3:13): Active ability - damage handled via SpellData. +-- TalentMap["3:13"] - Hemorrhage: handled by SpellData + +-- Serrated Blades (3:14): Ignore armor (scales with level) + 10% Rupture -- damage per rank (10/20/30%) -TalentMap["3:11"] = { +TalentMap["3:14"] = { name = "Serrated Blades", maxRank = 3, effects = { @@ -377,9 +393,13 @@ TalentMap["3:11"] = { }, } --- Heightened Senses (3:12): +3% stealth detection, +2% chance to hit per rank. +-- Sleight of Hand (3:15): -1% chance to be crit, +2% Pickpocket range per rank. +-- Defensive/utility. +-- TalentMap["3:15"] - Sleight of Hand: deferred (defensive) + +-- Heightened Senses (3:16): +3% stealth detection, +2% chance to hit per rank. -- The hit component is damage-relevant. -TalentMap["3:12"] = { +TalentMap["3:16"] = { name = "Heightened Senses", maxRank = 2, effects = { @@ -387,47 +407,19 @@ TalentMap["3:12"] = { }, } --- Preparation (3:13): Resets cooldowns of certain abilities. Active ability. --- TalentMap["3:13"] — Preparation: deferred (cooldown reset) - --- Dirty Deeds (3:14): -10 Energy on Cheap Shot/Garrote per rank, +10% damage --- to targets below 35% HP per rank (10/20%) -TalentMap["3:14"] = { - name = "Dirty Deeds", - maxRank = 2, - effects = { - { type = MOD.DAMAGE_MULTIPLIER, value = 0.10, perRank = true, stacking = "additive", - filter = { targetHealthBelow = 35 } }, - }, -} - --- Hemorrhage (3:15): Active ability — damage handled via SpellData. --- TalentMap["3:15"] — Hemorrhage: handled by SpellData - --- Master of Subtlety (3:16): +4/7/10% damage while stealthed and 6 sec after --- NOTE: Stealth-conditional buff. Should be handled via AuraMap when the --- Master of Subtlety buff is active (non-linear scaling: 4/7/10%). --- TalentMap["3:16"] — Master of Subtlety: deferred to AuraMap - -- Deadliness (3:17): +2% attack power per rank (2/4/6/8/10%) --- NOTE: Stat multiplier on AP — the increased AP is reflected in +-- NOTE: Stat multiplier on AP - the increased AP is reflected in -- playerState.stats.attackPower when collected by StateCollector. --- TalentMap["3:17"] — Deadliness: handled via stats +-- TalentMap["3:17"] - Deadliness: handled via stats -- Enveloping Shadows (3:18): +5% Cloak/Feint effect per rank. Defensive. --- TalentMap["3:18"] — Enveloping Shadows: deferred (defensive) - --- Premeditation (3:19): Adds 2 combo points from stealth. Active ability. --- TalentMap["3:19"] — Premeditation: deferred (utility) +-- TalentMap["3:18"] - Enveloping Shadows: deferred (defensive) --- Cheat Death (3:20): Reduces all damage taken by a killing blow. Defensive. --- TalentMap["3:20"] — Cheat Death: deferred (defensive) - --- Sinister Calling (3:21): +3% Agility, +1% BS/Hemo damage bonus per rank +-- Sinister Calling (3:19): +3% Agility, +1% BS/Hemo damage bonus per rank -- (3/6/9/12/15% Agi, 1/2/3/4/5% BS/Hemo) -- NOTE: The Agility portion is a stat multiplier handled by StateCollector. -- The BS/Hemo damage bonus is modeled here. -TalentMap["3:21"] = { +TalentMap["3:19"] = { name = "Sinister Calling", maxRank = 5, effects = { @@ -436,10 +428,18 @@ TalentMap["3:21"] = { }, } --- Shadowstep (3:22): Teleport behind target, +20% damage on next ability, --- -50% threat. Active ability — handled via AuraMap when the Shadowstep +-- Master of Subtlety (3:20): +4/7/10% damage while stealthed and 6 sec after +-- NOTE: Stealth-conditional buff. Should be handled via AuraMap when the +-- Master of Subtlety buff is active (non-linear scaling: 4/7/10%). +-- TalentMap["3:20"] - Master of Subtlety: deferred to AuraMap + +-- Shadowstep (3:21): Teleport behind target, +20% damage on next ability, +-- -50% threat. Active ability - handled via AuraMap when the Shadowstep -- damage buff is active. --- TalentMap["3:22"] — Shadowstep: deferred to AuraMap +-- TalentMap["3:21"] - Shadowstep: deferred to AuraMap + +-- Cheat Death (3:22): Reduces all damage taken by a killing blow. Defensive. +-- TalentMap["3:22"] - Cheat Death: deferred (defensive) -- Merge into addon namespace with class prefix for key, data in pairs(TalentMap) do diff --git a/Data/TalentMap_Shaman.lua b/Data/TalentMap_Shaman.lua index bb175a2..6efbd33 100644 --- a/Data/TalentMap_Shaman.lua +++ b/Data/TalentMap_Shaman.lua @@ -1,10 +1,14 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Shaman --- Shaman talent modifier definitions for PhDamage +-- TalentMap_Shaman.lua +-- Shaman talent effects mapped to modifier descriptors for TBC Anniversary +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) -- -- Supported versions: TBC Anniversary ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... + ns.TalentMap = ns.TalentMap or {} local MOD = ns.MOD @@ -13,6 +17,16 @@ local TalentMap = {} ------------------------------------------------------------------------------- -- Elemental (Tab 1) +-- 1:1 Call of Flame (3) 1:2 Call of Thunder (5) +-- 1:3 Concussion (5) 1:4 Convection (5) +-- 1:5 Elemental Fury (1) 1:6 Improved Fire Totems (2) +-- 1:7 Earth's Grasp (2) 1:8 Elemental Mastery (1) +-- 1:9 Elemental Focus (1) 1:10 Reverberation (5) +-- 1:11 Lightning Mastery (5) 1:12 Elemental Warding (3) +-- 1:13 Storm Reach (2) 1:14 Eye of the Storm (3) +-- 1:15 Elemental Devastation (3) 1:16 Unrelenting Storm (5) +-- 1:17 Elemental Shields (3) 1:18 Elemental Precision (3) +-- 1:19 Lightning Overload (5) 1:20 Totem of Wrath (1) ------------------------------------------------------------------------------- -- Call of Thunder: +1% crit chance on Lightning Bolt and Chain Lightning per rank @@ -66,6 +80,17 @@ TalentMap["1:18"] = { ------------------------------------------------------------------------------- -- Enhancement (Tab 2) +-- 2:1 Anticipation (5) 2:2 Flurry (5) +-- 2:3 Improved Ghost Wolf (2) 2:4 Improved Lightning Shield (3) +-- 2:5 Guardian Totems (2) 2:6 Enhancing Totems (2) +-- 2:7 Elemental Weapons (3) 2:8 Shield Specialization (5) +-- 2:9 Thundering Strikes (5) 2:10 Ancestral Knowledge (5) +-- 2:11 Toughness (5) 2:12 Spirit Weapons (1) +-- 2:13 Shamanistic Focus (1) 2:14 Stormstrike (1) +-- 2:15 Weapon Mastery (5) 2:16 Improved Weapon Totems (2) +-- 2:17 Unleashed Rage (5) 2:18 Dual Wield (1) +-- 2:19 Mental Quickness (3) 2:20 Dual Wield Specialization (3) +-- 2:21 Shamanistic Rage (1) ------------------------------------------------------------------------------- -- Weapon Mastery: +2% melee damage per rank @@ -99,6 +124,16 @@ TalentMap["2:20"] = { ------------------------------------------------------------------------------- -- Restoration (Tab 3) +-- 3:1 Ancestral Healing (3) 3:2 Totemic Mastery (1) +-- 3:3 Nature's Guidance (3) 3:4 Improved Healing Wave (5) +-- 3:5 Healing Focus (5) 3:6 Restorative Totems (5) +-- 3:7 Improved Reincarnation (2) 3:8 Mana Tide Totem (1) +-- 3:9 Nature's Swiftness (1) 3:10 Purification (5) +-- 3:11 Tidal Focus (5) 3:12 Tidal Mastery (5) +-- 3:13 Totemic Focus (5) 3:14 Healing Grace (3) +-- 3:15 Healing Way (3) 3:16 Focused Mind (3) +-- 3:17 Nature's Blessing (3) 3:18 Improved Chain Heal (2) +-- 3:19 Earth Shield (1) 3:20 Nature's Guardian (5) ------------------------------------------------------------------------------- -- Improved Healing Wave: -0.1s cast time on Healing Wave per rank diff --git a/Data/TalentMap_Warrior.lua b/Data/TalentMap_Warrior.lua index 1197070..13601a0 100644 --- a/Data/TalentMap_Warrior.lua +++ b/Data/TalentMap_Warrior.lua @@ -1,10 +1,11 @@ +local ADDON_NAME, ns = ... + ------------------------------------------------------------------------------- --- TalentMap_Warrior --- Warrior talent modifier definitions for PhDamage --- --- Supported versions: TBC Anniversary +-- Warrior Talent Modifiers - TBC Anniversary (2.5.5) +-- Talent positions (tab:index) verified in-game on TBC Anniversary +-- (ordered by internal talentID) ------------------------------------------------------------------------------- -local ADDON_NAME, ns = ... + ns.TalentMap = ns.TalentMap or {} local MOD = ns.MOD @@ -12,11 +13,39 @@ local MOD = ns.MOD local TalentMap = {} ------------------------------------------------------------------------------- --- Arms (Tab 1) +-- Arms (Tab 1, 23 talents) +-- 1:1 Deep Wounds (3) 1:2 Sword Specialization (5) +-- 1:3 Improved Heroic Strike (3) 1:4 Mace Specialization (5) +-- 1:5 Improved Charge (2) 1:6 Improved Rend (3) +-- 1:7 Improved Thunder Clap (3) 1:8 Improved Hamstring (3) +-- 1:9 Deflection (5) 1:10 Improved Overpower (2) +-- 1:11 Poleaxe Specialization (5) 1:12 Death Wish (1) +-- 1:13 Improved Intercept (2) 1:14 Mortal Strike (1) +-- 1:15 Two-Handed Weapon Specialization (5) +-- 1:16 Anger Management (1) 1:17 Iron Will (5) +-- 1:18 Impale (2) 1:19 Endless Rage (1) +-- 1:20 Improved Disciplines (3) 1:21 Second Wind (2) +-- 1:22 Blood Frenzy (2) 1:23 Improved Mortal Strike (5) ------------------------------------------------------------------------------- --- Improved Rend: +25% Rend bleed damage per rank -TalentMap["1:3"] = { +-- Deep Wounds: 20% weapon average damage bleed over 12s per rank +-- NOTE: Proc-based (triggers on crit). Damage calculation is complex +-- (based on weapon average damage). Best handled via AuraMap when +-- Deep Wounds buff is active. Included here for completeness as a +-- placeholder - the engine does not yet calculate Deep Wounds ticks. +-- TalentMap["1:1"] - Deep Wounds: deferred to AuraMap + +-- Sword Specialization: 1% chance per rank for extra attack on hit +-- NOTE: Proc-based extra attack - cannot be expressed as a simple +-- damage modifier. Would require simulation or proc-rate modeling. +-- TalentMap["1:2"] - Sword Specialization: deferred (proc-based) + +-- Mace Specialization: Chance to stun + generate rage per rank +-- NOTE: Proc-based - cannot be expressed as a damage modifier. +-- TalentMap["1:4"] - Mace Specialization: deferred (proc-based) + +-- Improved Rend: +25% Rend bleed damage per rank (Arms 1:6) +TalentMap["1:6"] = { name = "Improved Rend", maxRank = 3, effects = { @@ -25,8 +54,8 @@ TalentMap["1:3"] = { }, } --- Improved Overpower: +25% Overpower crit chance per rank -TalentMap["1:7"] = { +-- Improved Overpower: +25% Overpower crit chance per rank (Arms 1:10) +TalentMap["1:10"] = { name = "Improved Overpower", maxRank = 2, effects = { @@ -35,18 +64,27 @@ TalentMap["1:7"] = { }, } --- Deep Wounds: 20% weapon average damage bleed over 12s per rank --- NOTE: Proc-based (triggers on crit). Damage calculation is complex --- (based on weapon average damage). Best handled via AuraMap when --- Deep Wounds buff is active. Included here for completeness as a --- placeholder — the engine does not yet calculate Deep Wounds ticks. --- TalentMap["1:9"] — Deep Wounds: deferred to AuraMap +-- Poleaxe Specialization: +1% crit with Axes and Polearms per rank (Arms 1:11) +-- TODO: Needs weaponType filter support in MatchesFilter. Currently applies +-- to all abilities regardless of weapon type. +TalentMap["1:11"] = { + name = "Poleaxe Specialization", + maxRank = 5, + effects = { + { type = MOD.CRIT_BONUS, value = 0.01, perRank = true }, + }, +} + +-- Death Wish: +20% physical damage, active ability (30s duration) +-- NOTE: This is an activated ability, not a passive talent. The damage +-- bonus should be handled via AuraMap when the Death Wish buff is active. +-- TalentMap["1:12"] - Death Wish: deferred to AuraMap --- Two-Handed Weapon Specialization: +1% damage with 2H weapons per rank +-- Two-Handed Weapon Specialization: +1% damage with 2H weapons per rank (Arms 1:15) -- TODO: Needs weaponType filter support in MatchesFilter. Currently applies -- to all abilities regardless of weapon type. This is acceptable for 2H -- warriors but over-applies for dual-wielders. -TalentMap["1:10"] = { +TalentMap["1:15"] = { name = "Two-Handed Weapon Specialization", maxRank = 5, effects = { @@ -54,8 +92,8 @@ TalentMap["1:10"] = { }, } --- Impale: +10% crit damage bonus per rank -TalentMap["1:11"] = { +-- Impale: +10% crit damage bonus per rank (Arms 1:18) +TalentMap["1:18"] = { name = "Impale", maxRank = 2, effects = { @@ -63,38 +101,13 @@ TalentMap["1:11"] = { }, } --- Poleaxe Specialization: +1% crit with Axes and Polearms per rank --- TODO: Needs weaponType filter support in MatchesFilter. Currently applies --- to all abilities regardless of weapon type. -TalentMap["1:12"] = { - name = "Poleaxe Specialization", - maxRank = 5, - effects = { - { type = MOD.CRIT_BONUS, value = 0.01, perRank = true }, - }, -} - --- Death Wish: +20% physical damage, active ability (30s duration) --- NOTE: This is an activated ability, not a passive talent. The damage --- bonus should be handled via AuraMap when the Death Wish buff is active. --- TalentMap["1:13"] — Death Wish: deferred to AuraMap - --- Sword Specialization: 1% chance per rank for extra attack on hit --- NOTE: Proc-based extra attack — cannot be expressed as a simple --- damage modifier. Would require simulation or proc-rate modeling. --- TalentMap["1:15"] — Sword Specialization: deferred (proc-based) - --- Mace Specialization: Chance to stun + generate rage per rank --- NOTE: Proc-based — cannot be expressed as a damage modifier. --- TalentMap["1:14"] — Mace Specialization: deferred (proc-based) - -- Blood Frenzy: +2% physical damage taken debuff on target per rank -- NOTE: This is a debuff applied to the target via Rend/Deep Wounds. -- Should be handled via AuraMap keyed to the Blood Frenzy debuff spellID. --- TalentMap["1:19"] — Blood Frenzy: deferred to AuraMap +-- TalentMap["1:22"] - Blood Frenzy: deferred to AuraMap --- Improved Mortal Strike: +1% Mortal Strike damage per rank -TalentMap["1:22"] = { +-- Improved Mortal Strike: +1% Mortal Strike damage per rank (Arms 1:23) +TalentMap["1:23"] = { name = "Improved Mortal Strike", maxRank = 5, effects = { @@ -104,11 +117,38 @@ TalentMap["1:22"] = { } ------------------------------------------------------------------------------- --- Fury (Tab 2) +-- Fury (Tab 2, 21 talents) +-- 2:1 Commanding Presence (5) 2:2 Enrage (5) +-- 2:3 Flurry (5) 2:4 Cruelty (5) +-- 2:5 Booming Voice (5) 2:6 Unbridled Wrath (5) +-- 2:7 Piercing Howl (1) 2:8 Improved Demoralizing Shout (5) +-- 2:9 Sweeping Strikes (1) 2:10 Improved Cleave (3) +-- 2:11 Bloodthirst (1) 2:12 Improved Slam (2) +-- 2:13 Blood Craze (3) 2:14 Improved Berserker Rage (2) +-- 2:15 Improved Execute (2) 2:16 Weapon Mastery (2) +-- 2:17 Dual Wield Specialization (5) 2:18 Improved Whirlwind (2) +-- 2:19 Precision (3) 2:20 Improved Berserker Stance (5) +-- 2:21 Rampage (1) ------------------------------------------------------------------------------- --- Cruelty: +1% melee crit chance per rank -TalentMap["2:2"] = { +-- Commanding Presence: +5% Battle Shout AP / Commanding Shout HP per rank +-- NOTE: This modifies buff strength, not direct damage. The increased AP +-- from Battle Shout is already reflected in playerState.stats.attackPower. +-- No TalentMap entry needed - the buff effect is captured by StateCollector. +-- TalentMap["2:1"] - Commanding Presence: handled via stats + +-- Enrage: +5% melee damage for 12s after being crit, per rank +-- NOTE: Proc-based buff triggered by receiving a critical hit. +-- Should be handled via AuraMap when the Enrage buff is active. +-- TalentMap["2:2"] - Enrage: deferred to AuraMap + +-- Flurry: +5% attack speed for 3 swings after crit, per rank +-- NOTE: Proc-based buff with charge consumption. Cannot be expressed +-- as a simple modifier - requires proc modeling or AuraMap. +-- TalentMap["2:3"] - Flurry: deferred to AuraMap + +-- Cruelty: +1% melee crit chance per rank (Fury 2:4) +TalentMap["2:4"] = { name = "Cruelty", maxRank = 5, effects = { @@ -116,8 +156,8 @@ TalentMap["2:2"] = { }, } --- Improved Cleave: +40% bonus Cleave damage per rank -TalentMap["2:5"] = { +-- Improved Cleave: +40% bonus Cleave damage per rank (Fury 2:10) +TalentMap["2:10"] = { name = "Improved Cleave", maxRank = 3, effects = { @@ -126,23 +166,7 @@ TalentMap["2:5"] = { }, } --- Commanding Presence: +5% Battle Shout AP / Commanding Shout HP per rank --- NOTE: This modifies buff strength, not direct damage. The increased AP --- from Battle Shout is already reflected in playerState.stats.attackPower. --- No TalentMap entry needed — the buff effect is captured by StateCollector. --- TalentMap["2:8"] — Commanding Presence: handled via stats - --- Dual Wield Specialization: +5% off-hand damage per rank --- TODO: Needs off-hand vs main-hand differentiation in the engine. --- Currently the engine does not model OH vs MH separately. --- TalentMap["2:9"] — Dual Wield Specialization: deferred (OH-specific) - --- Enrage: +5% melee damage for 12s after being crit, per rank --- NOTE: Proc-based buff triggered by receiving a critical hit. --- Should be handled via AuraMap when the Enrage buff is active. --- TalentMap["2:11"] — Enrage: deferred to AuraMap - --- Improved Slam: -0.5s Slam cast time per rank +-- Improved Slam: -0.5s Slam cast time per rank (Fury 2:12) TalentMap["2:12"] = { name = "Improved Slam", maxRank = 2, @@ -152,14 +176,14 @@ TalentMap["2:12"] = { }, } --- Weapon Mastery: -1% chance to be dodged per rank +-- Weapon Mastery: -1% chance to be dodged per rank (Fury 2:16) -- NOTE: This reduces the target's effective dodge rate against the warrior. -- Functionally similar to hit bonus but specifically reduces dodge. -- Using SPELL_HIT_BONUS as the universal hit accumulator (same as -- Hunter's Surefooted) since the engine treats all hit bonuses uniformly. -- The dodge reduction is mechanically different from hit rating but has -- the same net effect on damage expectation calculations. -TalentMap["2:14"] = { +TalentMap["2:16"] = { name = "Weapon Mastery", maxRank = 2, effects = { @@ -167,13 +191,20 @@ TalentMap["2:14"] = { }, } --- Flurry: +5% attack speed for 3 swings after crit, per rank --- NOTE: Proc-based buff with charge consumption. Cannot be expressed --- as a simple modifier — requires proc modeling or AuraMap. --- TalentMap["2:16"] — Flurry: deferred to AuraMap +-- Dual Wield Specialization: +5% off-hand damage per rank +-- TODO: Needs off-hand vs main-hand differentiation in the engine. +-- Currently the engine does not model OH vs MH separately. +-- TalentMap["2:17"] - Dual Wield Specialization: deferred (OH-specific) --- Precision: +1% melee hit per rank -TalentMap["2:17"] = { +-- Improved Whirlwind: -1s Whirlwind cooldown per rank +-- NOTE: Cooldown reduction does not affect per-hit damage, only DPS +-- throughput. The current engine computes per-hit damage, not DPS. +-- No MOD type for cooldown reduction exists. Included as a comment +-- for future DPS modeling. +-- TalentMap["2:18"] - Improved Whirlwind: deferred (cooldown reduction) + +-- Precision: +1% melee hit per rank (Fury 2:19) +TalentMap["2:19"] = { name = "Precision", maxRank = 3, effects = { @@ -181,22 +212,27 @@ TalentMap["2:17"] = { }, } --- Improved Whirlwind: -1s Whirlwind cooldown per rank --- NOTE: Cooldown reduction does not affect per-hit damage, only DPS --- throughput. The current engine computes per-hit damage, not DPS. --- No MOD type for cooldown reduction exists. Included as a comment --- for future DPS modeling. --- TalentMap["2:19"] — Improved Whirlwind: deferred (cooldown reduction) - -- Improved Berserker Stance: +2% AP in Berserker Stance per rank -- NOTE: The AP bonus is stance-dependent and should be reflected in -- playerState.stats.attackPower when in Berserker Stance. Could be -- handled via AuraMap keyed to Berserker Stance buff, or via -- StateCollector detecting the stance. Deferred for now. --- TalentMap["2:20"] — Improved Berserker Stance: deferred to AuraMap +-- TalentMap["2:20"] - Improved Berserker Stance: deferred to AuraMap ------------------------------------------------------------------------------- --- Protection (Tab 3) +-- Protection (Tab 3, 22 talents) +-- 3:1 Anticipation (5) 3:2 Toughness (5) +-- 3:3 Tactical Mastery (3) 3:4 Improved Bloodrage (2) +-- 3:5 Improved Taunt (2) 3:6 Defiance (3) +-- 3:7 Improved Shield Block (1) 3:8 Improved Sunder Armor (3) +-- 3:9 Improved Revenge (3) 3:10 Shield Slam (1) +-- 3:11 Improved Shield Bash (2) 3:12 Improved Shield Wall (2) +-- 3:13 Improved Disarm (3) 3:14 Concussion Blow (1) +-- 3:15 Last Stand (1) 3:16 One-Handed Weapon Specialization (5) +-- 3:17 Shield Specialization (5) 3:18 Improved Defensive Stance (3) +-- 3:19 Vitality (5) 3:20 Shield Mastery (3) +-- 3:21 Focused Rage (3) 3:22 Devastate (1) +-- -- Most Protection talents are defensive/threat-oriented. Only damage- -- relevant talents are included here. ------------------------------------------------------------------------------- @@ -215,7 +251,7 @@ TalentMap["2:17"] = { -- - One-Handed Weapon Specialization: +damage with 1H weapons -- - Focused Rage: rage cost reduction -- --- One-Handed Weapon Specialization (3:19) could be added but needs +-- One-Handed Weapon Specialization (3:16) could be added but needs -- weaponType filter support (same issue as Two-Handed Weapon Spec). -- Merge into addon namespace with class prefix diff --git a/tests/test_hunter.lua b/tests/test_hunter.lua index 13899a4..894c0aa 100644 --- a/tests/test_hunter.lua +++ b/tests/test_hunter.lua @@ -86,13 +86,13 @@ describe("Hunter", function() end) end) - describe("Talent: Lethal Shots (2:2)", function() + describe("Talent: Lethal Shots (2:4)", function() it("should increase crit by 5% at rank 5", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["2:2"] = 5 + state.talents["2:4"] = 5 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.critChance + 0.05, buffed.critChance, 0.001) @@ -102,33 +102,33 @@ describe("Hunter", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["2:2"] = 1 + state.talents["2:4"] = 1 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.critChance + 0.01, buffed.critChance, 0.001) end) end) - describe("Talent: Mortal Shots (2:10)", function() + describe("Talent: Mortal Shots (2:9)", function() it("should increase crit multiplier by 30% at rank 5", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["2:10"] = 5 + state.talents["2:9"] = 5 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.critMultiplier + 0.30, buffed.critMultiplier, 0.001) end) end) - describe("Talent: Barrage (2:13)", function() + describe("Talent: Barrage (2:7)", function() it("should increase Multi-Shot damage by 12% at rank 3", function() local state = makeHunterState() local base = Pipeline.Calculate(2643, state) - state.talents["2:13"] = 3 + state.talents["2:7"] = 3 local buffed = Pipeline.Calculate(2643, state) assert.is_near(base.expectedDamageWithMiss * 1.12, buffed.expectedDamageWithMiss, 0.5) @@ -138,7 +138,7 @@ describe("Hunter", function() local state = makeHunterState() local base = Pipeline.Calculate(1510, state) - state.talents["2:13"] = 3 + state.talents["2:7"] = 3 local buffed = Pipeline.Calculate(1510, state) assert.is_near(base.expectedDamageWithMiss * 1.12, buffed.expectedDamageWithMiss, 0.5) @@ -148,33 +148,33 @@ describe("Hunter", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["2:13"] = 3 + state.talents["2:7"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss, buffed.expectedDamageWithMiss, 0.01) end) end) - describe("Talent: Ranged Weapon Specialization (2:15)", function() + describe("Talent: Ranged Weapon Specialization (2:13)", function() it("should increase all ranged damage by 5% at rank 5", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["2:15"] = 5 + state.talents["2:13"] = 5 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss * 1.05, buffed.expectedDamageWithMiss, 0.5) end) end) - describe("Talent: Improved Stings (2:9)", function() + describe("Talent: Improved Stings (2:8)", function() it("should increase Serpent Sting damage by 18% at rank 3", function() local state = makeHunterState() local base = Pipeline.Calculate(1978, state) - state.talents["2:9"] = 3 + state.talents["2:8"] = 3 local buffed = Pipeline.Calculate(1978, state) assert.is_near(base.expectedDamageWithMiss * 1.18, buffed.expectedDamageWithMiss, 0.5) @@ -184,60 +184,60 @@ describe("Hunter", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["2:9"] = 3 + state.talents["2:8"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss, buffed.expectedDamageWithMiss, 0.01) end) end) - describe("Talent: Surefooted (3:12)", function() + describe("Talent: Surefooted (3:8)", function() it("should increase hit by 3% at rank 3", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["3:12"] = 3 + state.talents["3:8"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.hitChance + 0.03, buffed.hitChance, 0.001) end) end) - describe("Talent: Survival Instincts (3:14)", function() + describe("Talent: Survival Instincts (3:18)", function() it("should increase crit by 4% at rank 2", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["3:14"] = 2 + state.talents["3:18"] = 2 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.critChance + 0.04, buffed.critChance, 0.001) end) end) - describe("Talent: Focused Fire (1:3)", function() + describe("Talent: Focused Fire (1:15)", function() it("should increase all damage by 2% at rank 2", function() local state = makeHunterState() local base = Pipeline.Calculate(3044, state) - state.talents["1:3"] = 2 + state.talents["1:15"] = 2 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss * 1.02, buffed.expectedDamageWithMiss, 0.5) end) end) - describe("Talent: Monster Slaying (3:1)", function() + describe("Talent: Monster Slaying (3:16)", function() it("should increase damage by 3% vs Beasts at rank 3", function() local state = makeHunterState() state.targetCreatureType = "Beast" local base = Pipeline.Calculate(3044, state) - state.talents["3:1"] = 3 + state.talents["3:16"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss * 1.03, buffed.expectedDamageWithMiss, 0.5) @@ -248,7 +248,7 @@ describe("Hunter", function() state.targetCreatureType = "Humanoid" local base = Pipeline.Calculate(3044, state) - state.talents["3:1"] = 3 + state.talents["3:16"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss, buffed.expectedDamageWithMiss, 0.01) @@ -259,21 +259,21 @@ describe("Hunter", function() state.targetCreatureType = nil local base = Pipeline.Calculate(3044, state) - state.talents["3:1"] = 3 + state.talents["3:16"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss, buffed.expectedDamageWithMiss, 0.01) end) end) - describe("Talent: Humanoid Slaying (3:2)", function() + describe("Talent: Humanoid Slaying (3:1)", function() it("should increase damage by 3% vs Humanoids at rank 3", function() local state = makeHunterState() state.targetCreatureType = "Humanoid" local base = Pipeline.Calculate(3044, state) - state.talents["3:2"] = 3 + state.talents["3:1"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss * 1.03, buffed.expectedDamageWithMiss, 0.5) @@ -284,7 +284,7 @@ describe("Hunter", function() state.targetCreatureType = "Beast" local base = Pipeline.Calculate(3044, state) - state.talents["3:2"] = 3 + state.talents["3:1"] = 3 local buffed = Pipeline.Calculate(3044, state) assert.is_near(base.expectedDamageWithMiss, buffed.expectedDamageWithMiss, 0.01) @@ -305,8 +305,8 @@ describe("Hunter", function() it("should stack multiple additive damage talents", function() local state = makeHunterState() - state.talents["2:15"] = 5 -- +5% ranged weapon spec - state.talents["1:3"] = 2 -- +2% focused fire + state.talents["2:13"] = 5 -- +5% ranged weapon spec + state.talents["1:15"] = 2 -- +2% focused fire -- Both additive: total +7% local result = Pipeline.Calculate(3044, state) diff --git a/tests/test_paladin_talents.lua b/tests/test_paladin_talents.lua index df7551d..6dbe09c 100644 --- a/tests/test_paladin_talents.lua +++ b/tests/test_paladin_talents.lua @@ -12,7 +12,7 @@ local Pipeline = ns.Engine.Pipeline ------------------------------------------------------------------------------- -- Default Paladin state reference (from bootstrap): -- spellPower: Holy(2)=800 --- healingPower: 900 (not used by engine — SP used for heals too) +-- healingPower: 900 (not used by engine -- SP used for heals too) -- spellCrit: Holy(2)=0.15 -- spellHit = 0.05, intellect = 350, attackPower = 200 -- @@ -28,7 +28,7 @@ local Pipeline = ns.Engine.Pipeline -- SP bonus: 800 * 0.286 = 228.8 -- min = 637 + 228.8 = 865.8, max = 748 + 228.8 = 976.8 -- --- Base spell hit: spellHit=0.05 → hitChance stored raw, hitProbability = 1 - 0.16 + 0.05 = 0.89 +-- Base spell hit: spellHit=0.05 -> hitChance stored raw, hitProbability = 1 - 0.16 + 0.05 = 0.89 -- Base crit mult (spell): 1.5 ------------------------------------------------------------------------------- @@ -39,7 +39,7 @@ describe("Paladin Talents", function() --------------------------------------------------------------------------- --------------------------------------------------------------------------- - -- 1. Healing Light (1:7) — +4%/rank healing to HL & FoL (additive) + -- 1. Healing Light (1:5) -- +4%/rank healing to HL & FoL (additive) --------------------------------------------------------------------------- describe("Healing Light", function() @@ -47,7 +47,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(635, makePaladinState()) local state = makePaladinState() - state.talents["1:7"] = 3 + state.talents["1:5"] = 3 local result = Pipeline.Calculate(635, state) -- HL base: min=2767.2, max=3017.2 @@ -60,7 +60,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(19750, makePaladinState()) local state = makePaladinState() - state.talents["1:7"] = 3 + state.talents["1:5"] = 3 local result = Pipeline.Calculate(19750, state) assert.is_near(baseResult.minDmg * 1.12, result.minDmg, 1) @@ -71,7 +71,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["1:7"] = 3 + state.talents["1:5"] = 3 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.minDmg, result.minDmg, 0.01) @@ -80,7 +80,7 @@ describe("Paladin Talents", function() end) --------------------------------------------------------------------------- - -- 2. Sanctified Light (1:14) — +2%/rank crit to HL & FoL + -- 2. Sanctified Light (1:11) -- +2%/rank crit to HL & FoL --------------------------------------------------------------------------- describe("Sanctified Light", function() @@ -88,7 +88,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(635, makePaladinState()) local state = makePaladinState() - state.talents["1:14"] = 3 + state.talents["1:11"] = 3 local result = Pipeline.Calculate(635, state) assert.is_near(baseResult.critChance + 0.06, result.critChance, 0.001) @@ -98,7 +98,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(19750, makePaladinState()) local state = makePaladinState() - state.talents["1:14"] = 3 + state.talents["1:11"] = 3 local result = Pipeline.Calculate(19750, state) assert.is_near(baseResult.critChance + 0.06, result.critChance, 0.001) @@ -108,7 +108,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["1:14"] = 3 + state.talents["1:11"] = 3 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.critChance, result.critChance, 0.001) @@ -116,7 +116,7 @@ describe("Paladin Talents", function() end) --------------------------------------------------------------------------- - -- 3. Purifying Power (1:16) — +10%/rank crit to Exorcism & Holy Wrath + -- 3. Purifying Power (1:16) -- +10%/rank crit to Exorcism & Holy Wrath --------------------------------------------------------------------------- describe("Purifying Power", function() @@ -152,7 +152,7 @@ describe("Paladin Talents", function() end) --------------------------------------------------------------------------- - -- 4. Holy Power (1:19) — +1%/rank Holy spell crit + -- 4. Holy Power (1:13) -- +1%/rank Holy spell crit --------------------------------------------------------------------------- describe("Holy Power", function() @@ -160,7 +160,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["1:19"] = 5 + state.talents["1:13"] = 5 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.critChance + 0.05, result.critChance, 0.001) @@ -170,7 +170,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(635, makePaladinState()) local state = makePaladinState() - state.talents["1:19"] = 5 + state.talents["1:13"] = 5 local result = Pipeline.Calculate(635, state) assert.is_near(baseResult.critChance + 0.05, result.critChance, 0.001) @@ -178,13 +178,13 @@ describe("Paladin Talents", function() end) --------------------------------------------------------------------------- - -- 5. Holy Guidance (1:25) — +5%/rank INT as spell power + -- 5. Holy Guidance (1:19) -- +5%/rank INT as spell power --------------------------------------------------------------------------- describe("Holy Guidance", function() it("should add 25% of intellect as spell power at 5/5 on Exorcism", function() local state = makePaladinState() - state.talents["1:25"] = 5 + state.talents["1:19"] = 5 local r = Pipeline.Calculate(879, state) -- INT = 350, bonus SP = 350 * 0.25 = 87.5 -- Effective SP = 800 + 87.5 = 887.5 @@ -196,7 +196,7 @@ describe("Paladin Talents", function() it("should add 10% of intellect as spell power at 2/5 on Exorcism", function() local state = makePaladinState() - state.talents["1:25"] = 2 + state.talents["1:19"] = 2 local r = Pipeline.Calculate(879, state) -- INT = 350, bonus SP = 350 * 0.10 = 35 -- SP bonus increase = 35 * 0.429 = 15.015 @@ -207,7 +207,7 @@ describe("Paladin Talents", function() it("should also affect Holy Light (heal)", function() local state = makePaladinState() - state.talents["1:25"] = 5 + state.talents["1:19"] = 5 local r = Pipeline.Calculate(635, state) -- INT = 350, bonus SP = 350 * 0.25 = 87.5 -- SP bonus = (800 + 87.5) * 0.714 = 887.5 * 0.714 = 633.675 @@ -218,7 +218,7 @@ describe("Paladin Talents", function() it("should scale with higher intellect values", function() local state = makePaladinState() - state.talents["1:25"] = 5 + state.talents["1:19"] = 5 state.stats.intellect = 500 local r = Pipeline.Calculate(879, state) -- INT = 500, bonus SP = 500 * 0.25 = 125 @@ -233,7 +233,7 @@ describe("Paladin Talents", function() --------------------------------------------------------------------------- --------------------------------------------------------------------------- - -- 6. Precision (2:4) — +1%/rank spell hit + -- 6. Precision (2:15) -- +1%/rank spell hit --------------------------------------------------------------------------- describe("Precision", function() @@ -241,7 +241,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["2:4"] = 3 + state.talents["2:15"] = 3 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.hitChance + 0.03, result.hitChance, 0.001) @@ -251,7 +251,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["2:4"] = 1 + state.talents["2:15"] = 1 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.hitChance + 0.01, result.hitChance, 0.001) @@ -259,7 +259,7 @@ describe("Paladin Talents", function() end) --------------------------------------------------------------------------- - -- 7. Combat Expertise (2:23) — +1%/rank crit (all spells) + -- 7. Combat Expertise (2:20) -- +1%/rank crit (all spells) --------------------------------------------------------------------------- describe("Combat Expertise", function() @@ -267,7 +267,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["2:23"] = 5 + state.talents["2:20"] = 5 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.critChance + 0.05, result.critChance, 0.001) @@ -277,7 +277,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(635, makePaladinState()) local state = makePaladinState() - state.talents["2:23"] = 5 + state.talents["2:20"] = 5 local result = Pipeline.Calculate(635, state) assert.is_near(baseResult.critChance + 0.05, result.critChance, 0.001) @@ -289,13 +289,13 @@ describe("Paladin Talents", function() --------------------------------------------------------------------------- --------------------------------------------------------------------------- - -- 8. Crusade (3:10) — +1%/rank all damage (additive) + -- 8. Crusade (3:16) -- +1%/rank all damage (additive) --------------------------------------------------------------------------- describe("Crusade", function() it("should increase Exorcism damage by 3% at 3/3", function() local state = makePaladinState() - state.talents["3:10"] = 3 + state.talents["3:16"] = 3 local r = Pipeline.Calculate(879, state) -- Base: min=969.2, max=1041.2 -- With 3/3: * (1 + 0.03) = 1.03 @@ -305,7 +305,7 @@ describe("Paladin Talents", function() it("should increase Holy Wrath damage by 3% at 3/3", function() local state = makePaladinState() - state.talents["3:10"] = 3 + state.talents["3:16"] = 3 local r = Pipeline.Calculate(2812, state) -- Base: min=865.8, max=976.8 assert.is_near(865.8 * 1.03, r.minDmg, 0.1) @@ -316,7 +316,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(635, makePaladinState()) local state = makePaladinState() - state.talents["3:10"] = 3 + state.talents["3:16"] = 3 local result = Pipeline.Calculate(635, state) -- Crusade has no isHeal filter, but also no filter at all, @@ -327,7 +327,7 @@ describe("Paladin Talents", function() end) --------------------------------------------------------------------------- - -- 9. Sanctified Seals (3:20) — +1%/rank crit (all spells) + -- 9. Sanctified Seals (3:21) -- +1%/rank crit (all spells) --------------------------------------------------------------------------- describe("Sanctified Seals", function() @@ -335,7 +335,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(879, makePaladinState()) local state = makePaladinState() - state.talents["3:20"] = 3 + state.talents["3:21"] = 3 local result = Pipeline.Calculate(879, state) assert.is_near(baseResult.critChance + 0.03, result.critChance, 0.001) @@ -345,7 +345,7 @@ describe("Paladin Talents", function() local baseResult = Pipeline.Calculate(635, makePaladinState()) local state = makePaladinState() - state.talents["3:20"] = 3 + state.talents["3:21"] = 3 local result = Pipeline.Calculate(635, state) assert.is_near(baseResult.critChance + 0.03, result.critChance, 0.001) @@ -358,15 +358,15 @@ describe("Paladin Talents", function() describe("Metadata", function() local expectedKeys = { - "PALADIN:1:7", -- Healing Light - "PALADIN:1:14", -- Sanctified Light + "PALADIN:1:5", -- Healing Light + "PALADIN:1:11", -- Sanctified Light + "PALADIN:1:13", -- Holy Power "PALADIN:1:16", -- Purifying Power - "PALADIN:1:19", -- Holy Power - "PALADIN:1:25", -- Holy Guidance - "PALADIN:2:4", -- Precision - "PALADIN:2:23", -- Combat Expertise - "PALADIN:3:10", -- Crusade - "PALADIN:3:20", -- Sanctified Seals + "PALADIN:1:19", -- Holy Guidance + "PALADIN:2:15", -- Precision + "PALADIN:2:20", -- Combat Expertise + "PALADIN:3:16", -- Crusade + "PALADIN:3:21", -- Sanctified Seals } for _, key in ipairs(expectedKeys) do @@ -392,8 +392,8 @@ describe("Paladin Talents", function() it("Holy Power + Sanctified Light should stack crit on Holy Light", function() local state = makePaladinState() - state.talents["1:19"] = 5 -- Holy Power +5% crit - state.talents["1:14"] = 3 -- Sanctified Light +6% crit + state.talents["1:13"] = 5 -- Holy Power +5% crit + state.talents["1:11"] = 3 -- Sanctified Light +6% crit local r = Pipeline.Calculate(635, state) -- critChance = 0.15 + 0.05 + 0.06 = 0.26 assert.is_near(0.26, r.critChance, 0.001) @@ -401,8 +401,8 @@ describe("Paladin Talents", function() it("Combat Expertise + Sanctified Seals should stack crit on Exorcism", function() local state = makePaladinState() - state.talents["2:23"] = 5 -- Combat Expertise +5% crit - state.talents["3:20"] = 3 -- Sanctified Seals +3% crit + state.talents["2:20"] = 5 -- Combat Expertise +5% crit + state.talents["3:21"] = 3 -- Sanctified Seals +3% crit local r = Pipeline.Calculate(879, state) -- critChance = 0.15 + 0.05 + 0.03 = 0.23 assert.is_near(0.23, r.critChance, 0.001) @@ -410,8 +410,8 @@ describe("Paladin Talents", function() it("Crusade + Holy Guidance should both affect Exorcism", function() local state = makePaladinState() - state.talents["3:10"] = 3 -- Crusade +3% dmg (additive) - state.talents["1:25"] = 5 -- Holy Guidance: +87.5 SP + state.talents["3:16"] = 3 -- Crusade +3% dmg (additive) + state.talents["1:19"] = 5 -- Holy Guidance: +87.5 SP local r = Pipeline.Calculate(879, state) -- SP = 800 + 87.5 = 887.5 -- SP bonus = 887.5 * 0.429 = 380.7375 @@ -425,8 +425,8 @@ describe("Paladin Talents", function() it("Healing Light + Sanctified Light should not cross-affect Exorcism", function() local state = makePaladinState() - state.talents["1:7"] = 3 -- Healing Light: +12% HL/FoL healing - state.talents["1:14"] = 3 -- Sanctified Light: +6% HL/FoL crit + state.talents["1:5"] = 3 -- Healing Light: +12% HL/FoL healing + state.talents["1:11"] = 3 -- Sanctified Light: +6% HL/FoL crit local rExo = Pipeline.Calculate(879, state) local rExoBase = Pipeline.Calculate(879, makePaladinState()) diff --git a/tests/test_priest_auras.lua b/tests/test_priest_auras.lua index f50201a..4ddbec3 100644 --- a/tests/test_priest_auras.lua +++ b/tests/test_priest_auras.lua @@ -90,7 +90,7 @@ describe("Priest Auras", function() it("should stack Shadowform with Darkness talent", function() local state = makePriestState() state.auras.player[15473] = true -- Shadowform +15% (multiplicative) - state.talents["3:15"] = 5 -- Darkness +10% (additive) + state.talents["3:2"] = 5 -- Darkness +10% (additive) local r = Pipeline.Calculate(8092, state) -- Mind Blast R11 -- Talent additive: 1.10, Shadowform multiplicative: *1.15 -- Total: 1136.6 * 1.10 * 1.15 diff --git a/tests/test_priest_talents.lua b/tests/test_priest_talents.lua index 77762e3..ec555e0 100644 --- a/tests/test_priest_talents.lua +++ b/tests/test_priest_talents.lua @@ -44,7 +44,7 @@ describe("Priest Talents", function() describe("Focused Power", function() it("should add 4% damage at 2/2", function() local state = makePriestState() - state.talents["1:20"] = 2 + state.talents["1:18"] = 2 local r = Pipeline.Calculate(8092, state) -- Mind Blast R11 assert.is_near(1136.6 * 1.04, r.minDmg, 1) assert.is_near(1176.6 * 1.04, r.maxDmg, 1) @@ -52,7 +52,7 @@ describe("Priest Talents", function() it("should affect holy spells", function() local state = makePriestState() - state.talents["1:20"] = 2 + state.talents["1:18"] = 2 local r = Pipeline.Calculate(585, state) -- Smite R10 assert.is_near(1259.3 * 1.04, r.minDmg, 1) end) @@ -60,7 +60,7 @@ describe("Priest Talents", function() it("should stack additively with Force of Will", function() local state = makePriestState() state.talents["1:15"] = 5 -- +5% - state.talents["1:20"] = 2 -- +4% + state.talents["1:18"] = 2 -- +4% local r = Pipeline.Calculate(8092, state) -- Mind Blast -- Total additive: 1 + 0.05 + 0.04 = 1.09 assert.is_near(1136.6 * 1.09, r.minDmg, 1) @@ -74,14 +74,14 @@ describe("Priest Talents", function() describe("Holy Specialization", function() it("should add 5% holy crit at 5/5", function() local state = makePriestState() - state.talents["2:3"] = 5 + state.talents["2:2"] = 5 local r = Pipeline.Calculate(585, state) -- Smite R10 assert.is_near(0.15, r.critChance, 0.001) end) it("should not affect Shadow spells", function() local state = makePriestState() - state.talents["2:3"] = 5 + state.talents["2:2"] = 5 local r = Pipeline.Calculate(8092, state) -- Mind Blast assert.is_near(0.10, r.critChance, 0.001) end) @@ -90,7 +90,7 @@ describe("Priest Talents", function() describe("Searing Light", function() it("should add 10% Smite damage at 2/2", function() local state = makePriestState() - state.talents["2:13"] = 2 + state.talents["2:4"] = 2 local r = Pipeline.Calculate(585, state) -- Smite R10 assert.is_near(1259.3 * 1.10, r.minDmg, 1) assert.is_near(1325.3 * 1.10, r.maxDmg, 1) @@ -98,7 +98,7 @@ describe("Priest Talents", function() it("should add 10% Holy Fire damage at 2/2", function() local state = makePriestState() - state.talents["2:13"] = 2 + state.talents["2:4"] = 2 local r = Pipeline.Calculate(14914, state) -- Holy Fire R9 assert.is_near(1269 * 1.10, r.directMin, 1) assert.is_near(1379 * 1.10, r.directMax, 1) @@ -107,14 +107,14 @@ describe("Priest Talents", function() it("should not affect Mind Blast", function() local state = makePriestState() - state.talents["2:13"] = 2 + state.talents["2:4"] = 2 local r = Pipeline.Calculate(8092, state) -- Mind Blast assert.is_near(1136.6, r.minDmg, 1) end) it("should not affect Holy Nova", function() local state = makePriestState() - state.talents["2:13"] = 2 + state.talents["2:4"] = 2 local r = Pipeline.Calculate(15237, state) -- Holy Nova R7 assert.is_near(403, r.minDmg, 1) end) @@ -126,14 +126,14 @@ describe("Priest Talents", function() describe("Shadow Focus", function() it("should add 10% shadow hit at 5/5", function() local state = makePriestState() - state.talents["3:2"] = 5 + state.talents["3:3"] = 5 local r = Pipeline.Calculate(8092, state) -- Mind Blast assert.is_near(0.13, r.hitChance, 0.001) end) it("should not affect holy spells", function() local state = makePriestState() - state.talents["3:2"] = 5 + state.talents["3:3"] = 5 local r = Pipeline.Calculate(585, state) -- Smite assert.is_near(0.03, r.hitChance, 0.001) end) @@ -142,7 +142,7 @@ describe("Priest Talents", function() describe("Improved Shadow Word: Pain", function() it("should add 6% SWP damage at 2/2", function() local state = makePriestState() - state.talents["3:4"] = 2 + state.talents["3:8"] = 2 local r = Pipeline.Calculate(589, state) -- SWP R10 -- 2334 * 1.06 = 2474.04 assert.is_near(2334 * 1.06, r.totalDmg, 1) @@ -151,7 +151,7 @@ describe("Priest Talents", function() it("should not affect Mind Blast", function() local state = makePriestState() - state.talents["3:4"] = 2 + state.talents["3:8"] = 2 local r = Pipeline.Calculate(8092, state) -- Mind Blast assert.is_near(1136.6, r.minDmg, 1) end) @@ -160,7 +160,7 @@ describe("Priest Talents", function() describe("Darkness", function() it("should add 10% shadow damage at 5/5", function() local state = makePriestState() - state.talents["3:15"] = 5 + state.talents["3:2"] = 5 local r = Pipeline.Calculate(8092, state) -- Mind Blast R11 assert.is_near(1136.6 * 1.10, r.minDmg, 1) assert.is_near(1176.6 * 1.10, r.maxDmg, 1) @@ -168,21 +168,21 @@ describe("Priest Talents", function() it("should add 4% at 2/5", function() local state = makePriestState() - state.talents["3:15"] = 2 + state.talents["3:2"] = 2 local r = Pipeline.Calculate(8092, state) assert.is_near(1136.6 * 1.04, r.minDmg, 1) end) it("should not affect holy spells", function() local state = makePriestState() - state.talents["3:15"] = 5 + state.talents["3:2"] = 5 local r = Pipeline.Calculate(585, state) -- Smite assert.is_near(1259.3, r.minDmg, 1) end) it("should affect SWP", function() local state = makePriestState() - state.talents["3:15"] = 5 + state.talents["3:2"] = 5 local r = Pipeline.Calculate(589, state) -- SWP R10 assert.is_near(2334 * 1.10, r.totalDmg, 1) end) @@ -191,7 +191,7 @@ describe("Priest Talents", function() describe("Shadow Power", function() it("should add 50% crit damage bonus at 5/5", function() local state = makePriestState() - state.talents["3:22"] = 5 + state.talents["3:18"] = 5 local r = Pipeline.Calculate(8092, state) -- Mind Blast -- critMult = 1.5 + 0.50 = 2.0 assert.is_near(2.0, r.critMult, 0.001) @@ -199,14 +199,14 @@ describe("Priest Talents", function() it("should add 20% at 2/5", function() local state = makePriestState() - state.talents["3:22"] = 2 + state.talents["3:18"] = 2 local r = Pipeline.Calculate(8092, state) assert.is_near(1.7, r.critMult, 0.001) end) it("should not affect holy spells", function() local state = makePriestState() - state.talents["3:22"] = 5 + state.talents["3:18"] = 5 local r = Pipeline.Calculate(585, state) -- Smite assert.is_near(1.5, r.critMult, 0.001) end) @@ -219,10 +219,10 @@ describe("Priest Talents", function() it("should combine Force of Will + Focused Power + Shadow Focus + Darkness + Shadow Power", function() local state = makePriestState() state.talents["1:15"] = 5 -- Force of Will +5% dmg, +5% crit - state.talents["1:20"] = 2 -- Focused Power +4% dmg - state.talents["3:2"] = 5 -- Shadow Focus +10% hit - state.talents["3:15"] = 5 -- Darkness +10% dmg - state.talents["3:22"] = 5 -- Shadow Power +50% crit bonus + state.talents["1:18"] = 2 -- Focused Power +4% dmg + state.talents["3:3"] = 5 -- Shadow Focus +10% hit + state.talents["3:2"] = 5 -- Darkness +10% dmg + state.talents["3:18"] = 5 -- Shadow Power +50% crit bonus local r = Pipeline.Calculate(8092, state) -- Mind Blast R11 -- Additive damage: 1 + 0.05 + 0.04 + 0.10 = 1.19 assert.is_near(1136.6 * 1.19, r.minDmg, 1) @@ -240,9 +240,9 @@ describe("Priest Talents", function() it("should combine Force of Will + Focused Power + Holy Spec + Searing Light on Smite", function() local state = makePriestState() state.talents["1:15"] = 5 -- Force of Will +5% dmg, +5% crit - state.talents["1:20"] = 2 -- Focused Power +4% dmg - state.talents["2:3"] = 5 -- Holy Specialization +5% crit - state.talents["2:13"] = 2 -- Searing Light +10% Smite/HF dmg + state.talents["1:18"] = 2 -- Focused Power +4% dmg + state.talents["2:2"] = 5 -- Holy Specialization +5% crit + state.talents["2:4"] = 2 -- Searing Light +10% Smite/HF dmg local r = Pipeline.Calculate(585, state) -- Smite R10 -- Additive damage: 1 + 0.05 + 0.04 + 0.10 = 1.19 assert.is_near(1259.3 * 1.19, r.minDmg, 1) diff --git a/tests/test_rogue_auras.lua b/tests/test_rogue_auras.lua index 585c568..50710aa 100644 --- a/tests/test_rogue_auras.lua +++ b/tests/test_rogue_auras.lua @@ -529,7 +529,7 @@ describe("Rogue Auras", function() it("Shadowstep + Aggression 3/3 on Sinister Strike", function() local state = makeRogueState() state.auras.player[36563] = true -- Shadowstep +20% - state.talents["2:19"] = 3 -- Aggression 3/3 (+6%) + state.talents["2:17"] = 3 -- Aggression 3/3 (+6%) local result = Pipeline.Calculate(1752, state) -- Aggression: additive +6% on base → 1.06 -- Shadowstep: multiplicative +20% → 1.20 @@ -561,7 +561,7 @@ describe("Rogue Auras", function() it("Remorseless R2 + Puncturing Wounds 3/3 on Backstab", function() local state = makeDaggerRogueState() state.auras.player[14149] = true -- Remorseless R2 +40% - state.talents["1:6"] = 3 -- Puncturing Wounds 3/3 (+30% BS) + state.talents["1:8"] = 3 -- Puncturing Wounds 3/3 (+30% BS) local result = Pipeline.Calculate(53, state) -- 0.25 + 0.40 + 0.30 = 0.95 assert.is_near(0.95, result.critChance, 0.01) @@ -570,7 +570,7 @@ describe("Rogue Auras", function() it("Remorseless R2 + Puncturing Wounds 3/3 on Mutilate", function() local state = makeDaggerRogueState() state.auras.player[14149] = true -- Remorseless R2 +40% - state.talents["1:6"] = 3 -- Puncturing Wounds 3/3 (+15% Mut) + state.talents["1:8"] = 3 -- Puncturing Wounds 3/3 (+15% Mut) local result = Pipeline.Calculate(1329, state) -- 0.25 + 0.40 + 0.15 = 0.80 assert.is_near(0.80, result.critChance, 0.01) diff --git a/tests/test_rogue_talents.lua b/tests/test_rogue_talents.lua index 0a6496d..9d293f8 100644 --- a/tests/test_rogue_talents.lua +++ b/tests/test_rogue_talents.lua @@ -54,14 +54,14 @@ end describe("Rogue Talents", function() --------------------------------------------------------------------------- - -- 1. Improved Eviscerate (1:1) — +5%/rank DAMAGE_MULT on Eviscerate + -- 1. Improved Eviscerate (1:7) - +5%/rank DAMAGE_MULT on Eviscerate -- 3 ranks, additive --------------------------------------------------------------------------- describe("Improved Eviscerate", function() it("should increase Eviscerate damage by 15% at 3/3", function() local state = makeRogueState() - state.talents["1:1"] = 3 + state.talents["1:7"] = 3 local r = Pipeline.Calculate(2098, state) -- avg = 1345 * 1.15 = 1546.75 local expectedMin = 1285 * 1.15 @@ -72,7 +72,7 @@ describe("Rogue Talents", function() it("should increase Eviscerate damage by 5% at 1/3", function() local state = makeRogueState() - state.talents["1:1"] = 1 + state.talents["1:7"] = 1 local r = Pipeline.Calculate(2098, state) assert.is_near(1285 * 1.05, r.minDmg, 0.1) assert.is_near(1405 * 1.05, r.maxDmg, 0.1) @@ -80,7 +80,7 @@ describe("Rogue Talents", function() it("should not affect Sinister Strike", function() local state = makeRogueState() - state.talents["1:1"] = 3 + state.talents["1:7"] = 3 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) assert.is_near(683.86, r.maxDmg, 0.01) @@ -88,7 +88,7 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 2. Malice (1:3) — +1%/rank CRIT_BONUS global, 5 ranks + -- 2. Malice (1:3) - +1%/rank CRIT_BONUS global, 5 ranks --------------------------------------------------------------------------- describe("Malice", function() @@ -116,14 +116,14 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 3. Murder (1:5) — +1%/rank DAMAGE_MULT, 2 ranks, additive + -- 3. Murder (1:6) - +1%/rank DAMAGE_MULT, 2 ranks, additive -- filter = { creatureTypes = Humanoid/Giant/Beast/Dragonkin/Critter } --------------------------------------------------------------------------- describe("Murder", function() it("should increase damage by 2% vs Humanoid at 2/2", function() local state = makeRogueState() - state.talents["1:5"] = 2 + state.talents["1:6"] = 2 state.targetCreatureType = "Humanoid" local r = Pipeline.Calculate(1752, state) -- Sinister Strike -- SS min * 1.02 @@ -133,7 +133,7 @@ describe("Rogue Talents", function() it("should increase damage by 2% vs Beast at 2/2", function() local state = makeRogueState() - state.talents["1:5"] = 2 + state.talents["1:6"] = 2 state.targetCreatureType = "Beast" local r = Pipeline.Calculate(1752, state) assert.is_near(570.857 * 1.02, r.minDmg, 0.1) @@ -141,7 +141,7 @@ describe("Rogue Talents", function() it("should not apply vs Undead", function() local state = makeRogueState() - state.talents["1:5"] = 2 + state.talents["1:6"] = 2 state.targetCreatureType = "Undead" local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) @@ -150,21 +150,21 @@ describe("Rogue Talents", function() it("should not apply when no target creature type set", function() local state = makeRogueState() - state.talents["1:5"] = 2 + state.talents["1:6"] = 2 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) end) end) --------------------------------------------------------------------------- - -- 4. Puncturing Wounds (1:6) + -- 4. Puncturing Wounds (1:8) -- +10%/rank CRIT on Backstab, +5%/rank CRIT on Mutilate, 3 ranks --------------------------------------------------------------------------- describe("Puncturing Wounds", function() it("should add 30% crit to Backstab at 3/3", function() local state = makeDaggerRogueState() - state.talents["1:6"] = 3 + state.talents["1:8"] = 3 local r = Pipeline.Calculate(53, state) -- critChance = 0.25 + 0.30 = 0.55 assert.is_near(0.55, r.critChance, 0.001) @@ -172,7 +172,7 @@ describe("Rogue Talents", function() it("should add 15% crit to Mutilate at 3/3", function() local state = makeDaggerRogueState() - state.talents["1:6"] = 3 + state.talents["1:8"] = 3 local r = Pipeline.Calculate(1329, state) -- critChance = 0.25 + 0.15 = 0.40 assert.is_near(0.40, r.critChance, 0.001) @@ -180,21 +180,21 @@ describe("Rogue Talents", function() it("should not affect Sinister Strike", function() local state = makeRogueState() - state.talents["1:6"] = 3 + state.talents["1:8"] = 3 local r = Pipeline.Calculate(1752, state) assert.is_near(0.25, r.critChance, 0.001) end) end) --------------------------------------------------------------------------- - -- 5. Lethality (1:9) — +6%/rank CRIT_MULT_BONUS, 5 ranks + -- 5. Lethality (1:2) - +6%/rank CRIT_MULT_BONUS, 5 ranks -- filter on SS, Gouge, BS, Ghostly Strike, Mutilate, Shiv, Hemorrhage --------------------------------------------------------------------------- describe("Lethality", function() it("should increase crit multiplier by 0.30 on Sinister Strike at 5/5", function() local state = makeRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(1752, state) -- critMult = 2.0 + 0.30 = 2.30 assert.is_near(2.30, r.critMult, 0.001) @@ -202,70 +202,70 @@ describe("Rogue Talents", function() it("should increase crit multiplier by 0.30 on Backstab at 5/5", function() local state = makeDaggerRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(53, state) assert.is_near(2.30, r.critMult, 0.001) end) it("should increase crit multiplier by 0.30 on Gouge at 5/5", function() local state = makeRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(1776, state) assert.is_near(2.30, r.critMult, 0.001) end) it("should increase crit multiplier by 0.30 on Ghostly Strike at 5/5", function() local state = makeRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(14278, state) assert.is_near(2.30, r.critMult, 0.001) end) it("should increase crit multiplier by 0.30 on Mutilate at 5/5", function() local state = makeDaggerRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(1329, state) assert.is_near(2.30, r.critMult, 0.001) end) it("should increase crit multiplier by 0.30 on Shiv at 5/5", function() local state = makeRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(5938, state) assert.is_near(2.30, r.critMult, 0.001) end) it("should increase crit multiplier by 0.30 on Hemorrhage at 5/5", function() local state = makeRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(16511, state) assert.is_near(2.30, r.critMult, 0.001) end) it("should increase crit multiplier by 0.12 at 2/5", function() local state = makeRogueState() - state.talents["1:9"] = 2 + state.talents["1:2"] = 2 local r = Pipeline.Calculate(1752, state) assert.is_near(2.12, r.critMult, 0.001) end) it("should not affect Eviscerate", function() local state = makeRogueState() - state.talents["1:9"] = 5 + state.talents["1:2"] = 5 local r = Pipeline.Calculate(2098, state) assert.is_near(2.0, r.critMult, 0.001) end) end) --------------------------------------------------------------------------- - -- 6. Vile Poisons (1:10) — +4%/rank DAMAGE_MULT on poisons + Envenom + -- 6. Vile Poisons (1:15) - +4%/rank DAMAGE_MULT on poisons + Envenom -- 5 ranks, additive --------------------------------------------------------------------------- describe("Vile Poisons", function() it("should increase Instant Poison damage by 20% at 5/5", function() local state = makeRogueState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(8679, state) -- Instant Poison R7 -- min = 146 * 1.20 = 175.2, max = 194 * 1.20 = 232.8 assert.is_near(175.2, r.minDmg, 0.1) @@ -274,7 +274,7 @@ describe("Rogue Talents", function() it("should increase Deadly Poison damage by 20% at 5/5", function() local state = makeRogueState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(2823, state) -- Deadly Poison R7 -- totalDmg = 180 * 1.20 = 216 assert.is_near(216, r.totalDmg, 0.1) @@ -282,7 +282,7 @@ describe("Rogue Talents", function() it("should increase Wound Poison damage by 20% at 5/5", function() local state = makeRogueState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(13219, state) -- Wound Poison R5 -- min = max = 65 * 1.20 = 78 assert.is_near(78, r.minDmg, 0.1) @@ -291,7 +291,7 @@ describe("Rogue Talents", function() it("should increase Envenom damage by 20% at 5/5", function() local state = makeRogueState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(32645, state) -- Envenom R2 -- min = max = 1200 * 1.20 = 1440 assert.is_near(1440, r.minDmg, 1) @@ -300,7 +300,7 @@ describe("Rogue Talents", function() it("should increase Instant Poison damage by 8% at 2/5", function() local state = makeRogueState() - state.talents["1:10"] = 2 + state.talents["1:15"] = 2 local r = Pipeline.Calculate(8679, state) assert.is_near(146 * 1.08, r.minDmg, 0.1) assert.is_near(194 * 1.08, r.maxDmg, 0.1) @@ -308,7 +308,7 @@ describe("Rogue Talents", function() it("should not affect Sinister Strike", function() local state = makeRogueState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) assert.is_near(683.86, r.maxDmg, 0.01) @@ -316,13 +316,13 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 7. Precision (2:6) — +1%/rank SPELL_HIT_BONUS, 5 ranks + -- 7. Precision (2:1) - +1%/rank SPELL_HIT_BONUS, 5 ranks --------------------------------------------------------------------------- describe("Precision", function() it("should increase hit probability by 5% at 5/5", function() local state = makeRogueState() - state.talents["2:6"] = 5 + state.talents["2:1"] = 5 local r = Pipeline.Calculate(1752, state) -- Sinister Strike -- missChance = max(0, 0.08 - 0.05) = 0.03 -- dodgeChance = 0.065 @@ -339,7 +339,7 @@ describe("Rogue Talents", function() it("should increase hit probability by 3% at 3/5", function() local state = makeRogueState() - state.talents["2:6"] = 3 + state.talents["2:1"] = 3 local r = Pipeline.Calculate(1752, state) -- missChance = max(0, 0.08 - 0.03) = 0.05 -- hitProbability = 1 - 0.05 - 0.065 = 0.885 @@ -348,13 +348,13 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 8. Dagger Specialization (2:11) — +1%/rank CRIT_BONUS global, 5 ranks + -- 8. Dagger Specialization (2:2) - +1%/rank CRIT_BONUS global, 5 ranks --------------------------------------------------------------------------- describe("Dagger Specialization", function() it("should add 5% crit at 5/5", function() local state = makeRogueState() - state.talents["2:11"] = 5 + state.talents["2:2"] = 5 local r = Pipeline.Calculate(1752, state) -- Sinister Strike -- critChance = 0.25 + 0.05 = 0.30 assert.is_near(0.30, r.critChance, 0.001) @@ -362,27 +362,27 @@ describe("Rogue Talents", function() it("should add 3% crit at 3/5", function() local state = makeRogueState() - state.talents["2:11"] = 3 + state.talents["2:2"] = 3 local r = Pipeline.Calculate(1752, state) assert.is_near(0.28, r.critChance, 0.001) end) it("should apply to all melee abilities", function() local state = makeDaggerRogueState() - state.talents["2:11"] = 5 + state.talents["2:2"] = 5 local r = Pipeline.Calculate(53, state) -- Backstab assert.is_near(0.30, r.critChance, 0.001) end) end) --------------------------------------------------------------------------- - -- 9. Fist Weapon Specialization (2:16) — +1%/rank CRIT_BONUS global, 5 ranks + -- 9. Fist Weapon Specialization (2:3) - +1%/rank CRIT_BONUS global, 5 ranks --------------------------------------------------------------------------- describe("Fist Weapon Specialization", function() it("should add 5% crit at 5/5", function() local state = makeRogueState() - state.talents["2:16"] = 5 + state.talents["2:3"] = 5 local r = Pipeline.Calculate(1752, state) -- Sinister Strike -- critChance = 0.25 + 0.05 = 0.30 assert.is_near(0.30, r.critChance, 0.001) @@ -390,28 +390,28 @@ describe("Rogue Talents", function() it("should add 2% crit at 2/5", function() local state = makeRogueState() - state.talents["2:16"] = 2 + state.talents["2:3"] = 2 local r = Pipeline.Calculate(1752, state) assert.is_near(0.27, r.critChance, 0.001) end) it("should apply to all melee abilities", function() local state = makeDaggerRogueState() - state.talents["2:16"] = 5 + state.talents["2:3"] = 5 local r = Pipeline.Calculate(53, state) -- Backstab assert.is_near(0.30, r.critChance, 0.001) end) end) --------------------------------------------------------------------------- - -- 10. Aggression (2:19) — +2%/rank DAMAGE_MULT on SS/BS/Evis + -- 10. Aggression (2:17) - +2%/rank DAMAGE_MULT on SS/BS/Evis -- 3 ranks, additive --------------------------------------------------------------------------- describe("Aggression", function() it("should increase Sinister Strike damage by 6% at 3/3", function() local state = makeRogueState() - state.talents["2:19"] = 3 + state.talents["2:17"] = 3 local r = Pipeline.Calculate(1752, state) -- SS min = 570.857 * 1.06 = 605.108 assert.is_near(570.857 * 1.06, r.minDmg, 0.1) @@ -420,7 +420,7 @@ describe("Rogue Talents", function() it("should increase Backstab damage by 6% at 3/3", function() local state = makeDaggerRogueState() - state.talents["2:19"] = 3 + state.talents["2:17"] = 3 local r = Pipeline.Calculate(53, state) assert.is_near(896.786 * 1.06, r.minDmg, 0.1) assert.is_near(1027.286 * 1.06, r.maxDmg, 0.1) @@ -428,7 +428,7 @@ describe("Rogue Talents", function() it("should increase Eviscerate damage by 6% at 3/3", function() local state = makeRogueState() - state.talents["2:19"] = 3 + state.talents["2:17"] = 3 local r = Pipeline.Calculate(2098, state) -- Evis avg = 1345 * 1.06 = 1425.7 assert.is_near(1285 * 1.06, r.minDmg, 0.1) @@ -437,7 +437,7 @@ describe("Rogue Talents", function() it("should not affect Hemorrhage", function() local state = makeRogueState() - state.talents["2:19"] = 3 + state.talents["2:17"] = 3 local r = Pipeline.Calculate(16511, state) assert.is_near(520.14, r.minDmg, 0.01) assert.is_near(644.44, r.maxDmg, 0.01) @@ -445,14 +445,14 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 11. Surprise Attacks (2:24) — +10% DAMAGE_MULT on SS/BS/Shiv/Gouge + -- 11. Surprise Attacks (2:22) - +10% DAMAGE_MULT on SS/BS/Shiv/Gouge -- 1 rank, additive --------------------------------------------------------------------------- describe("Surprise Attacks", function() it("should increase Sinister Strike damage by 10% at 1/1", function() local state = makeRogueState() - state.talents["2:24"] = 1 + state.talents["2:22"] = 1 local r = Pipeline.Calculate(1752, state) -- SS min = 570.857 * 1.10 = 627.943 assert.is_near(570.857 * 1.10, r.minDmg, 0.1) @@ -461,7 +461,7 @@ describe("Rogue Talents", function() it("should increase Backstab damage by 10% at 1/1", function() local state = makeDaggerRogueState() - state.talents["2:24"] = 1 + state.talents["2:22"] = 1 local r = Pipeline.Calculate(53, state) assert.is_near(896.786 * 1.10, r.minDmg, 0.1) assert.is_near(1027.286 * 1.10, r.maxDmg, 0.1) @@ -469,7 +469,7 @@ describe("Rogue Talents", function() it("should increase Shiv damage by 10% at 1/1", function() local state = makeRogueState() - state.talents["2:24"] = 1 + state.talents["2:22"] = 1 local r = Pipeline.Calculate(5938, state) assert.is_near(472.857 * 1.10, r.minDmg, 0.1) assert.is_near(585.857 * 1.10, r.maxDmg, 0.1) @@ -477,7 +477,7 @@ describe("Rogue Talents", function() it("should increase Gouge damage by 10% at 1/1", function() local state = makeRogueState() - state.talents["2:24"] = 1 + state.talents["2:22"] = 1 local r = Pipeline.Calculate(1776, state) assert.is_near(105 * 1.10, r.minDmg, 0.1) assert.is_near(105 * 1.10, r.maxDmg, 0.1) @@ -485,7 +485,7 @@ describe("Rogue Talents", function() it("should not affect Eviscerate", function() local state = makeRogueState() - state.talents["2:24"] = 1 + state.talents["2:22"] = 1 local r = Pipeline.Calculate(2098, state) assert.is_near(1285, r.minDmg, 1) assert.is_near(1405, r.maxDmg, 1) @@ -493,14 +493,14 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 12. Opportunity (3:2) — +4%/rank DAMAGE_MULT on BS/Mutilate/Garrote/Ambush + -- 12. Opportunity (3:6) - +4%/rank DAMAGE_MULT on BS/Mutilate/Garrote/Ambush -- 5 ranks, additive --------------------------------------------------------------------------- describe("Opportunity", function() it("should increase Backstab damage by 20% at 5/5", function() local state = makeDaggerRogueState() - state.talents["3:2"] = 5 + state.talents["3:6"] = 5 local r = Pipeline.Calculate(53, state) -- BS R10 min = 896.786 * 1.20 = 1076.143 assert.is_near(896.786 * 1.20, r.minDmg, 0.1) @@ -509,7 +509,7 @@ describe("Rogue Talents", function() it("should increase Mutilate damage by 20% at 5/5", function() local state = makeDaggerRogueState() - state.talents["3:2"] = 5 + state.talents["3:6"] = 5 local r = Pipeline.Calculate(1329, state) assert.is_near(443.857 * 1.20, r.minDmg, 0.1) assert.is_near(530.857 * 1.20, r.maxDmg, 0.1) @@ -517,7 +517,7 @@ describe("Rogue Talents", function() it("should increase Garrote damage by 20% at 5/5", function() local state = makeRogueState() - state.talents["3:2"] = 5 + state.talents["3:6"] = 5 local r = Pipeline.Calculate(703, state) -- Garrote R8 totalDmg = 1170 * 1.20 = 1404 assert.is_near(1170 * 1.20, r.totalDmg, 1) @@ -525,7 +525,7 @@ describe("Rogue Talents", function() it("should increase Ambush damage by 20% at 5/5", function() local state = makeDaggerRogueState() - state.talents["3:2"] = 5 + state.talents["3:6"] = 5 local r = Pipeline.Calculate(8676, state) assert.is_near(1864.107 * 1.20, r.minDmg, 0.1) assert.is_near(2103.357 * 1.20, r.maxDmg, 0.1) @@ -533,7 +533,7 @@ describe("Rogue Talents", function() it("should not affect Sinister Strike", function() local state = makeRogueState() - state.talents["3:2"] = 5 + state.talents["3:6"] = 5 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) assert.is_near(683.86, r.maxDmg, 0.01) @@ -541,7 +541,7 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 13. Improved Ambush (3:8) — +15%/rank CRIT_BONUS on Ambush, 3 ranks + -- 13. Improved Ambush (3:8) - +15%/rank CRIT_BONUS on Ambush, 3 ranks --------------------------------------------------------------------------- describe("Improved Ambush", function() @@ -570,14 +570,14 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 14. Serrated Blades (3:11) — +10%/rank DAMAGE_MULT on Rupture + -- 14. Serrated Blades (3:14) - +10%/rank DAMAGE_MULT on Rupture -- 3 ranks, additive --------------------------------------------------------------------------- describe("Serrated Blades", function() it("should increase Rupture damage by 30% at 3/3", function() local state = makeRogueState() - state.talents["3:11"] = 3 + state.talents["3:14"] = 3 local r = Pipeline.Calculate(1943, state) -- Rupture R7 totalDmg = 1480 * 1.30 = 1924 assert.is_near(1480 * 1.30, r.totalDmg, 1) @@ -585,14 +585,14 @@ describe("Rogue Talents", function() it("should increase Rupture damage by 10% at 1/3", function() local state = makeRogueState() - state.talents["3:11"] = 1 + state.talents["3:14"] = 1 local r = Pipeline.Calculate(1943, state) assert.is_near(1480 * 1.10, r.totalDmg, 1) end) it("should not affect Sinister Strike", function() local state = makeRogueState() - state.talents["3:11"] = 3 + state.talents["3:14"] = 3 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) assert.is_near(683.86, r.maxDmg, 0.01) @@ -600,13 +600,13 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 15. Heightened Senses (3:12) — +2%/rank HIT, 2 ranks + -- 15. Heightened Senses (3:16) - +2%/rank HIT, 2 ranks --------------------------------------------------------------------------- describe("Heightened Senses", function() it("should increase hit probability by 4% at 2/2", function() local state = makeRogueState() - state.talents["3:12"] = 2 + state.talents["3:16"] = 2 local r = Pipeline.Calculate(1752, state) -- Sinister Strike -- missChance = max(0, 0.08 - 0.04) = 0.04 -- dodgeChance = 0.065 @@ -616,7 +616,7 @@ describe("Rogue Talents", function() it("should increase hit probability by 2% at 1/2", function() local state = makeRogueState() - state.talents["3:12"] = 1 + state.talents["3:16"] = 1 local r = Pipeline.Calculate(1752, state) -- missChance = max(0, 0.08 - 0.02) = 0.06 -- hitProbability = 1 - 0.06 - 0.065 = 0.875 @@ -625,14 +625,14 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 16. Dirty Deeds (3:14) — +10%/rank DAMAGE_MULT global, 2 ranks, + -- 16. Dirty Deeds (3:9) - +10%/rank DAMAGE_MULT global, 2 ranks, -- additive, targetHealthBelow = 0.35 --------------------------------------------------------------------------- describe("Dirty Deeds", function() it("should increase damage by 20% when target below 35% HP at 2/2", function() local state = makeRogueState() - state.talents["3:14"] = 2 + state.talents["3:9"] = 2 state.targetHealthPercent = 30 -- Below 35% local r = Pipeline.Calculate(1752, state) -- Sinister Strike -- SS min = 570.857 * 1.20 = 685.029 @@ -642,7 +642,7 @@ describe("Rogue Talents", function() it("should increase damage by 10% at 1/2 when target below 35%", function() local state = makeRogueState() - state.talents["3:14"] = 1 + state.talents["3:9"] = 1 state.targetHealthPercent = 20 local r = Pipeline.Calculate(1752, state) assert.is_near(570.857 * 1.10, r.minDmg, 0.1) @@ -651,7 +651,7 @@ describe("Rogue Talents", function() it("should not apply when target at 50% HP", function() local state = makeRogueState() - state.talents["3:14"] = 2 + state.talents["3:9"] = 2 state.targetHealthPercent = 50 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) @@ -660,7 +660,7 @@ describe("Rogue Talents", function() it("should not apply when target at exactly 35% HP", function() local state = makeRogueState() - state.talents["3:14"] = 2 + state.talents["3:9"] = 2 state.targetHealthPercent = 35 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) @@ -668,7 +668,7 @@ describe("Rogue Talents", function() it("should apply globally to all spells when target below 35%", function() local state = makeDaggerRogueState() - state.talents["3:14"] = 2 + state.talents["3:9"] = 2 state.targetHealthPercent = 20 local r = Pipeline.Calculate(53, state) -- Backstab assert.is_near(896.786 * 1.20, r.minDmg, 0.1) @@ -677,14 +677,14 @@ describe("Rogue Talents", function() end) --------------------------------------------------------------------------- - -- 17. Sinister Calling (3:21) — +1%/rank DAMAGE_MULT on BS/Hemo + -- 17. Sinister Calling (3:19) - +1%/rank DAMAGE_MULT on BS/Hemo -- 5 ranks, additive --------------------------------------------------------------------------- describe("Sinister Calling", function() it("should increase Backstab damage by 5% at 5/5", function() local state = makeDaggerRogueState() - state.talents["3:21"] = 5 + state.talents["3:19"] = 5 local r = Pipeline.Calculate(53, state) -- BS R10 min = 896.786 * 1.05 = 941.625 assert.is_near(896.786 * 1.05, r.minDmg, 0.1) @@ -693,7 +693,7 @@ describe("Rogue Talents", function() it("should increase Hemorrhage damage by 5% at 5/5", function() local state = makeRogueState() - state.talents["3:21"] = 5 + state.talents["3:19"] = 5 local r = Pipeline.Calculate(16511, state) -- Hemo R4 min = 520.143 * 1.05 = 546.150 assert.is_near(520.143 * 1.05, r.minDmg, 0.1) @@ -702,7 +702,7 @@ describe("Rogue Talents", function() it("should increase Backstab damage by 2% at 2/5", function() local state = makeDaggerRogueState() - state.talents["3:21"] = 2 + state.talents["3:19"] = 2 local r = Pipeline.Calculate(53, state) assert.is_near(896.786 * 1.02, r.minDmg, 0.1) assert.is_near(1027.286 * 1.02, r.maxDmg, 0.1) @@ -710,7 +710,7 @@ describe("Rogue Talents", function() it("should not affect Sinister Strike", function() local state = makeRogueState() - state.talents["3:21"] = 5 + state.talents["3:19"] = 5 local r = Pipeline.Calculate(1752, state) assert.is_near(570.86, r.minDmg, 0.01) assert.is_near(683.86, r.maxDmg, 0.01) @@ -724,8 +724,8 @@ describe("Rogue Talents", function() it("Aggression + Surprise Attacks should stack additively on SS", function() local state = makeRogueState() - state.talents["2:19"] = 3 -- Aggression +6% - state.talents["2:24"] = 1 -- Surprise Attacks +10% + state.talents["2:17"] = 3 -- Aggression +6% + state.talents["2:22"] = 1 -- Surprise Attacks +10% local r = Pipeline.Calculate(1752, state) -- Total additive: 1 + 0.06 + 0.10 = 1.16 assert.is_near(570.857 * 1.16, r.minDmg, 0.1) @@ -734,8 +734,8 @@ describe("Rogue Talents", function() it("Opportunity + Sinister Calling should stack additively on BS", function() local state = makeDaggerRogueState() - state.talents["3:2"] = 5 -- Opportunity +20% - state.talents["3:21"] = 5 -- Sinister Calling +5% + state.talents["3:6"] = 5 -- Opportunity +20% + state.talents["3:19"] = 5 -- Sinister Calling +5% local r = Pipeline.Calculate(53, state) -- Total additive: 1 + 0.20 + 0.05 = 1.25 assert.is_near(896.786 * 1.25, r.minDmg, 0.1) @@ -745,7 +745,7 @@ describe("Rogue Talents", function() it("Malice + Puncturing Wounds should stack crit on BS", function() local state = makeDaggerRogueState() state.talents["1:3"] = 5 -- Malice +5% crit - state.talents["1:6"] = 3 -- Puncturing Wounds +30% BS crit + state.talents["1:8"] = 3 -- Puncturing Wounds +30% BS crit local r = Pipeline.Calculate(53, state) -- critChance = 0.25 + 0.05 + 0.30 = 0.60 assert.is_near(0.60, r.critChance, 0.001) @@ -753,8 +753,8 @@ describe("Rogue Talents", function() it("Improved Eviscerate + Aggression should stack additively on Evis", function() local state = makeRogueState() - state.talents["1:1"] = 3 -- Imp Evis +15% - state.talents["2:19"] = 3 -- Aggression +6% + state.talents["1:7"] = 3 -- Imp Evis +15% + state.talents["2:17"] = 3 -- Aggression +6% local r = Pipeline.Calculate(2098, state) -- Total additive: 1 + 0.15 + 0.06 = 1.21 assert.is_near(1285 * 1.21, r.minDmg, 0.1) @@ -763,9 +763,9 @@ describe("Rogue Talents", function() it("Opportunity + Aggression + Surprise Attacks should stack on BS", function() local state = makeDaggerRogueState() - state.talents["3:2"] = 5 -- Opportunity +20% - state.talents["2:19"] = 3 -- Aggression +6% - state.talents["2:24"] = 1 -- Surprise Attacks +10% + state.talents["3:6"] = 5 -- Opportunity +20% + state.talents["2:17"] = 3 -- Aggression +6% + state.talents["2:22"] = 1 -- Surprise Attacks +10% local r = Pipeline.Calculate(53, state) -- Total additive: 1 + 0.20 + 0.06 + 0.10 = 1.36 assert.is_near(896.786 * 1.36, r.minDmg, 0.1) @@ -774,8 +774,8 @@ describe("Rogue Talents", function() it("Precision + Heightened Senses should stack hit on SS", function() local state = makeRogueState() - state.talents["2:6"] = 5 -- Precision +5% hit - state.talents["3:12"] = 2 -- Heightened Senses +4% hit + state.talents["2:1"] = 5 -- Precision +5% hit + state.talents["3:16"] = 2 -- Heightened Senses +4% hit local r = Pipeline.Calculate(1752, state) -- missChance = max(0, 0.08 - 0.09) = 0 -- hitProbability = 1 - 0 - 0.065 = 0.935 @@ -785,7 +785,7 @@ describe("Rogue Talents", function() it("Malice + Dagger Spec should stack crit globally", function() local state = makeRogueState() state.talents["1:3"] = 5 -- Malice +5% crit - state.talents["2:11"] = 5 -- Dagger Spec +5% crit + state.talents["2:2"] = 5 -- Dagger Spec +5% crit local r = Pipeline.Calculate(1752, state) -- critChance = 0.25 + 0.05 + 0.05 = 0.35 assert.is_near(0.35, r.critChance, 0.001) diff --git a/tests/test_warrior_talents.lua b/tests/test_warrior_talents.lua index 92b585e..ed5fbe9 100644 --- a/tests/test_warrior_talents.lua +++ b/tests/test_warrior_talents.lua @@ -33,7 +33,7 @@ describe("Warrior Talents", function() it("should increase Rend damage by 75% at 3/3", function() local state = makeWarriorState() - state.talents["1:3"] = 3 + state.talents["1:6"] = 3 local r = Pipeline.Calculate(772, state) -- totalDmg = 223.05 * (1 + 3 * 0.25) = 223.05 * 1.75 assert.is_near(223.05 * 1.75, r.totalDmg, 0.1) @@ -41,7 +41,7 @@ describe("Warrior Talents", function() it("should not affect Mortal Strike", function() local state = makeWarriorState() - state.talents["1:3"] = 3 + state.talents["1:6"] = 3 local r = Pipeline.Calculate(12294, state) assert.is_near(881.43, r.minDmg, 0.01) assert.is_near(1031.43, r.maxDmg, 0.01) @@ -52,7 +52,7 @@ describe("Warrior Talents", function() it("should add 50% crit chance to Overpower at 2/2", function() local state = makeWarriorState() - state.talents["1:7"] = 2 + state.talents["1:10"] = 2 local r = Pipeline.Calculate(7384, state) -- critChance = 0.25 + 2 * 0.25 = 0.75 assert.is_near(0.75, r.critChance, 0.001) @@ -60,7 +60,7 @@ describe("Warrior Talents", function() it("should not affect Mortal Strike crit", function() local state = makeWarriorState() - state.talents["1:7"] = 2 + state.talents["1:10"] = 2 local r = Pipeline.Calculate(12294, state) assert.is_near(0.25, r.critChance, 0.001) end) @@ -70,7 +70,7 @@ describe("Warrior Talents", function() it("should increase Mortal Strike damage by 5% at 5/5", function() local state = makeWarriorState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(12294, state) -- damage * (1 + 5 * 0.01) = damage * 1.05 assert.is_near(881.43 * 1.05, r.minDmg, 0.1) @@ -79,7 +79,7 @@ describe("Warrior Talents", function() it("should apply to all melee abilities (no weapon filter yet)", function() local state = makeWarriorState() - state.talents["1:10"] = 5 + state.talents["1:15"] = 5 local r = Pipeline.Calculate(7384, state) -- Overpower assert.is_near(706.43 * 1.05, r.minDmg, 0.1) assert.is_near(856.43 * 1.05, r.maxDmg, 0.1) @@ -90,7 +90,7 @@ describe("Warrior Talents", function() it("should increase crit multiplier by 0.20 at 2/2", function() local state = makeWarriorState() - state.talents["1:11"] = 2 + state.talents["1:18"] = 2 local r = Pipeline.Calculate(12294, state) -- critMult = 2.0 + 2 * 0.10 = 2.20 assert.is_near(2.20, r.critMult, 0.001) @@ -98,7 +98,7 @@ describe("Warrior Talents", function() it("should increase crit multiplier by 0.10 at 1/2", function() local state = makeWarriorState() - state.talents["1:11"] = 1 + state.talents["1:18"] = 1 local r = Pipeline.Calculate(12294, state) assert.is_near(2.10, r.critMult, 0.001) end) @@ -108,7 +108,7 @@ describe("Warrior Talents", function() it("should add 5% crit chance at 5/5", function() local state = makeWarriorState() - state.talents["1:12"] = 5 + state.talents["1:11"] = 5 local r = Pipeline.Calculate(12294, state) -- critChance = 0.25 + 5 * 0.01 = 0.30 assert.is_near(0.30, r.critChance, 0.001) @@ -116,7 +116,7 @@ describe("Warrior Talents", function() it("should apply to all melee abilities", function() local state = makeWarriorState() - state.talents["1:12"] = 5 + state.talents["1:11"] = 5 local r = Pipeline.Calculate(7384, state) -- Overpower assert.is_near(0.30, r.critChance, 0.001) end) @@ -126,7 +126,7 @@ describe("Warrior Talents", function() it("should increase Mortal Strike damage by 5% at 5/5", function() local state = makeWarriorState() - state.talents["1:22"] = 5 + state.talents["1:23"] = 5 local r = Pipeline.Calculate(12294, state) -- damage * (1 + 5 * 0.01) = damage * 1.05 assert.is_near(881.43 * 1.05, r.minDmg, 0.1) @@ -135,7 +135,7 @@ describe("Warrior Talents", function() it("should not affect Overpower", function() local state = makeWarriorState() - state.talents["1:22"] = 5 + state.talents["1:23"] = 5 local r = Pipeline.Calculate(7384, state) assert.is_near(706.43, r.minDmg, 0.01) assert.is_near(856.43, r.maxDmg, 0.01) @@ -149,7 +149,7 @@ describe("Warrior Talents", function() it("should add 5% melee crit at 5/5", function() local state = makeWarriorState() - state.talents["2:2"] = 5 + state.talents["2:4"] = 5 local r = Pipeline.Calculate(12294, state) -- critChance = 0.25 + 5 * 0.01 = 0.30 assert.is_near(0.30, r.critChance, 0.001) @@ -157,7 +157,7 @@ describe("Warrior Talents", function() it("should apply to all melee abilities", function() local state = makeWarriorState() - state.talents["2:2"] = 5 + state.talents["2:4"] = 5 local r = Pipeline.Calculate(7384, state) -- Overpower assert.is_near(0.30, r.critChance, 0.001) end) @@ -167,7 +167,7 @@ describe("Warrior Talents", function() it("should increase hit probability at 3/3", function() local state = makeWarriorState() - state.talents["2:17"] = 3 + state.talents["2:19"] = 3 local r = Pipeline.Calculate(12294, state) -- missChance = max(0, 0.08 - 0.03) = 0.05 -- dodgeChance = 0.065 (no expertise) @@ -185,7 +185,7 @@ describe("Warrior Talents", function() describe("Improved Slam", function() - -- NOTE: Slam's raw cast time is reduced (1.5 → 0.5 at 2/2), but + -- NOTE: Slam's raw cast time is reduced (1.5 -> 0.5 at 2/2), but -- the final result.castTime is clamped to max(hastedCast, GCD). -- With 0% haste, GCD = 1.5s, so the effective time stays 1.5. -- To observe the reduction we need enough haste to push GCD below @@ -194,7 +194,7 @@ describe("Warrior Talents", function() it("should reduce effective cast time with high haste at 2/2", function() local state = makeWarriorState() state.talents["2:12"] = 2 - state.stats.meleeHaste = 1.0 -- 100% haste → GCD = 0.75 → clamped to 1.0 + state.stats.meleeHaste = 1.0 -- 100% haste -> GCD = 0.75 -> clamped to 1.0 local r = Pipeline.Calculate(1464, state) -- Raw cast = 1.5 - 1.0 = 0.5, hasted = 0.5 / 2.0 = 0.25 -- GCD = 1.5 / 2.0 = 0.75, clamped to 1.0 @@ -227,8 +227,8 @@ describe("Warrior Talents", function() it("Cruelty 5/5 + Impale 2/2 should both apply to Mortal Strike", function() local state = makeWarriorState() - state.talents["2:2"] = 5 -- Cruelty +5% crit - state.talents["1:11"] = 2 -- Impale +0.20 crit mult + state.talents["2:4"] = 5 -- Cruelty +5% crit + state.talents["1:18"] = 2 -- Impale +0.20 crit mult local r = Pipeline.Calculate(12294, state) assert.is_near(0.30, r.critChance, 0.001) assert.is_near(2.20, r.critMult, 0.001) @@ -236,8 +236,8 @@ describe("Warrior Talents", function() it("Two-Handed Weapon Spec + Improved MS should stack additively on MS", function() local state = makeWarriorState() - state.talents["1:10"] = 5 -- 2H Spec +5% - state.talents["1:22"] = 5 -- Imp MS +5% + state.talents["1:15"] = 5 -- 2H Spec +5% + state.talents["1:23"] = 5 -- Imp MS +5% local r = Pipeline.Calculate(12294, state) -- Both additive: damage * (1 + 0.05 + 0.05) = damage * 1.10 assert.is_near(881.43 * 1.10, r.minDmg, 0.1) @@ -246,8 +246,8 @@ describe("Warrior Talents", function() it("Poleaxe Spec + Cruelty should stack crit on Overpower", function() local state = makeWarriorState() - state.talents["1:12"] = 5 -- Poleaxe +5% crit - state.talents["2:2"] = 5 -- Cruelty +5% crit + state.talents["1:11"] = 5 -- Poleaxe +5% crit + state.talents["2:4"] = 5 -- Cruelty +5% crit local r = Pipeline.Calculate(7384, state) -- critChance = 0.25 + 0.05 + 0.05 = 0.35 assert.is_near(0.35, r.critChance, 0.001) From 68eb5126f61401b8ac11e53a272bad47faf9e1fc Mon Sep 17 00:00:00 2001 From: Lasse Nielsen <lasse@xerrion.dk> Date: Sun, 1 Mar 2026 21:28:31 +0100 Subject: [PATCH 2/3] fix: correct stale talent keys in Warlock tests and AuraMap (#21) Updated test files (test_modifiercalc, test_critcalc, test_new_spells, test_pipeline) to use reindexed Warlock TalentMap keys from PR #19. Fixed Master Demonologist talentAmplify.talentKey in AuraMap_Warlock from 2:16 (Soul Link) to 2:11 (Master Demonologist). --- Data/AuraMap_Warlock.lua | 4 ++-- tests/test_critcalc.lua | 4 ++-- tests/test_modifiercalc.lua | 26 +++++++++++++------------- tests/test_new_spells.lua | 10 +++++----- tests/test_pipeline.lua | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Data/AuraMap_Warlock.lua b/Data/AuraMap_Warlock.lua index 6aee2e3..881d17e 100644 --- a/Data/AuraMap_Warlock.lua +++ b/Data/AuraMap_Warlock.lua @@ -127,7 +127,7 @@ AuraMap[23761] = { { type = MOD.DAMAGE_MULTIPLIER, value = 0 }, }, talentAmplify = { - talentKey = "2:16", + talentKey = "2:11", perRank = 0.02, effectType = MOD.DAMAGE_MULTIPLIER, }, @@ -142,7 +142,7 @@ AuraMap[35702] = { { type = MOD.DAMAGE_MULTIPLIER, value = 0 }, }, talentAmplify = { - talentKey = "2:16", + talentKey = "2:11", perRank = 0.01, effectType = MOD.DAMAGE_MULTIPLIER, }, diff --git a/tests/test_critcalc.lua b/tests/test_critcalc.lua index 09b55d8..15def67 100644 --- a/tests/test_critcalc.lua +++ b/tests/test_critcalc.lua @@ -107,8 +107,8 @@ describe("CritCalc", function() it("should clamp crit chance to maximum of 1.0", function() playerState.stats.spellCrit[32] = 0.95 -- Add more crit via Devastation 5/5 + Backlash 3/3 = 0.08 - playerState.talents["3:7"] = 5 - playerState.talents["3:15"] = 3 + playerState.talents["3:11"] = 5 + playerState.talents["3:21"] = 3 local result = runFullChain(686, playerState) -- Total: 0.95 + 0.05 + 0.03 = 1.03, clamped to 1.0 diff --git a/tests/test_modifiercalc.lua b/tests/test_modifiercalc.lua index 1afe70c..a244e7f 100644 --- a/tests/test_modifiercalc.lua +++ b/tests/test_modifiercalc.lua @@ -163,7 +163,7 @@ describe("ModifierCalc", function() --------------------------------------------------------------------------- describe("ApplyModifiers with talents", function() it("should apply Shadow Mastery 5/5 as +0.10 talentDamageBonus on Shadow Bolt", function() - playerState.talents["1:15"] = 5 + playerState.talents["1:11"] = 5 local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -175,7 +175,7 @@ describe("ModifierCalc", function() end) it("should scale Shadow Mastery linearly with rank", function() - playerState.talents["1:15"] = 3 + playerState.talents["1:11"] = 3 local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -187,7 +187,7 @@ describe("ModifierCalc", function() end) it("should apply Contagion 5/5 as +0.05 talentDamageBonus on Corruption", function() - playerState.talents["1:16"] = 5 + playerState.talents["1:18"] = 5 local spellData = ns.SpellData[172] local rankData = spellData.ranks[8] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -199,8 +199,8 @@ describe("ModifierCalc", function() end) it("should stack SM 5/5 + Contagion 5/5 additively on Corruption", function() - playerState.talents["1:15"] = 5 -- +10% Shadow - playerState.talents["1:16"] = 5 -- +5% Corruption + playerState.talents["1:11"] = 5 -- +10% Shadow + playerState.talents["1:18"] = 5 -- +5% Corruption local spellData = ns.SpellData[172] local rankData = spellData.ranks[8] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -212,7 +212,7 @@ describe("ModifierCalc", function() end) it("should not apply Contagion to Shadow Bolt (name mismatch)", function() - playerState.talents["1:16"] = 5 + playerState.talents["1:18"] = 5 local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -224,7 +224,7 @@ describe("ModifierCalc", function() end) it("should apply Emberstorm 5/5 as +0.10 talentDamageBonus on Searing Pain", function() - playerState.talents["3:14"] = 5 + playerState.talents["3:8"] = 5 local spellData = ns.SpellData[5676] local rankData = spellData.ranks[8] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -236,7 +236,7 @@ describe("ModifierCalc", function() end) it("should not apply Emberstorm to Shadow spells", function() - playerState.talents["3:14"] = 5 + playerState.talents["3:8"] = 5 local spellData = ns.SpellData[686] -- Shadow Bolt (Shadow) local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -248,7 +248,7 @@ describe("ModifierCalc", function() end) it("should apply Devastation 5/5 as +0.05 critBonus on Shadow Bolt", function() - playerState.talents["3:7"] = 5 + playerState.talents["3:11"] = 5 local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -260,7 +260,7 @@ describe("ModifierCalc", function() end) it("should apply Bane 5/5 as -0.5 castTimeReduction on Shadow Bolt", function() - playerState.talents["3:3"] = 5 + playerState.talents["3:2"] = 5 local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -273,7 +273,7 @@ describe("ModifierCalc", function() end) it("should apply Improved Searing Pain rank 3 as +0.10 critBonus", function() - playerState.talents["3:11"] = 3 + playerState.talents["3:7"] = 3 local spellData = ns.SpellData[5676] local rankData = spellData.ranks[8] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -285,7 +285,7 @@ describe("ModifierCalc", function() end) it("should apply Ruin as +0.5 critMultBonus on Shadow Bolt", function() - playerState.talents["3:13"] = 1 + playerState.talents["3:9"] = 1 local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) @@ -297,7 +297,7 @@ describe("ModifierCalc", function() end) it("should apply totalDamage multiplier from Shadow Mastery to modified result", function() - playerState.talents["1:15"] = 5 -- +10% Shadow damage + playerState.talents["1:11"] = 5 -- +10% Shadow damage local spellData = ns.SpellData[686] local rankData = spellData.ranks[11] local baseResult = SpellCalc.ComputeBase(spellData, rankData, playerState) diff --git a/tests/test_new_spells.lua b/tests/test_new_spells.lua index 7fd8d2e..24ebff3 100644 --- a/tests/test_new_spells.lua +++ b/tests/test_new_spells.lua @@ -116,14 +116,14 @@ describe("Shadowfury", function() end) it("Devastation 5/5 should add +5% crit to Shadowfury", function() - playerState.talents["3:7"] = 5 + playerState.talents["3:11"] = 5 local result = Pipeline.Calculate(30283, playerState) -- Base crit = 0.10, Devastation +0.05 = 0.15 assert.is_near(0.15, result.critChance, 0.001) end) it("Ruin 1/1 should add +0.5 crit multiplier", function() - playerState.talents["3:13"] = 1 + playerState.talents["3:9"] = 1 local result = Pipeline.Calculate(30283, playerState) -- Base crit multiplier = 1.5, Ruin +0.5 = 2.0 assert.is_near(2.0, result.critMultiplier, 0.001) @@ -154,8 +154,8 @@ describe("Shadowfury", function() -- Without talents local r1 = Pipeline.Calculate(30283, playerState) -- With talents - playerState.talents["3:7"] = 5 -- Devastation: +5% crit - playerState.talents["3:13"] = 1 -- Ruin: +0.5 crit mult + playerState.talents["3:11"] = 5 -- Devastation: +5% crit + playerState.talents["3:9"] = 1 -- Ruin: +0.5 crit mult local r2 = Pipeline.Calculate(30283, playerState) assert.is_true(r2.expectedDamage > r1.expectedDamage) assert.is_near(0.15, r2.critChance, 0.001) @@ -212,7 +212,7 @@ describe("Curse of Doom", function() it("Shadow Mastery 5/5 should boost damage by +10%", function() playerState.stats.spellPower[32] = 0 -- isolate talent effect - playerState.talents["1:15"] = 5 + playerState.talents["1:11"] = 5 local result = Pipeline.Calculate(603, playerState) -- baseDmg = 4200, +10% = 4620 assert.is_near(4620, result.damageAfterMods, 0.1) diff --git a/tests/test_pipeline.lua b/tests/test_pipeline.lua index c4ccfe5..73fe5c9 100644 --- a/tests/test_pipeline.lua +++ b/tests/test_pipeline.lua @@ -106,7 +106,7 @@ describe("Pipeline", function() it("should reflect talent bonuses in final DPS", function() local r1 = Pipeline.Calculate(686, playerState) - playerState.talents["1:15"] = 5 -- Shadow Mastery +10% + playerState.talents["1:11"] = 5 -- Shadow Mastery +10% local r2 = Pipeline.Calculate(686, playerState) assert.is_true(r2.dps > r1.dps) end) From 8f78bd984affdb22e5a94c33746ed0d4633ab290 Mon Sep 17 00:00:00 2001 From: Lasse Nielsen <lasse@xerrion.dk> Date: Sun, 1 Mar 2026 22:06:21 +0100 Subject: [PATCH 3/3] feat: beautify tooltip with school colors and spell identity (#21) - Extract shared Format module (colors, symbols, FormatNumber, FormatDPS) - Add spell name and rank header line with school-colored text - Restore labeled detail lines (Coeff, Cast, Talents, Stats, Breakdown) - Hide 0% crit for DoT-only spells - Use directCoefficient/dotCoefficient for safe hybrid display - Replace non-rendering Unicode escapes with ASCII alternatives - Deduplicate FormatNumber between Tooltip and ActionBar --- Engine/CritCalc.lua | 2 + PhDamage.toc | 1 + Presentation/ActionBar.lua | 15 +- Presentation/Diagnostics.lua | 18 +- Presentation/Format.lua | 77 +++++++ Presentation/Tooltip.lua | 398 +++++++++++++++++----------------- tests/test_format.lua | 204 +++++++++++++++++ tests/test_tooltip_format.lua | 175 +++------------ 8 files changed, 528 insertions(+), 362 deletions(-) create mode 100644 Presentation/Format.lua create mode 100644 tests/test_format.lua diff --git a/Engine/CritCalc.lua b/Engine/CritCalc.lua index 70b46ff..fb3ba62 100644 --- a/Engine/CritCalc.lua +++ b/Engine/CritCalc.lua @@ -335,6 +335,8 @@ function CritCalc.BuildHybridResult( spellType = spellData.spellType, avgBaseDamage = modResult.avgBaseDamage, coefficient = modResult.coefficient, + directCoefficient = modResult.directCoefficient, + dotCoefficient = modResult.dotCoefficient, spellPowerBonus = modResult.spellPowerBonus, damageBeforeMods = modResult.damageBeforeMods, damageAfterMods = directDmg + dotDmg, diff --git a/PhDamage.toc b/PhDamage.toc index ca27f1d..42d2668 100644 --- a/PhDamage.toc +++ b/PhDamage.toc @@ -29,6 +29,7 @@ Engine\CritCalc.lua Engine\Pipeline.lua # Presentation +Presentation\Format.lua Presentation\Diagnostics.lua Presentation\Tooltip.lua Presentation\ActionBar.lua diff --git a/Presentation/ActionBar.lua b/Presentation/ActionBar.lua index 3c95950..035ee01 100644 --- a/Presentation/ActionBar.lua +++ b/Presentation/ActionBar.lua @@ -17,8 +17,6 @@ local HasAction = HasAction local GetActionInfo = GetActionInfo local GetMacroSpell = GetMacroSpell local wipe = wipe -local format = string.format -local floor = math.floor ------------------------------------------------------------------------------- -- Module state @@ -49,18 +47,9 @@ local function BuildSpellIDMap() end ------------------------------------------------------------------------------- --- FormatNumber(n) --- Compact display: 10000 → "10.0k", 1500 → "1.5k", 581 → "581" +-- FormatNumber - delegated to shared formatting module ------------------------------------------------------------------------------- -local function FormatNumber(n) - if n >= 10000 then - return format("%.0fk", n / 1000) - elseif n >= 1000 then - return format("%.1fk", n / 1000) - else - return tostring(floor(n + 0.5)) - end -end +local FormatNumber = function(n) return ns.Format.FormatNumber(n) end ------------------------------------------------------------------------------- -- ResolveSpellID(button) diff --git a/Presentation/Diagnostics.lua b/Presentation/Diagnostics.lua index 03a8b96..598046c 100644 --- a/Presentation/Diagnostics.lua +++ b/Presentation/Diagnostics.lua @@ -87,7 +87,7 @@ function Diagnostics.PrintAll() -- Header local className = state.class or "Unknown" local level = state.level or "?" - Diagnostics.Print(COLOR_HEADER .. "PhDamage \226\128\148 " +Diagnostics.Print(COLOR_HEADER .. "PhDamage - " .. className .. " (Level " .. level .. ")" .. COLOR_RESET) Diagnostics.Print(COLOR_HEADER .. LINE_DOUBLE .. COLOR_RESET) @@ -110,7 +110,7 @@ function Diagnostics.PrintSpellSummary(r) -- Utility: health/pet mana → player mana local sourceStr if r.healthCost then - sourceStr = COLOR_VALUE .. FN(r.healthCost) .. COLOR_RESET .. " HP \226\134\146 " + sourceStr = COLOR_VALUE .. FN(r.healthCost) .. COLOR_RESET .. " HP -> " else sourceStr = "" end @@ -144,7 +144,7 @@ function Diagnostics.PrintSpellSummary(r) .. LabelValue("Direct", directBase .. " base + " .. FN(r.directSpBonus or r.spellPowerBonus) .. " SP") .. " | " .. LabelValue("Crit", (r.critChance or 0) > 0 - and (FP(r.critChance) .. " (\195\151" .. string.format("%.2f", r.critMultiplier or 0) .. ")") +and (FP(r.critChance) .. " (x" .. string.format("%.2f", r.critMultiplier or 0) .. ")") or "n/a") if r.armorReduction and r.armorReduction > 0 then hybridDetailLine = hybridDetailLine .. " | " .. LabelValue("Armor", @@ -188,7 +188,7 @@ function Diagnostics.PrintSpellSummary(r) ) local chanDetailLine = " " .. LabelValue("Base", FN(r.avgBaseDamage)) .. " | " .. LabelValue("+SP", FN(r.spellPowerBonus)) - .. " | " .. LabelValue("Ticks", (r.numTicks or "?") .. "\195\151" .. FN(r.tickDamage)) + .. " | " .. LabelValue("Ticks", (r.numTicks or "?") .. "x" .. FN(r.tickDamage)) if r.armorReduction and r.armorReduction > 0 then chanDetailLine = chanDetailLine .. " | " .. LabelValue("Armor", "-" .. string.format("%.1f%%", r.armorReduction * 100)) @@ -211,7 +211,7 @@ function Diagnostics.PrintSpellSummary(r) .. COLOR_GOOD .. FN(r.dps) .. " " .. rateLabel .. COLOR_RESET ) local critStr = (r.critChance or 0) > 0 - and (FP(r.critChance) .. " (\195\151" .. string.format("%.2f", r.critMultiplier or 0) .. ")") + and (FP(r.critChance) .. " (x" .. string.format("%.2f", r.critMultiplier or 0) .. ")") or "n/a" local detailLine = " " .. LabelValue("Base", FN(r.avgBaseDamage)) .. " | " .. LabelValue("+SP", FN(r.spellPowerBonus)) @@ -255,7 +255,7 @@ function Diagnostics.PrintSpell(spellName) Diagnostics.Print( COLOR_SPELL .. r.spellName .. COLOR_RESET .. (rankStr ~= "" and (" (" .. rankStr .. ")") or "") - .. " \226\128\148 " .. schoolColor .. schoolName .. COLOR_RESET + .. " - " .. schoolColor .. schoolName .. COLOR_RESET ) Diagnostics.Print(COLOR_LABEL .. LINE_SINGLE .. COLOR_RESET) @@ -349,7 +349,7 @@ function Diagnostics.PrintSpellDirect(r, FN, FP, schoolColor, schoolName, state) Diagnostics.Print(" " .. LabelValue(Noun .. " after mods", FN(r.damageAfterMods))) Diagnostics.Print(" " .. LabelValue("Crit chance", (r.critChance or 0) > 0 - and (FP(r.critChance) .. " (\195\151" .. string.format("%.2f", r.critMultiplier or 0) .. " multiplier)") +and (FP(r.critChance) .. " (x" .. string.format("%.2f", r.critMultiplier or 0) .. " multiplier)") or "n/a")) Diagnostics.Print(" " .. LabelValue("Expected " .. noun, COLOR_VALUE .. FN(r.expectedDamage) .. COLOR_RESET)) Diagnostics.Print(" " .. LabelValue("Hit chance", FP(r.hitChance))) @@ -440,7 +440,7 @@ function Diagnostics.PrintSpellHybrid(r, FN, FP, schoolColor, _schoolName, state end Diagnostics.Print(" " .. LabelValue("Crit chance", (r.critChance or 0) > 0 - and (FP(r.critChance) .. " (\195\151" .. string.format("%.2f", r.critMultiplier or 0) +and (FP(r.critChance) .. " (x" .. string.format("%.2f", r.critMultiplier or 0) .. " multiplier, direct only)") or "n/a")) Diagnostics.Print(" " .. LabelValue("Expected total", COLOR_VALUE .. FN(r.expectedDamage) .. COLOR_RESET)) @@ -488,7 +488,7 @@ function Diagnostics.PrintState() local FP = Diagnostics.FormatPercent -- Header - Diagnostics.Print(COLOR_HEADER .. "PhDamage \226\128\148 Player State" .. COLOR_RESET) + Diagnostics.Print(COLOR_HEADER .. "PhDamage - Player State" .. COLOR_RESET) Diagnostics.Print(COLOR_HEADER .. LINE_DOUBLE .. COLOR_RESET) -- Class and level diff --git a/Presentation/Format.lua b/Presentation/Format.lua new file mode 100644 index 0000000..b2d31d8 --- /dev/null +++ b/Presentation/Format.lua @@ -0,0 +1,77 @@ +------------------------------------------------------------------------------- +-- Format.lua +-- Shared formatting helpers for Presentation layer (Tooltip, ActionBar) +-- +-- Supported versions: Retail, MoP Classic, TBC Anniversary, Cata, Classic +------------------------------------------------------------------------------- + +local ADDON_NAME, ns = ... + +local format = string.format +local floor = math.floor + +local Format = {} +ns.Format = Format + +------------------------------------------------------------------------------- +-- Color Palette (shared across Tooltip and ActionBar) +------------------------------------------------------------------------------- +Format.COLOR_GOLD = "|cffd1a800" -- Headers, addon name +Format.COLOR_GREEN = "|cff00ff00" -- DPS values, positive modifiers +Format.COLOR_WHITE = "|cffffffff" -- Default stat values +Format.COLOR_LABEL = "|cffc0a060" -- Dim labels (Coeff, Cast, etc.) +Format.COLOR_RESET = "|r" + +------------------------------------------------------------------------------- +-- Symbol Constants (replace raw UTF-8 escape sequences) +------------------------------------------------------------------------------- +Format.MULTIPLY = "x" -- crit multiplier prefix +Format.ARROW = "->" -- used in utility spells (HP -> mana) +Format.BULLET = "-" -- list separator + +------------------------------------------------------------------------------- +-- FormatNumber(n) +-- Compact display: 10000+ -> "15k", 1000-9999 -> "1.5k", else integer "581" +------------------------------------------------------------------------------- +function Format.FormatNumber(n) + if n == nil then return "?" end + if n >= 10000 then + return format("%.0fk", n / 1000) + elseif n >= 1000 then + return format("%.1fk", n / 1000) + else + return tostring(floor(n + 0.5)) + end +end + +------------------------------------------------------------------------------- +-- FormatDPS(n) +-- DPS/HPS with one decimal place, "k" suffix for large values. +------------------------------------------------------------------------------- +function Format.FormatDPS(n) + if n == nil then return "?" end + if n >= 10000 then + return format("%.0fk", n / 1000) + elseif n >= 1000 then + return format("%.1fk", n / 1000) + else + return format("%.1f", n) + end +end + +------------------------------------------------------------------------------- +-- GetSchoolColor(school) +-- Returns the WoW color escape code for a spell school bitmask. +-- Falls back to white for unknown schools. +------------------------------------------------------------------------------- +function Format.GetSchoolColor(school) + return ns.SCHOOL_COLORS and ns.SCHOOL_COLORS[school] or Format.COLOR_WHITE +end + +------------------------------------------------------------------------------- +-- ColorValue(text, school) +-- Wraps a string in school color codes with reset. +------------------------------------------------------------------------------- +function Format.ColorValue(text, school) + return Format.GetSchoolColor(school) .. text .. Format.COLOR_RESET +end diff --git a/Presentation/Tooltip.lua b/Presentation/Tooltip.lua index dae7027..7bb1740 100644 --- a/Presentation/Tooltip.lua +++ b/Presentation/Tooltip.lua @@ -10,6 +10,26 @@ local _, ns = ... local Tooltip = {} ns.Tooltip = Tooltip +------------------------------------------------------------------------------- +-- Formatting Helpers (delegate to ns.Format, kept for backward compatibility) +------------------------------------------------------------------------------- + +function Tooltip.FormatNumber(n) + return ns.Format.FormatNumber(n) +end + +function Tooltip.FormatDPS(n) + return ns.Format.FormatDPS(n) +end + +function Tooltip.GetSchoolColor(school) + return ns.Format.GetSchoolColor(school) +end + +function Tooltip.ColorValue(text, school) + return ns.Format.ColorValue(text, school) +end + -- Cache WoW globals local GameTooltip = GameTooltip local CreateFrame = CreateFrame @@ -18,19 +38,18 @@ local format = string.format local floor = math.floor local concat = table.concat --- SpellID reverse lookup: rankSpellID → { spellKey, rankIndex } +-- Import shared formatting (populated by Format.lua, loaded before this file) +local Format -- forward-declared; resolved in init +local FN, FD -- FormatNumber / FormatDPS aliases +local COLOR_GOLD, COLOR_GREEN, COLOR_WHITE, COLOR_LABEL, COLOR_RESET +local MULTIPLY, ARROW + +-- SpellID reverse lookup: rankSpellID -> { spellKey, rankIndex } local spellIDMap = {} -- Re-entry guard: tracks the spellID last appended to avoid duplicate lines local lastTooltipSpellID = nil --- Color constants -local COLOR_GOLD = "|cffffd100" -local COLOR_GREEN = "|cff00ff00" -local COLOR_WHITE = "|cffffffff" -local COLOR_RESET = "|r" -local COLOR_LABEL = "|cffc0a060" -- Soft gold for labels - ------------------------------------------------------------------------------- -- Companion tooltip frame ------------------------------------------------------------------------------- @@ -105,54 +124,9 @@ local function FinalizeFrame() companionFrame:Show() end -------------------------------------------------------------------------------- --- Formatting Helpers (exposed on ns.Tooltip for testability) -------------------------------------------------------------------------------- - ---- Formats a number for compact tooltip display. --- >= 10000 → "15k", >= 1000 → "1.5k", otherwise → integer "581" -function Tooltip.FormatNumber(n) - if n == nil then return "?" end - if n >= 10000 then - return format("%.0fk", n / 1000) - elseif n >= 1000 then - return format("%.1fk", n / 1000) - else - return tostring(floor(n + 0.5)) - end -end - ---- Formats a DPS/HPS value with one decimal place. --- Uses the same "k" suffix logic for large values. -function Tooltip.FormatDPS(n) - if n == nil then return "?" end - if n >= 10000 then - return format("%.0fk", n / 1000) - elseif n >= 1000 then - return format("%.1fk", n / 1000) - else - return format("%.1f", n) - end -end - ---- Returns the color escape code for a spell school bitmask. --- Falls back to white for unknown schools. -function Tooltip.GetSchoolColor(school) - return ns.SCHOOL_COLORS and ns.SCHOOL_COLORS[school] or COLOR_WHITE -end - ---- Wraps a formatted string in school color codes. -function Tooltip.ColorValue(text, school) - return Tooltip.GetSchoolColor(school) .. text .. COLOR_RESET -end - --- Local aliases for brevity inside this file -local FN = Tooltip.FormatNumber -local FD = Tooltip.FormatDPS - ------------------------------------------------------------------------------- -- BuildSpellIDMap --- Creates a reverse lookup from rank-specific spellID → { spellKey, rankIndex }. +-- Creates a reverse lookup from rank-specific spellID -> { spellKey, rankIndex }. -- spellKey is the base spellID used as the key in ns.SpellData. ------------------------------------------------------------------------------- @@ -167,7 +141,7 @@ local function BuildSpellIDMap() end ------------------------------------------------------------------------------- --- GetSpellIDMap — expose the reverse lookup for ActionBar.lua to reuse +-- GetSpellIDMap -- expose the reverse lookup for ActionBar.lua to reuse ------------------------------------------------------------------------------- function Tooltip.GetSpellIDMap() @@ -175,7 +149,21 @@ function Tooltip.GetSpellIDMap() end ------------------------------------------------------------------------------- --- Output type helpers +-- Identity line -- spell name (school-colored) + rank +------------------------------------------------------------------------------- + +local function AddIdentityLine(r) + local schoolColor = ns.Format.GetSchoolColor(r.school) + local name = schoolColor .. (r.spellName or "Unknown") .. COLOR_RESET + local rank = "" + if r.rank then + rank = " " .. COLOR_GOLD .. "(Rank " .. r.rank .. ")" .. COLOR_RESET + end + AddLine(name .. rank) +end + +------------------------------------------------------------------------------- +-- Value line -- expected damage/DPS (the "money line") ------------------------------------------------------------------------------- local function GetValueLabel(outputType) @@ -190,70 +178,88 @@ local function GetRateLabel(outputType) else return "DPS" end end -local function GetPowerLabel(r) - -- Melee results have dodgeChance set by CritCalc - if r.dodgeChance ~= nil then return "AP" end - return "SP" +local function AddValueLine(r) + local valueLabel = GetValueLabel(r.outputType) + local rateLabel = GetRateLabel(r.outputType) + local schoolColor = ns.Format.GetSchoolColor(r.school) + local dmgStr = schoolColor .. FN(r.expectedDamageWithMiss) .. COLOR_RESET + local dpsStr = COLOR_GREEN .. FD(r.dps) .. " " .. rateLabel .. COLOR_RESET + + AddLine(format("%s %s (%s)", dmgStr, valueLabel, dpsStr)) end ------------------------------------------------------------------------------- --- Scaling line builder +-- Coefficient line -- labeled ------------------------------------------------------------------------------- -local function AddScalingLine(r) - -- Skip for utility spells +local function AddCoeffLine(r) if r.spellType == "utility" then return end - local isMelee = r.dodgeChance ~= nil - - -- Coefficient line (skip for melee) - if not isMelee then - if r.spellType == "hybrid" and r.directSpBonus and r.dotSpBonus then - local directCoeff = (r.spellPowerBonus or 0) > 0 - and (r.directSpBonus / r.spellPowerBonus) or 0 - local dotCoeff = (r.spellPowerBonus or 0) > 0 - and (r.dotSpBonus / r.spellPowerBonus) or 0 - AddLine(format(" %sCoeff:%s %s%.2f+%.2f%s", - COLOR_LABEL, COLOR_RESET, COLOR_WHITE, directCoeff, dotCoeff, COLOR_RESET)) - elseif r.coefficient then - AddLine(format(" %sCoeff:%s %s%.3f%s", - COLOR_LABEL, COLOR_RESET, COLOR_WHITE, r.coefficient, COLOR_RESET)) - end + if isMelee then return end -- melee has no coefficient + + local label = COLOR_LABEL .. "Coeff:" .. COLOR_RESET .. " " + if r.spellType == "hybrid" then + local dc = r.directCoefficient or 0 + local dotc = r.dotCoefficient or 0 + AddLine(" " .. label .. format("%s%.2f + %.2f%s", COLOR_WHITE, dc, dotc, COLOR_RESET)) + elseif r.coefficient then + AddLine(" " .. label .. format("%s%.3f%s", COLOR_WHITE, r.coefficient, COLOR_RESET)) end +end + +------------------------------------------------------------------------------- +-- Cast time line -- labeled +------------------------------------------------------------------------------- - -- Cast time line +local function FormatCastTime(r) if (r.baseCastTime or 0) <= 0 then - AddLine(format(" %sCast:%s %sinstant%s", - COLOR_LABEL, COLOR_RESET, COLOR_WHITE, COLOR_RESET)) + return "instant" elseif r.spellType == "channel" then - AddLine(format(" %sCast:%s %s%.1fs channel%s", - COLOR_LABEL, COLOR_RESET, COLOR_WHITE, r.castTime, COLOR_RESET)) + return format("%.1fs channel", r.castTime) else - AddLine(format(" %sCast:%s %s%.1fs%s", - COLOR_LABEL, COLOR_RESET, COLOR_WHITE, r.castTime, COLOR_RESET)) + return format("%.1fs", r.castTime) end +end - -- Talent damage bonus line - if (r.talentDamageBonus or 0) > 0 then - AddLine(format(" %sTalents:%s %s+%.0f%%%s", - COLOR_LABEL, COLOR_RESET, COLOR_GREEN, r.talentDamageBonus * 100, COLOR_RESET)) - end +local function AddCastLine(r) + if r.spellType == "utility" then return end + local label = COLOR_LABEL .. "Cast:" .. COLOR_RESET .. " " + AddLine(" " .. label .. COLOR_WHITE .. FormatCastTime(r) .. COLOR_RESET) end ------------------------------------------------------------------------------- --- Stats line builder +-- Talent line -- labeled, only shown when talentDamageBonus > 0 ------------------------------------------------------------------------------- +local function AddTalentLine(r) + if (r.talentDamageBonus or 0) <= 0 then return end + + local label = COLOR_LABEL .. "Talents:" .. COLOR_RESET .. " " + AddLine(" " .. label .. format("%s+%.0f%%%s", + COLOR_GREEN, r.talentDamageBonus * 100, COLOR_RESET)) +end + +------------------------------------------------------------------------------- +-- Stats line -- labeled, SP/AP + crit (hidden when 0%) + hit +------------------------------------------------------------------------------- + +local function GetPowerLabel(r) + if r.dodgeChance ~= nil then return "AP" end + return "SP" +end + local function AddStatsLine(r) + local label = COLOR_LABEL .. "Stats:" .. COLOR_RESET .. " " local powerLabel = GetPowerLabel(r) local parts = {} - parts[#parts + 1] = format("%s+%s%s %s", COLOR_WHITE, FN(r.spellPowerBonus or 0), COLOR_RESET, powerLabel) + parts[#parts + 1] = format("%s+%s%s %s", + COLOR_WHITE, FN(r.spellPowerBonus or 0), COLOR_RESET, powerLabel) if (r.critChance or 0) > 0 then - parts[#parts + 1] = format("%s%.1f%%%s crit (%s\195\151%.2f%s)", + parts[#parts + 1] = format("%s%.1f%%%s crit (%s%s%.2f%s)", COLOR_WHITE, r.critChance * 100, COLOR_RESET, - COLOR_WHITE, r.critMultiplier or 0, COLOR_RESET) + COLOR_WHITE, MULTIPLY, r.critMultiplier or 0, COLOR_RESET) end if r.hitChance then @@ -261,22 +267,28 @@ local function AddStatsLine(r) COLOR_WHITE, floor(r.hitChance * 100 + 0.5), COLOR_RESET) end - AddLine(format(" %sStats:%s ", COLOR_LABEL, COLOR_RESET) .. concat(parts, " | ")) + AddLine(" " .. label .. concat(parts, " | ")) end ---- Adds melee-specific stats as two lines (AP/crit, then hit/dodge/armor) +------------------------------------------------------------------------------- +-- Melee stats + avoidance lines -- labeled +------------------------------------------------------------------------------- + local function AddMeleeStatsLines(r) - -- Line 1: AP + crit + -- Line 1: Stats label with AP + crit + local statsLabel = COLOR_LABEL .. "Stats:" .. COLOR_RESET .. " " local parts1 = {} - parts1[#parts1 + 1] = format("%s+%s%s AP", COLOR_WHITE, FN(r.spellPowerBonus or 0), COLOR_RESET) + parts1[#parts1 + 1] = format("%s+%s%s AP", + COLOR_WHITE, FN(r.spellPowerBonus or 0), COLOR_RESET) if (r.critChance or 0) > 0 then - parts1[#parts1 + 1] = format("%s%.1f%%%s crit (%s\195\151%.2f%s)", + parts1[#parts1 + 1] = format("%s%.1f%%%s crit (%s%s%.2f%s)", COLOR_WHITE, r.critChance * 100, COLOR_RESET, - COLOR_WHITE, r.critMultiplier or 0, COLOR_RESET) + COLOR_WHITE, MULTIPLY, r.critMultiplier or 0, COLOR_RESET) end - AddLine(format(" %sStats:%s ", COLOR_LABEL, COLOR_RESET) .. concat(parts1, " | ")) + AddLine(" " .. statsLabel .. concat(parts1, " | ")) - -- Line 2: hit + dodge + parry + armor + -- Line 2: Avoidance label with hit + dodge + parry + armor + local avoidLabel = COLOR_LABEL .. "Avoidance:" .. COLOR_RESET .. " " local parts2 = {} if r.hitChance then parts2[#parts2 + 1] = format("%s%d%%%s hit", @@ -295,35 +307,65 @@ local function AddMeleeStatsLines(r) COLOR_WHITE, r.armorReduction * 100, COLOR_RESET) end if #parts2 > 0 then - AddLine(format(" %sAvoidance:%s ", COLOR_LABEL, COLOR_RESET) .. concat(parts2, " | ")) + AddLine(" " .. avoidLabel .. concat(parts2, " | ")) end end ------------------------------------------------------------------------------- --- Header line (shared across all damage/heal types) +-- Breakdown line -- labeled, for DoT and Channel spells +-- Shows tick damage, total damage, duration, and tick count ------------------------------------------------------------------------------- -local function AddHeaderLine(r) - local valueLabel = GetValueLabel(r.outputType) - local rateLabel = GetRateLabel(r.outputType) - local schoolColor = Tooltip.GetSchoolColor(r.school) - local dmgStr = schoolColor .. FN(r.expectedDamageWithMiss) .. COLOR_RESET - local dpsStr = COLOR_GREEN .. FD(r.dps) .. " " .. rateLabel .. COLOR_RESET +local function AddBreakdownLine(r) + local label = COLOR_LABEL .. "Breakdown:" .. COLOR_RESET .. " " + local sc = ns.Format.GetSchoolColor(r.school) + local tickStr = sc .. FN(r.tickDamage or r.tickDmg or 0) .. COLOR_RESET + local totalStr = sc .. FN(r.expectedDamageWithMiss or r.totalDmg or 0) .. COLOR_RESET + local tickCount = r.numTicks or 0 + local duration = r.duration or 0 + + AddLine(format(" %s%s/tick | %s total (%ds, %d ticks)", + label, tickStr, totalStr, duration, tickCount)) +end + +------------------------------------------------------------------------------- +-- Hybrid breakdown lines -- Direct + DoT sub-lines with labels +------------------------------------------------------------------------------- + +local function AddHybridBreakdownLines(r) + local sc = ns.Format.GetSchoolColor(r.school) + + -- Direct line + local directLabel = COLOR_LABEL .. "Direct:" .. COLOR_RESET .. " " + local directStr = sc .. FN(r.directDamage or 0) .. COLOR_RESET + local directParts = { directStr } + if (r.critChance or 0) > 0 then + directParts[#directParts + 1] = format("%s%.1f%%%s crit (%s%s%.2f%s)", + COLOR_WHITE, r.critChance * 100, COLOR_RESET, + COLOR_WHITE, MULTIPLY, r.critMultiplier or 0, COLOR_RESET) + end + AddLine(" " .. directLabel .. concat(directParts, " | ")) - AddLine( - format("%sPhDamage:%s %s %s (%s)", COLOR_GOLD, COLOR_RESET, dmgStr, valueLabel, dpsStr), - 1, 1, 1 - ) + -- DoT line + local dotLabel = COLOR_LABEL .. "DoT:" .. COLOR_RESET .. " " + local tickStr = sc .. FN(r.tickDamage or 0) .. COLOR_RESET + local dotTotalStr = sc .. FN(r.dotDamage or r.dotTotalDmg or 0) .. COLOR_RESET + local tickCount = r.numTicks or 0 + local duration = r.duration or 0 + AddLine(format(" %s%s/tick | %s total (%ds, %d ticks)", + dotLabel, tickStr, dotTotalStr, duration, tickCount)) end ------------------------------------------------------------------------------- -- Spell-type-specific line builders ------------------------------------------------------------------------------- ---- Direct damage/heal spell (3 lines, or 4 for melee) local function AddDirectLines(r) - AddHeaderLine(r) - AddScalingLine(r) + AddIdentityLine(r) + AddValueLine(r) + AddCoeffLine(r) + AddCastLine(r) + AddTalentLine(r) if r.dodgeChance ~= nil then AddMeleeStatsLines(r) else @@ -331,96 +373,49 @@ local function AddDirectLines(r) end end ---- DoT spell (4 lines) local function AddDotLines(r) - AddHeaderLine(r) - AddScalingLine(r) - - -- Tick info line - local sc = Tooltip.GetSchoolColor(r.school) - local tickStr = sc .. FN(r.tickDamage or r.tickDmg or 0) .. COLOR_RESET - local totalStr = sc .. FN(r.expectedDamageWithMiss or 0) .. COLOR_RESET - local durStr = format("%ds, %d ticks", r.duration or 0, r.numTicks or 0) - AddLine( - format(" %sBreakdown:%s %s/tick | %s total (%s)", COLOR_LABEL, COLOR_RESET, tickStr, totalStr, durStr) - ) - + AddIdentityLine(r) + AddValueLine(r) + AddCoeffLine(r) + AddCastLine(r) + AddTalentLine(r) + AddBreakdownLine(r) AddStatsLine(r) end ---- Hybrid spell (5 lines) local function AddHybridLines(r) - AddHeaderLine(r) - AddScalingLine(r) - - local sc = Tooltip.GetSchoolColor(r.school) - - -- Direct line - local directStr = sc .. FN(r.directDamage or 0) .. COLOR_RESET - local directParts = { format("%sDirect:%s %s", COLOR_LABEL, COLOR_RESET, directStr) } - if (r.critChance or 0) > 0 then - directParts[#directParts + 1] = format("%s%.1f%%%s crit (%s\195\151%.2f%s)", - COLOR_WHITE, r.critChance * 100, COLOR_RESET, - COLOR_WHITE, r.critMultiplier or 0, COLOR_RESET) - end - AddLine(" " .. concat(directParts, " | ")) - - -- DoT line - local tickStr = sc .. FN(r.tickDamage or 0) .. COLOR_RESET - local dotTotalStr = sc .. FN(r.dotDamage or 0) .. COLOR_RESET - local durStr = format("%ds, %d ticks", r.duration or 0, r.numTicks or 0) - AddLine( - format(" %sDoT:%s %s/tick | %s total (%s)", COLOR_LABEL, COLOR_RESET, tickStr, dotTotalStr, durStr) - ) - - -- Stats line (no crit — already shown on direct line) - local statParts = {} - statParts[#statParts + 1] = format("%s+%s%s SP", COLOR_WHITE, FN(r.spellPowerBonus or 0), COLOR_RESET) - if r.hitChance then - statParts[#statParts + 1] = format("%s%d%%%s hit", - COLOR_WHITE, floor(r.hitChance * 100 + 0.5), COLOR_RESET) - end - AddLine(format(" %sStats:%s ", COLOR_LABEL, COLOR_RESET) .. concat(statParts, " | ")) + AddIdentityLine(r) + AddValueLine(r) + AddCoeffLine(r) + AddCastLine(r) + AddTalentLine(r) + AddHybridBreakdownLines(r) + AddStatsLine(r) end ---- Channel spell (4 lines) local function AddChannelLines(r) - AddHeaderLine(r) - AddScalingLine(r) - - -- Tick info line - local sc = Tooltip.GetSchoolColor(r.school) - local tickStr = sc .. FN(r.tickDamage or r.tickDmg or 0) .. COLOR_RESET - local totalStr = sc .. FN(r.expectedDamageWithMiss or 0) .. COLOR_RESET - local durStr = format("%ds, %d ticks", r.duration or 0, r.numTicks or 0) - AddLine( - format(" %sBreakdown:%s %s/tick | %s total (%s)", COLOR_LABEL, COLOR_RESET, tickStr, totalStr, durStr) - ) - + AddIdentityLine(r) + AddValueLine(r) + AddCoeffLine(r) + AddCastLine(r) + AddTalentLine(r) + AddBreakdownLine(r) AddStatsLine(r) end ---- Utility spell (2 lines) local function AddUtilityLines(r) + AddIdentityLine(r) + local sc = ns.Format.GetSchoolColor(r.school) if r.healthCost then - -- Life Tap style: health cost → mana gain (+SP bonus) - AddLine( - format("%sPhDamage:%s %s HP \226\134\146 %s mana (%s+%s SP%s)", - COLOR_GOLD, COLOR_RESET, - FN(r.healthCost), - FN(r.manaGain), - COLOR_GREEN, FN(r.spellPowerBonus or 0), COLOR_RESET), - 1, 1, 1 - ) + AddLine(format(" %s HP %s %s mana (%s+%s SP%s)", + sc .. FN(r.healthCost) .. COLOR_RESET, + ARROW, + sc .. FN(r.manaGain) .. COLOR_RESET, + COLOR_GREEN, FN(r.spellPowerBonus or 0), COLOR_RESET)) else - -- Dark Pact style: mana gain only (+SP bonus) - AddLine( - format("%sPhDamage:%s %s mana (%s+%s SP%s)", - COLOR_GOLD, COLOR_RESET, - FN(r.manaGain), - COLOR_GREEN, FN(r.spellPowerBonus or 0), COLOR_RESET), - 1, 1, 1 - ) + AddLine(format(" %s mana (%s+%s SP%s)", + sc .. FN(r.manaGain) .. COLOR_RESET, + COLOR_GREEN, FN(r.spellPowerBonus or 0), COLOR_RESET)) end end @@ -478,7 +473,7 @@ local function OnTooltipSetSpell(tooltip) end ------------------------------------------------------------------------------- --- HookTooltip — attaches the tooltip hooks (called once during init) +-- HookTooltip -- attaches the tooltip hooks (called once during init) ------------------------------------------------------------------------------- local function HookTooltip() @@ -492,7 +487,7 @@ local function HookTooltip() end ------------------------------------------------------------------------------- --- Initialization — deferred to PLAYER_LOGIN to ensure all data is ready +-- Initialization -- deferred to PLAYER_LOGIN to ensure all data is ready ------------------------------------------------------------------------------- local initFrame = CreateFrame("Frame") @@ -500,6 +495,19 @@ initFrame:RegisterEvent("PLAYER_LOGIN") initFrame:SetScript("OnEvent", function(self, event) if event == "PLAYER_LOGIN" then self:UnregisterEvent("PLAYER_LOGIN") + + -- Resolve shared formatting references + Format = ns.Format + FN = Format.FormatNumber + FD = Format.FormatDPS + COLOR_GOLD = Format.COLOR_GOLD + COLOR_GREEN = Format.COLOR_GREEN + COLOR_WHITE = Format.COLOR_WHITE + COLOR_LABEL = Format.COLOR_LABEL + COLOR_RESET = Format.COLOR_RESET + MULTIPLY = Format.MULTIPLY + ARROW = Format.ARROW + BuildSpellIDMap() -- Create companion tooltip frame diff --git a/tests/test_format.lua b/tests/test_format.lua new file mode 100644 index 0000000..b2b0d89 --- /dev/null +++ b/tests/test_format.lua @@ -0,0 +1,204 @@ +------------------------------------------------------------------------------- +-- test_format.lua +-- Unit tests for the shared Format module (Presentation/Format.lua) +------------------------------------------------------------------------------- + +local bootstrap = require("tests.bootstrap") +local ns = bootstrap.ns + +------------------------------------------------------------------------------- +-- Load Format.lua into the shared namespace +------------------------------------------------------------------------------- + +local fn, err = loadfile("Presentation/Format.lua") +if not fn then error("Failed to load Presentation/Format.lua: " .. tostring(err)) end +fn("PhDamage", ns) + +local Format = ns.Format + +------------------------------------------------------------------------------- +-- Tests +------------------------------------------------------------------------------- + +describe("Format Module", function() + + describe("FormatNumber", function() + it("returns '?' for nil", function() + assert.are.equal("?", Format.FormatNumber(nil)) + end) + + it("returns '0' for 0", function() + assert.are.equal("0", Format.FormatNumber(0)) + end) + + it("returns '1' for 1", function() + assert.are.equal("1", Format.FormatNumber(1)) + end) + + it("returns '500' for 500", function() + assert.are.equal("500", Format.FormatNumber(500)) + end) + + it("returns '999' for 999", function() + assert.are.equal("999", Format.FormatNumber(999)) + end) + + it("returns '1.0k' for 1000", function() + assert.are.equal("1.0k", Format.FormatNumber(1000)) + end) + + it("returns '1.5k' for 1500", function() + assert.are.equal("1.5k", Format.FormatNumber(1500)) + end) + + it("returns '10.0k' for 9999 (rounds)", function() + assert.are.equal("10.0k", Format.FormatNumber(9999)) + end) + + it("returns '10k' for 10000", function() + assert.are.equal("10k", Format.FormatNumber(10000)) + end) + + it("returns '15k' for 15000", function() + assert.are.equal("15k", Format.FormatNumber(15000)) + end) + + it("returns '100k' for 100000", function() + assert.are.equal("100k", Format.FormatNumber(100000)) + end) + + it("returns '0' for 0.4 (floors to 0)", function() + assert.are.equal("0", Format.FormatNumber(0.4)) + end) + + it("returns '1' for 0.6 (rounds up)", function() + assert.are.equal("1", Format.FormatNumber(0.6)) + end) + end) + + describe("FormatDPS", function() + it("returns '?' for nil", function() + assert.are.equal("?", Format.FormatDPS(nil)) + end) + + it("returns '0.0' for 0", function() + assert.are.equal("0.0", Format.FormatDPS(0)) + end) + + it("returns '1.0' for 1", function() + assert.are.equal("1.0", Format.FormatDPS(1)) + end) + + it("returns '250.7' for 250.7", function() + assert.are.equal("250.7", Format.FormatDPS(250.7)) + end) + + it("returns '999.9' for 999.9", function() + assert.are.equal("999.9", Format.FormatDPS(999.9)) + end) + + it("returns '1.0k' for 1000", function() + assert.are.equal("1.0k", Format.FormatDPS(1000)) + end) + + it("returns '1.5k' for 1500", function() + assert.are.equal("1.5k", Format.FormatDPS(1500)) + end) + + it("returns '10k' for 10000", function() + assert.are.equal("10k", Format.FormatDPS(10000)) + end) + end) + + describe("GetSchoolColor", function() + it("returns correct color for Shadow", function() + assert.are.equal("|cff9b59b6", Format.GetSchoolColor(ns.SCHOOL_SHADOW)) + end) + + it("returns correct color for Fire", function() + assert.are.equal("|cffe74c3c", Format.GetSchoolColor(ns.SCHOOL_FIRE)) + end) + + it("returns correct color for Holy", function() + assert.are.equal("|cfff1c40f", Format.GetSchoolColor(ns.SCHOOL_HOLY)) + end) + + it("returns correct color for Nature", function() + assert.are.equal("|cff2ecc71", Format.GetSchoolColor(ns.SCHOOL_NATURE)) + end) + + it("returns correct color for Frost", function() + assert.are.equal("|cff3498db", Format.GetSchoolColor(ns.SCHOOL_FROST)) + end) + + it("returns correct color for Arcane", function() + assert.are.equal("|cff1abc9c", Format.GetSchoolColor(ns.SCHOOL_ARCANE)) + end) + + it("returns correct color for Physical", function() + assert.are.equal("|cffbdc3c7", Format.GetSchoolColor(ns.SCHOOL_PHYSICAL)) + end) + + it("returns white fallback for unknown school (255)", function() + assert.are.equal("|cffffffff", Format.GetSchoolColor(255)) + end) + + it("returns white fallback for nil", function() + assert.are.equal("|cffffffff", Format.GetSchoolColor(nil)) + end) + end) + + describe("ColorValue", function() + it("wraps text with Shadow school color", function() + assert.are.equal("|cff9b59b6500|r", Format.ColorValue("500", ns.SCHOOL_SHADOW)) + end) + + it("wraps text with Fire school color", function() + assert.are.equal("|cffe74c3c1.5k|r", Format.ColorValue("1.5k", ns.SCHOOL_FIRE)) + end) + + it("wraps text with Frost school color", function() + assert.are.equal("|cff3498db250|r", Format.ColorValue("250", ns.SCHOOL_FROST)) + end) + + it("falls back to white for unknown school", function() + assert.are.equal("|cffffffff100|r", Format.ColorValue("100", 255)) + end) + end) + + describe("Symbol Constants", function() + it("MULTIPLY is ASCII 'x'", function() + assert.are.equal("x", Format.MULTIPLY) + end) + + it("ARROW is ASCII '->'", function() + assert.are.equal("->", Format.ARROW) + end) + + it("BULLET is ASCII '-'", function() + assert.are.equal("-", Format.BULLET) + end) + end) + + describe("Color Constants", function() + it("COLOR_GOLD starts with |cff", function() + assert.truthy(Format.COLOR_GOLD:match("^|cff")) + end) + + it("COLOR_GREEN is green", function() + assert.are.equal("|cff00ff00", Format.COLOR_GREEN) + end) + + it("COLOR_WHITE is white", function() + assert.are.equal("|cffffffff", Format.COLOR_WHITE) + end) + + it("COLOR_LABEL starts with |cff", function() + assert.truthy(Format.COLOR_LABEL:match("^|cff")) + end) + + it("COLOR_RESET is |r", function() + assert.are.equal("|r", Format.COLOR_RESET) + end) + end) +end) diff --git a/tests/test_tooltip_format.lua b/tests/test_tooltip_format.lua index bae2959..14c1861 100644 --- a/tests/test_tooltip_format.lua +++ b/tests/test_tooltip_format.lua @@ -1,6 +1,6 @@ ------------------------------------------------------------------------------- -- test_tooltip_format.lua --- Unit tests for PhDamage Tooltip formatting helpers +-- Tests that Tooltip formatting wrappers correctly delegate to ns.Format ------------------------------------------------------------------------------- local bootstrap = require("tests.bootstrap") @@ -10,7 +10,6 @@ local ns = bootstrap.ns -- Stub WoW globals required by Tooltip.lua at load time ------------------------------------------------------------------------------- --- Minimal frame stub for CreateFrame("Frame") local function MakeFrameStub() return { RegisterEvent = function() end, @@ -23,7 +22,6 @@ _G.CreateFrame = function() return MakeFrameStub() end --- Minimal GameTooltip stub _G.GameTooltip = { HookScript = function() end, AddLine = function() end, @@ -32,170 +30,57 @@ _G.GameTooltip = { } ------------------------------------------------------------------------------- --- Load Tooltip.lua into the shared namespace +-- Load Format.lua + Tooltip.lua into the shared namespace ------------------------------------------------------------------------------- +local fnFmt, errFmt = loadfile("Presentation/Format.lua") +if not fnFmt then error("Failed to load Presentation/Format.lua: " .. tostring(errFmt)) end +fnFmt("PhDamage", ns) + local fn, err = loadfile("Presentation/Tooltip.lua") if not fn then error("Failed to load Presentation/Tooltip.lua: " .. tostring(err)) end fn("PhDamage", ns) local Tooltip = ns.Tooltip +local Format = ns.Format ------------------------------------------------------------------------------- -- Tests ------------------------------------------------------------------------------- -describe("Tooltip Formatting", function() - - describe("FormatNumber", function() - it("returns '?' for nil", function() - assert.are.equal("?", Tooltip.FormatNumber(nil)) - end) - - it("returns '0' for 0", function() - assert.are.equal("0", Tooltip.FormatNumber(0)) - end) - - it("returns '1' for 1", function() - assert.are.equal("1", Tooltip.FormatNumber(1)) - end) - - it("returns '500' for 500", function() - assert.are.equal("500", Tooltip.FormatNumber(500)) - end) - - it("returns '999' for 999", function() - assert.are.equal("999", Tooltip.FormatNumber(999)) - end) - - it("returns '1.0k' for 1000", function() - assert.are.equal("1.0k", Tooltip.FormatNumber(1000)) - end) - - it("returns '1.5k' for 1500", function() - assert.are.equal("1.5k", Tooltip.FormatNumber(1500)) - end) - - it("returns '10.0k' for 9999 (rounds)", function() - assert.are.equal("10.0k", Tooltip.FormatNumber(9999)) - end) - - it("returns '10k' for 10000", function() - assert.are.equal("10k", Tooltip.FormatNumber(10000)) - end) - - it("returns '15k' for 15000", function() - assert.are.equal("15k", Tooltip.FormatNumber(15000)) - end) - - it("returns '100k' for 100000", function() - assert.are.equal("100k", Tooltip.FormatNumber(100000)) - end) - - it("returns '0' for 0.4 (floors to 0)", function() - assert.are.equal("0", Tooltip.FormatNumber(0.4)) - end) - - it("returns '1' for 0.6 (rounds up)", function() - assert.are.equal("1", Tooltip.FormatNumber(0.6)) - end) +describe("Tooltip Wrapper Delegation", function() - it("returns '1000' for 999.5 (rounds to 1000)", function() - assert.are.equal("1000", Tooltip.FormatNumber(999.5)) + describe("FormatNumber delegates to Format", function() + it("returns same result as Format.FormatNumber", function() + assert.are.equal(Format.FormatNumber(nil), Tooltip.FormatNumber(nil)) + assert.are.equal(Format.FormatNumber(0), Tooltip.FormatNumber(0)) + assert.are.equal(Format.FormatNumber(500), Tooltip.FormatNumber(500)) + assert.are.equal(Format.FormatNumber(1500), Tooltip.FormatNumber(1500)) + assert.are.equal(Format.FormatNumber(15000), Tooltip.FormatNumber(15000)) end) end) - describe("FormatDPS", function() - it("returns '?' for nil", function() - assert.are.equal("?", Tooltip.FormatDPS(nil)) - end) - - it("returns '0.0' for 0", function() - assert.are.equal("0.0", Tooltip.FormatDPS(0)) - end) - - it("returns '1.0' for 1", function() - assert.are.equal("1.0", Tooltip.FormatDPS(1)) - end) - - it("returns '250.7' for 250.7", function() - assert.are.equal("250.7", Tooltip.FormatDPS(250.7)) - end) - - it("returns '999.9' for 999.9", function() - assert.are.equal("999.9", Tooltip.FormatDPS(999.9)) - end) - - it("returns '1.0k' for 1000", function() - assert.are.equal("1.0k", Tooltip.FormatDPS(1000)) - end) - - it("returns '1.5k' for 1500", function() - assert.are.equal("1.5k", Tooltip.FormatDPS(1500)) - end) - - it("returns '10k' for 10000", function() - assert.are.equal("10k", Tooltip.FormatDPS(10000)) + describe("FormatDPS delegates to Format", function() + it("returns same result as Format.FormatDPS", function() + assert.are.equal(Format.FormatDPS(nil), Tooltip.FormatDPS(nil)) + assert.are.equal(Format.FormatDPS(0), Tooltip.FormatDPS(0)) + assert.are.equal(Format.FormatDPS(250.7), Tooltip.FormatDPS(250.7)) + assert.are.equal(Format.FormatDPS(10000), Tooltip.FormatDPS(10000)) end) end) - describe("GetSchoolColor", function() - it("returns correct color for Shadow", function() - assert.are.equal("|cff9b59b6", Tooltip.GetSchoolColor(ns.SCHOOL_SHADOW)) - end) - - it("returns correct color for Fire", function() - assert.are.equal("|cffe74c3c", Tooltip.GetSchoolColor(ns.SCHOOL_FIRE)) - end) - - it("returns correct color for Holy", function() - assert.are.equal("|cfff1c40f", Tooltip.GetSchoolColor(ns.SCHOOL_HOLY)) - end) - - it("returns correct color for Nature", function() - assert.are.equal("|cff2ecc71", Tooltip.GetSchoolColor(ns.SCHOOL_NATURE)) - end) - - it("returns correct color for Frost", function() - assert.are.equal("|cff3498db", Tooltip.GetSchoolColor(ns.SCHOOL_FROST)) - end) - - it("returns correct color for Arcane", function() - assert.are.equal("|cff1abc9c", Tooltip.GetSchoolColor(ns.SCHOOL_ARCANE)) - end) - - it("returns correct color for Physical", function() - assert.are.equal("|cffbdc3c7", Tooltip.GetSchoolColor(ns.SCHOOL_PHYSICAL)) - end) - - it("returns white fallback for unknown school (255)", function() - assert.are.equal("|cffffffff", Tooltip.GetSchoolColor(255)) - end) - - it("returns white fallback for nil", function() - assert.are.equal("|cffffffff", Tooltip.GetSchoolColor(nil)) + describe("GetSchoolColor delegates to Format", function() + it("returns same result as Format.GetSchoolColor", function() + assert.are.equal(Format.GetSchoolColor(ns.SCHOOL_SHADOW), Tooltip.GetSchoolColor(ns.SCHOOL_SHADOW)) + assert.are.equal(Format.GetSchoolColor(nil), Tooltip.GetSchoolColor(nil)) + assert.are.equal(Format.GetSchoolColor(255), Tooltip.GetSchoolColor(255)) end) end) - describe("ColorValue", function() - it("wraps text with school color and reset code", function() - local result = Tooltip.ColorValue("500", ns.SCHOOL_SHADOW) - assert.are.equal("|cff9b59b6500|r", result) - end) - - it("wraps text with Fire school color", function() - local result = Tooltip.ColorValue("1.5k", ns.SCHOOL_FIRE) - assert.are.equal("|cffe74c3c1.5k|r", result) - end) - - it("wraps text with Frost school color", function() - local result = Tooltip.ColorValue("250", ns.SCHOOL_FROST) - assert.are.equal("|cff3498db250|r", result) - end) - - it("falls back to white for unknown school", function() - local result = Tooltip.ColorValue("100", 255) - assert.are.equal("|cffffffff100|r", result) + describe("ColorValue delegates to Format", function() + it("returns same result as Format.ColorValue", function() + assert.are.equal(Format.ColorValue("500", ns.SCHOOL_FIRE), Tooltip.ColorValue("500", ns.SCHOOL_FIRE)) + assert.are.equal(Format.ColorValue("100", 255), Tooltip.ColorValue("100", 255)) end) end) end)