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

Dune Worm v1.05b

Created for Zephyr Challenge #6 (Submission)




Requires

Art Credits

Tooltip

18160676.png

(Description is a little different in the demo)

Changelog
Code:
--1.05b
- Various minor efficiency improvements.

--1.05a
- Removed left over SetUnitPosition
- Current level added to tooltip
- Follow through time added

-- 1.05
- Removed unneeded Filter() call.
- Struct members are now nulled.

-- 1.04
- Documentation and instructions added,
- Removed SetUnitPosition and GroupClear
- Much object data fixed.
- CIRCLE_DIST increased to 10 by default.

-- 1.03
- Changed to library from scope (with requirements)
- xebasic requirement removed.
- Using GetWidgetX/Y instead of GetUnitX/Y
- Removed UnitAddAbility(u, 'Avul')
- Removed keyword data
- Constants CIRCLE_DIST, LOCUST_ID added.
- Demo map organized a bit more.

-- 1.02
- Fixed bug where the spell wouldn't attack as many units as it should

-- 1.01
- Inlined GetAOE()
- AttackCount reduced
- Description fixed

-- 1.00
- Initial contest submission.

Spell Code
JASS:
library DuneWorm initializer onInit requires TimerUtils, UnitStatus
//===========================================================================
//  Dune Worm v1.05b by TriggerHappy
//============================================================================
//
//  How to Install:
//      1. Copy the Dune Worm folder over to your map.
//      2. In the object editor, copy the Dune Worm ability
//         and the SUMMON unit (unless you'd like to use your own)
//      3. Once copied, change ABILITY_ID and WORM_DUMMY
//         to the raw codes that fit your map.
//
//  Notes:
//      Created for the Zephyr Contest #6 at THW.
//      Credits to Vexorian and Rising_Dusk for their systems
//      and everyone who has helped improve this.
//===========================================================================


//===========================================================================
    //  Configurables
//===========================================================================
    
    globals
        private constant integer WORM_DUMMY      = 'h000' // The worm dummy raw code
        private constant integer ABILITY_ID      = 'A000' // The raw code of the spell
        private constant integer CIRCLE_DIST     = 10 // How far apart each effect created in the circle is (increase if lag)
        private constant integer LOCUST_ID      = 'Aloc' 
        private constant real WAIT_TIME          = 3.5 // The worm timer interval
        private constant real WORM_DISTANCE      = 150 // How far away from the target the worm spawns
        private constant real CIRLCE_INT         = 0.05 // The timer interval for the circle effect
        private constant string UPRISE_ANIM      = "Morph Alternate" // The animation to while when the worm rises
        private constant string DIG_ANIM         = "Morph" // The animation to while when the worm rises
        private constant string CIRCLE_FX        = "Abilities\\Spells\\Undead\\Impale\\ImpaleMissTarget.mdl" // The fx on cast
        private constant string BLOOD_FX         = "Objects\\Spawnmodels\\Human\\HumanLargeDeathExplode\\HumanLargeDeathExplode.mdl" // The effect when the worm impales the enemy
        private constant weapontype WEAPON_TYPE  = WEAPON_TYPE_WHOKNOWS // The weapon type
        private constant damagetype DAMAGE_TYPE  = DAMAGE_TYPE_NORMAL // The damage type
        private constant attacktype ATTACK_TYPE  = ATTACK_TYPE_NORMAL // The attack type
    endglobals
    
    private constant function GetDamage takes integer lvl returns real
        return lvl*(100.00)
    endfunction
    
    // Level 1: 300
    // Level 2: 400
    // Level 3: 500
    private constant function GetAOE takes integer lvl returns real
        return 200. + (lvl * 100)
    endfunction
    
    private constant function AttackCount takes integer lvl returns integer
        return lvl
    endfunction
    
    //==============================
    //  Do not edit past this line
    //==============================
    
    globals
        private group GLOBAL_GROUP = CreateGroup()
        private player TempPlayer
    endglobals
    
    native UnitAlive takes unit id returns boolean
    
    private struct data
        
        unit worm
        unit caster
        unit target = null
        unit last
        real castX
        real castY
        real aoe
        real dmg
        player p
        integer level
        integer max
        integer tick = 0
        boolean b = false
        string nextAnim = UPRISE_ANIM
        
        static method PickUnit takes nothing returns boolean
            return IsPlayerEnemy(GetOwningPlayer(GetFilterUnit()), TempPlayer) and UnitAlive(GetFilterUnit())
        endmethod
        
        method onDestroy takes nothing returns nothing
            set this.worm   = null
            set this.caster = null
            set this.target = null
            set this.last   = null
        endmethod
        
        static method callback takes nothing returns nothing
            local real x = 0
            local real y
            local real f
            local timer t   = GetExpiredTimer()
            local data this = GetTimerData(t)
            
            if this.tick >= this.max then
                call RemoveUnit(this.worm)
                call ReleaseTimer(t)
                call this.destroy()
                return
            endif
            
            if this.target == null then
                set TempPlayer = this.p
                call GroupEnumUnitsInRange(GLOBAL_GROUP, this.castX, this.castY, this.aoe, Filter(function data.PickUnit))
                call ForGroup(GLOBAL_GROUP, function GroupPickRandomUnitEnum) // The BJ is completely fine.
                set this.target = FirstOfGroup(GLOBAL_GROUP)
                if not UnitAlive(this.target) or this.target == null then
                    set this.target = null
                    set this.last   = null
                    set this.tick   = this.tick + 1
                    return
                endif
                set this.last   = this.target
            endif
            
            if this.target != null then
                
                if this.nextAnim == UPRISE_ANIM then
                    set f = GetRandomReal(0, 360)
                    set x = GetUnitX(this.target) + WORM_DISTANCE * Cos(f* bj_DEGTORAD)
                    set y = GetUnitY(this.target) + WORM_DISTANCE * Sin(f* bj_DEGTORAD)
                    
                    call SetUnitFacing(this.worm, f+180)
                    call StunUnitTimed(this.target, WAIT_TIME)
                    
                    call SetUnitX(this.worm, x)
                    call SetUnitY(this.worm, y)
                else
                    call UnitDamageTarget(this.caster, this.last, this.dmg, true, false, ATTACK_TYPE, DAMAGE_TYPE, WEAPON_TYPE)
                    call DestroyEffect(AddSpecialEffectTarget(BLOOD_FX, this.target, "origin"))
                endif
                
                call SetUnitAnimation(this.worm, this.nextAnim)
                
                set x = WAIT_TIME
                
                if this.nextAnim == UPRISE_ANIM then
                    set this.nextAnim = DIG_ANIM
                    set x = WAIT_TIME-1
                elseif this.nextAnim == DIG_ANIM then
                    set this.nextAnim = UPRISE_ANIM
                    set this.tick = this.tick + 1
                    set this.target = null
                endif
                
                call PauseTimer(t)
                call TimerStart(t, x, true, function data.callback)
                    
            endif
            
        endmethod
          
        static method create takes unit u, integer lvl, unit c returns data
            local data this = data.allocate()
            local timer tmr = NewTimer()
            local real x
            local real y
            local integer i = 0
            
            set this.caster = c
            set this.worm   = u
            set this.level  = lvl
            set this.castX  = GetSpellTargetX()
            set this.castY  = GetSpellTargetY()
            set this.aoe    = GetAOE(lvl)
            set this.p      = GetOwningPlayer(u)
            set this.dmg    = GetDamage(lvl)
            set this.max    = AttackCount(lvl)

            loop
                exitwhen i > 360
                set x = this.castX + this.aoe * Cos(i*bj_DEGTORAD)
                set y = this.castY + this.aoe * Sin(i*bj_DEGTORAD)
                call DestroyEffect(AddSpecialEffect(CIRCLE_FX, x, y))
                set i = i + CIRCLE_DIST
            endloop
            
            call UnitAddAbility(u, LOCUST_ID)
            call SetTimerData(tmr, this)
            call TimerStart(tmr, 0, true, function data.callback)
            
            return this
        endmethod
        
    endstruct
    
    private function Actions takes nothing returns boolean
        local unit u
        if GetSpellAbilityId() == ABILITY_ID then
            set u = GetTriggerUnit()
            call data.create(CreateUnit(GetOwningPlayer(u), WORM_DUMMY, 0, 0, 0), GetUnitAbilityLevel(u, ABILITY_ID), u)
            set u = null
        endif
        return false
    endfunction
    
    //===========================================================================
    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(t, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(t, Filter(function Actions))
    endfunction
    
endlibrary

Keywords:
dune,worm,sand,vjass,jass,dope,zephyr,contest,triggs,triggerhappy,monster,spell,ability
Contents

Dune Worm (Map)

Reviews
Dune Worm v1.05 | Reviewed by Maker | 20th October 2013 Concept[/COLOR]] A giant work emerging from the ground and having a go at its target is both original and impressive Triggers[/COLOR]] The spell is MUI There is sufficient...

Moderator

M

Moderator


Dune Worm v1.05 | Reviewed by Maker | 20th October 2013

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

Concept[/COLOR]]
126248-albums6177-picture66521.png

A giant work emerging from the ground and having a go at its target
is both original and impressive
Triggers[/COLOR]]
126248-albums6177-picture66521.png
  • The spell is MUI
  • There is sufficient configuration options and the coding is generally fine
126248-albums6177-picture66523.png
  • Get rid of call SetUnitPosition
Objects[/COLOR]]
126248-albums6177-picture66521.png
  • Almost everything seems to be set up correctly
126248-albums6177-picture66523.png
  • You should add follow through time so the caster completes the
    casting animation
Effects[/COLOR]]
126248-albums6177-picture66521.png
  • The effects are alright
126248-albums6177-picture66523.png
  • The spikes that rise from the ground could make a sound
  • The learn tooltip is missing the text that displays which level
    the ability is
  • When the ability is cast, the worm can be briefly seen a bit before
    it is supposed to emerge
  • The sound effect when the worm rises and goes back under ground
    could be something different. Try Impale, Burrow or Root/Uproot for example
Rating[/COLOR]]
CONCEPTTRIGGERSOBJECTSEFFECTSRATINGSTATUS
126248-albums6177-picture75360.jpg
126248-albums6177-picture75360.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75359.jpg
APPROVED
Links[/COLOR]]

[COLOR="gray"

[/TD]



Dune Worm v1.03 | Reviewed by Maker | 20th October 2013

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

[COLOR="gray"

Concept[/COLOR]]
126248-albums6177-picture66521.png

A giant work emerging from the ground and having a go at its target
is both original and impressive
Triggers[/COLOR]]
126248-albums6177-picture66521.png
  • The spell is MUI
  • There is sufficient configuration options and the coding is generally fine
126248-albums6177-picture66522.png
  • Null the struct unit members before calling destroy
126248-albums6177-picture66523.png
  • You could use SetUnitX/Y instead of SetUnitPosition
    SUP checks pathing and that is a slow process
  • By default the spell causes a big FPS drop since it creates so many
    special effects. Luckily it can be configured
  • There is no importing instructions
  • GroupEnumUnitsInRange clears the group before adding units to the group,
    so I don't think there is need for call GroupClear(GLOBAL_GROUP)
Objects[/COLOR]]
126248-albums6177-picture66521.png
  • The tooltip description is...well...descriptive and compact
  • The tooltip lists the stats per level
126248-albums6177-picture66523.png
  • The tooltip does not have learn hotkey and it is missing
    the text that displays the level
  • You could change the command card position to [0,2]
  • The cast animation of the spell is "spell channel" but the
    hero in the map does not have that animation
  • Art - Caster effect is the default Channel one, but Art - Duration is 0
    Is the ability really supposed to have that art?
  • Set Disable other abilities to false so the command card isn't
    blacked out
  • You should add follow through time so the caster completes the
    casting animation
  • The worm unit gives huge vision and uses upgrades and food
Effects[/COLOR]]
126248-albums6177-picture66521.png
  • The initial cast effects look fine
  • The custom worm model is great
  • The ground effect with the dust fit the spell very well
126248-albums6177-picture66523.png
  • The Art - caster should not be there
  • When the ability is cast, the worm can be briefly seen a bit before
    it is supposed to emerge
Rating[/COLOR]]
CONCEPTTRIGGERSOBJECTSEFFECTSRATINGSTATUS
126248-albums6177-picture75360.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75357.jpg
126248-albums6177-picture75359.jpg
126248-albums6177-picture75359.jpg
NEEDS FIX

[COLOR="gray"

[/TD]
 
Very good idea... nice spell. But in my test, sometimes worm didnt attack as often as he should, and then since a certain point no worm summoned anymore.

But looks really nice!

Ah you're right.

I'm updating the fixed version. The problem wasbj_groupRandomCurrentPickwould sometimes return null. I fixed it by usingFirstOfGroupinstead but does anyone know why it's happening?

EDIT: Updated.
 
Level 9
Joined
Dec 12, 2007
Messages
489
The problem wasbj_groupRandomCurrentPickwould sometimes return null.
your trigger
JASS:
call ForGroup(GLOBAL_GROUP, function GroupPickRandomUnitEnum)
from JASSCraft
JASS:
function GroupPickRandomUnitEnum takes nothing returns nothing
    set bj_groupRandomConsidered = bj_groupRandomConsidered + 1
    if (GetRandomInt(1,bj_groupRandomConsidered) == 1) then
        set bj_groupRandomCurrentPick = GetEnumUnit()
    endif
endfunction

function GroupPickRandomUnit takes group whichGroup returns unit
    // If the user wants the group destroyed, remember that fact and clear
    // the flag, in case it is used again in the callback.
    local boolean wantDestroy = bj_wantDestroyGroup
    set bj_wantDestroyGroup = false

    set bj_groupRandomConsidered = 0
    set bj_groupRandomCurrentPick = null
    call ForGroup(whichGroup, function GroupPickRandomUnitEnum)

    // If the user wants the group destroyed, do so now.
    if (wantDestroy) then
        call DestroyGroup(whichGroup)
    endif
    return bj_groupRandomCurrentPick
endfunction
as you can see, the GroupPickRandomUnitEnum uses bj_groupRandomConsidered to determine the random part,

the BJ alternative always set the bj_groupRandomConsidered to 0 before forgroup call, thus the random unit is at least will never be null from the first loop.

your bj_groupRandomConsidered is always increases and never reset to 0, thus to actually return not null unit, you need to get pass over this if-then-else if (GetRandomInt(1,bj_groupRandomConsidered) == 1) then

to solve this, you can either use the GroupPickRandomUnit() or set the bj_groupRandomConsidered to 0 before forgroup call.

I suggest you to add set bj_groupRandomCurrentPick = null in case of forgroup call on empty group returning last forgroup call result.

on the trigger itself,
JASS:
            call UnitAddAbility(u, 'Avul')
            call UnitAddAbility(u, 'Aloc')
why not place those rawcode into constant? probably for anyone who somehow change the default ability.

on the create method, is it necessary for 360 sfx at once? it probably lags a bit on lower spec PC.

JASS:
                    call DestroyEffect(AddSpecialEffectTarget(BLOOD_FX, this.target, "origin"))
allow the attachment point to be configurable.

in callback method, you should null the timer variable t
same case on variable tmr on create method.
 
Last edited:
your trigger
JASS:
call ForGroup(GLOBAL_GROUP, function GroupPickRandomUnitEnum)
from JASSCraft
JASS:
function GroupPickRandomUnitEnum takes nothing returns nothing
    set bj_groupRandomConsidered = bj_groupRandomConsidered + 1
    if (GetRandomInt(1,bj_groupRandomConsidered) == 1) then
        set bj_groupRandomCurrentPick = GetEnumUnit()
    endif
endfunction

function GroupPickRandomUnit takes group whichGroup returns unit
    // If the user wants the group destroyed, remember that fact and clear
    // the flag, in case it is used again in the callback.
    local boolean wantDestroy = bj_wantDestroyGroup
    set bj_wantDestroyGroup = false

    set bj_groupRandomConsidered = 0
    set bj_groupRandomCurrentPick = null
    call ForGroup(whichGroup, function GroupPickRandomUnitEnum)

    // If the user wants the group destroyed, do so now.
    if (wantDestroy) then
        call DestroyGroup(whichGroup)
    endif
    return bj_groupRandomCurrentPick
endfunction
as you can see, the GroupPickRandomUnitEnum uses bj_groupRandomConsidered to determine the random part,

the BJ alternative always set the bj_groupRandomConsidered to 0 before forgroup call, thus the random unit is at least will never be null from the first loop.

your bj_groupRandomConsidered is always increases and never reset to 0, thus to actually return not null unit, you need to get pass over this if-then-else if (GetRandomInt(1,bj_groupRandomConsidered) == 1) then

to solve this, you can either use the GroupPickRandomUnit() or set the bj_groupRandomConsidered to 0 before forgroup call

Thanks

Geshishouhu said:
This is a really really nice spell! If the movement or animation speed of the worm is configurable, it would be perfect.

Agreed, I'll consider adding those.

Geshishouhu said:
BTW: TriggerHappy, are you TriggerHappy187? :)

Yeah.

UPDATED WITH VIDEO
 
>> null local trigger t

Not needed, the trigger is never destroyed.

>> This might be usefull:]

Overkill IMO, maybe I can implement as an optional library or something.

>> I'm ~99% sure that 'Avul' is needless, when adding 'Aloc' aswell.

Thanks for the tip I'll look into it.

>> why not place those rawcode into constant? probably for anyone who somehow change the default ability.

mm, alright.

>>in callback method, you should null the timer variable t

The timers are recycled, it doesn't need be nulled.

>> Beside that, this spell is refreshing cool. :D

Thanks :)
Funny cause I made it years ago.
 
Last edited:
Level 11
Joined
Oct 11, 2012
Messages
711
>> null local trigger t

Not needed, the trigger is never destroyed.

>> This might be usefull:]

Overkill IMO, maybe I can implement as an optional library or something.

>> I'm ~99% sure that 'Avul' is needless, when adding 'Aloc' aswell.

Thanks for the tip I'll look into it.

>> Beside that, this spell is refreshing cool. :D

Thanks :)
Funny cause I made it years ago.

With "Aloc", the unit is invulnerable. :)

Edit:
Glad to see you back, TriggerHappy. :)
 
When you use TimerUtils with structs, you don't need to save the timer as a member of the struct. Remove all this. front of the struct member.

GetWidgetX/Y faster than GetUnitX/Y, this key is here:

JASS:
type unit extends widget
type widget extends agent
type agent extends handle

Because worm powerful and can move any terrain so i think don't need:
SetUnitPosition
=>
SetUnitX SetUnitY

Very nice spell but i love the colorful >.<, 5/5 with other people but with me is 4/5...^^~
 
Last edited:
When you use TimerUtils with structs, you don't need to save the timer as a member of the struct

Yeah, you're right.

EDIT: I don't do that?

Remove all this. front of the struct member.

That's a personal preference, why would I have to remove it?

GetWidgetX/Y faster than GetUnitX/Y, this key is here:

Forgot about those :p

Very nice spell but i love the colorful >.<, 5/5 with other people but with me is 4/5...^^~

What do you mean "but i love the colorful"?

--

I will be uploading an optimized version later (with more configuration).
 
Last edited:
Level 9
Joined
Dec 12, 2007
Messages
489
>> null local trigger t
Not needed, the trigger is never destroyed.
>>in callback method, you should null the timer variable t
The timers are recycled, it doesn't need be nulled.

it's not about the timer is recycled or the trigger is never destroyed, it's about the pointer leak. whether pointer leak is valid or not, its a good practice eventually.
 
Level 13
Joined
Mar 19, 2010
Messages
870
It's a cool spell. I'm thinking to use it in my project "Forsaken Bastion's Fall" but i need a worm looks like undead. A worm with bones or something like that. I would place him at a graveyard and he would do your spell in a loop the whole game. I send the creater of the model a message and if he would do that, here's my question to you: Can i use your code?

Reg.
 
I've tested that before and I tested it again, this is not true. If you can prove otherwise, please post a test configuration.

Thanks for that, I'll keep it in mind for the next update :thumbs_up:

pred1980 said:
A worm with bones or something like that. I would place him at a graveyard and he would do your spell in a loop the whole game. I send the creater of the model a message and if he would do that, here's my question to you: Can i use your code?

Yeah, go ahead (that's why I submitted it).

EDIT: v1.03 uploaded.
 
Last edited:
Level 37
Joined
Mar 6, 2006
Messages
9,240
The tooltip lacks the current level, and you could remove the "description" word. You could set the learned icon position to [0,2]. There is no learn hotkey.

The cast animation is set to an animation that the hero does not have. There is no follow through time so the casting animation gets aborted a bit too quickly. Set "Disable other abilities" to false. Is the ability meant to use the Art - caster effect? Art - duration is 0.

The worm uses food and upgrades. Give the unit Locust in object editor. You can check if the unit has Locust in the create method if you want to be sure the unit has it.

The spell causes a pretty heavy FPS drop by default, but luckily that can be countered with the variable. You could use radians in the loop instead of degrees.

GroupEnumUnitsInRange clears the group before adding units to the group, so I don't think there is need for call GroupClear(GLOBAL_GROUP)

You could use SetUnitX/Y for the worn instead of SetUnitPosition as you don't need to use pathing.

Null the local timer variable. Null the unit struct members before .destroy if they are not null already.
 
Level 20
Joined
Aug 13, 2013
Messages
1,696
Review


Dune Worm v1.03
This spell had been reviewed by jakeZinc.


Trigger Code
Thecode looks good and clean.
Documentation
The documentation is well good but in this line private constant integer LOCUST_ID = 'Aloc' this must have a documentation because some users may harm that if they didn't knew what was that for.
Configuration
The configuration is fine, but you can use global constant as for the damage base, damage per level and use the constant function for enumerating the constant globals.
Pros
It is nice spell and unique idea.
Cons
Having 360 special effect at once may cause lag, maybe add a configuration on that.
Importing Instruction
There's no importing instruction in the spell
Special Effects
The special effects are well fine and the worm model really fits the spell. Maybe make a special effect in the position of the worm while moving, example: Impale Dust, to make it more realistic
Usefulness
Can be useful hunting down enemies one by one
Suggestions
Fix the button position of the spell, the test map must have at least 2 casters or make the cooldown of ability to 0. You can use set tmr = NewTimerEx(this) instead of call SetTimerData(tmr, this) but I think there's no difference at that ;). You can use SpellEffectEvent so that the 3 lines will be 1 line. Put the onInit function and Action function inside the struct and make them static method. Merge the onCast and constructor into 1 static method. You can use thistype but I think data is shorter ;).
Description Tooltip
I think the learn tooltip is nice and it is support every level of the spell.

jakeZinc Score Board
Rejected: 1-10
Unacceptable : 11-30
Lacking : 31-49
Useful : 50-69
Recommended : 70-90
Highly Recommended : 91-100
Director Cut : 101-105
Scores
1) Triggering - 18/20
2) Documentation - 6/10
3) Importing Instruction - 0/10
4) Special Effects - 14/15
5) Usefulness - 15/20
6) Tooltip - 7/10
7) Configuration - 7/10
Ratings
Total Score - 67/105
jakeZinc Rating - 3.5/5
Status - Useful and Voting for Approval
 
Level 13
Joined
Mar 19, 2010
Messages
870
@TriggerHappy

As i told you, i'll include your really cool spell in our mod "Forsaken Bastion's Fall" as a great special event on our graveyard.

Sry, but i'd to adapt the code a bit because no unit/hero cast this worm and i've a hint for your "TempPlayer".

in the struct data you can create a variable "static thisytpe temp" and in the create() you can just write "set .temp = this" and so you can easy use the "p" in this way ".temp.p" to get the player ;-)

I'll give credit :) So. Thx a lot.
 
Top