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

Click-And-Hold Movement

A small library to enable click-and-hold movement (hold the right mouse-button to move your hero) for hero maps like aeon of strife, hero arenas, or rpgs, especially maps in which you have to move and dodge really fast. Every hero map should have a click-and-hold feature to not make its players suffer from carpal tunnel. :prazz:

To install, simply copy the code below into an empty trigger in your map.

JASS:
library ClickAndHoldMovement initializer Init

    /*
    ===========================================================================================
                                        Click-And-Hold Movement
                                            by Antares
                                        
    Enable click-and-hold movement (hold the right mouse-button to move your hero) for hero maps
                            like aeon of strife, hero arenas, or rpgs.
    ===========================================================================================
    */

globals
    //====================================================
    //Config
    //====================================================
    private constant real IGNORE_MOUSE_SPELL_DURATION         = 0.5    //Disables the click-and-hold trigger for this many seconds when a unit in the selected group casts a spell. This enables the user to cast a spell without releasing the right mouse-button.
    private constant real IGNORE_MOUSE_CLICK_DURATION         = 0.25    //Disables the click-and-hold trigger for this many seconds right after the right mouse-button is pressed. This prevents the trigger from firing on a simple right-click.
    private constant real MOUSE_MOVE_COOLDOWN_DURATION         = 0.1    //Duration of the cooldown on the click-and-hold trigger on giving orders.
 
    private constant boolean DISABLE_IF_MULTIPLE_SELECTED     = false
    private constant boolean STOP_MOVING_ON_RELEASE            = false //The commanded units will stop moving as soon as the right mouse-button is released. They will also ignore pathing blockers between the mouse-cursor and the unit.
    private constant boolean CUSTOM_CALLBACK                = false    //Enable if you want to write your own callback function for the click-and-hold trigger. Otherwise, it will be a replaced by a simple smart command.
 
    //====================================================

    private trigger array mouseMoveTrigger
    real array mouseX
    real array mouseY
    private boolean array rightClickOn
    private timer array ignoreMouseTimer
    private group array selectedUnits
    private boolean array setGroup
    private boolean array noOrders
endglobals

//====================================================
private function CustomCallback takes unit whichUnit, real x, real y returns nothing
    //Define your custom callback code here.
endfunction
//====================================================

private function SetAndCheckSelection takes integer P returns boolean
    local boolean invalidSelection
    local unit first
    local player whichPlayer = Player(P)

    set selectedUnits[P] = CreateGroup()
    call GroupEnumUnitsSelected(selectedUnits[P] , whichPlayer , null)

    static if DISABLE_IF_MULTIPLE_SELECTED then
        set invalidSelection = BlzGroupGetSize(selectedUnits[P]) != 1
    else
        set invalidSelection = BlzGroupGetSize(selectedUnits[P]) == 0
    endif
 
    set setGroup[P] = false

    if invalidSelection then
        call DestroyGroup(selectedUnits[P])
        return false
    else
        set first = FirstOfGroup(selectedUnits[P])
        if GetOwningPlayer(first) == whichPlayer or (IsUnitAlly(first,whichPlayer) and GetPlayerAlliance(GetOwningPlayer(first),whichPlayer,ALLIANCE_SHARED_CONTROL)) then
            set first = null
            return true
        else
            call DestroyGroup(selectedUnits[P])
            set first = null
            return false
        endif
    endif
endfunction

private function CommandGroup takes integer P returns nothing
    local integer i = BlzGroupGetSize(selectedUnits[P]) - 1
    local unit u
    local real angle
    loop
    exitwhen i < 0
        static if CUSTOM_CALLBACK then
            call CustomCallback( BlzGroupUnitAt(selectedUnits[P] , i) , mouseX[P] , mouseY[P] )
        elseif STOP_MOVING_ON_RELEASE then
            set u = BlzGroupUnitAt(selectedUnits[P] , i)
            set angle = Atan2( mouseY[P] - GetUnitY(u) , mouseX[P] - GetUnitX(u) )
            call IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , GetUnitX(u) + 50*Cos(angle) , GetUnitY(u) + 50*Sin(angle) )
        else
            call IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , mouseX[P] , mouseY[P] )
        endif
        set i = i - 1
    endloop
 
    set u = null
endfunction

private function Enable takes nothing returns nothing
    local integer i
    local integer P = 0
    local timer t = GetExpiredTimer()

    loop
        exitwhen t == ignoreMouseTimer[P]
        set P = P + 1
    endloop

    set noOrders[P] = false
 
    if rightClickOn[P] then
        if setGroup[P] then
            if SetAndCheckSelection(P) then
                call CommandGroup(P)
            endif
        else
            call CommandGroup(P)
        endif
    endif
    set t = null
endfunction

private function MouseSpellIgnore takes nothing returns nothing
    local unit whichUnit = GetSpellAbilityUnit()
    local integer P = GetPlayerId(GetOwningPlayer(whichUnit))
 
    if IsUnitInGroup(whichUnit , selectedUnits[P]) then
        set noOrders[P] = true
        call TimerStart( ignoreMouseTimer[P] , IGNORE_MOUSE_SPELL_DURATION , false , function Enable )
    endif
    set whichUnit = null
endfunction

private function MouseMove takes nothing returns nothing
    local integer i
    local integer P = GetPlayerId(GetTriggerPlayer())
    local real mX = BlzGetTriggerPlayerMouseX()
    local real mY = BlzGetTriggerPlayerMouseY()

    if mY != 0 or mX != 0 then
        set mouseX[P] = mX
        set mouseY[P] = mY
    endif
 
    if noOrders[P] then
        return
    endif
 
    if MOUSE_MOVE_COOLDOWN_DURATION > 0 then
        set noOrders[P] = true
        call TimerStart( ignoreMouseTimer[P] , MOUSE_MOVE_COOLDOWN_DURATION , false , function Enable )
    endif

    call CommandGroup(P)
endfunction

private function MouseRightClick takes nothing returns nothing
    local integer A
    local integer P
 
    if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT then
        set P = GetPlayerId(GetTriggerPlayer())
        set rightClickOn[P] = true
        set noOrders[P] = true
        set setGroup[P] = true
        set mouseX[P] = BlzGetTriggerPlayerMouseX()
        set mouseY[P] = BlzGetTriggerPlayerMouseY()
        call TimerStart( ignoreMouseTimer[P] , IGNORE_MOUSE_CLICK_DURATION , false , function Enable )
        call EnableTrigger(mouseMoveTrigger[P])
    endif
endfunction

private function MouseRightRelease takes nothing returns nothing
    local integer P
    if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT then
        set P = GetPlayerId(GetTriggerPlayer())
        call DestroyGroup(selectedUnits[P])
        set rightClickOn[P] = false
        call DisableTrigger(mouseMoveTrigger[P])
    endif
endfunction

private function At0s takes nothing returns nothing
    local integer P = 0
    local trigger trigDown = CreateTrigger()
    local trigger trigUp = CreateTrigger()
    local trigger trigIgnore = CreateTrigger()
    local trigger trigStore = CreateTrigger()

    loop
    exitwhen P > 23
        if GetPlayerSlotState(Player(P)) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(Player(P)) == MAP_CONTROL_USER then
            set mouseMoveTrigger[P] = CreateTrigger()
            call TriggerRegisterPlayerEvent( trigDown, Player(P), EVENT_PLAYER_MOUSE_DOWN )
            call TriggerRegisterPlayerEvent( trigUp, Player(P), EVENT_PLAYER_MOUSE_UP )
            call TriggerRegisterPlayerEvent( mouseMoveTrigger[P], Player(P), EVENT_PLAYER_MOUSE_MOVE )
            call TriggerAddCondition( mouseMoveTrigger[P] , function MouseMove )
            call DisableTrigger(mouseMoveTrigger[P])
            set ignoreMouseTimer[P] = CreateTimer()
        endif
        set P = P + 1
    endloop
    call DestroyTrigger(GetTriggeringTrigger())
 
    call TriggerAddCondition( trigDown , function MouseRightClick )
    call TriggerAddCondition( trigUp , function MouseRightRelease )
 
    call TriggerRegisterAnyUnitEventBJ( trigIgnore, EVENT_PLAYER_UNIT_SPELL_CAST )
    call TriggerAddCondition( trigIgnore , function MouseSpellIgnore )
 
    set trigDown = null
    set trigUp = null
    set trigIgnore = null
    set trigStore = null
endfunction

private function Init takes nothing returns nothing
    local trigger at0sTrigger = CreateTrigger()
    call TriggerRegisterTimerEvent( at0sTrigger , 0.0 , false )
    call TriggerAddAction( at0sTrigger , function At0s )
    set at0sTrigger = null
endfunction

endlibrary

Lua:
do  LIBRARY_ClickAndHoldMovement = true
    local SCOPE_PREFIX = "ClickAndHoldMovement_" ---@type string
    --[[
    ===========================================================================================
                                        Click-And-Hold Movement
                                            by Antares
                                         
    Enable click-and-hold movement (hold the right mouse-button to move your hero) for hero maps
                            like aeon of strife, hero arenas, or rpgs.

                                Requires totalInitialization.
    ===========================================================================================
    ]]
 
 
    --====================================================
    --Config
    --====================================================
    local IGNORE_MOUSE_SPELL_DURATION            = 0.5    ---@type number --Disables the click-and-hold trigger for this many seconds when a unit in the selected group casts a spell. This enables the user to cast a spell without releasing the right mouse-button.
    local IGNORE_MOUSE_CLICK_DURATION            = 0.25   ---@type number --Disables the click-and-hold trigger for this many seconds right after the right mouse-button is pressed. This prevents the trigger from firing on a simple right-click.
    local MOUSE_MOVE_COOLDOWN_DURATION           = 0.1    ---@type number --Duration of the cooldown on the click-and-hold trigger on giving orders.
 
    local DISABLE_IF_MULTIPLE_SELECTED           = false ---@type boolean
    local STOP_MOVING_ON_RELEASE                 = false  ---@type boolean --The commanded units will stop moving as soon as the right mouse-button is released. They will also ignore pathing blockers between the mouse-cursor and the unit.
    local CUSTOM_CALLBACK                        = false  ---@type boolean --Enable if you want to write your own callback function for the click-and-hold trigger. Otherwise, it will be a replaced by a simple smart command.
 
    --====================================================
    local mouseMoveTrigger={} ---@type trigger[]
    MouseX = {} ---@type number[]
    MouseY = {} ---@type number[]
    local rightClickOn = {} ---@type boolean[]
    local ignoreMouseTimer = {} ---@type timer[]
    local data = {}
    local selectedUnits={} ---@type group[]
    local setGroup = {} ---@type boolean[]
    local noOrders = {} ---@type boolean[]
 
    --====================================================
    ---@param whichUnit unit
    ---@param x number
    ---@param y number
    local function CustomCallback(whichUnit, x, y)
        --Define your custom callback code here.
    end
    --====================================================
 
    ---@param P player
    ---@return boolean
    local function SetAndCheckSelection(P)
        local invalidSelection ---@type boolean
        local first ---@type unit
 
        selectedUnits[P] = CreateGroup()
        GroupEnumUnitsSelected(selectedUnits[P] , P , nil)
 
        if DISABLE_IF_MULTIPLE_SELECTED then
            invalidSelection = BlzGroupGetSize(selectedUnits[P]) ~= 1
        else
            invalidSelection = BlzGroupGetSize(selectedUnits[P]) == 0
        end
     
        setGroup[P] = false
        if invalidSelection then
            DestroyGroup(selectedUnits[P])
            return false
        else
            first = FirstOfGroup(selectedUnits[P])
            if GetOwningPlayer(first) == P or (IsUnitAlly(first,P) and GetPlayerAlliance(GetOwningPlayer(first),P,ALLIANCE_SHARED_CONTROL)) then
                return true
            else
                DestroyGroup(selectedUnits[P])
                return false
            end
        end
    end
 
    ---@param P player
    local function CommandGroup(P)
        local i = BlzGroupGetSize(selectedUnits[P]) - 1 ---@type integer
        local u ---@type unit
        local angle ---@type number
        while i >= 0 do
            if CUSTOM_CALLBACK then
                CustomCallback( BlzGroupUnitAt(selectedUnits[P] , i) , MouseX[P] , MouseY[P] )
            elseif STOP_MOVING_ON_RELEASE then
                u = BlzGroupUnitAt(selectedUnits[P] , i)
                angle = Atan2( MouseY[P] - GetUnitY(u) , MouseX[P] - GetUnitX(u) )
                IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , GetUnitX(u) + 50*Cos(angle) , GetUnitY(u) + 50*Sin(angle) )
            else
                IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , MouseX[P] , MouseY[P] )
            end
            i = i - 1
        end
    end
 
    local function Enable()
        local P = data[GetExpiredTimer()] ---@type player
        noOrders[P] = false
     
        if rightClickOn[P] then
            if setGroup[P] then
                if SetAndCheckSelection(P) then
                    CommandGroup(P)
                end
            else
                CommandGroup(P)
            end
        end
    end
 
    local function MouseSpellIgnore()
        local whichUnit = GetSpellAbilityUnit() ---@type unit
        local P = GetOwningPlayer(whichUnit) ---@type player
     
        if IsUnitInGroup(whichUnit , selectedUnits[P]) then
            noOrders[P] = true
            TimerStart( ignoreMouseTimer[P] , IGNORE_MOUSE_SPELL_DURATION , false , Enable )
        end
    end
 
    local function MouseMove()
        local P = GetTriggerPlayer() ---@type player
        local mX = BlzGetTriggerPlayerMouseX() ---@type number
        local mY = BlzGetTriggerPlayerMouseY() ---@type number
        if mY ~= 0 or mX ~= 0 then
            MouseX[P] = mX
            MouseY[P] = mY
        end
     
        if noOrders[P] then
            return
        end
 
        if MOUSE_MOVE_COOLDOWN_DURATION > 0 then
            noOrders[P] = true
            TimerStart( ignoreMouseTimer[P] , MOUSE_MOVE_COOLDOWN_DURATION , false , Enable )
        end
        CommandGroup(P)
    end
 
    local function MouseRightClick()
        local P ---@type player
     
        if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT then
            P = GetTriggerPlayer()
            rightClickOn[P] = true
            noOrders[P] = true
            setGroup[P] = true
            MouseX[P] = BlzGetTriggerPlayerMouseX()
            MouseY[P] = BlzGetTriggerPlayerMouseY()
            TimerStart( ignoreMouseTimer[P] , IGNORE_MOUSE_CLICK_DURATION , false , Enable )
            EnableTrigger(mouseMoveTrigger[P])
        end
    end
 
    local function MouseRightRelease()
        local P ---@type player
        if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_RIGHT then
            P = GetTriggerPlayer()
            DestroyGroup(selectedUnits[P])
            rightClickOn[P] = false
            DisableTrigger(mouseMoveTrigger[P])
        end
    end
 
    local function At0s()
        local P ---@type player
        local trigDown = CreateTrigger() ---@type trigger
        local trigUp = CreateTrigger() ---@type trigger
        local trigIgnore = CreateTrigger() ---@type trigger
 
        for p = 0, 23 do
            P = Player(p)
            if GetPlayerSlotState(P) == PLAYER_SLOT_STATE_PLAYING and GetPlayerController(P) == MAP_CONTROL_USER then
                mouseMoveTrigger[P] = CreateTrigger()
                TriggerRegisterPlayerEvent( trigDown, P, EVENT_PLAYER_MOUSE_DOWN )
                TriggerRegisterPlayerEvent( trigUp, P, EVENT_PLAYER_MOUSE_UP )
                TriggerRegisterPlayerEvent( mouseMoveTrigger[P], P, EVENT_PLAYER_MOUSE_MOVE )
                TriggerAddAction( mouseMoveTrigger[P] , MouseMove )
                DisableTrigger(mouseMoveTrigger[P])
                ignoreMouseTimer[P] = CreateTimer()
                data[ignoreMouseTimer[P]] = P
            end
            p = p + 1
        end
        DestroyTrigger(GetTriggeringTrigger())
     
        TriggerAddAction( trigDown , MouseRightClick )
        TriggerAddAction( trigUp , MouseRightRelease )
     
        TriggerRegisterAnyUnitEventBJ( trigIgnore, EVENT_PLAYER_UNIT_SPELL_CAST )
        TriggerAddAction( trigIgnore , MouseSpellIgnore )
    end
 
    OnInit.global(function()
        local at0sTrigger = CreateTrigger() ---@type trigger
        TriggerRegisterTimerEvent( at0sTrigger , 0.0 , false )
        TriggerAddAction( at0sTrigger , At0s )
    end)
end
Contents

Click-And-Hold Movement (Map)

Reviews
Wrda
Changes were made, no leaks, works as expected. Approved

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
You have a lot of local reference leaks in most functions (no nulling before returning true or false, for example).

In enable function, you have the following code 2 times, each one in different parts of the if condition:
JASS:
set i = BlzGroupGetSize(selectedUnits[P]) - 1
loop
exitwhen i < 0
    static if CUSTOM_CALLBACK then
        call CustomCallback( BlzGroupUnitAt(selectedUnits[P] , i) , mouseX[P] , mouseY[P] )
    else
        call IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , mouseX[P] , mouseY[P] )
    endif
    set i = i - 1
endloop
You could engulf it into a function instead.

GetPlayerSlotState is known for causing desyncs at map initialization, better to set up the triggers with an expired timer of 0s.

A nice alternative to the right-click movement system for weak hands. 😁
Needs a few fixes nonetheless.
 
You have a lot of local reference leaks in most functions (no nulling before returning true or false, for example).

In enable function, you have the following code 2 times, each one in different parts of the if condition:
JASS:
set i = BlzGroupGetSize(selectedUnits[P]) - 1
loop
exitwhen i < 0
    static if CUSTOM_CALLBACK then
        call CustomCallback( BlzGroupUnitAt(selectedUnits[P] , i) , mouseX[P] , mouseY[P] )
    else
        call IssuePointOrderById( BlzGroupUnitAt(selectedUnits[P] , i) , 851971 , mouseX[P] , mouseY[P] )
    endif
    set i = i - 1
endloop
You could engulf it into a function instead.

GetPlayerSlotState is known for causing desyncs at map initialization, better to set up the triggers with an expired timer of 0s.

A nice alternative to the right-click movement system for weak hands. 😁
Needs a few fixes nonetheless.
Fixed all of those.
 

Wrda

Spell Reviewer
Level 26
Joined
Nov 18, 2012
Messages
1,888
You've missed the nulling of local unit "first" in the first return false, "whichPlayer" is also leaking (since it's an agent) in function SetAndCheckSelection.
JASS:
private function MouseMove takes nothing returns nothing
    local integer i
    local player whichPlayer = GetTriggerPlayer()
    local integer P = GetPlayerId(whichPlayer)
    local real mX = BlzGetTriggerPlayerMouseX()
    local real mY = BlzGetTriggerPlayerMouseY()

    if mY != 0 or mX != 0 then
        set mouseX[P] = mX
        set mouseY[P] = mY
    endif
    
    if noOrders[P] then
        return
    endif
    
    if MOUSE_MOVE_COOLDOWN_DURATION > 0 then
        set noOrders[P] = true
        call TimerStart( ignoreMouseTimer[P] , MOUSE_MOVE_COOLDOWN_DURATION , false , function Enable )
    endif

    call CommandGroup(P)
endfunction
whichPlayer isn't being nulled here either, and could just use local integer P = GetPlayerId(GetTriggerPlayer()) instead.
At0s and Init triggers' trigger variables also have to be nulled.

I would approve in good faith but our guidelines prevent us to do that, and wouldn't be fair for the rest of the section.
 
You're mistaken on the first leak. It does not need to be nulled here because the variable is not set yet at that point. Player variables also don't have to be nulled because players won't ever get deleted and so a reference leak can never manifest itself.

I made the change
Code:
local integer P = GetPlayerId(GetTriggerPlayer())
that you suggested. Must have used that variable for something else later in the code, but removed it and didn't update it.

I nulled the trigger locals. Since the triggers are permanent (except for the 0s trigger, which should be nulled I guess), it shouldn't be necessary to null them either, but it doesn't matter either way. Not a hill I'm willing to die on.
 
Top