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

Baradé's Black Arrow System 1.1

This bundle is marked as awaiting update. A staff member has requested changes to it before it can be approved.
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.

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.

Download and open the example map to see how the system can be used.
See usage in the comment of the vJass code for the details.
You have to specify the constant
JASS:
BUFF_ABILITY_ID
with the ability ID in your map.

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'))

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())
Import the following code into your map:
JASS:
// Baradé's Black Arrow System 1.1
//
// Supports Black Arrow abilities for target units with levels greater than 5.
// The standard Black Arrow abilities from Warcraft only work with target units up to level 5.
//
// Usage:
// - Copy this code into your map script or a trigger converted into code.
// - Copy the custom buff ability "Black Arrow Buff" (A000) into your map and adapt the raw code in the constant BUFF_ABILITY_ID to the new raw code in your map.
// - Optional: Add all preplaced units in your map with enabled Black Arrow auto casting using the function BlackArrowAddAutoCaster.
// - Optional: Use the API functions to register custom abilities and item types.
// - Optional: Create triggers and register Black Arrow events for further custom actions.
//
// Design:
// Auto casters are detected by issued orders. Preplaced units are created in the generated map script function CreateAllUnits which is called in the generated
// method main before the initialization of this system. This means that issued orders from preplaced units won't be detected by the system's order triggers.
// Hence, you have to use the function BlackArrowAddAutoCaster to add all preplaced units in your map with enabled Black Arrow auto casting.
//
// API:
//
// function BlackArrowAddAbility takes integer abilityId, integer level, integer summonedUnitTypeId, integer summonedUnitsCount, real summonedUnitDuration, real durationHero, real durationUnit, integer buffId returns integer
//
// Adds another custom ability to the system with the given configuration. Whenever a unit with the added ability at the given level kills a target with a level greater than 5, it will automatically summon the minions
// with the given unit type for the given amount of time.
// The function returns a unique index refering to the added ability. The first index starts at 1.
//
// function BlackArrowAddItemTypeId takes integer itemTypeId, integer abilityIndex returns integer
//
// Adds an item type which has the Black Arrow ability with the given index. You can combine this function with BlackArrowAddAbility and directly add the ability when adding the item type.
// Whenever a unit with carrying an item with the added item type kills a target unit with a level greater than 5, it will automatically summon the minions with the given configuration from the given ability.
// The function returns a unique index refering to the added item type. The first index starts at 1.
//
//
// function BlackArrowAddAutoCaster takes unit whichUnit returns nothing
//
// Adds the given unit as auto caster. This is required to detect damage caused by auto casters and cast the Black Arrow effect.
// All preplaced units with an enabled Black Arrow ability in the map must be added manually with this function.
//
// function BlackArrowRemoveAutoCaster takes unit whichUnit returns nothing
//
// Removes the given unit from the group of auto casters.
//
// function BlackArrowIsAutoCaster takes unit which returns boolean
//
// Returns true if the given unit is an auto caster. Otherwise, it returns false.
//
// function TriggerRegisterBlackArrowEvent takes trigger whichTrigger returns nothing
//
// Registers a Black Arrow event for the given callback trigger. This means that the trigger is evaluated and executed whenever an added Black Arrow ability is casted for a target unit above level 5.
// For the standard ability casts, you have to use the standard ability cast events instead.
//
// function GetTriggerBlackArrowCaster takes nothing returns unit
//
// Returns the casting unit for the current callback trigger.
//
// function GetTriggerBlackArrowTarget takes nothing returns unit
//
// Returns the target unit for the current callback trigger.
//
// function GetTriggerBlackArrowSummonedUnits takes nothing returns group
//
// Returns all summoned minions for the current callback trigger. Never destroy this group since it is basically bj_lastCreatedGroup and does not leak.
//
// function GetTriggerBlackArrowAbilityId takes nothing returns integer
//
// Returns the ability ID of the casted Black Arrow ability.
//
library BlackArrowSystem

globals
    public constant integer BUFF_ABILITY_ID = 'A0FU'
    //public constant integer BUFF_ABILITY_ID = 'A000'
    public constant string ORDER_ON = "blackarrowon"
    public constant string ORDER_OFF = "blackarrowoff"
    public constant boolean ADD_STANDARD_OBJECT_DATA = true
    public constant boolean ADD_ALL_UNITS_WITH_ORBS = true

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

    private integer array BlackArrowItemTypeId
    private integer array BlackArrowItemTypeAbilityIndex
    private integer BlackArrowItemTypeCounter = 1

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

    // callbacks
    private unit BlackArrowCaster = null
    private unit BlackArrowTarget = null
    private group BlackArrowSummonedUnits = null
    private integer BlackArrowAbilityId = 0
    private trigger array BlackArrowCallbackTrigger
    private integer BlackArrowCallbackTriggerCounter = 0

    private boolean hookEnabled = true
endglobals

function GetTriggerBlackArrowCaster takes nothing returns unit
    return BlackArrowCaster
endfunction

function GetTriggerBlackArrowTarget takes nothing returns unit
    return BlackArrowTarget
endfunction

function GetTriggerBlackArrowSummonedUnits takes nothing returns group
    return BlackArrowSummonedUnits
endfunction

function GetTriggerBlackArrowAbilityId takes nothing returns integer
    return BlackArrowAbilityId
endfunction

function TriggerRegisterBlackArrowEvent takes trigger whichTrigger returns nothing
    set BlackArrowCallbackTrigger[BlackArrowCallbackTriggerCounter] = whichTrigger
    set BlackArrowCallbackTriggerCounter = BlackArrowCallbackTriggerCounter + 1
endfunction

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 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 BlackArrowAddAutoCaster takes unit whichUnit returns nothing
    call GroupAddUnit(BlackArrowAutoCasters, whichUnit)
endfunction

function BlackArrowRemoveAutoCaster takes unit whichUnit returns nothing
    call GroupRemoveUnit(BlackArrowAutoCasters, whichUnit)
endfunction

function BlackArrowIsAutoCaster takes unit which returns boolean
    return IsUnitInGroup(which, BlackArrowAutoCasters)
endfunction

function BlackArrowPrintDebug takes nothing returns nothing
    call BJDebugMsg("Targets: " + I2S(CountUnitsInGroup(BlackArrowTargets)))
    call BJDebugMsg("Auto Casters: " + I2S(CountUnitsInGroup(BlackArrowAutoCasters)))
    call BJDebugMsg("Item Units: " + I2S(CountUnitsInGroup(BlackArrowItemUnits)))
endfunction

private 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

private 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

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

private function MarkTarget 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)
    if (IsUnitType(target, UNIT_TYPE_HERO)) then
        call TimerStart(whichTimer, BlackArrowAbiliyDurationHero[abilityIndex], false, function TimerFunctionBlackArrowBuffExpires)
    else
        call TimerStart(whichTimer, BlackArrowAbiliyDurationUnit[abilityIndex], false, function TimerFunctionBlackArrowBuffExpires)
    endif

    call UnitAddAbility(target, BUFF_ABILITY_ID)

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

private function ExecuteCallbackTriggers takes unit source, unit target, group summonedUnits, integer abilityId returns nothing
    local integer i = 0
    set BlackArrowCaster = source
    set BlackArrowTarget = target
    set BlackArrowSummonedUnits = summonedUnits
    set BlackArrowAbilityId = abilityId
    loop
        exitwhen (i == BlackArrowCallbackTriggerCounter)
        call TriggerExecute(BlackArrowCallbackTrigger[i])
        set i = i + 1
    endloop
endfunction

private function SummonEffect takes integer abilityIndex, unit source, unit target returns group
    local location tmpLocation = GetUnitLoc(target)
    // Does not leak since it uses bj_lastCreatedGroup:
    local group summonedUnits = CreateNUnitsAtLoc(BlackArrowAbiliySummonedUnitsCount[abilityIndex],  BlackArrowAbiliySummonedUnitTypeId[abilityIndex], GetOwningPlayer(source), tmpLocation, GetUnitFacing(target))
    local integer i = 0
    loop
        exitwhen (i == BlzGroupGetSize(summonedUnits))
        call SetUnitAnimation(BlzGroupUnitAt(summonedUnits, i), "Birth")
        call UnitApplyTimedLife(BlzGroupUnitAt(summonedUnits, i), BlackArrowAbiliyBuffId[abilityIndex], BlackArrowAbiliySummonedUnitDuration[abilityIndex])
        set i = i + 1
    endloop

    call ExecuteCallbackTriggers(source, target, summonedUnits, BlackArrowAbiliyId[abilityIndex])

    return summonedUnits
endfunction

private function Effect 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 summonedUnits = SummonEffect(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, BUFF_ABILITY_ID)
    call GroupRemoveUnit(BlackArrowTargets, target)

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

    set source = null

    return summonedUnits
endfunction

private function TriggerConditionDamage 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(), BlackArrowAutoCasters) 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

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

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

private function TriggerActionDeath takes nothing returns nothing
    call Effect(GetTriggerUnit())
endfunction

private function TriggerConditionOrder takes nothing returns boolean
    return GetIssuedOrderId() == OrderId(ORDER_ON) or GetIssuedOrderId() == OrderId(ORDER_OFF)
endfunction

private function TriggerActionOrder takes nothing returns nothing
    if (GetIssuedOrderId() == OrderId(ORDER_ON)) then
        if (not BlackArrowIsAutoCaster(GetTriggerUnit())) then
            call BlackArrowAddAutoCaster(GetTriggerUnit())
        //call BJDebugMsg("Adding unit " + GetUnitName(caster) + " to casters.")
        endif
    else
        if (BlackArrowIsAutoCaster(GetTriggerUnit())) then
            call BlackArrowRemoveAutoCaster(GetTriggerUnit())
            //call BJDebugMsg("Removing unit " + GetUnitName(GetTriggerUnit()) + " from casters.")
        endif
    endif
endfunction

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

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

private function TriggerConditionDropItem 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

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

private function AddStandardObjectData 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

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

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

private module Init

    private static method onInit takes nothing returns nothing
        call TriggerRegisterAnyUnitEventBJ(BlackArrowDamageTrigger, EVENT_PLAYER_UNIT_DAMAGED)
        call TriggerAddCondition(BlackArrowDamageTrigger, Condition(function TriggerConditionDamage))
        call TriggerAddAction(BlackArrowDamageTrigger, function TriggerActionDamage)

        call TriggerRegisterAnyUnitEventBJ(BlackArrowDeathTrigger, EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddCondition(BlackArrowDeathTrigger, Condition(function TriggerConditionDeath))
        call TriggerAddAction(BlackArrowDeathTrigger, function TriggerActionDeath)

        call TriggerRegisterAnyUnitEventBJ(BlackArrowOrderTrigger, EVENT_PLAYER_UNIT_ISSUED_ORDER)
        call TriggerAddCondition(BlackArrowOrderTrigger, Condition(function TriggerConditionOrder))
        call TriggerAddAction(BlackArrowOrderTrigger, function TriggerActionOrder)

        call TriggerRegisterAnyUnitEventBJ(BlackArrowItemPickupTrigger, EVENT_PLAYER_UNIT_PICKUP_ITEM)
        call TriggerAddCondition(BlackArrowItemPickupTrigger, Condition(function TriggerConditionPickupItem))
        call TriggerAddAction(BlackArrowItemPickupTrigger, function TriggerActionPickupItem)

        call TriggerRegisterAnyUnitEventBJ(BlackArrowItemDropTrigger, EVENT_PLAYER_UNIT_DROP_ITEM)
        call TriggerAddCondition(BlackArrowItemDropTrigger, Condition(function TriggerConditionDropItem))
        call TriggerAddAction(BlackArrowItemDropTrigger, function TriggerActionDropItem)

static if (ADD_STANDARD_OBJECT_DATA) then
        call AddStandardObjectData()
endif
static if (ADD_ALL_UNITS_WITH_ORBS) then
        call AddAllUnitsWithOrbs()
endif
    endmethod
endmodule

private struct S
    implement Init
endstruct

// ChangeLog:
//
// 1.1 2022-09-24:
// - Use vJass and a library, many private declarations and with early automatic initialization in a module.
// - Add options ADD_STANDARD_OBJECT_DATA and ADD_ALL_UNITS_WITH_ORBS.
// - Add function BlackArrowIsAutoCaster.
// - BlackArrowAbiliyDurationHero is used for target heroes now.
// - Add API documentation with usable functions.
// - Add event handling functions which allow adding actions to Black Arrow events.
// - Refactor some functions.
endlibrary
Contents

Baradé's Black Arrow System 1.1 (Map)

Reviews
Wrda
TriggerConditionOrder and TriggerActionOrder are doing the same checks, it's redundant. You can easily combine to one private function TriggerConditionOrder takes nothing returns boolean if (GetIssuedOrderId() == OrderId(ORDER_ON)) then...
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 25
Joined
Feb 2, 2006
Messages
1,686
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?
 
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: 11

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,870
// 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.
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,870
TriggerConditionOrder and TriggerActionOrder are doing the same checks, it's redundant.
JASS:
private function TriggerConditionOrder takes nothing returns boolean
    return GetIssuedOrderId() == OrderId(ORDER_ON) or GetIssuedOrderId() == OrderId(ORDER_OFF)
endfunction

private function TriggerActionOrder takes nothing returns nothing
    if (GetIssuedOrderId() == OrderId(ORDER_ON)) then
        if (not BlackArrowIsAutoCaster(GetTriggerUnit())) then
            call BlackArrowAddAutoCaster(GetTriggerUnit())
        //call BJDebugMsg("Adding unit " + GetUnitName(caster) + " to casters.")
        endif
    else
        if (BlackArrowIsAutoCaster(GetTriggerUnit())) then
            call BlackArrowRemoveAutoCaster(GetTriggerUnit())
            //call BJDebugMsg("Removing unit " + GetUnitName(GetTriggerUnit()) + " from casters.")
        endif
    endif
endfunction
You can easily combine to one
JASS:
private function TriggerConditionOrder takes nothing returns boolean
    if (GetIssuedOrderId() == OrderId(ORDER_ON)) then
        if (not BlackArrowIsAutoCaster(GetTriggerUnit())) then
            call BlackArrowAddAutoCaster(GetTriggerUnit())
        //call BJDebugMsg("Adding unit " + GetUnitName(caster) + " to casters.")
        endif
    else
        if (BlackArrowIsAutoCaster(GetTriggerUnit())) then
            call BlackArrowRemoveAutoCaster(GetTriggerUnit())
            //call BJDebugMsg("Removing unit " + GetUnitName(GetTriggerUnit()) + " from casters.")
        endif
    endif
    return false
endfunction

Not required, but you could add "exitwhen true" after "set result = i" to exit the loop instead of checking "result > 0" each time you loop. And it could become faster if you used the hashtable to store the item types id.
JASS:
private 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

Since TriggerConditionDamage function has potential for the user to modify it within his needs and the fact that you mentioned in known issues it would be easy to adapt the system for such things, you should mention this function specifically (and maybe some others, which have the same potential), and make it stand out in some way. E.g., making more space between this function and others, and a comment stating it is configurable.

The local group summonedUnits leaks a reference leak. Described in here: Memory Leaks
JASS:
private function SummonEffect takes integer abilityIndex, unit source, unit target returns group
    local location tmpLocation = GetUnitLoc(target)
    // Does not leak since it uses bj_lastCreatedGroup:
    local group summonedUnits = CreateNUnitsAtLoc(BlackArrowAbiliySummonedUnitsCount[abilityIndex],  BlackArrowAbiliySummonedUnitTypeId[abilityIndex], GetOwningPlayer(source), tmpLocation, GetUnitFacing(target))
    local integer i = 0
    loop
        exitwhen (i == BlzGroupGetSize(summonedUnits))
        call SetUnitAnimation(BlzGroupUnitAt(summonedUnits, i), "Birth")
        call UnitApplyTimedLife(BlzGroupUnitAt(summonedUnits, i), BlackArrowAbiliyBuffId[abilityIndex], BlackArrowAbiliySummonedUnitDuration[abilityIndex])
        set i = i + 1
    endloop

    call ExecuteCallbackTriggers(source, target, summonedUnits, BlackArrowAbiliyId[abilityIndex])

    return summonedUnits
endfunction

private function Effect 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 summonedUnits = SummonEffect(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, BUFF_ABILITY_ID)
    call GroupRemoveUnit(BlackArrowTargets, target)

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

    set source = null

    return summonedUnits
endfunction
Can be easily solved by having another global variable to return the corresponding group before nulling the local one.

Besides these relatively minor things, I have noticed some things:
Killing Ogre Lords (with any registered form of black arrow) will remove the dying unit instantly, whereas a unit with level lesser or equal to 5 will have its dying animation first, and then removed a bit after. I think you should replicate this behavior, doesn't have to be fully accurate. Otherwise it would be kind of weird.
Dark Ranger fails to give the "black arrow buff" to Doom Guard and spawn units upon death while Improved Fel Ravager or Fel Ravager does give, even if these last 2 unit types have auto cast disabled. Is it because they're melee and the buff and ability effect is applied, even though Doom Guard is Skin Resistant?

Bigger problem: Turning off auto cast after sending a black arrow projectile won't trigger the ability's effect on unit's level 6 or above; With auto cast off, manually targeting won't trigger the ability's effect on unit's level 6 or above.
I don't think it's actually possible to fix these two issues in all cases unless you triggered the whole ability, with custom projectile system.

Summing up, it's a nice system to remove the Warcraft 3 limitations. Besides these two problems, the other ones should be easier to fix, I'm only hesitant to approve because of them.
 
Level 25
Joined
Feb 2, 2006
Messages
1,686
Interesting finds. The memory leak does not happen since it uses bj_lastCreatedGroup but do you mean a null reference? I can simply return bj_lastCreatedGroup then.

About Resistant Skin: If it blocks Black Arrow, I can check for Resistant Skin. Is it only against ranged units or all all Black Arrow default abilities?!

How long is the corpse still there? The time of the death animation? I need to know to implement.

What do you mean by "With auto cast off, manually targeting won't trigger the ability's effect on unit's level 6 or above." Should it trigger it even with auto cast off because it is melee?!?!

I still have to understand everything properly before updating it.
 

Wrda

Spell Reviewer
Level 25
Joined
Nov 18, 2012
Messages
1,870
Interesting finds. The memory leak does not happen since it uses bj_lastCreatedGroup but do you mean a null reference? I can simply return bj_lastCreatedGroup then.
I mean, the object leak doesn't happen, but the reference counter does. Yes, bj_lastCreatedGroup will do, the local group isn't necessary anyway.
About Resistant Skin: If it blocks Black Arrow, I can check for Resistant Skin. Is it only against ranged units or all all Black Arrow default abilities?!
Resistant Skin is only blocking ranged units with black arrow, regardless whether it's default or custom ones.
How long is the corpse still there? The time of the death animation? I need to know to implement.
Yes, the Art - Death Time field.
What do you mean by "With auto cast off, manually targeting won't trigger the ability's effect on unit's level 6 or above." Should it trigger it even with auto cast off because it is melee?!?!
It seems it "casts" the black arrow ability without taking into account the autocast state because it's melee. I even tested with a footman, which has the black arrow ability (neutral hostile), 0 mana. It kills the Doom Guard and spawns the skeleton. Game logic? No sense...
 
Top