Spell Molten Matter

GoGo-Boy

You can change this now in User CP
Reaction score
40
GoGo-Boy aka Na_Dann_Ma_GoGo presents: MOLTEN MATTER


JASS: Yes.
vJASS: Yes.
MUI: Yes.
Laggless: Yes.
Leakless: Yes.
JESP: Hopefully, you got to tell me :)
Requires: NewGen Editor, ABCT Credits to Cohadar
The really nice testmap was made by Tinki3 and then edited by me to suit this certain spell testing.

Description:
This is a spell which can be used for dummy spells that are based of stuff that targets a location. A projectile will fly towards the targeted location and deals damage in a radius. While flying the projectile checks units in its range and little projectiles will spread out and target the checked units' location. This however only happens once to each unit. The little projectiles deal a minor damage in a minor radius. If the big projectile is in a certain range to it's impact location it will NOT release any further little balls, since that doesn't make any sense and I had to re-script quite a lot stuff to let the little projectiles fly on while the main one is already exploded.



Tooltip:
The caster throws a chunk of molten matter towards a targeted location. Whenever a units comes within 400 AOE of that liquid mass parts of the matter split and float to the units location (neither the big chunk nor the little chops FOLLOW a unit). On impact the little parts deal 15 * level damage while the big one can cause up to 75 * level damage in a certain radius. The more matter has been split the less damage the greater impact deals. A maximum of 15 matter chops can spread out before the main matter explodes without dealing any damage.

Level 1 - A chunk of molten matter that flies towards the targeted position and loses some matter, that target units. On impact the little chops deal 15 and the big matter ball up to 75 damage.

Level 2 - A chunk of molten matter that flies towards the targeted position and loses some matter, that target units. On impact the little chops deal 30 and the big matter ball up to 150 damage.

Level 3 - A chunk of molten matter that flies towards the targeted position and loses some matter, that target units. On impact the little chops deal 45 and the big matter ball up to 225 damage.

Screenshot:
moltenmatterhx9.jpg

Implantation can be read in an explanation above the trigger.

Here comes the code:
JASS:
scope MoltenMatter

globals
    private constant integer FIREBALL_DUMMY = 'u001' // this dummy model must have the dummy.mdx model to work. It is included in the import manager+
    private constant integer DUMMY_SPELL_ID = 'A000' // enter the correct ability ID of the ability you use
    private constant string EXPLODE_PATH = "Abilities\\Weapons\\PhoenixMissile\\Phoenix_Missile.mdl" // model path for the impact effect of the main lavaball
    private constant damagetype DAMAGE_TYPE = DAMAGE_TYPE_UNIVERSAL // the damage type
    private constant attacktype ATTACK_TYPE = ATTACK_TYPE_CHAOS // the attack type
    private constant real BIG_BALL_SIZE = 2.1 // the size of the main lavaball
    private constant real SIZE_LOSS = 0.04 // this determinates the size loss of the main ball whenever little ones spread out
    private constant real LITTLE_BALL_SIZE = 0.85 // the size of the spreeding lavaball
    private constant real SPEED = 20  // distance amount the fireballs move per interval
    private constant real LITTLE_SPEED = 20
    private constant integer MAX_BALLS = 15 // determinates the amount of balls that can spread out of the big one, should NOT be set too high because the struct limit might struggle then
    private constant real RADIUS = 400 // this is the radius in which little balls spread out of the main one and fly forwards enemy units
    // better do NOT combine a low LITTLE_SPEED with a hugh RADIUS and high MAX_BALLS because that could end up laggy, since a lot units might be moved in some situations
   
    private constant real BIG_DAMAGE_RADIUS = 300  // the damage radius for the main-impact
    private constant real LITTLE_DAMAGE_RADIUS = 150 // same for the one of the little lavaballs
    private constant real BIG_DAMAGE = 75 // AOE damage dealt on impact by the big lavaball
    private constant real LITTLE_DAMAGE = 15 // the amount of AOE damage dealt by little lavaballs for each ability level (level 3 will deal 45...)
    private constant real FIREBALL_FLIGHT_HEIGHT = 60 // fly high for all lavaballs, looks very well with 60 in my opinion
    
    // please do NOT touch these globals because they're not variables that are made for being configurable rather for improving the performance or provide savety!!
    private constant real SLIDE_PERIOD = 0.035 // interval in which the fireball moves [better don't change because 0.035 is smooth enough]
    private player PLAYER // global player to filter out enemy units because you can't use GetTriggerPlayer/Unit() in filterfunctions that are called in a callback function
    private group CHECK_GROUP // global group that is set in each callback function to ensure an easy filtering of units that are already hit --- similar to PLAYER above
    private group GROUP  = CreateGroup() // a global group to improve performance and perhaps even readability
    private real MaxX // this x-value will prevent the fireball from leaving the map and thus crashing the game [set at MapInit as well]
    private real MinX // " "
    private real MaxY // " same with y "
    private real MinY  // " "
endglobals


// function to determinate the damage caused by the impact of the hugh lavaball added this to
// the top because some guys wanna might deal in damage in a certain way [e.g. with attributes]
private function Damage takes integer level, real RaiseTo returns real  
    return level * BIG_DAMAGE * Pow(0.95,R2I(RaiseTo))
endfunction

private function LittleDamage takes integer level returns real  
    return level * LITTLE_DAMAGE
endfunction

// filterfunction to filter units around the big lavaball that shall be targeted by little lavaballs
// simply add an extra    "if ..... then " and for sure an "endif" afterwards, if you would like to add something 
private function filt takes nothing returns boolean
        local unit u = GetFilterUnit()
        if IsUnitType(u,UNIT_TYPE_STRUCTURE) != true then
            if GetWidgetLife(u) > 0 then
                if IsUnitInGroup(u,CHECK_GROUP) != true then
                    if IsUnitEnemy(u,PLAYER) then
                        set u = null
                        return true
                    endif
                endif
            endif
        endif
        set u = null
        return false
endfunction

// this function filters out the units that shall be damaged on little and big lava ball impacts
// again add an extra    "if ..... then " and for sure an "endif" afterwards...
private function filtdamager takes nothing returns boolean
        local unit u = GetFilterUnit()
        if IsUnitType(u,UNIT_TYPE_STRUCTURE) != true then
            if IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) != true then
                if GetWidgetLife(u) > 0 then
                    if IsUnitEnemy(u,PLAYER) then
                        set u = null
                        return true
                    endif
                endif
            endif
        endif
        set u = null
        return false
endfunction


// ------------> STOP ADAPTING THINGS TO YOUR WISHES FROM HERE ON IF YOU ARE NOT EXPERIENCED ENOUGH!!! <---------


// functin to take care about SetUnitX/Y
private function InMap takes real x, real y returns boolean
    return x >= MinX and x <= MaxX and y >= MinY and y <= MaxY
endfunction

private struct Fireball
    real x
    real y
    real array l_x[MAX_BALLS]     // arrays are used for the little lavaballs the "l_" - prefix means that these arrays are used for the little lavaballs
    real array l_y[MAX_BALLS]
    unit fireball
    unit array ball[MAX_BALLS]
    player owner
    unit caster
    real big_size = BIG_BALL_SIZE
    integer level
    integer ticks
    integer no_more = R2I(BIG_DAMAGE_RADIUS / SPEED) // this is used to prevent little lavaballs from spreading out shortly before the impact, because they were destroyed then anyway...
    integer array l_ticks[MAX_BALLS]
    integer current_balls = 0
    real facing
    real array l_facing[MAX_BALLS]
    group already_hit = CreateGroup()
    
    static method create takes nothing returns Fireball
        local Fireball data = Fireball.allocate()
        local real x
        local real y 
        local location l = GetSpellTargetLoc()
        local real x2 = GetLocationX(l)
        local real y2 = GetLocationY(l)
            set data.caster = GetTriggerUnit()
            set data.x = GetUnitX(data.caster)
            set data.y = GetUnitY(data.caster)
            set x = x2 - data.x
            set y = y2 - data.y 
            set data.owner = GetTriggerPlayer()
            set data.level = GetUnitAbilityLevel(data.caster,GetSpellAbilityId())
            set data.facing = Atan2(y,x)
            set data.fireball = CreateUnit(data.owner,FIREBALL_DUMMY,data.x,data.y,GetUnitFacing(data.caster))
            set data.ticks = R2I(SquareRoot(x * x + y * y) / SPEED)
                call SetUnitFlyHeight(data.fireball,FIREBALL_FLIGHT_HEIGHT,0)
                call SetUnitScale(data.fireball,BIG_BALL_SIZE,BIG_BALL_SIZE,BIG_BALL_SIZE)
                call RemoveLocation(l)
            set l = null
        return data
    endmethod
   
    method onDestroy takes nothing returns nothing
    local integer index = 1
        
        call DestroyGroup(.already_hit)
        call KillUnit(.fireball)
    set .fireball = null
    set .caster = null
       loop
                call KillUnit(.ball[index])
            set .ball[index] = null
            set index = index + 1
            exitwhen index == MAX_BALLS
        endloop  
   endmethod
        
endstruct

private function Conditions takes nothing returns boolean
    return GetSpellAbilityId() == DUMMY_SPELL_ID
endfunction

private function Callback takes nothing returns boolean
local Fireball data = ABCT_GetData()
local unit victim
local real x = data.x + SPEED * Cos(data.facing)
local real y = data.y + SPEED * Sin(data.facing)
local real l_x
local real l_y
local integer index = 0
local unit ball
    set PLAYER = data.owner
    set CHECK_GROUP = data.already_hit
    
        if data.current_balls <= MAX_BALLS then 
        
            if data.ticks >= data.no_more then 
                call GroupEnumUnitsInRange(GROUP,data.x,data.y,RADIUS,Filter(function filt))

                loop
                    set victim = FirstOfGroup(GROUP)
                    set index = data.current_balls
                        exitwhen victim == null // exitwhen victim == null means that the group is empty. That's why I don't use GroupClear()... but does it work without leaking that way??
                        set data.current_balls = data.current_balls + 1
                        set index = index + 1
                        set data.ball[index] = CreateUnit(data.owner,FIREBALL_DUMMY,data.x,data.y,bj_UNIT_FACING)
                        set data.big_size = data.big_size - SIZE_LOSS
                        set l_x = GetUnitX(victim) - data.x
                        set l_y = GetUnitY(victim) - data.y
                        set data.l_x[index] = data.x
                        set data.l_y[index] = data.y
                        set data.l_facing[index] = Atan2(l_y,l_x)                
                        set data.l_ticks[index] = R2I(SquareRoot(l_x *l_x + l_y * l_y) / LITTLE_SPEED)
                            call GroupRemoveUnit(GROUP,victim) // unit removing
                            call GroupAddUnit(data.already_hit,victim)                        
                            call SetUnitFlyHeight(data.ball[index],FIREBALL_FLIGHT_HEIGHT,0)
                            call SetUnitScale(data.ball[index],LITTLE_BALL_SIZE,LITTLE_BALL_SIZE,LITTLE_BALL_SIZE)
                endloop           
            endif 
        
        set index = 1    
        loop
        
            exitwhen index >  data.current_balls
            if GetWidgetLife(data.ball[index]) >= 0.406 then 
                if data.l_ticks[index] >= 1 then 
                    set data.l_x[index] = data.l_x[index] + LITTLE_SPEED * Cos(data.l_facing[index])
                    set data.l_y[index] = data.l_y[index] + LITTLE_SPEED * Sin(data.l_facing[index])
                    if InMap(data.l_x[index],data.l_y[index]) then
                        call SetUnitX(data.ball[index],data.l_x[index])
                        call SetUnitY(data.ball[index],data.l_y[index])
                    endif
                    set data.l_ticks[index] = data.l_ticks[index] - 1
                else 
                        

                    call GroupEnumUnitsInRange(GROUP,data.l_x[index],data.l_y[index],LITTLE_DAMAGE_RADIUS,Filter(function filtdamager))
        
                    loop
                            set victim = FirstOfGroup(GROUP)
                            exitwhen victim == null
                                call GroupRemoveUnit(GROUP,victim)
                                call UnitDamageTarget(data.caster,victim,LittleDamage(data.level),false,false,ATTACK_TYPE,DAMAGE_TYPE,WEAPON_TYPE_WHOKNOWS)
                    endloop
                    call KillUnit(data.ball[index])
                endif 
                endif 
                    set index = index + 1
        endloop
        
                if InMap(x,y) then
                    call SetUnitX(data.fireball,x)
                    call SetUnitY(data.fireball,y)
                endif
                
                call SetUnitScale(data.fireball,data.big_size,data.big_size,data.big_size)
                    set data.x = x
                    set data.y = y
                    set data.ticks = data.ticks - 1
                    
                if data.ticks <= 0 then 
                
                       
                    call GroupEnumUnitsInRange(GROUP,data.x,data.y,BIG_DAMAGE_RADIUS,Filter(function filtdamager))
                                
                    loop
                        set victim = FirstOfGroup(GROUP)
                        exitwhen victim == null
                    
                            call GroupRemoveUnit(GROUP,victim)
                            call UnitDamageTarget(data.caster,victim,Damage(data.level,R2I(data.current_balls)),false,false,ATTACK_TYPE,DAMAGE_TYPE,WEAPON_TYPE_WHOKNOWS)
                    endloop
                    call DestroyEffect(AddSpecialEffect(EXPLODE_PATH,GetUnitX(data.fireball),GetUnitY(data.fireball)))
                    
            set ball = null
                    call data.destroy()
                    
                return true
                
                endif
    
                    set ball = null
                return false
         

        endif 
            call data.destroy()
        return true
endfunction

private function Actions takes nothing returns nothing
local Fireball data = Fireball.create()

    call ABCT_Start(function Callback, data, SLIDE_PERIOD)
endfunction

public function InitTrig takes nothing returns nothing
local trigger trig = CreateTrigger()
 
    set MinX = GetRectMinX(bj_mapInitialPlayableArea) + 100
    set MaxX = GetRectMaxX(bj_mapInitialPlayableArea) - 100
    set MinY = GetRectMinY(bj_mapInitialPlayableArea) + 100
    set MaxY = GetRectMaxY(bj_mapInitialPlayableArea) - 100
        call TriggerRegisterAnyUnitEventBJ(trig,EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddCondition(trig, Condition(function Conditions))
        call TriggerAddAction(trig, function Actions)

endfunction

endscope

Changelog: Replaced GetUnitState(...) > 0 with GetWidgetLife(..) > 0
Remove an unnecessary timer and a global group is now used for all the loop stuff I do

Have fun testing it and please comment and critique me!! Probable this spell can be improved and yet does leak!

Give Credits!

PS: My first spell I submitted and in case my English isn't very well excuse me ^^
 

Attachments

  • Spell Molten Matter.w3x
    60.3 KB · Views: 480

Romek

Super Moderator
Reaction score
963
Didn't read through the entire code, but:
JASS:
private function filt takes nothing returns boolean
        local unit u = GetFilterUnit()
        if IsUnitType(u,UNIT_TYPE_STRUCTURE) != true then
            if GetUnitState(u,UNIT_STATE_LIFE) > 0 then
                if IsUnitInGroup(u,CHECK_GROUP) != true then
                    if IsUnitEnemy(u,PLAYER) then
                        set u = null
                        return true
                    endif
                endif
            endif
        endif
        set u = null
        return false
endfunction


Can be changed to:
JASS:
private function filt takes nothing returns boolean
        local unit u = GetFilterUnit()
        return IsUnitType(u,UNIT_TYPE_STRUCTURE) != true and GetUnitState(u,UNIT_STATE_LIFE) > 0 and IsUnitInGroup(u,CHECK_GROUP) != true and IsUnitEnemy(u,PLAYER)
       // set u = null << you won't be able to null because of the return.
endfunction


And

JASS:
private function filtdamager takes nothing returns boolean
        local unit u = GetFilterUnit()
        if IsUnitType(u,UNIT_TYPE_STRUCTURE) != true then
            if IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) != true then
                if GetUnitState(u,UNIT_STATE_LIFE) > 0 then
                    if IsUnitEnemy(u,PLAYER) then
                        set u = null
                        return true
                    endif
                endif
            endif
        endif
        set u = null
        return false
endfunction


Can be changed to:
JASS:
private function filtdamager takes nothing returns boolean
        local unit u = GetFilterUnit()
        return IsUnitType(u,UNIT_TYPE_STRUCTURE) != true and IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) != true and GetUnitState(u,UNIT_STATE_LIFE) > 0 and IsUnitEnemy(u,PLAYER)
        //set u = null << Same as the first function. Can't null because of return.
endfunction


Edit: The Nulling would be pointless though, so if you want to null the unit, you would need a simple If. Not a massive one like you had :p

You could do:
JASS:
private function filtdamager takes nothing returns boolean
        local unit u = GetFilterUnit()
        if IsUnitType(u,UNIT_TYPE_STRUCTURE) != true and IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) != true and GetUnitState(u,UNIT_STATE_LIFE) > 0 and IsUnitEnemy(u,PLAYER) then
             set u = null
             return true
        endif 
        set u = null
        return false
endfunction
 

PurgeandFire

zxcvmkgdfg
Reaction score
509
Great job on the coding and neat spell.

In Romek's example, you need to null, so you can probably just use a bj global or use filter unit.

JASS:
    private constant integer MAX_BALLS = 15 // determinates the amount of balls that can spread out of the big one, should NOT be set too high because the struct limit might struggle then


This just made my day. :p
 

Kenny

Back for now.
Reaction score
202
This spell is pretty cool... good work with the coding too, looks pretty clean. The only thing that i could point out is the filter functions you are using, but that has already been addressed by Romek. One more thing though, wouldn't it be better to use GetWidgetLife(GetFilterUnit())>.405 in your filters/functions rather than GetUnitState(GetFilterUnit(),UNIT_STATE_LIFE)>0? I remember reading and being told that GetWidget... is better for efficiency etc..
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
This spell is pretty cool... good work with the coding too, looks pretty clean. The only thing that i could point out is the filter functions you are using, but that has already been addressed by Romek. One more thing though, wouldn't it be better to use GetWidgetLife(GetFilterUnit())>.405 in your filters/functions rather than GetUnitState(GetFilterUnit(),UNIT_STATE_LIFE)>0? I remember reading and being told that GetWidget... is better for efficiency etc..

Thanks and for the GetWidgetLife thing... I didn't really know about it. I went over the bj way to find the right function and hence used GetUnitState. Will change that azap thanks :)

And thanks to Romek and PurgeandFire as well for feedback,
but I will NOT put my filters into one line. I know most people prefer having a very, very long line instead of a code block, but I'm not one of them. When I look at the filter block I can clearly see every condition at once while when inlining I had to scroll to the right side which is the worst thing on earth!! (isn't it? ;) )
And there isn't any speed difference between if ... then and "and"s anyway I think.

JASS:
private constant integer MAX_BALLS = 15 // determinates the amount of
balls that can spread out of the big one, should NOT be set too high because the struct limit might struggle then


This just made my day.

Hmm you're right lol :D
 

trb92

Throwing science at the wall to see what sticks
Reaction score
142
JASS:
integer no_more = R2I(BIG_DAMAGE_RADIUS / SPEED) // this is used to pretend little lavaballs from spreading out shortly before the impact, because they were destroyed then anyway...

...Used to pretend? Should this be prevent?

JASS:
private function Actions takes nothing returns nothing
local Fireball data = Fireball.create()
local timer t = CreateTimer()
call ABCT_Start(function Callback, data, SLIDE_PERIOD)
    set t = null
endfunction

You make and null the timer, why? It's not used in this function...

Overall, this is a very cool spell.
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
Yes it should be prevent and I really don't need the timer. I made it like this because cohadar did is as well in his ABCT test map... so when I was learning I overtook it and thought that ABCT attaches the struct to the timer (however :p). But I tested and it works without. Thanks.

Edit: I also now use a global group for my GroupEnum calls because creating and destroying the group is then unnecessary. I'm yet not sure whether it leaks the way it is... but my question wasn't yet answered in the JASS thread.
 

Kenny

Back for now.
Reaction score
202
For your global group, could you not just use:
JASS:
private group GROUP = CreateGroup()

instead of leaving it as nothing, then setting it to a struct variable?
That way there would be no need for the struct group:
JASS:
group already_hit = CreateGroup()

I'm pretty sure that it would still work the same. I'm not sure, i may be wrong here.

Other then that though, this spell seems very nice. Good Work :)
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
First of all you can NOT use function calls to initially set globals to a value. That's why do that at trigger initialization... same for the map border values as you can see.

And I don't know what you mean with my struct group "already_hit". I create it once and it is used for the MUI.
Every unit can be hit by little lavaballs from DIFFERENT greater lavaballs, at least once.
 

Kenny

Back for now.
Reaction score
202
Haha, my bad, didn't really read your InitTrig properly :p

Anywho, just out of curiosity, why can you not use CreateGroup() to set the global group? As i have seen it done in numerous MUI spells in the past...

Once again, Great spell :)
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
I've enjoyed the screeny, gj

Lol and thanks.

Anywho, just out of curiosity, why can you not use CreateGroup() to set the global group? As i have seen it done in numerous MUI spells in the past...

Hmmm seems like you're right. I was sure sure that I've been reading something about being unable to use function calls for instant global declaring. And it didn't work for MaxX = GetRectMaxX(bj_mapInitialPlayableArea). But looks like create group can be used (perhaps because you don't need to give it arguments??). Thanks for that, gonna fix it.

Once again, Great spell

Double thanks^^.
 

GoGo-Boy

You can change this now in User CP
Reaction score
40
Ahh finally thanks! I'm working on my structure... next one will hopefully be cleaner :s
 
General chit-chat
Help Users
  • No one is chatting at the moment.

      The Helper Discord

      Members online

      No members online now.

      Affiliates

      Hive Workshop NUON Dome World Editor Tutorials

      Network Sponsors

      Apex Steel Pipe - Buys and sells Steel Pipe.
      Top