• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

Dummy unit - Submission

Status
Not open for further replies.
Level 12
Joined
Jun 15, 2016
Messages
472
Here is a test map including mission 4, divided into parts. I ran into some strange bug which I can't seem to fix entirely in part 1 of all parts.

The thing is, some coordinates are not a viable target point for shockwave (firing on cliffs doesn't work for example), this causes the shockwave not to fire, and has happened multiple times in part 2 and part 1 (see picture "part 2 - unresponsive coorinate (cliff)"). However, in part 1 firing in some directions simply does not work, for no apparent reason (see other 2 pictures). There is no cliff or anything at the target, so I have a hard time understanding what causes them not to fire, but I did notice it happens only when facing directly north/south/west/east (i.e. facing = 0°/90°/180°/270°). Any ideas why that happens?

P.S.: type "fite me" to spawn enemy units around the hero unit.


JASS:
function Trig_Dummy_cast_part1_Actions takes nothing returns nothing
   local real facing = GetUnitFacing(udg_Hero)
   local real sx = GetUnitX(udg_Hero)
   local real sy = GetUnitY(udg_Hero)
   local unit dummy = CreateUnit(Player(0),'h000',sx,sy,facing)
   local real tx = sx + 1000.0*Cos(facing*bj_DEGTORAD)
   local real ty = sy + 1000.0*Sin(facing*bj_DEGTORAD)
   
    // Because apparently if the offset isn't high enough nothing will be cast :<
    call BJDebugMsg("X offset: " + R2S(1000.0*Cos(facing*bj_DEGTORAD)) + ", Y offset: " + R2S(1000.0*Sin(facing*bj_DEGTORAD)))
   call IssuePointOrder(dummy,"shockwave",tx,ty)
   
   call RemoveUnit(dummy)
   set dummy = null
endfunction

//===========================================================================
function InitTrig_Dummy_cast_part1 takes nothing returns nothing
    set gg_trg_Dummy_cast_part1 = CreateTrigger(  )
    call CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect())
   set udg_Hero = CreateUnit(Player(0),'Otch',0,0,GetRandomReal(0,360))
    set udg_Hash = InitHashtable()
   
   call TriggerRegisterPlayerEvent(gg_trg_Dummy_cast_part1,Player(0),EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( gg_trg_Dummy_cast_part1, function Trig_Dummy_cast_part1_Actions )
endfunction



JASS:
function Trig_Dummy_cast_part2_Actions takes nothing returns nothing
   local real facing = GetUnitFacing(udg_Hero)
   local real sx = GetUnitX(udg_Hero)
   local real sy = GetUnitY(udg_Hero)
   local unit dummy = CreateUnit(Player(0),'h000',sx,sy,facing)
   local real tx
   local real ty
    local integer shock_counter = 0
   
    loop
        exitwhen shock_counter >= 8
        set facing = facing + shock_counter*45.0
        set tx = sx + 1000.0*Cos(facing*bj_DEGTORAD)
        set ty = sy + 1000.0*Sin(facing*bj_DEGTORAD)
       
        call SetUnitFacing(dummy,facing)
        call IssuePointOrder(dummy,"shockwave",tx,ty)
       
        set shock_counter = shock_counter + 1
    endloop
   
   call RemoveUnit(dummy)
   set dummy = null
endfunction

//===========================================================================
function InitTrig_Dummy_cast_part2 takes nothing returns nothing
    set gg_trg_Dummy_cast_part2 = CreateTrigger(  )
    call CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect())
   set udg_Hero = CreateUnit(Player(0),'Otch',0,0,GetRandomReal(0,360))
    set udg_Hash = InitHashtable()
   
   call TriggerRegisterPlayerEvent(gg_trg_Dummy_cast_part2,Player(0),EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( gg_trg_Dummy_cast_part2, function Trig_Dummy_cast_part2_Actions )
endfunction



JASS:
function DU3_DummyInDestination takes integer pk, integer uck returns boolean
    local unit du = LoadUnitHandle(udg_Hash,pk,uck)
    local real target_x = LoadReal(udg_Hash,pk,uck + 1)
    local real target_y = LoadReal(udg_Hash,pk,uck + 2)
    local real dummy_x = GetUnitX(du)
    local real dummy_y = GetUnitY(du)
    local real distance_x = target_x - dummy_x
    local real distance_y = target_y - dummy_y
   
    return (distance_x < 50.0 and distance_x > -50.0) and (distance_y < 50.0 and distance_y > -50.0)
endfunction

function DU3_DummyTimeout takes nothing returns nothing
    local integer dth = GetHandleId(GetExpiredTimer())
    local integer total_time = LoadInteger(udg_Hash,dth,30) + 1
    local integer living_projectiles = LoadInteger(udg_Hash,dth,31)
    local integer counter = 0
   
    loop
        exitwhen counter >= 8
       
        if DU3_DummyInDestination(dth,counter*3) then
            call BJDebugMsg("dummy number " + I2S(counter) + " reached the goal")
            call RemoveUnit(LoadUnitHandle(udg_Hash,dth,counter*3))
            set living_projectiles = living_projectiles - 1
        endif
       
        set counter = counter + 1
    endloop
   
    if living_projectiles <= 0 then
        call DestroyTimer(GetExpiredTimer())
        call FlushChildHashtable(udg_Hash,dth)
        call BJDebugMsg("All projectiles destroyed, script done")
    else
        call BJDebugMsg(I2S(living_projectiles) + " projectiles remaining")
        call SaveInteger(udg_Hash,dth,30,total_time)
        call SaveInteger(udg_Hash,dth,31,living_projectiles)
    endif
endfunction

function Trig_Dummy_cast_part3_Actions takes nothing returns nothing
    local timer dt = CreateTimer()
    local integer dth = GetHandleId(dt)
   local real facing = GetUnitFacing(udg_Hero)
   local real sx = GetUnitX(udg_Hero)
   local real sy = GetUnitY(udg_Hero)
    local player dp = GetOwningPlayer(udg_Hero)
    local integer counter = 0
   local unit dummy
   local real tx
   local real ty
   
    loop
        exitwhen counter >= 8
       
        set dummy = CreateUnit(dp,'h001',sx,sy,facing)
        set tx = sx + 1000.0*Cos(facing*bj_DEGTORAD)
        set ty = sy + 1000.0*Sin(facing*bj_DEGTORAD)
        call IssuePointOrder(dummy,"move",tx,ty)
       
        call SaveUnitHandle(udg_Hash,dth,counter*3,dummy)
        call SaveReal(udg_Hash,dth,(counter*3 + 1),tx)
        call SaveReal(udg_Hash,dth,(counter*3 + 2),ty)
       
        set counter = counter + 1
        set facing = facing + 45.0
    endloop
   
    call SaveInteger(udg_Hash,dth,30,0) // store timer seconds
    call SaveInteger(udg_Hash,dth,31,8) // store living projectiles
   
    call TimerStart(dt,0.125,true,function DU3_DummyTimeout)
    set dummy = null
    set dt = null
    set dp = null
endfunction

//===========================================================================
function InitTrig_Dummy_cast_part3 takes nothing returns nothing
    set gg_trg_Dummy_cast_part3 = CreateTrigger(  )
    call CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect())
   set udg_Hero = CreateUnit(Player(0),'Otch',0,0,GetRandomReal(0,360))
    set udg_Hash = InitHashtable()
   
   call TriggerRegisterPlayerEvent(gg_trg_Dummy_cast_part3,Player(0),EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( gg_trg_Dummy_cast_part3, function Trig_Dummy_cast_part3_Actions )
endfunction



JASS:
function DU4_TargetFilter takes unit filter_unit, player dummy_player returns boolean
    if IsUnitEnemy(filter_unit,dummy_player) then
        return UnitAlive(filter_unit) and not IsUnitType(filter_unit,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(filter_unit,UNIT_TYPE_FLYING)
    else
        return false
    endif
endfunction

function DU4_DummyInDestination takes integer pk, integer uck returns boolean
    local unit du = LoadUnitHandle(udg_Hash,pk,uck)
    local real target_x = LoadReal(udg_Hash,pk,uck + 1)
    local real target_y = LoadReal(udg_Hash,pk,uck + 2)
    local real dummy_x = GetUnitX(du)
    local real dummy_y = GetUnitY(du)
    local real distance_x = target_x - dummy_x
    local real distance_y = target_y - dummy_y
    // Extra variables for damage targets
    local group collisions
    local unit u
    local player dp = GetOwningPlayer(du)
    local group targets
   
    if (distance_x < 50.0 and distance_x > -50.0) and (distance_y < 50.0 and distance_y > -50.0) then
        set collisions = null // need nulling uninitialized variables?
        set targets = null // need nulling uninitialized variables?
        set u = null // need nulling uninitialized variables?
        set dp = null // need nulling uninitialized variables?
       
        return true
    else
        set collisions = CreateGroup()
        set targets = LoadGroupHandle(udg_Hash,pk,32)
        call GroupEnumUnitsInRange(collisions,dummy_x,dummy_y,100.0,null)
       
        set u = FirstOfGroup(collisions)
        loop
            exitwhen u == null
           
            if DU4_TargetFilter(u,dp) then
                call GroupAddUnit(targets,u)
            endif
           
            call GroupRemoveUnit(collisions,u)
            set u = FirstOfGroup(collisions)
        endloop
       
        call DestroyGroup(collisions)
        set collisions = null
        set targets = null
        set dp = null
       
        return false
    endif
endfunction

function DU4_DummyTimeout takes nothing returns nothing
    local integer dth = GetHandleId(GetExpiredTimer())
    local integer total_time = LoadInteger(udg_Hash,dth,30) + 1
    local integer living_projectiles = LoadInteger(udg_Hash,dth,31)
    local integer counter = 0
    // Extra variables for damage event
    local group targets = LoadGroupHandle(udg_Hash,dth,32)
    local unit u

    if total_time/8*8 == total_time then
        set targets = LoadGroupHandle(udg_Hash,dth,32)
        set u = FirstOfGroup(targets)
        call BJDebugMsg("Run damage event for " + I2S(CountUnitsInGroup(targets)) + " units")
       
        loop
            exitwhen u == null
           
            call UnitDamageTarget(udg_Hero,u,100.0,false,false,ATTACK_TYPE_MAGIC,DAMAGE_TYPE_SONIC,WEAPON_TYPE_WHOKNOWS)
            call GroupRemoveUnit(targets,u)
            set u = FirstOfGroup(targets)
        endloop
    endif
   
    loop
        exitwhen counter >= 8
       
        if DU4_DummyInDestination(dth,counter*3) then
            // call BJDebugMsg("dummy number " + I2S(counter) + " reached the goal")
            call RemoveUnit(LoadUnitHandle(udg_Hash,dth,counter*3))
            set living_projectiles = living_projectiles - 1
        endif
       
        set counter = counter + 1
    endloop
   
    if living_projectiles <= 0 then
        call DestroyTimer(GetExpiredTimer())
        call FlushChildHashtable(udg_Hash,dth)
        // call BJDebugMsg("All projectiles destroyed, script done")
    else
        // call BJDebugMsg(I2S(living_projectiles) + " projectiles remaining")
        call SaveInteger(udg_Hash,dth,30,total_time)
        call SaveInteger(udg_Hash,dth,31,living_projectiles)
    endif
   
    set targets = null
    set u = null // need nulling uninitialized variables?
endfunction

function Trig_Dummy_cast_part4_Actions takes nothing returns nothing
    local timer dt = CreateTimer()
    local integer dth = GetHandleId(dt)
   local real facing = GetUnitFacing(udg_Hero)
   local real sx = GetUnitX(udg_Hero)
   local real sy = GetUnitY(udg_Hero)
    local player dp = GetOwningPlayer(udg_Hero)
    local integer counter = 0
   local unit dummy
   local real tx
   local real ty
   
    loop
        exitwhen counter >= 8
       
        set dummy = CreateUnit(dp,'h001',sx,sy,facing)
        set tx = sx + 1000.0*Cos(facing*bj_DEGTORAD)
        set ty = sy + 1000.0*Sin(facing*bj_DEGTORAD)
        call IssuePointOrder(dummy,"move",tx,ty)
       
        call SaveUnitHandle(udg_Hash,dth,counter*3,dummy)
        call SaveReal(udg_Hash,dth,(counter*3 + 1),tx)
        call SaveReal(udg_Hash,dth,(counter*3 + 2),ty)
       
        set counter = counter + 1
        set facing = facing + 45.0
    endloop
   
    call SaveInteger(udg_Hash,dth,30,0) // store timer seconds
    call SaveInteger(udg_Hash,dth,31,8) // store living projectiles
    call SaveGroupHandle(udg_Hash,dth,32,CreateGroup()) // store targets group
   
    call TimerStart(dt,0.125,true,function DU4_DummyTimeout)
    set dummy = null
    set dt = null
    set dp = null
endfunction

//===========================================================================
function InitTrig_Dummy_cast_part4 takes nothing returns nothing
    set gg_trg_Dummy_cast_part4 = CreateTrigger(  )
    call CreateFogModifierRectBJ( true, Player(0), FOG_OF_WAR_VISIBLE, GetPlayableMapRect())
   set udg_Hero = CreateUnit(Player(0),'Otch',0,0,GetRandomReal(0,360))
    set udg_Hash = InitHashtable()
   
    // Extra building targets
    call CreateUnit(Player(1),'hbar',2820,-2380,bj_UNIT_FACING)
    call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
    call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
    call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
   
   call TriggerRegisterPlayerEvent(gg_trg_Dummy_cast_part4,Player(0),EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( gg_trg_Dummy_cast_part4, function Trig_Dummy_cast_part4_Actions )
endfunction
 

Attachments

  • JASS class 4.w3x
    34.5 KB · Views: 90
  • part 2 - unresponsive coorinate (cliff).png
    part 2 - unresponsive coorinate (cliff).png
    2.4 MB · Views: 116
  • part 1 - unresponsive coorinate 1.png
    part 1 - unresponsive coorinate 1.png
    2.6 MB · Views: 107
  • part 1 - unresponsive coorinate 2.png
    part 1 - unresponsive coorinate 2.png
    2.6 MB · Views: 110

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
Shockwave and most other skills (breath of fire, impalte, carrion swarm, wards, etc) cannot be cast on cliffs.

I tested it in my submission and it's the same problem. If you are close to cliffs some shockwaves don't fire. I used 100 as offset by the way. Now testing smaller ones.
10 did not work for me (all shockwaves in same direction), 50 works though.

There is a solution, that you create the dummies at the offset and order them to cast at the position of the caster. If it only hits ground units it should work, because no ground unit can be at the position of the caster (which would be hit by 8 shockwaves).

The jass class is not about struggling with warcraft limitations, but learning jass, so it is not required to work perfectly with cliffs in this case.

You can use local real facing = GetUnitFacing(udg_Hero) * bj_DEGTORAD, so you only have to do *bj_DEGTORAD once instead of several times. It is not required for funtionality, but I will point out these things, so you are aware how to optimize triggers.

Part 1:
You have 3000 offset for tx/ty, but write only 1000 with BJDebugMsg. 3000 is most likely too much and the caster tries to target outside of playable map area

Part 2:
-

Part 3:
only a smaller version of part 4, so I will only look at part 4

Part 4:
It looks a bit complicated in my opinion. The DummyInDestination function does more than its name suggests.
I do not like the way you handled when units are damaged. There are two approaches one can solve the "100 damage per second" problem.
The first one is, that you rapidly damage the units (every 0.2 seconds or less), so that the unit is damaged, as long as it collides with the missile.
The second one is, that the unit is damaged when it is hit first by the missile and then every second it still collides with the missile.

In your case damage is dealt every second, so it often feels very delayed.
I think for the mission you are supposed to use approach one and just damage colliding units every x seconds for 100*x damage. x<=0.2
Then you can directly damage the units and do not even need a target group.
 
Level 12
Joined
Jun 15, 2016
Messages
472
You can use
local real facing = GetUnitFacing(udg_Hero) * bj_DEGTORAD
, so you only have to do *bj_DEGTORAD once instead of several times. It is not required for funtionality, but I will point out these things, so you are aware how to optimize triggers.

Neat. Didn't think about that.

Part 1:
You have 3000 offset for tx/ty, but write only 1000 with BJDebugMsg. 3000 is most likely too much and the caster tries to target outside of playable map area

Ugh... right... I had the problem of choosing a very small offset as a start, so I did some limit checks, must've forgot to revert it. I think my choice offset was ~200.

Part 4:
It looks a bit complicated in my opinion. The DummyInDestination function does more than its name suggests.
I do not like the way you handled when units are damaged. There are two approaches one can solve the "100 damage per second" problem.
The first one is, that you rapidly damage the units (every 0.2 seconds or less), so that the unit is damaged, as long as it collides with the missile.
The second one is, that the unit is damaged when it is hit first by the missile and then every second it still collides with the missile.

In your case damage is dealt every second, so it often feels very delayed.
I think for the mission you are supposed to use approach one and just damage colliding units every x seconds for 100*x damage. x<=0.2
Then you can directly damage the units and do not even need a target group.

Doing a DOT is one way of performing that, but I would rather stay with the implementation of "pulse damage" of 100 every second. The problem is, I didn't see any way to distinguish between units already hit by the dummy except for a complex filtering method such as a groups for each time frame (i.e. if timer callback function runs every 0.2 seconds then 5 groups, etc.). I'll try thinking of something more clever, worst case scenerio do as you suggest.
 
Level 12
Joined
Jun 15, 2016
Messages
472
New attempt at implementing part 4 with structs. I'm not sure if this works 100% yet, but this is my first go with vJASS, so might as well post it early. Note this is still supposed to do one pulse of 100 damage each second, only this time without the delay.


JASS:
struct DummyStruct
    unit DummyUnit
    unit CasterUnit
    player CasterPlayer
    real DestX
    real DestY
    unit array TargetArray[1]
    integer array CooldownArray[1]
    integer TargetIndex
    integer CooldownExpire
  
    static method create takes unit caster, real x, real y, real facing returns thistype
        local thistype this = thistype.allocate()
        set this.CasterUnit = caster
        set this.CasterPlayer = GetOwningPlayer(caster)
        set this.DummyUnit = CreateUnit(this.CasterPlayer,'h001',x,y,facing)
        set this.DestX = x + 1000.0*Cos(facing*bj_DEGTORAD)
        set this.DestY = y + 1000.0*Sin(facing*bj_DEGTORAD)
        set this.TargetIndex = 0
      
        call IssuePointOrder(this.DummyUnit,"move",this.DestX,this.DestY)
        return this
    endmethod
  
    method destroy takes nothing returns nothing
        local integer index = 0
      
        call RemoveUnit(this.DummyUnit)
        set this.DummyUnit = null
        set this.CasterUnit = null
        loop
            exitwhen index >= this.TargetIndex
            set this.TargetArray[index] = null
            set index = index + 1
        endloop
      
        call this.deallocate()
    endmethod

//===========================================================================
  
    method DummyInTarget takes real current_x, real current_y returns boolean
        local real distance_x = current_x - this.DestX
        local real distance_y = current_y - this.DestY
        return (distance_x < 50.0 and distance_x > -50.0) and (distance_y < 50.0 and distance_y > -50.0)
    endmethod
  
    method DummyTargetCheck takes unit u returns boolean
        local integer index = 0
      
        if IsUnitEnemy(u,this.CasterPlayer) and UnitAlive(u) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING) then
            loop
                exitwhen index >= this.TargetIndex
              
                if u == this.TargetArray[index] then
                    return false
                endif
              
                set index = index + 1
            endloop
          
            return true
        else
            return false
        endif
    endmethod
  
    method DummyTimerEvent takes nothing returns boolean
        local group enum_targets
        local unit temp
        local real current_x = GetUnitX(this.DummyUnit)
        local real current_y = GetUnitY(this.DummyUnit)
        local integer index = 0
      
        if this.DummyInTarget(current_x,current_y) then
            call this.destroy()
          
            return true
          
        else
            loop // Iterate over all cooldown units and update values
                exitwhen index >= this.TargetIndex
              
                set this.CooldownArray[index] = this.CooldownArray[index] + 1
                if this.CooldownArray[index] == udg_CooldownExpire then
                    set this.TargetArray[index] = this.TargetArray[this.TargetIndex]
                    set this.CooldownArray[index] = this.CooldownArray[this.TargetIndex]
                  
                    set this.TargetArray[this.TargetIndex] = null
                    set this.TargetIndex = this.TargetIndex - 1
                endif
              
                set index = index + 1
            endloop
          
            set enum_targets = CreateGroup()
            call GroupEnumUnitsInRange(enum_targets,current_x,current_y,100.,null)
          
            set temp = FirstOfGroup(enum_targets)
            loop // Iterate over all new collisions and filter new targets
                exitwhen temp == null
              
                if this.DummyTargetCheck(temp) then
                    call UnitDamageTarget(this.CasterUnit,temp,100.,false,false,ATTACK_TYPE_MAGIC,DAMAGE_TYPE_SONIC,WEAPON_TYPE_WHOKNOWS)
                  
                    set this.TargetIndex = this.TargetIndex + 1
                    set this.TargetArray[this.TargetIndex] = temp
                    set this.CooldownArray[this.TargetIndex] = 0
                endif
              
                call GroupRemoveUnit(enum_targets,temp)
                set temp = FirstOfGroup(enum_targets)
            endloop
          
            call DestroyGroup(enum_targets)
            set enum_targets = null
          
            return false
        endif
    endmethod
endstruct

//===========================================================================

function DU4V_Timeout takes nothing returns nothing
    local integer dth = GetHandleId(GetExpiredTimer())
    local integer index = 0
    local integer living_projectiles = LoadInteger(udg_Hash,dth,9)
    local DummyStruct dummy
  
    loop
        exitwhen index >= 8
      
        set dummy = LoadInteger(udg_Hash,dth,index)
        if dummy == 0 then
            // Projectile removed, no action required
        elseif dummy.DummyTimerEvent() then
            set living_projectiles = living_projectiles - 1
        endif
      
        set index = index + 1
    endloop
  
    if living_projectiles <= 0 then
        // call BJDebugMsg("All projectiles destroyed, script done")
        call DestroyTimer(GetExpiredTimer())
        call FlushChildHashtable(udg_Hash,dth)
    else
        // call BJDebugMsg(I2S(living_projectiles) + " projectiles remaining")
        call SaveInteger(udg_Hash,dth,9,living_projectiles)
    endif
endfunction

function Trig_Dummy_cast_part4_vJASS_Actions takes nothing returns nothing
    local timer dt = CreateTimer()
    local integer dth = GetHandleId(dt)
   local real facing = GetUnitFacing(udg_Hero)
   local real sx = GetUnitX(udg_Hero)
   local real sy = GetUnitY(udg_Hero)
    local integer index = 0
  
    loop
        exitwhen index >= 8
      
        call SaveInteger(udg_Hash,dth,index,DummyStruct.create(udg_Hero,sx,sy,facing))
      
        set facing = facing + 45.
        set index = index + 1
    endloop
  
    call SaveInteger(udg_Hash,dth,9,8) // Store the amount of living projectiles
    call TimerStart(dt,0.125,true,function DU4V_Timeout)
endfunction

//===========================================================================
function InitTrig_Dummy_cast_part4_vJASS takes nothing returns nothing
    set gg_trg_Dummy_cast_part4_vJASS = CreateTrigger(  )
    call CreateFogModifierRectBJ(true,Player(0),FOG_OF_WAR_VISIBLE,GetPlayableMapRect())
   set udg_Hero = CreateUnit(Player(0),'Otch',0,0,GetRandomReal(0,360))
    set udg_Hash = InitHashtable()
    set udg_CooldownExpire = 8
  
    // Extra building targets
    call CreateUnit(Player(1),'hbar',2820,-2380,bj_UNIT_FACING)
    call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
    call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
    call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
  
   call TriggerRegisterPlayerEvent(gg_trg_Dummy_cast_part4_vJASS,Player(0),EVENT_PLAYER_END_CINEMATIC)
    call TriggerAddAction( gg_trg_Dummy_cast_part4_vJASS, function Trig_Dummy_cast_part4_vJASS_Actions )
endfunction
 

Attachments

  • JASS class 4.w3x
    37.1 KB · Views: 87

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
Technically it looks ok. In the destroy method you also have to null CasterPlayer.
I think you also have to change the maximum number if indices from 1 to the maximum amount of targets. Struct members are always arrays when converted from vJASS into JASS, so you can have up to 8192 instances of a struct, as you use the instance id of the struct (each struct instance has an id, so a struct variable is basically an integer) as index for the array. Now if you want to use an array as struct member, you can have only 8192/array_size instances, because each instance needs array_size indices of the total array. You need to set the array size, so the compiler knows, how many indices each instance needs.
I am also quite new to structs, so please correct me if I say something wrong @IcemanBo

DummyTargetCheck can be made more efficiently. You loop through an array to see, if the unit was already hit. If you use a hashtable with the keys struct instance id and unit handle id you can store for each instance+unit combination, if the unit was already hit by that instance. In the destroy method you can flush this hashtable with the struct instance id.
Depending on the amount of units hit by one shockwave the hashtable can be a lot faster. If it is only 5 for example array is probably faster, because arrays are faster than hashtables.

When you use structs or vJASS in general you want to follow OOP philosophies. It makes your code more structured and more readable. In this case it makes sense to have the whole code inside the struct. In the struct example below you can see how to put everything inside the struct.

At least the timer logic should be inside the struct. Then you only have to create the struct instance and everything else is handled internally in the struct.
The trigger that creates the struct can also be inside the struct, but does not have to be. For example if you want to use this missile in different situations it would make sense to not have all of the triggers inside the struct. Let's say you have a normal shockwave, a shockwave into 8 directions and a shockwave into 4 directions, it would probably be too much to put all of them into the struct. Instead you have 3 normal triggers that all use this struct. If you only have one trigger that utilizes the struct, it's better to put the trigger inside the struct.

I hope this helps. IcemanBo can tell you more. He is a lot more experienced with vJASS than I am.

I used TimerUtils. It is very simple:
NewTimer() gives you a timer (similar to CreateTimer)
ReleaseTimer() recycles a timer (similar to DestroyTimer)
GetTimerData and SetTimerData allow you to get/set one integer per timer. It is similar to using a hashtable to do the same. It is useful, because struct instance ids are integers, so you can attach a struct instance to a timer.
NewTimerEx(x) is SetTimerData(NewTimer(),x)
JASS:
struct Drain
    private unit caster
    private unit target
    private real damage
   
    private static method create takes unit caster, unit target, real damage returns thistype
        local thistype this = thistype.allocate()
        set this.caster = caster
        set this.target = target
        set this.damage = damage
        return this
    endmethod
   
    private method destroy takes nothing returns nothing
        set this.caster = null
        set this.target = null
        call this.deallocate()
    endmethod
   
    private static method onTimer takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local thistype this = GetTimerData(t)
       
        call UnitDamageTarget(this.caster, this.target, this.damage, true, false, ATTACK_TYPE_NORMAL, DAMAGE_TYPE_MAGIC, WEAPON_TYPE_WHOKNOWS)
        call SetWidgetLife(this.caster, GetWidgetLife(this.caster)+this.damage)
        call this.destroy()
       
        call ReleaseTimer(t)
        set t = null
    endmethod
   
    private static method onCast takes nothing returns nothing
        local unit caster = GetTriggerUnit()
        local unit target = GetSpellTargetUnit()
        local thistype d = thistype.create(caster,target,100)
        local timer t = NewTimerEx(d)
        call TimerStart(t, 5, false, function thistype.onTimer)
   
        set caster = null
        set target = null
        set t = null
    endmethod
   
    private static method onInit takes nothing returns nothing
        local trigger trg = CreateTrigger()
        call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_SPELL_EFFECT)
        call TriggerAddAction(trg, function thistype.onCast)
        set trg = null
    endmethod
endstruct
 
Binding all shockwaves to unit with static keys 1-8, and 1 timer to unit makes it less useable than it can be. Each shockwave is checked independantly for "death" and if all are destrroyed, then the timer is released, too. They also hold place holders from 1-8 at the unit, which means that if the caster casts 2times, then they might get overwritten.
Each shockwave could exist on it's own, with knowing it's target/direction, and knowing the corresponding caster, and having it's own timer. Then it can exist completly independently of other shockwaves, and we don't need to track them all from 1-8 anymore for the unit.

Using "move" order is not best practice for projectile behaviour. Moving a unit with SetUnitX/Y does not even consider something like movement type, or pathability, plus its speed is nicely configurable.

100 damage/second is a reference value, like for example 100km/h, or 100 miles/h. It does not necessarily mean you need to damage exactly "100" each second, but you can damage of course each iteration, so it sums up to 100/second. You interpreted it probably the other way, and doesn't really matter now, but yeh, it makes it a bit more complicated even I guess, as you need to handle the cooldowns.

The array[] inside structs is sadly not a good idea, because of what Jampion explained. The number is a staticly defined value, and you can't really change it dynamicly.
If you really would need to keep track of units for an instance, a new type is suggested that can hold multiple units without using array; for example "group", or custom linked lists structures.
(if there would be no cooldown, I guess you dont need to track them at all)

What Jampion says makes good sense! And then also names like "Trig_Dummy_cast_part4_vJASS_Actions" are not needed, and nice, short names can be used inside the struct!
 
Level 12
Joined
Jun 15, 2016
Messages
472
DummyTargetCheck can be made more efficiently. You loop through an array to see, if the unit was already hit. If you use a hashtable with the keys struct instance id and unit handle id you can store for each instance+unit combination, if the unit was already hit by that instance. In the destroy method you can flush this hashtable with the struct instance id.
Depending on the amount of units hit by one shockwave the hashtable can be a lot faster. If it is only 5 for example array is probably faster, because arrays are faster than hashtables.

I don't quite understand what you mean here. Do you intend to store units hit by the dummy saved like this hashtable[parentkay=structindex][childkey=unitkey]? In that case, how do I store how much time has passed since said unit was hit? Is there a way to check all units saved with a certain parent key?

In any case the problem is not one of efficiency here (I think, I don't expect any of the dummies to even reach 5 targets), rather the fact that struct members cannot be indexed dynamically (this complicates matter, huh...). In that case, should I try to rely on a static amount of unit groups, each one containing the targets at a certain time iteration in a second? For example: the timer goes off every 0.2 seconds, so I create 5 groups which will hold the units hit at each time iteration, then check if a new target is in all previous iterations, and once a full second has passed, the first group will be refilled with new targets, then the second, and third, etc... . There's even a shiny nice native for that(IsUnitInGroup). That way, even if the timer runs 8 times a second I still have 8191/8>1000 instances left, more then enough I think.

Binding all shockwaves to unit with static keys 1-8, and 1 timer to unit makes it less useable than it can be. Each shockwave is checked independantly for "death" and if all are destrroyed, then the timer is released, too. They also hold place holders from 1-8 at the unit, which means that if the caster casts 2times, then they might get overwritten.
Each shockwave could exist on it's own, with knowing it's target/direction, and knowing the corresponding caster, and having it's own timer. Then it can exist completly independently of other shockwaves, and we don't need to track them all from 1-8 anymore for the unit.

Less usable is true enough. My initial thought was to spare 7 timers, as well as creating some sort of "mainframe" for the ability. What I don't get is dummies might get overwritten in multiple casts. Each cast creates a new timer and binds the dummy structs to static child keys under the timer handle as the parent key. Casting twice will simply create a new timer and a new parent key for the structs. How can there be a case of overwrite?

Using "move" order is not best practice for projectile behaviour. Moving a unit with SetUnitX/Y does not even consider something like movement type, or pathability, plus its speed is nicely configurable.

True. But since "move" ability already works in the current mission I'll leave the SetX/Y to slider missions if that's okay :grin:

100 damage/second is a reference value, like for example 100km/h, or 100 miles/h. It does not necessarily mean you need to damage exactly "100" each second, but you can damage of course each iteration, so it sums up to 100/second. You interpreted it probably the other way, and doesn't really matter now, but yeh, it makes it a bit more complicated even I guess, as you need to handle the cooldowns.

Yeah I feel a bit like an idiot for just going ahead with pulse damage, but this is turning out to be a very good learning experience.

At least the timer logic should be inside the struct. Then you only have to create the struct instance and everything else is handled internally in the struct.
The trigger that creates the struct can also be inside the struct, but does not have to be. For example if you want to use this missile in different situations it would make sense to not have all of the triggers inside the struct. Let's say you have a normal shockwave, a shockwave into 8 directions and a shockwave into 4 directions, it would probably be too much to put all of them into the struct. Instead you have 3 normal triggers that all use this struct. If you only have one trigger that utilizes the struct, it's better to put the trigger inside the struct.

About that, I'm not sure I agree. Sure it looks nicer on the inside of the struct, but in usability terms onTimeout has to be a static method, right? And TimerUtils just binds the timer handle to a struct member in a hashtable (just like I already did), right?

In other words I don't see much of a difference between your code:

JASS:
        local thistype d = thistype.create(caster,target,100)
        local timer t = NewTimerEx(d)

and my code (except for the fact all dummies are saved on the same timer handle):

JASS:
    local timer dt = CreateTimer()
    local integer dth = GetHandleId(dt)
    ...
    call SaveInteger(udg_Hash,dth,index,DummyStruct.create(udg_Hero,sx,sy,facing))

Or in other words, even if the timer logic is done "inside the struct", the timeout function still needs a workaround (namely: hashing) in order to be connected to a specific struct instance.

EDIT:

In any case, I put all of the code inside the struct and changed the target testing to an array of groups with static size. Here is the code:


JASS:
struct DummyStruct2
    //========== STRUCT MEMBER DECLERATION ==========
    unit DummyUnit
    unit CasterUnit
    player CasterPlayer
    real DestX
    real DestY
    group array TargetGroup[8] // group array TargetGroup[udg_TimeoutGroup] raises wrong size definition error
    integer CurrentIteration
    //===============================================
   
    //========== CREATE & DESTROY METHODS ==========
    private static method create takes unit caster, real x, real y, real facing returns thistype
        local integer index = 0
       
        local thistype this = thistype.allocate()
        set this.CasterUnit = caster
        set this.CasterPlayer = GetOwningPlayer(caster)
        set this.DummyUnit = CreateUnit(this.CasterPlayer,'h001',x,y,facing)
        set this.DestX = x + 1000.0*Cos(facing*bj_DEGTORAD)
        set this.DestY = y + 1000.0*Sin(facing*bj_DEGTORAD)
        set this.CurrentIteration = 0
       
        loop
            exitwhen index >= udg_TimeoutGroup
           
            set TargetGroup[index] = CreateGroup()
           
            set index = index + 1
        endloop
       
        call IssuePointOrder(this.DummyUnit,"move",this.DestX,this.DestY)
        return this
    endmethod
   
    private method destroy takes nothing returns nothing
        local integer index = 0
       
        call RemoveUnit(this.DummyUnit)
        set this.DummyUnit = null
        set this.CasterUnit = null
        set this.CasterPlayer = null
       
        loop
            exitwhen index >= udg_TimeoutGroup
           
            call DestroyGroup(TargetGroup[index])
            set TargetGroup[index] = null
           
            set index = index + 1
        endloop
       
        call this.deallocate()
    endmethod
    //==============================================
   
    //========== UTILITY FUNCTIONS ==========
    private method DummyInDestination takes real current_x, real current_y returns boolean
        local real distance_x = current_x - this.DestX
        local real distance_y = current_y - this.DestY
        return (distance_x < 50.0 and distance_x > -50.0) and (distance_y < 50.0 and distance_y > -50.0)
    endmethod
   
    private method DummyTargetCheck takes unit u returns boolean
        local integer index = this.CurrentIteration
        set index = index + 1
        if index >= udg_TimeoutGroup then
            set index = 0
        endif
       
        if IsUnitEnemy(u,this.CasterPlayer) and UnitAlive(u) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING) then
            loop
                exitwhen index == this.CurrentIteration
                // call BJDebugMsg("this group: " + I2S(this.CurrentIteration) + " || test group: " + I2S(index))
               
                if IsUnitInGroup(u,this.TargetGroup[index]) then
                    return false
                endif
               
                set index = index + 1
                if index >= udg_TimeoutGroup then
                    set index = 0
                endif
            endloop
           
            return true
        else
            return false
        endif
    endmethod
    //=======================================
   
    private static method onTimeout takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local integer th = GetHandleId(t)
        local thistype this = LoadInteger(udg_Hash,th,0)
        local real current_x = GetUnitX(this.DummyUnit)
        local real current_y = GetUnitY(this.DummyUnit)
        local group collisions
        local unit u
       
        set this.CurrentIteration = this.CurrentIteration + 1
        if this.CurrentIteration >= udg_TimeoutGroup then
            set this.CurrentIteration = 0
        endif
       
        if this.DummyInDestination(current_x,current_y) then
            call DestroyTimer(t)
            call FlushChildHashtable(udg_Hash,th)
            call this.destroy()
            set t = null
        else
            set collisions = CreateGroup()
            call GroupEnumUnitsInRange(collisions,current_x,current_y,100.,null)
            set u = FirstOfGroup(collisions)
            call GroupClear(this.TargetGroup[this.CurrentIteration])
            loop
                exitwhen u == null
               
                if this.DummyTargetCheck(u) then
                    call UnitDamageTarget(this.CasterUnit,u,100.,false,false,ATTACK_TYPE_MAGIC,DAMAGE_TYPE_SONIC,WEAPON_TYPE_WHOKNOWS)
                    call GroupAddUnit(this.TargetGroup[this.CurrentIteration],u)
                endif
               
                call GroupRemoveUnit(collisions,u)
                set u = FirstOfGroup(collisions)
            endloop
        endif
    endmethod
   
    private static method onCast takes nothing returns nothing
        local real facing = GetUnitFacing(udg_Hero)
        local real sx = GetUnitX(udg_Hero)
        local real sy = GetUnitY(udg_Hero)
        local integer index = 0
        local timer dt
        local integer dth
   
        loop
            exitwhen index >= 1
           
            set dt = CreateTimer()
            set dth = GetHandleId(dt)
            call SaveInteger(udg_Hash,dth,0,thistype.create(udg_Hero,sx,sy,facing))
            call TimerStart(dt,0.125,true,function thistype.onTimeout)
       
            set facing = facing + 45.
            set index = index + 1
        endloop
       
        set dt = null
    endmethod
   
    private static method onInit takes nothing returns nothing
        set gg_trg_Dummy_cast_part4_vJASS = CreateTrigger(  )
        call CreateFogModifierRectBJ(true,Player(0),FOG_OF_WAR_VISIBLE,GetPlayableMapRect())
        set udg_Hero = CreateUnit(Player(0),'Otch',0,0,GetRandomReal(0,360))
        set udg_Hash = InitHashtable()
        set udg_TimeoutGroup = 8
   
        // Extra building targets
        call CreateUnit(Player(1),'hbar',2820,-2380,bj_UNIT_FACING)
        call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
        call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
        call CreateUnit(Player(1),'hpea',2620,-2380,bj_UNIT_FACING)
   
        call TriggerRegisterPlayerEvent(gg_trg_Dummy_cast_part4_vJASS,Player(0),EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddAction( gg_trg_Dummy_cast_part4_vJASS, function thistype.onCast)
    endmethod
endstruct


There is one very strange thing I noticed while testing this, though. At first I wanted to initialize the number of groups in TargetGroup array like so group array TargetGroup[udg_TimeoutGroup], but it raised a "wrong size definition error", so does that mean that you can only declare an array size in a struct with size value?

Another, more important thing is that I set the value of udg_TimeoutGroup only in the onInit method, but it did not work. Only after setting the variables initial value in the built-in variable editor did the value of udg_TimeoutGroup stayed as the number I set it to. So does that mean that the onInit method run before the global declaration the map does?

i.e.: onInit method runs setting udg_TimeoutGroup = 8, then the map global decleration runs setting udg_TimeoutGroup to it's initial value in the variable editor (which is 0).
 

Attachments

  • JASS class 4.w3x
    38.9 KB · Views: 71
Last edited:

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
I don't quite understand what you mean here. Do you intend to store units hit by the dummy saved like this
hashtable[parentkay=structindex][childkey=unitkey]
? In that case, how do I store how much time has passed since said unit was hit? Is there a way to check all units saved with a certain parent key?
I was only looking at DummyTargetCheck, yeah in DummyTimerEvent this does not work this way. You can add them to a group and iterate through that group. DummyTimerEvent won't be faster than, but DummyTargetCheck will.
So when a unit is hit you add it to a group member of the struct and store it in a hashtable as you said: hashtable[parentkay=structindex][childkey=unitkey]
This makes it easier for you to detect, if a unit was already hit in DummyTargetCheck.
In DummyTimerEvent you pick every unit in the group and update the cooldown for these units in this struct instance.

Using a timer you probably can make DummyTimerEvent faster as well, because you get rid of constantly updating the cooldown. In this case you do not even need the group.
You start the timer and attach struct instance and unit to it. You will only have a boolean in the hashtable, which indicates if this struct already hit this unit. When the unit is hit, the timer is started, so the boolean is set to false after the cooldown.

I think the code example will make it easier to understand. I wrote this code from my memory without syntax checker, so there could be some mistakes, but it's about the idea to use timers.
JASS:
static method resetCooldown takes nothing returns nothing
        local timer t = GetExpiredTimer()
        local thistype this = LoadInteger(timerTable, GetHandleId(t), 0)
        local integer u = LoadInteger(timerTable, GetHandleId(t), 1)
        call SaveBoolean(structTable, this, u, false)
        call FlushChildHashtable(GetHandleId(t))
        call DestroyTimer(t)
        set t = null
endmethod

method DummyTargetCheck takes unit u returns boolean
        local integer index = 0
        local timer t
        if IsUnitEnemy(u,this.CasterPlayer) and UnitAlive(u) and not IsUnitType(u,UNIT_TYPE_MAGIC_IMMUNE) and not IsUnitType(u,UNIT_TYPE_FLYING) then
            if( LoadBoolean(structTable, this, GetHandleId(u)) == true) then
                return false
            endif
            call SaveBoolean(structTable, this, u, true)
            set t = CreateTimer()
            call TimerStart(t, 1, false, function thistype.resetCooldown)
            call SaveInteger(timerTable, GetHandleId(t), 0, this)
            call SaveInteger(timerTable, GetHandleId(t), 1, GetHandleId(u))
            return true
        else
            return false
        endif
endmethod
with the timer method, you don't need to update anything in DummyTimerEvent.

Yeah I feel a bit like an idiot for just going ahead with pulse damage, but this is turning out to be a very good learning experience.
I think damaging every second instead of more frequently is how it would have been done in warcraft 3. All damage over time abilities in warcraft 3 damage every second (poison, acid bomb, liquid fire, ...)

About that, I'm not sure I agree. Sure it looks nicer on the inside of the struct, but in usability terms
onTimeout
has to be a static method, right? And TimerUtils just binds the timer handle to a struct member in a hashtable (just like I already did), right?
It's not about efficiency or functionality in this case, but following philospohies, that are commonly used in OOP. They make the code more readable. This philosophy is called Encapsulation (computer programming) - Wikipedia


TimerUtils is not different from using a hashtable to attach data to a timer. It is more comfortable to use, if you only want to attach one integer, which can also be a struct instance.
The advantage is, that attaching a struct instance is faster, because only one value has to be loaded from a hashtable and the other values are arrays, which are faster.
The big advantage of TimerUtils, is that you recycle timers instead of creating and destroying them.
You don't have to use TimerUtils, it is just that I had this struct already created before and did not want to change it.[/icode]
 
Level 12
Joined
Jun 15, 2016
Messages
472
Perhaps it wasn't noticed, I've updated my submission in the previous post and changed the target cooldown implementation to an array of groups with a static number.

I think the code example will make it easier to understand. I wrote this code from my memory without syntax checker, so there could be some mistakes, but it's about the idea to use timers.

While it is implementation is more efficient "operation wise", it takes another hashtable to store the units on cooldown. My implementation relies on a static array of groups, checking every unit in every group with this native IsUnitInGroup().

It's not about efficiency or functionality in this case, but following philospohies, that are commonly used in OOP. They make the code more readable. This philosophy is called Encapsulation (computer programming) - Wikipedia

True enough. My initial idea was that instances of the dummy struct are not the same as instances of the spell, but I get why this is important, especially when importing scripts (pun unintended).
 
Status
Not open for further replies.
Top