• 🏆 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] Landmine Spell

Status
Not open for further replies.
Level 19
Joined
Oct 12, 2007
Messages
1,821
I'm trying to create some sort of a spell that works like the Goblin Landmine.
It's supposed to summon a landmine unit under the feet of the caster. This dummy unit will have a 25sec generic expiration timer but will explode after 24 seconds or whenever any enemy comes in range of it; dealing damage. It will only be able to explode at least 2 seconds after it has been placed.

For some reason the spell is not doing anything. It's not even summoning the bomb unit itself. Is there something I'm doing wrong? I am, after all, able to save the code without errors.

JASS:
scope DwarvenLandmine initializer init

globals
    private boolexpr Bool
    
endglobals

private struct Data
unit caster
unit bomb
unit target

real dmg
real delay
real x
real y


group radius

static integer array Ar
static integer Total = 0
static timer Time = CreateTimer()

    static method create takes unit u returns Data
        local Data Dat = Data.allocate()
        local player p = GetOwningPlayer(u)
        
        set Dat.x = GetUnitX(u)
        set Dat.y = GetUnitY(u)
        set Dat.target = null
        set Dat.caster = u
        set Dat.delay = 24.
        set Dat.dmg = I2R(GetHeroInt(u, true))*2.2
        set Dat.bomb = CreateUnit(p, 'n00V', Dat.x, Dat.y, GetUnitFacing(u))
        
        call UnitApplyTimedLife(Dat.bomb, 'BTLF', 25.)
        
        if Dat.Total == 0 then
            call TimerStart(Dat.Time,0.1,true,function Data.Loop)
        endif
        
        set Dat.Ar[Dat.Total] = Dat
        set Dat.Total = Dat.Total + 1
        
        set u = null
        
        return Dat
    endmethod
    
    static method Loop takes nothing returns nothing
        local Data Dat
        local integer i = 0
        
        loop
            exitwhen i >= Dat.Total
            set Dat = Dat.Ar[i]
            
            set Dat.delay = Dat.delay - 0.1
            
            if Dat.delay < 22. then
                call GroupEnumUnitsInRange(Dat.radius, Dat.x, Dat.y, 200., Bool)
            endif
            
            if Dat.delay <= 0. or FirstOfGroup(Dat.radius) != null then
                call DestroyEffect(AddSpecialEffect("war3mapImported\\Incendiary mortar v2.mdx", Dat.x, Dat.y))
                call RemoveUnit(Dat.bomb)
                call ShakeCamera(GetOwningPlayer(Dat.caster), 5.00)
                
                loop
                    set Dat.target = FirstOfGroup(Dat.radius)
                    exitwhen Dat.target == null
                    call UnitDamageTargetEx(Dat.caster, Dat.target, Dat.dmg, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_SPELLFIRE, false)
                    call GroupRemoveUnit(Dat.radius, Dat.target)
                endloop
                
                
                set Dat.Total = Dat.Total - 1
                set Dat.Ar[i] = Dat.Ar[Dat.Total]
                set i = i - 1
                call Dat.destroy()
            endif
            
            set i = i + 1
        endloop
        
        if Dat.Total == 0 then
            call PauseTimer(Dat.Time)
        endif
    endmethod
    
    method onDestroy takes nothing returns nothing
        set .caster = null
        set .target = null
    endmethod
endstruct

private function filt takes nothing returns boolean
    local unit f = GetFilterUnit()
    local unit u = GetTriggerUnit()
    local boolean ok = GetWidgetLife(f) > .305 and IsUnitEnemy(f,GetOwningPlayer(u))
    set f = null
    set u = null
    return ok
endfunction

private function OnCast takes nothing returns boolean  
    local Data Dat
    local unit u

    if GetSpellAbilityId() == 'A03M' then ///edit
        set u = GetTriggerUnit()
        set Dat = Data.create(u)
        set u = null
        
    endif
    
    return false
endfunction

private function init takes nothing returns nothing
    local trigger trig = CreateTrigger()
    local integer index = 0
    
    loop
        exitwhen index >= bj_MAX_PLAYER_SLOTS
        call TriggerRegisterPlayerUnitEvent(trig,Player(index),EVENT_PLAYER_UNIT_SPELL_EFFECT,BOOLEXPR_TRUE)
        set index = index + 1
    endloop
    
    call TriggerAddCondition(trig, Condition(function OnCast))
    set Bool = Filter(function filt)
    
    set trig = null
endfunction
endscope
 
Level 10
Joined
May 27, 2009
Messages
494
you may try using http://www.hiveworkshop.com/forums/jass-resources-412/snippet-spelleffectevent-187193/ as your base init

or change this line
JASS:
loop
        exitwhen index >= bj_MAX_PLAYER_SLOTS
        @call TriggerRegisterPlayerUnitEvent(trig,Player(index),EVENT_PLAYER_UNIT_SPELL_EFFECT,BOOLEXPR_TRUE)@
        set index = index + 1
    endloop
to
JASS:
loop
        exitwhen index >= bj_MAX_PLAYER_SLOTS
        @call TriggerRegisterPlayerUnitEvent(trig,Player(index),EVENT_PLAYER_UNIT_SPELL_EFFECT,null)@
        set index = index + 1
    endloop

and change GetWidgetLife(f) > .305
to UnitAlive(f) or not IsUnitType(f,UNIT_TYPE_DEAD) but if you're gonna use UnitAlive, you must first declare the native... but it's better if you use UnitAlive anyways

and you may just use the filt function as an inline function... those vars just give some little heavy operation... or you may not, just in case you can't read it anymore

OR, you may just use that filt together with the Loop group
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Tried some things, remade the spell and I got it to work.
Think the spell in the object editor was bugged.:S

It's still not working though.
The mine spawns and explodes after 24seconds, however it doesn't deal damage to nearby enemies or even trigger the explosion when an enemy comes close. I even removed the bool to see if it works on other units too but that didn't work either. hmm.
 
Level 10
Joined
May 27, 2009
Messages
494
because you did the Filt on map Init and the damage is dealt during the loop.. so GetTriggerUnit is already nulled or invalid during that point..

oh.. and you didn't CreateGroup() dat.radius.. i think?

so here's a little workaround i made on your script:

JASS:
static group tmpGroup = CreateGroup()

    static method Loop takes nothing returns nothing
        local thistype Dat
        local integer i = 0
        local unit f
        
        loop
            exitwhen i >= Dat.Total
            set Dat = Dat.Ar[i]
            
            set Dat.delay = Dat.delay - 0.1
            
            if Dat.delay < 22. then
                call GroupEnumUnitsInRange(tmpGroup, Dat.x, Dat.y, 200, null)
            endif
            
            if Dat.delay <= 0 or FirstOfGroup(tmpGroup) != null then
                call DestroyEffect(AddSpecialEffect("war3mapImported\\Incendiary mortar v2.mdx", Dat.x, Dat.y))
                call RemoveUnit(Dat.bomb)
                call ShakeCamera(GetOwningPlayer(Dat.caster), 5.00)
                
                loop
                    set f = FirstOfGroup(tmpGroup)
                    exitwhen f==null
                    call GroupRemoveUnit(tmpGroup, f)
                    if UnitAlive(f) and IsUnitEnemy(f,GetOwningPlayer(Dat.caster)) then
                        call UnitDamageTargetEx(Dat.caster, f, Dat.dmg, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_SPELLFIRE, false)
                    endif
                endloop
                
                
                set Dat.Total = Dat.Total - 1
                set Dat.Ar[i] = Dat.Ar[Dat.Total]
                set i = i - 1
                call Dat.destroy()
            endif
            
            set i = i + 1
        endloop
        
        if Dat.Total == 0 then
            call PauseTimer(Dat.Time)
        endif
        set f=null
    endmethod

Also, remove the variables target and radius.. since they're already useless in this one.. (and really.. they are.. at least)
the global Bool is also useless.. since your filtering units on map init and the boolexpr is called during the time where there is no valid GetTriggerUnit()
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Thanks for the help.
I see what I did wrong there.
Forgot to CreateGroup() and all the mistakes with the Bool.
As for UnitAlive(unit) and things like SpellEffectEvent(..), I'm deffo gonna check those out and maybe use them. Sounds like they make spellmaking a lot easier.
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Currently 99% of my spells have GetWidgetLife(u) > 0.405 (RaiN. once told be it had to be this number because of a bug or something)
The spells I made after this post have IsUnitType(u, UNIT_TYPE_DEAD) == false

Guess that should be fine
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
IsUnitType has a bug of its own: UNIT_TYPE_DEAD is a boolean value (either true or false), and when a unit is removed from the game or has decayed, the boolean is set back to the default (false). So checking UNIT_TYPE_DEAD on a null unit, removed unit or decayed unit will return false, so your check will think that the unit is alive. One needs the accompanying "GetUnitTypeId == 0" check to ensure the check does not bug, unless you know for sure you are only checking existing units (for example a group enumeration does not need this check).

UnitAlive is arguably the least buggy of the three options and definitely the most intuitive. The only bug I know of by using it is that it returns true if the unit was freshly removed (as in, within the same exact thread a RemoveUnit was called on it). I'll let you form your opinion on if this bug is even worth considering.
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Ah, thanks for the info.

I'll just use GetWidgetLife for now because I don't think I won't get problems with using that and I've been using it with the other ~80-120 spells as well. If I ever encounter problems with it I might start using UnitAlive.
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
This is what I currently got for the spell: (I added a slow in addition to the dmg)

JASS:
scope DwarvenLandmine initializer init

private struct Data
unit caster
unit bomb

real dmg
real delay
real x
real y


static group dmgGroup = CreateGroup()
static integer array Ar
static integer Total = 0
static timer Time = CreateTimer()

    static method create takes unit u returns Data
        local Data Dat = Data.allocate()
        local player p = GetOwningPlayer(u)
        
        set Dat.x = GetUnitX(u)
        set Dat.y = GetUnitY(u)
        set Dat.caster = u
        set Dat.delay = 24.
        set Dat.dmg = I2R(GetHeroInt(u, true))*2.4
        set Dat.bomb = CreateUnit(p, 'n00V', Dat.x, Dat.y, GetUnitFacing(u))
        
        call UnitApplyTimedLife(Dat.bomb, 'BTLF', 24.)
        
        if Dat.Total == 0 then
            call TimerStart(Dat.Time,0.25,true,function Data.Loop)
        endif
        
        set Dat.Ar[Dat.Total] = Dat
        set Dat.Total = Dat.Total + 1
        
        set u = null
        
        return Dat
    endmethod
    
    static method Loop takes nothing returns nothing
        local Data Dat
        local integer i = 0
        local unit t
        local unit dummy
        
        loop
            exitwhen i >= Dat.Total
            set Dat = Dat.Ar[i]
            
            set Dat.delay = Dat.delay - 0.25
            
            if Dat.delay < 22. then
                call GroupEnumUnitsInRange(Dat.dmgGroup, Dat.x, Dat.y, 175., null)
            endif
            
            loop
                set t = FirstOfGroup(Dat.dmgGroup)
                exitwhen IsUnitEnemy(t, GetOwningPlayer(Dat.caster)) == true and GetWidgetLife(Dat.u) >= .405 and IsUnitType(t, UNIT_TYPE_MAGIC_IMMUNE) == false and GetUnitAbilityLevel(t, 'Avul') == 0
                call GroupRemoveUnit(Dat.dmgGroup, t)
            endloop
            
            if Dat.delay <= 0. or t != null then
                call GroupClear(Dat.dmgGroup)
                call GroupEnumUnitsInRange(Dat.dmgGroup, Dat.x, Dat.y, 275., null)
                call DestroyEffect(AddSpecialEffect("war3mapImported\\Incendiary mortar v2.mdx", Dat.x, Dat.y))
                call RemoveUnit(Dat.bomb)
                call ShakeCamera(GetOwningPlayer(Dat.caster), 6.00)
                
                loop
                    set t = FirstOfGroup(Dat.dmgGroup)
                    exitwhen t == null
                    if IsUnitEnemy(t, GetOwningPlayer(Dat.caster)) == true and GetWidgetLife(Dat.u) >= .405 then
                        call UnitDamageTargetEx(Dat.caster, t, Dat.dmg, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_SPELLFIRE, false)
                        call ShakeCamera(GetOwningPlayer(t), 6.00)
                        set dummy = CreateUnit(GetOwningPlayer(Dat.caster),'h00J',Dat.x,Dat.y,0.)
                        call UnitAddAbility(dummy,'A0EH')
                        call IssueTargetOrder(dummy, "slow", t)
                        call UnitApplyTimedLife(dummy,'BTLF',1.)
                        set dummy = null
                    endif
                    call GroupRemoveUnit(Dat.dmgGroup, t)
                    set t = null
                endloop
                
                
                set Dat.Total = Dat.Total - 1
                set Dat.Ar[i] = Dat.Ar[Dat.Total]
                set i = i - 1
                call Dat.destroy()
            endif
            
            set i = i + 1
        endloop
        
        if Dat.Total == 0 then
            call PauseTimer(Dat.Time)
        endif
    endmethod
    
    method onDestroy takes nothing returns nothing
        set .caster = null
        set .bomb = null
    endmethod
endstruct

private function OnCast takes nothing returns boolean  
    local Data Dat
    local unit u

    if GetSpellAbilityId() == 'A0M3' then
        set u = GetTriggerUnit()
        set Dat = Data.create(u)
        set u = null
        
    endif
    
    return false
endfunction

private function init takes nothing returns nothing
    local trigger trig = CreateTrigger()
    local integer index = 0
    
    loop
        exitwhen index >= bj_MAX_PLAYER_SLOTS
        call TriggerRegisterPlayerUnitEvent(trig,Player(index),EVENT_PLAYER_UNIT_SPELL_EFFECT,null)
        set index = index + 1
    endloop
    
    call TriggerAddCondition(trig, Condition(function OnCast))
    
    set trig = null
endfunction
endscope
 

Bribe

Code Moderator
Level 50
Joined
Sep 26, 2009
Messages
9,468
Hmm, well you don't need I2R (it's only useful to avoid truncating division).

Inlining the TriggerRegisterAnyUnitEventBJ is overkill, it was only worth inlining when boolexpr's leaked but well, they don't leak any more so I recommend just bite the bullet and go with the red text.

onDestroy is also overkill, since you only call .destroy once, so right above where you call .destroy on the struct, null the two variables right there. Mission successful.

You might even save 0.0001% of the processor performance by doing it with my recommendations. The real benefit is further compression in the compiled code - 8MB is a really bad limitation and is not always easy to avoid.
 
Level 40
Joined
Dec 14, 2005
Messages
10,532
Currently 99% of my spells have GetWidgetLife(u) > 0.405 (RaiN. once told be it had to be this number because of a bug or something)
That's because 99% of people just do what other people tell them to do without wondering why they do it, which is why 0.405 is still used so much.

Or using AoE healing spells or gargoyle "stone form", plus the value needs to be 0.405, not 0, due to another wc3 issue.
This is a complete myth. The only reason you need to use 0.405 instead of 0 is if you are checking to see if damage that hasn't yet been done will kill a unit; the moment a unit dies its life is set to 0. Here's a test map: note that the BJDebugMsg is after the SetWidgetLife, not before, so there is the least possible delay between the set and the message.

http://www.hiveworkshop.com/forums/pastebin.php?id=l8arsz

--

Edit: Code critique:

  • JASS:
    private function OnCast takes nothing returns boolean  
        local Data Dat
        local unit u
    
        if GetSpellAbilityId() == 'A0M3' then
            set u = GetTriggerUnit()
            set Dat = Data.create(u)
            set u = null
            
        endif
        
        return false
    endfunction

    Should be

    JASS:
    private function OnCast takes nothing returns boolean
        if GetSpellAbilityId() == 'A0M3' then
            call Data.create(GetTriggerUnit())
        endif
        return false
    endfunction
  • It is not necessary to clear a group before running GroupEnumUnits
  • You don't need to null parameters.
  • You can use X and not X instead of X == true and X == false respectively and it looks a little cleaner.
  • You should really be using a filterfunc for what you're doing with that group, but whatever floats your boat I guess.
 
Level 40
Joined
Dec 14, 2005
Messages
10,532
It seems like there's an abundance of people making claims like that now that stopwatch doesn't work any more. I distinctly recall it being the reverse but unfortunately I don't know of any libraries allowing me to demonstrate it; last time someone made a similar claim I spent an hour or so looking for one.
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Multiple people have helped now, and I got pretty much tips. But because of the discussions I don't really know what I should improve now.
At the moment the spell is working fine, but after a while of playing it sometimes doesn't explode when a unit passes it. I don't really know why it sometimes won't respond, even when a certain unit is literally standing on it for a while.

Here's the current code:

JASS:
scope DwarvenLandmine initializer init

private struct Data
unit caster
unit bomb

real dmg
real delay
real x
real y


static group dmgGroup = CreateGroup()
static integer array Ar
static integer Total = 0
static timer Time = CreateTimer()

    static method create takes unit u returns Data
        local Data Dat = Data.allocate()
        local player p = GetOwningPlayer(u)
        
        set Dat.x = GetUnitX(u)
        set Dat.y = GetUnitY(u)
        set Dat.caster = u
        set Dat.delay = 24.
        set Dat.dmg = I2R(GetHeroInt(u, true))*2.4
        set Dat.bomb = CreateUnit(p, 'n00V', Dat.x, Dat.y, GetUnitFacing(u))
        
        call UnitApplyTimedLife(Dat.bomb, 'BTLF', 24.)
        
        if Dat.Total == 0 then
            call TimerStart(Dat.Time,0.25,true,function Data.Loop)
        endif
        
        set Dat.Ar[Dat.Total] = Dat
        set Dat.Total = Dat.Total + 1
        
        set u = null
        
        return Dat
    endmethod
    
    static method Loop takes nothing returns nothing
        local Data Dat
        local integer i = 0
        local unit t
        local unit dummy
        
        loop
            exitwhen i >= Dat.Total
            set Dat = Dat.Ar[i]
            
            set Dat.delay = Dat.delay - 0.25
            
            if Dat.delay < 22. then
                call GroupEnumUnitsInRange(Dat.dmgGroup, Dat.x, Dat.y, 175., null)
            endif
            
            loop
                set t = FirstOfGroup(Dat.dmgGroup)
                exitwhen IsUnitEnemy(t, GetOwningPlayer(Dat.caster)) == true and GetWidgetLife(t) >= .405 and GetUnitAbilityLevel(t, 'Avul') == 0
                call GroupRemoveUnit(Dat.dmgGroup, t)
            endloop
            
            if Dat.delay <= 0. or t != null then
                call GroupEnumUnitsInRange(Dat.dmgGroup, Dat.x, Dat.y, 275., null)
                call DestroyEffect(AddSpecialEffect("war3mapImported\\Incendiary mortar v2.mdx", Dat.x, Dat.y))
                call RemoveUnit(Dat.bomb)
                call ShakeCamera(GetOwningPlayer(Dat.caster), 6.00)
                
                loop
                    set t = FirstOfGroup(Dat.dmgGroup)
                    exitwhen t == null
                    if IsUnitEnemy(t, GetOwningPlayer(Dat.caster)) == true and GetWidgetLife(t) >= .405 then
                        call UnitDamageTargetEx(Dat.caster, t, Dat.dmg, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_SPELLFIRE, false)
                        call ShakeCamera(GetOwningPlayer(t), 6.00)
                        set dummy = CreateUnit(GetOwningPlayer(Dat.caster),'h00J',Dat.x,Dat.y,0.)
                        call UnitAddAbility(dummy,'A0EH')
                        call IssueTargetOrder(dummy, "slow", t)
                        call UnitApplyTimedLife(dummy,'BTLF',1.)
                        set dummy = null
                    endif
                    call GroupRemoveUnit(Dat.dmgGroup, t)
                    set t = null
                endloop
                
                
                set Dat.Total = Dat.Total - 1
                set Dat.Ar[i] = Dat.Ar[Dat.Total]
                set i = i - 1
                call Dat.destroy()
            endif
            
            set i = i + 1
        endloop
        
        if Dat.Total == 0 then
            call PauseTimer(Dat.Time)
        endif
    endmethod
    
    method onDestroy takes nothing returns nothing
        set .caster = null
        set .bomb = null
    endmethod
endstruct

private function OnCast takes nothing returns boolean  
    local Data Dat

    if GetSpellAbilityId() == 'A0M3' then
        set Dat = Data.create(GetTriggerUnit())
    endif
    
    return false
endfunction

private function init takes nothing returns nothing
    local trigger trig = CreateTrigger()

    call TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(trig, Condition(function OnCast))
    
    set trig = null
endfunction
endscope
 

Cokemonkey11

Spell Reviewer
Level 29
Joined
May 9, 2006
Messages
3,534
This should be the issue:

JASS:
            loop
                set t = FirstOfGroup(Dat.dmgGroup)
                exitwhen IsUnitEnemy(t, GetOwningPlayer(Dat.caster)) == true and GetWidgetLife(t) >= .405 and GetUnitAbilityLevel(t, 'Avul') == 0
                call GroupRemoveUnit(Dat.dmgGroup, t)
            endloop

If no unit is standing there then it causes an infinite loop.

You need to exit the loop when t==null.

Edit: By the way, excellent job so far. This script is really nice. My only other suggestion is to define some constants to make it more modular. Shake camera duration, enumeration range, timer fidelity, damage multiplier for example.
 
Level 19
Joined
Oct 12, 2007
Messages
1,821
Well I don't need those constants really.
I will use this ability for my own ORPG so it won't become an ability I'll post somewere. So it's not supposed to be easy for others, as long as I understand it.:)
This used to be the way RaiN. coded, and I kinda copied his style to be able to work together on the same map.
The ShakeCamera command is a small system he made for this project.
Every spell in this ORPG will cause the camera to shake. Small spells like autocast heals shake only with a very small magnitude, while impacts like this landmine will be pretty strong. This is one of the things we use to make gameplay more fun and dynamic, because it gives you a good feeling even while killing simple creeps.;-)


EDIT:
It's still acting pretty weird.
I get the feeling a bomb can't explode if a bomb that has been placed a few seconds before the current one didn't explode yet.
So scenario:
Bomb A has been placed 10 seconds ago and didn't explode yet.
Bomb B has been placed 3 seconds ago and didn't explode yet.
A unit crosses bomb B but it doesn't explode at all. I let that unit cross Bomb A and it explodes, then I let him cross Bomb B again and it explodes too.

Sometimes I also see bombs explode after they ran out of time. So a Bomb is supposed to last for 24 seconds and explode when the time ran out. But now I see bombs exploding even after the 24 seconds have passed.
 
Status
Not open for further replies.
Top