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

[JASS] Limit of Summoned Units

Status
Not open for further replies.
I'm trying to make a spell that counts up the number of summoned units and staggers their index backwards when the limit is reached or when summoned unit dies, kinda like how Carrion Beetles work, except with wards. But it doesn't work as intended - the limit on summoned units seems to 'increase' if I already have the maximum amount of units (Most likely that 1 ward is getting left behind when I cycle through the wards, but I not very familiar with this method)

Also I'm using 2D arrays.

JASS:
library SummonLimit initializer init

    globals
        private trigger HL_onLearn = CreateTrigger()
        private trigger HL_onRetrain = CreateTrigger()
        private trigger HL_onSummon = CreateTrigger()
        private trigger HL_onDeath = CreateTrigger()
        private integer array Maximum
        private integer array CurrentNumber
        private integer array OrderIndex
        private unit array Minion
        private unit array Summoner
        private integer array Vision_Abil
        private integer array TrueSight_Abil
        private boolean array IsExcluded
    endglobals

    
    
    /*======== ACTIONS ========*/
    
    //onLearn
    function Trig_HiddenLantern_onLearn takes nothing returns boolean
        
        local unit source = null
        local integer id = 0
        local integer Level = 0
        
        if GetLearnedSkill() == 'Afhl' then
            
            set source = GetTriggerUnit()
            set id = GetUnitUserData(source)
            set Level = GetUnitAbilityLevel(source, 'Afhl')
            set Maximum[id] = Level + 1
            call BJDebugMsg(I2S(Maximum[id]))
            
            if Level == 1 then
                set Vision_Abil[id] = 'Als1'
                set TrueSight_Abil[id] = 'Ats1'
            elseif Level == 2 then
                set Vision_Abil[id] = 'Als2'
                set TrueSight_Abil[id] = 'Ats2'
            elseif Level == 3 then
                set Vision_Abil[id] = 'Als3'
                set TrueSight_Abil[id] = 'Ats3'
            endif
            
            set source = null
            
        endif
        return false
    endfunction


    //onRetrain
    function Trig_HiddenLantern_onRetrain takes nothing returns boolean
        
        local unit source = null
        local integer id = 0
        local integer WHid = 0 //WH for Width and Height
        local integer iLoop = 0
        
        if GetItemTypeId(GetManipulatedItem()) == 'tret' and GetUnitAbilityLevel(GetTriggerUnit(), 'Afhl') > 0 then
        
            set source = GetTriggerUnit()
            set id = GetUnitUserData(source)
            
            loop
                exitwhen iLoop > CurrentNumber[id]
                set iLoop = iLoop + 1
                
                set WHid = id * Maximum[id] + iLoop
                call UnitApplyTimedLife(Minion[WHid], 'BTLF', 0.01)
                set Minion[WHid] = null
            endloop
            
            set Maximum[id] = 0
            set CurrentNumber[id] = 0
            
            set source = null
        
        endif
        return false
    endfunction
    
    
    //onSummon
    function Trig_HiddenLantern_onSummon takes nothing returns boolean
    
        local unit ward = GetTriggerUnit()
        local integer id = 0
        local integer WHid = 0
        local integer iLoop = 0
        
        if GetUnitTypeId(ward) == 'ffln' then
        
            set id = GetUnitUserData(GetSummoningUnit())
            
            call UnitAddAbility(ward, Vision_Abil[id])
            call UnitAddAbility(ward, TrueSight_Abil[id])
            
            if CurrentNumber[id] < Maximum[id] then
                set CurrentNumber[id] = CurrentNumber[id] + 1
                set WHid = id * Maximum[id] + CurrentNumber[id]
                set Minion[WHid] = ward
                //set OrderIndex[id] = OrderIndex[id] + 1
                //set OrderIndex[WHid] = OrderIndex[id]
            else
                set WHid = id * Maximum[id] + 1
                call UnitApplyTimedLife(Minion[WHid], 'BTLF', 0.01)
                set Minion[WHid] = null
                set Summoner[GetUnitUserData(Minion[WHid])] = null
                set IsExcluded[GetUnitUserData(Minion[WHid])] = true
                
                loop
                    set iLoop = iLoop + 1
                    exitwhen iLoop == Maximum[id]
                    
                    set WHid = id * Maximum[id] + iLoop
                    set Minion[WHid] = Minion[WHid + 1]
                endloop
                
                //set WHid = id * Maximum[id] + Maximum[id]
                set Minion[WHid + 1] = ward
            endif
            set Summoner[GetUnitUserData(ward)] = GetSummoningUnit()
            call BJDebugMsg("Max: " + I2S(Maximum[id]))
            call BJDebugMsg("CN: " + I2S(CurrentNumber[id]))
        endif
        
        set ward = null
        return false
    endfunction
    
    
    //onDeath
    function Trig_HiddenLantern_onDeath takes nothing returns boolean
    
        local unit ward = null
        local integer id = 0
        local integer ward_id = 0
        local integer WHid = 0
        local integer iLoop = 0
        local integer NewIndex = 0
        call BJDebugMsg("onDeath")
        if GetUnitTypeId(GetTriggerUnit()) == 'ffln' then
            
            set ward = GetTriggerUnit()
            set ward_id = GetUnitUserData(ward)
            set id = GetUnitUserData(Summoner[ward_id])
            
            if IsExcluded[ward_id] then
                set IsExcluded[ward_id] = false
            else
                loop
                    set iLoop = iLoop + 1
                    exitwhen iLoop == CurrentNumber[id]
                    
                    set WHid = id * Maximum[id] + iLoop
                    if not UnitAlive(Minion[WHid]) then
                        set NewIndex = iLoop
                        loop
                            set NewIndex = NewIndex + 1
                            exitwhen NewIndex == CurrentNumber[id]
                            
                            set WHid = id * Maximum[id] + iLoop
                            set Minion[WHid] = Minion[WHid + 1]
                        endloop
                    endif
                endloop
                set Summoner[ward_id] = null
                set CurrentNumber[id] = CurrentNumber[id] - 1
            endif
            
            set ward = null
        
        endif
        return false
    endfunction

    
    
    /*======== EVENTS ========*/
    
    //EVENT onLearn
    function InitTrig_HL_onLearn takes nothing returns nothing
        call TriggerRegisterAnyUnitEventBJ( HL_onLearn, EVENT_PLAYER_HERO_SKILL )
        call TriggerAddCondition( HL_onLearn, function Trig_HiddenLantern_onLearn )
    endfunction

    //EVENT onRetrain
    function InitTrig_HL_onRetrain takes nothing returns nothing
        call TriggerRegisterAnyUnitEventBJ( HL_onRetrain, EVENT_PLAYER_UNIT_USE_ITEM )
        call TriggerAddCondition( HL_onRetrain, function Trig_HiddenLantern_onRetrain )
    endfunction

    //EVENT onSummon
    function InitTrig_HL_onSummon takes nothing returns nothing
        call TriggerRegisterAnyUnitEventBJ( HL_onSummon, EVENT_PLAYER_UNIT_SUMMON )
        call TriggerAddCondition( HL_onSummon, function Trig_HiddenLantern_onSummon )
    endfunction
    
    //EVENT onSummon
    function InitTrig_HL_onDeath takes nothing returns nothing
        call TriggerRegisterAnyUnitEventBJ( HL_onDeath, EVENT_PLAYER_UNIT_DEATH )
        call TriggerAddCondition( HL_onDeath, function Trig_HiddenLantern_onDeath )
    endfunction
    
    private function init takes nothing returns nothing
        call InitTrig_HL_onDeath()
        call InitTrig_HL_onRetrain()
        call InitTrig_HL_onSummon()
        call InitTrig_HL_onLearn()
    endfunction

endlibrary
 
Level 11
Joined
Dec 19, 2012
Messages
411
In Trig_HiddenLantern_onRetrain :
JASS:
                exitwhen iLoop > CurrentNumber[id] //Should be iLoop == CurrentNumber[id]
                set iLoop = iLoop + 1

or
                set iLoop = iLoop + 1
                exitwhen iLoop > CurrentNumber[id]

In Trig_HiddenLantern_onSummon:
set id = GetUnitUserData(GetSummoningUnit())
Are you sure that you get the correct id?
 
@onRetrain
Oh, right, will fix.

@onSummon
The id is based on the caster, which is the SummoningUnit(). The only variables indexed to the ward's custom value are the caster unit, and a boolean that checks whether the ward was removed by 'overflow' (aka a new ward was summoned when the maximum was already reached, so the oldest ward is destroyed for the newest one) or was killed. If it was removed by overflow, don't trigger the onDeath lines.
 
Level 7
Joined
Oct 19, 2015
Messages
286
You are simulating 2D arrays incorrectly. You need to use a separate, static maximum value when multiplying the id of the caster. The Maximum[] value you are currently using can change when the skill is leveled up, at which point you would loose the reference to all your current wards. Also, by using the current maximum values of the casters, two casters with different levels of the ability could end up trying to store their data in the same place.

Arrays are not really the most suitable tool for this job, as you can probably tell by those loops you need to do in order to keep the correct order when removing units from the array. A more suitable data structure would be a linked list. I once wrote an example for precisely this problem that used a linked list: limited summon.

By the way, in vJass, you can replace function Trig_HiddenLantern_onLearn with private function onLearn, similarly to what you are already doing in the globals block. Saves you some typing.
 
Anitarf said:
The Maximum[] value you are currently using can change when the skill is leveled up
... well that's embarassing, even more so because I knew that already T_T. However, the maximum maximum is 4, and even when I set it to respect the actual limit, it still had the same problem.

Anyway, i dropped my spell and tried to setup using your limited summon, but it's only permitting me 1 unit per caster. If I leave the line as is, it kills my ward immediately.
JASS:
private function SummonLimit takes unit u returns integer
    //this determines how many units of the chosen type a caster can control
    return GetUnitAbilityLevel(u, SUMMON_ABILITY_ID) + 1
endfunction

it seems to only care about the +1. The ability level is being ignored. I'm using Sentry Ward as my base spell, and the wards created by the spell itself.

PS: could you include tome of retraining to kill the summons if you respec your hero?
 
Level 7
Joined
Oct 19, 2015
Messages
286
Hmm, not sure what could be going wrong. Do you have the SUMMON_ABILITY_ID set up correctly? Try adding a debug message to the SummonLimit function to display the level. You can try additional messages in the create and onDestroy methods of both structs to confirm that the underlying data structures are getting allocated as needed. Did you take the required libraries out of the map or did you get them from other sources? Can't think of anything else at the moment, do some tests with debug messages and tell me the results. You can also try copying the units, spell and libraries into an empty test map and if it doesn't work there, you can attach the map here and then I'll look into it.

Once we get it to work, I can edit it for you so that it will also support the tome of retraining.
 
Level 7
Joined
Oct 19, 2015
Messages
286
Okay, here is the edited code with added support for tome of retraining:
JASS:
//*****************************************************************
//*  spell template - LimitedSummon
//*
//*  written by: Anitarf
//*  requires: -Table
//*            -LinkedList
//*
//*            -a unit summoning ability
//*            -a unit summoned by the above ability
//*
//*  description: Limits the number of summoned units of one type
//*               that a single caster may summon. If the caster
//*               summons more than that many the oldest summoned
//*               unit belonging to that caster is removed.
//*
//*  technical: The spell may malfunction if the summoned units
//*             can be removed by triggers before they die. To
//*             avoid this, always kill your units before removing
//*             them, not just because of this spell but because
//*             removing units in general is a lame thing to do.
//*
//*****************************************************************

scope LimitedSummon initializer Init

    globals
        private constant integer SUMMON_UNIT_ID = 'h000' //the type of the summoned unit
        private constant integer SUMMON_ABILITY_ID = 'A000' //the ability used to summon the unit
        private constant integer RETRAIN_ITEM_ID = 'tret' //tome of retraining

        //the special effect to be used when an old summoned unit is killed to make room for the new:
        private constant string KILL_EFFECT_PATH = "Abilities\\Spells\\Other\\Charm\\CharmTarget.mdl"
        private constant string KILL_EFFECT_POINT = "origin"
    endglobals

    private function SummonLimit takes unit u returns integer
        //this determines how many units of the chosen type a caster can control
        local integer level = GetUnitAbilityLevel(u, SUMMON_ABILITY_ID)
        if level == 0 then
            return 0
        endif
        return level+1
    endfunction
// END OF CALIBRATION SECTION    
// ================================================================


    globals
        private HandleTable gc
    endglobals

    private keyword summoned
    private struct summoner
        unit u
        List l

        static method create takes unit u returns summoner
            local summoner s = summoner.allocate()
            set gc[u] = integer(s)
            set s.l = List.create()
            set s.u = u
            return s
        endmethod
        static method get takes unit u returns summoner
            return summoner(gc[u])
        endmethod

        method onDestroy takes nothing returns nothing
            call gc.flush(.u)
            call .l.destroy()
        endmethod

        method countCheck takes nothing returns nothing
            local unit k
            loop
                if .l.size > SummonLimit(.u) then
                    set k = summoned(.l.last.data).u
                    call DestroyEffect(AddSpecialEffectTarget(KILL_EFFECT_PATH, k, KILL_EFFECT_POINT))
                    call KillUnit(k)
                    set k = null
                else
                    exitwhen true
                endif
            endloop
        endmethod
    endstruct

// ================================================================

    private struct summoned
        unit u
        Link l
        summoner parent

        static method get takes unit u returns summoned
            return summoned(gc[u])
        endmethod
        static method create takes unit u, unit summoning returns summoned
            local summoned s = summoned.allocate()
            local summoner sum = summoner.get(summoning)

            if sum==0 then
                set sum = summoner.create(summoning)
            endif

            set s.u = u
            set s.parent = sum
            set s.l = Link.create(sum.l, integer(s))
            set gc[u] = integer(s)

            call s.parent.countCheck()

            return s
        endmethod

        method onDestroy takes nothing returns nothing
            call gc.flush(.u)
            call .l.destroy()
            if .parent.l.size==0 then
                call .parent.destroy()
            endif
        endmethod
    endstruct

// ================================================================

    private function Summoning takes nothing returns boolean
        if GetUnitTypeId(GetSummonedUnit())==SUMMON_UNIT_ID then
            call summoned.create(GetSummonedUnit(), GetSummoningUnit())
        endif
        return false
    endfunction

    private function SummonDeath takes nothing returns boolean
        local summoned s
        if GetUnitTypeId(GetTriggerUnit())==SUMMON_UNIT_ID then
            set s = summoned.get(GetTriggerUnit())
            if s != 0 then
                call s.destroy()
            endif
        endif
        return false
    endfunction

    private function ItemUse takes nothing returns boolean
        local summoner sum = summoner.get(GetTriggerUnit())
        if sum != 0 and  GetItemTypeId( GetManipulatedItem() )==RETRAIN_ITEM_ID then
            call sum.countCheck()
        endif
    endfunction

// ================================================================

    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SUMMON)
        call TriggerAddCondition(t, Condition(function Summoning))

        set t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_DEATH)
        call TriggerAddCondition(t, Condition(function SummonDeath))

        set t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_USE_ITEM)
        call TriggerAddCondition(t, Condition(function ItemUse))

        set gc = HandleTable.create()
    endfunction
endscope
I wrote the edit on the forums so hopefully it will compile, if not then see if you can spot any easy-to-fix typos yourself.

Edit: fixed the errors noted below.
 
Last edited:
Level 7
Joined
Oct 19, 2015
Messages
286
The problem is in the SummonLimit function, since the formula is level+1, it will still allow one ward at level 0. Here's the fix:
JASS:
    private function SummonLimit takes unit u returns integer
        //this determines how many units of the chosen type a caster can control
        local integer level = GetUnitAbilityLevel(u, SUMMON_ABILITY_ID)
        if level == 0 then
            return 0
        endif
        return level+1
    endfunction
 
Status
Not open for further replies.
Top