• 🏆 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!

Real, actual attack indexing has been achieved

Another major achievement has been made: tracking a unit's attacks from the attacked event to the damage event (attack indexing). This attaches attack data to the weapon type (weapon sound) of the unit in order to correlate a particular damage event with its attack.

Attack indexing was a vision Nestharus had many years ago, covered in Indexing Vanilla Warcraft 3 Attacks Solved . However, it never picked up because it was based on setting a unit's attack damage and this caused an interface problem (as well as requiring that every unit's damage dice in Object Editor be pre-defined). This could have been mitigated somewhat by the new Blizzard natives that allow a damage base/dice to be set at-will, but this trick works much better because the end-user is completely unaware of anything happening with the attacks (because it doesn't change the UI at all).

This is somewhat requirement-heavy at the moment, and I'm not pleased with having to use timers to manually-expire evaded attacks, but it is an entirely-working concept. I've attached a demo map which shows unreleased edits to some of the dependencies (which I'll get to, but I wanted to hammer away at this first).

I eventually plan to build this directly into Lua Damage Engine (as currently there is a lot of hardcoded stuff). It should reduce the overall code length, complexity and efficiency that way. I should probably give vJass Damage Engine one more update, but that won't be until much later, if at all. vJass2Lua is working for almost all cases, and I've been re-writing many popular vJass libraries into efficient Lua to help incentivize people to switch over.

Lua:
OnGlobalInit(function()
--[[
    AttackIndexer version 0.1.0.0 by Bribe
    Years ago, Nestharus had "cracked" the concept of attack indexing with an idea to set a unit's damage
    to some kind of indexing value in order to track the flow of events from "unit is attacked" to "unit
    is damaged".
    Instead of setting damage, this is setting a unit's "weapon sound", which is an integer between 0 and 23.
    This gives us access to 24 unique damage indices per unit at a time, or 12 if we want to distinguish
    between attack 1 and attack 2 (which this system does). WarCraft 3 does not allow you to set the weapon
    type nor attack type to be out-of-bounds for this sort of thing.
  
    Disclaimer: Max simultaneous attacks before damaging
    While this does not happen in normal WarCraft 3, the system could bug in a custom map with a unit with a
    very long range combined with very slow projectiles and a very fast attack. Depending on the environment,
    it is possible to use attacktypes in combination with changing a unit's weapon type to have up to 84
    distinct indices, or 168 if we ditched the weapon index detection. However, if I were to do that, attack
    type variations would have a UI impact as the unit will constantly be alternating between pierce/siege/
    hero/chaos/etc. I think what I've done here is the most practical and functional approach, and will work
    in all normal scenarios.
  
    Credits:
        Nestharus for the concept of indexing attacks based on a property of the attacking unit:
            https://www.hiveworkshop.com/threads/indexing-vanilla-warcraft-3-attacks-solved.253120/
        MyPad and Lt_Hawkeye for doing investigative work into the Blz integer fields.
            https://www.hiveworkshop.com/threads/list-of-non-working-object-data-constants.317769
        Almia, even though timers didn't work in all cases, thank you for making the effort.
            https://www.hiveworkshop.com/threads/attack-indexer.279304/
]]
    local _PRIORTY                  = -99999    --should be lower than any other damage event.
    local _USE_MELEE_RANGE          = true      --should line up with whatever you have Damage Engine configured to.
    local _EXPIRE_AFTER             = 20        --expire an attack after this many seconds of it not hitting
    local _AUTO_EXPIRE_ON_ATTACK    = true      --whether to expire an attack each time it attacks after _EXPIRE_AFTER seconds.
    local _AUTO_EXPIRE_ON_REMOVAL   = false     --when a unit is removed from the game, wait _EXPIRE_AFTER seconds before removing any
                                                --of their attacks (useful only if the above is set to false).
    Event.attack = Event.create("udg_AttackEvent", EQUAL)
    Event.attackCleanup = Event.create("udg_AttackEvent", NOT_EQUAL)
    local attackFunc = 13
    local damageFunc = 14
    local numAttacks = 15
    local get = BlzGetUnitWeaponIntegerField
    local set = BlzSetUnitWeaponIntegerField
    local weapon = UNIT_WEAPON_IF_ATTACK_WEAPON_SOUND
    ---@class attackIndexTable : table
    ---@field source unit
    ---@field target unit
    ---@field index integer
    ---@field data integer
    ---@field private active boolean
    local attackIndexTable = {} ---@type attackIndexTable[]
    ---@return attackIndexTable
    local function getTable()
        local data = Event.args
        if data then
            return data[1]
        else
            return Damage.index.attack
        end
    end
  
    --[[Optional GUI compatibility.
      
        AttackEventSource will return the attacking unit in "AttackEvent" or any of DamageEngine's events.
        AttackEventTarget will return the attacked unit in "AttackEvent" or any of DamageEngine's events (even if it is different from the damaged unit)
            This means that for an AOE attack or a multishot (barrage) attack, the AttackEventTarget can still be read successfully as the originally-attacked unit.
        AttackEventIndex is unknown at the time that the "AttackEvent" runs, but is set to 0 or 1 depending on whether the unit used its first attack or its second.
            If it is -1, that means it's not been set yet. So if it is -1 from an AttackEvent Not Equal event, then the attack never hit (e.g. evasion/curse/hasn't hit yet due to slow projectile).
        AttackEventData is meant to be set from an AttackEvent and read by a Damage event. See the below GUI pseudo-code for an example.
      
        Event:
            AttackEvent
        Actions:
            Set AttackEventData = DamageTypeCriticalStrike
        ...
        Event:
            PreDamageEvent
        Actions:
            Set DamageEventType = AttackEventData
    ]]
    if GlobalRemap then
        GlobalRemap("udg_AttackEventSource", function() return getTable().source end)
        GlobalRemap("udg_AttackEventTarget", function() return getTable().target end)
        GlobalRemap("udg_AttackEventIndex", function() return getTable().index end)
        GlobalRemap("udg_AttackEventData", function() return getTable().data end, function(val) getTable().data = val end)
    end
    ---@param attack attackIndexTable
    local function cleanup(attack)
        if attack.active then
            local data = attackIndexTable[attack.source]
            data[numAttacks] = data[numAttacks] - 1
            attack.active = nil
            Event.attackCleanup:run(attack)
        end
    end
    local currentAttack ---@type table
    Damage.register(Damage.damagingEvent, function()
        local damage = Damage.index ---@type damageInstance
        if damage.isAttack and not damage.isCode then
            local unit = damage.source
            local data = attackIndexTable[unit]
            if data then
                local attackPoint = GetHandleId(damage.weaponType)
                local tablePoint = attackPoint // 2
                local offset = (tablePoint * 2 == attackPoint) and 0 or 1 --whether it's using the primary or secondary attack
                local attack = data[tablePoint + 1]
                if currentAttack ~= attack then --make sure not to re-allocate for splash damage.
                    if currentAttack then cleanup(currentAttack) end
                    currentAttack = attack
                    currentAttack.index = offset
                    damage.attack = currentAttack
                    data[damageFunc](damage, offset)
                end
            end
        end
    end, _PRIORTY)
    Damage.register(Damage.sourceEvent, function()
        if currentAttack then
            cleanup(currentAttack)
        end
        currentAttack = nil
    end).minAOE = 0
  
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ATTACKED, function()
        local data = attackIndexTable[GetAttacker()]
        if data then data[attackFunc]() end
    end)
  
    UnitEvent.onCreate(
    ---@param ut UnitEvent
    function(ut)
        local attacker = ut.unit
        local data = {}
        attackIndexTable[attacker] = data
        local original, point ---@type integer
        data[attackFunc] =
        function()
            if original then
                data[numAttacks] = data[numAttacks] + 1
            else
                data[numAttacks] = 1
                point = 0
                original = {
                    get(attacker, weapon, 0),
                    get(attacker, weapon, 1)
                }
            end
            local thisAttack = { ---@type attackIndexTable
                source = attacker,
                target = GetTriggerUnit(),
                active = true,
                index = -1
            }
            set(attacker, weapon, 0, point*2)
            set(attacker, weapon, 1, point*2 + 1) --the system doesn't know if attack 0 or 1 is used at this point. Try them both.
            point = point + 1
            local old = data[point]
            if old and old.active then
                cleanup(old)
            end
            data[point] = thisAttack
            if point > 11 then point = 0 end
            Event.attack:run(thisAttack)
            if _AUTO_EXPIRE_ON_ATTACK then
                Timed.call(_EXPIRE_AFTER, function()
                    if thisAttack.active then
                        cleanup(thisAttack)
                    end
                end)
            end
        end
        data[damageFunc] =
        function(damage, offset)
            damage.weaponType = original[offset]
            if _USE_MELEE_RANGE and damage.isMelee and offset == 1 and original[2] == 0 and IsUnitType(attacker, UNIT_TYPE_RANGED_ATTACKER) then
                damage.isMelee = nil
                damage.isRanged = true
            end
        end
    end)
    UnitEvent.onRemoval(function(ut)
        local data = attackIndexTable[ut.unit]
        if data then
            attackIndexTable[ut.unit] = nil
            if _AUTO_EXPIRE_ON_REMOVAL and not _AUTO_EXPIRE_ON_ATTACK and data[numAttacks] > 0 then
                Timed.call(_EXPIRE_AFTER, function()
                    for i=1, 12 do
                        if data[i].active then
                            cleanup(data[i])
                        end
                    end
                    data = nil
                end)
            end
        end
    end)
end)
 

Attachments

  • Lua Attack Indexer.w3x
    98.4 KB · Views: 18
Last edited:

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,570
Does this mean that the days of using the "A unit is attacked" Event in order to make things like a Weapon Ammo system will finally have a proper solution? If so, nice job! If not, I'm confused but it sounds awesome regardless.

Also, I get an error at the start of the test map:
2489: attempt to index a nil value (global 'UnitEvent')
 
Does this mean that the days of using the "A unit is attacked" Event in order to make things like a Weapon Ammo system will finally have a proper solution? If so, nice job! If not, I'm confused but it sounds awesome regardless.

Also, I get an error at the start of the test map:
"2489: attempt to index a nil value (global 'UnitEvent')
Thanks - I've just fixed up the demo map, and it was also missing a string to actually show what it was doing. You can see now whenever a damage event wraps up, that it displays the correct attacked unit from the Mortar Team, and will do the same thing for barrage (though barrage isn't in the demo map). It also shows which attack is used (attack 1 or attack 2). The Mountain Giant will switch from its first attack to its second attack when it picks up a tree.

This links the "unit is attacked" event with the "damaging" event for follow-through, but the actual missile launch event is nonexistent. If the user spams the "stop" order, it will cost the unit ammo each time the event fires (if you base the ammo depletion event on the attacking event).
 

Uncle

Warcraft Moderator
Level 64
Joined
Aug 10, 2018
Messages
6,570
Thanks - I've just fixed up the demo map, and it was also missing a string to actually show what it was doing. You can see now whenever a damage event wraps up, that it displays the correct attacked unit from the Mortar Team, and will do the same thing for barrage (though barrage isn't in the demo map). It also shows which attack is used (attack 1 or attack 2). The Mountain Giant will switch from its first attack to its second attack when it picks up a tree.

This links the "unit is attacked" event with the "damaging" event for follow-through, but the actual missile launch event is nonexistent. If the user spams the "stop" order, it will cost the unit ammo each time the event fires (if you base the ammo depletion event on the attacking event).
I see, I've definitely run into problems related to this so it's great to finally have a solution. I'll test it again.
Edit: Works great. I can already think of a few use cases for it.
 
Last edited:
Here's the next big thing: projectile is launched event. Maps with a lot of customizations to their attack speed abilities/items will need to do some manual tooling, but this now takes into account all natural attack speed modifiers WarCraft 3 has.

Tomorrow, I'll be starting work on an event that detects when the attack is canceled (as my focus has been on getting the timing right), which is needed in order to ensure the accuracy of this new event I've created.

By the end of the week, my plan is to have this completed and submitted as a resource.

Lua:
OnGlobalInit(function()
--[[
    AttackIndexer version 2.0.0.0 (beta) by Bribe
    Years ago, Nestharus had "cracked" the concept of attack indexing with an idea to set a unit's damage
    to some kind of indexing value in order to track the flow of events from "unit is attacked" to "unit
    is damaged".
    Instead of setting damage, this is setting a unit's "weapon sound", which is an integer between 0 and 23.
    This gives us access to 24 unique damage indices per unit at a time, or 12 if we want to distinguish
    between attack 1 and attack 2 (which this system does). WarCraft 3 does not allow you to set the weapon
    type nor attack type to be out-of-bounds for this sort of thing.
    
    Disclaimer: Max simultaneous attacks before damaging
    While this does not happen in normal WarCraft 3, the system could bug in a custom map with a unit with a
    very long range combined with very slow projectiles and a very fast attack. Depending on the environment,
    it is possible to use attacktypes in combination with changing a unit's weapon type to have up to 84
    distinct indices, or 168 if we ditched the weapon index detection. However, if I were to do that, attack
    type variations would have a UI impact as the unit will constantly be alternating between pierce/siege/
    hero/chaos/etc. I think what I've done here is the most practical and functional approach, and will work
    in all normal scenarios.
    
    Credits:
        Nestharus for the concept of indexing attacks based on a property of the attacking unit:
            https://www.hiveworkshop.com/threads/indexing-vanilla-warcraft-3-attacks-solved.253120/
        MyPad and Lt_Hawkeye for doing investigative work into the Blz integer fields.
            https://www.hiveworkshop.com/threads/list-of-non-working-object-data-constants.317769
        Almia, even though timers didn't work in all cases, thank you for making the effort.
            https://www.hiveworkshop.com/threads/attack-indexer.279304/
        Everyone on the Hive Discord channel who helped answer some critical questions along the way that
        saved me a lot of time (e.g. MindWorX, Tasyen, WaterKnight).
]]
    local _AGI_BONUS                = 0.02      --should match the gameplay constant for attack speed per agility point
    local _FROST_SPEED_DEC          = 0.25      --defined in the Gameplay Constants as "Spells - Frost Attack Speed Reduction"
    local _MAX_SPEED                = 4         --WarCraft 3 has a hard cap of 400 percent attack speed buff.
    local _MIN_SPEED                = 0.2       --WarCraft 3 has a hard cap of 80 percent attack speed debuff.
    local _SLOW_POISON_DEC          = 0.25      --Slow poison must be hardcoded as it will crash the thread if attempted to be read dynamically
    local _ENDURANCE_AURA_PER_LVL   = 0.05      --Assumes rate of 5 percent speed increase per level is constant amongst all Endurance Aura clones and levels (which is the case by default in WarCraft 3).
    local _BASE_COOLDOWN            = 0.022     --WarCraft 3 adds this to the damage point of all units in-game.
    local _PRIORITY                 = -99999    --should be lower than any other damage event.
    local _USE_MELEE_RANGE          = true      --should line up with whatever you have Damage Engine configured to.
    local _EXPIRE_AFTER             = 5         --expire an attack after this many seconds of it not hitting
    local _AUTO_EXPIRE_ON_ATTACK    = true      --whether to expire an attack each time it attacks after _EXPIRE_AFTER seconds.
    local _AUTO_EXPIRE_ON_REMOVAL   = false     --when a unit is removed from the game, wait _EXPIRE_AFTER seconds before removing any
                                                --of their attacks (useful only if the above is set to false).
                                                
    local buffIndexTable = {}
    local triggeringUnit = GetTriggerUnit
    local getAttackSpeedBonus
    do
        local getAgi            = GetHeroAgi
        local isType            = IsUnitType
        local isHero            = UNIT_TYPE_HERO
        local math              = math
        local rawcode           = FourCC
        local getAbilityLvl     = GetUnitAbilityLevel
        local getSpellAbility   = GetSpellAbility
        local getUnitAbility    = BlzGetUnitAbility
        local getAbilityReal    = BlzGetAbilityRealLevelField
        local getAbilId         = GetSpellAbilityId
        local getTarget         = GetSpellTargetUnit
        local abilities = {}    ---@type table[]
        local debuffs = {}      ---@type table[]
        local buffTypes = {}   ---@type boolean[] indexes based on abil ID or buff ID
        ---Register an ability and buff and their properties.
        ---@param abilId integer
        ---@param buffId integer
        ---@param multiplier integer -1 or 1
        ---@param buffType string
        ---@param field abilityreallevelfield
        local function registerAbil(abilId, buffId, multiplier, buffType, field)
            abilId = rawcode(abilId)
            buffTypes[buffType == "spell" and abilId or buffId] = buffType
            
            local data = {abilId, rawcode(buffId), multiplier, field}
            
            abilities[#abilities+1] = data
            
            if buffType == "damage" then
                debuffs[#debuffs+1] = data --a shorter list to avoid having to check too much on a damage event.
            end
        end
        --Register spells that apply a buff or debuff
        registerAbil("Aslo", "Bslo", -1, "spell",  ABILITY_RLF_ATTACK_SPEED_FACTOR_SLO2)            --Slow
        registerAbil("Acri", "Bcri", -1, "spell",  ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_CRI2) --Cripple
        registerAbil("Ablo", "Bblo",  1, "spell",  ABILITY_RLF_ATTACK_SPEED_INCREASE_PERCENT_BLO1)  --Bloodlust
        registerAbil("Auhf", "BUhf",  1, "spell",  ABILITY_RLF_ATTACK_SPEED_BONUS_PERCENT)          --Unholy Frenzy, field 'Uhf1'
        registerAbil("Absk", "Bbsk",  1, "spell",  ABILITY_RLF_ATTACK_SPEED_INCREASE_BSK2)          --Berserk
        --Register debuffs that are applied on-hit
        registerAbil("AHtc", "BHtc", -1, "damage", ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_HTC4) --Thunderclap
        registerAbil("ACtc", "BCtc", -1, "damage", ABILITY_RLF_ATTACK_SPEED_REDUCTION_CTC4)         --Slam
        registerAbil("AHca", "BHca", -1, "damage", ABILITY_RLF_ATTACK_SPEED_FACTOR_HCA3)            --Cold Arrows
        registerAbil("Aliq", "Bliq", -1, "damage", ABILITY_RLF_ATTACK_SPEED_REDUCTION_LIQ3)         --Liquid Fire
        
        --building a custom function for gloves of haste as I'm sure many custom maps will have several copies of their own variations with different values.
        --There's an ability 'Als2' which is "Item attack speed bonus (greater)", but I can't find the corresponding item ID.
        local gloves = {} ---@type table[]
        local function registerGloves(itemId, abilId)
            gloves[#gloves+1] = {rawcode(itemId), rawcode(abilId)}
        end
        registerGloves("gcel", "Alsx")
        
        --ABILITY_RLF_ATTACK_SPEED_INCREASE_PERCENT_OAE2 is for Endurance Aura (AOae, SCae, AOr2 and AIae*).
        --*Item ability used by 'ajen' (Ancient Janggo of Endurance)
        --None of these have a way for their attack speed bonuses be correctly read, so I have hardcoded their values.
        --[[Set to have 0 attack speed modification, so I'll ignore them:
        
            ABILITY_RLF_ATTACK_SPEED_FACTOR_DEF4            --Defend
            ABILITY_RLF_ATTACK_SPEED_MODIFIER --'Nsi4'      --Silence
            ABILITY_RLF_ATTACK_SPEED_FACTOR_ESH3            --Shadow Strike
            ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_HBN2 --Banish
            ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_NAB2 --Acid Bomb
            ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_NSO5 --Soul Burn
            These completely crash because they read bad memory (attack speed and movement speed are swapped by some kind of implementation error):
            ABILITY_RLF_ATTACK_SPEED_FACTOR_SPO3            --Slow Poison.
            ABILITY_RLF_ATTACK_SPEED_FACTOR_POI2            --Poison Sting.
            ABILITY_RLF_ATTACK_SPEED_FACTOR_POA3            --Poison Arrows.
        ]]
        AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_SPELL_EFFECT, function()
            local id = getAbilId()
            if buffTypes[id] then
                buffIndexTable[getTarget() or triggeringUnit()][id] = getSpellAbility()
            end
        end)
        
        AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_DAMAGED, function()
            local u = triggeringUnit()
            for _,list in ipairs(debuffs) do
                if getAbilityLvl(u, list[2]) > 0 then
                    local abil = list[1]
                    local index = buffIndexTable[u]
                    if not index[abil] then
                        index[abil] = getUnitAbility(GetEventDamageSource(), abil)
                    end
                end
            end
        end)
        local _FROST            = rawcode("bfro")
        local _POISON           = rawcode("Bspo")
        local _ENDURANCE_AURA   = rawcode("BOae")
        getAttackSpeedBonus = function(unit)
            local bonus = 0
            local buffs = buffIndexTable[unit]
            for _,list in ipairs(abilities) do
                local abil = list[1]
                local buff = buffs[abil]
                if buff then
                    local lvl = getAbilityLvl(unit, list[2])
                    if lvl > 0 then
                        local factor = getAbilityReal(buff, list[4], lvl - 1)
                        print(factor)
                        bonus = bonus + factor*list[3]
                    else
                        buffIndexTable[unit][abil] = nil
                    end
                end
            end
            if getAbilityLvl(unit, _FROST) > 0 then
                bonus = bonus - _FROST_SPEED_DEC
            end
            if getAbilityLvl(unit, _POISON) > 0 then
                bonus = bonus - _SLOW_POISON_DEC
            end
            local lvl = getAbilityLvl(unit, _ENDURANCE_AURA)
            if lvl > 0 then
                bonus = bonus + lvl*_ENDURANCE_AURA_PER_LVL
            end
            if isType(unit, isHero) then
                bonus = bonus + _AGI_BONUS*getAgi(unit, true)
                if #gloves > 0 then
                    for i = 0, UnitInventorySize(unit) - 1 do
                        local item = UnitItemInSlot(unit, i)
                        if item then
                            local id = GetItemTypeId(item)
                            for j = 1, #gloves do
                                if id == gloves[j][1] then
                                    bonus = bonus + getAbilityReal(BlzGetItemAbility(item, gloves[j][2]), ABILITY_RLF_ATTACK_SPEED_INCREASE_ISX1, 0)
                                end
                            end
                        end
                    end
                end
            end
            return math.min(math.max(bonus, _MIN_SPEED), _MAX_SPEED)
        end
    end
    Event.attack        = Event.create("udg_AttackEvent", EQUAL)
    Event.attackCleanup = Event.create("udg_AttackEvent", NOT_EQUAL)
    Event.attackLaunch  = Event.create("udg_AttackLaunchEvent", EQUAL)
    Event.attackCancel  = Event.create("udg_AttackLaunchEvent", NOT_EQUAL)
    local attackFunc    = 13
    local damageFunc    = 14
    local numAttacks    = 15
    local queuedAttack  = 16
    local setOriginal   = 17
    local getWeaponInt  = BlzGetUnitWeaponIntegerField
    local setWeaponInt  = BlzSetUnitWeaponIntegerField
    local getWeaponReal = BlzGetUnitWeaponRealField
    local damagePt      = UNIT_WEAPON_RF_ATTACK_DAMAGE_POINT
    local weapon        =  UNIT_WEAPON_IF_ATTACK_WEAPON_SOUND
    ---@class attackIndexTable : table
    ---@field source unit
    ---@field target unit
    ---@field index integer
    ---@field data integer
    ---@field private active boolean
    local attackIndexTable = {} ---@type attackIndexTable[]
    ---@return attackIndexTable
    local function getTable()
        local data = Event.args
        if data then
            return data[1]
        else
            return Damage.index.attack
        end
    end
    
    --[[Optional GUI compatibility.
        
        AttackEventSource will return the attacking unit in "AttackEvent" or any of DamageEngine's events.
        AttackEventTarget will return the attacked unit in "AttackEvent" or any of DamageEngine's events (even if it is different from the damaged unit)
            This means that for an AOE attack or a multishot (barrage) attack, the AttackEventTarget can still be read successfully as the originally-attacked unit.
        AttackEventIndex is unknown at the time that the "AttackEvent" runs, but is set to 0 or 1 depending on whether the unit used its first attack or its second.
            If it is -1, that means it's not been set yet. So if it is -1 from an AttackEvent Not Equal event, then the attack never hit (e.g. evasion/curse/hasn't hit yet due to slow projectile).
        AttackEventData is meant to be set from an AttackEvent and read by a Damage event. See the below GUI pseudo-code for an example.
        
        Event:
            AttackEvent
        Actions:
            Set AttackEventData = DamageTypeCriticalStrike
        ...
        Event:
            PreDamageEvent
        Actions:
            Set DamageEventType = AttackEventData
    ]]
    if GlobalRemap then
        GlobalRemap("udg_AttackEventSource", function() return getTable().source end)
        GlobalRemap("udg_AttackEventTarget", function() return getTable().target end)
        GlobalRemap("udg_AttackEventIndex", function() return getTable().index end)
        GlobalRemap("udg_AttackEventData", function() return getTable().data end, function(val) getTable().data = val end)
    end
    ---@param attack attackIndexTable
    local function cleanup(attack)
        if attack.active then
            local data = attackIndexTable[attack.source]
            data[numAttacks] = data[numAttacks] - 1
            attack.active = nil
            Event.attackCleanup:run(attack)
        end
    end
    local currentAttack ---@type table
    Damage.register(Damage.damagingEvent, function()
        local damage = Damage.index ---@type damageInstance
        if damage.isAttack and not damage.isCode then
            local unit = damage.source
            local data = attackIndexTable[unit]
            if data then
                local attackPoint = GetHandleId(damage.weaponType)
                local tablePoint = attackPoint // 2
                local offset = (tablePoint * 2 == attackPoint) and 0 or 1 --whether it's using the primary or secondary attack
                local attack = data[tablePoint + 1]
                if currentAttack ~= attack then --make sure not to re-allocate for splash damage.
                    if currentAttack then cleanup(currentAttack) end
                    currentAttack = attack
                    currentAttack.index = offset
                    damage.attack = currentAttack
                    data[damageFunc](damage, offset)
                end
            end
        end
    end, _PRIORITY)
    Damage.register(Damage.sourceEvent, function()
        if currentAttack then
            cleanup(currentAttack)
        end
        currentAttack = nil
    end).minAOE = 0
    
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ATTACKED, function()
        local data = attackIndexTable[GetAttacker()]
        if data then data[attackFunc]() end
    end)
    
    UnitEvent.onTransform(function(ut) 
        local data = attackIndexTable[ut.unit]
        if data then data[setOriginal]() end
    end)
    UnitEvent.onCreate(
    function(ut)
        local attacker = ut.unit
        local data = {}
        attackIndexTable[attacker] = data
        buffIndexTable[attacker] = {}
        local original, point ---@type integer
        
        data[setOriginal] =
        function()
            original = {
                getWeaponInt(attacker, weapon, 0),
                getWeaponInt(attacker, weapon, 1)
            }
        end
        data[attackFunc] =
        function()
            if original then
                data[numAttacks] = data[numAttacks] + 1
            else
                data[numAttacks] = 1
                point = 0
                data[setOriginal]()
            end
            local thisAttack = { ---@type attackIndexTable
                source = attacker,
                target = triggeringUnit(),
                active = true,
                index = -1
            }
            setWeaponInt(attacker, weapon, 0, point*2)
            setWeaponInt(attacker, weapon, 1, point*2 + 1) --the system doesn't know if attack 0 or 1 is used at this point. Try them both.
            point = point + 1
            local old = data[point]
            if old and old.active then
                cleanup(old)
            end
            data[point] = thisAttack
            if point > 11 then point = 0 end
            Event.attack:run(thisAttack)
            data[queuedAttack] = function()
                if thisAttack.active then
                    data[queuedAttack] = nil
                    if _AUTO_EXPIRE_ON_ATTACK then
                        Timed.call(_EXPIRE_AFTER, function()
                            if thisAttack.active then
                                cleanup(thisAttack)
                            end
                        end)
                    end
                    print "attack launched"
                    Event.attackLaunch:run(thisAttack)
                end
            end
            Timed.call(_BASE_COOLDOWN + getWeaponReal(attacker, damagePt, 0) / (1 + getAttackSpeedBonus(attacker)), data[queuedAttack])
        end
        data[damageFunc] =
        function(damage, offset)
            damage.weaponType = original[offset]
            if _USE_MELEE_RANGE and damage.isMelee and offset == 1 and original[2] == 0 and IsUnitType(attacker, UNIT_TYPE_RANGED_ATTACKER) then
                damage.isMelee = nil
                damage.isRanged = true
            end
        end
    end)
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ISSUED_ORDER, function()
        local data = attackIndexTable[triggeringUnit()]
        if data then
            if data[queuedAttack] then
                data[queuedAttack].active = nil
            end
        end
    end)
    UnitEvent.onRemoval(function(ut)
        local data = attackIndexTable[ut.unit]
        if data then
            attackIndexTable[ut.unit] = nil
            buffIndexTable[ut.unit] = nil
            if _AUTO_EXPIRE_ON_REMOVAL and not _AUTO_EXPIRE_ON_ATTACK and data[numAttacks] > 0 then
                Timed.call(_EXPIRE_AFTER, function()
                    for i=1, 12 do
                        if data[i].active then
                            cleanup(data[i])
                        end
                    end
                    data = nil
                end)
            end
        end
    end)
end)
 

Attachments

  • Lua Attack Indexer 2 beta.w3x
    151.9 KB · Views: 15
Haven't gotten as far into it as I would have liked to by now, but this is now called AttackEngine, which sounds cool.

Lua:
OnGlobalInit(function()
--[[
    Attack Engine version 0.1.0.0 by Bribe
    Detect when a unit starts an attack, finishes an attack or has its attack interrupted. Data can be
    indexed to the attack itself, making this MAI (multi-attack instantiable, or having "indexed attacks").
    Years ago, Nestharus had "cracked" the concept of attack indexing with an idea to set a unit's damage
    to some kind of indexing value in order to track the flow of events from "unit is attacked" to "unit
    is damaged".
    Instead of setting damage, this is setting a unit's "weapon sound", which is an integer between 0 and 23.
    This gives us access to 24 unique damage indices per unit at a time, or 12 if we want to distinguish
    between attack 1 and attack 2 (which this system does). WarCraft 3 does not allow you to set the weapon
    type nor attack type to be out-of-bounds for this sort of thing.
    
    Disclaimer: Timer system
    Attack Engine uses a timer to detect when a unit actually finishes its attack (either on projectile
    launch, or an instant before the damage event fires for a melee attack). However, maps with custom attack
    speed bonus abilities/items will need to "declare" themselves, and their respective buffs, in the code
    below. I've noted where they can be added.
    
    Disclaimer: Max simultaneous attacks before damaging
    While this does not happen in normal WarCraft 3, the system could bug in a custom map with a unit with a
    very long range combined with very slow projectiles and a very fast attack. Depending on the environment,
    it is possible to use attacktypes in combination with changing a unit's weapon type to have up to 84
    distinct indices, or 168 if we ditched the weapon index detection. What I've done here is the least buggy
    and most practical, functional approach.
    
    Credits:
        Nestharus for the concept of indexing attacks based on a property of the attacking unit:
            https://www.hiveworkshop.com/threads/indexing-vanilla-warcraft-3-attacks-solved.253120/
        MyPad and Lt_Hawkeye for doing investigative work into the Blz integer fields.
            https://www.hiveworkshop.com/threads/list-of-non-working-object-data-constants.317769
        Almia, even though timers didn't work in all cases, thank you for making the effort.
            https://www.hiveworkshop.com/threads/attack-indexer.279304/
        Everyone on the Hive Discord channel who helped answer some critical questions along the way that
        saved me a lot of time (e.g. MindWorX, Tasyen, WaterKnight).
]]
    --values that should align with Gameplay Constants:
    local _AGI_BONUS                = 0.02      --"Hero Attributes - Attack speed bonus per agility point"
    local _FROST_SPEED_DEC          = 0.25      --"Spells - Frost Attack Speed Reduction"
    local _EXPIRE_AFTER             = 5         --expire an attack after this many seconds of it not hitting (should match what's in Gameplay Constants)
    local _MAX_SPEED                = 4         --WarCraft 3 has a hard cap of 400 percent attack speed buff.
    local _MIN_SPEED                = 0.2       --WarCraft 3 has a hard cap of 80 percent attack speed debuff.
    local _SLOW_POISON_DEC          = 0.25      --Slow poison must be hardcoded as it will crash the thread if attempted to be read dynamically
    local _ENDURANCE_AURA_PER_LVL   = 0.05      --Assumes rate of 5 percent speed increase per level is constant amongst all Endurance Aura clones and levels (which is the case by default in WarCraft 3).
    local _ENDURANCE_AURA_RADIUS    = 900       --Assumes the aura radius is consistent at 900 across the board.
    local _PRIORITY                 = -99999    --should be lower than any other damage event.
    local _USE_MELEE_RANGE          = true      --should line up with whatever you have Damage Engine configured to.
    
    local buffIndexTable = {}
    local triggeringUnit = GetTriggerUnit
    local getUnitDamagePoint
    do
        local getAgi            = GetHeroAgi
        local isType            = IsUnitType
        local isHero            = UNIT_TYPE_HERO
        local rawcode           = FourCC
        local getAbilityLvl     = GetUnitAbilityLevel
        local getSpellAbility   = GetSpellAbility
        local getUnitAbility    = BlzGetUnitAbility
        local getAbilityReal    = BlzGetAbilityRealLevelField
        local getAbilId         = GetSpellAbilityId
        local getTarget         = GetSpellTargetUnit
        local abilities = {}    ---@type table[]
        local debuffs = {}      ---@type table[]
        local buffTypes = {}    ---@type boolean[] indexes based on abil ID or buff ID
        local g = CreateGroup()
        ---Register an ability and buff and their properties.
        ---@param abilId integer
        ---@param buffId integer
        ---@param multiplier integer -1 or 1
        ---@param buffType string
        ---@param field abilityreallevelfield
        local function registerAbil(abilId, buffId, multiplier, buffType, field)
            abilId = rawcode(abilId)
            buffTypes[buffType == "spell" and abilId or buffId] = buffType
            
            local data = {abilId, rawcode(buffId), multiplier, field}
            
            abilities[#abilities+1] = data
            
            if buffType == "damage" then
                debuffs[#debuffs+1] = data --a shorter list to avoid having to check too much on a damage event.
            end
        end
        --Register spells that apply a buff or debuff
        registerAbil("Aslo", "Bslo", -1, "spell",  ABILITY_RLF_ATTACK_SPEED_FACTOR_SLO2)            --Slow
        registerAbil("Acri", "Bcri", -1, "spell",  ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_CRI2) --Cripple
        registerAbil("Ablo", "Bblo",  1, "spell",  ABILITY_RLF_ATTACK_SPEED_INCREASE_PERCENT_BLO1)  --Bloodlust
        registerAbil("Auhf", "BUhf",  1, "spell",  ABILITY_RLF_ATTACK_SPEED_BONUS_PERCENT)          --Unholy Frenzy, field 'Uhf1'
        registerAbil("Absk", "Bbsk",  1, "spell",  ABILITY_RLF_ATTACK_SPEED_INCREASE_BSK2)          --Berserk
        --Register debuffs that are applied on-hit
        registerAbil("AHtc", "BHtc", -1, "damage", ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_HTC4) --Thunderclap
        registerAbil("ACtc", "BCtc", -1, "damage", ABILITY_RLF_ATTACK_SPEED_REDUCTION_CTC4)         --Slam
        registerAbil("AHca", "BHca", -1, "damage", ABILITY_RLF_ATTACK_SPEED_FACTOR_HCA3)            --Cold Arrows
        registerAbil("Aliq", "Bliq", -1, "damage", ABILITY_RLF_ATTACK_SPEED_REDUCTION_LIQ3)         --Liquid Fire
        
        --building a custom function for gloves of haste as I'm sure many custom maps will have several copies of their own variations with different values.
        --There's an ability 'Als2' which is "Item attack speed bonus (greater)", but I can't find the corresponding item ID.
        local gloves = {} ---@type table[]
        local function registerGloves(itemId, abilId)
            gloves[#gloves+1] = {rawcode(itemId), rawcode(abilId)}
        end
        registerGloves("gcel", "Alsx")
        
        --ABILITY_RLF_ATTACK_SPEED_INCREASE_PERCENT_OAE2 is for Endurance Aura (AOae, SCae, AOr2 and AIae*).
        --*Item ability used by 'ajen' (Ancient Janggo of Endurance)
        --None of these have a way for their attack speed bonuses be correctly read, so I have hardcoded their values.
        --[[Set to have 0 attack speed modification, so I'll ignore them:
        
            ABILITY_RLF_ATTACK_SPEED_FACTOR_DEF4            --Defend
            ABILITY_RLF_ATTACK_SPEED_MODIFIER --'Nsi4'      --Silence
            ABILITY_RLF_ATTACK_SPEED_FACTOR_ESH3            --Shadow Strike
            ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_HBN2 --Banish
            ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_NAB2 --Acid Bomb
            ABILITY_RLF_ATTACK_SPEED_REDUCTION_PERCENT_NSO5 --Soul Burn
            These completely crash because they read bad memory (attack speed and movement speed are swapped by some kind of implementation error):
            ABILITY_RLF_ATTACK_SPEED_FACTOR_SPO3            --Slow Poison.
            ABILITY_RLF_ATTACK_SPEED_FACTOR_POI2            --Poison Sting.
            ABILITY_RLF_ATTACK_SPEED_FACTOR_POA3            --Poison Arrows.
        ]]
        AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_SPELL_EFFECT, function()
            local id = getAbilId()
            if buffTypes[id] then
                buffIndexTable[getTarget() or triggeringUnit()][id] = getSpellAbility()
            end
        end)
        
        AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_DAMAGED, function()
            local u = triggeringUnit()
            for _,list in ipairs(debuffs) do
                if getAbilityLvl(u, list[2]) > 0 then
                    local abil = list[1]
                    local index = buffIndexTable[u]
                    if not index[abil] then
                        index[abil] = getUnitAbility(GetEventDamageSource(), abil)
                    end
                end
            end
        end)
        local math              = math
        local getWeaponReal     = BlzGetUnitWeaponRealField
        local damagePt          = UNIT_WEAPON_RF_ATTACK_DAMAGE_POINT
        local _FROST            = rawcode("bfro")
        local _POISON           = rawcode("Bspo")
        local _ENDURANCE_AURA   = rawcode("BOae")
        local _ENDURANCE_AURA_ABIL = rawcode("AOae")
        getUnitDamagePoint = function(unit)
            local bonus = 1
            local buffs = buffIndexTable[unit]
            for _,list in ipairs(abilities) do
                local abil = list[1]
                local buff = buffs[abil]
                if buff then
                    local lvl = getAbilityLvl(unit, list[2])
                    if lvl > 0 then
                        local factor = getAbilityReal(buff, list[4], lvl - 1)
                        --print(factor)
                        bonus = bonus + factor*list[3]
                    else
                        buffIndexTable[unit][abil] = nil
                    end
                end
            end
            if getAbilityLvl(unit, _FROST) > 0 then
                bonus = bonus - _FROST_SPEED_DEC
            end
            if getAbilityLvl(unit, _POISON) > 0 then
                bonus = bonus - _SLOW_POISON_DEC
            end
            if getAbilityLvl(unit, _ENDURANCE_AURA) > 0 then
                GroupEnumUnitsInRange(g, GetUnitX(unit), GetUnitY(unit), _ENDURANCE_AURA_RADIUS, nil)
                local level = 0
                for i=0, BlzGroupGetSize(g)-1 do
        
                    local u = BlzGroupUnitAt(g, i)
                    local lvl = getAbilityLvl(u, _ENDURANCE_AURA_ABIL)
                    
                    if lvl > level and IsUnitAlly(u, GetOwningPlayer(unit)) then
                        level = lvl
                    end
                end
                bonus = bonus + level*_ENDURANCE_AURA_PER_LVL
            end
            if isType(unit, isHero) then
                bonus = bonus + _AGI_BONUS*getAgi(unit, true)
                if #gloves > 0 then
                    for i = 0, UnitInventorySize(unit) - 1 do
                        local item = UnitItemInSlot(unit, i)
                        if item then
                            local id = GetItemTypeId(item)
                            for j = 1, #gloves do
                                if id == gloves[j][1] then
                                    bonus = bonus + getAbilityReal(BlzGetItemAbility(item, gloves[j][2]), ABILITY_RLF_ATTACK_SPEED_INCREASE_ISX1, 0)
                                end
                            end
                        end
                    end
                end
            end
            return getWeaponReal(unit, damagePt, 0) / math.min(math.max(bonus, _MIN_SPEED), _MAX_SPEED)
        end
    end
    Event.attack        = Event.create("udg_AttackEvent", EQUAL)
    Event.attackCleanup = Event.create("udg_AttackEvent", NOT_EQUAL)
    Event.attackLaunch  = Event.create("udg_AttackLaunchEvent", EQUAL)
    Event.attackCancel  = Event.create("udg_AttackLaunchEvent", NOT_EQUAL)
    local attackFunc    = 13
    local damageFunc    = 14
    local numAttacks    = 15
    local queuedAttack  = 16
    local setOriginal   = 17
    local getWeaponInt  = BlzGetUnitWeaponIntegerField
    local setWeaponInt  = BlzSetUnitWeaponIntegerField
    local weapon        = UNIT_WEAPON_IF_ATTACK_WEAPON_SOUND
    ---@class attackIndexTable : table
    ---@field source unit
    ---@field target unit
    ---@field index integer
    ---@field data integer
    ---@field private active boolean
    local attackIndexTable = {} ---@type attackIndexTable[]
    ---@return attackIndexTable
    local function getTable()
        local data = Event.args
        if data then
            return data[1]
        else
            return Damage.index.attack
        end
    end
    
    --[[Optional GUI compatibility.
        
        AttackEventSource will return the attacking unit in "AttackEvent" or any of DamageEngine's events.
        AttackEventTarget will return the attacked unit in "AttackEvent" or any of DamageEngine's events (even if it is different from the damaged unit)
            This means that for an AOE attack or a multishot (barrage) attack, the AttackEventTarget can still be read successfully as the originally-attacked unit.
        AttackEventIndex is unknown at the time that the "AttackEvent" runs, but is set to 0 or 1 depending on whether the unit used its first attack or its second.
            If it is -1, that means it's not been set yet. So if it is -1 from an AttackEvent Not Equal event, then the attack never hit (e.g. evasion/curse/hasn't hit yet due to slow projectile).
        AttackEventData is meant to be set from an AttackEvent and read by a Damage event. See the below GUI pseudo-code for an example.
        
        Event:
            AttackEvent
        Actions:
            Set AttackEventData = DamageTypeCriticalStrike
        ...
        Event:
            PreDamageEvent
        Actions:
            Set DamageEventType = AttackEventData
    ]]
    if GlobalRemap then
        GlobalRemap("udg_AttackEventSource", function() return getTable().source end)
        GlobalRemap("udg_AttackEventTarget", function() return getTable().target end)
        GlobalRemap("udg_AttackEventIndex", function() return getTable().index end)
        GlobalRemap("udg_AttackEventData", function() return getTable().data end, function(val) getTable().data = val end)
    end
    ---@param attack attackIndexTable
    local function cleanup(attack)
        if attack.active then
            local data = attackIndexTable[attack.source]
            data[numAttacks] = data[numAttacks] - 1
            attack.active = nil
            Event.attackCleanup:run(attack)
        end
    end
    local currentAttack ---@type table
    Damage.register(Damage.damagingEvent, function()
        local damage = Damage.index ---@type damageInstance
        if damage.isAttack and not damage.isCode then
            local unit = damage.source
            local data = attackIndexTable[unit]
            if data then
                local attackPoint = GetHandleId(damage.weaponType)
                local tablePoint = attackPoint // 2
                local offset = (tablePoint * 2 == attackPoint) and 0 or 1 --whether it's using the primary or secondary attack
                local attack = data[tablePoint + 1]
                if currentAttack ~= attack then --make sure not to re-allocate for splash damage.
                    if currentAttack then cleanup(currentAttack) end
                    currentAttack = attack
                    currentAttack.index = offset
                    damage.attack = currentAttack
                    data[damageFunc](damage, offset)
                end
            end
        end
    end, _PRIORITY)
    Damage.register(Damage.sourceEvent, function()
        if currentAttack then
            cleanup(currentAttack)
        end
        currentAttack = nil
    end).minAOE = 0
    
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ATTACKED, function()
        local data = attackIndexTable[GetAttacker()]
        if data then data[attackFunc]() end
    end)
    
    UnitEvent.onTransform(function(ut) 
        local data = attackIndexTable[ut.unit]
        if data then data[setOriginal]() end
    end)
    UnitEvent.onCreate(
    function(ut)
        local attacker = ut.unit
        local data = {}
        attackIndexTable[attacker] = data
        buffIndexTable[attacker] = {}
        local original, point ---@type integer
        
        data[setOriginal] =
        function()
            original = {
                getWeaponInt(attacker, weapon, 0),
                getWeaponInt(attacker, weapon, 1)
            }
        end
        data[attackFunc] =
        function()
            if original then
                data[numAttacks] = data[numAttacks] + 1
            else
                data[numAttacks] = 1
                point = 0
                data[setOriginal]()
            end
            local thisAttack = { ---@type attackIndexTable
                source = attacker,
                target = triggeringUnit(),
                active = true,
                index = -1
            }
            setWeaponInt(attacker, weapon, 0, point*2)
            setWeaponInt(attacker, weapon, 1, point*2 + 1) --the system doesn't know if attack 0 or 1 is used at this point. Try them both.
            point = point + 1
            local old = data[point]
            if old and old.active then
                cleanup(old)
            end
            data[point] = thisAttack
            if point > 11 then point = 0 end
            Event.attack:run(thisAttack)
            data[queuedAttack] = function(fail)
                data[queuedAttack] = nil
                if fail then thisAttack.active = nil ; return end
                if thisAttack.active then
                    Timed.call(IsUnitType(attacker, UNIT_TYPE_RANGED_ATTACKER) and _EXPIRE_AFTER or 0.01, function()
                        if thisAttack.active then
                            cleanup(thisAttack)
                        end
                    end)
                    --print "attack launched"
                    Event.attackLaunch:run(thisAttack)
                    --[[
                    This could be useful to have as an event if you want to instantly detect if a melee attack fails, or
                    if you ended up changing the attack type at the end of the attack sequence and wanted to fix it before
                    it made a visual change to the UI (e.g. if you need more missiles indexed than currently possible).
                    
                    It is technically possible to calculate the duration between missile launch and target impact point,
                    in order to instantly detect if a missile misses, but that would require a periodic event in case the
                    target moves.
                    
                    Timed.call(0.01, function() print "clean up attack data" end)
                    ]]
                end
            end
            Timed.call(getUnitDamagePoint(attacker), data[queuedAttack])
        end
        data[damageFunc] =
        function(damage, offset)
            damage.weaponType = original[offset]
            if _USE_MELEE_RANGE and damage.isMelee and offset == 1 and original[2] == 0 and IsUnitType(attacker, UNIT_TYPE_RANGED_ATTACKER) then
                damage.isMelee = nil
                damage.isRanged = true
            end
        end
    end)
    local function cancelAttack()
        local data = attackIndexTable[triggeringUnit()]
        print (data)
        if data and data[queuedAttack] then
            data[queuedAttack](true)
            Event.attackCancel:run(data)
        end
    end
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ISSUED_ORDER, cancelAttack)
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER, cancelAttack)
    AnyPlayerUnitEvent.add(EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER, cancelAttack)
    UnitEvent.onRemoval(function(ut)
        if attackIndexTable[ut.unit] then
            attackIndexTable[ut.unit] = nil
            buffIndexTable[ut.unit] = nil
        end
    end)
end)
 
I'll indicate the work that I am planning for this:

  • Detect changes to attack speed mid-attack (currently, this is only accurate if the attack speed does not change in between the "unit is attacked" event and the "attack is launched" expected window.
  • Event binding between AttackEvent and Damage Engine via CreateEvent, allowing a user the full end-to-end control over an attack (do something at missile launch, wait until missile hits, do something else).
  • Figure out a solution for when an attack "misses" (as missed attacks will potentially need the user to manually clean up data if they do something like attach a special effect during an attack event and want to clean it up from damage engine).
  • Figure out the best way to integrate it with Damage Engine (with a minimal impact on performance, and definitely a boolean to disable the extra functionality brought on by the Attack Engine).
I intend to have this done by the end of the year, as I've been laying a lot of groundwork in order to get there (e.g. with systems such as CreateEvent and Action) to let smaller systems such as Unit Event and Spell System serve as proofs of the concept.
 
Last edited:
I've released a partial implementation of this for vJass here:

 
So to detect the 'attack is launched' event, I had previously been using timers and buff/debuff checks. This is annoying and hard to configure per-map.

I've therefore come up with a different solution, that may or may not work in the end:
1) Take a unit's base cooldown, subtract from it the backswing point and the damage point. This is the actual cooldown between attacks.
2) Remove the extra cooldown and just have the cooldown at the damage point.
3) Once the unit has re-triggered the 'attack' event, the missile has launched (other things in the system track when the attack is interrupted).
4) Figure out a way to prevent the unit from starting the attack backswing animation for the remaining cooldown period + backswing point
5) Repeat from step 3.

The problem is with step 4. I have no idea what to do here. Pausing it will prevent a lot of other things from working correctly in the meanwhile. Maybe I should set a timer that constantly interrupt's the unit's attack until that actual cooldown has taken effect?

I could also try locking the unit's animation in-place.
 
Last edited:
Top