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

Chernobyliss v1.02

This bundle is marked as useful / simple. Simplicity is bliss, low effort and/or may contain minor bugs.
  • Like
Reactions: Haley
Hi guys, I'm here with another spell that will be featured in my map, Enfo's FFB Edition. This time, I uploaded the ultimate of Shandris, the Nightblade, a poison based hero.

Anyways, here is the description of the spell:


Infects an area with an acid explosion that erupts from the ground, knocking back all nearby enemies and infecting them with a deadly toxin that slows them and deals damage over time. The toxin will have a chance to spread to nearby uninfected units every second, until it wears off.


It requires TimerUtils by Vexorian, and optionally KBS by kenny.

And here is the trigger:

JASS:
scope Chernobyliss initializer OnInit
//Chernobyliss by Strikest
//Requires TimerUtils by Vexorian(http://www.wc3c.net/showthread.php?t=101322)
//Optionally requires KBS by kenny(http://www.thehelper.net/threads/knockback-system-kbs.96837/)

    globals
        private constant integer ABIL_ID = 'A000' //Raw code of ability.
        private constant integer SLOW_ID = 'A001' //Raw code of the slow ability that is attached to affected units.
        private constant string EFFECT1 = "war3mapImported\\Acid Ex.mdx"//Path of the desired special effect that is shown at target area.
        private constant string EFFECT2 = "war3mapImported\\VenomousGaleV2_Portrait.mdx" //Path of the desired special effect you want attached to affected units.
        private constant string EFFECT3 = "Abilities\\Spells\\Other\\AcidBomb\\BottleMissile.mdl"//Path of the desired special effect that is shown when a unit is affected.
        private constant string ATTACH = "chest" //String of the desired attachment point for EFFECT2.
        private constant string ATTACH2 = "chest"//String of the desired attachment point for EFFECT3
        private constant string KB_SFX = "" //Path of the desired special effect you want attached to knocked back units.
        private constant real TIMER_INTERVAL = 32. //Timer interval.
        private constant real KB_DURATION = .6 //Duration of optional knockback.
        
        private constant boolean CHANGE_VERTEX_COLOR = true//Choose whether or not you want to gradually change the vertex color of affected units as their health decreases.
        
        private constant attacktype ATTACK_TYPE = ATTACK_TYPE_MAGIC //Attacktype you want
        private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_MAGIC //Damagetype you want
        
        private constant real TREE_AOE = 0.0
        private constant boolean ALLOW_MOVE = false
        private constant boolean CHECK_PATHING = false
//Copied from kenny's KBS trigger
//**    - TREE_AOE      - This determines whether or not trees will be knocked down.   **
//**                      For trees to be knocked down, a positive number (real) must  **
//**                      be used, such as 150.00, which would give a radius of 150.00 **
//**                      in which trees will be knocked down.                         **
//**                      For trees to not be knocked down, a negative number (real)   **
//**                      must be used, such as -150.00, which would create a radius   **
//**                      that if a tree comes within it, the unit will stop moving.   **
//**                      For none of those effects, the number 0 (0.00) can be used.  **
//**                      This will just cause the units to "bounce" off trees.  
//**    -ALLOW_MOVE     - This boolean will decided whether or not you want the unit   **
//**                      to have the ability to move while being knocked back.        **
//**                      "true" will allow them to move, while "false" will not.      **
//**    -CHECK_PATHING  - A boolean that, if true, will check for unpathable terrain   **
//**                      such as a wall or cliff, or where doodads may be. If false   **
//**                      it will ignore these changes and the unit will be pushed     **
//**                      along the wall, cliff or doodad.         
            
        private constant group GROUP = CreateGroup( )//Don't touch.
        private unit CASTER //Don't touch this either.
        private real SPELL_X
        private real SPELL_Y
    endglobals
    
    private constant function DamagePerSecond takes integer level,integer herostr,integer heroagi, integer heroint returns real
        return (7. + level * 2.) / TIMER_INTERVAL
    endfunction
    
    private constant function Duration takes integer level returns real
        return 12. + level * 2.
    endfunction
    
    private constant function AoE takes integer level returns real //Area of effect of the spell.
        return 215. + (10. * level)
    endfunction
    
    private constant function SpreadAoE takes integer level returns real //Area of effect around affected units that it will spread to.
        return 170.
    endfunction
    
    private constant function KBDistance takes integer level returns real //Distance the enemy will optionally be knocked back from target point.
        return 200. + level * 10.
    endfunction
    
    private constant function ChanceToSpread takes integer level returns real //Chance to spread the toxin. A value of 1. will mean that it has a 100% of spreading.
        return 100. / TIMER_INTERVAL
    endfunction
    
    private constant function FilterUnits takes unit u returns boolean //Filter which units will be afffected.
        return IsUnitEnemy( u, GetOwningPlayer( CASTER ) )
    endfunction
//////////////////////////////////////////////////////NO TOUCHY PAST THIS POINT UNLESS YOU KNOW WHAT YOU ARE DOING/////////////////////////////////////////////////////////////////////////////
    
    private struct Cbliss
        unit cast
        unit t
        real x
        real y
        real dur
        integer lvl
        effect attach
        
        method destroy takes nothing returns nothing
            set this.cast = null
            set this.t = null
            set this.attach = null
            call this.deallocate()
        endmethod
    endstruct
        
    native UnitAlive takes unit id returns boolean
        
    private function Handler takes nothing returns nothing
        local timer t = GetExpiredTimer( )
        local Cbliss data = GetTimerData( t )
        local unit u
        local unit ug = null
        local unit cast
        local integer lvl = data.lvl
        local real dur = data.dur - 1. / TIMER_INTERVAL
        local real life
        local real mlife
        local real plife
        
        set cast = data.cast
    
        if data.dur <= 0 or not UnitAlive(data.t) then
            call DestroyEffect( data.attach )
            call UnitRemoveAbility( data.t, SLOW_ID )
            static if CHANGE_VERTEX_COLOR then
                call SetUnitVertexColor(data.t,255,255,255,255)
            endif
            call data.destroy( )
            call ReleaseTimer( t )
        else
            call UnitDamageTarget(data.cast,data.t,DamagePerSecond(data.lvl,GetHeroStr(data.cast,true),GetHeroAgi(data.cast,true),GetHeroInt(data.cast,true)),false,true,ATTACK_TYPE,DAMAGE_TYPE,WEAPON_TYPE_WHOKNOWS)
            static if CHANGE_VERTEX_COLOR then
                set life = GetWidgetLife(data.t)
                set mlife = GetUnitState(data.t,UNIT_STATE_MAX_LIFE)
                set plife = life/mlife
                if plife >= .3 then
                    call SetUnitVertexColor(data.t,R2I(plife* 255.),255,R2I(plife* 255.),255)
                else
                    call SetUnitVertexColor(data.t,60,130,60,255)
                endif
            endif
            set data.dur = data.dur - 1. / TIMER_INTERVAL
            call SetTimerData( t, data )
            call TimerStart( t, 1. / TIMER_INTERVAL, false, function Handler )
            set data.x = GetUnitX(data.t)
            set data.y = GetUnitY(data.t)
            call GroupEnumUnitsInRange( GROUP,data.x,data.y, SpreadAoE(lvl), null )
            loop
                set ug = FirstOfGroup(GROUP)
                exitwhen ug == null
                if UnitAlive(ug) and GetUnitAbilityLevel(ug,SLOW_ID) == 0 and data.dur > 1. / TIMER_INTERVAL and GetRandomInt(0,100) <= R2I(ChanceToSpread(lvl)) then
                    if FilterUnits(ug) then
                        set data = data.create()
                        set data.cast = cast
                        set data.t = ug
                        set data.x = GetUnitX(ug)
                        set data.y = GetUnitY(ug)
                        set data.lvl = lvl
                        set data.dur = dur
                        set data.attach = AddSpecialEffectTarget(EFFECT2,ug,ATTACH)
                        call DestroyEffect(AddSpecialEffectTarget(EFFECT3,ug,ATTACH2))

                        call UnitAddAbility(ug,SLOW_ID)
                        call SetUnitAbilityLevel(ug,SLOW_ID,lvl)
                
                        call TimerStart(NewTimerEx(data),1. / TIMER_INTERVAL,false,function Handler)
                    endif
                endif
                call GroupRemoveUnit(GROUP,ug)
            endloop

        endif
    
        set t = null
        set u = null
        set ug = null
        set cast = null
    endfunction
    
    private function FilterActions takes nothing returns boolean
        local Cbliss data
        local unit u = GetFilterUnit()
        local real x1 = SPELL_X
        local real y1 = SPELL_Y
        local real x2 = GetUnitX(u)
        local real y2 = GetUnitY(u)
        local real a = bj_RADTODEG * Atan2(y2 - y1, x2 - x1)
        local integer lvl = GetUnitAbilityLevel(CASTER,ABIL_ID)
        
        if UnitAlive(u) then
            if FilterUnits(u) then
                static if LIBRARY_KBS then
                    call KBS_BeginEx( u, KBDistance(lvl), KB_DURATION, a, KB_SFX, TREE_AOE, ALLOW_MOVE, CHECK_PATHING )
                endif
                set data = Cbliss.create()
                set data.cast = CASTER
                set data.t = u
                set data.x = x2
                set data.y = y2
                set data.attach = AddSpecialEffectTarget(EFFECT2,u,ATTACH)
                set data.lvl = lvl
                set data.dur = Duration(lvl)
                
                call DestroyEffect(AddSpecialEffectTarget(EFFECT3,u,ATTACH2))
                call UnitAddAbility(u,SLOW_ID)
                call SetUnitAbilityLevel(u,SLOW_ID,lvl)
                
                call TimerStart(NewTimerEx(data),1. / TIMER_INTERVAL,false,function Handler)
            endif
        endif
        
        set u = null
        return false
    endfunction
        
    private function OnSpell takes nothing returns boolean
        local real x = GetSpellTargetX()
        local real y = GetSpellTargetY()
        
        if GetSpellAbilityId() == ABIL_ID then
            call DestroyEffect(AddSpecialEffect(EFFECT1,x,y))
            set CASTER = GetTriggerUnit()
            set SPELL_X = x
            set SPELL_Y = y
            call GroupEnumUnitsInRange(GROUP,x,y,AoE(GetUnitAbilityLevel(CASTER,ABIL_ID)), Filter( function FilterActions ) )
        endif
        return false
    endfunction

    private function OnInit takes nothing returns nothing
        local trigger trig = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ( trig, EVENT_PLAYER_UNIT_SPELL_EFFECT )
        call TriggerAddCondition( trig, Condition( function OnSpell ) )
        set trig = null
    endfunction
        
endscope

And I'd like to thank the following for their resources:

Vexorian: TimerUtils.
kenny: KBS.
judash137: Acid Ex.mdx
JesusHipster: VenomousGaleV2_Portrait.mdx

Changelog

v1.01
Fixed a bug that caused alot of lag past level one of the spell.
Merged Actions and Onspell into one.
Nulled struct members.
Changed the function names to the "standard"
Changed GetUnitState to GetWidgetLife.
Set the life/mlife calculation to a variable.
Updated the spell to have 4 levels.
Updated the spell tooltips.

v1.02
Called my own destroy method instead of onDestroy.



Keywords:
poison, toxic, spread, radioactive, venom, disease, infection, virus, infect, plague
Contents

Chernobyliss v1.00 (Map)

Reviews
12th Dec 2015 IcemanBo: Too long as NeedsFix. Rejected. 10:31, 4th Feb 2014 BPower: - Get rid of the ForGroup enumeration and use a FirstOfGroup loop instead. - The timer should be periodic, also pass in TIMER_INTERVAL and not...

Moderator

M

Moderator

12th Dec 2015
IcemanBo: Too long as NeedsFix. Rejected.

10:31, 4th Feb 2014
BPower:
- Get rid of the ForGroup enumeration and use a FirstOfGroup loop instead.
- The timer should be periodic, also pass in TIMER_INTERVAL and not 1/TIMER_INTERVAL
- One spell cast easily allocates 40 instances, hence use one timer per spell cast coupled with a stack (could use Table by Bribe or Vex) or one timer for the whole scope with a linked list and a stack.
- Add a player member to the struct and get rid of CASTER, SPELL_X and SPELL_Y
- You could pass a player and a unit variable into FilterUnits to get rid of the GetOwningPlayer(..)
- You don't have to null ug, it gets nulled by the FoG loop you are using.
- Move the UnitAlive native to the top, because Cohadars JassHelper allows only one declaration.
- You don't need the Cbliss x and y member at all.
- R2I(ChanceToSpread(lvl)) is constant per cast, you could add a variable for it.
- Since you change the vertex color, RGB should be configurable

The concept and object are good and I would give you a rating of ~3.5 for it.
The current coding is ~1.5 and needs to be changed.
 
  • Remove the Actions function altogether and place it directly in the GetSpellAbilityId() block.
  • You should be nullingCASTERas well as your struct members to prevent leaks.
  • JASS:
    TimerStart(...,1. / TIMER_INTERVAL)
    // shouldn't it be
    TimerStart(..., TIMER_INTERVAL)
  • There should be a bit more documentation for things like importing and spell description.
 
but CASTER isn't a local? It's a global variable, like udg? And people keep telling me null struct members, but how do you do that? I never learned because if I recall it's a fine practice and does not leak anything even remotely noticeable.

1./TIMER_INTERVAL is so it's easier for people to change the timer interval, as it will be a whole number. For example, my TIMER_INTERVAL is 32, so 1./32. = 0.03125.

I dunno about merging the actions and conditions... makes it more confusing for people to learn my code.
 
but CASTER isn't a local? It's a global variable, like udg? And people keep telling me null struct members, but how do you do that? I never learned because if I recall it's a fine practice and does not leak anything even remotely noticeable.

They leak, so just null them.

To null struct members just define your own destroy method

JASS:
method destroy takes nothing returns nothing
    this.caster = null
    this.fx = null
    // ect..
endmethod

1./TIMER_INTERVAL is so it's easier for people to change the timer interval, as it will be a whole number. For example, my TIMER_INTERVAL is 32, so 1./32. = 0.03125.

So then put that directly in the global otherwise it will confuse people. It's also more efficient.

JASS:
private constant integer TIMER_INTERVAL = 0.03125
// or
private constant integer TIMER_INTERVAL = 1./32.

I dunno about merging the actions and conditions... makes it more confusing for people to learn my code.

It's just another function call that's not needed. I don't think it makes it any less legible.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
OnSpell and Actions could be merged into one.
GetUnitState(data.t,UNIT_STATE_LIFE) --> GetWidgetLife(data.t)
It is not a rule, but why aren't you working on the pending resources of yours before posting new ones? Me and and others pointed out many issues that should/must be changed.
I get the feeling, you want someone to double check your stuff rather than contributing it to the community.

Again consider this JPAG. To get one thing clear JPAG is not inconsequential.
 
I get the feeling, you want someone to double check your stuff rather than contributing it to the community.

When you assume you make an ass of u and me. If I just wanted someone to double check my other spell, I wouldn't have updated it seven times. Also, I fixed most of the things that others pointed out, The only things I didn't fix are the changes I didn't understand, which I asked them to explain more.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
  • TIMER_INTERVAL should be 0.03125 and not 1/32
  • functions names should be DamagePerSecond and not DAMAGE_PER_SECOND ( important )
  • OnSpell and Actions should be merged into one
  • I recommend a FirstOfGroup loop over the ForGroup you are currently using.
  • CASTER as variable is not nessesary at all.
  • Use a player variable as member of the struct
  • The timers should be periodic. As alternative you could use only one timer for everything and a linked list.
  • life / mlife calculate that once and store it to a local real.
  • null the struct members cast, t, attach once they are destroyed or the struct instance is deallocated.
  • The spell should have a maximum of 4 levels otherwise it prolongs the map loading time
  • The tooltip could be improved, use the blizzard standart hero abilities as template (color, hotkey)

The effects are cool, but the coding is not efficient. That is one of the reasons for fps drops once more than a few units are infected by the poison.
The other reason is the huge amount of special effects. (Well that one can't be changed)

Maybe I have time on the weekend, to show you how to improve your code. Or maybe someone else here does.
 
Level 18
Joined
Sep 14, 2012
Messages
3,413
I strongly suggest to merge the three func actions and use FoG loop : this will lead to faster executions.
Some variables are unneeded like SPELL_X SPELL_Y CASTER ....

local unit ug = null
Why ?

Don't use TimerUtils but use only one single timer for everything xD
One timer every 0.0312500 is helly performance eater :/
 
Updated to v.101, fixing a bug that made the spell lag alot, and also fixed most of the things TriggerHappy and BPower mentioned, including:

Merging Actions and Onspell into one.
Nulling struct members.
Changing the function names to the "standard"
Changed GetUnitState to GetWidgetLife.
Set the life/mlife calculation to a variable.
Updated the spell to have 4 levels.
Updated the spell tooltips.

I did not understand, why do i have to add a player member to the struct?

And how do I use periodic? I never really used them, restarting timers felt like the same thing to me.

I did not change the timer interval because at the end of the day it's a matter of preference, 1/32 or .03125, the end result is 1/32 or .03125. Also I believe FoG groups are optional since the performance difference from GroupEnumUnitsInRange is negligible.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Do not use method onDestroy.
There's another known method, but please, if you ever see someone use it, you
should flame them give them a link to this tutorial:

JASS:
method onDestroy takes nothing returns nothing
    // do stuff when you destroy
endmethod

This method is bad because it generates useless functions.
Instead of creating this method that runs when you call destroy,
why don't you just write the destroy method?
Isn't that logical?
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
To achieve a acceptable performance I would recommend a radical change of the whole code. On a second view I think you don't have to use a linked list for the timer.

Using TimerUtils and one timer per spell cast is fair enough. Note currently you are using one timer per struct instance.
One way would be to use two structs one for the main cast and the other for each chernoblala projectiles ^^ plus a Table array.
It is my favorized style for coding spells with many proectiles. But I don't know how to guide you here, it would basically mean that I write the whole code for you.

The current problem is, that you use ~50 timers per spell instance or more after all it is ENFO.
 
So currently the way I did it is the most optimized it can be, and to make it even more optimize I have to recode it your way? Can I code it with an integer array for one struct, and then use the second struct recall the integer array for the spell effect? Sort of like what you do to make spells MUI in GUI, using a disabled loop that activates when you cast the spell.
 
Top