• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[JASS] CooldownAdjust

CooldownAdjust allows to extend shorten cooldowns by giving abilities/Buffs a cooldownAdjustmentValue. The caster's current cooldownAdjustmentValue is calced on Spellcasting. This value then affects the cooldown only for an small moment, the spell casting. Cooldown adjustment value uses an simlear fomular as armorReduction.

I used this cooldown adjustment value instead of direct % values to avoid reaching big % adjustments to fast, when stacking up multiple sources.

JASS:
//CooldownAdjust 1.04c
//By Tasyen

// CooldownAdjust extends / shortens cooldowns of any casted ability, based on casters current cooldownAdjustmentValue.
// The casters current cooldownAdjustmentValue is calced on Spellcasting.
// This value then affects the cooldown only for an small moment, the spell casting.
// Cooldownadjustment supports 2 ways of interpretations of cooldownAdjustmentValue: Linear (LoL Like) or armor Like.
// Sources of cooldownAdjustment are Buffs / Abilities which are saved in array form.
// One can bind Sources to groups to use only the best current available of the group.
// It's also possible for a cooldownAdjustment-Source to affect only itemSpells or normalSpells or both.

//=======================
// Variables of CooldownAdjust

//	 udg_CdAdjustAbility:	If an unit has this ability on SpellEffect it will be affected by this CdAdjust.
//	 udg_CdAdjustBuff:	 like udg_CdAdjustAbility but Buff (GUI Friendly allow both), Bugg (Buff allways return 1 as Level).

//	udg_CdAdjustType: affect itemSpells or/and normal spells
//		0 = affect both; 1 affect normal spells; 2 = affect itemSpells

//	udg_CdAdjustGroup: a group is a number, all sources with the same number will be considered as one group.
//					 : only the highest absolute cooldownValue of each group is used. (-30 overpowers 28 and changes the groups value from 28 CooldownReduction to 30 CooldownExtension)
//					 : sources with group = 0 will stack freely.
//					 : I personaly use an abilityId like 'A000' the one first beeing added to that group which is treated as represent of that group.

//	udg_CdAdjustValue: the basePower of this CdAdjustment
//	udg_CdAdjustValuePerLevel: Increases the adjustValue for each Level of this source owned by the caster (Level 1 is affected by this) (Buff Level is not recognized)

//		A + value reduces cooldown
//		A - value extends cooldown
//		the power of each point of udg_CdAdjustValue is influenced by the definition "function CooldownAdjustRatio".
//		supports 2 types of interpretation, linear and armor like. One can define which one is used inside the definition "function CooldownUseValueLinear".

//	used Indexes are 1 to udg_CdAdjustLast

//=======================
// Linear Scaling
//	Each point affects the value with the same weight.

// Example:
// the buff of Brillianz-Aura reduces Cooldowns by 10%.

// we use ratio = 0.01 check "function CooldownAdjustRatio"
// then we use register the Brillianz-Aura-Buff as source of CooldownAdjust with "function CdAdjustRegister" takes integer abilityId, real value, integer adjustType returns nothing
// ->
// call CdAdjustRegister('BHab', 10, 0)

// with this line: any caster having the buff 'BHab' will gain 10 CooldownAdjustValue. Each point interpretated as 1% cooldown reduction. the 0 means affect item and normal spells.
// so the caster would now have 10% CooldownReduction.

// if one would want to have 20%, one just takes the double.
// call CdAdjustRegister('BHab', 20, 0)

//=======================
// Armor Like
// The first points give alot of %, while later points give less %.
// Cause of that it can be quite complicated to calc the wanted amount of CdAdjust, which also is done from point zero.

// How to calc a % cooldown reduction?

// use this equation to calculate the +CdAdjustValue.

//	value = ( 1 / CooldownAdjustRatio() ) / ( 1 /wanted Reduction - 1)

// Example: ratio = 0.01, wanted % = 20%

//	value = 100 / (1/0.2 - 1)
//	-> 100 / (5 - 1) = 100/4 = 25
// 25 cooldownAdjustmentValue would reduce cooldown by 20%.

// Let's test the tripple, how much one needs for 60%?
//	100 / (1/4/5 -1)
//	100 / (5/4 - 1)
//	100 / (3/4) = 400 / 3 = 133 + 1/3
// you see to reach 60% Reduction one would need 133.33 Value which is kinda is more than 5.3 times of the value needed for 20% reduction.

//=======================
//	negative cooldownadjustment is always linear

// with ratio 0.01
// each -1 cooldownAdjustmentValue extends cooldown by 1%, so -100 would double the cooldown, -200 would tripple it.
// this is only the case if the total cooldownAdjustmentValue goes below 0.
// if an unit has + and - they first add each other.
//	-20 and 45 -> 25 (shorten by ~20%)

//=======================
// Avoid Cooldown Adjustment
//	There are multiple Options to avoid cooldownAdjustment in unwanted situations.

//	First there is the "udg_CdAdjustExlcudes" Ability-List, this List can contain all Spells you want to be unaffected by CooldownAdjust.
//	Second there is the definition function "CooldownAdjustThreshold", Every spell with a default cooldown (the global one) below or equal to this definition will be ignored.
//	Third there is the definition function "CooldownAdjustFilter", A function in which you have access to caster, usedItem (if there is one), and to the used Spell let it return false if you want the current spllcast untouched from CooldownAdjust.

//	CdAdjustExlcudes:	a List of castable abilities not beeing affected by CooldownAdjust, spells with a default (inside the global value) cooldown equal or less to CooldownAdjustThreshold() are ignored on default.
//	used Indexes are 1 to CdAdjustExlcudesLast

//=======================
//Variables to do the 0 Timer skip over the cooldown start. Don't Touch them.
//udg_CdAdjust__Unit	- Unit array
//udg_CdAdjust__Spell - integer array
//udg_CdAdjust__Add - real array
//udg_CdAdjust__Last - integer
//udg_CdAdjust__Timer - timer

//=======================
//API
//=======================
//	function CooldownAdjust_GetValue takes unit caster, item usedItem returns real
//		- returns the cooldownAdjustmentValue caster would have, use usedItem == null if none is used.

//	function CooldownAdjust_CalcMulti takes real value returns real
//		converts cooldownAdjustmentValue in an multiplier respecting "CooldownAdjustCapShorten" and "CooldownAdjustCapExtend"

//1 Line setters
//	function CdAdjustRegisterSuper takes integer abilityId, real value, real valuePerLevel, integer adjustType, integer stackingGroup returns nothing
//		- when an unit has abilityId it gains CooldownAdjustValue: value + valuePerLevel * abilityLevel (buffs allways use level 1).
//		- if the adjustType is 0 it affects all spells, with 2 spells from items, with 1 unit spells
//		- only the best of one stackingGroup is used, if an unit has multiple sources of cooldown adjustments of the same group. stackingGroup = 0 for free stacking.

//	function CdAdjustRegister takes integer abilityId, real value, integer adjustType returns nothing
//		-wrapper, no stackingGroup, only 1 level

//	function CdAdjustExclude takes integer abilityId returns nothing
//		- exclude that ability beeing affected from CooldownAdjust

//=======================
//Definitions
//=======================
constant function CooldownAdjustThreshold takes nothing returns real
	return 0.5	//Abilities with a default cooldown of this or less seconds, won't have their cooldown touched.
endfunction
constant function CooldownAdjustRatio takes nothing returns real
	return 0.01	//How Strong is each point
endfunction
constant function CooldownAdjustCapExtend takes nothing returns real
	return 3.0	//Maximum cooldown Multi; used by - udg_CdAdjustValue, 3.0 => Cooldown can be extended to 3 times as long
endfunction
constant function CooldownAdjustCapShorten takes nothing returns real
	return 0.60	//Minimum cooldown Multi; used by + udg_CdAdjustValue, 0.6 => Cooldown can be shortend to 60%
endfunction
constant function CooldownUseValueLinear takes nothing returns boolean
	return false
	// true = each point reduces cooldown by CooldownAdjustRatio()%
	// false = armor like, the first ones give much % the later ones give less %.
endfunction

function CooldownAdjustFilter takes unit caster, integer spell, item usedItem returns boolean
	return true
	//customizeable filter
	//return true to allow cooldownadjustment for this situation
	//return false to disallow cooldownadjustment for this situation
endfunction

//============
//Code
//=======================

function LogBasedRising takes real value, real power, real ratio returns real
	if power >= 0 then
		set power =  (power * ratio) / (power * ratio + 1 )
		return value - value * power
	else
		set power = - power
		return value + value * power * ratio	//negative power is linear
	endif
endfunction

function CooldownAdjust_CDReset takes nothing returns nothing
	local integer level
	local real cd
	loop	//Revert the cooldown change done for all casters in the list
		exitwhen udg_CdAdjust__Last == 0
		set level = GetUnitAbilityLevel(udg_CdAdjust__Unit[udg_CdAdjust__Last], udg_CdAdjust__Spell[udg_CdAdjust__Last]) - 1
		set cd = BlzGetUnitAbilityCooldown(udg_CdAdjust__Unit[udg_CdAdjust__Last], udg_CdAdjust__Spell[udg_CdAdjust__Last], level)
		call BlzSetUnitAbilityCooldown(udg_CdAdjust__Unit[udg_CdAdjust__Last], udg_CdAdjust__Spell[udg_CdAdjust__Last], level, cd - udg_CdAdjust__Add[udg_CdAdjust__Last])
		set udg_CdAdjust__Last = udg_CdAdjust__Last - 1
	endloop
endfunction
function CooldownAdjust_CalcMulti takes real value returns real
	if value != 0 then //Is there a value?
	
		//Change this part if you want another Value interpretation
		if CooldownUseValueLinear() then
			set value = 1.0 - value * CooldownAdjustRatio() //Linear
		else	
			set value = LogBasedRising(1, value, CooldownAdjustRatio()) // Armor Like
		endif
				
		if value > CooldownAdjustCapExtend() then // Extension Cap
			return CooldownAdjustCapExtend()
		elseif	value < CooldownAdjustCapShorten() then // Reduction Cap
			return CooldownAdjustCapShorten()
		else //Normal Value
			return value
		endif	
	else  //No Adjustment
		return 1.0
	endif	
endfunction
function CooldownAdjust_GetValue takes unit caster, boolean itemSpell returns real
	local integer buffIndex = udg_CdAdjustLast
	local integer array buffGroupsUsed
	local integer buffGroupsUsedLast = 0
	local integer newGroupChecker = 0
	local integer level
	local real array buffGroupsUsedMax
	local real result = 0
	local real currentValue
	local boolean newGroup
	
	loop	//Loop all possible Cooldown Reductions
		exitwhen buffIndex == 0
		// Has this Cooldown Reduction, also does this affect Items or this is no itemSpell?
		//Accept buff or ability (GUI Friendly)!!
		set level = IMaxBJ(GetUnitAbilityLevel(caster, udg_CdAdjustBuff[buffIndex]), GetUnitAbilityLevel(caster, udg_CdAdjustAbility[buffIndex]))
		if level != 0 and ( (udg_CdAdjustType[buffIndex] == 0) or ( udg_CdAdjustType[buffIndex] == 1 and not itemSpell) or (udg_CdAdjustType[buffIndex] == 2 and itemSpell) ) then
			set currentValue = udg_CdAdjustValue[buffIndex] + udg_CdAdjustValuePerLevel[buffIndex] * level
			if udg_CdAdjustGroup[buffIndex] != 0 then //uses Stacking Groups?
				set newGroup = true
				set newGroupChecker = buffGroupsUsedLast
				loop //Test for beeing a new Cooldown Reduction Group
					exitwhen newGroupChecker == 0
					if buffGroupsUsed[newGroupChecker] == udg_CdAdjustGroup[buffIndex] then //same group?
						if RAbsBJ(buffGroupsUsedMax[newGroupChecker]) < RAbsBJ(currentValue) then //only the absolute strongest of one group is used. -100 will overpower, 99 completly
							set buffGroupsUsedMax[newGroupChecker] = currentValue
						endif
						set newGroup = false
						exitwhen true	//each Group is used only once.
					endif
					set newGroupChecker = newGroupChecker - 1
				endloop
				if newGroup then //New Group -> add it to the list
					set buffGroupsUsedLast = buffGroupsUsedLast + 1
					set buffGroupsUsedMax[buffGroupsUsedLast] = currentValue
					set buffGroupsUsed[buffGroupsUsedLast] = udg_CdAdjustGroup[buffIndex]
				endif	
			else // free stacking
				set result = result + currentValue
			endif
		endif
		set buffIndex = buffIndex - 1
	endloop
	
	loop //calc in the bests of all found CooldownGroups
		exitwhen buffGroupsUsedLast == 0
		set result = result + buffGroupsUsedMax[buffGroupsUsedLast]
		set buffGroupsUsedLast = buffGroupsUsedLast - 1
	endloop
	
	return result
endfunction
function CooldownAdjust_OnEffect takes nothing returns nothing
	local unit caster = GetTriggerUnit()
	local integer spell = GetSpellAbilityId()	
	local integer spellOrder = GetUnitCurrentOrder(caster)
	local integer level = GetUnitAbilityLevel(caster, spell) - 1
	local real cd  = BlzGetUnitAbilityCooldown(caster, spell, level)
	local boolean itemSpell = ( spellOrder >= 852008 and spellOrder <= 852013)
	local real cooldownAdjustmentValue
	local integer excludeList = udg_CdAdjustExlcudesLast
	local item usedItem
	if itemSpell then
		set usedItem = UnitItemInSlot(caster, spellOrder - 852008)
	else
		set usedItem = null
	endif	
	if BlzGetAbilityCooldown(spell,level) > CooldownAdjustThreshold() and CooldownAdjustFilter(caster, spell, usedItem) then	//Does this ability pass the cooldown Threshold, and the customizeable Filter?
		loop
			exitwhen excludeList == 0
			if spell == udg_CdAdjustExlcudes[excludeList] then	//Excluded spell?
				set caster = null
				return
			endif	
			set excludeList = excludeList - 1
		endloop
		set cooldownAdjustmentValue = CooldownAdjust_GetValue(caster, usedItem !=null )
		if cooldownAdjustmentValue != 0 then
			set cd = BlzGetUnitAbilityCooldown(caster, spell, level)
			set udg_CdAdjust__Last = udg_CdAdjust__Last + 1
			set udg_CdAdjust__Unit[udg_CdAdjust__Last] = caster
			set udg_CdAdjust__Spell[udg_CdAdjust__Last] = spell
			set udg_CdAdjust__Add[udg_CdAdjust__Last] =  cd * CooldownAdjust_CalcMulti(cooldownAdjustmentValue) - cd
			call BlzSetUnitAbilityCooldown(caster, spell, level, cd + udg_CdAdjust__Add[udg_CdAdjust__Last])
			call TimerStart(udg_CdAdjust__Timer, 0.01, false, function CooldownAdjust_CDReset)
		endif
	endif
	set caster = null
	set usedItem = null
endfunction

function CdAdjustRegisterSuper takes integer abilityId, real value, real valuePerLevel, integer adjustType, integer stackingGroup returns nothing
	set udg_CdAdjustLast = udg_CdAdjustLast + 1
	set udg_CdAdjustAbility[udg_CdAdjustLast] = abilityId
	set udg_CdAdjustValue[udg_CdAdjustLast] = value
	set udg_CdAdjustValuePerLevel[udg_CdAdjustLast] = valuePerLevel
	set udg_CdAdjustType[udg_CdAdjustLast] = adjustType
	set udg_CdAdjustGroup[udg_CdAdjustLast] = stackingGroup
endfunction

function CdAdjustRegister takes integer abilityId, real value, integer adjustType returns nothing
	call CdAdjustRegisterSuper(abilityId, value, 0, adjustType, 0)
endfunction

function CdAdjustExclude takes integer abilityId returns nothing
	set udg_CdAdjustExlcudesLast = udg_CdAdjustExlcudesLast + 1
	set udg_CdAdjustExlcudes[udg_CdAdjustExlcudesLast] = abilityId
endfunction
function CdAdjustFilterMorphs takes nothing returns boolean
	//Warcraft 3 1.30.1, Morphings spells crash when chaing cooldown of such spells(thanks Jampion)
	local integer unitOrder = GetUnitCurrentOrder(GetTriggerUnit())
	call BJDebugMsg(OrderId2String(unitOrder))
	if unitOrder == OrderId("burrow") or unitOrder == OrderId("unburrow") or unitOrder == OrderId("sphinxform") or unitOrder == OrderId("unsphinxform") or unitOrder == OrderId("bearform") or unitOrder == OrderId("unbearform") or unitOrder == OrderId("ravenform") or unitOrder == OrderId("unravenform") or unitOrder == OrderId("root") or unitOrder == OrderId("unroot") or unitOrder == OrderId("etherealform") or unitOrder == OrderId("unetherealform") or unitOrder == OrderId("corporealform") or unitOrder == OrderId("uncorporealform") or unitOrder == OrderId("submerge") or unitOrder == OrderId("unsubmerge") or unitOrder == OrderId("robogoblin") or unitOrder == OrderId("unrobogoblin") or unitOrder == OrderId("metamorphosis") or unitOrder == OrderId("stoneform") or unitOrder == OrderId("unstoneform") or unitOrder == OrderId("chemicalrage") or unitOrder == OrderId("elementalfury") then
//		call BJDebugMsg("Filter")
		return false
	endif
	//Warcraft 3 1.30.1, Also necrotic spells crash
	//instant = raisedead autocast
	if unitOrder == OrderId("raisedead") or unitOrder == OrderId("instant")or unitOrder == OrderId("vengeance") or unitOrder == OrderId("vengeanceinstant") or unitOrder == OrderId("carrionscarabs") or unitOrder == OrderId("carrionscarabsinstant") then
//		call BJDebugMsg("Filter")
		return false
	endif
	//Some orders with unmetamorph
	if unitOrder == OrderId("smart") or unitOrder == OrderId("attack") then
//		call BJDebugMsg("Filter")
		return false
	endif
	return true
endfunction
//=======================
function InitTrig_CooldownAdjust takes nothing returns nothing
	local integer index = 0
	set gg_trg_CooldownAdjust = CreateTrigger()
	call TriggerAddAction(gg_trg_CooldownAdjust, function CooldownAdjust_OnEffect)
	//call TriggerAddCondition(gg_trg_CooldownAdjust, Condition(function CdAdjustFilterMorphs))
	loop
		call TriggerRegisterPlayerUnitEvent(gg_trg_CooldownAdjust, Player(index), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
		set index = index + 1
		exitwhen index == bj_MAX_PLAYER_SLOTS
	endloop
endfunction

ChangeLog:
1.04c Fixed Ability Cooldown reading/setting for V1.31+, Disable the Morph Filter​
1.04a&b
Added missing Filters: elementalfury, chemicalrage, stoneform, raise skeletons, some metamoph cases​
1.04
Fixed a null
timer 0.01 -> 0.0
GetValue(unit, item) -> GetValue(unit, boolean)
Filters out any spell using an morphing order.​
1.03
Revamped the Register Functions to 2 in number.
Allows now Linear Scaling.
Improved Docu.​
1.02
The groupstacking ifs do now fit the cooldownValue.
Added a customizeable filterfunction CooldownAdjustFilter. inside Definitions
Splitted CalcMult into 2 functions GetValue and CalcMulti
CooldownAdjust_GetValue now has acces to the usedItem, but currently does nothing with it.
Added text explaning negative cooldownadjustmentValue.
Renamed the Caps and Min Cooldown Threshold.​
 

Attachments

  • CoolDownAdjust1.04c.w3x
    32.4 KB · Views: 5
Last edited:
Level 39
Joined
Feb 27, 2007
Messages
5,010
  • Since the cooldown natives only exist in 1.30+ where vJASS is also natively supported I see no reason you can't encapsulate this into a library structure with proper public/private prefixes for functions.

  • I see the map exists but in the documentation there should be some example code to show how to use the system. Right now I do not think your API and comments do a very good job explaining and it's real confusing. This might just be because of the format you have adopted here but it seems all over the place with multiple different ways to do the same thing and no clear reason why one way should be used over another.
    • Why are there CdAdjustRegister1 CdAdjustRegister2 CdAdjustRegister3 CdAdjustRegister4 and CdAdjustRegister5???
    • Do I need to register each spell on each unit separately?
    • Do I need to register each level separately?
    • If I need to register spells then why do udg_CdAdjustAbility and udg_CdAdjustBuff exist?
    • How to use udg_CdAdjustGroup needs better documentation.

  • The formula for converting the +CdAdjustValue should be easily editable and should include an option for a hard cap (i realize the min spell cooldown is a hard cap in its own right but that's not what I mean). I think more people would be likely to want a CDR system like LoL's where each point is X% lower cooldown and you can't go above Y% total. Diminishing returns as you've coded it is reasonable if there are LOTS of sources of CDR but what's to say everyone wants it to work that way?
 
I never wrote anything in vJass.

Why are there CdAdjustRegister1 CdAdjustRegister2 CdAdjustRegister3 CdAdjustRegister4 and CdAdjustRegister5???
Register 2 to 5 are wrappers to use Register1, using only a subset of available arguments. Yeah the naming Register1/2/3/4/5 does not say much, one has to look at the names of the arguments.

  • Do I need to register each spell on each unit separately?
  • Do I need to register each level separately?
This system affects all Spells casted on default.
One does not register the castable spells, one registers possible sources abilities/Buffs reducing/extending cooldowns. If an unit has such an source on the onEffect Event the current casted spell's cooldown will be adjusted.
As user one fills the list and checks/changes definitions then one is done.
CooldownAdjustment supports multiple Level for ability sources but not the buffs sources (cause there is no API for that).

How to use udg_CdAdjustGroup needs better documentation.
the Group feature allows to use only the strongest cdAdjustment of multiple sources using the same Group the caster currently is affected by.
Example
If a caster has "Aura of Quick spell" and "quick-spell" one could save both as Group with the id of "quick-spell" to use only one of both the one which gives more cdAdjustment.
But yeah I will extend the docu handeling it.

If I need to register spells then why do udg_CdAdjustAbility and udg_CdAdjustBuff exist?

udg_CdAdjustAbility and udg_CdAdjustBuff both exist to allow GUI users to select Abilities or Buffs when they write the list with only set "variable" acions.

This might just be because of the format you have adopted here but it seems all over the place with multiple different ways to do the same thing and no clear reason why one way should be used over another.
One can registers castable spells into the blacklist to have fixed cooldowns for them.
Or one can filter unwanted situations inside the function "CooldownAdjustFilter", example exclude dummy casters.
Also a map could provide "option-abilities" which tend to have low cooldowns which are filtered with "CooldownAdjustThreshold".

There are 2 caps they are not in the cdAdjustmentValue format they are in multiplier.
CooldownAdjustCapShorten is the lowest cooldown multi this system will use.
CooldownAdjustCapExtend is the hightest cooldown multi the system will use

I think it wouldn't be hard to change it to the LoL mechanic but yeah if one wants the LoL cooldown meachnic then one wants that without changing anything except maybe a definition.
So you think I should also provide this mechanics:
% + % (LoL)
% x % (that's results into something simlear to the current but scales much stronger)

Edit: Thanks for your Feedback Pyrogasm, I uploaded Version 1.03.
 
Last edited:
I now wrote a vJass Version. It is placed in this post. The vJass Version does not contain an equal to udg_CdAdjustBuff.
JASS:
library CooldownAdjust initializer Init
//CooldownAdjust 1.04b
//By Tasyen

// CooldownAdjust extends / shortens cooldowns of any casted ability, based on casters current cooldownAdjustmentValue.
// The casters current cooldownAdjustmentValue is calced on Spellcasting.
// This value then affects the cooldown only for an small moment, the spell casting.
// Cooldownadjustment supports 2 ways of interpretations of cooldownAdjustmentValue: Linear (LoL Like) or armor Like.
// Sources of cooldownAdjustment are Buffs / Abilities which are saved in array form.
// One can bind Sources to groups to use only the best current available of the group.
// It's also possible for a cooldownAdjustment-Source to affect only itemSpells or normalSpells or both.

// Morph Spells / Raise skeleton based spells won't be affected.
// warcraft 3 V1.30.1 crashes when manipulating mana or cooldown with the blz natives of Morph Spells / Raise skeleton based spells. Also the other Raise skeleton spells from avatar and crypta lord
// Cause of that any orders of that spell are hardcoded excluded from CooldownAdjust, inside "private function FilterMorphs takes nothing returns boolean"

//=======================
//API
//=======================
//   public function GetValue takes unit caster, boolean itemSpell returns real
//       - returns the cooldownAdjustmentValue caster would have

//   function CooldownAdjust_CalcMulti takes real value returns real
//       converts cooldownAdjustmentValue in an multiplier respecting "CAP_SHORTEN" and "CAP_EXTENSION"

//1 Line setters
//   function CooldownAdjust_RegisterSuper takes integer abilityId, real value, real valuePerLevel, integer adjustType, integer stackingGroup returns nothing
//       - when an unit has abilityId it gains CooldownAdjustValue: value + valuePerLevel * abilityLevel (buffs allways use level 1).
//       - if the adjustType is 0 it affects all spells, with 2 spells from items, with 1 unit spells
//       - only the best of one stackingGroup is used, if an unit has multiple sources of cooldown adjustments of the same group. stackingGroup = 0 for free stacking.

//   function CooldownAdjust_Register takes integer abilityId, real value, integer adjustType returns nothing
//       -wrapper, no stackingGroup, only 1 level

//   function CooldownAdjust_Exclude takes integer abilityId returns nothing
//       - exclude that ability beeing affected from CooldownAdjust
//=======================
globals
//Definitions
   private constant real THRESHOLD = 0.5 //Casted abilities with a default cooldown of this or less seconds, won't have their cooldown touched.
   private constant real RATIO = 0.01   //How Strong is each point
   private constant real CAP_EXTENSION = 3.0   //Maximum cooldown Multi; used by - value, 3.0 => Cooldown can be extended to 3 times as long
   private constant real CAP_SHORTEN = 0.10   //Minimum cooldown Multi; used by + value, 0.6 => Cooldown can be shortend to 60%
   private constant boolean LINEARSCALE = false   // true = each point reduces cooldown by RATIO%   false = armor like, the first ones give much % the later ones give less %.
   public trigger Trigger = CreateTrigger()   //Access CooldownAdjust_Trigger
//0s Timer variables to reach cooldown started. Don't Touch them.
   private unit array timerUnit
   private integer array timerSpell
   private real array timerAdd
   private integer timerLastIndex = 0
   private timer revertCd = CreateTimer()
// DataVariables
   public integer array cooldownSource //If an unit has this ability on SpellEffect it will be affected by this cooldownSource.
   public integer array cooldownGroup // a cooldownGroup is a number, all cooldownSources with the same number will be considered as one group. Only the highest absolute cooldownValue of each group is used. (-30 overpowers 28 -> Results into 28 CooldownReduction to 30 CooldownExtension) // use 0 to stack freely.// I personaly use an abilityId like 'A000' the one first beeing added to that group which is treated as represent of that group.
   public integer array affectionType   //affect itemSpells or/and normal spells   0 = affect both; 1 affect normal spells; 2 = affect itemSpells
   public real array value           //the baseValue of this CdAdjustment-Source
   public real array valuePerLevel //value + valuePerLevel * cooldownSourceLevel = total value
   public integer sourcesCounter = 0   //uses 1 to sourcesCounter
//ExcludeList 
   public integer array exclude // this castable spells are not affected by cooldownAdjust
   public integer excludeCounter = 0 //uses 1 to excludeCounter
endglobals
//=======================
// Linear Scaling
//   Each point affects the value with RATIO.

// Example:
// the buff of Brillianz-Aura reduces Cooldowns by 10%.

// we use RATIO = 0.01
// then we use register the Brillianz-Aura-Buff as source of CooldownAdjust with "function CooldownAdjust_Register" takes integer abilityId, real value, integer adjustType returns nothing
// ->
// call CooldownAdjust_Register('BHab', 10, 0)

// with this line: any caster having the buff 'BHab' will gain 10 CooldownAdjustValue. Each point interpretated as 1% cooldown reduction. the 0 means affect item and normal spells.
// so the caster would now have 10% CooldownReduction.

// if one would want to have 20%, one just takes the double.
// call CooldownAdjust_Register('BHab', 20, 0)

//=======================
// Armor Like
// The first points give alot of %, while later points give less %.
// Cause of that it can be quite complicated to calc the wanted amount of CdAdjust, which also is done from point zero.

// How to calc a % cooldown reduction?

// use this equation to calculate the +CdAdjustValue.

//   value = ( 1 / RATIO ) / ( 1 /wanted Reduction - 1)

// Example: RATIO = 0.01, wanted % = 20%

//   value = 100 / (1/0.2 - 1)
//   -> 100 / (5 - 1) = 100/4 = 25
// 25 cooldownAdjustmentValue would reduce cooldown by 20%.

// Let's test the tripple, how much one needs for 60%?
//   100 / (1/4/5 -1)
//   100 / (5/4 - 1)
//   100 / (3/4) = 400 / 3 = 133 + 1/3
// you see to reach 60% Reduction one would need 133.33 Value beeing more than 5.3 times of the value needed for 20% reduction.

//=======================
//negative cooldownadjustment scales always linear and extends the cooldown.

// With RATIO 0.01 each -1 cooldownAdjustmentValue extends cooldown by 1%, so -100 would double the cooldown, -200 would tripple it.
//=======================
// Avoid Cooldown Adjustment
//   There are multiple Options to avoid cooldownAdjustment in unwanted situations.

//   First there is the "exclude" Ability-List, this List can contain all Spells you want to be unaffected by CooldownAdjust.
//   Second there is the definition function "CooldownAdjustThreshold", Every spell with a default cooldown (the global one) below or equal to this definition will be ignored.
//   Third there is the definition function "CooldownAdjustFilter", A function in which you have access to caster, usedItem (if there is one), and to the used Spell let it return false if you want the current spllcast untouched from CooldownAdjust.
//   Fourth deactivade CooldownAdjust_Trigger
//=======================

private function customFilter takes unit caster, integer spell, integer level, item usedItem returns boolean

   if BlzGetAbilityCooldown(spell,level) <= THRESHOLD then
       return false
   endif 
   return true
   //customizeable filter
   //return true to allow cooldownadjustment for this situation
   //return false to disallow cooldownadjustment for this situation
endfunction

//============
//Code
//=======================

function LogBasedRising takes real value, real power, real RATIO returns real
   if power >= 0 then
       set power =  (power * RATIO) / (power * RATIO + 1 )
       return value - value * power
   else
       set power = - power
       return value + value * power * RATIO   //negative power is linear
   endif
endfunction

private function ReverteCdChange takes nothing returns nothing
   local integer level
   local real cd
   loop   //Revert the cooldown change done for all casters in the list
       exitwhen timerLastIndex == 0
       set level = GetUnitAbilityLevel(timerUnit[timerLastIndex], timerSpell[timerLastIndex])
       set cd = BlzGetUnitAbilityCooldown(timerUnit[timerLastIndex], timerSpell[timerLastIndex], level)
       call BlzSetUnitAbilityCooldown(timerUnit[timerLastIndex], timerSpell[timerLastIndex], level, cd - timerAdd[timerLastIndex])
       set timerLastIndex = timerLastIndex - 1
   endloop
endfunction
public function CalcMulti takes real cooldownValue returns real
   if cooldownValue != 0 then //Is there a cooldownValue?
       if LINEARSCALE then
           set cooldownValue = 1.0 - cooldownValue * RATIO //Linear
       else 
           set cooldownValue = LogBasedRising(1, cooldownValue, RATIO) // Armor Like
       endif
            
       if cooldownValue > CAP_EXTENSION then // Extension Cap
           return CAP_EXTENSION
       elseif cooldownValue < CAP_SHORTEN then // Reduction Cap
           return CAP_SHORTEN
       else //Normal cooldownValue
           return cooldownValue
       endif 
   else  //No Adjustment
       return 1.0
   endif 
endfunction
public function GetValue takes unit caster, boolean itemSpell returns real
   local integer index = sourcesCounter
   local integer array groupsUsed
   local integer groupsUsedLast = 0
   local integer newGroupChecker
   local integer level
   local real array groupsUsedMax
   local real result = 0
   local real currentValue
   local boolean newGroup

   loop   //Loop all possible Cooldown Reductions
       exitwhen index == 0
       // Has this Cooldown Reduction, also does this affect Items or this is no itemSpell?
       set level = GetUnitAbilityLevel(caster, cooldownSource[index])
       if level != 0 and ( (affectionType[index] == 0) or ( affectionType[index] == 1 and not itemSpell) or (affectionType[index] == 2 and itemSpell) ) then
           set currentValue = value[index] + valuePerLevel[index] * level
           if cooldownGroup[index] != 0 then //uses Stacking Groups?
               set newGroup = true
               set newGroupChecker = groupsUsedLast
               loop //Test for beeing a new Cooldown Reduction Group
                   exitwhen newGroupChecker == 0
                   if groupsUsed[newGroupChecker] == cooldownGroup[index] then //same group?
                       if RAbsBJ(groupsUsedMax[newGroupChecker]) < RAbsBJ(currentValue) then //only the absolute strongest of one group is used. -100 will overpower, 99 completly
                           set groupsUsedMax[newGroupChecker] = currentValue
                       endif
                       set newGroup = false
                       exitwhen true   //each Group is used only once.
                   endif
                   set newGroupChecker = newGroupChecker - 1
               endloop
               if newGroup then //New Group -> add it to the list
                   set groupsUsedLast = groupsUsedLast + 1
                   set groupsUsedMax[groupsUsedLast] = currentValue
                   set groupsUsed[groupsUsedLast] = cooldownGroup[index]
               endif 
           else // free stacking
               set result = result + currentValue
           endif
       endif
       set index = index - 1
   endloop
   loop //calc in the bests of all found CooldownGroups
       exitwhen groupsUsedLast == 0
       set result = result + groupsUsedMax[groupsUsedLast]
       set groupsUsedLast = groupsUsedLast - 1
   endloop
   return result
endfunction
private function OnEffect takes nothing returns nothing
   local unit caster = GetTriggerUnit()
   local integer spell = GetSpellAbilityId() 
   local integer spellOrder = GetUnitCurrentOrder(caster)
   local integer level = GetUnitAbilityLevel(caster, spell)
   local real cd  = BlzGetUnitAbilityCooldown(caster, spell, level)
   local boolean itemSpell = ( spellOrder >= 852008 and spellOrder <= 852013)
   local real cooldownAdjustmentValue
   local integer excludeList = excludeCounter
   local item usedItem
   if itemSpell then
       set usedItem = UnitItemInSlot(caster, spellOrder - 852008)
   else
       set usedItem = null
   endif 
   if customFilter(caster, spell, level, usedItem) then   //Does this ability pass the cooldown THRESHOLD, and the customizeable Filter?
       loop
           exitwhen excludeList == 0
           if spell == exclude[excludeList] then   //Excluded spell?
               set caster = null
               return
           endif 
           set excludeList = excludeList - 1
       endloop
       set cooldownAdjustmentValue = GetValue(caster, usedItem != null)
       if cooldownAdjustmentValue != 0 then
           set cd = BlzGetUnitAbilityCooldown(caster, spell, level)
           set timerLastIndex = timerLastIndex + 1
           set timerUnit[timerLastIndex] = caster
           set timerSpell[timerLastIndex] = spell
           set timerAdd[timerLastIndex] =  cd * CalcMulti(cooldownAdjustmentValue) - cd
           call BlzSetUnitAbilityCooldown(caster, spell, level, cd + timerAdd[timerLastIndex])
           //call BJDebugMsg(R2S(BlzGetUnitAbilityCooldown(caster, spell, level)))
           call TimerStart(revertCd, 0.00, false, function ReverteCdChange)
       endif
   endif
   set caster = null
   set usedItem = null
endfunction

public function RegisterSuper takes integer abilityId, real valueArgument, real valuePerLevelArgument, integer adjustType, integer stackingGroup returns nothing
   set sourcesCounter = sourcesCounter + 1
   set cooldownSource[sourcesCounter] = abilityId
   set value[sourcesCounter] = valueArgument
   set valuePerLevel[sourcesCounter] = valuePerLevelArgument
   set affectionType[sourcesCounter] = adjustType
   set cooldownGroup[sourcesCounter] = stackingGroup
endfunction

public function Register takes integer abilityId, real value, integer adjustType returns nothing
   call RegisterSuper(abilityId, value, 0, adjustType, 0)
endfunction

public function Exclude takes integer abilityId returns nothing
   set excludeCounter = excludeCounter + 1
   set exclude[excludeCounter] = abilityId
endfunction
private function FilterMorphs takes nothing returns boolean
   //Warcraft 3 1.30.1, Morphings spells crash when chaing cooldown of such spells(thanks Jampion)#
   //This function filters them out
   local integer unitOrder = GetUnitCurrentOrder(GetTriggerUnit())
   //call BJDebugMsg(OrderId2String(unitOrder))
   if unitOrder == OrderId("burrow") or unitOrder == OrderId("unburrow") or unitOrder == OrderId("sphinxform") or unitOrder == OrderId("unsphinxform") or unitOrder == OrderId("bearform") or unitOrder == OrderId("unbearform") or unitOrder == OrderId("ravenform") or unitOrder == OrderId("unravenform") or unitOrder == OrderId("root") or unitOrder == OrderId("unroot") or unitOrder == OrderId("etherealform") or unitOrder == OrderId("unetherealform") or unitOrder == OrderId("corporealform") or unitOrder == OrderId("uncorporealform") or unitOrder == OrderId("submerge") or unitOrder == OrderId("unsubmerge") or unitOrder == OrderId("robogoblin") or unitOrder == OrderId("unrobogoblin") or unitOrder == OrderId("metamorphosis") or unitOrder == OrderId("stoneform") or unitOrder == OrderId("unstoneform") or unitOrder == OrderId("chemicalrage") or unitOrder == OrderId("elementalfury") then
       //call BJDebugMsg("Filter")
       return false
   endif
    //Warcraft 3 1.30.1, Also necrotic spells crash
   //instant = raisedead autocast
   if unitOrder == OrderId("raisedead") or unitOrder == OrderId("instant")or unitOrder == OrderId("vengeance") or unitOrder == OrderId("vengeanceinstant") or unitOrder == OrderId("carrionscarabs") or unitOrder == OrderId("carrionscarabsinstant") then
       //call BJDebugMsg("Filter")
       return false
   endif
   //Some orders with unmetamorph
   if unitOrder == OrderId("smart") or unitOrder == OrderId("attack") or unitOrder == OrderId("patrol") or unitOrder == OrderId("holdposition") then
       //call BJDebugMsg("Filter")
       return false
   endif
   return true
endfunction

private function Init takes nothing returns nothing
   local integer index = 0
   call TriggerAddAction(Trigger, function OnEffect)
    call TriggerAddCondition(Trigger, Condition(function FilterMorphs))
   loop
       call TriggerRegisterPlayerUnitEvent(Trigger, Player(index), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)
       set index = index + 1
       exitwhen index == bj_MAX_PLAYER_SLOTS
   endloop
endfunction
//=======================
endlibrary

Example:
JASS:
    call CooldownAdjust_Register('B000', 25, 1) //the Buff 'B000' increases CooldownAdjustmentValue for spells by 25
    call CooldownAdjust_Register('AIa3', 25, 0) //the Ability 'AIa3' increases CooldownAdjustmentValue by 25
    call CooldownAdjust_Register('AIi3', 25, 0)
    call CooldownAdjust_Register('AIs3', 25, 0)
    call CooldownAdjust_Register('AIs6', -100, 0)
    call CooldownAdjust_Register('AIau', 100, 2) //the Ability 'AIau' increases CooldownAdjustmentValue for ItemSpells by 100

ChangeLog:
1.04b) filters elementalfury, chemicalrage
1.04a) added stoneform, some metamoph cases and raise skeletons.
1.04) added a morph order filter. fixed forgetting null, changed arguments of GetValue to (unit ,boolean), the timer is now 0.00 previously 0.01
 
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
I checked out the vJASS version. Adjusting cooldowns when they are cast is a smart idea to catch all possible abilities a unit can use.

I don't understand why the GetValue function has an item parameter. As far as I can tell it is only used to determine whether cooldown modifiers that only affect spells or only items are considered. In that case a simply boolean variable would be better. usedItemClass is not used in that function.

Changing the cooldown of morphing abilities crashes the game when they are used. You should add it in your documentation to put them in the blacklist or use the custom filter to make sure their cooldown is never changed.

In the OnEffect function you should null usedItem.

In the test map I used I am getting a lot of crashes. After removing the new natives I didn't get any more crashes, though it might just have been luck.
I placed a ton of fully upgraded casters with infinite mana and made them fight each other. I used Bloodlust and Heal as cooldown adjusting buffs.
Even though I disabled the system from changing cooldowns (by setting the custom filter to return false instead of true) it would reliably crash.
It didn't crash when the entire trigger was disabled or when all new natives were dsiabled. That's why I assume it could be the natives.

I upload the map, so you can see for yourself. I added wrappers for the new natives, so I can easily disable them. It is at the top of the library.
 

Attachments

  • CooldownAdjust.w3x
    31.8 KB · Views: 85
I don't understand why the GetValue function has an item parameter
I had an Idea allowing to reduce only itemspells beeing on specific itemClasses, Which is difficult when one does not know the item.
But yeah in the current form it does not make much sense. And a boolean argument would be more simple/Better.

Changing the cooldown of morphing abilities crashes the game when they are used. You should add it in your documentation to put them in the blacklist or use the custom filter to make sure their cooldown is never changed.
Did not know morphing are crashing when cooldown change was used on them.
But that is a problem with the native itself. Even if you simply change the cooldown outside of any cast event the game will crash as soon you press the morph button.

Could add an order filter, filtering the orders of any Morphing and Un Morphing. Then they would be unaffected

In the OnEffect function you should null usedItem.
Y, will fix that.



Your map also crashes for me.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
But that is a problem with the native itself. Even if you simply change the cooldown outside of any cast event the game will crash as soon you press the morph button.
That's why I meant to include it in the documentation. It has nothing to do with this system. I noticed these crashes when I implemented cooldown reduction in my map.
Could add an order filter, filtering the orders of any Morphing and Un Morphing. Then they would be unaffected
That would probably be the best solution, so the user doesn't have to blacklist all the abilities manually. Maybe it even makes sense to use a hashtable, so you don't have to go through all orders of morphing abilities. I think there are quite a lot of them.

One of the crashes comes from polymorph, another morphing ability which I forgot to exclude. Another one comes from Raise Dead (also Spirit of Vengeance). Carrion Beetles just don't get summoned at all (spell is casted, cooldown, corpse disappears), because they have infinite duration.

After adding both spells to the exclude list I was able to finish one fight without crashing.
 
I have to change cooldown instantly to affect the current spellcasting, cause Cooldown timer starts instantly after Effect Event.
One could argue: Would usage of an earlier Spell-Event solve the trouble? But I do not think so.

The crashs occur when manipulating morphing / raise dead( any variation) spells. This spells bugg (upto crash), if you use the new Blz cooldown/Manacost natives onto them. Regardless if the manipulation is done during a spell casting or outside of such.
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,464
Are the morph spells on both forms of the unit? Or does one have it and the other doesn't?

What about the summoned units? So animate dead, resurrection and raise skeleton?

Sounds like it's bug report time.

The new spell system has cooldown detection but not manipulation. I figure systems like this one shouldn't be directly integrated.
 
Are the morph spells on both forms of the unit? Or does one have it and the other doesn't?
Tested with basic melee spells, so both forms have the morph spell.

What about the summoned units?
I tested waterelemental, spiritwolf, raise skeletons(avatar of revenge) / (necromnacer) / ( anub ), resurrection and animate dead(death knight).

Buggy were the ones sharing the raise skeleton mechanic (necromancer...).
Pala and death knight ulties were fine from my easy test.

The new spell system has cooldown detection but not manipulation. I figure systems like this one shouldn't be directly integrated.
Depends how much you want your system to do. But cooldown manipulation proably wouldn't fit into your spell system, cause Spell System makes the spell creation more easy.
While cooldown manipulations is more like changing "attackspeed" for abilities.
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
I think the resource works well, but I have some suggestions to make it faster and easier to modify cooldowns.

Right now it is kind of 2 functionalities in one system: create cooldown modifying effects and change cooldown on spell usage
I think it would be good to decouple them in some way. For instance if one could define a custom GetValue function, one could manually set the cooldown adjustment value and would not need to use the cooldown modifiers from this library. So you could have a map where every hero has stats stored in variables and you would want to look up the stat in the variable rather than recalculating it based on abilities the hero has. Increasing cooldown reduction when the hero levels up or you research an upgrade could be handled without the need to create new abilities for that.

The FilterMorph conditions are growing quite large. I think it would be good to use a hashtable to store excluded ability ids and order strings. You could then have two functions for exclusion: one for ability ids and one for order strings
In the init function you could call a function that excludes all the morph strings by default.
This would probably be faster than checking for 30+ order strings (which also calls OrderId every time) and also easier to read:
JASS:
private function ExcludeMorphs takes nothing returns nothing
    call ExcludeOrder("burrow")
    call ExcludeOrder("unburrow")
    call ExcludeOrder("sphinxform")
    ...
endfunction

I am a little bit worried about the performance. On every spell effect event you first run 30+ order string comparisons and then you loop through all possible sources of cooldown reduction.
The spell effect events tend to have a lot of actions connected, if you have a complex spell for example. That's why I think it would make sense to try to keep it as performant as possible.
 
Maybe it was one the abilities that are filtered by orderId in older versions of Warcraft 3 they crashed the game :( . Hence this resource has wierd filters.

And I didn't updated this version to consider that the new instance api starts with index 0 for level 1. Which it did not when it was released.
I created a Lua version of this and didn't updated the (v)jass version since then. (Although I did not upload the Lua version)

Edit: To fix the jass version for V1.31+ one would change 2 lines:

256: local integer level = GetUnitAbilityLevel(caster, spell)
->
local integer level = GetUnitAbilityLevel(caster, spell) - 1

173: set level = GetUnitAbilityLevel(udg_CdAdjust__Unit[udg_CdAdjust__Last], udg_CdAdjust__Spell[udg_CdAdjust__Last])
->
set level = GetUnitAbilityLevel(udg_CdAdjust__Unit[udg_CdAdjust__Last], udg_CdAdjust__Spell[udg_CdAdjust__Last]) - 1
 
Last edited:
Top