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

Forest Soul v1.06

Forest Soul v1.06

This is the first JASS submission, uploaded it to get constructive feedback to make my coding better. All the information can be found in the code.

Requires


• Dummy.mdx by Vexorian/Anitarf/Infrane

Code

Phase 1

Phase 2

Phase 3

Pushback

GIF

JASS:
////////////////////////////////////////////////////////////
// Forest Soul v1.06 by Meatmuffin
// Conjures the very essence of the forest and sends it
// to the target enemy. Once it reaches the target, it will
// circle around it, taking its life force and sending it
// to the caster. After that, returns to the caster and
// repeats the same action. Meanwhile the forest spirits
// guard the caster, pushing away enemies based on the time
// the soul was circling.
//
// The functions below are meant for configuration only.
// Anything below the configuration can be changed,
// although it does take some programming knowledge to do so.
//
////////////// HOW TO IMPORT ////////////////
// 1. Go to File -> Preferences and tick the box that says:
// "Automatically create unknown variables when pasting trigger
// data"
//
// 2. Copy the dummy unit (if you don't have one) and the
// ability and match their ID's with the configuration.
//
// 3. Copy the "Forest Soul" category into your map along with
// the variable creator. Variable creator creates variables for
// you so you don't have to manually create them yourself.
//
// 4. Configure the spell as it fits best for your map.
//
// 5. Enjoy!
//
////////////// CONFIGURATION ////////////////
//
//
// The looping speed, this indicates the time between
// effects of the spell, how many times per second does
// it loop. 0.031250000 is recommended because it is 
// exactly 32 times per second. Other values should fit
// in between 0.03 and 0.04. Outside those, it becomes
// inefficient.
constant function FS_TimerSpeed takes nothing returns real
    return 0.031250000
endfunction

// The Dummy ID, this indicates what type of dummy we are
// going to use in the spell. Current dummy is using an
// imported model which was made by Vexorian/Anitarf/Infrane,
// import this if you don't have it already in your map. Make
// sure to match the ID of the dummy with the ID below.
constant function FS_DummyID takes nothing returns integer
    return 'h000'
endfunction

// The ability ID, this indicates what ability this code is
// based on. Once again, make sure to match this ID with
// the one your ability has.
constant function FS_AbilityID takes nothing returns integer
    return 'A000'
endfunction

// The dummy player, this is for what player we are creating the
// dummy, it is currently set to "Neutral Passive" so that it
// wouldn't mess up any statistics ingame. This is okay because
// we damage target directly with the caster.
constant function FS_DummyPlayer takes nothing returns player
    return Player(14)
endfunction

// The channel ID that is used when making comparisons if the
// caster is still chanelling.
constant function FS_ChannelID takes nothing returns integer
    return 852600
endfunction

// The initial duration, change this to your wanted initial
// duration.
constant function FS_InitialDuration takes nothing returns real
    return 5.00
endfunction

// Duration per level, change this to how much seconds you want
// to add to the duration per level.
constant function FS_DurationPerLevel takes nothing returns real
    return 5.00
endfunction

// Initial damage per second, change this to your wanted initial
// damage per second.
constant function FS_DPSInitial takes nothing returns real
    return 0.00 * FS_TimerSpeed()
endfunction

// Damage per second per level, change this to how much damage
// you want to add to the damage per level.
constant function FS_DPSPerLevel takes nothing returns real
    return 25.00 * FS_TimerSpeed()
endfunction

// The damage type, what type of damage you want to use against
// your enemy.
constant function FS_DamageType takes nothing returns damagetype
    return DAMAGE_TYPE_MAGIC
endfunction

// The attack type, what type of attack you want to use against
// your enemy.
constant function FS_AttackType takes nothing returns attacktype
    return ATTACK_TYPE_NORMAL
endfunction

// The weapon type, what kind of weapon(?) you want to use against
// your enemy.
constant function FS_WeaponType takes nothing returns weapontype
    return WEAPON_TYPE_WHOKNOWS
endfunction

// This indicates the initial speed of the soul in the first phase.
// The first phase is when the soul is being sent to the enemy.
constant function FS_FirstPhaseInitialSpeed takes nothing returns real
    return 200.00 * FS_TimerSpeed()
endfunction

// This indicates the speed per level of the soul in the first phase.
constant function FS_FirstPhaseSpeedPerLevel takes nothing returns real
    return 100.00 * FS_TimerSpeed()
endfunction

// This indicates the max height you want your soul to reach in the
// first phase. Note that this doesn't actually change the speed, just
// the aesthetic.
constant function FS_ParabolaHeight takes nothing returns real
    return 300.00
endfunction

// This indicates the initial duration of the second phase. The second
// phase is when the soul is circuling around the target, draining life.
constant function FS_SecondPhaseInitialDuration takes nothing returns real
    return 0.00
endfunction

// This indicates the duration per level of the second phase.
constant function FS_SecondPhaseDurationPerLevel takes nothing returns real
    return 2.00
endfunction

// This indicates how fast is the soul circling around the target in the
// second phase. 360.00 - it makes a full circle in 1 second.
constant function FS_SecondPhaseAngleCircleRate takes nothing returns real
    return 180.00 * FS_TimerSpeed()
endfunction

// This indicates how much height are we going to add in addition to the
// flying height of the target. At the calculated value, the missile
// hovers (phase 2).
constant function FS_SecondPhaseSoulHeight takes nothing returns real
    return 50.00
endfunction

// When the soul transitions into phase 2, it has to drop(jump) to the
// height indicated above. This indicates that drop/jump rate. Default
// is set to 1 second (rate and height both match).
constant function FS_SecondPhaseTransitionRate takes nothing returns real
    return FS_SecondPhaseSoulHeight() * 1.00
endfunction

// This indicates the distance between the soul and the target, it is also
// when the soul will transition from the first to the second phase.
constant function FS_CirclingRadius takes nothing returns real
    return 200.00
endfunction

// This indicates the inital speed of the soul in the third phase. The
// third phase is when the soul is going back to the caster.
constant function FS_ThirdPhaseInitialSpeed takes nothing returns real
    return 300.00 * FS_TimerSpeed()
endfunction

// This indicates the speed per level of the soul in the third phase.
constant function FS_ThirdPhaseSpeedPerLevel takes nothing returns real
    return 175.00 * FS_TimerSpeed()
endfunction

// This indicates how small does the distance between the soul and the
// caster has to be in the third phase to transition into the first one.
constant function FS_ThirdPhaseReturnRadius takes nothing returns real
    return 75.00
endfunction

// This indicates the model of the soul. It isn't set in the object
// editor because "Dummy.mdx" takes that spot. We just attach the 
// effect which is much more simple.
constant function FS_MissileModel takes nothing returns string
    return "Abilities\\Weapons\\IllidanMissile\\IllidanMissile.mdl"
endfunction

// This indicates the size of the soul. 1.00 - normal size.
constant function FS_MissileSize takes nothing returns real
    return 2.00
endfunction

// This indicates the attachment point that we are going to use for our
// selected special effect for the soul.
constant function FS_MissileAttachmentPoint takes nothing returns string
    return "origin"
endfunction

// This indicates the attachment point that we are going to use for our
// selected special effect for the indicator.
constant function FS_IndicatorAttachmentPoint takes nothing returns string
    return "origin"
endfunction

// This indicates the primary protection AoE around the caster. Units
// that come within the protection AoE are being pushed back.
constant function FS_PrimaryProtectionAoe takes nothing returns real
    return 200.00
endfunction

// This indicates how much of the duration is converted to protection
// AoE when the soul gets back to the caster. The value entered must
// match how much you want it to be converted over 1 second.
constant function FS_DurationToProtectionAoe takes nothing returns real
    return 75.00
endfunction

// This indicates the initial pushback per second. The pushback happens
// throughout the spell, it pushes back enemies that come close to the
// caster.
constant function FS_InitialPushbackPerSecond takes nothing returns real
    return 0.00
endfunction

// This indicates the pushback per second per level.
constant function FS_PushbackPerSecondPerLevel takes nothing returns real
    return 100.00 * FS_TimerSpeed()
endfunction

// This indicates the model that is going to represent our pushback boundries.
constant function FS_PushbackIndicatorModel takes nothing returns string
    return "Abilities\\Weapons\\GreenDragonMissile\\GreenDragonMissile.mdl"
endfunction

// This indicates the size of the pushback model.
constant function FS_PushbackIndicatorSize takes nothing returns real
    return 0.25
endfunction

// This indicates how fast is the indicator turning. 360 - it makes a full
// circle over 1 second.
constant function FS_PushbackIndicatorTurnRate takes nothing returns real
    return 720.00 * FS_TimerSpeed()
endfunction

// This indicates the draining special effect. Both the caster and the target
// have this effect.
constant function FS_DrainingEffect takes nothing returns string
    return "Objects\\Spawnmodels\\NightElf\\EntBirthTarget\\EntBirthTarget.mdl"
endfunction

// This indicates at which attachment point does the draining effect happen.
// "origin" happens at the feet of the affected unit.
constant function FS_DrainingEffectLoc takes nothing returns string
    return "origin"
endfunction

// This indicates how fast is the effect made. I included this because it
// can become quite spammy. It is currently set to each 5 times the loop fires.
constant function FS_EffectInterval takes nothing returns real
    return FS_TimerSpeed() * 5.00
endfunction

// This indicates how fast is the effect made when pushbacking the enemies.
// This was implemented for the save reason as above.
constant function FS_PushbackEffectInterval takes nothing returns real
    return FS_TimerSpeed() * 5.00
endfunction

// This indicates the effect that is used on the units that are being
// pushed back.
constant function FS_PushbackEffect takes nothing returns string
    return "Abilities\\Weapons\\GreenDragonMissile\\GreenDragonMissile.mdl"
endfunction

// This indicates at which attachment point does the pushback effect happen.
// Once again, "origin" happens at the feet of the affected unit.
constant function FS_PushbackEffectLoc takes nothing returns string
    return "origin"
endfunction

// This indicates how high is the pushback boundry indicator hovering.
// It is recommended not to set it too high because it can screw up the
// aesthetic.
constant function FS_IndicatorHeight takes nothing returns real
    return 50.00
endfunction

// This indicates the integer ID that we are going to use to apply 0.01
// timed life to units that no longer have a use.
constant function FS_DeathID takes nothing returns integer
    return 'BTLF'
endfunction

// This indicates the breaking range of the spell. If the target moves
// out of this rangem the spell breaks. It can only break on phase 3.
constant function FS_MaxDistance takes nothing returns real
    return 1500.00
endfunction

// This allows you to switch between if you want the spell to break when
// regarding distance or not.
constant function FS_EnableMaxDistance takes nothing returns boolean
    return false
endfunction

////////////////////////// END OF CONFIGURATION /////////////////////////


// Pushback target filter. If a unit passes all the conditions, it gets
// pushed back.
function FS_TargetFilter takes unit u, player pl returns boolean
    return not(IsUnitType(u, UNIT_TYPE_DEAD) == true) and not(IsUnitType(u, UNIT_TYPE_STRUCTURE) == true) and IsUnitEnemy(u, pl) == true
endfunction


// Function to check if the unit is already channeling.
// This is used to prevent it from affecting multiple units at once.
function FS_CheckChannel takes unit u returns nothing
    local integer Node = 0
    loop
        set Node = udg_FS_NextNode[Node]
        exitwhen (udg_FS_Caster[Node] == u) or (Node == 0)
    endloop
    set udg_FS_Duration[Node] = 0.00
endfunction


// Function used to get the terrain height at the used point.
function FS_GetLocZ takes real x, real y returns real
    call MoveLocation(udg_FS_Loc, x, y)
    return GetLocationZ(udg_FS_Loc)
endfunction


// The main function of the spell (loop). Each (timer speed) seconds
// this function runs.
function FS_Loop takes nothing returns nothing
    // Setting up locals
    local real x
    local real y
    local real z
    local real x2
    local real y2
    local real z2
    local real dx
    local real dy
    local real totalD
    local real angle
    local integer Node = 0
    local unit u

    // Initializing the loop.
    loop
        // Cycling through each node.
        set Node = udg_FS_NextNode[Node]
        exitwhen Node == 0
       
       // Subtracting the duration by the timer speed.
        set udg_FS_Duration[Node] = udg_FS_Duration[Node] - FS_TimerSpeed()
        // Checking if all the arguments are met (Both the target and the caster are alive,
        // current order is channel (852600) and the duration hasn't expired yet.)
        if udg_FS_Duration[Node] > 0.00 and GetUnitCurrentOrder(udg_FS_Caster[Node]) == FS_ChannelID() and not IsUnitType(udg_FS_Caster[Node], UNIT_TYPE_DEAD) and GetUnitTypeId(udg_FS_Caster[Node]) != 0 and not IsUnitType(udg_FS_Target[Node], UNIT_TYPE_DEAD) and GetUnitTypeId(udg_FS_Target[Node]) != 0 then
            
            // Setting up all the coordinates
            set x = GetUnitX(udg_FS_Caster[Node])
            set y = GetUnitY(udg_FS_Caster[Node])
            set x2 = GetUnitX(udg_FS_Target[Node])
            set y2 = GetUnitY(udg_FS_Target[Node])
            // Rotating the indicator to make it look more appealing.
            set udg_FS_Angle[Node] = udg_FS_Angle[Node] - FS_PushbackIndicatorTurnRate() * bj_DEGTORAD
            call SetUnitX(udg_FS_Indicator[Node], x + udg_FS_IndicatorAoE[Node] * Cos(udg_FS_Angle[Node]))
            call SetUnitY(udg_FS_Indicator[Node], y + udg_FS_IndicatorAoE[Node] * Sin(udg_FS_Angle[Node]))
            
            // Calculating the distances. We do not need to calculate them in the second phase
            // so we use the "or" function to check if the current phase is either 1 or 3.
            if udg_FS_CurrentPhase[Node] == 1 or udg_FS_CurrentPhase[Node] == 3 then
                set z = FS_GetLocZ(x, y) + GetUnitFlyHeight(udg_FS_Caster[Node])
                set z2 = FS_GetLocZ(x2, y2) + GetUnitFlyHeight(udg_FS_Target[Node])
                set dx = x2 - x
                set dy = y2 - y
                set totalD = SquareRoot(dx * dx + dy * dy)
                
                // Adding up to the partial distance.
                if udg_FS_CurrentPhase[Node] == 1 then
                    set udg_FS_PartialD[Node] = udg_FS_PartialD[Node] + udg_FS_FirstPhaseSpeed[Node]
                else
                    set udg_FS_PartialD[Node] = udg_FS_PartialD[Node] + udg_FS_ThirdPhaseSpeed[Node]
                endif
            
            // Current phase is 2, execute the second phase.
            else
                // Setting up the angle and rotating the soul accordingly.
                set udg_FS_SoulAngle[Node] = udg_FS_SoulAngle[Node] + FS_SecondPhaseAngleCircleRate() * bj_DEGTORAD
                call SetUnitX(udg_FS_Soul[Node], x2 + FS_CirclingRadius() * Cos(udg_FS_SoulAngle[Node]))
                call SetUnitY(udg_FS_Soul[Node], y2 + FS_CirclingRadius() * Sin(udg_FS_SoulAngle[Node]))
                // Damaging the target and healing the caster.
                call UnitDamageTarget(udg_FS_Caster[Node], udg_FS_Target[Node], udg_FS_Damage[Node], true, false, FS_AttackType(), FS_DamageType(), FS_WeaponType())
                call SetWidgetLife(udg_FS_Caster[Node], GetWidgetLife(udg_FS_Caster[Node]) + udg_FS_Damage[Node])
                
                // Counting down the duration and using the effect at the end to make it less spammy.
                set udg_FS_EffectDuration[Node] = udg_FS_EffectDuration[Node] - FS_TimerSpeed()
                if udg_FS_EffectDuration[Node] < 0.00 then
                    call DestroyEffect(AddSpecialEffectTarget(FS_DrainingEffect(), udg_FS_Target[Node], FS_DrainingEffectLoc()))
                    call DestroyEffect(AddSpecialEffectTarget(FS_DrainingEffect(), udg_FS_Caster[Node], FS_DrainingEffectLoc()))
                    set udg_FS_EffectDuration[Node] = FS_EffectInterval()
                endif
                
                // Counting down the second phase duration.
                set udg_FS_SecondPhase[Node] = udg_FS_SecondPhase[Node] - FS_TimerSpeed()
                if udg_FS_SecondPhase[Node] < 0.00 then
                    // Second phase has ended, preparing for phase 3.
                    set udg_FS_SecondPhase[Node] = FS_SecondPhaseInitialDuration() + FS_SecondPhaseDurationPerLevel() * udg_FS_AbilityLevel[Node]
                    set udg_FS_CurrentPhase[Node] = 3
                    set udg_FS_PartialD[Node] = FS_CirclingRadius()
                endif
            
            endif
            // Curent phase is 1, execute the first phase.
            if udg_FS_CurrentPhase[Node] == 1 then
                // Setting up the angle and moving the soul accordingly.
                set udg_FS_SoulAngle[Node] = Atan2(y2 - y, x2 - x)
                call SetUnitX(udg_FS_Soul[Node], x + udg_FS_PartialD[Node] * Cos(udg_FS_SoulAngle[Node]))
                call SetUnitY(udg_FS_Soul[Node], y + udg_FS_PartialD[Node] * Sin(udg_FS_SoulAngle[Node]))
                
                // Calculating the parabola and using it to make it look like the soul is being actually thrown.
                call SetUnitFlyHeight(udg_FS_Soul[Node], 4.00 * FS_ParabolaHeight() / totalD * (totalD - udg_FS_PartialD[Node]) * (udg_FS_PartialD[Node] / totalD) + udg_FS_PartialD[Node] * (z2 - z) / (totalD + 1.00) + z, 0.00)
                
                // Checking if the soul has reached the target.
                if totalD - udg_FS_PartialD[Node] < FS_CirclingRadius() then
                    // The soul has reached the target, transition into phase 2.
                    call SetUnitFlyHeight(udg_FS_Soul[Node], GetUnitFlyHeight(udg_FS_Target[Node]) + FS_SecondPhaseSoulHeight(), FS_SecondPhaseTransitionRate())
                    set udg_FS_PartialD[Node] = 0.00
                    set udg_FS_CurrentPhase[Node] = 2
                    set udg_FS_SoulAngle[Node] = Atan2(y - y2, x - x2)
                endif
          
            // Current phase is 3, execute the third phase.
            elseif udg_FS_CurrentPhase[Node] == 3 then 
                
                // Gradually setting the height to that of the caster's. 
                if z2 < z then
                    call SetUnitFlyHeight(udg_FS_Soul[Node], ((totalD - udg_FS_PartialD[Node]) * (z2 - z + 1.00) / totalD) + z, 0.00)
                else
                    call SetUnitFlyHeight(udg_FS_Soul[Node], ((totalD - udg_FS_PartialD[Node]) * (z2 + 1.00) / totalD) + z + 50.00, 0.00)
                endif
                
                // Setting up the angle and moving the soul accordingly.
                set udg_FS_SoulAngle[Node] = Atan2(y - y2, x - x2)
                call SetUnitX(udg_FS_Soul[Node], x2 + udg_FS_PartialD[Node] * Cos(udg_FS_SoulAngle[Node]))
                call SetUnitY(udg_FS_Soul[Node], y2 + udg_FS_PartialD[Node] * Sin(udg_FS_SoulAngle[Node]))
                
                // Checking if the soul has reached the caster.
                if totalD - udg_FS_PartialD[Node] < FS_ThirdPhaseReturnRadius() then
                    // The soul has reached the target, increase the pushback AoE and transition back to phase 1.
                    set udg_FS_IndicatorAoE[Node] = udg_FS_IndicatorAoE[Node] + udg_FS_SecondPhase[Node] * FS_DurationToProtectionAoe()
                    set udg_FS_CurrentPhase[Node] = 1
                    set udg_FS_PartialD[Node] = 0.00
                    // Checking if the max distance has been exceeded. If yes, break the spell.
                    if totalD > FS_MaxDistance() and FS_EnableMaxDistance() == true then
                        set udg_FS_Duration[Node] = 0.00
                    endif
                endif
            endif
            
            // Reducing the duration for the pushback special effect.
            set udg_FS_SpecialEfDuration[Node] = udg_FS_SpecialEfDuration[Node] - FS_TimerSpeed()
            
            // Enumerating all units that come within AoE of the pushback.
            call GroupEnumUnitsInRange(udg_FS_TempGroup, x, y, udg_FS_IndicatorAoE[Node], null)
            loop
                // Using the FirstOfGroup method
                set u = FirstOfGroup(udg_FS_TempGroup)
                exitwhen u == null
                // Checking if the target is not dead, not a structure and is an enemy.
                if FS_TargetFilter(u, udg_FS_Player[Node]) then
                    // Arguments are passed, setting up the angle and pushing back enemies.
                    set x2 = GetUnitX(u)
                    set y2 = GetUnitY(u)
                    set angle = Atan2(y2 - y, x2 - x)
                    call SetUnitX(u, x2 + udg_FS_Pushback[Node] * Cos(angle))
                    call SetUnitY(u, y2 + udg_FS_Pushback[Node] * Sin(angle))
                    if udg_FS_SpecialEfDuration[Node] < 0.00 then
                        call DestroyEffect(AddSpecialEffectTarget(FS_PushbackEffect(), u, FS_PushbackEffectLoc()))
                    endif
                endif
                call GroupRemoveUnit(udg_FS_TempGroup, u)
             endloop
             
             if udg_FS_SpecialEfDuration[Node] < 0.00 then
                set udg_FS_SpecialEfDuration[Node] = FS_PushbackEffectInterval()
             endif
        else
            
            // The spell has ended, clean up the effects.
            if udg_FS_Duration[Node] > 0.00 then
                call IssueImmediateOrder(udg_FS_Caster[Node], "stop")
            endif
            call DestroyEffect(udg_FS_SpecialEf[Node])
            call UnitApplyTimedLife(udg_FS_Soul[Node], FS_DeathID(), 0.01)
            call UnitApplyTimedLife(udg_FS_Indicator[Node], FS_DeathID(), 0.01)
            call DestroyEffect(udg_FS_IndicatorEf[Node])
            
            // Recycling the node.
            set udg_FS_RecycleNodes[udg_FS_RecyclableNodes] = Node
            set udg_FS_RecyclableNodes = udg_FS_RecyclableNodes + 1
            set udg_FS_NextNode[udg_FS_PrevNode[Node]] = udg_FS_NextNode[Node]
            set udg_FS_PrevNode[udg_FS_NextNode[Node]] = udg_FS_PrevNode[Node]
            // Checking if this was the last instance running. If yes, pauses the timer.
            if (udg_FS_NextNode[0] == 0) then
                call PauseTimer(udg_FS_Timer)
            endif
        endif
    endloop
endfunction


// Function that is run on cast.
function FS_OnCast takes nothing returns boolean
    // Setting up the locals
    local real x
    local real y
    local integer Node
    local real angle
    local unit u
    // Checking if the casted ability was this one.
    if (GetSpellAbilityId() == FS_AbilityID()) then
        
        // Calling the function that checks if the unit is currently chanelling
        // an instance.
        set u = GetTriggerUnit()
        call FS_CheckChannel(u)
        
        // Setting up the node.
        if (udg_FS_RecyclableNodes == 0) then
            set udg_FS_NodeNumber = udg_FS_NodeNumber + 1
            set Node = udg_FS_NodeNumber
        else
            set udg_FS_RecyclableNodes = udg_FS_RecyclableNodes - 1
            set Node = udg_FS_RecycleNodes[udg_FS_RecyclableNodes]
        endif
        set udg_FS_NextNode[Node] = 0
        set udg_FS_NextNode[udg_FS_PrevNode[0]] = Node
        set udg_FS_PrevNode[Node] = udg_FS_PrevNode[0]
        set udg_FS_PrevNode[0] = Node
        // Initializing all necessary values for the spell.
        set udg_FS_Caster[Node] = u
        set udg_FS_Player[Node] = GetTriggerPlayer()
        set udg_FS_AbilityLevel[Node] = GetUnitAbilityLevel(udg_FS_Caster[Node], FS_AbilityID())
        set udg_FS_SpecialEfDuration[Node] = FS_PushbackEffectInterval()
        set udg_FS_Duration[Node] = FS_InitialDuration() + FS_DurationPerLevel() * udg_FS_AbilityLevel[Node]
        set udg_FS_Damage[Node] = FS_DPSInitial() + FS_DPSPerLevel() * udg_FS_AbilityLevel[Node]
        set udg_FS_FirstPhaseSpeed[Node] = FS_FirstPhaseInitialSpeed() + FS_FirstPhaseSpeedPerLevel() * udg_FS_AbilityLevel[Node]
        set udg_FS_ThirdPhaseSpeed[Node] = FS_ThirdPhaseInitialSpeed() + FS_ThirdPhaseSpeedPerLevel() * udg_FS_AbilityLevel[Node]
        set udg_FS_Pushback[Node] = FS_InitialPushbackPerSecond() + FS_PushbackPerSecondPerLevel() * udg_FS_AbilityLevel[Node]
        set udg_FS_CurrentPhase[Node] = 1
        set udg_FS_SoulAngle[Node] = 0.00
        set udg_FS_PartialD[Node] = 0.00
        set udg_FS_EffectDuration[Node] = FS_EffectInterval()
        set udg_FS_SecondPhase[Node] = FS_SecondPhaseInitialDuration() + FS_SecondPhaseDurationPerLevel() * udg_FS_AbilityLevel[Node]
        set udg_FS_Target[Node] = GetSpellTargetUnit()
        set x = GetUnitX(udg_FS_Caster[Node])
        set y = GetUnitY(udg_FS_Caster[Node])
        set udg_FS_Soul[Node] = CreateUnit(FS_DummyPlayer(), FS_DummyID(), x, y, GetRandomReal(0, 360))
        set udg_FS_SpecialEf[Node] = AddSpecialEffectTarget(FS_MissileModel(), udg_FS_Soul[Node], FS_MissileAttachmentPoint())
        call SetUnitScale(udg_FS_Soul[Node], FS_MissileSize(), 0.00, 0.00)
        call UnitRemoveAbility(udg_FS_Soul[Node], 'Amov')
        set udg_FS_IndicatorAoE[Node] = FS_PrimaryProtectionAoe()
        
        // Creating the indicator of the pushback AoE.
        set udg_FS_Angle[Node] = GetRandomReal(0, 360) * bj_DEGTORAD
        set x = GetUnitX(udg_FS_Caster[Node])
        set y = GetUnitY(udg_FS_Caster[Node])
        set x = x + udg_FS_IndicatorAoE[Node] * Cos(udg_FS_Angle[Node])
        set y = y + udg_FS_IndicatorAoE[Node] * Sin(udg_FS_Angle[Node])
        set udg_FS_Indicator[Node] = CreateUnit(FS_DummyPlayer(), FS_DummyID(), x, y, udg_FS_Angle[Node])
        set udg_FS_IndicatorEf[Node] = AddSpecialEffectTarget(FS_PushbackIndicatorModel(), udg_FS_Indicator[Node], FS_IndicatorAttachmentPoint())
        call SetUnitFlyHeight(udg_FS_Indicator[Node], FS_IndicatorHeight(), 0.00)
        
        // Checking if this instance was the first one. If yes, starts the timer.
        if (udg_FS_PrevNode[Node] == 0) then
            call TimerStart(udg_FS_Timer, FS_TimerSpeed(), true, function FS_Loop)
        endif
        set u = null
    endif
    return false
endfunction


// Initialization trigger
function InitTrig_Forest_Soul takes nothing returns nothing
    local trigger FS = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(FS, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddCondition(FS, Condition(function FS_OnCast))
    
    set udg_FS_Loc = Location(0, 0)
endfunction

/////////////////////// END OF THE SPELL //////////////////////
In the phase 1 of the spell, the caster is sending out a spirit in a
parabola graph. I set the default speed to quite low for phase recognition. This phase doesn't do
that much, it's only a followup to the damaging/healing phase (2).
In the phase 2 of the spell, the spirit is leeching life from the target
and sending it instantly to the caster while rotating around the target for aesthetic reasons.
The caster will still get healed if the target takes low, or no damage at all.
In the phase 3 of the spell, the spirit returns to the caster
(I set the default speed to high, since it's basically a transition to phase 1 again) and increases
the AoE of the pushback radius.
Throughout all of the phases, there is a spirit hovering the caster at a set radius.
The spirit indicates the pushback AoE. Any enemy that comes close to the target will get slightly
pushed away. I implemented this because the caster is a little fragile while casting this. If anyone
requests, I will add an option whether it should be enabled or not.
giphy.gif


Changelog


v1.00 - Initial release
v1.01 - Optimized the code based on BPower and KILLCIDE reviews.
v1.02 - Removed a leftover, parabola is a little smoother now (?)
v1.03 - Added an option to break the spell using max distance.
v1.04 - Implemented a seperate parabola distance calculation to make it look completely smooth now;
Added functions: 1. Target filter for easier filtering, 2. A check whether the caster is already channeling or not.
v1.05 - Updated the parabola to be proper with different starting/end heights. Gonna update to 1.05b later today if the circumstances are in my favor.
v1.05b - Improved compatability when the caster's height is heigher than the target's.
v1.05c - Fixed some visual/testing issues and fixed the bug after the discussion in chat with T-C.
v1.06 - Radically optimised the code, cutting down the distance calculations, locals count and resource code lines in general. Credits to T-C again.
v1.06 again - GIF preview.

Note:
I will probably have to use world bounds and not move the indicator if it is out of map bounds.

Feedback is very welcome.

Keywords:
Forest Soul, Forest, Soul, Nature, Green, Chanelling, Phases, Meatmuffin, Healing, Draining, Damaging, Long, kek
Contents

Forest Soul v1.06 (Map)

Reviews
22:06, 4th Apr 2016 Tank-Commander: v1.06 - A complex and beautiful ability, code looks good is well documented and structured; well deserving of a 5/5 22:24, 3rd Apr 2016 Tank-Commander: v1.05b - Very good looking but presently there's a large...

Moderator

M

Moderator

22:06, 4th Apr 2016
Tank-Commander: v1.06 - A complex and beautiful ability, code looks good is well documented and structured; well deserving of a 5/5


22:24, 3rd Apr 2016
Tank-Commander: v1.05b - Very good looking but presently there's a large bug in the program. See my post
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
First impression:

Pretty cool idea.

For me FPS drop rapidly if many units are affected. ( Casted in the north-east bear group )
Could be the fx model or something within the code.
I would guess it's this aura a-like fx for close affected units.

From set z = 4.00 * FS_ParabolaHeight() * partialD * (totalD - partialD) / (totalD * totalD)
(totalD * totalD)--> Make sure this can't be 0, otherwise the thread will crash due to (x / 0) division.

set udg_FS_AbilityLevel[Node] = GetUnitAbilityLevel(udg_FS_Caster[Node], 'A000')
^'A000' should be replaced.

GetUnitCurrentOrder(udg_FS_Caster[Node]) == 852600
^I would appreciate if you place the order id into a constant function.

if not (IsUnitType(udg_FS_Caster[Node], UNIT_TYPE_DEAD) == true) then
--> if not IsUnitType(udg_FS_Caster[Node], UNIT_TYPE_DEAD) then
^The == true comparison is redundant.
It's a classic issue of improving close to zero performance, but it looks cleaner.

A proper IsUnitDead comparison includes a GetUnitTypeId(unit) != 0 check,
because UNIT_TYPE_DEAD doesn't work properly for removed units.
It looks like if not IsUnitType(u, UNIT_TYPE_DEAD) and GetUnitTypeId(unit) != 0 then

Deg2Rad(angle) is slower than angle*bj_DEGTORAD

call SetUnitState(udg_FS_Caster[Node], UNIT_STATE_LIFE, GetUnitState(udg_FS_Caster[Node], UNIT_STATE_LIFE) + udg_FS_Damage[Node])
--> Better go with native GetWidgetLife and SetWidgetLife.

JASS:
        if (udg_FS_CastedCount == 1) then
            call TimerStart(udg_FS_Timer, FS_TimerSpeed(), true, function FS_Loop)
        endif
^Just for the record: This comparison could replaced by udg_FS_PrevNode[Node] == 0.
But using a counter is also totally fine.
 
Level 37
Joined
Jul 22, 2015
Messages
3,485
Only skimmed the code. I will take a more in depth look later!

  • Needs more white space :p
  • FS_CastedCount currently only acts as some type of indicator to start and pause the timer. You can get rid of it and do it like this:
    JASS:
    //on cast
    if udg_FS_PrevNode[Node] == 0 then
        call TimerStart()
    endif
    
    //on deindex
    if udg__NextNode[0] == 0 then
        call PauseTimer()
    endif
  • Deg2Rad(GetRandomReal(1, 360)) Angles start at 0, not 1 :p
  • Use a variable like DistanceTraveled = DistanceTraveled + Speed to see if the soul has reached the caster. It's less taxing than having to constantly keep calculating distance
  • Some things that I think shouldn't be hardcoded: attachment point of SpecialEf / channel order id / Flyheight when transititioning to phase 2 / integer id in UnitApplyTimedLife

EDIT: Wow.... darn you BPower. I was editing mine and making it look nice :(
 
Level 11
Joined
Jul 25, 2014
Messages
490
Thanks KILLCIDE and BPower, will get straight to it :p

~Updated. The only thing I didn't do was implement a DistanceTravelled variable. It does make lots of additions each loop, also if I would keep one then there would be some sense to use it on the first phase as well, but I can't use it that way, since the target is moving -- anyway, thanks!

Also added a counter effect duration just as I did with the life draining effect, to the pushback effect, to make it less spammy. Thanks BPower.
 
Last edited:
Level 37
Joined
Jul 22, 2015
Messages
3,485
I can't use it that way, since the target is moving -- anyway, thanks!

Oh my apologies. I didn't realize the target can move. If that's the case, add another configurable of what value should a missle be considered "within range" of a target. A couple of other things:
  • Get rid of set udg_FS_CastedCount = udg_FS_CastedCount + 1 & set udg_FS_CastedCount = udg_FS_CastedCount - 1
  • JASS:
    if udg_FS_EffectDuration[Node] < 0.00 then
        call DestroyEffect(AddSpecialEffectTarget(FS_DrainingEffect(), udg_FS_Target[Node], FS_DrainingEffectLoc()))
        call DestroyEffect(AddSpecialEffectTarget(FS_DrainingEffect(), udg_FS_Caster[Node], FS_DrainingEffectLoc()))
        set udg_FS_EffectDuration[Node] = FS_EffectInterval()
    endif
    ^I'm assuming you did this to add amplification to the SFX? I recommend using a loop and adding a configurable to how many of the same effects will happen at this part.

I like the special effects. My only gripe is how awkward it looks when the missle reaches its target in the middle of the parabola.
 
Level 11
Joined
Jul 25, 2014
Messages
490
There is a configurable for target range, not for the caster range, that's hardcoded, will implement this.

Oh yeah, that casted count, forgot to remove it.

In that loop, I decrease a duration and when it ends, I create a special effect at the caster's origin and at the target's origin because if I don't, it looks ridiculous and I guess it's laggy when you spam it. I'm .. not entirely sure what you meant by that.

I guess instead of using a GetUnitX(FS_Target[Node]) I can get the radius offset from the target to make the parabola look smooth. Thanks for the input :>

Edit: ~Updated
 
Last edited:
Level 37
Joined
Jul 22, 2015
Messages
3,485
In that loop, I decrease a duration and when it ends, I create a special effect at the caster's origin and at the target's origin because if I don't, it looks ridiculous and I guess it's laggy when you spam it. I'm .. not entirely sure what you meant by that.
Now that I look at the parameters, I didn't realize they were casted on different targets xD my bad.
 
Level 11
Joined
Jul 25, 2014
Messages
490
I thought about adding a max distance, but then it would be too underpowered. And think about it, it's not really beneficial for the caster if the target runs away far enough -- the spell becomes less efficient, since it requires time to transition between phases (that being the speed of the soul in p1/p3)
 
Quite a nice idea
Just some quick things I noticed:

Major:
- You don't need to set x3/y3 to any value unless it's in phase 1 or 3 (you have an if condition which already checks this, and it's otherwise not used)
- you can reuse x/y values that aren't used again (for instance you don't need x2/y2 in the onCast function
- You can channel on multiple units at once while moving cancels; this could be optional since normally you wouldn't be able to do it
- The ability works on enemies and allies (you can also target yourself)

Minor:
- Target filters could be in their own function (so it can be altered more easily by users)
- No proper manacost or cooldown
- you should add/remove stormcrow form to any dummy that has fly height controls (allows things like ground & floating dummies to have flyheight changed which has slightly different implications and sometimes different visual effects)
- levelup trigger can cause your unit losing the ability (a bit irritating when testing but nothing to do with the ability)
 
Level 11
Joined
Jul 25, 2014
Messages
490
- You can channel on multiple units at once while moving cancels; this could be optional since normally you wouldn't be able to do it

I'd appreciate if you'd tell me how to do that :p.

As for the other major things, I did them except for the re-using x/y values. In the onCast tried to reuse the same, it bugged out. As for the loop, I know some of them are a bit redundant, though I did it for easier editing and readability.
 
I don't follow why it would bug unless you did something odd with it given you don't refer to x again after you assign x2, perhaps you didn't change a variable reference?

eitherway when preventing multiple channelling the easiest way to do that is to run a check if the unit already has an instance tied to it when it attempts to cast the spell again - normally a O(n) search through the linked list for instances where the Caster[Node] == unit
you can then instantly terminate that instance (setting the duration to 0) or overwrite the instance with all-new data - the former is easier to do but both are valid methods
 
While testing I encountered this major bug (appears to be related to multiple instances either recycling or channel cancelling - I've included both a still image of the frame the bug occurred and the GIF of my complete actions starting from the first cast
(Specifically I start 3 casts then restart the 2nd one - the return projectile from the restarted 2nd one appears to be the specifically bugging component)

As a side note it looks pretty odd when the the energy is going through the ground like in the other attached image, is that intentional?


Projectile%20Jump.jpg


Edit: as a quick aside your object data isn't set up correctly: Level 3 of the ability is not visible (cannot be used)
 

Attachments

  • Jump Frame.gif
    Jump Frame.gif
    261.5 KB · Views: 159
  • In the ground.png
    In the ground.png
    381.5 KB · Views: 123
Level 11
Joined
Jul 25, 2014
Messages
490
As a side note it looks pretty odd when the the energy is going through the ground like in the other attached image, is that intentional?
Ah. When I was playing around with air units, i didnt add the soul height to the formula. Will fix.

Edit: as a quick aside your object data isn't set up correctly: Level 3 of the ability is not visible (cannot be used)
Oh I see, that explains things. Will fix.

Will check out the bugging in a bit.

~Edit: Updated after the discussion in chat.
~Edit2: Radical code efficiency update.
 
Last edited:
Top