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

Can someone help me make DOTA-like AI for my map?

Level 17
Joined
Jul 19, 2007
Messages
973
I have a map that it kinda similar to DOTA and it has alot of triggered spells that knockback units or pauses them and this makes problem with the attack-move order because when they are knockedback or paused by a "Cronosphere" ability, the units will stop the attack-move order and just stand in place and this is very annoying so I wonder if anyone could help me fix an AI system for the attack-moving units in my map to never stop the attack-move order?
 
I have a map that it kinda similar to DOTA and it has alot of triggered spells that knockback units or pauses them and this makes problem with the attack-move order because when they are knockedback or paused by a "Cronosphere" ability, the units will stop the attack-move order and just stand in place and this is very annoying so I wonder if anyone could help me fix an AI system for the attack-moving units in my map to never stop the attack-move order?
The simplest way to do this is by combining a timer (0.50s or less) to issue an attack order to AI + unit group to pick a nearby enemy unit with the highest priority to be attacked. You define the priority, which can be related to health, mana, items, level, ability/buff or a combination of all of these.

It's important to include filters in the function's conditions or if/then/else block to prevent the action of issue the attack order to the AI from being executed if the AI is already attacking or performing another action that you deem more important at that moment.
 
Been working on a similar problem (unit responsiveness / avoiding order spam). Biggest improvement for me was consolidating everything into a single decision loop + priority-based order gating.

I also ran into knockback/displacement breaking behavior, so I added a re-engage failsafe (distance > 900 → high-priority attack-move), which helped stabilize things.

I posted a more detailed breakdown here if any part is useful: link
 
Been working on a similar problem (unit responsiveness / avoiding order spam). Biggest improvement for me was consolidating everything into a single decision loop + priority-based order gating.

I also ran into knockback/displacement breaking behavior, so I added a re-engage failsafe (distance > 900 → high-priority attack-move), which helped stabilize things.

I posted a more detailed breakdown here if any part is useful: link
I can't open the map becuase it says trigger functions do not exist.
 
Since you can’t open my map ( I'm using better triggers modification + the map is outdated ), here’s the best way to handle it conceptually, I'm still testing this atm:

Use a single AI decision loop – run on a short timer (e.g., 0.2–0.5 sec) that checks each unit’s status and issues orders if needed.

Re-engage failsafe – if a unit is knocked back or displaced far from its intended target or formation (for example, distance > 900), issue a high-priority Attack-Move to bring it back in.

Priority-based order gating– don’t spam orders unnecessarily. Only issue a new order if:
  • The unit is idle,
  • Or the current order is “out of date” (target moved far away or blocked).
Optional filters – you can define priorities for attack targets (lowest HP, closest, most dangerous buff, etc.) so units always pick the right enemy when re-engaging.



Conceptually, this loop constantly monitors unit position and target status, and gently “corrects” any unit whose attack-move was interrupted by knockback or pause. The result is that units never just stand there — they always try to get back into the fight or formation.


Basically, it’s timer-based monitoring + conditional order issuing rather than trying to rely on continuous orders or static AI behaviors, which get broken by knockback/spells.”

I'll post a portion from the map here since you cannot open it.
  • AI Formation Horde
    • Events
      • Time - Every 0.33 seconds of game time
    • Conditions
      • AI_Emergency Equal to False
      • AI_Cinematic Equal to False
      • AI_Enabled Equal to True
      • (Number of units in AI_HordeGroup) Greater than 0
      • AI_Hold_Horde Equal to False
    • Actions
      • Set AI_LoopIndex_H = 0
      • Set AI_TempPoint_HG = (Position of AI_Thrall)
      • Unit Group - Pick every unit in AI_HordeGroup and do (Actions)
        • Loop - Actions
          • -------- // ----- 1. Determine if there is a threat ---- --------
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • AI_ThreatTarget_H Not equal to No unit
              • (AI_ThreatTarget_H is alive) Equal to True
            • Then - Actions
              • -------- // ----- Distance to threat ----- --------
              • Set AI_TempPoint = (Position of (Picked unit))
              • Set AI_TempPoint2 = (Position of AI_ThreatTarget_H)
              • Set AI_TempReal = (Distance between AI_TempPoint and AI_TempPoint2)
              • Custom script: call RemoveLocation( udg_AI_TempPoint)
              • -------- // ----- Decision based on distance ----- --------
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • AI_TempReal Greater than or equal to 250.00
                  • AI_TempReal Less than or equal to 800.00
                • Then - Actions
                  • -------- // Mid range – advance (attack‑move) --------
                  • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851986, udg_AI_ThreatTarget_H, 12) then
                  • Unit - Order (Picked unit) to Attack-Move To AI_TempPoint2
                  • Custom script: endif
                • Else - Actions
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • AI_TempReal Less than 250.00
                    • Then - Actions
                      • -------- // Close range – attack directly --------
                      • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851983, udg_AI_ThreatTarget_H, 15) then
                      • Unit - Order (Picked unit) to Attack AI_ThreatTarget_H
                      • Custom script: endif
                    • Else - Actions
                      • -------- // Too far ( > 800 ) – move closer (but not formation) --------
                      • -------- // Use attack‑move with lower priority so formation can still override if needed --------
                      • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851986, udg_AI_ThreatTarget_H, 8) then
                      • Unit - Order (Picked unit) to Attack-Move To AI_TempPoint2
                      • Custom script: endif
              • Custom script: call RemoveLocation( udg_AI_TempPoint2)
            • Else - Actions
              • -------- // ----- No threat – formation movement ----- --------
              • -------- // Only move if unit's state is IDLE (0) and distance > threshold --------
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • LoadInteger(udg_AI_Hashtable, GetHandleId(GetEnumUnit()), 9) == 0
                • Then - Actions
                  • -------- // Calculate formation point --------
                  • Set AI_Formation_Angle = ((Facing of AI_Thrall) + (180.00 + (((Real(AI_LoopIndex_H)) mod 8.00) x 45.00)))
                  • Set AI_Formation_Radius = (AI_FormationSpacing + (((Real(AI_LoopIndex_H)) / 8.00) x 80.00))
                  • Set AI_FormationFacing_Angle = ((Facing of AI_Thrall) + 180.00)
                  • Set AI_FormationP_HG = (AI_TempPoint_HG offset by AI_Formation_Radius towards AI_Formation_Angle degrees.)
                  • Set AI_TempTargetX = (X of AI_FormationP_HG)
                  • Set AI_TempTargetY = (Y of AI_FormationP_HG)
                  • Custom script: call AI_FindWalkablePoint(udg_AI_TempTargetX, udg_AI_TempTargetY, udg_AI_SearchStep, udg_AI_MaxSearchRadius)
                  • Set AI_FormationP_HG = (Point(AI_TempTargetX, AI_TempTargetY))
                  • Set AI_PickedUnit_P = (Position of (Picked unit))
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • (Distance between AI_PickedUnit_P and AI_FormationP_HG) Greater than 50.00
                    • Then - Actions
                      • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851986, null, 5) then
                      • Unit - Order (Picked unit) to Move To AI_FormationP_HG
                      • Custom script: endif
                    • Else - Actions
                  • Custom script: call RemoveLocation( udg_AI_FormationP_HG)
                  • Custom script: call RemoveLocation( udg_AI_PickedUnit_P)
                • Else - Actions
          • Set AI_LoopIndex_H = (AI_LoopIndex_H + 1)
      • Custom script: call RemoveLocation( udg_AI_TempPoint_HG)
Code:
//===========================================================================
// Walkability checks
//===========================================================================

function IsTerrainWalkable takes real x, real y returns boolean
    local real dX
    local real dY
    call SetItemVisible(udg_PathingChecker, true)
    call SetItemPosition(udg_PathingChecker, x, y)
    set dX = GetItemX(udg_PathingChecker)
    set dY = GetItemY(udg_PathingChecker)
    call SetItemVisible(udg_PathingChecker, false)
    return (x - dX) * (x - dX) + (y - dY) * (y - dY) < 10 * 10
endfunction

function IsTerrainWalkableLoc takes location l returns boolean
    local real x = GetLocationX(l)
    local real y = GetLocationY(l)
    local real dX
    local real dY
    call SetItemVisible(udg_PathingChecker, true)
    call SetItemPosition(udg_PathingChecker, x, y)
    set dX = GetItemX(udg_PathingChecker)
    set dY = GetItemY(udg_PathingChecker)
    call SetItemVisible(udg_PathingChecker, false)
    return (x - dX) * (x - dX) + (y - dY) * (y - dY) < 10 * 10
endfunction

//===========================================================================
// Spiral search for a walkable point near (x,y)
//===========================================================================

function AI_FindWalkablePoint takes real x, real y, real step, real maxRadius returns nothing
    local real angle = 0.0
    local real radius = step
    local real tx
    local real ty
    loop
        exitwhen radius > maxRadius
        set angle = 0.0
        loop
            exitwhen angle >= 6.28319
            set tx = x + radius * Cos(angle)
            set ty = y + radius * Sin(angle)
            if IsTerrainWalkable(tx, ty) then
                set udg_AI_TempTargetX = tx
                set udg_AI_TempTargetY = ty
                return
            endif
            set angle = angle + 0.5
        endloop
        set radius = radius + step
    endloop
    // fallback: original point
    set udg_AI_TempTargetX = x
    set udg_AI_TempTargetY = y
endfunction

//===========================================================================
// Smart target selection with focus fire and threat weighting
//===========================================================================

function AI_GetBestTarget takes unit hero, group g returns unit
    local unit u
    local unit best = null
    local real bestScore = 999999.0
    local real dx
    local real dy
    local real dist
    local real hp
    local real score
    local unit lastAttacker

    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        set dx = GetUnitX(hero) - GetUnitX(u)
        set dy = GetUnitY(hero) - GetUnitY(u)
        set dist = SquareRoot(dx*dx + dy*dy)
        set hp = GetUnitLifePercent(u) * 100.0
        set score = dist

        // Hero priority
        if IsUnitType(u, UNIT_TYPE_HERO) then
            set score = score * 0.6
        endif

        // Low HP bonus
        set score = score - (100.0 - hp) * 1.5

        // Close range bonus
        if dist < 300.0 then
            set score = score * 0.8
        endif

        // Focus fire bonus
        set lastAttacker = LoadUnitHandle(udg_AI_Hashtable, GetHandleId(hero), 1)
        if u == lastAttacker then
            set score = score * 0.7
        endif

        // Threat bonus for dangerous units (adjust rawcodes to your map)
        if GetUnitTypeId(u) == 'hcth' or GetUnitTypeId(u) == 'hwat' then
            set score = score * 0.8
        endif

        if score < bestScore then
            set bestScore = score
            set best = u
        endif
        call GroupRemoveUnit(g, u)
    endloop
    return best
endfunction



//===========================================================================
// Per‑unit order gate with priority and cooldown
//===========================================================================

function AI_CanIssueOrder takes unit u, integer orderId, unit target, integer priority returns boolean
    local integer id = GetHandleId(u)
    local real lastTime = LoadReal(udg_AI_Hashtable, id, 7)
    local integer lastPrio = LoadInteger(udg_AI_Hashtable, id, 8)
    local real now = TimerGetElapsed(udg_AI_GameTimer)
    local real cooldown = 0.35
    local integer lastOrder
    local unit lastTarget

    // Cooldown and priority check
    if now - lastTime < cooldown and priority <= lastPrio then
        return false
    endif

    // Same order / same target check
    // For move orders (851986) with a target (attack‑move), we DO want to check repetition.
    // For pure move orders (target == null), we allow them to be repeated freely.
    if orderId != 851986 or target != null then
        set lastOrder = LoadInteger(udg_AI_Hashtable, id, 5)
        set lastTarget = LoadUnitHandle(udg_AI_Hashtable, id, 6)
        if lastOrder == orderId and ((target == null and lastTarget == null) or (target == lastTarget)) then
            return false
        endif
    endif

    // Update records
    call SaveReal(udg_AI_Hashtable, id, 7, now)
    call SaveInteger(udg_AI_Hashtable, id, 8, priority)
    call SaveInteger(udg_AI_Hashtable, id, 5, orderId)
    if target != null then
        call SaveUnitHandle(udg_AI_Hashtable, id, 6, target)
    else
        call RemoveSavedHandle(udg_AI_Hashtable, id, 6)
    endif
    return true
endfunction

I'm still polishing all of this, but some snippets from it may come handy to your design I believe.


For a DOTA-style attack-move AI, the cleanest way is to run a single, short-interval decision loop as Chaosium suggested. Each unit checks:
  • Threats (enemy units in range, with priority)
  • Distance to target or lane/formation point
  • Whether it’s idle or displaced
If a unit is knocked back or paused, the loop detects it and reissues a high-priority attack-move toward the correct target or lane.

Importantly, the AI shouldn’t spam orders blindly — you only reissue them if the unit is idle, displaced, or the target changed. This avoids jitter and makes units stay engaged without needing a “suicide” behavior.

I think it could also be useful if someone wants to jump in and weigh the pros and cons of:

  1. Writing a very simple “suicide” AI script (just constant attack-move behavior per Chaosium’s suggestion), versus
  2. Following the slightly more structured approach I outlined here (single decision loop + priority-based orders + re-engage failsafe).
Any thoughts from veterans or people who’ve done similar systems would be very helpful — especially regarding stability, responsiveness, and potential pitfalls.

If you already happen to have your AI script, you can post here, so we can find the simplest workaround
 
Since you can’t open my map ( I'm using better triggers modification + the map is outdated ), here’s the best way to handle it conceptually, I'm still testing this atm:

Use a single AI decision loop – run on a short timer (e.g., 0.2–0.5 sec) that checks each unit’s status and issues orders if needed.

Re-engage failsafe – if a unit is knocked back or displaced far from its intended target or formation (for example, distance > 900), issue a high-priority Attack-Move to bring it back in.

Priority-based order gating– don’t spam orders unnecessarily. Only issue a new order if:
  • The unit is idle,
  • Or the current order is “out of date” (target moved far away or blocked).
Optional filters – you can define priorities for attack targets (lowest HP, closest, most dangerous buff, etc.) so units always pick the right enemy when re-engaging.



Conceptually, this loop constantly monitors unit position and target status, and gently “corrects” any unit whose attack-move was interrupted by knockback or pause. The result is that units never just stand there — they always try to get back into the fight or formation.


Basically, it’s timer-based monitoring + conditional order issuing rather than trying to rely on continuous orders or static AI behaviors, which get broken by knockback/spells.”

I'll post a portion from the map here since you cannot open it.
  • AI Formation Horde
    • Events
      • Time - Every 0.33 seconds of game time
    • Conditions
      • AI_Emergency Equal to False
      • AI_Cinematic Equal to False
      • AI_Enabled Equal to True
      • (Number of units in AI_HordeGroup) Greater than 0
      • AI_Hold_Horde Equal to False
    • Actions
      • Set AI_LoopIndex_H = 0
      • Set AI_TempPoint_HG = (Position of AI_Thrall)
      • Unit Group - Pick every unit in AI_HordeGroup and do (Actions)
        • Loop - Actions
          • -------- // ----- 1. Determine if there is a threat ---- --------
          • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
            • If - Conditions
              • AI_ThreatTarget_H Not equal to No unit
              • (AI_ThreatTarget_H is alive) Equal to True
            • Then - Actions
              • -------- // ----- Distance to threat ----- --------
              • Set AI_TempPoint = (Position of (Picked unit))
              • Set AI_TempPoint2 = (Position of AI_ThreatTarget_H)
              • Set AI_TempReal = (Distance between AI_TempPoint and AI_TempPoint2)
              • Custom script: call RemoveLocation( udg_AI_TempPoint)
              • -------- // ----- Decision based on distance ----- --------
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • AI_TempReal Greater than or equal to 250.00
                  • AI_TempReal Less than or equal to 800.00
                • Then - Actions
                  • -------- // Mid range – advance (attack‑move) --------
                  • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851986, udg_AI_ThreatTarget_H, 12) then
                  • Unit - Order (Picked unit) to Attack-Move To AI_TempPoint2
                  • Custom script: endif
                • Else - Actions
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • AI_TempReal Less than 250.00
                    • Then - Actions
                      • -------- // Close range – attack directly --------
                      • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851983, udg_AI_ThreatTarget_H, 15) then
                      • Unit - Order (Picked unit) to Attack AI_ThreatTarget_H
                      • Custom script: endif
                    • Else - Actions
                      • -------- // Too far ( > 800 ) – move closer (but not formation) --------
                      • -------- // Use attack‑move with lower priority so formation can still override if needed --------
                      • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851986, udg_AI_ThreatTarget_H, 8) then
                      • Unit - Order (Picked unit) to Attack-Move To AI_TempPoint2
                      • Custom script: endif
              • Custom script: call RemoveLocation( udg_AI_TempPoint2)
            • Else - Actions
              • -------- // ----- No threat – formation movement ----- --------
              • -------- // Only move if unit's state is IDLE (0) and distance > threshold --------
              • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                • If - Conditions
                  • LoadInteger(udg_AI_Hashtable, GetHandleId(GetEnumUnit()), 9) == 0
                • Then - Actions
                  • -------- // Calculate formation point --------
                  • Set AI_Formation_Angle = ((Facing of AI_Thrall) + (180.00 + (((Real(AI_LoopIndex_H)) mod 8.00) x 45.00)))
                  • Set AI_Formation_Radius = (AI_FormationSpacing + (((Real(AI_LoopIndex_H)) / 8.00) x 80.00))
                  • Set AI_FormationFacing_Angle = ((Facing of AI_Thrall) + 180.00)
                  • Set AI_FormationP_HG = (AI_TempPoint_HG offset by AI_Formation_Radius towards AI_Formation_Angle degrees.)
                  • Set AI_TempTargetX = (X of AI_FormationP_HG)
                  • Set AI_TempTargetY = (Y of AI_FormationP_HG)
                  • Custom script: call AI_FindWalkablePoint(udg_AI_TempTargetX, udg_AI_TempTargetY, udg_AI_SearchStep, udg_AI_MaxSearchRadius)
                  • Set AI_FormationP_HG = (Point(AI_TempTargetX, AI_TempTargetY))
                  • Set AI_PickedUnit_P = (Position of (Picked unit))
                  • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
                    • If - Conditions
                      • (Distance between AI_PickedUnit_P and AI_FormationP_HG) Greater than 50.00
                    • Then - Actions
                      • Custom script: if AI_CanIssueOrder(GetEnumUnit(), 851986, null, 5) then
                      • Unit - Order (Picked unit) to Move To AI_FormationP_HG
                      • Custom script: endif
                    • Else - Actions
                  • Custom script: call RemoveLocation( udg_AI_FormationP_HG)
                  • Custom script: call RemoveLocation( udg_AI_PickedUnit_P)
                • Else - Actions
          • Set AI_LoopIndex_H = (AI_LoopIndex_H + 1)
      • Custom script: call RemoveLocation( udg_AI_TempPoint_HG)
Code:
//===========================================================================
// Walkability checks
//===========================================================================

function IsTerrainWalkable takes real x, real y returns boolean
    local real dX
    local real dY
    call SetItemVisible(udg_PathingChecker, true)
    call SetItemPosition(udg_PathingChecker, x, y)
    set dX = GetItemX(udg_PathingChecker)
    set dY = GetItemY(udg_PathingChecker)
    call SetItemVisible(udg_PathingChecker, false)
    return (x - dX) * (x - dX) + (y - dY) * (y - dY) < 10 * 10
endfunction

function IsTerrainWalkableLoc takes location l returns boolean
    local real x = GetLocationX(l)
    local real y = GetLocationY(l)
    local real dX
    local real dY
    call SetItemVisible(udg_PathingChecker, true)
    call SetItemPosition(udg_PathingChecker, x, y)
    set dX = GetItemX(udg_PathingChecker)
    set dY = GetItemY(udg_PathingChecker)
    call SetItemVisible(udg_PathingChecker, false)
    return (x - dX) * (x - dX) + (y - dY) * (y - dY) < 10 * 10
endfunction

//===========================================================================
// Spiral search for a walkable point near (x,y)
//===========================================================================

function AI_FindWalkablePoint takes real x, real y, real step, real maxRadius returns nothing
    local real angle = 0.0
    local real radius = step
    local real tx
    local real ty
    loop
        exitwhen radius > maxRadius
        set angle = 0.0
        loop
            exitwhen angle >= 6.28319
            set tx = x + radius * Cos(angle)
            set ty = y + radius * Sin(angle)
            if IsTerrainWalkable(tx, ty) then
                set udg_AI_TempTargetX = tx
                set udg_AI_TempTargetY = ty
                return
            endif
            set angle = angle + 0.5
        endloop
        set radius = radius + step
    endloop
    // fallback: original point
    set udg_AI_TempTargetX = x
    set udg_AI_TempTargetY = y
endfunction

//===========================================================================
// Smart target selection with focus fire and threat weighting
//===========================================================================

function AI_GetBestTarget takes unit hero, group g returns unit
    local unit u
    local unit best = null
    local real bestScore = 999999.0
    local real dx
    local real dy
    local real dist
    local real hp
    local real score
    local unit lastAttacker

    loop
        set u = FirstOfGroup(g)
        exitwhen u == null
        set dx = GetUnitX(hero) - GetUnitX(u)
        set dy = GetUnitY(hero) - GetUnitY(u)
        set dist = SquareRoot(dx*dx + dy*dy)
        set hp = GetUnitLifePercent(u) * 100.0
        set score = dist

        // Hero priority
        if IsUnitType(u, UNIT_TYPE_HERO) then
            set score = score * 0.6
        endif

        // Low HP bonus
        set score = score - (100.0 - hp) * 1.5

        // Close range bonus
        if dist < 300.0 then
            set score = score * 0.8
        endif

        // Focus fire bonus
        set lastAttacker = LoadUnitHandle(udg_AI_Hashtable, GetHandleId(hero), 1)
        if u == lastAttacker then
            set score = score * 0.7
        endif

        // Threat bonus for dangerous units (adjust rawcodes to your map)
        if GetUnitTypeId(u) == 'hcth' or GetUnitTypeId(u) == 'hwat' then
            set score = score * 0.8
        endif

        if score < bestScore then
            set bestScore = score
            set best = u
        endif
        call GroupRemoveUnit(g, u)
    endloop
    return best
endfunction



//===========================================================================
// Per‑unit order gate with priority and cooldown
//===========================================================================

function AI_CanIssueOrder takes unit u, integer orderId, unit target, integer priority returns boolean
    local integer id = GetHandleId(u)
    local real lastTime = LoadReal(udg_AI_Hashtable, id, 7)
    local integer lastPrio = LoadInteger(udg_AI_Hashtable, id, 8)
    local real now = TimerGetElapsed(udg_AI_GameTimer)
    local real cooldown = 0.35
    local integer lastOrder
    local unit lastTarget

    // Cooldown and priority check
    if now - lastTime < cooldown and priority <= lastPrio then
        return false
    endif

    // Same order / same target check
    // For move orders (851986) with a target (attack‑move), we DO want to check repetition.
    // For pure move orders (target == null), we allow them to be repeated freely.
    if orderId != 851986 or target != null then
        set lastOrder = LoadInteger(udg_AI_Hashtable, id, 5)
        set lastTarget = LoadUnitHandle(udg_AI_Hashtable, id, 6)
        if lastOrder == orderId and ((target == null and lastTarget == null) or (target == lastTarget)) then
            return false
        endif
    endif

    // Update records
    call SaveReal(udg_AI_Hashtable, id, 7, now)
    call SaveInteger(udg_AI_Hashtable, id, 8, priority)
    call SaveInteger(udg_AI_Hashtable, id, 5, orderId)
    if target != null then
        call SaveUnitHandle(udg_AI_Hashtable, id, 6, target)
    else
        call RemoveSavedHandle(udg_AI_Hashtable, id, 6)
    endif
    return true
endfunction

I'm still polishing all of this, but some snippets from it may come handy to your design I believe.


For a DOTA-style attack-move AI, the cleanest way is to run a single, short-interval decision loop as Chaosium suggested. Each unit checks:
  • Threats (enemy units in range, with priority)
  • Distance to target or lane/formation point
  • Whether it’s idle or displaced
If a unit is knocked back or paused, the loop detects it and reissues a high-priority attack-move toward the correct target or lane.

Importantly, the AI shouldn’t spam orders blindly — you only reissue them if the unit is idle, displaced, or the target changed. This avoids jitter and makes units stay engaged without needing a “suicide” behavior.

I think it could also be useful if someone wants to jump in and weigh the pros and cons of:

  1. Writing a very simple “suicide” AI script (just constant attack-move behavior per Chaosium’s suggestion), versus
  2. Following the slightly more structured approach I outlined here (single decision loop + priority-based orders + re-engage failsafe).
Any thoughts from veterans or people who’ve done similar systems would be very helpful — especially regarding stability, responsiveness, and potential pitfalls.

If you already happen to have your AI script, you can post here, so we can find the simplest workaround
Well I'm currently just using these triggers to order the defending units to attack-move to enemys castle.
  • Good Attack
    • Events
      • Time - Every 40.00 seconds of game time
    • Conditions
    • Actions
      • Set VariableSet IntWaveNumberGood = (IntWaveNumberGood + 1)
      • -------- Lane 1 --------
      • Unit - Create 4 Gondor Swordsman for Player 10 (Light Blue) at GoodAttack[0] facing Default building facing degrees
      • Unit - Create 2 Rohan Archer for Player 10 (Light Blue) at GoodAttack[0] facing Default building facing degrees
      • Unit - Create 1 Elven Archer for Player 10 (Light Blue) at GoodAttack[0] facing Default building facing degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberGood Equal to 4
        • Then - Actions
          • Unit - Create 1 Huorn for Player 10 (Light Blue) at GoodAttack[0] facing Default building facing degrees
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberGood Greater than or equal to IntWaveNumberGoodSpawnDwarfL1
          • (Orc Mound 0088 <gen> is alive) Equal to True
        • Then - Actions
          • Unit - Create 1 Dwarf Shieldbreaker for Player 10 (Light Blue) at GoodAttack[0] facing Default building facing degrees
          • Set VariableSet IntWaveNumberGoodSpawnDwarfL1 = (IntWaveNumberGoodSpawnDwarfL1 + 2)
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Orc Mound 0088 <gen> is alive) Equal to False
        • Then - Actions
          • Unit - Create 1 Dwarf Shieldbreaker for Player 10 (Light Blue) at GoodAttack[0] facing Default building facing degrees
        • Else - Actions
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Good Attacks 1 <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[2])
      • -------- Lane 2 --------
      • Unit - Create 4 Gondor Swordsman for Player 10 (Light Blue) at GoodAttack[1] facing Default building facing degrees
      • Unit - Create 2 Rohan Archer for Player 10 (Light Blue) at GoodAttack[1] facing Default building facing degrees
      • Unit - Create 1 Elven Archer for Player 10 (Light Blue) at GoodAttack[1] facing Default building facing degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberGood Equal to 4
        • Then - Actions
          • Unit - Create 1 Huorn for Player 10 (Light Blue) at GoodAttack[1] facing Default building facing degrees
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberGood Greater than or equal to IntWaveNumberGoodSpawnDwarfL2
          • (Orc Mound 0087 <gen> is alive) Equal to True
        • Then - Actions
          • Unit - Create 1 Dwarf Shieldbreaker for Player 10 (Light Blue) at GoodAttack[1] facing Default building facing degrees
          • Set VariableSet IntWaveNumberGoodSpawnDwarfL2 = (IntWaveNumberGoodSpawnDwarfL2 + 2)
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Orc Mound 0087 <gen> is alive) Equal to False
        • Then - Actions
          • Unit - Create 1 Dwarf Shieldbreaker for Player 10 (Light Blue) at GoodAttack[1] facing Default building facing degrees
        • Else - Actions
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Good Attacks 2 <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[3])
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberGood Greater than or equal to 6
        • Then - Actions
          • Set VariableSet IntWaveNumberGood = 0
          • Set VariableSet IntWaveNumberGoodSpawnDwarfL1 = 2
          • Set VariableSet IntWaveNumberGoodSpawnDwarfL2 = 2
        • Else - Actions
  • Evil Attack
    • Events
      • Time - Every 40.00 seconds of game time
    • Conditions
    • Actions
      • Set VariableSet IntWaveNumberEvil = (IntWaveNumberEvil + 1)
      • -------- Lane 1 --------
      • Unit - Create 4 Orc Slasher for Player 12 (Brown) at EvilAttack[0] facing Default building facing degrees
      • Unit - Create 2 Goblin Archer for Player 12 (Brown) at EvilAttack[0] facing Default building facing degrees
      • Unit - Create 1 Uruk-Hai Bowman for Player 12 (Brown) at EvilAttack[0] facing Default building facing degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberEvil Equal to 4
        • Then - Actions
          • Unit - Create 1 Troll Bonecleaver for Player 12 (Brown) at EvilAttack[0] facing Default building facing degrees
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberEvil Greater than or equal to IntWaveNumberEvilSpawnUrukL1
          • (Barracks 0045 <gen> is alive) Equal to True
        • Then - Actions
          • Unit - Create 1 Uruk-Hai Berserker for Player 12 (Brown) at EvilAttack[0] facing Default building facing degrees
          • Set VariableSet IntWaveNumberEvilSpawnUrukL1 = (IntWaveNumberEvilSpawnUrukL1 + 2)
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Barracks 0045 <gen> is alive) Equal to False
        • Then - Actions
          • Unit - Create 1 Uruk-Hai Berserker for Player 12 (Brown) at EvilAttack[0] facing Default building facing degrees
        • Else - Actions
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Evil Attacks 1 <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[3])
      • -------- Lane 2 --------
      • Unit - Create 4 Orc Slasher for Player 12 (Brown) at EvilAttack[1] facing Default building facing degrees
      • Unit - Create 2 Goblin Archer for Player 12 (Brown) at EvilAttack[1] facing Default building facing degrees
      • Unit - Create 1 Uruk-Hai Bowman for Player 12 (Brown) at EvilAttack[1] facing Default building facing degrees
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberEvil Equal to 4
        • Then - Actions
          • Unit - Create 1 Troll Bonecleaver for Player 12 (Brown) at EvilAttack[1] facing Default building facing degrees
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberEvil Greater than or equal to IntWaveNumberEvilSpawnUrukL2
          • (Barracks 0046 <gen> is alive) Equal to True
        • Then - Actions
          • Unit - Create 1 Uruk-Hai Berserker for Player 12 (Brown) at EvilAttack[1] facing Default building facing degrees
          • Set VariableSet IntWaveNumberEvilSpawnUrukL2 = (IntWaveNumberEvilSpawnUrukL2 + 2)
        • Else - Actions
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • (Barracks 0046 <gen> is alive) Equal to False
        • Then - Actions
          • Unit - Create 1 Uruk-Hai Berserker for Player 12 (Brown) at EvilAttack[1] facing Default building facing degrees
        • Else - Actions
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Evil Attacks 2 <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[2])
      • If (All Conditions are True) then do (Then Actions) else do (Else Actions)
        • If - Conditions
          • IntWaveNumberEvil Greater than or equal to 6
        • Then - Actions
          • Set VariableSet IntWaveNumberEvil = 0
          • Set VariableSet IntWaveNumberEvilSpawnUrukL1 = 2
          • Set VariableSet IntWaveNumberEvilSpawnUrukL2 = 2
        • Else - Actions
  • Orders
    • Events
      • Time - Every 1.00 seconds of game time
    • Conditions
    • Actions
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in good move 1 <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[2])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in bad move 1 Copy <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in good move 1 Copy <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[3])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in bad move 1 Copy Copy <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in bad move 1 Copy Copy <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[3])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in good move 1 Copy <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in bad move 1 Copy <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[2])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in good move 1 <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Not enter goods fountain <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Not enter goods fountain 2 <gen> owned by Player 12 (Brown)) and do (Unit - Order (Picked unit) to Attack-Move To EvilAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Not enter evils fountain <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[4])
      • Custom script: set bj_wantDestroyGroup=true
      • Unit Group - Pick every unit in (Units in Not enter evils fountain 2 <gen> owned by Player 10 (Light Blue)) and do (Unit - Order (Picked unit) to Attack-Move To GoodAttack[4])
Btw I don't think I can do the AI by myself because I do not understand how it works. Would be thankful if someone could try fix it for my map.
 
Your current setup already works as a wave system, the main issue is just how often orders are being issued.

If you want to improve it step by step, I’d start with this:

-remove the 1.00 second global order loop
only issue attack-move once (on spawn or when units are idle)

-let units handle combat naturally instead of forcing behavior every tick

That alone will make it feel much smoother without rewriting everything.

If you want to go further, then it becomes more about per-unit logic (cooldowns, conditions, etc.), but that’s something you build gradually and test thoroughly rather than replacing everything at once ; otherwise it becomes hard to maintain and scale.
 
Your current setup already works as a wave system, the main issue is just how often orders are being issued.

If you want to improve it step by step, I’d start with this:

-remove the 1.00 second global order loop
only issue attack-move once (on spawn or when units are idle)

-let units handle combat naturally instead of forcing behavior every tick

That alone will make it feel much smoother without rewriting everything.

If you want to go further, then it becomes more about per-unit logic (cooldowns, conditions, etc.), but that’s something you build gradually and test thoroughly rather than replacing everything at once ; otherwise it becomes hard to maintain and scale.
Ehm... If I remove the 1.00 global order loop then they will not attack-move at all and just stand in place where they was summoned? What do you mean with "let units handle combat naturally"...? I'm totally lost in how to make the AI because I've never used it before...
 
Here's one template, open it with a text editor (ideally Jasscraft but notepad+ can work I guess), you just need to imput your custom units raw codes and apply it to the computer player and it should attack automatically
Player 11 and Player 12 is the computer players in my map. So I should do something like this?
script.png
 
Yes and you need to place that somewhere at the beginning of the map

  • Actions
    • AI - Start campaign AI script for Player 11 (Dark Green): Script1.ai
    • AI - Start campaign AI script for Player 12 (Brown): Script2.ai
Ok I just made it like that and now indeed they keep attack-moving to the ordered region but it seems like they start to acting very wierdly too because they start to moving to the middle of the map and attacking neutral creeps which is meant for jungling for players :eek:2 Why is this happening?
 
The weird behavior happens because AI scripts ignore your lane setup and will target anything (including neutrals).

I’d stick to triggers for your map and just fix your current system instead.

  • Events
  • AI_OrderAttack
    • Events
      • Time - Every 0.50 seconds of game time
    • Conditions
    • Actions
      • // ----- Player 10 (Light Blue) -----
      • Set TempGroup = (Units owned by Player 10 (Light Blue))
      • Unit Group - Pick every unit in TempGroup and do (Actions)
        • Loop - Actions
          • Set Picked = (Picked unit)
          • If (All Conditions are True) then do (Then Actions)
            • If - Conditions
              • (Picked is alive) Equal to True
              • Or - Any (Conditions) are true
                • Conditions
                  • (Picked is idle) Equal to True
                  • (Current order of Picked) Not equal to (Order(attack))
            • Then - Actions
              • Unit - Order Picked to Attack-Move To EvilAttack[2]
            • Else - Actions
          • Else - Actions
      • Custom script: call DestroyGroup(udg_TempGroup)
      • // ----- Player 12 (Brown) -----
      • Set TempGroup = (Units owned by Player 12 (Brown))
      • Unit Group - Pick every unit in TempGroup and do (Actions)
        • Loop - Actions
          • Set Picked = (Picked unit)
          • If (All Conditions are True) then do (Then Actions)
            • If - Conditions
              • (Picked is alive) Equal to True
              • Or - Any (Conditions) are true
                • Conditions
                  • (Picked is idle) Equal to True
                  • (Current order of Picked) Not equal to (Order(attack))
            • Then - Actions
              • Unit - Order Picked to Attack-Move To GoodAttack[2]
            • Else - Actions
          • Else - Actions
      • Custom script: call DestroyGroup(udg_TempGroup)


Important: Remove your 1.00 sec global order loop; the lane logic already comes from spawn + the initial attack-move order, not the 1.00 sec loop.

 
The weird behavior happens because AI scripts ignore your lane setup and will target anything (including neutrals).

I’d stick to triggers for your map and just fix your current system instead.

  • Events
  • AI_OrderAttack
    • Events
      • Time - Every 0.50 seconds of game time
    • Conditions
    • Actions
      • // ----- Player 10 (Light Blue) -----
      • Set TempGroup = (Units owned by Player 10 (Light Blue))
      • Unit Group - Pick every unit in TempGroup and do (Actions)
        • Loop - Actions
          • Set Picked = (Picked unit)
          • If (All Conditions are True) then do (Then Actions)
            • If - Conditions
              • (Picked is alive) Equal to True
              • Or - Any (Conditions) are true
                • Conditions
                  • (Picked is idle) Equal to True
                  • (Current order of Picked) Not equal to (Order(attack))
            • Then - Actions
              • Unit - Order Picked to Attack-Move To EvilAttack[2]
            • Else - Actions
          • Else - Actions
      • Custom script: call DestroyGroup(udg_TempGroup)
      • // ----- Player 12 (Brown) -----
      • Set TempGroup = (Units owned by Player 12 (Brown))
      • Unit Group - Pick every unit in TempGroup and do (Actions)
        • Loop - Actions
          • Set Picked = (Picked unit)
          • If (All Conditions are True) then do (Then Actions)
            • If - Conditions
              • (Picked is alive) Equal to True
              • Or - Any (Conditions) are true
                • Conditions
                  • (Picked is idle) Equal to True
                  • (Current order of Picked) Not equal to (Order(attack))
            • Then - Actions
              • Unit - Order Picked to Attack-Move To GoodAttack[2]
            • Else - Actions
          • Else - Actions
      • Custom script: call DestroyGroup(udg_TempGroup)


Important: Remove your 1.00 sec global order loop; the lane logic already comes from spawn + the initial attack-move order, not the 1.00 sec loop.

Hmm I will try but in my map there is villagers each camp owned by lightblue and brown and they are meant to just stand in place and do nothing. If I do it like this then also villagers will start to move along with the defending units who is supposed to attack-move to the enemies castle?
 
Hmm I will try but in my map there is villagers each camp owned by lightblue and brown and they are meant to just stand in place and do nothing. If I do it like this then also villagers will start to move along with the defending units who is supposed to attack-move to the enemies castle?
You would want to filter only the attacking units, just assign attack-move orders on spawn and only loop for units that might get stuck. You would want to tweak and test the behaviour, since I do not know the layout of your map nor the specs. If you really get stuck send me the map in dm.
 
Back
Top