From 38158d8b8fa8d8322524e57e50beca22214e3564 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 6 Dec 2025 14:19:14 -0500 Subject: [PATCH] warrior slam specific condditionals and update readme --- Conditionals.lua | 212 +++++++++++++++++++++++++++++++++++++++++++++++ Core.lua | 90 ++++++++++++++++++++ README.md | 88 ++++++++++++++++---- 3 files changed, 374 insertions(+), 16 deletions(-) diff --git a/Conditionals.lua b/Conditionals.lua index 8d35b66..512cccc 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -552,6 +552,188 @@ function CleveRoids.ValidateSwingTimer(operator, amount) return false end +-- Constants for Slam window calculations +local GCD_DURATION = 1.5 -- Global cooldown in seconds +local DEFAULT_SLAM_CAST = 1.5 -- Default Slam cast time in vanilla WoW + +-- Cache for Slam cast time from tooltip +local cachedSlamCastTime = nil +local slamCastTimeLastUpdate = 0 +local SLAM_CACHE_DURATION = 2 -- Re-scan tooltip every 2 seconds + +-- Hidden tooltip for scanning spell info +local SlamScanTooltip = nil + +-- Create hidden tooltip for scanning (once) +local function GetSlamScanTooltip() + if not SlamScanTooltip then + SlamScanTooltip = CreateFrame("GameTooltip", "CleveRoidsSlamScanTooltip", nil, "GameTooltipTemplate") + SlamScanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE") + end + return SlamScanTooltip +end + +-- Parse cast time from tooltip text (e.g., "1.5 sec cast" or "1.59 sec cast") +local function ParseCastTimeFromText(text) + if not text then return nil end + -- Match patterns like "1.5 sec cast", "1.59 sec cast", "2 sec cast" + local castTime = string.match(text, "(%d+%.?%d*) sec cast") + if castTime then + return tonumber(castTime) + end + return nil +end + +-- Get Slam's spellbook slot +local function GetSlamSpellSlot() + -- Search through spellbook for Slam + local i = 1 + while true do + local spellName, spellRank = GetSpellName(i, BOOKTYPE_SPELL) + if not spellName then break end + if spellName == "Slam" then + return i, BOOKTYPE_SPELL + end + i = i + 1 + end + return nil, nil +end +-- Expose for debug command +CleveRoids.GetSlamSpellSlot = GetSlamSpellSlot + +-- Scan Slam tooltip for cast time +local function ScanSlamCastTime() + local slot, bookType = GetSlamSpellSlot() + if not slot then return nil end + + local tooltip = GetSlamScanTooltip() + tooltip:ClearLines() + tooltip:SetSpell(slot, bookType) + + -- Scan tooltip lines for cast time + for i = 1, tooltip:NumLines() do + local leftText = getglobal("CleveRoidsSlamScanTooltipTextLeft" .. i) + if leftText then + local text = leftText:GetText() + local castTime = ParseCastTimeFromText(text) + if castTime then + return castTime + end + end + local rightText = getglobal("CleveRoidsSlamScanTooltipTextRight" .. i) + if rightText then + local text = rightText:GetText() + local castTime = ParseCastTimeFromText(text) + if castTime then + return castTime + end + end + end + + return nil +end + +-- Get Slam's cast time in seconds (with caching) +-- Reads from spellbook tooltip to get accurate cast time with haste/talents +function CleveRoids.GetSlamCastTime() + local now = GetTime() + + -- Use cached value if still valid + if cachedSlamCastTime and (now - slamCastTimeLastUpdate) < SLAM_CACHE_DURATION then + return cachedSlamCastTime + end + + -- Try to scan tooltip for cast time + local castTime = ScanSlamCastTime() + if castTime and castTime > 0 then + cachedSlamCastTime = castTime + slamCastTimeLastUpdate = now + return castTime + end + + -- Fall back to default + return DEFAULT_SLAM_CAST +end + +-- Force refresh of cached Slam cast time (call when buffs change) +function CleveRoids.RefreshSlamCastTime() + cachedSlamCastTime = nil + slamCastTimeLastUpdate = 0 +end + +-- Calculate maximum elapsed swing timer % to cast Slam without clipping auto-attack +-- Formula: MaxSlamPercent = (SwingTimer - SlamCastTime) / SwingTimer * 100 +function CleveRoids.GetSlamWindowPercent() + local attackSpeed = UnitAttackSpeed("player") + if not attackSpeed or attackSpeed <= 0 then return 0 end + + local slamCastTime = CleveRoids.GetSlamCastTime() + local maxSlamStart = attackSpeed - slamCastTime + + if maxSlamStart <= 0 then return 0 end -- Slam cast time exceeds swing timer + + return (maxSlamStart / attackSpeed) * 100 +end + +-- Calculate maximum elapsed swing timer % to cast instant without clipping NEXT Slam +-- Scenario: No Slam this swing, cast instant, then Slam next swing without clipping +-- Formula: MaxInstantPercent = (2 * SwingTimer - SlamCastTime - GCD) / SwingTimer * 100 +function CleveRoids.GetInstantWindowPercent() + local attackSpeed = UnitAttackSpeed("player") + if not attackSpeed or attackSpeed <= 0 then return 0 end + + local slamCastTime = CleveRoids.GetSlamCastTime() + local maxInstantStart = (2 * attackSpeed) - slamCastTime - GCD_DURATION + + if maxInstantStart <= 0 then return 0 end -- Window is impossible with current timings + + return (maxInstantStart / attackSpeed) * 100 +end + +-- Validate if current swing timer is within the Slam window (no clip) +-- Returns true if casting Slam NOW will NOT clip the auto-attack +function CleveRoids.ValidateNoSlamClip() + -- Check if SP_SwingTimer is loaded + if st_timer == nil then + if not CleveRoids._slamClipErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [noslamclip] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) + CleveRoids._slamClipErrorShown = true + end + return false + end + + local attackSpeed = UnitAttackSpeed("player") + if not attackSpeed or attackSpeed <= 0 then return false end + + local timeElapsed = attackSpeed - st_timer + local percentElapsed = (timeElapsed / attackSpeed) * 100 + local maxPercent = CleveRoids.GetSlamWindowPercent() + + return percentElapsed <= maxPercent +end + +-- Validate if current swing timer is within the instant window for next Slam +-- Returns true if casting an instant NOW will NOT cause the NEXT Slam to clip +function CleveRoids.ValidateNoNextSlamClip() + -- Check if SP_SwingTimer is loaded + if st_timer == nil then + if not CleveRoids._slamClipErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [nonextslamclip] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) + CleveRoids._slamClipErrorShown = true + end + return false + end + + local attackSpeed = UnitAttackSpeed("player") + if not attackSpeed or attackSpeed <= 0 then return false end + + local timeElapsed = attackSpeed - st_timer + local percentElapsed = (timeElapsed / attackSpeed) * 100 + local maxPercent = CleveRoids.GetInstantWindowPercent() + + return percentElapsed <= maxPercent +end + function CleveRoids.ValidateLevel(unit, operator, amount) if not unit or not operator or not amount then return false end local level = UnitLevel(unit) @@ -3509,6 +3691,36 @@ CleveRoids.Keywords = { end, conditionals, "notte") end, + -- Slam clip window conditionals for Warrior Slam rotation optimization + -- Based on math: MaxSlamPercent = (SwingTimer - SlamCastTime) / SwingTimer * 100 + -- Requires SP_SwingTimer addon and Nampower for cast time lookup + + -- [noslamclip] - True if casting Slam NOW will NOT clip the auto-attack + -- Usage: /cast [noslamclip] Slam + noslamclip = function(conditionals) + return CleveRoids.ValidateNoSlamClip() + end, + + -- [slamclip] - True if casting Slam NOW WILL clip the auto-attack (negated) + -- Usage: /cast [slamclip] Heroic Strike -- Use HS instead when past slam window + slamclip = function(conditionals) + return not CleveRoids.ValidateNoSlamClip() + end, + + -- [nonextslamclip] - True if casting an instant NOW will NOT cause NEXT Slam to clip + -- Scenario: Skip Slam this swing, cast instant, then Slam next swing without clipping + -- Formula: MaxInstantPercent = (2 * SwingTimer - SlamCastTime - GCD) / SwingTimer * 100 + -- Usage: /cast [nonextslamclip] Bloodthirst + nonextslamclip = function(conditionals) + return CleveRoids.ValidateNoNextSlamClip() + end, + + -- [nextslamclip] - True if casting an instant NOW WILL cause NEXT Slam to clip (negated) + -- Usage: Use this when you want to know you're past the instant window + nextslamclip = function(conditionals) + return not CleveRoids.ValidateNoNextSlamClip() + end, + -- Checks if the target uses a specific power type (mana, rage, energy) powertype = function(conditionals) local unit = conditionals.target or "target" diff --git a/Core.lua b/Core.lua index 7fd0b54..5ad6931 100644 --- a/Core.lua +++ b/Core.lua @@ -4237,6 +4237,8 @@ SlashCmdList["CLEVEROID"] = function(msg) DEFAULT_CHAT_FRAME:AddMessage('/cleveroid channeldebug - Toggle [channeltime] conditional debug output') DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Action Slot Debug:|r") DEFAULT_CHAT_FRAME:AddMessage('/cleveroid slotdebug - Debug action slot state (tooltip/range/mana)') + DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Slam Rotation (Warrior):|r") + DEFAULT_CHAT_FRAME:AddMessage('/cleveroid slamdebug - Show Slam cast time and clip window calculations') return end @@ -5113,6 +5115,94 @@ SlashCmdList["CLEVEROID"] = function(msg) return end + -- slamdebug (show Slam cast time and window calculations for Warrior rotation) + if cmd == "slamdebug" or cmd == "slam" or cmd == "slamtime" then + CleveRoids.Print("|cff88ff88=== Slam Clip Window Debug ===|r") + + -- Get swing timer info + local attackSpeed = UnitAttackSpeed("player") + if not attackSpeed or attackSpeed <= 0 then + CleveRoids.Print("|cffff0000No attack speed available!|r") + CleveRoids.Print("You need to be in combat or have a weapon equipped.") + return + end + + -- Check SP_SwingTimer + if st_timer == nil then + CleveRoids.Print("|cffff0000SP_SwingTimer not detected!|r") + CleveRoids.Print("The [noslamclip] conditionals require SP_SwingTimer addon.") + CleveRoids.Print("Get it at: https://github.com/jrc13245/SP_SwingTimer") + else + local timeElapsed = attackSpeed - st_timer + local percentElapsed = (timeElapsed / attackSpeed) * 100 + CleveRoids.Print("|cffffaa00SP_SwingTimer:|r st_timer=" .. string.format("%.3f", st_timer) .. "s") + CleveRoids.Print("Swing elapsed: " .. string.format("%.2f", percentElapsed) .. "%") + end + + CleveRoids.Print(" ") + CleveRoids.Print("|cffffaa00Swing Timer:|r " .. string.format("%.3f", attackSpeed) .. "s") + + -- Get Slam cast time from tooltip scanning + local slamCastTime = CleveRoids.GetSlamCastTime() + local isDefault = (slamCastTime == 1.5) + + CleveRoids.Print("|cffffaa00Slam Cast Time:|r " .. string.format("%.3f", slamCastTime) .. "s" .. (isDefault and " (default)" or " (from tooltip)")) + + -- Show tooltip scanning diagnostic info + if CleveRoids.GetSlamSpellSlot then + local slot = CleveRoids.GetSlamSpellSlot() + if slot then + CleveRoids.Print(" Slam found in spellbook slot: " .. slot) + else + CleveRoids.Print(" |cffff0000Slam not found in spellbook!|r Are you a Warrior?") + end + end + + if isDefault then + CleveRoids.Print(" Note: Could not read from tooltip, using default 1.5s") + end + + -- Calculate windows + CleveRoids.Print(" ") + local slamWindowTime = attackSpeed - slamCastTime + local slamWindow = 0 + if slamWindowTime > 0 then + slamWindow = (slamWindowTime / attackSpeed) * 100 + end + + local instantWindowTime = (2 * attackSpeed) - slamCastTime - 1.5 + local instantWindow = 0 + if instantWindowTime > 0 then + instantWindow = (instantWindowTime / attackSpeed) * 100 + end + + CleveRoids.Print("|cff00ff00=== Window Calculations ===|r") + CleveRoids.Print("|cffffaa00Slam Window:|r " .. string.format("%.2f", slamWindow) .. "%") + CleveRoids.Print(" Formula: (SwingTimer - SlamCast) / SwingTimer") + CleveRoids.Print(" (" .. string.format("%.3f", attackSpeed) .. " - " .. string.format("%.3f", slamCastTime) .. ") / " .. string.format("%.3f", attackSpeed) .. " = " .. string.format("%.2f", slamWindow) .. "%") + CleveRoids.Print(" Max time to cast Slam: " .. string.format("%.3f", slamWindowTime) .. "s into swing") + + CleveRoids.Print(" ") + CleveRoids.Print("|cffffaa00Instant Window:|r " .. string.format("%.2f", instantWindow) .. "%") + CleveRoids.Print(" Formula: (2×SwingTimer - SlamCast - GCD) / SwingTimer") + CleveRoids.Print(" (2×" .. string.format("%.3f", attackSpeed) .. " - " .. string.format("%.3f", slamCastTime) .. " - 1.5) / " .. string.format("%.3f", attackSpeed) .. " = " .. string.format("%.2f", instantWindow) .. "%") + CleveRoids.Print(" Max time to cast instant: " .. string.format("%.3f", instantWindowTime) .. "s into swing") + + -- Show current status + if st_timer ~= nil then + CleveRoids.Print(" ") + CleveRoids.Print("|cff00ff00=== Current Status ===|r") + local timeElapsed = attackSpeed - st_timer + local percentElapsed = (timeElapsed / attackSpeed) * 100 + local inSlamWindow = percentElapsed <= slamWindow + local inInstantWindow = percentElapsed <= instantWindow + CleveRoids.Print("[noslamclip]: " .. (inSlamWindow and "|cff00ff00TRUE|r (safe to Slam)" or "|cffff0000FALSE|r (would clip)")) + CleveRoids.Print("[nonextslamclip]: " .. (inInstantWindow and "|cff00ff00TRUE|r (safe to instant)" or "|cffff0000FALSE|r (would clip next Slam)")) + end + + return + end + -- Unknown command fallback CleveRoids.Print("Usage:") DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings") diff --git a/README.md b/README.md index 427c0f5..86c5b31 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,6 @@ The addon will not function without all three DLL mods installed. /cleveroid talenttabs -- Show talent tab IDs /cleveroid listtab -- List talents in a tab /cleveroid talents -- Show current talent ranks -/cleveroid testtalent -- Test talent modifier -/cleveroid diagnosetalent -- Diagnose talent issues -/cleveroid testequip -- Test equipment modifier ``` ### Combo Point Tracking @@ -133,21 +130,23 @@ Both formats work identically: ### Multi-Value Conditionals -#### OR Operator (`/`) +#### Conditionals are split by / for OR, & for AND... ```lua -[zone:Stormwind_City/Ironforge] -- In Stormwind OR Ironforge -[nozone:Stormwind_City/Ironforge] -- NOT in Stormwind OR NOT in Ironforge -[nomypower:>10/<20] -- Power NOT >10 OR NOT <20 +[zone:Stormwind_City/Ironforge] -- In at least one of the zones +[zone:Stormwind_City&Ironforge] -- In both zones (impossible, but simple to understand) +``` +#### Negated Conditionals are reversed... +```lua +[nozone:Stormwind_City/Ironforge] -- NOT in Stormwind AND NOT in Ironforge (neither) +[nozone:Stormwind_City&Ironforge] -- NOT in Stormwind OR NOT in Ironforge (either) +``` +#### More Examples +```lua +[nomypower:>10/<20] -- Power NOT >10 AND NOT <20 +[nomybuff:Mark_of_the_Wild/Thorns] -- Missing both buffs +[nomybuff:"Seal of Wisdom"&"Seal of Light"] -- Missing at least one of the buffs ``` -#### AND Operator (`&`) -```lua -[zone:Stormwind_City&Elwynn_Forest] -- In both zones (subzone & zone) -[nozone:Stormwind_City&Ironforge] -- NOT in Stormwind AND NOT in Ironforge -[nomybuff:Mark_of_the_Wild&Thorns] -- Missing both buffs -``` - -**Important:** `/` always means OR, even for negated conditionals. Use `&` for AND logic. ### Negation Prefix with `no` to negate (marked as "Noable" in tables): @@ -192,7 +191,7 @@ Use slot numbers (1-19): | Prefix | Example | Description | |:------:|---------|-------------| -| `!` | `!Attack`
`!Cat Form` | Only use if not active
Shorthand for `[nomybuff]` | +| `!` | `!Attack`
`!Cat Form` | Only use if not active
Shorthand for `[nomybuff]` connected with an AND | | `?` | `?[equipped:Swords] Ability` | Hides icon/tooltip
Must be first character | | `~` | `~Slow Fall` | Toggle buff on/off | @@ -695,6 +694,63 @@ For 3.5s weapon: --- +# Slam Clip Conditionals (Warrior Only) + +Specialized conditionals for Warrior Slam rotation to prevent auto-attack clipping. + +**Note:** These conditionals are designed specifically for Warrior Slam rotation optimization. They are not intended for general-purpose use with other classes or abilities. + +**Requirements:** +- [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) addon +- Warrior class (reads Slam cast time from spellbook tooltip) + +**How It Works:** +Slam pauses the swing timer if it would complete during the cast, causing auto-attack clipping. These conditionals calculate optimal windows based on your current swing timer and Slam cast time (accounting for haste and talents). + +**The Math:** +- **Slam Window**: `(SwingTimer - SlamCastTime) / SwingTimer × 100` +- **Instant Window**: `(2 × SwingTimer - SlamCastTime - GCD) / SwingTimer × 100` + +**Conditionals:** +```lua +[noslamclip] -- True if Slam NOW won't clip auto-attack +[slamclip] -- True if Slam NOW WILL clip (negated) +[nonextslamclip] -- True if instant NOW won't cause NEXT Slam to clip +[nextslamclip] -- True if instant NOW WILL cause NEXT Slam to clip (negated) +``` + +**Debug Command:** +```lua +/cleveroid slamdebug -- Show cast time, windows, and current status +``` + +**Example Values** (2.7s swing timer, 1.6s Slam cast): +- Slam window: ~40% (can cast Slam up to 40% into swing) +- Instant window: ~85% (can cast BT/WW up to 85% into swing) + +**Examples:** +```lua +-- Cast Slam only when safe +#showtooltip Slam +/cast [noslamclip] Slam + +-- Use Heroic Strike when past Slam window +#showtooltip Heroic Strike +/cast [slamclip] Heroic Strike + +-- Cast Bloodthirst only when next Slam won't clip +#showtooltip Bloodthirst +/cast [nonextslamclip] Bloodthirst + +-- Full Slam rotation macro +#showtooltip +/cast [noslamclip] Slam +/cast [nonextslamclip] Bloodthirst +/cast [slamclip] Heroic Strike +``` + +--- + # Threat Integration Requires [TWThreat](https://github.com/MarcelineVQ/TWThreat).