- Joined
- Sep 26, 2009
- Messages
- 9,529
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.
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
Last edited: