diff --git a/Conditionals.lua b/Conditionals.lua
index 80b69e1..6adbe9f 100644
--- a/Conditionals.lua
+++ b/Conditionals.lua
@@ -338,6 +338,38 @@ function CleveRoids.ValidateComboPoints(operator, amount)
return false
end
+-- Validates swing timer percentage for SP_SwingTimer addon integration
+-- operator: Comparison operator (>, <, =, >=, <=, ~=)
+-- amount: Percentage of swing time (e.g., 20 means 20% of attack speed)
+-- returns: True if st_timer [operator] (attackSpeed * percent/100)
+function CleveRoids.ValidateSwingTimer(operator, amount)
+ if not operator or not amount then return false end
+
+ -- Check if SP_SwingTimer is loaded by checking for st_timer global
+ if st_timer == nil then
+ -- Only show error once per session
+ if not CleveRoids._swingTimerErrorShown then
+ DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swingtimer] conditional requires the SP_SwingTimer addon. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5)
+ CleveRoids._swingTimerErrorShown = true
+ end
+ return false
+ end
+
+ -- Get player's attack speed (main hand)
+ local attackSpeed = UnitAttackSpeed("player")
+ if not attackSpeed or attackSpeed <= 0 then return false end
+
+ -- Calculate threshold: percentage of swing time
+ local threshold = attackSpeed * (amount / 100)
+
+ -- Compare current swing timer against threshold
+ if CleveRoids.operators[operator] then
+ return CleveRoids.comparators[operator](st_timer, threshold)
+ end
+
+ return false
+end
+
function CleveRoids.ValidateLevel(unit, operator, amount)
if not unit or not operator or not amount then return false end
local level = UnitLevel(unit)
@@ -2220,5 +2252,39 @@ CleveRoids.Keywords = {
end
return not CleveRoids.CheckImmunity(conditionals.target or "target", checkValue)
+ end,
+
+ -- SP_SwingTimer integration conditionals
+ -- Checks if swing timer is at a percentage of attack speed
+ -- Usage: [swingtimer:>20] = st_timer > UnitAttackSpeed("player") * 0.2
+ swingtimer = function(conditionals)
+ return And(conditionals.swingtimer, function(args)
+ if type(args) ~= "table" then return false end
+ return CleveRoids.ValidateSwingTimer(args.operator, args.amount)
+ end)
+ end,
+
+ -- Alias for swingtimer
+ stimer = function(conditionals)
+ return And(conditionals.stimer, function(args)
+ if type(args) ~= "table" then return false end
+ return CleveRoids.ValidateSwingTimer(args.operator, args.amount)
+ end)
+ end,
+
+ -- Negated swingtimer
+ noswingtimer = function(conditionals)
+ return And(conditionals.noswingtimer, function(args)
+ if type(args) ~= "table" then return false end
+ return not CleveRoids.ValidateSwingTimer(args.operator, args.amount)
+ end)
+ end,
+
+ -- Alias for noswingtimer
+ nostimer = function(conditionals)
+ return And(conditionals.nostimer, function(args)
+ if type(args) ~= "table" then return false end
+ return not CleveRoids.ValidateSwingTimer(args.operator, args.amount)
+ end)
end
}
diff --git a/README.md b/README.md
index 6ac65c4..5e983bf 100644
--- a/README.md
+++ b/README.md
@@ -203,6 +203,8 @@ The addon automatically applies talent-based duration modifications to debuffs.
**Calculation Order:** `final_duration = (base + combo_points) + talent_modifier`
**Supported Talent Modifiers:**
+- **Warrior:**
+ - **Booming Voice** (5 ranks): +12% per rank to Demoralizing Shout duration (60% at rank 5)
- **Rogue:**
- **Taste for Blood** (3 ranks): +2s per rank to Rupture duration
- **Improved Gouge** (3 ranks): +0.5s per rank to Gouge duration
@@ -236,10 +238,10 @@ The **Carnage** talent (Feral Combat tree, 2/2 required) causes **Ferocious Bite
- Rip/Rake must already be active on the target
### Equipment Modifiers
-The addon supports item-based duration modifications.
+The addon supports item-based duration modifications.
**IF YOUR EQUIPMENT DEBUFF DURATION MODIFIER IS MISSING, PLEASE MAKE A BUG REPORT!!!**
-**Calculation Order:** `final_duration = ((base + combo_points) + talent_modifier) × equipment_modifier`
+**Calculation Order:** `final_duration = (((base + combo_points) + talent_modifier) × equipment_modifier) + set_bonus`
**Supported Equipment Modifiers:**
- **Idol of Savagery** (Item ID: 61699, Druid Idol): Reduces the time between periodic ticks and the duration of Rake and Rip by 10% (×0.9 multiplier)
@@ -250,6 +252,28 @@ The addon supports item-based duration modifications.
3. Talent modifier: 18s + 0 = 18s (if no talent)
4. Equipment modifier: 18s × 0.9 = **16.2s final**
+### Set Bonus Modifiers
+The addon supports set bonus duration modifications by counting equipped set pieces.
+**IF YOUR SET BONUS DEBUFF DURATION MODIFIER IS MISSING, PLEASE MAKE A BUG REPORT!!!**
+
+**How It Works:**
+The system counts how many pieces from a set are equipped and applies the modifier when the threshold is met.
+
+**Supported Set Bonus Modifiers:**
+- **Druid:**
+ - **Dreamwalker Regalia (4/9)**: +3s to Moonfire duration, +2s to Insect Swarm duration
+ - Item IDs: 47372, 47373, 47374, 47375, 47376, 47377, 47378, 47379, 47380
+ - **Haruspex's Garb (3/5)**: +5s to Faerie Fire duration (non-feral only)
+ - Item IDs: 19613, 19955, 19840, 19839, 19838
+
+**Example:** Moonfire with Dreamwalker Regalia 4-set:
+1. Base duration: 18s
+2. Set bonus modifier: 18s + 3s = **21s final**
+
+**Example:** Insect Swarm with Dreamwalker Regalia 4-set:
+1. Base duration: 18s
+2. Set bonus modifier: 18s + 2s = **20s final**
+
### Immunity Tracking System
The addon automatically learns and tracks NPC immunities from combat log messages:
@@ -442,6 +466,7 @@ The addon automatically learns and tracks NPC immunities from combat log message
| queuedspell | [queuedspell]
[queuedspell:X] | | * | if the player has any or a specific spell queued with nampower. |
| onswingpending | [onswingpending] | | * | If the player has a on swing spell pending.|
| mhimbue/ohimbue | [mhimbue:Flametongue]
[ohimbue] | | * | If the player has weapon imbue on their mh/oh.|
+| swingtimer/stimer | [swingtimer:>20]
[stimer:<=50] | * | * | If the swing timer is at a percentage of attack speed. Requires [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) addon. See [below](#swing-timer-integration) for details.|
### Unit Based
### The default @unitid is usually @target if you don't specify one
@@ -589,6 +614,65 @@ The addon automatically learns and tracks NPC immunities from combat log message
* ShaguTweaks
+### Swing Timer Integration
+The addon integrates with [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) to provide swing timer conditionals for abilities like Slam that benefit from being cast at specific points in your swing cycle.
+
+**Requirements:**
+- [SP_SwingTimer](https://github.com/jrc13245/SP_SwingTimer) addon must be installed
+- If SP_SwingTimer is not loaded, the conditional will return false and display an error message once
+
+**How It Works:**
+The conditional compares the current swing timer (`st_timer`) against a percentage of your attack speed.
+
+`[swingtimer:>20]` checks if `st_timer > UnitAttackSpeed("player") * 0.2`
+
+This means "cast if more than 20% of my swing time remains."
+
+**Syntax:**
+- `[swingtimer:>X]` - True if swing timer > X% of attack speed
+- `[swingtimer:=X]` - True if swing timer >= X% of attack speed
+- `[swingtimer:<=X]` - True if swing timer <= X% of attack speed
+- `[stimer:>X]` - Alias for swingtimer
+
+**Negation:**
+- `[noswingtimer:<80]` - True if NOT (swing timer < 80% of attack speed)
+- `[nostimer:>20]` - Alias for noswingtimer
+
+**Example Macros:**
+
+```lua
+-- Cast Slam only if more than 20% of swing time remains
+-- Equivalent to: /run if st_timer>UnitAttackSpeed("player")*0.2 then CastSpellByName("Slam") end
+#showtooltip Slam
+/cast [swingtimer:>20] Slam
+```
+
+```lua
+-- Heroic Strike queue management - only queue if swing timer is low
+#showtooltip Heroic Strike
+/cast [stimer:<30] Heroic Strike
+```
+
+```lua
+-- Complex rotation with swing timer awareness
+#showtooltip
+/cast [swingtimer:>25] Slam
+/cast Heroic Strike
+```
+
+**Understanding the Percentage:**
+- 100% = Full swing time (just attacked, full cooldown remaining)
+- 50% = Half of swing time remaining
+- 20% = Only 20% of swing time left before next auto-attack
+- 0% = About to swing
+
+For a warrior with a 3.5 second weapon:
+- `[swingtimer:>20]` = True if st_timer > 0.7 seconds
+- `[swingtimer:>50]` = True if st_timer > 1.75 seconds
+
+---
+
### HealComm Support
This addon uses [SuperWoW](https://github.com/balakethelock/SuperWoW)'s CastSpellByName which is not compatible with the standard HealComm-1.0 library. MarcelineVQ has an updated version of [LunaUnitFrames](https://github.com/MarcelineVQ/LunaUnitFrames) where they added [SuperWoW](https://github.com/balakethelock/SuperWoW) support to the HealComm-1.0 library. If you use [SuperWoW](https://github.com/balakethelock/SuperWoW) and want proper HealComm support with your macros, you need to do one or both of the following:
diff --git a/Utility.lua b/Utility.lua
index 6807eb6..bc14225 100644
--- a/Utility.lua
+++ b/Utility.lua
@@ -1361,6 +1361,11 @@ ev:SetScript("OnEvent", function()
if duration and CleveRoids.ApplyEquipmentModifier then
duration = CleveRoids.ApplyEquipmentModifier(spellID, duration)
end
+
+ -- Apply set bonus modifiers for non-combo spells
+ if duration and CleveRoids.ApplySetBonusModifier then
+ duration = CleveRoids.ApplySetBonusModifier(spellID, duration)
+ end
end
-- DEBUG: Show what duration we calculated
@@ -1693,6 +1698,17 @@ CleveRoids.talentModifiers[9827] = { tab = 2, id = 4, talent = "Brutal Impact",
-- Carnage is a refresh mechanic: Ferocious Bite at 5 CP refreshes Rip/Rake to their original duration
-- This is handled separately in the Carnage refresh logic (lines 1125-1192)
+-- WARRIOR talent modifiers
+-- Booming Voice: Increases duration of Battle Shout and Demoralizing Shout by 12% per rank (5 ranks max)
+-- Talent Position: Tab 2 (Fury), Talent 1
+-- Talent Spell IDs: 12321 (Rank 1), 12835 (Rank 2), 12836 (Rank 3), 12837 (Rank 4), 12838 (Rank 5)
+local boomingVoiceModifier = function(base, rank) return base * (1 + rank * 0.12) end -- 12% per rank, 60% at rank 5
+CleveRoids.talentModifiers[1160] = { tab = 2, id = 1, talent = "Booming Voice", modifier = boomingVoiceModifier } -- Demoralizing Shout Rank 1
+CleveRoids.talentModifiers[6190] = { tab = 2, id = 1, talent = "Booming Voice", modifier = boomingVoiceModifier } -- Demoralizing Shout Rank 2
+CleveRoids.talentModifiers[11554] = { tab = 2, id = 1, talent = "Booming Voice", modifier = boomingVoiceModifier } -- Demoralizing Shout Rank 3
+CleveRoids.talentModifiers[11555] = { tab = 2, id = 1, talent = "Booming Voice", modifier = boomingVoiceModifier } -- Demoralizing Shout Rank 4
+CleveRoids.talentModifiers[11556] = { tab = 2, id = 1, talent = "Booming Voice", modifier = boomingVoiceModifier } -- Demoralizing Shout Rank 5
+
-- Function to get talent rank
-- Supports both position-based (tab, id) and name-based lookup
-- Position-based is preferred as it's more reliable and locale-independent (like Cursive addon)
@@ -1993,6 +2009,127 @@ function CleveRoids.RegisterEquipmentModifier(spellID, slotID, modifierFunc)
return true
end
+-- ============================================================================
+-- SET BONUS MODIFIER SYSTEM
+-- ============================================================================
+
+-- Database of set bonus modifiers for debuff durations
+-- Structure: [spellID] = { items = {itemID1, itemID2, ...}, threshold = X, modifier = function(baseDuration) }
+CleveRoids.setbonusModifiers = CleveRoids.setbonusModifiers or {}
+
+-- Function to count how many items from a set are currently equipped
+function CleveRoids.CountEquippedSetItems(items)
+ if not items or type(items) ~= "table" then return 0 end
+
+ local count = 0
+ -- Check all equipment slots (1-19)
+ for slot = 1, 19 do
+ local itemLink = GetInventoryItemLink("player", slot)
+ if itemLink then
+ local _, _, itemID = string.find(itemLink, "item:(%d+)")
+ if itemID then
+ itemID = tonumber(itemID)
+ -- Check if this item is in the set
+ for _, setItemID in ipairs(items) do
+ if itemID == setItemID then
+ count = count + 1
+ break
+ end
+ end
+ end
+ end
+ end
+
+ return count
+end
+
+-- Apply set bonus modifiers to a debuff duration
+-- Parameters:
+-- spellID: The spell ID
+-- baseDuration: The base duration (after combo points, talents, and equipment)
+-- Returns: Modified duration, or baseDuration if no set bonus modifier applies
+function CleveRoids.ApplySetBonusModifier(spellID, baseDuration)
+ if not spellID or not baseDuration then
+ return baseDuration
+ end
+
+ local modifier = CleveRoids.setbonusModifiers[spellID]
+ if not modifier then
+ return baseDuration
+ end
+
+ -- Check if player has enough set pieces equipped
+ local equippedCount = CleveRoids.CountEquippedSetItems(modifier.items)
+ if equippedCount < modifier.threshold then
+ return baseDuration
+ end
+
+ local modifiedDuration = modifier.modifier(baseDuration)
+
+ if modifiedDuration ~= baseDuration and CleveRoids.debug then
+ DEFAULT_CHAT_FRAME:AddMessage(
+ string.format("|cff00ffff[Set Bonus Modifier]|r %s (ID:%d): %.1fs -> %.1fs (%d/%d pieces)",
+ SpellInfo(spellID) or "Unknown", spellID, baseDuration, modifiedDuration,
+ equippedCount, modifier.threshold)
+ )
+ end
+
+ return modifiedDuration
+end
+
+-- Helper function to register a set bonus modifier
+-- Usage: CleveRoids.RegisterSetBonusModifier(spellID, itemsTable, threshold, modifierFunction)
+function CleveRoids.RegisterSetBonusModifier(spellID, items, threshold, modifierFunc)
+ if not spellID or not items or not threshold or not modifierFunc then
+ return false
+ end
+
+ CleveRoids.setbonusModifiers[spellID] = {
+ items = items,
+ threshold = threshold,
+ modifier = modifierFunc
+ }
+
+ return true
+end
+
+-- DRUID set bonus modifiers
+-- Dreamwalker Regalia (4/9): Increases Moonfire duration by 3 seconds and Insect Swarm by 2 seconds
+-- Item IDs: 47372, 47373, 47374, 47375, 47376, 47377, 47378, 47379, 47380
+local dreamwalkerItems = { 47372, 47373, 47374, 47375, 47376, 47377, 47378, 47379, 47380 }
+local dreamwalkerMoonfireModifier = function(base) return base + 3 end
+local dreamwalkerInsectSwarmModifier = function(base) return base + 2 end
+
+-- Moonfire (all ranks)
+CleveRoids.setbonusModifiers[8921] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 1
+CleveRoids.setbonusModifiers[8924] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 2
+CleveRoids.setbonusModifiers[8925] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 3
+CleveRoids.setbonusModifiers[8926] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 4
+CleveRoids.setbonusModifiers[8927] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 5
+CleveRoids.setbonusModifiers[8928] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 6
+CleveRoids.setbonusModifiers[8929] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 7
+CleveRoids.setbonusModifiers[9833] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 8
+CleveRoids.setbonusModifiers[9834] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 9
+CleveRoids.setbonusModifiers[9835] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerMoonfireModifier } -- Moonfire Rank 10
+
+-- Insect Swarm (all ranks)
+CleveRoids.setbonusModifiers[5570] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 1
+CleveRoids.setbonusModifiers[24974] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 2
+CleveRoids.setbonusModifiers[24975] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 3
+CleveRoids.setbonusModifiers[24976] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 4
+CleveRoids.setbonusModifiers[24977] = { items = dreamwalkerItems, threshold = 4, modifier = dreamwalkerInsectSwarmModifier } -- Insect Swarm Rank 5
+
+-- Haruspex's Garb (3/5): Increases Faerie Fire duration by 5 seconds
+-- Item IDs: 19613, 19955, 19840, 19839, 19838
+local haruspexItems = { 19613, 19955, 19840, 19839, 19838 }
+local haruspexFaerieFireModifier = function(base) return base + 5 end
+
+-- Faerie Fire (non-feral, all ranks)
+CleveRoids.setbonusModifiers[770] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 1
+CleveRoids.setbonusModifiers[778] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 2
+CleveRoids.setbonusModifiers[9749] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 3
+CleveRoids.setbonusModifiers[9907] = { items = haruspexItems, threshold = 3, modifier = haruspexFaerieFireModifier } -- Faerie Fire Rank 4
+
-- ============================================================================
-- IMMUNITY TRACKING SYSTEM
-- ============================================================================