Baradé's Black Arrow System 1.0

This bundle is marked as pending. It has not been reviewed by a staff member yet.
This system allows Black Arrow abilities to target units with a level greater than 5.
Warcraft III prevents this by default and it cannot be changed in the Object Editor or Gameplay constants (at least not to my own knowledge).

Features:
  • Supports custom abilities.
  • Supports custom orb items.
  • Supports registering auto casters from the beginning of the game.
  • Minimalistic: No vJass except the globals, no other systems required.

Known issues:
  • The buff of the summoned minions is always named "Timed Life" instead of the actual buff name.
  • You cannot specify any other fixed maximum unit level but it would be easy to adapt the system.
  • Some fields passed registering the abilities can be retrieved with the new Blizzard natives and the hero duration is probably not needed. I might update this in the future.

You have to register custom abilities and custom orbs using the JASS functions from the system:
JASS:
call BlackArrowAddAbility('A001', 1, 'ndr1', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddAbility('A001', 2, 'ndr2', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddAbility('A001', 3, 'ndr3', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddAbility('A002', 3, 'ndr3', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddItemTypeId('I000', BlackArrowAddAbility('A004', 1, 'ndr1', 3, 80.0, 0.0, 2.0, 'BNdm'))
call InitBlackArrowSystem()

The standard abilities and orb will be detected by default.

You have to manually add all units on the map with an active auto cast of a Black Arrow ability in their unit properties:
  • Actions
    • Unit Group - Pick every unit in (Units of type Dark Ranger) and do (Actions)
      • Loop - Actions
        • Custom script: call BlackArrowAddAutoCaster(GetEnumUnit())
    • Unit Group - Pick every unit in (Units of type Improved Dark Ranger) and do (Actions)
      • Loop - Actions
        • Custom script: call BlackArrowAddAutoCaster(GetEnumUnit())
    • Unit Group - Pick every unit in (Units of type Fel Ravager) and do (Actions)
      • Loop - Actions
        • Custom script: call BlackArrowAddAutoCaster(GetEnumUnit())
Import the following code into your map:
JASS:
// Baradé's Black Arrow System 1.0
//
// Supports Black Arrow abilities for units with levels greater than 5.
//
// Usage:
// - Copy the custom buff ability and custom buff into your map.
// - Copy this code into your map script.
// - Adapt the raw code ID of the constant to the one in your map.
// - Call the function InitItemUnstackSystem during the map initialization.
// - Optional: Use the API functions to register custom abilities and items.
// - Optional: Register all auto casters.

globals
    constant integer BLACK_ARROW_BUFF_ABILITY_ID = 'A000'
    constant string BLACK_ARROW_ORDER_ON = "blackarrowon"
    constant string BLACK_ARROW_ORDER_OFF = "blackarrowoff"

    integer array BlackArrowAbiliyId
    integer array BlackArrowAbiliyLevel
    integer array BlackArrowAbiliySummonedUnitTypeId
    integer array BlackArrowAbiliySummonedUnitsCount
    real array BlackArrowAbiliySummonedUnitDuration
    real array BlackArrowAbiliyDurationHero
    real array BlackArrowAbiliyDurationUnit
    integer array BlackArrowAbiliyBuffId
    integer BlackArrowAbilityCounter = 1

    integer array BlackArrowItemTypeId
    integer array BlackArrowItemTypeAbilityIndex
    integer BlackArrowItemTypeCounter = 1

    hashtable BlackArrowHashTable = InitHashtable()
    group BlackArrowTargets = CreateGroup()
    group BlackArrowCasters = CreateGroup()
    group BlackArrowItemUnits = CreateGroup()
    trigger BlackArrowDamageTrigger = CreateTrigger()
    trigger BlackArrowDeathTrigger = CreateTrigger()
    trigger BlackArrowOrderTrigger = CreateTrigger()
    trigger BlackArrowItemPickupTrigger = CreateTrigger()
    trigger BlackArrowItemDropTrigger = CreateTrigger()
endglobals

function BlackArrowAddAbility takes integer abilityId, integer level, integer summonedUnitTypeId, integer summonedUnitsCount, real summonedUnitDuration, real durationHero, real durationUnit, integer buffId returns integer
    set BlackArrowAbiliyId[BlackArrowAbilityCounter] = abilityId
    set BlackArrowAbiliyLevel[BlackArrowAbilityCounter] = level
    set BlackArrowAbiliySummonedUnitTypeId[BlackArrowAbilityCounter] = summonedUnitTypeId
    set BlackArrowAbiliySummonedUnitsCount[BlackArrowAbilityCounter] = summonedUnitsCount
    set BlackArrowAbiliySummonedUnitDuration[BlackArrowAbilityCounter] = summonedUnitDuration
    set BlackArrowAbiliyDurationHero[BlackArrowAbilityCounter] = durationHero
    set BlackArrowAbiliyDurationUnit[BlackArrowAbilityCounter] = durationUnit
    set BlackArrowAbiliyBuffId[BlackArrowAbilityCounter] = buffId

    set BlackArrowAbilityCounter = BlackArrowAbilityCounter + 1

    return BlackArrowAbilityCounter - 1
endfunction

function GetMatchingBlackArrowAbilityIndex takes unit caster returns integer
    local integer result = 0
    local integer i = 1
    loop
        exitwhen (i >= BlackArrowAbilityCounter or result > 0)
        if (GetUnitAbilityLevel(caster, BlackArrowAbiliyId[i]) == BlackArrowAbiliyLevel[i]) then
            set result = i
        endif
        set i = i + 1
    endloop

    return result
endfunction

function BlackArrowAddItemTypeId takes integer itemTypeId, integer abilityIndex returns integer
    set BlackArrowItemTypeId[BlackArrowItemTypeCounter] = itemTypeId
    set BlackArrowItemTypeAbilityIndex[BlackArrowItemTypeCounter] = abilityIndex

    set BlackArrowItemTypeCounter = BlackArrowItemTypeCounter + 1

    return BlackArrowItemTypeCounter - 1
endfunction

function GetMatchingBlackArrowItemTypeIndex takes integer itemTypeId returns integer
    local integer result = 0
    local integer i = 1
    loop
        exitwhen (i >= BlackArrowItemTypeCounter or result > 0)
        if (BlackArrowItemTypeId[i] == itemTypeId) then
            set result = i
        endif
        set i = i + 1
    endloop

    return result
endfunction

function BlackArrowAddStandardObjectData takes nothing returns nothing
    call BlackArrowAddAbility('ANba', 1, 'ndr1', 1, 80.0, 0.0, 2.0, 'BNdm')
    call BlackArrowAddAbility('ANba', 2, 'ndr2', 1, 80.0, 0.0, 2.0, 'BNdm')
    call BlackArrowAddAbility('ANba', 3, 'ndr3', 1, 80.0, 0.0, 2.0, 'BNdm')
    call BlackArrowAddAbility('ACbk', 1, 'ndr1', 1, 80.0, 0.0, 2.0, 'BNdm')
    call BlackArrowAddItemTypeId('odef', BlackArrowAddAbility('ANbs', 1, 'ndr1', 1, 80.0, 0.0, 2.0, 'BNdm'))
endfunction

function BlackArrowAddAutoCaster takes unit caster returns nothing
    if (not IsUnitInGroup(caster, BlackArrowCasters)) then
        call GroupAddUnit(BlackArrowCasters, caster)
        //call BJDebugMsg("Adding unit " + GetUnitName(caster) + " to casters.")
    endif
endfunction

function TimerFunctionBlackArrowBuffExpires takes nothing returns nothing
    local unit target = LoadUnitHandle(BlackArrowHashTable, GetHandleId(GetExpiredTimer()), 0)
    call FlushChildHashtable(BlackArrowHashTable, GetHandleId(target))
    call UnitRemoveAbility(target, BLACK_ARROW_BUFF_ABILITY_ID)
    call GroupRemoveUnit(BlackArrowTargets, target)
    set target = null
endfunction

function BlackArrowMarkTarget takes integer abilityIndex, unit source, unit target returns nothing
    local timer whichTimer = LoadTimerHandle(BlackArrowHashTable, 0, GetHandleId(target))

    //call BJDebugMsg("Marking Black Arrow target " + GetUnitName(GetTriggerUnit()))

    if (whichTimer != null) then
        call FlushChildHashtable(BlackArrowHashTable, GetHandleId(whichTimer))
        call PauseTimer(whichTimer)
        call DestroyTimer(whichTimer)
        set whichTimer = null
    endif

    set whichTimer = CreateTimer()
    call SaveUnitHandle(BlackArrowHashTable, GetHandleId(whichTimer), 0, target)
    call SaveTimerHandle(BlackArrowHashTable, GetHandleId(target), 0, whichTimer)
    call SaveUnitHandle(BlackArrowHashTable, GetHandleId(target), 1, source)
    call SaveInteger(BlackArrowHashTable, GetHandleId(target), 2, abilityIndex)
    call TimerStart(whichTimer, BlackArrowAbiliyDurationUnit[abilityIndex], false, function TimerFunctionBlackArrowBuffExpires)

    call UnitAddAbility(target, BLACK_ARROW_BUFF_ABILITY_ID)

    if (not IsUnitInGroup(target, BlackArrowTargets)) then
        call GroupAddUnit(BlackArrowTargets, target)
    endif
endfunction

function BlackArrowSummonEffect takes integer abilityIndex, unit source, unit target returns group
    local location tmpLocation = GetUnitLoc(target)
    local group result = CreateNUnitsAtLoc(BlackArrowAbiliySummonedUnitsCount[abilityIndex],  BlackArrowAbiliySummonedUnitTypeId[abilityIndex], GetOwningPlayer(source), tmpLocation, GetUnitFacing(target))
    local integer i = 0
    loop
        exitwhen (i == BlzGroupGetSize(result))
        call SetUnitAnimation(BlzGroupUnitAt(result, i), "Birth")
        call UnitApplyTimedLife(BlzGroupUnitAt(result, i), BlackArrowAbiliyBuffId[abilityIndex], BlackArrowAbiliySummonedUnitDuration[abilityIndex])
        set i = i + 1
    endloop

    return result
endfunction

function BlackArrowEffect takes unit target returns group
     local timer whichTimer = LoadTimerHandle(BlackArrowHashTable, GetHandleId(target), 0)
     local unit source = LoadUnitHandle(BlackArrowHashTable, GetHandleId(target), 1)
     local integer abilityIndex = LoadInteger(BlackArrowHashTable, GetHandleId(target), 2)
     local group result = BlackArrowSummonEffect(abilityIndex, source, target)

     //call BJDebugMsg("Black Arrow effect on target " + GetUnitName(target) + " with ability level " + I2S(BlackArrowAbiliyLevel[abilityIndex]) + " summoning units of type " + GetObjectName(BlackArrowAbiliySummonedUnitTypeId[abilityIndex]))

    if (whichTimer != null) then
        call FlushChildHashtable(BlackArrowHashTable, GetHandleId(whichTimer))
        call PauseTimer(whichTimer)
        call DestroyTimer(whichTimer)
        set whichTimer = null
    endif

    call FlushChildHashtable(BlackArrowHashTable, GetHandleId(target))
    call UnitRemoveAbility(target, BLACK_ARROW_BUFF_ABILITY_ID)
    call GroupRemoveUnit(BlackArrowTargets, target)

    // remove the decaying corpse
    call RemoveUnit(target)
    set target = null

    set source = null

    return result
endfunction

function TriggerConditionBlackArrowDamage takes nothing returns boolean
    return not IsUnitType(GetTriggerUnit(), UNIT_TYPE_HERO) and not IsUnitType(GetTriggerUnit(), UNIT_TYPE_SUMMONED) and not IsUnitType(GetTriggerUnit(), UNIT_TYPE_MECHANICAL) and not IsUnitType(GetTriggerUnit(), UNIT_TYPE_STRUCTURE) and not IsUnitType(GetTriggerUnit(), UNIT_TYPE_RESISTANT) and not IsUnitType(GetTriggerUnit(), UNIT_TYPE_MAGIC_IMMUNE) and GetUnitLevel(GetTriggerUnit()) > 5 and ((IsUnitInGroup(GetEventDamageSource(), BlackArrowCasters) and GetMatchingBlackArrowAbilityIndex(GetEventDamageSource()) > 0) or IsUnitInGroup(GetEventDamageSource(), BlackArrowItemUnits))
endfunction

function BlackArrowUnitGetOrbItem takes unit whichUnit, item excludeItem returns integer
    local integer i = 0
    loop
        exitwhen (i >= UnitInventorySize(whichUnit))
        if ((excludeItem == null or UnitItemInSlot(whichUnit, i) != excludeItem) and GetMatchingBlackArrowItemTypeIndex(GetItemTypeId(UnitItemInSlot(whichUnit, i))) > 0) then
            return i
        endif
        set i = i + 1
    endloop
    return -1
endfunction

function TriggerActionBlackArrowDamage takes nothing returns nothing
    local integer itemIndex = BlackArrowUnitGetOrbItem(GetEventDamageSource(), null)
    local integer abilityIndex = GetMatchingBlackArrowAbilityIndex(GetEventDamageSource())
    if (itemIndex != -1 and abilityIndex == 0) then
        call BlackArrowMarkTarget(BlackArrowItemTypeAbilityIndex[itemIndex], GetEventDamageSource(), GetTriggerUnit())
    else
        call BlackArrowMarkTarget(abilityIndex, GetEventDamageSource(), GetTriggerUnit())
    endif
endfunction

function TriggerConditionBlackArrowDeath takes nothing returns boolean
    return IsUnitInGroup(GetTriggerUnit(), BlackArrowTargets)
endfunction

function TriggerActionBlackArrowDeath takes nothing returns nothing
    call BlackArrowEffect(GetTriggerUnit())
endfunction

function TriggerConditionBlackArrowOrder takes nothing returns boolean
    return GetIssuedOrderId() == OrderId(BLACK_ARROW_ORDER_ON) or GetIssuedOrderId() == OrderId(BLACK_ARROW_ORDER_OFF)
endfunction

function TriggerActionBlackArrowOrder takes nothing returns nothing
    if (GetIssuedOrderId() == OrderId(BLACK_ARROW_ORDER_ON)) then
        call BlackArrowAddAutoCaster(GetTriggerUnit())
    else
        if (IsUnitInGroup(GetTriggerUnit(), BlackArrowCasters)) then
            call GroupRemoveUnit(BlackArrowCasters, GetTriggerUnit())
            //call BJDebugMsg("Removing unit " + GetUnitName(GetTriggerUnit()) + " from casters.")
        endif
    endif
endfunction

function TriggerConditionBlackArrowPickupItem takes nothing returns boolean
    return not IsUnitInGroup(GetTriggerUnit(), BlackArrowItemUnits) and GetMatchingBlackArrowItemTypeIndex(GetItemTypeId(GetManipulatedItem())) > 0
endfunction

function TriggerActionBlackArrowPickupItem takes nothing returns nothing
    call GroupAddUnit(BlackArrowItemUnits, GetTriggerUnit())
    //call BJDebugMsg("Unit " + GetUnitName(GetTriggerUnit()) + " picked up a Black Arrow orb item.")
endfunction

function TriggerConditionBlackArrowDropItem takes nothing returns boolean
    local boolean result = IsUnitInGroup(GetTriggerUnit(), BlackArrowItemUnits) and GetMatchingBlackArrowItemTypeIndex(GetItemTypeId(GetManipulatedItem())) > 0

    if (result) then
        // we need to exclude the dropped item since it is not dropped yet
        return BlackArrowUnitGetOrbItem(GetTriggerUnit(), GetManipulatedItem()) == -1
    endif

    return result
endfunction

function TriggerActionBlackArrowDropItem takes nothing returns nothing
    call GroupRemoveUnit(BlackArrowItemUnits, GetTriggerUnit())
    //call BJDebugMsg("Unit " + GetUnitName(GetTriggerUnit()) + " dropped the final Black Arrow orb item.")
endfunction

function FilterBlackArrowForUnitWithOrb takes nothing returns boolean
    return UnitInventorySize(GetFilterUnit()) > 0 and BlackArrowUnitGetOrbItem(GetFilterUnit(), null) != -1
endfunction

function BlackArrowAddAllUnitsWithOrbs takes nothing returns nothing
    local group whichGroup = CreateGroup()
    call GroupEnumUnitsInRect(whichGroup, GetPlayableMapRect(), Filter(function FilterBlackArrowForUnitWithOrb))
    set bj_wantDestroyGroup = true
    call GroupAddGroup(whichGroup, BlackArrowItemUnits)
    //call BJDebugMsg("Units with orbs size " + I2S(CountUnitsInGroup(BlackArrowItemUnits)))
    set whichGroup = null
endfunction

function InitBlackArrowSystem takes nothing returns nothing
    call TriggerRegisterAnyUnitEventBJ(BlackArrowDamageTrigger, EVENT_PLAYER_UNIT_DAMAGED)
    call TriggerAddCondition(BlackArrowDamageTrigger, Condition(function TriggerConditionBlackArrowDamage))
    call TriggerAddAction(BlackArrowDamageTrigger, function TriggerActionBlackArrowDamage)

    call TriggerRegisterAnyUnitEventBJ(BlackArrowDeathTrigger, EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddCondition(BlackArrowDeathTrigger, Condition(function TriggerConditionBlackArrowDeath))
    call TriggerAddAction(BlackArrowDeathTrigger, function TriggerActionBlackArrowDeath)

    call TriggerRegisterAnyUnitEventBJ(BlackArrowOrderTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
    call TriggerAddCondition(BlackArrowOrderTrigger, Condition(function TriggerConditionBlackArrowOrder))
    call TriggerAddAction(BlackArrowOrderTrigger, function TriggerActionBlackArrowOrder)

    call TriggerRegisterAnyUnitEventBJ(BlackArrowItemPickupTrigger, EVENT_PLAYER_UNIT_PICKUP_ITEM)
    call TriggerAddCondition(BlackArrowItemPickupTrigger, Condition(function TriggerConditionBlackArrowPickupItem))
    call TriggerAddAction(BlackArrowItemPickupTrigger, function TriggerActionBlackArrowPickupItem)

    call TriggerRegisterAnyUnitEventBJ(BlackArrowItemDropTrigger, EVENT_PLAYER_UNIT_DROP_ITEM)
    call TriggerAddCondition(BlackArrowItemDropTrigger, Condition(function TriggerConditionBlackArrowDropItem))
    call TriggerAddAction(BlackArrowItemDropTrigger, function TriggerActionBlackArrowDropItem)

    call BlackArrowAddStandardObjectData()
    call BlackArrowAddAllUnitsWithOrbs()
endfunction
Contents

Baradé's Black Arrow System 1.0 (Map)

MyPad

Spell Reviewer
Level 24
Joined
May 9, 2014
Messages
1,730
I think this system will greatly benefit from using structs to register/organize the custom black arrow ability IDs, ranging from compact and defined behavior to clarity in the code. Since the system already takes advantage of vJASS features (declaration of global variables), I suggest enclosing the system in a library and privatizing the members.
 
Level 19
Joined
Feb 2, 2006
Messages
1,198
Yes, maybe I will change it to vJass because of the globals. I am not sure how to handle stacking. Right now it uses the first matching item or first matching ability of the damaging unit. Is it the same way with the Black Arrow ability in Warcraft?
Maybe I have to add some tests. I don't think multiple abilities or items will stack?
 

MyPad

Spell Reviewer
Level 24
Joined
May 9, 2014
Messages
1,730
Based on the testmap attached here, the applied buff (on target) from the Black Arrow ability will stack with custom buffs. However, only the most recent Black Arrow ability (or derivatives) that managed to apply the buff will be used to spawn the summoned unit/s.

Going further into the way the damage instance is dealt from the black arrows, it appears that the damage from the unit's attack is applied before the buff. This leads to the observation that when you have distinct Black Arrow abilities, the summoned unit will be based on the most recent Black Arrow ability applied (a bit of circular reasoning, I suppose?).

For instance, say that we have 2 Black Arrow Abilities. A target was hit with Black Arrow A and survived. Then, when the target would be hit by Black Arrow B, the following scenarios may play out:
  1. Target Lives
  2. Target Dies
In #1, the most recent Black Arrow ability to be referenced when creating the summoned units would become Black Arrow B.
In #2, since the unit died before the buff was applied, it will summon units based on the data from Black Arrow A.

A similar scenario will play out if we swap Black Arrow A with Black Arrow B.
 

Attachments

  • Arrow Test.w3x
    19.3 KB · Views: 5
Level 20
Joined
Nov 18, 2012
Messages
1,621
// Usage:
// - Copy the custom buff ability and custom buff into your map.
// - Copy this code into your map script.
// - Adapt the raw code ID of the constant to the one in your map.
// - Call the function InitItemUnstackSystem during the map initialization.
// - Optional: Use the API functions to register custom abilities and items.
// - Optional: Register all auto casters.
1 - There's no custom buff, only a custom ability, that acts as a buff, called "Black Arrow Buff". Specifying the name would be better.
4 - InitItemUnstackSystem doesn't exit. InitBlackArrowSystem. I don't understand why you don't implement this on map initialization yourself, with library. This is just an unnecessary step for users to take into account while they should do the least steps possible. It shouldn't be a big deal since you're already using vjass.
5 - I can tell that BlackArrowAddAbility and BlackArrowAddAutoCaster are available to the user, but I don't know if that's all of API, or if there's more, nor should users have to look through the code to know the API or if it is really part of it. So putting them at the top with comments would save a lot of time, with a very simple description of each of them.

You have to register your custom abilities and custom orbs using the JASS functions from the system:
JASS:
JASS:
call BlackArrowAddAbility('A001', 1, 'ndr1', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddAbility('A001', 2, 'ndr2', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddAbility('A001', 3, 'ndr3', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddAbility('A002', 3, 'ndr3', 3, 80.0, 0.0, 2.0, 'BNdm')
call BlackArrowAddItemTypeId('I000', BlackArrowAddAbility('A004', 1, 'ndr1', 3, 80.0, 0.0, 2.0, 'BNdm'))
call InitBlackArrowSystem()

The standard abilities and orb will be detected by default.
I have these issues because a friend asked me if this system could solve his problems and how to use it correctly, and the documentation wasn't clear.
 
Top