• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.
  • The Hive's 22nd Icon Contest: Creep Abilities is now concluded, time to vote for your favourite set of icons! Click here to vote!
  • ✅ The POLL for Hive's Texturing Contest #34 is OPEN! Vote for the TOP 3 SKINS! 🔗Click here to cast your vote!
  • ✅ The POLL for Hive's Techtree Contest #20 is OPEN! Vote for the TOP 3 FACTIONS! 🔗Click here to cast your vote!

Custom missile spell, having issues trying to work it out properly

Level 10
Joined
May 24, 2016
Messages
357
Hello, im trying to make my projectile work properly as intended. To be honest, I got sick last days and probably my head is too dizzy to solve anything. I made a too complex projectile spell to make it work considering my current condition (and Im just kinda dumb anyway for something like that)

Phase 0:

  • Launch phase
  • Projectile starts with strong vertical velocity (<span>vz</span>)
  • Very small horizontal movement
  • Gradually loses upward force because of simulated gravity
  • Slowly drifts toward target direction
Phase 1:

  • Glide / stabilization phase
  • Projectile begins gaining horizontal speed
  • Vertical speed becomes near-zero or slightly negative
  • Smooth turning toward target using limited turn rate
  • Intended to look like a cruise missile leveling out in the air
Phase 2:

  • Dive / attack phase
  • Projectile aggressively homes toward target
  • Gravity increases strongly
  • Horizontal speed accelerates
  • Projectile dives toward the ground and explodes on ground contact
Other details:

  • Projectile stores custom x/y/z coordinates manually
  • Uses angle interpolation instead of instant turning
  • Uses separate vertical velocity (<span>vz</span>) and horizontal speed
  • Projectile can optionally semi-home toward a moving target
  • Explosion occurs when projectile reaches low enough z height during phase 2
  • Goal is cinematic movement:
    sharp launch ↗
    climb ↗
    glide →
    dive ↘
    acceleration ↘↘
    impact 💥


    Issues:
    1. After phase 0, projectile movement becomes jittery/shaky.
      The missile appears to constantly micro-correct its direction, causing unnatural movement instead of smooth glide or dive behavior.
    2. Height consistency is unstable.
      Some projectiles enter phase 1 at a very high altitude, while others transition much lower, even under seemingly similar conditions/distances.
    3. Rarely, the projectile physics completely break.
      Possible failure cases include:
    4. infinite diving
    5. spinning/circling in the air
    6. sliding/flying along the ground without exploding
    7. continuing movement after visually touching the ground
      The projectile is supposed to explode once it reaches a low enough height during phase 2, but in some edge cases this condition fails or becomes unstable.
JASS:
function Watcher_L takes nothing returns nothing //launches missile 

    local timer t=GetExpiredTimer()
    local integer idT=GetHandleId(t)

    local unit wt=LoadUnitHandle(Hash,idT,0)
    local unit c=LoadUnitHandle(Hash,idT,1)
    //local unit F=LoadUnitHandle(Hash,idT,3)

    local real r=LoadReal(Hash,idT,2)-1.

    local real x
    local real y

    local real x2
    local real y2

    local real totalDist
    local real maxHeight

    local timer mt
    local integer id

    local unit missile

    if not IsUnitDead(wt) then

        set x=GetUnitX(wt)
        set y=GetUnitY(wt)

        //
        // target
        //
        
        set I3 = GetPlayerId(GetOwningPlayer(c))
    set F =  GetClosestUnit2(x,y,Condition(function ChainLightning_F))
        
        if F != null and DistanceXY(x,y,GetUnitX(F),GetUnitY(F)) <= 850 then 
        
        set x2=GetUnitX(F)
        set y2=GetUnitY(F)

        //
        // distance
        //
        set totalDist=SquareRoot((x2-x)*(x2-x)+(y2-y)*(y2-y))

        //
        // adaptive height
        //
        set maxHeight=325.+totalDist*0.45

        if maxHeight>620. then
            set maxHeight=620.
        endif

        if maxHeight<300. then
            set maxHeight=300.
        endif

        //
        // missile dummy
        //
        set missile=CreateUnit(GetOwningPlayer(c),'dumi',x,y,0)

        call UnitAddAbility(missile,'Amrf')
        call UnitRemoveAbility(missile,'Amrf')

        call SetUnitFlyHeight(missile,0.,0.)

        //
        // model
        //
        call AddSpecialEffectTarget("Abilities\\Weapons\\FireBallMissile\\FireBallMissile.mdl",missile,"origin")

        call SetUnitScale(missile,0.40,0.40,0.40)

        //
        // missile timer
        //
        set mt=CreateTimer()

        set id=GetHandleId(mt)

        //
        // save units
        //
        call SaveUnitHandle(Hash,id,StringHash("dummy"),missile)
        call SaveUnitHandle(Hash,id,StringHash("caster"),c)
        call SaveUnitHandle(Hash,id,StringHash("target"),F)

        //
        // save coords
        //
        call SaveReal(Hash,id,StringHash("x"),x)
        call SaveReal(Hash,id,StringHash("y"),y)
        call SaveReal(Hash,id,StringHash("z"),0.)

        call SaveReal(Hash,id,StringHash("tx"),x2)
        call SaveReal(Hash,id,StringHash("ty"),y2)

        //
        // physics
        //
        call SaveReal(Hash,id,StringHash("vz"),24.)
        call SaveReal(Hash,id,StringHash("speed"),1.5)

        call SaveReal(Hash,id,StringHash("angle"),Atan2(y2-y,x2-x))

        //
        // movement state
        //
        call SaveInteger(Hash,id,StringHash("phase"),0)

        //
        // misc
        //
        call SaveReal(Hash,id,StringHash("totalDist"),totalDist)
        call SaveReal(Hash,id,StringHash("traveled"),0.)
        call SaveReal(Hash,id,StringHash("maxHeight"),maxHeight)
        call SaveReal(Hash,id,StringHash("scale"),0.40)

        //
        // debug
        //
        call MyUtils_echo("LAUNCH x="+R2S(x)+" y="+R2S(y)+" tx="+R2S(x2)+" ty="+R2S(y2)+" dist="+R2S(totalDist))

        //
        // start missile physics
        //
        call TimerStart(mt,0.03,true,function WatcherShot_OnPeriod)
        endif
        

    else

        call FlushChildHashtable(Hash,idT)

        call PauseTimer(t)
        call DestroyTimer(t)

    endif

    if r<=0. then

        call FlushChildHashtable(Hash,idT)

        call PauseTimer(t)
        call DestroyTimer(t)

    else

        call SaveReal(Hash,idT,2,r)

    endif

    set missile=null
    set mt=null

    set wt=null
    set c=null
    set F=null

    set t=null

endfunction




// Period

function WatcherShot_OnPeriod takes nothing returns nothing

    local timer t=GetExpiredTimer()
    local integer id=GetHandleId(t)

    local unit missile=LoadUnitHandle(Hash,id,StringHash("dummy"))
    local unit caster=LoadUnitHandle(Hash,id,StringHash("caster"))
    local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

    local real x=LoadReal(Hash,id,StringHash("x"))
    local real y=LoadReal(Hash,id,StringHash("y"))
    local real z=LoadReal(Hash,id,StringHash("z"))

    local real tx=LoadReal(Hash,id,StringHash("tx"))
    local real ty=LoadReal(Hash,id,StringHash("ty"))

    local real vz=LoadReal(Hash,id,StringHash("vz"))
    local real speed=LoadReal(Hash,id,StringHash("speed"))
    local real angle=LoadReal(Hash,id,StringHash("angle"))

    local integer phase=LoadInteger(Hash,id,StringHash("phase"))

    local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
    local real traveled=LoadReal(Hash,id,StringHash("traveled"))
    local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

    local real scale=LoadReal(Hash,id,StringHash("scale"))

    local real targetAngle
    local real angleDiff
    local real turnRate

    local real dist

    local real moveX
    local real moveY

    //
    // current target position
    //
    if target!=null and UnitAlive(target) then

        set tx=GetUnitX(target)
        set ty=GetUnitY(target)

    endif

    //
    // distance
    //
    set dist=SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

    //
    // target angle
    //
    set targetAngle=Atan2(ty-y,tx-x)

    //
    // angle difference
    //
    set angleDiff=targetAngle-angle

    if angleDiff>bj_PI then
        set angleDiff=angleDiff-2.*bj_PI
    endif

    if angleDiff<-bj_PI then
        set angleDiff=angleDiff+2.*bj_PI
    endif

    //
    // ==================================================
    // PHASE 0
    // LAUNCH
    // ==================================================
    //
    if phase==0 then

        set turnRate=0.003

        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        endif

        //
        // minimal drift
        //
        if speed<1.4 then
            set speed=speed+0.03
        endif

        //
        // strong upward impulse
        //
        set vz=vz-0.32

        //
        // prevent infinite climb
        //
        if vz<5. then
            set vz=5.
        endif

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // transition
        //
        if z>=maxHeight or traveled>=totalDist*0.16 then

            set phase=1

            call MyUtils_echo("P0->P1 z="+R2S(z)+" dist="+R2S(dist))

        endif

        //
        // blue
        //
        call SetUnitVertexColor(missile,80,140,255,255)

    //
    // ==================================================
    // PHASE 1
    // GLIDE
    // ==================================================
    //
    elseif phase==1 then

        set turnRate=0.010

        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        else
            set angle=targetAngle
        endif

        //
        // smooth horizontal acceleration
        //
        set speed=speed+0.05

        if speed>8. then
            set speed=8.
        endif

        //
        // slight gravity
        //
        set vz=vz-0.09

        if vz<-1.2 then
            set vz=-1.2
        endif

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // phase 2 transition
        //
        if dist<=totalDist*0.45 then

            set phase=2

            set vz=-2.5

            call MyUtils_echo("P1->P2 z="+R2S(z)+" dist="+R2S(dist))

        endif

        //
        // scale growth
        //
        if scale<2.20 then

            set scale=scale+0.11

            if scale>2.20 then
                set scale=2.20
            endif

            call SetUnitScale(missile,scale,scale,scale)

        endif

        //
        // green
        //
        call SetUnitVertexColor(missile,100,255,120,255)

    //
    // ==================================================
    // PHASE 2
    // DIVE
    // ==================================================
    //
    else

        //
        // aggressive steering
        //
        set turnRate=0.035

        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        else
            set angle=targetAngle
        endif

        //
        // acceleration
        //
        set speed=speed+0.22

        if speed>26. then
            set speed=26.
        endif

        //
        // strong gravity
        //
        set vz=vz-1.10

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // red
        //
        call SetUnitVertexColor(missile,255,90,90,255)

    endif

    //
    // ground impact
    //
    if z<=15. and phase==2 then

        call MyUtils_echo("HIT x="+R2S(x)+" y="+R2S(y)+" dist="+R2S(dist))

        call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx",x,y))

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,350.,null)

        loop
            set target=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen target==null

            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,target)

            if UnitAlive(target) and IsUnitEnemy(target,GetOwningPlayer(caster)) then

                call UnitDamageTarget(caster,target,300.,false,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,null)

            endif

        endloop

        call RemoveUnit(missile)

        call FlushChildHashtable(Hash,id)

        call PauseTimer(t)
        call DestroyTimer(t)

        set missile=null
        set caster=null
        set target=null
        set t=null

        return

    endif

    //
    // apply movement
    //
    call SetUnitX(missile,x)
    call SetUnitY(missile,y)

    call SetUnitFlyHeight(missile,z,0.)

    call SetUnitFacing(missile,angle*bj_RADTODEG)

    //
    // save
    //
    call SaveReal(Hash,id,StringHash("x"),x)
    call SaveReal(Hash,id,StringHash("y"),y)
    call SaveReal(Hash,id,StringHash("z"),z)

    call SaveReal(Hash,id,StringHash("tx"),tx)
    call SaveReal(Hash,id,StringHash("ty"),ty)

    call SaveReal(Hash,id,StringHash("vz"),vz)
    call SaveReal(Hash,id,StringHash("speed"),speed)
    call SaveReal(Hash,id,StringHash("angle"),angle)

    call SaveReal(Hash,id,StringHash("traveled"),traveled)

    call SaveReal(Hash,id,StringHash("scale"),scale)

    call SaveInteger(Hash,id,StringHash("phase"),phase)

    set missile=null
    set caster=null
    set target=null
    set t=null

endfunction
 
Last edited:
You never calculated dist local var so it becomes a ghost. This missing calculation causes a catastrophic chain reaction that explains every single one of your bugs I believe.


Because dist is 0.0( engine defaults it to 0.0); your initial check if dist<=12. then is always true. This means the script constantly snaps the angle directly to the target (Atan2) and completely skips your smoothturning else block.


if dist<=totalDist*0.45 then set phase=2.

Because dist is always 0.0, the missile enters Phase 1 and instantly skips to Phase 2 on the exact same frame. It never gets to glide.

Basically, it begins the aggressive Phase 2 dive way too early and way too high. The aggressive vz = vz - 0.9 gravity pulls it into the ground, but if the target moves or the math desyncs slightly, it orbits the target while grinding its face into the dirt.


Calculate the distance after loading x, y, tx, ty:

set dist = SquareRoot((tx - x)*(tx - x) + (ty - y)*(ty - y))


Also, I'm worried about the orbit in Phase 2...

if the target is moving fast and the missile misses the z <= 30 check by a fraction of a second, it will dive underground? (because vz keeps getting more negative) and circle forever.

Try adding a safety net to your impact condition:Change if z<=30. and phase == 2 then to if (z<=30. or dist <= 40.) and phase == 2 then....

This should ensure that even if the height math gets weird on uneven terrain, a direct bodyhit on the target will still trigger the explosion.
 
Last edited:
You never calculated dist local var so it becomes a ghost. This missing calculation causes a catastrophic chain reaction that explains every single one of your bugs I believe.


Because dist is 0.0( engine defaults it to 0.0); your initial check if dist<=12. then is always true. This means the script constantly snaps the angle directly to the target (Atan2) and completely skips your smoothturning else block.


if dist<=totalDist*0.45 then set phase=2.

Because dist is always 0.0, the missile enters Phase 1 and instantly skips to Phase 2 on the exact same frame. It never gets to glide.

Basically, it begins the aggressive Phase 2 dive way too early and way too high. The aggressive vz = vz - 0.9 gravity pulls it into the ground, but if the target moves or the math desyncs slightly, it orbits the target while grinding its face into the dirt.


Calculate the distance after loading x, y, tx, ty:

set dist = SquareRoot((tx - x)*(tx - x) + (ty - y)*(ty - y))


Also, I'm worried about the orbit in Phase 2...

if the target is moving fast and the missile misses the z <= 30 check by a fraction of a second, it will dive underground? (because vz keeps getting more negative) and circle forever.

Try adding a safety net to your impact condition:Change if z<=30. and phase == 2 then to if (z<=30. or dist <= 40.) and phase == 2 then....

This should ensures that even if the height math gets weird on uneven terrain, a direct bodyhit on the target will still trigger the explosion.
Thx u very much for your attention. Ill try to read your answer one more time to properly get things u said

BY THE WAY I accidentally posted a version of my spell that has been still slightly attached to Missile system that in the end I discontinued to use in any means to be sure it wont interrupt in my calculations.
So Update my original post.
 
Thx u very much for your attention. Ill try to read your answer one more time to properly get things u said

BY THE WAY I accidentally posted a version of my spell that has been still slightly attached to Missile system that in the end I discontinued to use in any means to be sure it wont interrupt in my calculations.
So Update my original post.
Okay, the updated code covers almost everything I previously wrote; does the described behaviour still persist? I'd throw a tiny safety net:

if (z <= 15. or dist <= 40.) and phase == 2 then
 
Okay, the updated code covers almost everything I previously wrote; does the described behaviour still persist? I'd throw a tiny safety net:

if (z <= 15. or dist <= 40.) and phase == 2 then
The ability stopped being completely insane and now behaves almost as intended. Almost.

The current issue is that during Phase 1 the projectile already reaches (or nearly reaches) the enemy target position. Because of that, Phase 2 either:

  • visually barely happens at all,
  • or the missile suddenly drops almost vertically like it instantly became 1000 tons heavier, then detonates immediately.
The intended behavior is different:

  • Phase 1 should be relatively short,
  • while most of the visible flight time should happen during Phase 2,
    where the missile gradually accelerates and dives toward the ground.
Right now it feels like:

  • Phase 0 gains too much altitude,
  • Phase 1 moves the projectile too aggressively toward the target,
  • and Phase 2 is forced to awkwardly compensate for everything at the last second.
There is also still a rare bug where a projectile completely breaks and flies off somewhere into the distant wilderness instead of following its intended trajectory.

JASS:
function WatcherShot_OnPeriod takes nothing returns nothing

    local timer t=GetExpiredTimer()
    local integer id=GetHandleId(t)

    local unit missile=LoadUnitHandle(Hash,id,StringHash("dummy"))
    local unit caster=LoadUnitHandle(Hash,id,StringHash("caster"))
    local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

    local real x=LoadReal(Hash,id,StringHash("x"))
    local real y=LoadReal(Hash,id,StringHash("y"))
    local real z=LoadReal(Hash,id,StringHash("z"))

    local real tx=LoadReal(Hash,id,StringHash("tx"))
    local real ty=LoadReal(Hash,id,StringHash("ty"))

    local real vz=LoadReal(Hash,id,StringHash("vz"))
    local real speed=LoadReal(Hash,id,StringHash("speed"))
    local real angle=LoadReal(Hash,id,StringHash("angle"))

    local integer phase=LoadInteger(Hash,id,StringHash("phase"))

    local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
    local real traveled=LoadReal(Hash,id,StringHash("traveled"))
    local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

    local real scale=LoadReal(Hash,id,StringHash("scale"))

    local real targetAngle
    local real angleDiff
    local real turnRate

    local real dist

    local real moveX
    local real moveY

    //
    // HOMING TARGET UPDATE
    //
    if target!=null and not IsUnitDead(target) then

        set tx=GetUnitX(target)
        set ty=GetUnitY(target)

    endif

    //
    // IMPORTANT:
    // RECALCULATE DISTANCE
    //
    set dist=SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

    //
    // TARGET ANGLE
    //
    set targetAngle=Atan2(ty-y,tx-x)

    //
    // if extremely close:
    // stop smooth steering
    //
    if dist<=12. then

        set angle=targetAngle

    else

        //
        // ANGLE DIFFERENCE
        //
        set angleDiff=targetAngle-angle

        if angleDiff>bj_PI then
            set angleDiff=angleDiff-2.*bj_PI
        endif

        if angleDiff<-bj_PI then
            set angleDiff=angleDiff+2.*bj_PI
        endif

        //
        // TURN RATE
        //
        if phase==0 then
            set turnRate=0.003
        elseif phase==1 then
            set turnRate=0.010
        else
            set turnRate=0.035
        endif

        //
        // SMOOTH TURNING
        //
        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        else
            set angle=targetAngle
        endif

    endif

    //
    // ==================================================
    // PHASE 0
    // LAUNCH
    // ==================================================
    //
    if phase==0 then

        if speed<1.4 then
            set speed=speed+0.03
        endif

        //
        // upward force decay
        //
        set vz=vz-0.32

        //
        // prevent infinite climb
        //
        if vz<5. then
            set vz=5.
        endif

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // transition
        //
        if z>=maxHeight or traveled>=totalDist*0.16 then

            set phase=1

            call MyUtils_echo("P0->P1 z="+R2S(z)+" dist="+R2S(dist))

        endif

        //
        // BLUE
        //
        call SetUnitVertexColor(missile,80,140,255,255)

    //
    // ==================================================
    // PHASE 1
    // GLIDE
    // ==================================================
    //
    elseif phase==1 then

        //
        // horizontal acceleration
        //
        set speed=speed+0.05

        if speed>8. then
            set speed=8.
        endif

        //
        // slight gravity
        //
        set vz=vz-0.09

        if vz<-1.2 then
            set vz=-1.2
        endif

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // IMPORTANT FIX:
        // later phase 2 transition
        //
        if dist<=totalDist*0.45 then

            set phase=2

            set vz=-2.5

            call MyUtils_echo("P1->P2 z="+R2S(z)+" dist="+R2S(dist))

        endif

        //
        // SCALE GROWTH
        //
        if scale<2.20 then

            set scale=scale+0.11

            if scale>2.20 then
                set scale=2.20
            endif

            call SetUnitScale(missile,scale,scale,scale)

        endif

        //
        // GREEN
        //
        call SetUnitVertexColor(missile,100,255,120,255)

    //
    // ==================================================
    // PHASE 2
    // DIVE
    // ==================================================
    //
    else

        //
        // acceleration
        //
        set speed=speed+0.22

        if speed>26. then
            set speed=26.
        endif

        //
        // gravity
        //
        set vz=vz-1.10

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // RED
        //
        call SetUnitVertexColor(missile,255,90,90,255)

    endif

    //
    // ==================================================
    // IMPACT
    // ==================================================
    //
    if (z<=15. or dist<=40.) and phase==2 then

        call MyUtils_echo("HIT x="+R2S(x)+" y="+R2S(y)+" dist="+R2S(dist))

        call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx",x,y))

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,350.,null)

        loop

            set target=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen target==null

            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,target)

            if not IsUnitDead(target) and IsUnitEnemy(target,GetOwningPlayer(caster)) then

                call UnitDamageTarget(caster,target,300.,false,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,null)

            endif

        endloop

        call RemoveUnit(missile)

        call FlushChildHashtable(Hash,id)

        call PauseTimer(t)
        call DestroyTimer(t)

        set missile=null
        set caster=null
        set target=null
        set t=null

        return

    endif

    //
    // APPLY MOVEMENT
    //
    call SetUnitX(missile,x)
    call SetUnitY(missile,y)

    call SetUnitFlyHeight(missile,z,0.)

    call SetUnitFacing(missile,angle*bj_RADTODEG)

    //
    // SAVE
    //
    call SaveReal(Hash,id,StringHash("x"),x)
    call SaveReal(Hash,id,StringHash("y"),y)
    call SaveReal(Hash,id,StringHash("z"),z)

    call SaveReal(Hash,id,StringHash("tx"),tx)
    call SaveReal(Hash,id,StringHash("ty"),ty)

    call SaveReal(Hash,id,StringHash("vz"),vz)
    call SaveReal(Hash,id,StringHash("speed"),speed)
    call SaveReal(Hash,id,StringHash("angle"),angle)

    call SaveReal(Hash,id,StringHash("traveled"),traveled)

    call SaveReal(Hash,id,StringHash("scale"),scale)

    call SaveInteger(Hash,id,StringHash("phase"),phase)

    set missile=null
    set caster=null
    set target=null
    set t=null

endfunction


I pinned a test map if needed. Paladin has that ability
 

Attachments

The ability stopped being completely insane and now behaves almost as intended. Almost.

The current issue is that during Phase 1 the projectile already reaches (or nearly reaches) the enemy target position. Because of that, Phase 2 either:

  • visually barely happens at all,
  • or the missile suddenly drops almost vertically like it instantly became 1000 tons heavier, then detonates immediately.
The intended behavior is different:

  • Phase 1 should be relatively short,
  • while most of the visible flight time should happen during Phase 2,
    where the missile gradually accelerates and dives toward the ground.
Right now it feels like:

  • Phase 0 gains too much altitude,
  • Phase 1 moves the projectile too aggressively toward the target,
  • and Phase 2 is forced to awkwardly compensate for everything at the last second.
There is also still a rare bug where a projectile completely breaks and flies off somewhere into the distant wilderness instead of following its intended trajectory.

JASS:
function WatcherShot_OnPeriod takes nothing returns nothing

    local timer t=GetExpiredTimer()
    local integer id=GetHandleId(t)

    local unit missile=LoadUnitHandle(Hash,id,StringHash("dummy"))
    local unit caster=LoadUnitHandle(Hash,id,StringHash("caster"))
    local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

    local real x=LoadReal(Hash,id,StringHash("x"))
    local real y=LoadReal(Hash,id,StringHash("y"))
    local real z=LoadReal(Hash,id,StringHash("z"))

    local real tx=LoadReal(Hash,id,StringHash("tx"))
    local real ty=LoadReal(Hash,id,StringHash("ty"))

    local real vz=LoadReal(Hash,id,StringHash("vz"))
    local real speed=LoadReal(Hash,id,StringHash("speed"))
    local real angle=LoadReal(Hash,id,StringHash("angle"))

    local integer phase=LoadInteger(Hash,id,StringHash("phase"))

    local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
    local real traveled=LoadReal(Hash,id,StringHash("traveled"))
    local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

    local real scale=LoadReal(Hash,id,StringHash("scale"))

    local real targetAngle
    local real angleDiff
    local real turnRate

    local real dist

    local real moveX
    local real moveY

    //
    // HOMING TARGET UPDATE
    //
    if target!=null and not IsUnitDead(target) then

        set tx=GetUnitX(target)
        set ty=GetUnitY(target)

    endif

    //
    // IMPORTANT:
    // RECALCULATE DISTANCE
    //
    set dist=SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

    //
    // TARGET ANGLE
    //
    set targetAngle=Atan2(ty-y,tx-x)

    //
    // if extremely close:
    // stop smooth steering
    //
    if dist<=12. then

        set angle=targetAngle

    else

        //
        // ANGLE DIFFERENCE
        //
        set angleDiff=targetAngle-angle

        if angleDiff>bj_PI then
            set angleDiff=angleDiff-2.*bj_PI
        endif

        if angleDiff<-bj_PI then
            set angleDiff=angleDiff+2.*bj_PI
        endif

        //
        // TURN RATE
        //
        if phase==0 then
            set turnRate=0.003
        elseif phase==1 then
            set turnRate=0.010
        else
            set turnRate=0.035
        endif

        //
        // SMOOTH TURNING
        //
        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        else
            set angle=targetAngle
        endif

    endif

    //
    // ==================================================
    // PHASE 0
    // LAUNCH
    // ==================================================
    //
    if phase==0 then

        if speed<1.4 then
            set speed=speed+0.03
        endif

        //
        // upward force decay
        //
        set vz=vz-0.32

        //
        // prevent infinite climb
        //
        if vz<5. then
            set vz=5.
        endif

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // transition
        //
        if z>=maxHeight or traveled>=totalDist*0.16 then

            set phase=1

            call MyUtils_echo("P0->P1 z="+R2S(z)+" dist="+R2S(dist))

        endif

        //
        // BLUE
        //
        call SetUnitVertexColor(missile,80,140,255,255)

    //
    // ==================================================
    // PHASE 1
    // GLIDE
    // ==================================================
    //
    elseif phase==1 then

        //
        // horizontal acceleration
        //
        set speed=speed+0.05

        if speed>8. then
            set speed=8.
        endif

        //
        // slight gravity
        //
        set vz=vz-0.09

        if vz<-1.2 then
            set vz=-1.2
        endif

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // IMPORTANT FIX:
        // later phase 2 transition
        //
        if dist<=totalDist*0.45 then

            set phase=2

            set vz=-2.5

            call MyUtils_echo("P1->P2 z="+R2S(z)+" dist="+R2S(dist))

        endif

        //
        // SCALE GROWTH
        //
        if scale<2.20 then

            set scale=scale+0.11

            if scale>2.20 then
                set scale=2.20
            endif

            call SetUnitScale(missile,scale,scale,scale)

        endif

        //
        // GREEN
        //
        call SetUnitVertexColor(missile,100,255,120,255)

    //
    // ==================================================
    // PHASE 2
    // DIVE
    // ==================================================
    //
    else

        //
        // acceleration
        //
        set speed=speed+0.22

        if speed>26. then
            set speed=26.
        endif

        //
        // gravity
        //
        set vz=vz-1.10

        //
        // movement
        //
        set moveX=speed*Cos(angle)
        set moveY=speed*Sin(angle)

        set x=x+moveX
        set y=y+moveY

        set z=z+vz

        set traveled=traveled+speed

        //
        // RED
        //
        call SetUnitVertexColor(missile,255,90,90,255)

    endif

    //
    // ==================================================
    // IMPACT
    // ==================================================
    //
    if (z<=15. or dist<=40.) and phase==2 then

        call MyUtils_echo("HIT x="+R2S(x)+" y="+R2S(y)+" dist="+R2S(dist))

        call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx",x,y))

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,350.,null)

        loop

            set target=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen target==null

            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,target)

            if not IsUnitDead(target) and IsUnitEnemy(target,GetOwningPlayer(caster)) then

                call UnitDamageTarget(caster,target,300.,false,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,null)

            endif

        endloop

        call RemoveUnit(missile)

        call FlushChildHashtable(Hash,id)

        call PauseTimer(t)
        call DestroyTimer(t)

        set missile=null
        set caster=null
        set target=null
        set t=null

        return

    endif

    //
    // APPLY MOVEMENT
    //
    call SetUnitX(missile,x)
    call SetUnitY(missile,y)

    call SetUnitFlyHeight(missile,z,0.)

    call SetUnitFacing(missile,angle*bj_RADTODEG)

    //
    // SAVE
    //
    call SaveReal(Hash,id,StringHash("x"),x)
    call SaveReal(Hash,id,StringHash("y"),y)
    call SaveReal(Hash,id,StringHash("z"),z)

    call SaveReal(Hash,id,StringHash("tx"),tx)
    call SaveReal(Hash,id,StringHash("ty"),ty)

    call SaveReal(Hash,id,StringHash("vz"),vz)
    call SaveReal(Hash,id,StringHash("speed"),speed)
    call SaveReal(Hash,id,StringHash("angle"),angle)

    call SaveReal(Hash,id,StringHash("traveled"),traveled)

    call SaveReal(Hash,id,StringHash("scale"),scale)

    call SaveInteger(Hash,id,StringHash("phase"),phase)

    set missile=null
    set caster=null
    set target=null
    set t=null

endfunction


I pinned a test map if needed. Paladin has that ability
Man, this was a hellish ride.

I think the missile physics are finally mostly stable now — the projectile properly arcs, glides, and transitions into a dive instead of instantly cratering or endlessly orbiting.

However, I still couldn’t fully isolate why some missiles explode visually before reaching the ground. At this point, I no longer think the issue is purely the flight math itself; it’s more likely tied to impact conditions, object destruction/recycling, or another external interaction happening during runtime.

The current library version:


vJASS:
//TESH.scrollpos=52
//TESH.alwaysfold=0
library WatcherMissile initializer Init uses SpellIndex, Missile, MyUtils

globals
    private constant attacktype ATTACK_TYPE=ATTACK_TYPE_NORMAL
    private constant damagetype DAMAGE_TYPE=DAMAGE_TYPE_MAGIC

    private constant real EXPLOSION_RADIUS=350.
    private constant real EXPLOSION_DAMAGE=300.

    private constant string ON_EXPLODE_FX="war3mapImported\\Tidal Burst - Classic.mdx"
endglobals

private function FilterUnits takes unit target, player owner returns boolean
    return UnitAlive(target) and IsUnitEnemy(target,owner) and MyUtils_UnitTypeNotDummy(target)
endfunction

struct WatcherShot extends array

    // =====================
    // EXPLOSION
    // =====================
    private static method explode takes Missile missile returns nothing
        local integer id=missile
        local real x=LoadReal(Hash,id,StringHash("x"))
        local real y=LoadReal(Hash,id,StringHash("y"))
        local unit u

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,EXPLOSION_RADIUS,null)
        loop
            set u=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen u==null
            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,u)
            if FilterUnits(u,missile.owner) then
                call UnitDamageTarget(missile.source,u,EXPLOSION_DAMAGE,false,false,ATTACK_TYPE,DAMAGE_TYPE,null)
            endif
        endloop
        call DestroyEffect(AddSpecialEffect(ON_EXPLODE_FX,x,y))
    endmethod

    // =====================
    // MAIN PHYSICS
    // =====================
    private static method onPeriod takes Missile missile returns boolean
        local integer id=missile

        local real x=LoadReal(Hash,id,StringHash("x"))
        local real y=LoadReal(Hash,id,StringHash("y"))
        local real z=LoadReal(Hash,id,StringHash("z"))

        local real tx=LoadReal(Hash,id,StringHash("tx"))
        local real ty=LoadReal(Hash,id,StringHash("ty"))
        local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

        local real vz=LoadReal(Hash,id,StringHash("vz"))
        local real speed=LoadReal(Hash,id,StringHash("speed"))
        local real scale=LoadReal(Hash,id,StringHash("scale"))
        local real angle=LoadReal(Hash,id,StringHash("angle"))

        local integer phase=LoadInteger(Hash,id,StringHash("phase"))

        local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
        local real traveled=LoadReal(Hash,id,StringHash("traveled"))
        local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

        local real targetAngle
        local real angleDiff
        local real turnRate
        local real dist

        local real moveX
        local real moveY
        local boolean shouldExplode

        // ---- update target position if alive ----
        if target != null and UnitAlive(target) then
            set tx = GetUnitX(target)
            set ty = GetUnitY(target)
        endif

        // ---- distance to target ----
        set dist = SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

        // ---- dead target failsafe ----
        if target == null or IsUnitDeadBJ(target) then
            set phase = 2
            set vz = -3.0
        endif

        // ---- steering ----
        if dist <= 12. then
            set angle = Atan2(ty-y, tx-x)
        else
            set targetAngle = Atan2(ty-y, tx-x)
            set angleDiff = targetAngle - angle

            if angleDiff > bj_PI then
                set angleDiff = angleDiff - 2.*bj_PI
            endif
            if angleDiff < -bj_PI then
                set angleDiff = angleDiff + 2.*bj_PI
            endif

            if phase == 0 then
                set turnRate = 0.004
            elseif phase == 1 then
                set turnRate = 0.015
            else
                set turnRate = 0.040
            endif

            // reduced close‑range turn commits, doesn't orbit
            if phase == 2 and dist <= 80. then
                set turnRate = 0.035
            endif

            if angleDiff > turnRate then
                set angle = angle + turnRate
            elseif angleDiff < -turnRate then
                set angle = angle - turnRate
            else
                set angle = targetAngle
            endif
        endif

        // ================== PHASE 0 (LAUNCH) ==================
        if phase == 0 then
            call SetUnitVertexColor(missile.dummy, 80, 80, 255, 255)
            if speed > 1.6 then
                set speed = 1.6
            endif

            set vz = vz - 0.45
            // gentler upward cap prevents orbit altitudes
            if vz < 2.5 then
                set vz = 2.5
            endif

            set traveled = traveled + speed

            if traveled >= totalDist*0.18 or z >= maxHeight then
                set phase = 1
                call MyUtils_echo("P0->P1 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))
            endif

        // ================== PHASE 1 (GLIDE) ==================
        elseif phase == 1 then
            call SetUnitVertexColor(missile.dummy, 80, 255, 80, 255)
            set speed = speed + 0.02
            if speed > 4.5 then
                set speed = 4.5
            endif

            if scale < 2.20 then
                set scale = scale + 0.15
                if scale > 2.20 then
                    set scale = 2.20
                endif
                set missile.scale = scale
            endif

            set vz = vz - 0.05
            if vz < -1.5 then
                set vz = -1.5
            endif

            set traveled = traveled + speed

            if traveled >= totalDist*0.35 then
                set phase = 2
                set vz = -1.0
                call MyUtils_echo("P1->P2 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))
            endif

        // ================== PHASE 2 (DIVE) ==================
        else
            call SetUnitVertexColor(missile.dummy, 255, 80, 80, 255)

            // terminal slowdown  heavy artillery, not a jet fighter lol
            if dist <= 140. then
                set speed = speed * 0.92
            endif

            set speed = speed + 0.18
            if speed > 24. then
                set speed = 24.
            endif

            set vz = vz - 0.55
            if vz < -12.0 then
                set vz = -12.0
            endif

            set traveled = traveled + speed

            if dist <= 120. then
                call MyUtils_echo("FINAL dist="+R2S(dist)+" speed="+R2S(speed)+" z="+R2S(z))
            endif
        endif

        // ---- movement ----
        set moveX = speed * Cos(angle)
        set moveY = speed * Sin(angle)
        set x = x + moveX
        set y = y + moveY
        set z = z + vz

        // ---- impact detection (tight  BOTH low AND close) ----
        if phase != 2 then
            set shouldExplode = false
        elseif traveled > totalDist * 1.20 then
            set shouldExplode = true
        elseif not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
            set shouldExplode = true
        elseif dist <= speed + 24. and z <= 120. then
            // close enough AND descending true ground impact
            set shouldExplode = true
        else
            set shouldExplode = false
        endif

        if shouldExplode then
            call MyUtils_echo("HIT dist="+R2S(dist)+" speed="+R2S(speed)+" vz="+R2S(vz)+" z="+R2S(z))
            set z = 0.
            call SaveReal(Hash, id, StringHash("x"), x)
            call SaveReal(Hash, id, StringHash("y"), y)
            call missile.destroy()
            return false
        endif

        // ---- apply position ----
        call SetUnitX(missile.dummy, x)
        call SetUnitY(missile.dummy, y)
        call SetUnitFlyHeight(missile.dummy, z, 0.)
        call SetUnitFacing(missile.dummy, angle * bj_RADTODEG)

        // ---- save state ----
        call SaveReal(Hash, id, StringHash("x"), x)
        call SaveReal(Hash, id, StringHash("y"), y)
        call SaveReal(Hash, id, StringHash("z"), z)
        call SaveReal(Hash, id, StringHash("vz"), vz)
        call SaveReal(Hash, id, StringHash("speed"), speed)
        call SaveReal(Hash, id, StringHash("angle"), angle)
        call SaveReal(Hash, id, StringHash("scale"), scale)
        call SaveReal(Hash, id, StringHash("traveled"), traveled)
        call SaveInteger(Hash, id, StringHash("phase"), phase)

        return false
    endmethod

    private static method onRemove takes Missile missile returns boolean
        call thistype.explode(missile)
        return false
    endmethod

    implement MissileStruct

endstruct

private function Init takes nothing returns nothing
endfunction

endlibrary


The most probable causes left:


1. Multiple spell-event registrations



If the launch trigger fires twice per cast (SPELL_EFFECT + SPELL_CAST, duplicated registration, etc.), you can end up with overlapping missiles or premature destruction.

Add:
call BJDebugMsg("Missile launched!")
near WatcherShot.launch() and verify it only appears once per cast.


2. Impact condition still too strict



Current condition:
elseif dist <= speed + 24. and z <= 120. then
requires BOTH:
  • very close horizontal distance,
  • and sufficiently low altitude.

Depending on velocity + turn rate, the missile can overshoot horizontally while still above the z-threshold, causing it to miss the impact window entirely.



3. Phase 2 steering oscillation



Even with reduced close-range turn commits, the missile can still curve around moving targets instead of fully committing to impact.


Reducing close-range turn rate further may help:


set turnRate = 0.030




4. Premature destruction through onRemove



onRemove() always explodes the missile:


private static method onRemove takes Missile missile returns boolean
call thistype.explode(missile)


So if anything destroys/recycles the missile object whether external systems, MissileStruct internals, or dummy death the explosion fires immediately.


That’s currently the most suspicious behavior.

A very good isolation test:


temporarily disable onRemove() entirely and check whether missiles still vanish mid-air.


If they do:


something else is destroying the missile.

If they don’t:


the issue is almost certainly tied to unintended destroy() calls or object recycling.


5. Dead-target fallback behavior



This:
set phase = 2
set vz = -3.0


can create unnatural emergency dives if the target dies early.


It may be cleaner to immediately detonate at the missile’s current position instead of forcing a terminal dive state.


Let me know if you get these V2 ballistic missiles to hit the ground tho. :goblin_boom:
 
Last edited:
Man, this was a hellish ride.

I think the missile physics are finally mostly stable now — the projectile properly arcs, glides, and transitions into a dive instead of instantly cratering or endlessly orbiting.

However, I still couldn’t fully isolate why some missiles explode visually before reaching the ground. At this point, I no longer think the issue is purely the flight math itself; it’s more likely tied to impact conditions, object destruction/recycling, or another external interaction happening during runtime.

The current library version:


vJASS:
//TESH.scrollpos=52
//TESH.alwaysfold=0
library WatcherMissile initializer Init uses SpellIndex, Missile, MyUtils

globals
    private constant attacktype ATTACK_TYPE=ATTACK_TYPE_NORMAL
    private constant damagetype DAMAGE_TYPE=DAMAGE_TYPE_MAGIC

    private constant real EXPLOSION_RADIUS=350.
    private constant real EXPLOSION_DAMAGE=300.

    private constant string ON_EXPLODE_FX="war3mapImported\\Tidal Burst - Classic.mdx"
endglobals

private function FilterUnits takes unit target, player owner returns boolean
    return UnitAlive(target) and IsUnitEnemy(target,owner) and MyUtils_UnitTypeNotDummy(target)
endfunction

struct WatcherShot extends array

    // =====================
    // EXPLOSION
    // =====================
    private static method explode takes Missile missile returns nothing
        local integer id=missile
        local real x=LoadReal(Hash,id,StringHash("x"))
        local real y=LoadReal(Hash,id,StringHash("y"))
        local unit u

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,EXPLOSION_RADIUS,null)
        loop
            set u=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen u==null
            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,u)
            if FilterUnits(u,missile.owner) then
                call UnitDamageTarget(missile.source,u,EXPLOSION_DAMAGE,false,false,ATTACK_TYPE,DAMAGE_TYPE,null)
            endif
        endloop
        call DestroyEffect(AddSpecialEffect(ON_EXPLODE_FX,x,y))
    endmethod

    // =====================
    // MAIN PHYSICS
    // =====================
    private static method onPeriod takes Missile missile returns boolean
        local integer id=missile

        local real x=LoadReal(Hash,id,StringHash("x"))
        local real y=LoadReal(Hash,id,StringHash("y"))
        local real z=LoadReal(Hash,id,StringHash("z"))

        local real tx=LoadReal(Hash,id,StringHash("tx"))
        local real ty=LoadReal(Hash,id,StringHash("ty"))
        local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

        local real vz=LoadReal(Hash,id,StringHash("vz"))
        local real speed=LoadReal(Hash,id,StringHash("speed"))
        local real scale=LoadReal(Hash,id,StringHash("scale"))
        local real angle=LoadReal(Hash,id,StringHash("angle"))

        local integer phase=LoadInteger(Hash,id,StringHash("phase"))

        local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
        local real traveled=LoadReal(Hash,id,StringHash("traveled"))
        local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

        local real targetAngle
        local real angleDiff
        local real turnRate
        local real dist

        local real moveX
        local real moveY
        local boolean shouldExplode

        // ---- update target position if alive ----
        if target != null and UnitAlive(target) then
            set tx = GetUnitX(target)
            set ty = GetUnitY(target)
        endif

        // ---- distance to target ----
        set dist = SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

        // ---- dead target failsafe ----
        if target == null or IsUnitDeadBJ(target) then
            set phase = 2
            set vz = -3.0
        endif

        // ---- steering ----
        if dist <= 12. then
            set angle = Atan2(ty-y, tx-x)
        else
            set targetAngle = Atan2(ty-y, tx-x)
            set angleDiff = targetAngle - angle

            if angleDiff > bj_PI then
                set angleDiff = angleDiff - 2.*bj_PI
            endif
            if angleDiff < -bj_PI then
                set angleDiff = angleDiff + 2.*bj_PI
            endif

            if phase == 0 then
                set turnRate = 0.004
            elseif phase == 1 then
                set turnRate = 0.015
            else
                set turnRate = 0.040
            endif

            // reduced close‑range turn commits, doesn't orbit
            if phase == 2 and dist <= 80. then
                set turnRate = 0.035
            endif

            if angleDiff > turnRate then
                set angle = angle + turnRate
            elseif angleDiff < -turnRate then
                set angle = angle - turnRate
            else
                set angle = targetAngle
            endif
        endif

        // ================== PHASE 0 (LAUNCH) ==================
        if phase == 0 then
            call SetUnitVertexColor(missile.dummy, 80, 80, 255, 255)
            if speed > 1.6 then
                set speed = 1.6
            endif

            set vz = vz - 0.45
            // gentler upward cap prevents orbit altitudes
            if vz < 2.5 then
                set vz = 2.5
            endif

            set traveled = traveled + speed

            if traveled >= totalDist*0.18 or z >= maxHeight then
                set phase = 1
                call MyUtils_echo("P0->P1 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))
            endif

        // ================== PHASE 1 (GLIDE) ==================
        elseif phase == 1 then
            call SetUnitVertexColor(missile.dummy, 80, 255, 80, 255)
            set speed = speed + 0.02
            if speed > 4.5 then
                set speed = 4.5
            endif

            if scale < 2.20 then
                set scale = scale + 0.15
                if scale > 2.20 then
                    set scale = 2.20
                endif
                set missile.scale = scale
            endif

            set vz = vz - 0.05
            if vz < -1.5 then
                set vz = -1.5
            endif

            set traveled = traveled + speed

            if traveled >= totalDist*0.35 then
                set phase = 2
                set vz = -1.0
                call MyUtils_echo("P1->P2 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))
            endif

        // ================== PHASE 2 (DIVE) ==================
        else
            call SetUnitVertexColor(missile.dummy, 255, 80, 80, 255)

            // terminal slowdown  heavy artillery, not a jet fighter lol
            if dist <= 140. then
                set speed = speed * 0.92
            endif

            set speed = speed + 0.18
            if speed > 24. then
                set speed = 24.
            endif

            set vz = vz - 0.55
            if vz < -12.0 then
                set vz = -12.0
            endif

            set traveled = traveled + speed

            if dist <= 120. then
                call MyUtils_echo("FINAL dist="+R2S(dist)+" speed="+R2S(speed)+" z="+R2S(z))
            endif
        endif

        // ---- movement ----
        set moveX = speed * Cos(angle)
        set moveY = speed * Sin(angle)
        set x = x + moveX
        set y = y + moveY
        set z = z + vz

        // ---- impact detection (tight  BOTH low AND close) ----
        if phase != 2 then
            set shouldExplode = false
        elseif traveled > totalDist * 1.20 then
            set shouldExplode = true
        elseif not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
            set shouldExplode = true
        elseif dist <= speed + 24. and z <= 120. then
            // close enough AND descending true ground impact
            set shouldExplode = true
        else
            set shouldExplode = false
        endif

        if shouldExplode then
            call MyUtils_echo("HIT dist="+R2S(dist)+" speed="+R2S(speed)+" vz="+R2S(vz)+" z="+R2S(z))
            set z = 0.
            call SaveReal(Hash, id, StringHash("x"), x)
            call SaveReal(Hash, id, StringHash("y"), y)
            call missile.destroy()
            return false
        endif

        // ---- apply position ----
        call SetUnitX(missile.dummy, x)
        call SetUnitY(missile.dummy, y)
        call SetUnitFlyHeight(missile.dummy, z, 0.)
        call SetUnitFacing(missile.dummy, angle * bj_RADTODEG)

        // ---- save state ----
        call SaveReal(Hash, id, StringHash("x"), x)
        call SaveReal(Hash, id, StringHash("y"), y)
        call SaveReal(Hash, id, StringHash("z"), z)
        call SaveReal(Hash, id, StringHash("vz"), vz)
        call SaveReal(Hash, id, StringHash("speed"), speed)
        call SaveReal(Hash, id, StringHash("angle"), angle)
        call SaveReal(Hash, id, StringHash("scale"), scale)
        call SaveReal(Hash, id, StringHash("traveled"), traveled)
        call SaveInteger(Hash, id, StringHash("phase"), phase)

        return false
    endmethod

    private static method onRemove takes Missile missile returns boolean
        call thistype.explode(missile)
        return false
    endmethod

    implement MissileStruct

endstruct

private function Init takes nothing returns nothing
endfunction

endlibrary


The most probable causes left:


1. Multiple spell-event registrations



If the launch trigger fires twice per cast (SPELL_EFFECT + SPELL_CAST, duplicated registration, etc.), you can end up with overlapping missiles or premature destruction.

Add:
call BJDebugMsg("Missile launched!")
near WatcherShot.launch() and verify it only appears once per cast.


2. Impact condition still too strict



Current condition:
elseif dist <= speed + 24. and z <= 120. then
requires BOTH:
  • very close horizontal distance,
  • and sufficiently low altitude.

Depending on velocity + turn rate, the missile can overshoot horizontally while still above the z-threshold, causing it to miss the impact window entirely.



3. Phase 2 steering oscillation



Even with reduced close-range turn commits, the missile can still curve around moving targets instead of fully committing to impact.


Reducing close-range turn rate further may help:


set turnRate = 0.030




4. Premature destruction through onRemove



onRemove() always explodes the missile:


private static method onRemove takes Missile missile returns boolean
call thistype.explode(missile)


So if anything destroys/recycles the missile object whether external systems, MissileStruct internals, or dummy death the explosion fires immediately.


That’s currently the most suspicious behavior.

A very good isolation test:


temporarily disable onRemove() entirely and check whether missiles still vanish mid-air.


If they do:


something else is destroying the missile.

If they don’t:


the issue is almost certainly tied to unintended destroy() calls or object recycling.


5. Dead-target fallback behavior



This:
set phase = 2
set vz = -3.0


can create unnatural emergency dives if the target dies early.


It may be cleaner to immediately detonate at the missile’s current position instead of forcing a terminal dive state.


Let me know if you get these V2 ballistic missiles to hit the ground tho. :goblin_boom:
Did u... Reworked the version that takes Missile library as a base? It relies some modification (periodic timer, visual model, remove dummy and etc) related with dumy missile to the Missile library, and uses custom calls such as onRemove(). I decided to leave this Missile library at all and make y own standalone version of Watcher spell, so I can be sure Missile library wont affect anything that happens with dummy missile. Well, I guess it does not matter anyway, as long as move your changes to the WatcherShot_OnPeriod function.

call BJDebugMsg("Missile launched!") I implemented it yesterday before going to sleep and can confirm there's no dublication of launching dummy missile 2 times.



I'll look through your code to apply new changes and will test it out. Thanks
 
Did u... Reworked the version that takes Missile library as a base? It relies some modification (periodic timer, visual model, remove dummy and etc) related with dumy missile to the Missile library, and uses custom calls such as onRemove(). I decided to leave this Missile library at all and make y own standalone version of Watcher spell, so I can be sure Missile library wont affect anything that happens with dummy missile. Well, I guess it does not matter anyway, as long as move your changes to the WatcherShot_OnPeriod function.

call BJDebugMsg("Missile launched!") I implemented it yesterday before going to sleep and can confirm there's no dublication of launching dummy missile 2 times.



I'll look through your code to apply new changes and will test it out. Thanks
Yeah the WatcherMissile V2, tho there were other versions and quite a lot scripts, I thought that was the one. That's why I basically suspected that other utils are standing in the way. Was tired to dig further, so I focused on the physics mostly.
 
Yeah the WatcherMissile V2, tho there were other versions and quite a lot scripts, I thought that was the one. That's why I basically suspected that other utils are standing in the way. Was tired to dig further, so I focused on the physics mostly.
I repinned a demo map, all related with watcher spell is located "CreepSpellsCast" trigger


I applied your changed to WatcherShot_OnPeriod function
JASS:
function WatcherShot_OnPeriod takes nothing returns nothing

    local timer t=GetExpiredTimer()
    local integer id=GetHandleId(t)

    local unit missile=LoadUnitHandle(Hash,id,StringHash("dummy"))
    local unit caster=LoadUnitHandle(Hash,id,StringHash("caster"))
    local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

    local real x=LoadReal(Hash,id,StringHash("x"))
    local real y=LoadReal(Hash,id,StringHash("y"))
    local real z=LoadReal(Hash,id,StringHash("z"))

    local real tx=LoadReal(Hash,id,StringHash("tx"))
    local real ty=LoadReal(Hash,id,StringHash("ty"))

    local real vz=LoadReal(Hash,id,StringHash("vz"))
    local real speed=LoadReal(Hash,id,StringHash("speed"))
    local real scale=LoadReal(Hash,id,StringHash("scale"))
    local real angle=LoadReal(Hash,id,StringHash("angle"))

    local integer phase=LoadInteger(Hash,id,StringHash("phase"))

    local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
    local real traveled=LoadReal(Hash,id,StringHash("traveled"))
    local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

    local real targetAngle
    local real angleDiff
    local real turnRate
    local real dist

    local real moveX
    local real moveY

    local boolean shouldExplode=false

    //
    // UPDATE TARGET POSITION
    //
    if target!=null and not IsUnitDead(target) then
        set tx=GetUnitX(target)
        set ty=GetUnitY(target)
    endif

    //
    // DISTANCE
    //
    set dist=SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

    //
    // DEAD TARGET FAILSAFE
    //
    if target==null or IsUnitDead(target) then
        set phase=2
        set vz=-3.
    endif

    //
    // STEERING
    //
    if dist<=12. then

        set angle=Atan2(ty-y,tx-x)

    else

        set targetAngle=Atan2(ty-y,tx-x)

        set angleDiff=targetAngle-angle

        if angleDiff>bj_PI then
            set angleDiff=angleDiff-2.*bj_PI
        endif

        if angleDiff<-bj_PI then
            set angleDiff=angleDiff+2.*bj_PI
        endif

        //
        // TURN RATE
        //
        if phase==0 then
            set turnRate=0.004
        elseif phase==1 then
            set turnRate=0.015
        else
            set turnRate=0.040
        endif

        //
        // REDUCED CLOSE RANGE TURNING
        //
        if phase==2 and dist<=80. then
            set turnRate=0.030
        endif

        //
        // APPLY TURNING
        //
        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        else
            set angle=targetAngle
        endif

    endif

    //
    // ==================================================
    // PHASE 0
    // LAUNCH
    // ==================================================
    //
    if phase==0 then

        call SetUnitVertexColor(missile,80,80,255,255)

        //
        // LOW HORIZONTAL DRIFT
        //
        if speed>1.6 then
            set speed=1.6
        endif

        //
        // ASCENT FORCE
        //
        set vz=vz-0.45

        //
        // PREVENT INSANE ALTITUDES
        //
        if vz<2.5 then
            set vz=2.5
        endif

        set traveled=traveled+speed

        //
        // TRANSITION
        //
        if traveled>=totalDist*0.18 or z>=maxHeight then

            set phase=1

            call MyUtils_echo("P0->P1 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))

        endif

    //
    // ==================================================
    // PHASE 1
    // GLIDE
    // ==================================================
    //
    elseif phase==1 then

        call SetUnitVertexColor(missile,80,255,80,255)

        //
        // HORIZONTAL ACCELERATION
        //
        set speed=speed+0.02

        if speed>4.5 then
            set speed=4.5
        endif

        //
        // SCALE GROWTH
        //
        if scale<2.20 then

            set scale=scale+0.15

            if scale>2.20 then
                set scale=2.20
            endif

            call SetUnitScale(missile,scale,scale,scale)

        endif

        //
        // CONTROLLED DESCENT
        //
        set vz=vz-0.05

        if vz<-1.5 then
            set vz=-1.5
        endif

        set traveled=traveled+speed

        //
        // TRANSITION TO DIVE
        //
        if traveled>=totalDist*0.35 then

            set phase=2

            set vz=-1.0

            call MyUtils_echo("P1->P2 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))

        endif

    //
    // ==================================================
    // PHASE 2
    // DIVE
    // ==================================================
    //
    else

        call SetUnitVertexColor(missile,255,80,80,255)

        //
        // TERMINAL SLOWDOWN
        //
        if dist<=140. then
            set speed=speed*0.92
        endif

        //
        // ACCELERATION
        //
        set speed=speed+0.18

        if speed>24. then
            set speed=24.
        endif

        //
        // DIVE GRAVITY
        //
        set vz=vz-0.55

        if vz<-12. then
            set vz=-12.
        endif

        set traveled=traveled+speed

        //
        // DEBUG
        //
        if dist<=120. then

            call MyUtils_echo("FINAL dist="+R2S(dist)+" speed="+R2S(speed)+" z="+R2S(z))

        endif

    endif

    //
    // MOVEMENT
    //
    set moveX=speed*Cos(angle)
    set moveY=speed*Sin(angle)

    set x=x+moveX
    set y=y+moveY

    set z=z+vz

    //
    // IMPACT DETECTION
    //
    if phase!=2 then

        set shouldExplode=false

    elseif traveled>totalDist*1.20 then

        set shouldExplode=true

    elseif not RectContainsCoords(bj_mapInitialPlayableArea,x,y) then

        set shouldExplode=true

    elseif dist<=speed+24. and z<=120. then

        set shouldExplode=true

    endif

    //
    // IMPACT
    //
    if shouldExplode then

        call MyUtils_echo("HIT dist="+R2S(dist)+" speed="+R2S(speed)+" vz="+R2S(vz)+" z="+R2S(z))

        call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx",x,y))

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,350.,null)

        loop

            set target=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen target==null

            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,target)

            if not IsUnitDead(target) and IsUnitEnemy(target,GetOwningPlayer(caster)) then

                call UnitDamageTarget(caster,target,300.,false,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,null)

            endif

        endloop

        call RemoveUnit(missile)

        call FlushChildHashtable(Hash,id)

        call PauseTimer(t)
        call DestroyTimer(t)

        set missile=null
        set caster=null
        set target=null
        set t=null

        return

    endif

    //
    // APPLY POSITION
    //
    call SetUnitX(missile,x)
    call SetUnitY(missile,y)

    call SetUnitFlyHeight(missile,z,0.)

    call SetUnitFacing(missile,angle*bj_RADTODEG)

    //
    // SAVE STATE
    //
    call SaveReal(Hash,id,StringHash("x"),x)
    call SaveReal(Hash,id,StringHash("y"),y)
    call SaveReal(Hash,id,StringHash("z"),z)

    call SaveReal(Hash,id,StringHash("tx"),tx)
    call SaveReal(Hash,id,StringHash("ty"),ty)

    call SaveReal(Hash,id,StringHash("vz"),vz)
    call SaveReal(Hash,id,StringHash("speed"),speed)
    call SaveReal(Hash,id,StringHash("scale"),scale)
    call SaveReal(Hash,id,StringHash("angle"),angle)

    call SaveReal(Hash,id,StringHash("traveled"),traveled)

    call SaveInteger(Hash,id,StringHash("phase"),phase)

    set missile=null
    set caster=null
    set target=null
    set t=null

endfunction

Yeah the WatcherMissile V2, tho there were other versions and quite a lot scripts, I thought that was the one. That's why I basically suspected that other utils are standing in the way. Was tired to dig further, so I focused on the physics mostly.
The projectile is completely broken in testing. It doesn’t fly at all, it just slides along the ground in one fixed direction forever.
 

Attachments

I repinned a demo map, all related with watcher spell is located "CreepSpellsCast" trigger


I applied your changed to WatcherShot_OnPeriod function
JASS:
function WatcherShot_OnPeriod takes nothing returns nothing

    local timer t=GetExpiredTimer()
    local integer id=GetHandleId(t)

    local unit missile=LoadUnitHandle(Hash,id,StringHash("dummy"))
    local unit caster=LoadUnitHandle(Hash,id,StringHash("caster"))
    local unit target=LoadUnitHandle(Hash,id,StringHash("target"))

    local real x=LoadReal(Hash,id,StringHash("x"))
    local real y=LoadReal(Hash,id,StringHash("y"))
    local real z=LoadReal(Hash,id,StringHash("z"))

    local real tx=LoadReal(Hash,id,StringHash("tx"))
    local real ty=LoadReal(Hash,id,StringHash("ty"))

    local real vz=LoadReal(Hash,id,StringHash("vz"))
    local real speed=LoadReal(Hash,id,StringHash("speed"))
    local real scale=LoadReal(Hash,id,StringHash("scale"))
    local real angle=LoadReal(Hash,id,StringHash("angle"))

    local integer phase=LoadInteger(Hash,id,StringHash("phase"))

    local real totalDist=LoadReal(Hash,id,StringHash("totalDist"))
    local real traveled=LoadReal(Hash,id,StringHash("traveled"))
    local real maxHeight=LoadReal(Hash,id,StringHash("maxHeight"))

    local real targetAngle
    local real angleDiff
    local real turnRate
    local real dist

    local real moveX
    local real moveY

    local boolean shouldExplode=false

    //
    // UPDATE TARGET POSITION
    //
    if target!=null and not IsUnitDead(target) then
        set tx=GetUnitX(target)
        set ty=GetUnitY(target)
    endif

    //
    // DISTANCE
    //
    set dist=SquareRoot((tx-x)*(tx-x)+(ty-y)*(ty-y))

    //
    // DEAD TARGET FAILSAFE
    //
    if target==null or IsUnitDead(target) then
        set phase=2
        set vz=-3.
    endif

    //
    // STEERING
    //
    if dist<=12. then

        set angle=Atan2(ty-y,tx-x)

    else

        set targetAngle=Atan2(ty-y,tx-x)

        set angleDiff=targetAngle-angle

        if angleDiff>bj_PI then
            set angleDiff=angleDiff-2.*bj_PI
        endif

        if angleDiff<-bj_PI then
            set angleDiff=angleDiff+2.*bj_PI
        endif

        //
        // TURN RATE
        //
        if phase==0 then
            set turnRate=0.004
        elseif phase==1 then
            set turnRate=0.015
        else
            set turnRate=0.040
        endif

        //
        // REDUCED CLOSE RANGE TURNING
        //
        if phase==2 and dist<=80. then
            set turnRate=0.030
        endif

        //
        // APPLY TURNING
        //
        if angleDiff>turnRate then
            set angle=angle+turnRate
        elseif angleDiff<-turnRate then
            set angle=angle-turnRate
        else
            set angle=targetAngle
        endif

    endif

    //
    // ==================================================
    // PHASE 0
    // LAUNCH
    // ==================================================
    //
    if phase==0 then

        call SetUnitVertexColor(missile,80,80,255,255)

        //
        // LOW HORIZONTAL DRIFT
        //
        if speed>1.6 then
            set speed=1.6
        endif

        //
        // ASCENT FORCE
        //
        set vz=vz-0.45

        //
        // PREVENT INSANE ALTITUDES
        //
        if vz<2.5 then
            set vz=2.5
        endif

        set traveled=traveled+speed

        //
        // TRANSITION
        //
        if traveled>=totalDist*0.18 or z>=maxHeight then

            set phase=1

            call MyUtils_echo("P0->P1 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))

        endif

    //
    // ==================================================
    // PHASE 1
    // GLIDE
    // ==================================================
    //
    elseif phase==1 then

        call SetUnitVertexColor(missile,80,255,80,255)

        //
        // HORIZONTAL ACCELERATION
        //
        set speed=speed+0.02

        if speed>4.5 then
            set speed=4.5
        endif

        //
        // SCALE GROWTH
        //
        if scale<2.20 then

            set scale=scale+0.15

            if scale>2.20 then
                set scale=2.20
            endif

            call SetUnitScale(missile,scale,scale,scale)

        endif

        //
        // CONTROLLED DESCENT
        //
        set vz=vz-0.05

        if vz<-1.5 then
            set vz=-1.5
        endif

        set traveled=traveled+speed

        //
        // TRANSITION TO DIVE
        //
        if traveled>=totalDist*0.35 then

            set phase=2

            set vz=-1.0

            call MyUtils_echo("P1->P2 z="+R2S(z)+" traveled="+R2S(traveled)+" dist="+R2S(dist))

        endif

    //
    // ==================================================
    // PHASE 2
    // DIVE
    // ==================================================
    //
    else

        call SetUnitVertexColor(missile,255,80,80,255)

        //
        // TERMINAL SLOWDOWN
        //
        if dist<=140. then
            set speed=speed*0.92
        endif

        //
        // ACCELERATION
        //
        set speed=speed+0.18

        if speed>24. then
            set speed=24.
        endif

        //
        // DIVE GRAVITY
        //
        set vz=vz-0.55

        if vz<-12. then
            set vz=-12.
        endif

        set traveled=traveled+speed

        //
        // DEBUG
        //
        if dist<=120. then

            call MyUtils_echo("FINAL dist="+R2S(dist)+" speed="+R2S(speed)+" z="+R2S(z))

        endif

    endif

    //
    // MOVEMENT
    //
    set moveX=speed*Cos(angle)
    set moveY=speed*Sin(angle)

    set x=x+moveX
    set y=y+moveY

    set z=z+vz

    //
    // IMPACT DETECTION
    //
    if phase!=2 then

        set shouldExplode=false

    elseif traveled>totalDist*1.20 then

        set shouldExplode=true

    elseif not RectContainsCoords(bj_mapInitialPlayableArea,x,y) then

        set shouldExplode=true

    elseif dist<=speed+24. and z<=120. then

        set shouldExplode=true

    endif

    //
    // IMPACT
    //
    if shouldExplode then

        call MyUtils_echo("HIT dist="+R2S(dist)+" speed="+R2S(speed)+" vz="+R2S(vz)+" z="+R2S(z))

        call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx",x,y))

        call GroupEnumUnitsInRange(SpellIndex.GLOBAL_GROUP,x,y,350.,null)

        loop

            set target=FirstOfGroup(SpellIndex.GLOBAL_GROUP)
            exitwhen target==null

            call GroupRemoveUnit(SpellIndex.GLOBAL_GROUP,target)

            if not IsUnitDead(target) and IsUnitEnemy(target,GetOwningPlayer(caster)) then

                call UnitDamageTarget(caster,target,300.,false,false,ATTACK_TYPE_NORMAL,DAMAGE_TYPE_MAGIC,null)

            endif

        endloop

        call RemoveUnit(missile)

        call FlushChildHashtable(Hash,id)

        call PauseTimer(t)
        call DestroyTimer(t)

        set missile=null
        set caster=null
        set target=null
        set t=null

        return

    endif

    //
    // APPLY POSITION
    //
    call SetUnitX(missile,x)
    call SetUnitY(missile,y)

    call SetUnitFlyHeight(missile,z,0.)

    call SetUnitFacing(missile,angle*bj_RADTODEG)

    //
    // SAVE STATE
    //
    call SaveReal(Hash,id,StringHash("x"),x)
    call SaveReal(Hash,id,StringHash("y"),y)
    call SaveReal(Hash,id,StringHash("z"),z)

    call SaveReal(Hash,id,StringHash("tx"),tx)
    call SaveReal(Hash,id,StringHash("ty"),ty)

    call SaveReal(Hash,id,StringHash("vz"),vz)
    call SaveReal(Hash,id,StringHash("speed"),speed)
    call SaveReal(Hash,id,StringHash("scale"),scale)
    call SaveReal(Hash,id,StringHash("angle"),angle)

    call SaveReal(Hash,id,StringHash("traveled"),traveled)

    call SaveInteger(Hash,id,StringHash("phase"),phase)

    set missile=null
    set caster=null
    set target=null
    set t=null

endfunction


The projectile is completely broken in testing. It doesn’t fly at all, it just slides along the ground in one fixed direction forever.
The projectile math itself turned out to be solid.
The ground-sliding issue was actually caused by map integration rather than the flight physics system itself.
To verify that, I rebuilt the missile from scratch in a completely isolated test map:
  • No external libraries
  • No Missile systems
  • No SpellIndex
  • No utility dependencies
  • Just a single JASS trigger and one dummy unit from the OE
The result is a standalone ballistic missile system with:
  • Steep vertical launch
  • Smooth high-arc ascent
  • Accelerating terminal dive
  • Target tracking during descent
  • Proper ground impact handling
  • No sliding or premature detonation behavior



One thing to note: because the missile uses live target tracking during the terminal dive, it can occasionally make a small directional correction right before impact if the target moves unexpectedly or the arc alignment becomes suboptimal. In practice this may appear as a slight “twitch” or snap toward the target immediately before detonation.

I'm attaching the full standalone script below.




JASS:
//===========================================================================
//  WATCHER MISSILE 
//  Two phases: ascent (constant drift) → terminal dive (accelerating toward target)
//===========================================================================

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(udg_Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(udg_Hash, id, StringHash("caster"))
    local unit target  = LoadUnitHandle(udg_Hash, id, StringHash("target"))

    local real x  = LoadReal(udg_Hash, id, StringHash("x"))
    local real y  = LoadReal(udg_Hash, id, StringHash("y"))
    local real z  = LoadReal(udg_Hash, id, StringHash("z"))

    local real vx = LoadReal(udg_Hash, id, StringHash("vx"))
    local real vy = LoadReal(udg_Hash, id, StringHash("vy"))
    local real vz = LoadReal(udg_Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle

    local integer phase = LoadInteger(udg_Hash, id, StringHash("phase"))

    // safety
    if missile == null then
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        // target dead maintain current horizontal direction
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // ==================== PHASE 0 : ASCENT ====================
    if phase == 0 then
         set vz = vz - 0.30
        // horizontal drift stays constant (vx, vy unchanged)

        call SetUnitVertexColor(missile, 100, 100, 255, 255)

        // reach apex → begin dive
        if vz <= 0.0 then
            set phase = 1
        endif

    // ==================== PHASE 1 : TERMINAL DIVE ====================
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif

        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif

        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    // ==================== MOVEMENT ====================
    set x = x + vx
    set y = y + vy
    set z = z + vz

    // ==================== IMPACT ====================
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(caster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: left the map
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // ==================== VISUALS ====================
    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    // ==================== SAVE ====================
    call SaveReal(udg_Hash, id, StringHash("x"), x)
    call SaveReal(udg_Hash, id, StringHash("y"), y)
    call SaveReal(udg_Hash, id, StringHash("z"), z)
    call SaveReal(udg_Hash, id, StringHash("vx"), vx)
    call SaveReal(udg_Hash, id, StringHash("vy"), vy)
    call SaveReal(udg_Hash, id, StringHash("vz"), vz)
    call SaveInteger(udg_Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
endfunction

//===========================================================================
//  CAST
//===========================================================================
function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
    local unit caster = GetTriggerUnit()
    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()

    local group g = CreateGroup()
    local unit target = null
    local unit missile

    local timer t
    local integer id
    local real angle

    if GetSpellAbilityId() != 'A01M' then
        set caster = null
        set g = null
        return
    endif

    // find enemy near target point
    call GroupEnumUnitsInRange(g, tx, ty, 900.0, null)
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDeadBJ(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
        set caster = null
        set g = null
        return
    endif

    // create missile dummy
    set missile = CreateUnit(GetOwningPlayer(caster), 'dumi', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(udg_Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(udg_Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(udg_Hash, id, StringHash("target"), target)

    call SaveReal(udg_Hash, id, StringHash("x"), sx)
    call SaveReal(udg_Hash, id, StringHash("y"), sy)
    call SaveReal(udg_Hash, id, StringHash("z"), 0.0)

    call SaveReal(udg_Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(udg_Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(udg_Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(udg_Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
    set g       = null
endfunction

//===========================================================================
//  INIT
//===========================================================================
function InitTrig_WatcherPhysicsTest takes nothing returns nothing
    local trigger trig = CreateTrigger()
    set udg_Hash = InitHashtable()
    call TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddAction(trig, function Trig_WatcherPhysicsTest_Actions)
    set trig = null
endfunction




 
The projectile math itself turned out to be solid.
The ground-sliding issue was actually caused by map integration rather than the flight physics system itself.
To verify that, I rebuilt the missile from scratch in a completely isolated test map:
  • No external libraries
  • No Missile systems
  • No SpellIndex
  • No utility dependencies
  • Just a single JASS trigger and one dummy unit from the OE
The result is a standalone ballistic missile system with:
  • Steep vertical launch
  • Smooth high-arc ascent
  • Accelerating terminal dive
  • Target tracking during descent
  • Proper ground impact handling
  • No sliding or premature detonation behavior



One thing to note: because the missile uses live target tracking during the terminal dive, it can occasionally make a small directional correction right before impact if the target moves unexpectedly or the arc alignment becomes suboptimal. In practice this may appear as a slight “twitch” or snap toward the target immediately before detonation.

I'm attaching the full standalone script below.




JASS:
//===========================================================================
//  WATCHER MISSILE
//  Two phases: ascent (constant drift) → terminal dive (accelerating toward target)
//===========================================================================

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(udg_Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(udg_Hash, id, StringHash("caster"))
    local unit target  = LoadUnitHandle(udg_Hash, id, StringHash("target"))

    local real x  = LoadReal(udg_Hash, id, StringHash("x"))
    local real y  = LoadReal(udg_Hash, id, StringHash("y"))
    local real z  = LoadReal(udg_Hash, id, StringHash("z"))

    local real vx = LoadReal(udg_Hash, id, StringHash("vx"))
    local real vy = LoadReal(udg_Hash, id, StringHash("vy"))
    local real vz = LoadReal(udg_Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle

    local integer phase = LoadInteger(udg_Hash, id, StringHash("phase"))

    // safety
    if missile == null then
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        // target dead maintain current horizontal direction
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // ==================== PHASE 0 : ASCENT ====================
    if phase == 0 then
         set vz = vz - 0.30
        // horizontal drift stays constant (vx, vy unchanged)

        call SetUnitVertexColor(missile, 100, 100, 255, 255)

        // reach apex → begin dive
        if vz <= 0.0 then
            set phase = 1
        endif

    // ==================== PHASE 1 : TERMINAL DIVE ====================
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif

        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif

        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    // ==================== MOVEMENT ====================
    set x = x + vx
    set y = y + vy
    set z = z + vz

    // ==================== IMPACT ====================
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(caster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: left the map
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // ==================== VISUALS ====================
    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    // ==================== SAVE ====================
    call SaveReal(udg_Hash, id, StringHash("x"), x)
    call SaveReal(udg_Hash, id, StringHash("y"), y)
    call SaveReal(udg_Hash, id, StringHash("z"), z)
    call SaveReal(udg_Hash, id, StringHash("vx"), vx)
    call SaveReal(udg_Hash, id, StringHash("vy"), vy)
    call SaveReal(udg_Hash, id, StringHash("vz"), vz)
    call SaveInteger(udg_Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
endfunction

//===========================================================================
//  CAST
//===========================================================================
function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
    local unit caster = GetTriggerUnit()
    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()

    local group g = CreateGroup()
    local unit target = null
    local unit missile

    local timer t
    local integer id
    local real angle

    if GetSpellAbilityId() != 'A01M' then
        set caster = null
        set g = null
        return
    endif

    // find enemy near target point
    call GroupEnumUnitsInRange(g, tx, ty, 900.0, null)
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDeadBJ(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
        set caster = null
        set g = null
        return
    endif

    // create missile dummy
    set missile = CreateUnit(GetOwningPlayer(caster), 'dumi', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(udg_Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(udg_Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(udg_Hash, id, StringHash("target"), target)

    call SaveReal(udg_Hash, id, StringHash("x"), sx)
    call SaveReal(udg_Hash, id, StringHash("y"), sy)
    call SaveReal(udg_Hash, id, StringHash("z"), 0.0)

    call SaveReal(udg_Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(udg_Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(udg_Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(udg_Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
    set g       = null
endfunction

//===========================================================================
//  INIT
//===========================================================================
function InitTrig_WatcherPhysicsTest takes nothing returns nothing
    local trigger trig = CreateTrigger()
    set udg_Hash = InitHashtable()
    call TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddAction(trig, function Trig_WatcherPhysicsTest_Actions)
    set trig = null
endfunction




View attachment 588377
Woah man solid work gotta say. Im dunno what do you mean by "map integration", but I guess you mean some system on my demo map may affect this missile spell?

The projectile math itself turned out to be solid.
The ground-sliding issue was actually caused by map integration rather than the flight physics system itself.
To verify that, I rebuilt the missile from scratch in a completely isolated test map:
  • No external libraries
  • No Missile systems
  • No SpellIndex
  • No utility dependencies
  • Just a single JASS trigger and one dummy unit from the OE
The result is a standalone ballistic missile system with:
  • Steep vertical launch
  • Smooth high-arc ascent
  • Accelerating terminal dive
  • Target tracking during descent
  • Proper ground impact handling
  • No sliding or premature detonation behavior



One thing to note: because the missile uses live target tracking during the terminal dive, it can occasionally make a small directional correction right before impact if the target moves unexpectedly or the arc alignment becomes suboptimal. In practice this may appear as a slight “twitch” or snap toward the target immediately before detonation.

I'm attaching the full standalone script below.




JASS:
//===========================================================================
//  WATCHER MISSILE
//  Two phases: ascent (constant drift) → terminal dive (accelerating toward target)
//===========================================================================

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(udg_Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(udg_Hash, id, StringHash("caster"))
    local unit target  = LoadUnitHandle(udg_Hash, id, StringHash("target"))

    local real x  = LoadReal(udg_Hash, id, StringHash("x"))
    local real y  = LoadReal(udg_Hash, id, StringHash("y"))
    local real z  = LoadReal(udg_Hash, id, StringHash("z"))

    local real vx = LoadReal(udg_Hash, id, StringHash("vx"))
    local real vy = LoadReal(udg_Hash, id, StringHash("vy"))
    local real vz = LoadReal(udg_Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle

    local integer phase = LoadInteger(udg_Hash, id, StringHash("phase"))

    // safety
    if missile == null then
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        // target dead maintain current horizontal direction
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // ==================== PHASE 0 : ASCENT ====================
    if phase == 0 then
         set vz = vz - 0.30
        // horizontal drift stays constant (vx, vy unchanged)

        call SetUnitVertexColor(missile, 100, 100, 255, 255)

        // reach apex → begin dive
        if vz <= 0.0 then
            set phase = 1
        endif

    // ==================== PHASE 1 : TERMINAL DIVE ====================
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif

        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif

        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    // ==================== MOVEMENT ====================
    set x = x + vx
    set y = y + vy
    set z = z + vz

    // ==================== IMPACT ====================
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(caster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: left the map
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // ==================== VISUALS ====================
    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    // ==================== SAVE ====================
    call SaveReal(udg_Hash, id, StringHash("x"), x)
    call SaveReal(udg_Hash, id, StringHash("y"), y)
    call SaveReal(udg_Hash, id, StringHash("z"), z)
    call SaveReal(udg_Hash, id, StringHash("vx"), vx)
    call SaveReal(udg_Hash, id, StringHash("vy"), vy)
    call SaveReal(udg_Hash, id, StringHash("vz"), vz)
    call SaveInteger(udg_Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
endfunction

//===========================================================================
//  CAST
//===========================================================================
function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
    local unit caster = GetTriggerUnit()
    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()

    local group g = CreateGroup()
    local unit target = null
    local unit missile

    local timer t
    local integer id
    local real angle

    if GetSpellAbilityId() != 'A01M' then
        set caster = null
        set g = null
        return
    endif

    // find enemy near target point
    call GroupEnumUnitsInRange(g, tx, ty, 900.0, null)
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDeadBJ(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
        set caster = null
        set g = null
        return
    endif

    // create missile dummy
    set missile = CreateUnit(GetOwningPlayer(caster), 'dumi', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(udg_Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(udg_Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(udg_Hash, id, StringHash("target"), target)

    call SaveReal(udg_Hash, id, StringHash("x"), sx)
    call SaveReal(udg_Hash, id, StringHash("y"), sy)
    call SaveReal(udg_Hash, id, StringHash("z"), 0.0)

    call SaveReal(udg_Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(udg_Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(udg_Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(udg_Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
    set g       = null
endfunction

//===========================================================================
//  INIT
//===========================================================================
function InitTrig_WatcherPhysicsTest takes nothing returns nothing
    local trigger trig = CreateTrigger()
    set udg_Hash = InitHashtable()
    call TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddAction(trig, function Trig_WatcherPhysicsTest_Actions)
    set trig = null
endfunction




View attachment 588377
I tried to make myself an isolated map using your code to make it finally work. WTF still the same results. Is the reason that Im using 1.26 warcraft or what? :goblin_wtf:

JASS:
globals
hashtable Hash = InitHashtable()
integer LVL = 0
endglobals

//****************************универсальная проверка жив ли юнит***************************************
function IsUnitDead takes unit u returns boolean
return GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD)
endfunction 


function UnitTypeNotDummy takes unit ui returns boolean
if GetUnitAbilityLevel(ui,'Aloc') > 0 then
return false //that means it's dummy 
endif
return true
endfunction
//===========================================================================
//  WATCHER MISSILE 
//  Two phases: ascent (constant drift) > terminal dive (accelerating toward target)
//===========================================================================

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(Hash, id, StringHash("caster"))
    local unit target  = LoadUnitHandle(Hash, id, StringHash("target"))

    local real x  = LoadReal(Hash, id, StringHash("x"))
    local real y  = LoadReal(Hash, id, StringHash("y"))
    local real z  = LoadReal(Hash, id, StringHash("z"))

    local real vx = LoadReal(Hash, id, StringHash("vx"))
    local real vy = LoadReal(Hash, id, StringHash("vy"))
    local real vz = LoadReal(Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle

    local integer phase = LoadInteger(Hash, id, StringHash("phase"))

    // safety
    if missile == null then
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        // target dead maintain current horizontal direction
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // ==================== PHASE 0 : ASCENT ====================
    if phase == 0 then
         set vz = vz - 0.30
        // horizontal drift stays constant (vx, vy unchanged)

        call SetUnitVertexColor(missile, 100, 100, 255, 255)

        // reach apex > begin dive
        if vz <= 0.0 then
            set phase = 1
        endif

    // ==================== PHASE 1 : TERMINAL DIVE ====================
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif

        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif

        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    // ==================== MOVEMENT ====================
    set x = x + vx
    set y = y + vy
    set z = z + vz

    // ==================== IMPACT ====================
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(caster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: left the map
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // ==================== VISUALS ====================
    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    // ==================== SAVE ====================
    call SaveReal(Hash, id, StringHash("x"), x)
    call SaveReal(Hash, id, StringHash("y"), y)
    call SaveReal(Hash, id, StringHash("z"), z)
    call SaveReal(Hash, id, StringHash("vx"), vx)
    call SaveReal(Hash, id, StringHash("vy"), vy)
    call SaveReal(Hash, id, StringHash("vz"), vz)
    call SaveInteger(Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
endfunction

//===========================================================================
//  CAST
//===========================================================================





function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
local timer t2=GetExpiredTimer()
    local integer idT=GetHandleId(t2)

    local unit caster =LoadUnitHandle(Hash,idT,0) //wt
    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()
    
    local real r=LoadReal(Hash,idT,2)-1.

    local group g = CreateGroup()
    local unit target = null
    local unit missile

    local timer t
    local integer id
    local real angle

   /* if GetSpellAbilityId() != 'A01M' then
        set caster = null
        set g = null
        return
    endif */
    
    if IsUnitDead(caster) then
    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t2)
    return 
    endif

    // find enemy near target point
    call GroupEnumUnitsInRange(g, sx, sy, 900.0, null) //tx ty
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDead(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t2)
        set caster = null
        set g = null
        return
    endif

    // create missile dummy
    set missile = CreateUnit(GetOwningPlayer(caster), 'h000', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(Hash, id, StringHash("target"), target)

    call SaveReal(Hash, id, StringHash("x"), sx)
    call SaveReal(Hash, id, StringHash("y"), sy)
    call SaveReal(Hash, id, StringHash("z"), 0.0)

    call SaveReal(Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)
    
    if r<=0. then

        call FlushChildHashtable(Hash,idT)

        call PauseTimer(t2)
        call DestroyTimer(t2)

    else

        call SaveReal(Hash,idT,2,r)

    endif

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
    set g       = null
endfunction


function WatcherAnimReset takes nothing returns nothing

local timer t = GetExpiredTimer()
local integer idT = GetHandleId(t)


call SetUnitAnimation(LoadUnitHandle(Hash,idT,0),"stand")
call FlushChildHashtable(Hash,idT)
call DestroyTimer(t)
set t=null

endfunction




function Trig_creepspellscast_Actions takes nothing returns nothing
local unit c = GetSpellAbilityUnit()
    local unit tr 
    local real r
    local real r2
    local timer t
    local integer idT
local integer spellid = GetSpellAbilityId()
local integer gold
local integer id
local integer i2
local real x
local real y
local unit f 
local group g 
//call echo("spell check1")
if UnitTypeNotDummy(c) then
    
    if spellid == 'A000' then
    set x = GetSpellTargetX()
    set y = GetSpellTargetY()
    //set LVL = GetUnitAbilityLevel(c,'A01I')
    set LVL = 1 
    set f = CreateUnit(GetOwningPlayer(c),'e000',x,y,0)
    if LVL == 1 then
    call SetUnitScale(f,1,1,1)
    call UnitApplyTimedLife(f,'BTLF',8.)
    elseif LVL == 2 then 
    call SetUnitScale(f,1.,1.,1.)
    call UnitApplyTimedLife(f,'BTLF',8.)
    elseif LVL == 3 then
    call SetUnitScale(f,1.2,1.2,1.2)
    call UnitApplyTimedLife(f,'BTLF',8.)
    
    endif
    
    call SetUnitFlyHeight(f, -100.00, 0.00)
    call SetUnitVertexColor(f,255,255,255,40)
    call SetUnitAnimation(f,"birth")
    set t= CreateTimer()
    call SaveUnitHandle(Hash,GetHandleId(t),0,f)
    call TimerStart(t,0.7,false,function WatcherAnimReset)
    
    
    set t=CreateTimer()
    set idT =GetHandleId(t)
    call SaveUnitHandle(Hash,idT,0,f)
    call SaveUnitHandle(Hash,idT,1,c)
    call SaveReal(Hash,idT,2,12.)
    call TimerStart(t,1.0,true,function Trig_WatcherPhysicsTest_Actions)
    //call GroupAddUnit(AppearingGroup, f )
    
    //call SaveUnitHandle(Hash,GetHandleId(c),StringHash("FishGeyser"),f)
    //call SaveInteger(Hash,GetHandleId(c),StringHash("FishGeyserLVL"),LVL)
    
    endif
    endif
set c=null
set tr=null
set t=null
set f=null
set g=null
endfunction

//===========================================================================
function InitTrig_creepspellscast takes nothing returns nothing



set gg_trg_creepspellscast = CreateTrigger(  )
    
    
    call TriggerRegisterPlayerUnitEvent(gg_trg_creepspellscast, Player(0), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)

    
    call TriggerAddAction( gg_trg_creepspellscast, function Trig_creepspellscast_Actions )

endfunction
 

Attachments

Last edited:
Woah man solid work gotta say. Im dunno what do you mean by "map integration", but I guess you mean some system on my demo map may affect this missile spell?


I tried to make myself an isolated map using your code to make it finally work. WTF still the same results. Is the reason that Im using 1.26 warcraft or what? :goblin_wtf:

JASS:
globals
hashtable Hash = InitHashtable()
integer LVL = 0
endglobals

//****************************универсальная проверка жив ли юнит***************************************
function IsUnitDead takes unit u returns boolean
return GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD)
endfunction


function UnitTypeNotDummy takes unit ui returns boolean
if GetUnitAbilityLevel(ui,'Aloc') > 0 then
return false //that means it's dummy
endif
return true
endfunction
//===========================================================================
//  WATCHER MISSILE
//  Two phases: ascent (constant drift) > terminal dive (accelerating toward target)
//===========================================================================

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(Hash, id, StringHash("caster"))
    local unit target  = LoadUnitHandle(Hash, id, StringHash("target"))

    local real x  = LoadReal(Hash, id, StringHash("x"))
    local real y  = LoadReal(Hash, id, StringHash("y"))
    local real z  = LoadReal(Hash, id, StringHash("z"))

    local real vx = LoadReal(Hash, id, StringHash("vx"))
    local real vy = LoadReal(Hash, id, StringHash("vy"))
    local real vz = LoadReal(Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle

    local integer phase = LoadInteger(Hash, id, StringHash("phase"))

    // safety
    if missile == null then
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        // target dead maintain current horizontal direction
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // ==================== PHASE 0 : ASCENT ====================
    if phase == 0 then
         set vz = vz - 0.30
        // horizontal drift stays constant (vx, vy unchanged)

        call SetUnitVertexColor(missile, 100, 100, 255, 255)

        // reach apex > begin dive
        if vz <= 0.0 then
            set phase = 1
        endif

    // ==================== PHASE 1 : TERMINAL DIVE ====================
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif

        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif

        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    // ==================== MOVEMENT ====================
    set x = x + vx
    set y = y + vy
    set z = z + vz

    // ==================== IMPACT ====================
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(caster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: left the map
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // ==================== VISUALS ====================
    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    // ==================== SAVE ====================
    call SaveReal(Hash, id, StringHash("x"), x)
    call SaveReal(Hash, id, StringHash("y"), y)
    call SaveReal(Hash, id, StringHash("z"), z)
    call SaveReal(Hash, id, StringHash("vx"), vx)
    call SaveReal(Hash, id, StringHash("vy"), vy)
    call SaveReal(Hash, id, StringHash("vz"), vz)
    call SaveInteger(Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
endfunction

//===========================================================================
//  CAST
//===========================================================================





function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
local timer t2=GetExpiredTimer()
    local integer idT=GetHandleId(t2)

    local unit caster =LoadUnitHandle(Hash,idT,0) //wt
    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()
 
    local real r=LoadReal(Hash,idT,2)-1.

    local group g = CreateGroup()
    local unit target = null
    local unit missile

    local timer t
    local integer id
    local real angle

   /* if GetSpellAbilityId() != 'A01M' then
        set caster = null
        set g = null
        return
    endif */
 
    if IsUnitDead(caster) then
    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t2)
    return
    endif

    // find enemy near target point
    call GroupEnumUnitsInRange(g, sx, sy, 900.0, null) //tx ty
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDead(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t2)
        set caster = null
        set g = null
        return
    endif

    // create missile dummy
    set missile = CreateUnit(GetOwningPlayer(caster), 'h000', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(Hash, id, StringHash("target"), target)

    call SaveReal(Hash, id, StringHash("x"), sx)
    call SaveReal(Hash, id, StringHash("y"), sy)
    call SaveReal(Hash, id, StringHash("z"), 0.0)

    call SaveReal(Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)
 
    if r<=0. then

        call FlushChildHashtable(Hash,idT)

        call PauseTimer(t2)
        call DestroyTimer(t2)

    else

        call SaveReal(Hash,idT,2,r)

    endif

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
    set g       = null
endfunction


function WatcherAnimReset takes nothing returns nothing

local timer t = GetExpiredTimer()
local integer idT = GetHandleId(t)


call SetUnitAnimation(LoadUnitHandle(Hash,idT,0),"stand")
call FlushChildHashtable(Hash,idT)
call DestroyTimer(t)
set t=null

endfunction




function Trig_creepspellscast_Actions takes nothing returns nothing
local unit c = GetSpellAbilityUnit()
    local unit tr
    local real r
    local real r2
    local timer t
    local integer idT
local integer spellid = GetSpellAbilityId()
local integer gold
local integer id
local integer i2
local real x
local real y
local unit f
local group g
//call echo("spell check1")
if UnitTypeNotDummy(c) then
 
    if spellid == 'A000' then
    set x = GetSpellTargetX()
    set y = GetSpellTargetY()
    //set LVL = GetUnitAbilityLevel(c,'A01I')
    set LVL = 1
    set f = CreateUnit(GetOwningPlayer(c),'e000',x,y,0)
    if LVL == 1 then
    call SetUnitScale(f,1,1,1)
    call UnitApplyTimedLife(f,'BTLF',8.)
    elseif LVL == 2 then
    call SetUnitScale(f,1.,1.,1.)
    call UnitApplyTimedLife(f,'BTLF',8.)
    elseif LVL == 3 then
    call SetUnitScale(f,1.2,1.2,1.2)
    call UnitApplyTimedLife(f,'BTLF',8.)
 
    endif
 
    call SetUnitFlyHeight(f, -100.00, 0.00)
    call SetUnitVertexColor(f,255,255,255,40)
    call SetUnitAnimation(f,"birth")
    set t= CreateTimer()
    call SaveUnitHandle(Hash,GetHandleId(t),0,f)
    call TimerStart(t,0.7,false,function WatcherAnimReset)
 
 
    set t=CreateTimer()
    set idT =GetHandleId(t)
    call SaveUnitHandle(Hash,idT,0,f)
    call SaveUnitHandle(Hash,idT,1,c)
    call SaveReal(Hash,idT,2,12.)
    call TimerStart(t,1.0,true,function Trig_WatcherPhysicsTest_Actions)
    //call GroupAddUnit(AppearingGroup, f )
 
    //call SaveUnitHandle(Hash,GetHandleId(c),StringHash("FishGeyser"),f)
    //call SaveInteger(Hash,GetHandleId(c),StringHash("FishGeyserLVL"),LVL)
 
    endif
    endif
set c=null
set tr=null
set t=null
set f=null
set g=null
endfunction

//===========================================================================
function InitTrig_creepspellscast takes nothing returns nothing



set gg_trg_creepspellscast = CreateTrigger(  )
 
 
    call TriggerRegisterPlayerUnitEvent(gg_trg_creepspellscast, Player(0), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)

 
    call TriggerAddAction( gg_trg_creepspellscast, function Trig_creepspellscast_Actions )

endfunction



I changed the dummy movement speed from 0.00 to 1.00 and it immediately started flying lol.


Now I'm genuinely curious how the engine internally resolves units with 0.00 movement speed. I wonder if the engine effectively treats them like immobile objects/buildings for certain pathing, interpolation, or facing updates, even when the projectile is being moved entirely through SetUnitX/Y physics.



Also noticed another thing while testing:

In your Trig_creepspellscast_Actions trigger, the dummy spawner (f) receives an 8-second timed life:

call UnitApplyTimedLife(f,'BTLF',8.)
But the missile timer is configured to fire 12 times, once per second.



That means after 8 seconds the spawner fully decays and gets removed, while missiles 9–12 can still be active. When those missiles land, the explosion code tries to deal damage using the spawner as the damage source — except the spawner no longer exists, so the engine safely resolves the damage as 0 instead of risking invalid references.



The clean solution is simply to use the real caster as the damage source instead of the temporary spawner dummy; the code already stores the real caster in the hashtable, so the setup is basically already there.



Still really interested in the 0.00 movement speed behavior though, because the projectile itself never relied on native unit movement at all.
 
Last edited:



I changed the dummy movement speed from 0.00 to 1.00 and it immediately started flying lol.


Now I'm genuinely curious how the engine internally resolves units with 0.00 movement speed. I wonder if the engine effectively treats them like immobile objects/buildings for certain pathing, interpolation, or facing updates, even when the projectile is being moved entirely through SetUnitX/Y physics.



Also noticed another thing while testing:

In your Trig_creepspellscast_Actions trigger, the dummy spawner (f) receives an 8-second timed life:

call UnitApplyTimedLife(f,'BTLF',8.)
But the missile timer is configured to fire 12 times, once per second.



That means after 8 seconds the spawner fully decays and gets removed, while missiles 9–12 can still be active. When those missiles land, the explosion code tries to deal damage using the spawner as the damage source — except the spawner no longer exists, so the engine safely resolves the damage as 0 instead of risking invalid references.



The clean solution is simply to use the real caster as the damage source instead of the temporary spawner dummy; the code already stores the real caster in the hashtable, so the setup is basically already there.



Still really interested in the 0.00 movement speed behavior though, because the projectile itself never relied on native unit movement at all.
Now it works even in my original demo. God damn this dummy units. Thank you very much. I almost got crazy in the process of making this spell work, sitting all night trying to make it work (hope my brain cells are not hurt because they might be lol).

Solved. Dont wanna waste any more of your time.
 
Now it works even in my original demo. God damn this dummy units. Thank you very much. I almost got crazy in the process of making this spell work, sitting all night trying to make it work (hope my brain cells are not hurt because they might be lol).

Solved. Dont wanna waste any more of your time.
Honestly, no problem, I'm glad I was helpful, I'm just in shock by the 1.00 movement speed thing, get some sleep lol. :ugly:
 
Hi, lurker here - so is the final solution to take what's in this post and change...something from 0.00 to 1.00?
and to change call UnitApplyTimedLife(f,'BTLF',8.) from 8 to 12?

Just wondering what the final code is and if there's a final demo map. I tried out the one here but it didn't seem to do anything with missiles.
 
Hi, lurker here - so is the final solution to take what's in this post and change...something from 0.00 to 1.00?
and to change call UnitApplyTimedLife(f,'BTLF',8.) from 8 to 12?

Just wondering what the final code is and if there's a final demo map. I tried out the one here but it didn't seem to do anything with missiles.
Do you wish the missiles to be spawned from the hero or from the target point of ability being cast ?
 
Hi, lurker here - so is the final solution to take what's in this post and change...something from 0.00 to 1.00?
and to change call UnitApplyTimedLife(f,'BTLF',8.) from 8 to 12?

Just wondering what the final code is and if there's a final demo map. I tried out the one here but it didn't seem to do anything with missiles.
This demo should work nice and smooth.

It spawns a visually seen dummy unit that just last for 12 secs (thats why I changed 8 to 12, since timer loop last for 12 secs)

That "launches" from its x y missiles into the air that eventually aim for the random unit in range. 1 missiles every 1 secs for 12 secs.

The fix that was vitale is changing the "missile dummy" ('h000') movement speed from 0 to 1, since wc3 is kinda strange, I dunno but it now works.
 

Attachments

This demo should work nice and smooth.

It spawns a visually seen dummy unit that just last for 12 secs (thats why I changed 8 to 12, since timer loop last for 12 secs)

That "launches" from its x y missiles into the air that eventually aim for the random unit in range. 1 missiles every 1 secs for 12 secs.

The fix that was vitale is changing the "missile dummy" ('h000') movement speed from 0 to 1, since wc3 is kinda strange, I dunno but it now works.
For some reason, my editor (1.27b) crashes every time I try to open this map 😭. Is there a version requirement? I'm able to open and run the other one I linked before 🤔


Do you wish the missiles to be spawned from the hero or from the target point of ability being cast ?
Oh, I'd assumed they'd be spawned/launched from a unit/hero, but I would be curious to look at anything that works.
 
For some reason, my editor (1.27b) crashes every time I try to open this map 😭. Is there a version requirement? I'm able to open and run the other one I linked before 🤔



Oh, I'd assumed they'd be spawned/launched from a unit/hero, but I would be curious to look at anything that works.
it's 1.26. I can post the code
JASS:
globals
hashtable Hash = InitHashtable()
integer LVL = 0
endglobals

//****************************универсальная проверка жив ли юнит***************************************
function IsUnitDead takes unit u returns boolean
return GetUnitTypeId(u) == 0 or IsUnitType(u, UNIT_TYPE_DEAD)
endfunction 


function UnitTypeNotDummy takes unit ui returns boolean
if GetUnitAbilityLevel(ui,'Aloc') > 0 then
return false //that means it's dummy 
endif
return true
endfunction
//===========================================================================
//  WATCHER MISSILE 
//  Two phases: ascent (constant drift) > terminal dive (accelerating toward target)
//===========================================================================

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("war3mapImported\\Tidal Burst - Classic.mdx", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(Hash, id, StringHash("caster"))
    local unit realcaster  = LoadUnitHandle(Hash, id, StringHash("realcaster"))
    local unit target  = LoadUnitHandle(Hash, id, StringHash("target"))

    local real x  = LoadReal(Hash, id, StringHash("x"))
    local real y  = LoadReal(Hash, id, StringHash("y"))
    local real z  = LoadReal(Hash, id, StringHash("z"))

    local real vx = LoadReal(Hash, id, StringHash("vx"))
    local real vy = LoadReal(Hash, id, StringHash("vy"))
    local real vz = LoadReal(Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle

    local integer phase = LoadInteger(Hash, id, StringHash("phase"))

    // safety
    if missile == null then
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        // target dead maintain current horizontal direction
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // ==================== PHASE 0 : ASCENT ====================
    if phase == 0 then
         set vz = vz - 0.30
        // horizontal drift stays constant (vx, vy unchanged)

        call SetUnitVertexColor(missile, 100, 100, 255, 255)

        // reach apex > begin dive
        if vz <= 0.0 then
            set phase = 1
        endif

    // ==================== PHASE 1 : TERMINAL DIVE ====================
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif

        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif

        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    // ==================== MOVEMENT ====================
    set x = x + vx
    set y = y + vy
    set z = z + vz

    // ==================== IMPACT ====================
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(realcaster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: left the map
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // ==================== VISUALS ====================
    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    // ==================== SAVE ====================
    call SaveReal(Hash, id, StringHash("x"), x)
    call SaveReal(Hash, id, StringHash("y"), y)
    call SaveReal(Hash, id, StringHash("z"), z)
    call SaveReal(Hash, id, StringHash("vx"), vx)
    call SaveReal(Hash, id, StringHash("vy"), vy)
    call SaveReal(Hash, id, StringHash("vz"), vz)
    call SaveInteger(Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
    set realcaster = null
endfunction

//===========================================================================
//  CAST
//===========================================================================





function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
local timer t2=GetExpiredTimer()
    local integer idT=GetHandleId(t2)

    local unit caster =LoadUnitHandle(Hash,idT,0) //is actually a dummy unit that spawns missiles into the air
    local unit realcaster = LoadUnitHandle(Hash,idT,1) // is our caster
    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()
    
    local real r=LoadReal(Hash,idT,2)-1.

    local group g = CreateGroup()
    local unit target = null
    local unit missile

    local timer t
    local integer id
    local real angle

   /* if GetSpellAbilityId() != 'A01M' then
        set caster = null
        set g = null
        return
    endif */
    
    if IsUnitDead(caster) then
    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t2)
    return 
    endif

    // find enemy near target point
    call GroupEnumUnitsInRange(g, sx, sy, 900.0, null) //tx ty
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDead(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
    call FlushChildHashtable(Hash,idT)
    call DestroyTimer(t2)
        set caster = null
        set g = null
        return
    endif

    // create missile dummy
    set missile = CreateUnit(GetOwningPlayer(caster), 'h000', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(Hash, id, StringHash("realcaster"), realcaster)
    call SaveUnitHandle(Hash, id, StringHash("target"), target)

    call SaveReal(Hash, id, StringHash("x"), sx)
    call SaveReal(Hash, id, StringHash("y"), sy)
    call SaveReal(Hash, id, StringHash("z"), 0.0)

    call SaveReal(Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)
    
    if r<=0. then

        call FlushChildHashtable(Hash,idT)

        call PauseTimer(t2)
        call DestroyTimer(t2)

    else

        call SaveReal(Hash,idT,2,r)

    endif

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
    set realcaster = null
    set g       = null
endfunction


function WatcherAnimReset takes nothing returns nothing

local timer t = GetExpiredTimer()
local integer idT = GetHandleId(t)


call SetUnitAnimation(LoadUnitHandle(Hash,idT,0),"stand")
call FlushChildHashtable(Hash,idT)
call DestroyTimer(t)
set t=null

endfunction




function Trig_creepspellscast_Actions takes nothing returns nothing
local unit c = GetSpellAbilityUnit()
    local unit tr 
    local real r
    local real r2
    local timer t
    local integer idT
local integer spellid = GetSpellAbilityId()
local integer gold
local integer id
local integer i2
local real x
local real y
local unit f 
local group g 
//call echo("spell check1")
if UnitTypeNotDummy(c) then
    
    if spellid == 'A000' then
    set x = GetSpellTargetX()
    set y = GetSpellTargetY()
    //set LVL = GetUnitAbilityLevel(c,'A01I')
    set LVL = 1 
    set f = CreateUnit(GetOwningPlayer(c),'e000',x,y,0)
    if LVL == 1 then
    call SetUnitScale(f,1,1,1)
    call UnitApplyTimedLife(f,'BTLF',12.)
    elseif LVL == 2 then 
    call SetUnitScale(f,1.,1.,1.)
    call UnitApplyTimedLife(f,'BTLF',12.)
    elseif LVL == 3 then
    call SetUnitScale(f,1.2,1.2,1.2)
    call UnitApplyTimedLife(f,'BTLF',12.)
    
    endif
    
    call SetUnitFlyHeight(f, -100.00, 0.00)
    call SetUnitVertexColor(f,255,255,255,40)
    call SetUnitAnimation(f,"birth")
    set t= CreateTimer()
    call SaveUnitHandle(Hash,GetHandleId(t),0,f)
    call TimerStart(t,0.7,false,function WatcherAnimReset)
    
    
    set t=CreateTimer()
    set idT =GetHandleId(t)
    call SaveUnitHandle(Hash,idT,0,f)
    call SaveUnitHandle(Hash,idT,1,c)
    call SaveReal(Hash,idT,2,12.)
    call TimerStart(t,1.0,true,function Trig_WatcherPhysicsTest_Actions)
    //call GroupAddUnit(AppearingGroup, f )
    
    //call SaveUnitHandle(Hash,GetHandleId(c),StringHash("FishGeyser"),f)
    //call SaveInteger(Hash,GetHandleId(c),StringHash("FishGeyserLVL"),LVL)
    
    endif
    endif
set c=null
set tr=null
set t=null
set f=null
set g=null
endfunction

//===========================================================================
function InitTrig_creepspellscast takes nothing returns nothing



set gg_trg_creepspellscast = CreateTrigger(  )
    
    
    call TriggerRegisterPlayerUnitEvent(gg_trg_creepspellscast, Player(0), EVENT_PLAYER_UNIT_SPELL_EFFECT, null)

    
    call TriggerAddAction( gg_trg_creepspellscast, function Trig_creepspellscast_Actions )

endfunction

function InitTrig_creepspellscast is the simple spell cast trigger register
function Trig_creepspellscast_Actions is the action (it does not check for spell id, so give spell id check if needed)
function WatcherAnimReset is just resetting the anim, not used in the demo
function Trig_WatcherPhysicsTest_Actions is timer loop
function ExplodeMissile is effect upon explosion + damage

unit under id 'h000' is the simple blank dummy missile, use anything you had before (can take from any missile spell on hiveworkshop). Just make sure movement speed is 1, not 0, and it has some special effect model in object editor
unit under id 'e000' can be anything u want, it just spawns on x y spell location, and serves only as cosmetic thing (lasts for 12 secs, just like the timer loop)


Use any hashtable you have, just replace "Hash" with the one on your map. Or leave the declaration just like at the top of the code.
Same for LVL integer
 
For some reason, my editor (1.27b) crashes every time I try to open this map 😭. Is there a version requirement? I'm able to open and run the other one I linked before 🤔



Oh, I'd assumed they'd be spawned/launched from a unit/hero, but I would be curious to look at anything that works.
Here is the version where the missiles launch from the caster's position and dynamically redirect if their current target dies.



JASS:
//===========================================================================
//  WATCHER MISSILE  BALLISTIC by the.Axolotl
//  12-missile barrage from the caster
//  DYNAMIC RETARGETING: missile will acquire a new target if the original dies
//===========================================================================

globals
    constant real RETARGET_RANGE = 1500.0   // how far to search for a new target
endglobals

//===========================================================================
//  EXPLOSION
//===========================================================================
function ExplodeMissile takes unit caster, real x, real y returns nothing
    local unit u
    local group g = CreateGroup()

    call DestroyEffect(AddSpecialEffect("Objects\\Spawnmodels\\Undead\\ImpaleTargetDust\\ImpaleTargetDust.mdl", x, y))

    call GroupEnumUnitsInRange(g, x, y, 325.0, null)
    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        call GroupRemoveUnit(g, u)
        if IsUnitEnemy(u, GetOwningPlayer(caster)) and not IsUnitType(u, UNIT_TYPE_STRUCTURE) and not IsUnitDeadBJ(u) then
            call UnitDamageTarget(caster, u, 400.0, false, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_FIRE, null)
        endif
    endloop

    call DestroyGroup(g)
    set g = null
    set u = null
endfunction

//===========================================================================
//  PERIODIC PHYSICS (WITH DYNAMIC RETARGETING)
//===========================================================================
function WatcherMissileLoop takes nothing returns nothing
    local timer t   = GetExpiredTimer()
    local integer id = GetHandleId(t)

    local unit missile = LoadUnitHandle(udg_Hash, id, StringHash("missile"))
    local unit caster  = LoadUnitHandle(udg_Hash, id, StringHash("caster"))
    local unit target  = LoadUnitHandle(udg_Hash, id, StringHash("target"))

    local real x  = LoadReal(udg_Hash, id, StringHash("x"))
    local real y  = LoadReal(udg_Hash, id, StringHash("y"))
    local real z  = LoadReal(udg_Hash, id, StringHash("z"))

    local real vx = LoadReal(udg_Hash, id, StringHash("vx"))
    local real vy = LoadReal(udg_Hash, id, StringHash("vy"))
    local real vz = LoadReal(udg_Hash, id, StringHash("vz"))

    local real speed
    local real dx
    local real dy
    local real dist
    local real angle
    local group g
    local unit newTarget

    local integer phase = LoadInteger(udg_Hash, id, StringHash("phase"))

    if missile == null then
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set t = null
        return
    endif

    // ---- DYNAMIC RETARGETING ----
    if target != null and IsUnitDeadBJ(target) then
        // current target is dead, look for a new one near the missile
        set g = CreateGroup()
        call GroupEnumUnitsInRange(g, x, y, RETARGET_RANGE, null)
        set newTarget = null
        loop
            set newTarget = FirstOfGroup(g)
            exitwhen newTarget == null
            call GroupRemoveUnit(g, newTarget)
            if IsUnitEnemy(newTarget, GetOwningPlayer(caster)) and not IsUnitDeadBJ(newTarget) and not IsUnitType(newTarget, UNIT_TYPE_STRUCTURE) then
                // found a valid replacement
                call SaveUnitHandle(udg_Hash, id, StringHash("target"), newTarget)
                set target = newTarget
                exitwhen true
            endif
            set newTarget = null
        endloop
        call DestroyGroup(g)
        if newTarget == null then
            call RemoveSavedHandle(udg_Hash, id, StringHash("target"))
            set target = null
        endif
        set g = null
        set newTarget = null
    endif

    // update target position or keep drifting
    if target != null and not IsUnitDeadBJ(target) then
        set dx = GetUnitX(target) - x
        set dy = GetUnitY(target) - y
    else
        set dx = vx
        set dy = vy
    endif

    set dist = SquareRoot(dx*dx + dy*dy)

    // PHASE 0 : ASCENT
    if phase == 0 then
        set vz = vz - 0.30
        call SetUnitVertexColor(missile, 100, 100, 255, 255)
        if vz <= 0.0 then
            set phase = 1
        endif
    // PHASE 1 : TERMINAL DIVE
    else
        if dist > 0.0 then
            set angle = Atan2(dy, dx)
            set speed = SquareRoot(vx*vx + vy*vy)
            set speed = speed + 0.45
            if speed > 34.0 then
                set speed = 34.0
            endif
            set vx = speed * Cos(angle)
            set vy = speed * Sin(angle)
        endif
        set vz = vz - 1.10
        if vz < -22.0 then
            set vz = -22.0
        endif
        call SetUnitVertexColor(missile, 255, 90, 90, 255)
    endif

    set x = x + vx
    set y = y + vy
    set z = z + vz

    // IMPACT
    if (phase == 1 and z <= 35.0 and dist <= 120.0) or (phase == 1 and z <= 0.0) then
        call ExplodeMissile(caster, x, y)
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    // failsafe: out of bounds
    if not RectContainsCoords(bj_mapInitialPlayableArea, x, y) then
        call RemoveUnit(missile)
        call FlushChildHashtable(udg_Hash, id)
        call PauseTimer(t)
        call DestroyTimer(t)
        set missile = null
        set caster  = null
        set target  = null
        set t       = null
        return
    endif

    call SetUnitX(missile, x)
    call SetUnitY(missile, y)
    call SetUnitFlyHeight(missile, z, 0.0)
    set angle = Atan2(vy, vx)
    call SetUnitFacing(missile, angle * bj_RADTODEG)

    call SaveReal(udg_Hash, id, StringHash("x"), x)
    call SaveReal(udg_Hash, id, StringHash("y"), y)
    call SaveReal(udg_Hash, id, StringHash("z"), z)
    call SaveReal(udg_Hash, id, StringHash("vx"), vx)
    call SaveReal(udg_Hash, id, StringHash("vy"), vy)
    call SaveReal(udg_Hash, id, StringHash("vz"), vz)
    call SaveInteger(udg_Hash, id, StringHash("phase"), phase)

    set missile = null
    set caster  = null
    set target  = null
    set t       = null
endfunction

//===========================================================================
//  LAUNCH ONE MISSILE (called by the repeating timer)
//===========================================================================
function MissileBarrageTick takes nothing returns nothing
    local timer tLoop = GetExpiredTimer()
    local integer idT = GetHandleId(tLoop)

    local unit caster = LoadUnitHandle(udg_Hash, idT, 1)
    local real tx = LoadReal(udg_Hash, idT, StringHash("target_x"))
    local real ty = LoadReal(udg_Hash, idT, StringHash("target_y"))
    local integer remaining = LoadInteger(udg_Hash, idT, 2)

    local real sx = GetUnitX(caster)
    local real sy = GetUnitY(caster)

    local group g = CreateGroup()
    local unit target = null
    local unit missile
    local timer t
    local integer id
    local real angle

    if IsUnitDeadBJ(caster) or remaining <= 0 then
        call FlushChildHashtable(udg_Hash, idT)
        call PauseTimer(tLoop)
        call DestroyTimer(tLoop)
        set caster = null
        set g = null
        return
    endif

    call GroupEnumUnitsInRange(g, tx, ty, 900.0, null)
    loop
        set target = FirstOfGroup(g)
        exitwhen target == null
        call GroupRemoveUnit(g, target)
        if IsUnitEnemy(target, GetOwningPlayer(caster)) and not IsUnitDeadBJ(target) and not IsUnitType(target, UNIT_TYPE_STRUCTURE) then
            exitwhen true
        endif
        set target = null
    endloop
    call DestroyGroup(g)

    if target == null then
        set remaining = remaining - 1
        call SaveInteger(udg_Hash, idT, 2, remaining)
        if remaining <= 0 then
            call FlushChildHashtable(udg_Hash, idT)
            call PauseTimer(tLoop)
            call DestroyTimer(tLoop)
        endif
        set caster = null
        return
    endif

    set missile = CreateUnit(GetOwningPlayer(caster), 'dumi', sx, sy, 0)
    call UnitAddAbility(missile, 'Amrf')
    call UnitRemoveAbility(missile, 'Amrf')
    call SetUnitFlyHeight(missile, 0.0, 0.0)
    call SetUnitScale(missile, 1.8, 1.8, 1.8)

    set angle = Atan2(GetUnitY(target) - sy, GetUnitX(target) - sx)

    set t  = CreateTimer()
    set id = GetHandleId(t)

    call SaveUnitHandle(udg_Hash, id, StringHash("missile"), missile)
    call SaveUnitHandle(udg_Hash, id, StringHash("caster"), caster)
    call SaveUnitHandle(udg_Hash, id, StringHash("target"), target)

    call SaveReal(udg_Hash, id, StringHash("x"), sx)
    call SaveReal(udg_Hash, id, StringHash("y"), sy)
    call SaveReal(udg_Hash, id, StringHash("z"), 0.0)

    call SaveReal(udg_Hash, id, StringHash("vx"), 1.2 * Cos(angle))
    call SaveReal(udg_Hash, id, StringHash("vy"), 1.2 * Sin(angle))
    call SaveReal(udg_Hash, id, StringHash("vz"), 28.0)

    call SaveInteger(udg_Hash, id, StringHash("phase"), 0)

    call TimerStart(t, 0.03, true, function WatcherMissileLoop)

    set remaining = remaining - 1
    call SaveInteger(udg_Hash, idT, 2, remaining)
    if remaining <= 0 then
        call FlushChildHashtable(udg_Hash, idT)
        call PauseTimer(tLoop)
        call DestroyTimer(tLoop)
    endif

    set missile = null
    set target  = null
    set t       = null
    set caster  = null
endfunction

//===========================================================================
//  SPELL CAST (starts the 12-missile barrage)
//===========================================================================
function Trig_WatcherPhysicsTest_Actions takes nothing returns nothing
    local unit caster = GetTriggerUnit()
    local real tx = GetSpellTargetX()
    local real ty = GetSpellTargetY()
    local timer t
    local integer idT

    if GetSpellAbilityId() != 'A01M' then
        set caster = null
        return
    endif

    set t = CreateTimer()
    set idT = GetHandleId(t)

    call SaveUnitHandle(udg_Hash, idT, 1, caster)
    call SaveReal(udg_Hash, idT, StringHash("target_x"), tx)
    call SaveReal(udg_Hash, idT, StringHash("target_y"), ty)
    call SaveInteger(udg_Hash, idT, 2, 12)

    call TimerStart(t, 1.0, true, function MissileBarrageTick)

    set caster = null
    set t = null
endfunction

//===========================================================================
//  INITIALIZATION
//===========================================================================
function InitTrig_WatcherPhysicsTest takes nothing returns nothing
    local trigger trig = CreateTrigger()
    set udg_Hash = InitHashtable()
    call TriggerRegisterAnyUnitEventBJ(trig, EVENT_PLAYER_UNIT_SPELL_EFFECT)
    call TriggerAddAction(trig, function Trig_WatcherPhysicsTest_Actions)
    set trig = null
endfunction
 

Attachments

Back
Top