Замена фреймивентов тултипами

Добавлен , опубликован
Все три с половиной человека, делающие карты на рефе с фреймами знают о неприятном баге с фрейм-ивентами, появившемся ещё в 2022 году с патчем 1.33 PTR. При срабатывании события фрейма, курсор игрока на короткий промежуток времени изменяет свои экранные координаты (оказывается в другом месте), а затем возвращается в прежнее положение. Покадровая демонстрация на картинке.
Убедиться в этом легко, достаточно запустить карту с этим кодом, и покликать на кнопку.
do
    local function createClassicButton(x, y, size)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),0,0)

        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dclick here|r")

        return button
    end

    function MarkGameStarted()
        BlzTriggerRegisterFrameEvent(CreateTrigger(), createClassicButton(.2, .3, .1), FRAMEEVENT_CONTROL_CLICK)
    end
end
Особенно пострадали события входа курсора во фрейм, и выхода из него, поскольку триггер с событием входа при первом срабатывании выбрасывает курсор за пределы фрейма, и затем возвращает назад, что вызывает повторное срабатывание триггера, и таким образом появляется бесконечный луп событий входа и выхода, до тех пор пока курсор не будет убран с фрейма.
Об этом периодически репортят близзам, но нужно признать, что, скорее всего, это уже никогда не пофиксят.
В принципе, с этим можно жить, но зачем, если можно подпереть костылём? Есть одно лежащее не поверхности решение, это система тултипов. Тултип — это фрейм с динамической видимостью, подвязанный к другому фрейму. Он появляется в тот момент, когда на его, условно, хозяйский фрейм навели курсор, и исчезает, соответственно, когда курсор убрали. Это не требует триггера с событием, и близзы это пока не сломали. Используя нативку BlzFrameIsVisible(frame) можно отказаться от использования фрейм-ивентов в пользу проверки видимости тултипов для контролируемых фреймов.
Базовый алгоритм:
  1. К каждому контрольному фрейму нужно привязать свой уникальный тултип. Тултип может быть как настоящим (обводка, всплывающая подсказка и т.д.), так и фейковым (пустой текстовый фрейм, прозрачный бекдроп и т.д.).
  1. Тултипы и фреймы объединяются в пары ключ-значение.
  1. В нужный момент проверяется видимость тултипа, если он видим, то можно сделать вывод о том, что курсор находится на фрейме.
Пример кода
do
    local function createClassicButton(x, y, size)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),0,0)
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dclick here|r")
        return button
    end

    local function createTooltip(button)
        local tooltip = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetTooltip(button, tooltip)
        BlzFrameSetPoint(tooltip, FRAMEPOINT_BOTTOMLEFT, button, FRAMEPOINT_TOP, 0, 0)
        BlzFrameSetEnable(tooltip, false)
        BlzFrameSetText(tooltip, "this is tooltip lol")
        return tooltip
    end
    
    function MarkGameStarted()
        local button = createClassicButton(.2, .3, .1)
        local tooltip = createTooltip(button)

        local trig = CreateTrigger()
        TriggerRegisterPlayerEvent(trig, GetLocalPlayer(), EVENT_PLAYER_MOUSE_DOWN)

        TriggerAddCondition(trig, Condition(function()
            if BlzFrameIsVisible(tooltip) then
                print("игрок нажал на фрейм ", button)
            end
        end))
    end
end
Результат на видео
На самом деле, это всё, что я хотел сказать, и на этом статью можно закончить, ибо те, кто зашёл сюда за ответом на вопрос “Как сделать события фреймов без улетающего вдаль курсора?” уже его получили, и могут смело перекатываться на южапи отправляться переделывать триггеры на тултипы.
Но у меня завалялось несколько простыней говнокода по этой теме, которыми я могу до неприличия растянуть эту статью для случайно заглянувших мимокрокодилов, которые не поняли о чём тут вообще речь.

События входа и выхода

В общем, тут можно действовать максимально прямолинейно. Создаём таймер и проверяем видимость тултипов.
Пример кода
do
    local dict = {} -- Используем словарь

    local currentFrame -- переменная для хранения фрейма в который мы вошли (если вошли)
    local lastFrame -- промежуточная переменная, а также заодно переменная для хранения фрейма из которого мы вышли (если вышли)


    local function createClassicButton(x, y, size)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetFrameByName("ConsoleUIBackdrop", 0),0,0)
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dclick here|r")
        return button
    end

    local function createTooltip(button)
        local tooltip = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetTooltip(button, tooltip)
        BlzFrameSetPoint(tooltip, FRAMEPOINT_BOTTOMLEFT, button, FRAMEPOINT_TOP, 0, 0)
        BlzFrameSetEnable(tooltip, false)
        BlzFrameSetText(tooltip, "")
        return tooltip
    end
    
    function MarkGameStarted()
        for i = 1, 4 do
            local button = createClassicButton(GetRandomReal(.5, .8), GetRandomReal(.15, .55), .1)
            dict[createTooltip(button)] = button
        end

        TimerStart(CreateTimer(), .015, true, function()
            lastFrame = currentFrame
            currentFrame = nil

            for k, v in pairs(dict) do
                if BlzFrameIsVisible(k) then
                    currentFrame = v
                    break
                end
            end

            if currentFrame and lastFrame ~= currentFrame then
                if lastFrame then
                    -- print("перескочил с фрейма")
                    -- По сути это перескок с фрейма на фрейм, при необходимости возможно использовать и такое событие

                    print("вышел из фрейма", lastFrame)
                    print("вошёл на фрейм", currentFrame)
                else
                    print("вошёл на фрейм:", currentFrame)
                end
            elseif lastFrame and not currentFrame then
                print("вышел из фрейма:", lastFrame)
            end
        end)
    end
end
Результат на видео
Вот более прикладная демонстрация того, как это работает, в виде простой мини-игры. Пока мышь на иконке панды – пиво льётся, иначе панда трезвеет. А секрет в том, что пиво это тултип.
Можно посмотреть в код и убедиться, что ивенты не используются. Да и в целом триггеры не используются, всё по тикам таймеров.
Код
do
    local dict = {} -- Используем словарь

    local currentFrame -- переменная для хранения фрейма в который мы вошли (если вошли)
    local lastFrame -- промежуточная переменная, а также заодно переменная для хранения фрейма из которого мы вышли (если вышли)

    local function hideDefaultUI()
        local gameui = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
        BlzFrameSetVisible(BlzFrameGetChild(gameui, 1), false)
        BlzFrameSetAbsPoint(BlzGetFrameByName("ConsoleUIBackdrop", 0), FRAMEPOINT_TOPRIGHT, 0, 0)

        for i = 0, 11 do
          BlzFrameSetVisible(BlzGetFrameByName("CommandButton_" .. i, 0), false)
        end

        BlzHideOriginFrames(true)
        BlzFrameSetScale(BlzFrameGetChild(BlzGetFrameByName("ConsoleUI", 0), 5), 0.001)
    end

    local function createBackdrop(parent, x, y, size, texture)
        local fr = BlzCreateFrameByType("BACKDROP", "", parent, "", 1)
        BlzFrameSetAbsPoint(fr, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(fr, size, size)
        BlzFrameSetTexture(fr, texture, 0, true)
        return fr
    end

    local function createClassicButton(x, y, size)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetFrameByName("ConsoleUIBackdrop", 0),0,0)
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dclick here|r")
        return button
    end

    local function createTextFrame(x, y, size, str, scale)
        local text = BlzCreateFrameByType("TEXT", "", BlzGetFrameByName("ConsoleUIBackdrop", 0), "", 0)
        BlzFrameSetAbsPoint(text, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(text, size, size)
        BlzFrameSetEnable(text, false)
        BlzFrameSetText(text, str)
        BlzFrameSetScale(text, scale)
        return text
    end

    local function createBar(parent, x, y, model, scale, min, max, current)
        local bar = BlzCreateFrameByType("STATUSBAR", "", parent, "", 0)
        BlzFrameSetAbsPoint(bar, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(bar, 0.00001, 0.00001)
        BlzFrameSetScale(bar, scale)
        BlzFrameSetModel(bar, model, 0)
        BlzFrameSetMinMaxValue(bar, min, max)
        BlzFrameSetValue(bar, current)
        return bar
    end


    local function buildMiniGame()
        local bars = {}
        local fulledBars = {}
        local tooltips = {}

        local textures = {
            "ReplaceableTextures\\CommandButtons\\BTNEarthBrewmaster",
            "ReplaceableTextures\\CommandButtons\\BTNFireBrewmaster",
            "ReplaceableTextures\\CommandButtons\\BTNStormBrewmaster",
            "ReplaceableTextures\\CommandButtons\\BTNStrongDrink"
        }

        local function restart()
            print "zasoh :("
            fulledBars = {}
            for i = 1, #bars do
                BlzFrameSetValue(bars[i], 1000)
                BlzFrameSetVisible(bars[i], true)
                dict[tooltips[i]] = bars[i]
            end
        end

        local function won()
            createTextFrame(.7, .3, .1, "|cffFCD20DYou won!|r", 3)
            BlzFrameClick(BlzGetFrameByName("UpperButtonBarMenuButton", 0))
        end

        -----

        hideDefaultUI()
        BlzFrameSetTextAlignment(createTextFrame(.4, .68, .1, "|cffFCD20DNe dai sebe zasohnut|r", 2.5), TEXT_JUSTIFY_BOTTOM, TEXT_JUSTIFY_CENTER)

        -- Здесь создаём кнопки с тултипами
        for i = 1, 3 do
            local listener = createClassicButton(.08, .5 - .2 * (i - 1), .1)
            local texture = createBackdrop(listener, .08, .5 - .2 * (i - 1), .1, textures[i])
            local tooltip = createBackdrop(listener, .08, .5 - .2 * (i - 1), .025, textures[4])
            BlzFrameSetEnable(listener, false)
            BlzFrameClearAllPoints(tooltip)
            BlzFrameSetPoint(tooltip, FRAMEPOINT_RIGHT, listener, FRAMEPOINT_LEFT, -.01, 0)
            BlzFrameSetTooltip(listener, tooltip)

            local bar = createBar(listener, 0.2, .45 - .2 * (i - 1), "ui/feedback/buildprogressbar/buildprogressbar", 3, 0, 2000, 1000)

            table.insert(tooltips, tooltip)
            table.insert(bars, bar)
            dict[tooltip] = bar -- Образуем пары ключ-значение для тултипов и баров
            -- Да, именно баров, в данном случае кнопки нам больше не нужны, несмотря на то, что именно они реагируют на мышь
        end


        TimerStart(CreateTimer(), .015, true, function()
            if #fulledBars >= #bars then
                DestroyTimer(GetExpiredTimer())
                won()
                return
            end

            for _, v in ipairs(bars) do
                local val = BlzFrameGetValue(v)

                if val <= 0 then
                    restart()
                    return
                end

                if val < 1997 then
                    -- проверяем активный фрейм сравнивая его с переменной currentFrame
                    --

                    BlzFrameSetValue(v, (v == currentFrame and val + 3) or val - 1.25)
                elseif BlzFrameIsVisible(v) then
                    table.insert(fulledBars, v)
                    BlzFrameSetVisible(v, false)
                end
            end
        end)
    end

    function MarkGameStarted()
        buildMiniGame()

        -- Запускаем таймер параллельно остальному коду, и в любой момент обращаемся к переменной currentFrame
        TimerStart(CreateTimer(), .015, true, function()
            lastFrame = currentFrame
            currentFrame = nil

            for k, v in pairs(dict) do
                if BlzFrameIsVisible(k) then
                    currentFrame = v
                    break
                end
            end
        end)
    end
end
Здесь таймер работает параллельно остальному коду, и в любой момент можно обратиться к переменной currentFrame. Но конкретно в этой карте можно применить другой подход и просто интегрировать скрипт в код и проходить по ключам-тултипам при необходимости.
То же самое, но немного по-другому
do
    local dict = {}
    -- Словарь остался, но переменные нам уже не нужны

    local function hideDefaultUI()
        local gameui = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
        BlzFrameSetVisible(BlzFrameGetChild(gameui, 1), false)
        BlzFrameSetAbsPoint(BlzGetFrameByName("ConsoleUIBackdrop", 0), FRAMEPOINT_TOPRIGHT, 0, 0)

        for i = 0, 11 do
          BlzFrameSetVisible(BlzGetFrameByName("CommandButton_" .. i, 0), false)
        end

        BlzHideOriginFrames(true)
        BlzFrameSetScale(BlzFrameGetChild(BlzGetFrameByName("ConsoleUI", 0), 5), 0.001)
    end

    local function createBackdrop(parent, x, y, size, texture)
        local fr = BlzCreateFrameByType("BACKDROP", "", parent, "", 1)
        BlzFrameSetAbsPoint(fr, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(fr, size, size)
        BlzFrameSetTexture(fr, texture, 0, true)
        return fr
    end

    local function createClassicButton(x, y, size)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetFrameByName("ConsoleUIBackdrop", 0),0,0)
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dclick here|r")
        return button
    end

    local function createTextFrame(x, y, size, str, scale)
        local text = BlzCreateFrameByType("TEXT", "", BlzGetFrameByName("ConsoleUIBackdrop", 0), "", 0)
        BlzFrameSetAbsPoint(text, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(text, size, size)
        BlzFrameSetEnable(text, false)
        BlzFrameSetText(text, str)
        BlzFrameSetScale(text, scale)
        return text
    end

    local function createBar(parent, x, y, model, scale, min, max, current)
        local bar = BlzCreateFrameByType("STATUSBAR", "", parent, "", 0)
        BlzFrameSetAbsPoint(bar, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(bar, 0.00001, 0.00001)
        BlzFrameSetScale(bar, scale)
        BlzFrameSetModel(bar, model, 0)
        BlzFrameSetMinMaxValue(bar, min, max)
        BlzFrameSetValue(bar, current)
        return bar
    end

    local function buildMiniGame()
        local bars = {}
        local tooltips = {}

        local textures = {
            "ReplaceableTextures\\CommandButtons\\BTNEarthBrewmaster",
            "ReplaceableTextures\\CommandButtons\\BTNFireBrewmaster",
            "ReplaceableTextures\\CommandButtons\\BTNStormBrewmaster",
            "ReplaceableTextures\\CommandButtons\\BTNStrongDrink"
        }

        local function restart()
            print "zasoh :("
            for i = 1, #bars do
                BlzFrameSetValue(bars[i], 1000)
                BlzFrameSetVisible(bars[i], true)
                dict[tooltips[i]] = bars[i]
            end
        end

        local function won()
            createTextFrame(.7, .3, .1, "|cffFCD20DYou won!|r", 3)
            BlzFrameClick(BlzGetFrameByName("UpperButtonBarMenuButton", 0))
        end

        -----

        hideDefaultUI()
        BlzFrameSetTextAlignment(createTextFrame(.4, .68, .1, "|cffFCD20DNe dai sebe zasohnut|r", 2.5), TEXT_JUSTIFY_BOTTOM, TEXT_JUSTIFY_CENTER)

        -- Здесь создаём кнопки с тултипами
        for i = 1, 3 do
            local listener = createClassicButton(.08, .5 - .2 * (i - 1), .1)
            local texture = createBackdrop(listener, .08, .5 - .2 * (i - 1), .1, textures[i])
            local tooltip = createBackdrop(listener, .08, .5 - .2 * (i - 1), .025, textures[4])
            BlzFrameSetEnable(listener, false)
            BlzFrameClearAllPoints(tooltip)
            BlzFrameSetPoint(tooltip, FRAMEPOINT_RIGHT, listener, FRAMEPOINT_LEFT, -.01, 0)
            BlzFrameSetTooltip(listener, tooltip)

            local bar = createBar(listener, 0.2, .45 - .2 * (i - 1), "ui/feedback/buildprogressbar/buildprogressbar", 3, 0, 2000, 1000)

            table.insert(tooltips, tooltip)
            table.insert(bars, bar)
            dict[tooltip] = bar -- Образуем пары ключ-значение для тултипов и баров
            -- Да, именно баров, в данном случае кнопки нам больше не нужны, несмотря на то, что именно они реагируют на мышь
        end

        TimerStart(CreateTimer(), .015, true, function()
            if not dict[tooltips[1]] and not dict[tooltips[2]] and not dict[tooltips[3]] then
                DestroyTimer(GetExpiredTimer())
                won()
                return
            end

            for k, v in pairs(dict) do -- Проверяем видимость тултипов каждый тик
                local val = BlzFrameGetValue(v)
                if val <= 0 then
                    restart()
                    return
                end

                if BlzFrameIsVisible(k) then
                    BlzFrameSetValue(v, val + 3)
                else
                    BlzFrameSetValue(v, val - 1.25)
                end

                if val >= 1997 then
                    dict[k] = nil
                    BlzFrameSetVisible(v, false)
                end
            end
        end)
    end

    function MarkGameStarted()
        buildMiniGame()
    end
end

События клавиш мыши

В целом весь код из предыдущего раздела остаётся нетронутым, ибо нам нужно знать текущий фрейм под мышью, но теперь ещё и добавляется триггер с событиями на нажимание и отпускание мыши. Тут надо исходить из задач, но в общем, чтобы отловить полноценный клик нужны оба события.
Пример реализации в коде
do
    local dict = {} -- Используем словарь

    local currentFrame -- переменная для хранения фрейма в который мы вошли (если вошли)
    local lastFrame -- промежуточная переменная, а также заодно переменная для хранения фрейма из которого мы вышли (если вышли)
    local pressedFrame -- дополнительная переменная для отслеживания кликов

    local function createClassicButton(x, y, size)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetFrameByName("ConsoleUIBackdrop", 0),0,0)

        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dclick here|r")

        return button
    end

    local function createTooltip(button)
        local tooltip = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetTooltip(button, tooltip)
        BlzFrameSetPoint(tooltip, FRAMEPOINT_BOTTOMLEFT, button, FRAMEPOINT_TOP, 0, 0)
        BlzFrameSetEnable(tooltip, false)
        BlzFrameSetText(tooltip, "")

        return tooltip
    end

    local function resetFrame(fr)
        -- Обязательно используем эту функцию после нажатий на кнопки для возврата фокуса
        BlzFrameSetEnable(fr, false)
        BlzFrameSetEnable(fr, true)
    end


    function MarkGameStarted()
        for i = 1, 4 do
            local button = createClassicButton(GetRandomReal(.5, .8), GetRandomReal(.15, .55), .1)
            dict[createTooltip(button)] = button
        end

        local trig = CreateTrigger()
        TriggerRegisterPlayerEvent(trig, GetLocalPlayer(), EVENT_PLAYER_MOUSE_DOWN)
        TriggerRegisterPlayerEvent(trig, GetLocalPlayer(), EVENT_PLAYER_MOUSE_UP)
        TriggerAddCondition(trig, Condition(function()
            if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
                if currentFrame then
                    print("нажал ЛКМ на фрейме")
                    pressedFrame = currentFrame
                    resetFrame(currentFrame)
                end
            elseif BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_UP then
                if pressedFrame then
                    if pressedFrame == currentFrame then
                        print("отпустил ЛКМ на нажатом фрейме (клик)")
                    elseif currentFrame then
                        print("отпустил ЛКМ не на нажатом фрейме (на другом фрейме)")
                    else
                        print("отпустил ЛКМ не на нажатом фрейме (вне контрольных фреймов)")
                    end
                elseif currentFrame then
                    print("отпустил ЛКМ на ненажатом фрейме (не было нажатого фрейма)")
                end
                pressedFrame = nil
            end
        end))

        TimerStart(CreateTimer(), .015, true, function()
            lastFrame = currentFrame
            currentFrame = nil

            for k, v in pairs(dict) do
                if BlzFrameIsVisible(k) then
                    currentFrame = v
                    break
                end
            end
            --[[
            if currentFrame and lastFrame ~= currentFrame then
                if lastFrame then
                    -- print("перескочил с фрейма")
                    -- По сути это перескок с фрейма на фрейм, при необходимости возможно использовать и такое событие

                    print("вышел из фрейма", lastFrame)
                    print("вошёл на фрейм", currentFrame)
                else
                    print("вошёл на фрейм:", currentFrame)
                end
            elseif lastFrame and not currentFrame then
                print("вышел из фрейма:", lastFrame)
            end]]
        end)
    end
end
Видео. Получаем разные события от кнопок, и курсор никуда не улетает, красота.
Помимо currentFrame и lastFrame здесь нужно добавить ещё одну вспомогательную переменную pressedFrame. Можно настроить взаимодействие для трёх клавиш мыши, а также гибко настроить события, обнаружить удержание кнопки, отпускание, двойной клик, и ещё что-нибудь. В общем, плюс-минус всё то же самое, что и с фреймивентами, но без баганного курсора.
Накидываю вариант мини-игры с управлением на фрейм-кнопках. Три кнопки, курсор на месте.
Код
do
    local buttonHandler = {} -- Сюда сохраним функции кнопок
    local dict = {} -- Используем словарь

    local currentFrame -- переменная для хранения фрейма в который мы вошли (если вошли)
    local lastFrame -- промежуточная переменная, а также заодно переменная для хранения фрейма из которого мы вышли (если вышли)
    local pressedFrame -- дополнительная переменная для отслеживания кликов

    local function hideDefaultUI()
        local gameui = BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0)
        --BlzFrameSetVisible(BlzFrameGetChild(gameui, 1), false)
        BlzFrameSetAbsPoint(BlzGetFrameByName("ConsoleUIBackdrop", 0), FRAMEPOINT_TOPRIGHT, 0, 0)

        for i = 0, 11 do
            BlzFrameSetVisible(BlzGetFrameByName("CommandButton_" .. i, 0), false)
        end

        BlzHideOriginFrames(true)
        BlzFrameSetScale(BlzFrameGetChild(BlzGetFrameByName("ConsoleUI", 0), 5), 0.001)
    end

    local function createButton(x, y, size)
        local button = BlzCreateFrameByType("BUTTON", "", BlzGetFrameByName("ConsoleUIBackdrop", 0), "ScoreScreenTabButtonTemplate", 0)

        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, "|cffFCD20Dcooldown|r")
        return button
    end

    local function createTextFrame(x, y, size, str, scale)
        local text = BlzCreateFrameByType("TEXT", "", BlzGetFrameByName("ConsoleUIBackdrop", 0), "", 0)
        BlzFrameSetAbsPoint(text, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(text, size, size)
        BlzFrameSetEnable(text, false)
        BlzFrameSetText(text, str)
        BlzFrameSetScale(text, scale)
        return text
    end

    local function createTooltip(button)
        local tooltip = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetTooltip(button, tooltip)
        BlzFrameSetPoint(tooltip, FRAMEPOINT_BOTTOMLEFT, button, FRAMEPOINT_TOP, 0, 0)
        BlzFrameSetEnable(tooltip, false)
        BlzFrameSetText(tooltip, "")
        return tooltip
    end

    local function createBackdrop(parent, x, y, size, texture)
        local fr = BlzCreateFrameByType("BACKDROP", "", parent, "", 1)
        BlzFrameSetAbsPoint(fr, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(fr, size, size)
        BlzFrameSetTexture(fr, texture, 0, true)
        return fr
    end

    local function resetFrame(fr)
        -- Обязательно используем эту функцию после нажатий на кнопки для возврата фокуса
        BlzFrameSetEnable(fr, false)
        BlzFrameSetEnable(fr, true)
    end

    local function pressFrame(fr)
        BlzFrameSetScale(fr, .9)
    end

    local function unpressFrame(fr)
        BlzFrameSetScale(fr, .9)
        TimerStart(CreateTimer(), 1 / 32, false, function()
            BlzFrameSetScale(fr, 1)
            DestroyTimer(GetExpiredTimer())
        end)
    end

    local function startCooldown(fr)
        local t, cooldown = CreateTimer(), 20
        BlzFrameSetEnable(fr, false)
        BlzFrameSetAlpha(fr, 50)
        print("перезарядка "..cooldown.." секунд")
        TimerStart(t, cooldown, false, function()
            BlzFrameSetEnable(fr, true)
            BlzFrameSetAlpha(fr, 255)
            DestroyTimer(t)
        end)
    end

    local group = CreateGroup()
    local spawnTimer = CreateTimer()
    local farm

    local function startMiniGame()
        -- всякий мусор для спавна гулей, можно не смотреть

        local innerX1, innerY1 = -1000, -1000
        local innerX2, innerY2 = 1000, 1000
        local outerX1, outerY1 = -2500, -2500
        local outerX2, outerY2 = 2500, 2500

        local function getPoint()
            local x, y

            while true do
                x, y = math.random() * (outerX2 - outerX1) + outerX1, math.random() * (outerY2 - outerY1) + outerY1
                if not (x >= innerX1 and x <= innerX2 and y >= innerY1 and y <= innerY2) then
                    break
                end
            end
            return x, y
        end

        farm = CreateUnit(Player(0), FourCC('hhou'), 0, 0, bj_UNIT_FACING)
        SetCameraPosition(0 ,0)

        TimerStart(spawnTimer, .17, true, function()
            if IsUnitDeadBJ(farm) then
                DestroyTimer(spawnTimer)
                RestartGame(nil)
            end
            local x, y = getPoint()
            local u = CreateUnit(Player(1), FourCC('ugho'), x, y, 0)
            IssueTargetOrder(u, 'attack', farm)
        end)
    end

    function MarkGameStarted()
        math.randomseed(os.time())
        hideDefaultUI()
        startMiniGame()

        local gameTime = 120
        local timerFrame, gameTimer = createTextFrame(.4, .02, .05, "00:00", 2), CreateTimer()
        BlzFrameSetTextAlignment(timerFrame, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)

        TimerStart(gameTimer, .01, true, function()
            gameTime = gameTime - .01
            if gameTime <= 0 then
                DestroyTimer(spawnTimer)
                DestroyTimer(gameTimer)
                AddUnitAnimationProperties(farm, "second", true)
                AddUnitAnimationProperties(farm, "upgrade", true)
                BlzSetUnitSkin(farm, FourCC('hcas'))
                SetUnitInvulnerable(farm, true)
                BlzFrameClick(BlzGetFrameByName("UpperButtonBarMenuButton", 0))
                return
            end
            local seconds = math.floor(gameTime)
            local mSeconds = math.floor((gameTime - seconds) * 100)

            BlzFrameSetText(timerFrame, string.format("%02d:%02d", seconds, mSeconds))
        end)

        local textures = {
            "ReplaceableTextures\\CommandButtons\\BTNRepairOff",
            "ReplaceableTextures\\CommandButtons\\BTNSelfDestructOff",
            "ReplaceableTextures\\CommandButtons\\BTNDwarvenLongRifle"
        }

        -- Создаём набор кнопок
        local buttons = {}
        for i = 1, 3 do
            local button = createButton(.2 * i, .1, .055)
            local texture = createBackdrop(button, .2 * i, .1, .055, textures[i])
            BlzFrameClearAllPoints(texture)
            BlzFrameSetAllPoints(texture, button)
            dict[createTooltip(button)] = button
            table.insert(buttons, button)
        end

        -- Для каждой кнопки создана своя функция
        -- Теперь фрейм это просто ключ, по которому можно вызвать соответствующую функцию

        buttonHandler = {
            [buttons[1]] = function()
                startCooldown(buttons[1])
                local t = CreateTimer()
                local counter = 0
                TimerStart(t, 0.1, true, function()
                    if counter > 50 then
                        DestroyTimer(t)
                        return
                    end
                    PlaySound("abilities\\spells\\other\\repair\\PeonRepair1")

                    local life = GetUnitState(farm, UNIT_STATE_LIFE)
                    if life < GetUnitState(farm, UNIT_STATE_MAX_LIFE) - 10 then
                        SetUnitState(farm, UNIT_STATE_LIFE, life + 10)
                    else
                        SetUnitState(farm, UNIT_STATE_LIFE, GetUnitState(farm, UNIT_STATE_MAX_LIFE))
                        DestroyTimer(t)
                    end
                    counter = counter + 1
                end)
            end,
            [buttons[2]] = function()
                startCooldown(buttons[2])
                PlaySound("sound\\destructibles\\BarrelExplosion1")
                GroupEnumUnitsInRange(group, 0, 0, 800)
                GroupRemoveUnit(group, farm)
                while true do
                    local u = FirstOfGroup(group)
                    if u == nil then break end
                    GroupRemoveUnit(group, u)
                    if UnitAlive(u)  then
                        ExplodeUnitBJ(u)
                    end
                end
                GroupRemoveUnit(group, farm)
                ExplodeUnitBJ(GroupPickRandomUnit(group))
            end,
            [buttons[3]] = function()
                PlaySound("units\\human\\rifleman\\RiflemanAttack2")
                GroupEnumUnitsInRange(group, 0, 0, 600)
                GroupRemoveUnit(group, farm)
                ExplodeUnitBJ(GroupPickRandomUnit(group))
            end,
        }

        -- Таймер проверяет в каком фрейме курсор
        -- А триггер ловит нажатия
        -- Командная работа

        local trig = CreateTrigger()
        TriggerRegisterPlayerEvent(trig, GetLocalPlayer(), EVENT_PLAYER_MOUSE_DOWN)
        TriggerRegisterPlayerEvent(trig, GetLocalPlayer(), EVENT_PLAYER_MOUSE_UP)
        TriggerAddCondition(trig, Condition(function()
            if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
                if currentFrame then
                    if BlzFrameGetEnable(currentFrame) then -- Проверяем, что кнопка активна
                        pressFrame(currentFrame)
                        --print("нажал ЛКМ на фрейме")
                        pressedFrame = currentFrame
                        resetFrame(currentFrame)
                    end
                end
            elseif BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_UP then
                -- Нам нужно обрабатывать только клик
                if pressedFrame then
                    if pressedFrame == currentFrame and BlzFrameGetEnable(currentFrame) then -- Проверяем, что кнопка активна
                        unpressFrame(currentFrame)
                        buttonHandler[currentFrame]()
                    end
                end
            end
        end))

        TimerStart(CreateTimer(), .015, true, function()
            lastFrame = currentFrame
            currentFrame = nil

            for k, v in pairs(dict) do
                if BlzFrameIsVisible(k) then
                    currentFrame = v
                    break
                end
            end
        end)
    end
end
Тут надо ещё упомянуть один неочевидный момент. Тутлтип продолжает работает у фреймов выключенных с помощью нативки BlzFrameSetEnable(frame, enabled), поэтому чтобы избежать нажатий на неактивную кнопку следует добавить в промежутке проверку BlzFrameGetEnable(frame). Так реализовано отключение на время кулдауна в примере выше.

Мультиплеер

База по мультиплееру здесь.
Очевидно, что нативка BlzFrameIsVisible(frame) для одного и того же фрейма, но для разных игроков может вернуть разные результаты. Я не особо вникал как это работает, но думаю, что лучше вообще не лезть в эти приколы, а сразу дать каждому игроку свою кнопку. Вернее дать кнопки всем, но показывать выборочно.
Код (показываем выборочно)
do
    local function createClassicButton(x, y, size, text, isVisible)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),0,0)
        BlzFrameSetVisible(button, isVisible)
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, text)
        return button
    end

    function MarkGameStarted()
        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
                createClassicButton(.4, .35, .2, "|cffFCD20DЭта кнопка видна только игроку "..i.."|r", GetLocalPlayer() == Player(i))
            end
        end
    end
end
В принципе, если сейчас взять написанную выше систему отслеживания тултипов, добавить к ней "безопасное" создание фреймов для каждого игрока в карте, и запустить в мультиплеерном режиме, то можно обнаружить, что она… работает. Но только локально. Каждому игроку честно отпринтит куда он зашёл, и что он кликнул, только другие игроки об этом знать не будут, и триггер клика мыши будет работать локально. А полноценная сетевая игра закончится в тот момент, как только мы попытаемся по клику одного из игроков на фрейм создать новый игровый объект, например выдать герою предмет из магазина на фреймах.
Для синхронизации нужно добавить одно новое звено в цепи, а именно новый триггер. Пояснение ниже.
-- ...
local trig = CreateTrigger()
local trigSync = CreateTrigger() -- добавим триггер для синхронизации

for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
    if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
        -- ...
        TriggerRegisterPlayerEvent(trig, Player(i), EVENT_PLAYER_MOUSE_DOWN)
        TriggerRegisterPlayerEvent(trig, Player(i), EVENT_PLAYER_MOUSE_UP)

        -- На каждого зарегистрируем событие оправки синх данных
        BlzTriggerRegisterPlayerSyncEvent(trigSync, Player(i), "mouse_click", false)
    end
end

TriggerAddCondition(trig, Condition(function()
    if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
        -- ...
    elseif BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_UP then
        if pressedFrame then
            if pressedFrame == currentFrame then
                --print("отпустил ЛКМ на нажатом фрейме (клик)")

                -- Допустим в этом месте мы хотим добавить в игру юнита
                -- Это приведёт к немедленной десинхронизации, поэтому отказываемся от этой идеи в пользу отправки Sync Data

                BlzSendSyncData("mouse_click", BlzFrameGetName(currentFrame)) -- передаём всем префикс и имя фрейма
            end
        end
    end
end))

-- trigSync срабатывает когда синх данные прилетели от игрока

TriggerAddCondition(trigSync, Condition(function()
    local prefix = BlzGetTriggerSyncPrefix()
    local data = BlzGetTriggerSyncData()

    if prefix == "mouse_click" then
        -- Если мы оказались здесь, значит кто-то на что-то кликнул левой кнопкой мыши
        -- В этом месте можно безопасно создавать игровые объекты

        local player = GetTriggerPlayer() -- получение игрока
        local frame = BlzGetFrameByName(data.name, data.context) -- получение фрейма
    end
end))
По сути, описанная в предыдущих двух разделах система остаётся нетронутой, меняется лишь результат её выполнения. Вся движуха по поиску кнопок должна закончиться отправкой данных о том, что такой-то игрок сделал такое-то действие с таким-то фреймом. Игрока получим из триггера, действие из префикса, а для передачи фрейма воспользуемся его именем. Да, возникает сложность: придётся взять за привычку давать фреймам осмысленные имена, ну или хотя бы уникальные.
На самом деле, имени не всегда будет достаточно, в следующем примере имя должно обязательно быть "ScriptDialogButton", чтобы унаследовать шаблон кнопки. Здесь на помощь придёт контекст. Я заранее записал пары фрейм - контекст в таблицу frameContext, чтобы в дальнейшем синхронизировать.
Помимо контекста, на выходе используем ещё нативку BlzFrameGetName(frame), а вот на входе мы уже сможем без проблем получить нужный фрейм нативкой BlzGetFrameByName(name, createContext).
У каждого игрока свой набор кнопок, но теперь все знают о том, кто на что кликнул
do
    local dict = {} -- Словарь можно оставить общий для всех

    -- А вот переменные создадим уникальные для каждого игрока
    local MUIStuff = {} -- инициализируем позже

    --[[local currentFrame -- переменная для хранения фрейма в который мы вошли (если вошли)
    local lastFrame -- промежуточная переменная, а также заодно переменная для хранения фрейма из которого мы вышли (если вышли)
    local pressedFrame -- дополнительная переменная для отслеживания кликов]]

    local frameContext = {} -- не теряем контекст

    local function createClassicButton(x, y, size, text, isVisible, context)
        local button = BlzCreateFrame("ScriptDialogButton", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0),0, context)
        BlzFrameSetVisible(button, isVisible)
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetText(button, text)

        return button
    end

    local function createTooltip(button)
        local tooltip = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetTooltip(button, tooltip)
        BlzFrameSetPoint(tooltip, FRAMEPOINT_BOTTOMLEFT, button, FRAMEPOINT_TOP, 0, 0)
        BlzFrameSetEnable(tooltip, false)
        BlzFrameSetText(tooltip, "")

        return tooltip
    end

    local function resetFrame(fr)
        -- Обязательно используем эту функцию после нажатий на кнопки для возврата фокуса
        BlzFrameSetEnable(fr, false)
        BlzFrameSetEnable(fr, true)
    end

    function MarkGameStarted()
        local trig = CreateTrigger()
        local trigSync = CreateTrigger() -- добавим триггер для синхронизации
        local contextCounter = 0 -- переменная для контекста

        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
                MUIStuff[i] = {} -- Таблица для переменных теперь будет доступна по ключу ID-игрока

                for f = 1, 4 do
                    local button = createClassicButton(GetRandomReal(.5, .8), GetRandomReal(.15, .55), .1, "|cffFCD20DЭта кнопка видна только игроку "..i.."|r", GetLocalPlayer() == Player(i), contextCounter)
                    frameContext[button] = contextCounter
                    contextCounter = contextCounter + 1
                    dict[createTooltip(button)] = button
                end

                TriggerRegisterPlayerEvent(trig, Player(i), EVENT_PLAYER_MOUSE_DOWN)
                TriggerRegisterPlayerEvent(trig, Player(i), EVENT_PLAYER_MOUSE_UP)

                -- На каждого зарегестрируем событие оправки синх данных
                BlzTriggerRegisterPlayerSyncEvent(trigSync, Player(i), "mouse_click", false)
            end
        end

        TriggerAddCondition(trig, Condition(function()
            if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
                local t = MUIStuff[GetPlayerId(GetTriggerPlayer())] -- получили таблицу для проверяемого игрока, и теперь будем заполнять pressedFrame только для него
                if t.currentFrame then
                    --print("нажал ЛКМ на фрейме")
                    t.pressedFrame = t.currentFrame
                    resetFrame(t.currentFrame)
                end
            elseif BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_UP then
                local t = MUIStuff[GetPlayerId(GetTriggerPlayer())] -- получили таблицу для проверяемого игрока, и теперь будем заполнять pressedFrame только для него
                if t.pressedFrame then
                    if t.pressedFrame == t.currentFrame then
                        --print("отпустил ЛКМ на нажатом фрейме (клик)")
                        BlzSendSyncData("mouse_click", BlzFrameGetName(t.currentFrame).."|"..frameContext[t.currentFrame]) -- передаём всем имя и контекст
                    elseif t.currentFrame then
                        --print("отпустил ЛКМ не на нажатом фрейме (на другом фрейме)")
                    else
                        --print("отпустил ЛКМ не на нажатом фрейме (вне контрольных фреймов)")
                    end
                elseif t.currentFrame then
                    --print("отпустил ЛКМ на ненажатом фрейме (не было нажатого фрейма)")
                end
                t.pressedFrame = nil
            end
        end))
        
        -- Когда синх данные прилетели от игрока
        TriggerAddCondition(trigSync, Condition(function()
            local prefix = BlzGetTriggerSyncPrefix()
            local data = BlzGetTriggerSyncData()

            if prefix == "mouse_click" then
                local name, context = string.match(data, "([^|]+)|([^|]+)")
                print("Игрок ("..GetPlayerId(GetTriggerPlayer())..") кликнул на фрейм "..name.." ("..context..")")
                local frame = BlzGetFrameByName(name, context) -- получение фрейма
            end
        end))

        TimerStart(CreateTimer(), .015, true, function()
            for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
                if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
                    local t = MUIStuff[i] -- получили таблицу для проверяемого игрока, и теперь будем заполнять lastFrame и currentFrame только для него
                    t.lastFrame = t.currentFrame
                    t.currentFrame = nil

                    for k, v in pairs(dict) do
                        if BlzFrameIsVisible(k) then
                            t.currentFrame = v
                            break
                        end
                    end
                end
            end
        end)
    end
end
А в игре это будет выглядеть вот так:

Вот небольшой пример с MUI магазином на фреймах. Каждый игрок имеет магазин независимо от остальных, и покупает предметы для себя безопасно, не вызывая десинхронизации.
Корявенький код
do
    -- Список доступных для продажи предметов, 15 штук
    local itemDB = {'afac', 'spsh', 'ajen', 'bgst', 'belv', 'bspd', 'cnob', 'ratc','rat6','rat9', 'clfm', 'clsd', 'crys', 'dsum', 'rst1'}

    -- Цены на эти предметы
    local costs = {}

    -- Вот эта таблица будет для хранения уникальных данных каждого игрока
    -- Постоянно будем к ней обращаться
    local MUIStuff = {}

    -- ID игроков в карте
    local playerIDs = {}

    function MarkGameStarted()
        -- накидаем цены предметов
        for i = 1, #itemDB do
            costs[itemDB[i]] = GetRandomInt(999, 9999)
        end

        -- Перебираем всех игроков и накидываем каждому штуки для муи
        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
                table.insert(playerIDs, i)
                MUIStuff[i] = {
                    dict = {}, -- тултипный словарь для отслеживания кликов
                    lastFrame = nil, -- переменные для отслеживания кликов
                    currentFrame = nil,
                    pressedFrame = nil,

                    frameToItem = {}, -- свяжем фреймы с кодами итемов здесь
                    cancelShopFrame = nil, -- кнопка выхода из магазина
                    hero = CreateUnit(Player(i), FourCC('Hpal'), 900 * i, 0, 0) --герой, которому будем выдавать предметы
                }
                SetPlayerState(Player(i), PLAYER_STATE_RESOURCE_GOLD, 300000) --накинем голды
            end
        end

        initShop() --создадим магазин
        initFramesTrigger() -- инициализируем систему отслеживания кнопок
    end

    function initShop()
        local size = 0.03 --размер кнопки
        local startX, startY = 0.5, 0.5 --начальная позиция
        local rows, columns = 4, 4 --количество рядов и столбов
        local deltaX, deltaY = 0.05, -0.05 --расстояние между кнопками

        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
                -- создаём набор кнопок магазина для каждого игрока
                -- вкючаем видимость только для локального игрока
                -- нейминг фреймов по формуле: "shop" + номер игрока + номер кнопки

                local counter = 1 --счётчик предметов
                local t = MUIStuff[i]
                local x, y
                for r = 1, rows do --ряды
                    y = startY + deltaY * (r - 1)
                    for c = 1, columns do --столбцы
                        x = startX + deltaX * (c - 1)
                        if counter >= 16 then -- 16-ую позицию используем для кнопки выхода
                            t.cancelShopFrame = createButton("shop"..i..counter, x, y, size, "ReplaceableTextures\\CommandButtons\\btncancel", "Exit shop", "", i)
                            break
                        end
                        local item = itemDB[counter] -- получили равкод итема
                        local text, path = getItemInfo(item)
                        local button = createButton("shop"..i..counter, x, y, size, path, text, ""..costs[item], i)

                        t.frameToItem[button] = item --записали пару "фрейм = равкод предмета"
                        counter = counter + 1
                    end
                end
            end
        end
    end

    function getItemInfo(id) --вернёт иконку и тултип предмета из РО
        local item = CreateItem(FourCC(id), 10000, 10000)
        local text = BlzGetItemTooltip(item)
        local path = BlzGetItemIconPath(item)
        RemoveItem(item)
        return text, path
    end

    function clickToShopFrame(frame, player)
        local t = MUIStuff[GetPlayerId(player)]

        if frame == t.cancelShopFrame then
            for k, _ in pairs(t.frameToItem) do
                BlzFrameSetVisible(k, false)
            end
            BlzFrameSetVisible(frame, false)
            return
        end
        sellItem(frame, player)
    end

    function resetFrame(fr)
        BlzFrameSetEnable(fr, false)
        BlzFrameSetEnable(fr, true)
    end

    function sellItem(frame, player)
        local t = MUIStuff[GetPlayerId(player)]

        -- Здесь мы проверяем голду и делаем что-нибудь с предметом
        local gold = PLAYER_STATE_RESOURCE_GOLD
        local currentGold = GetPlayerState(player, gold)
        local cost = costs[t.frameToItem[frame]] --получили цену предмета
        if cost <= currentGold then
            SetPlayerState(player, gold, GetPlayerState(player, gold) - cost)
            local item = CreateItem(FourCC(t.frameToItem[frame]), 10000, 10000)
            if not UnitAddItem(t.hero, item) then -- если в инвентаре нет места, то создаём рядом
                SetItemPosition(item, GetUnitX(t.hero), GetUnitY(t.hero))
            end
            outputItemInfo(item, cost, player)
            return
        end
        print "Not enough gold!"
    end

    function outputItemInfo(item, cost, player)
        -- Покажем сообщение только игроку-покупателю
        DisplayTextToForce(bj_FORCE_PLAYER[GetPlayerId(player)], "Received a |c0000FF80"..GetItemName(item).. "|r worth |c00FFFF00"..cost.."|r gold")
    end

    function createButton(name, x, y, size, iconPath, tooltipText, cost, playerID)
        -- Создаём фрейм-кнопку, тултип, иконку-бекдроп, и текстовый фрейм для цены

        local button = BlzCreateFrameByType("BUTTON", name, BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI, 0), "ScoreScreenTabButtonTemplate", 0)
        local icon = BlzCreateFrameByType("BACKDROP", "", button, "", 0)

        BlzFrameSetVisible(button, Player(playerID) == GetLocalPlayer())

        BlzFrameSetAllPoints(icon, button) --икнока будет полностью закрывать кнопку
        BlzFrameSetAbsPoint(button, FRAMEPOINT_CENTER, x, y)
        BlzFrameSetSize(button, size, size)
        BlzFrameSetTexture(icon, iconPath, 0, false)

        local tooltip = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetTooltip(button, tooltip)
        BlzFrameSetPoint(tooltip, FRAMEPOINT_BOTTOMLEFT, button, FRAMEPOINT_TOP, 0, 0)
        BlzFrameSetEnable(tooltip, false)
        BlzFrameSetText(tooltip, "|c00FFFF00"..tooltipText.."|r")

        local costFrame = BlzCreateFrameByType("TEXT", "", button, "", 0)
        BlzFrameSetPoint(costFrame, FRAMEPOINT_TOPLEFT, button, FRAMEPOINT_BOTTOMLEFT, 0, 0)
        BlzFrameSetPoint(costFrame, FRAMEPOINT_BOTTOMRIGHT, button, FRAMEPOINT_BOTTOMRIGHT, 0, -0.01)
        BlzFrameSetEnable(costFrame, false)
        BlzFrameSetText(costFrame, cost)
        BlzFrameSetTextAlignment(costFrame, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_MIDDLE)

        -- Самая главная строка в этой функции
        -- Записали пару тултип-кнопка в словарь игрока
        MUIStuff[playerID].dict[tooltip] = button

        return button, icon, tooltip, costFrame
    end


    function initFramesTrigger()
        TimerStart(CreateTimer(), .004, true, function()
            -- Мониторим текущий фрейм для каждого игрока, путём обхода словаря его тултипов
            for i = 1, #playerIDs do
                local id = playerIDs[i]
                if GetPlayerSlotState(Player(id)) == PLAYER_SLOT_STATE_PLAYING then
                    local t = MUIStuff[id]
                    t.lastFrame = t.currentFrame
                    t.currentFrame = nil

                    for k, v in pairs(t.dict) do
                        if BlzFrameIsVisible(k) then
                            t.currentFrame = v
                            break
                        end
                    end
                else
                    table.remove(playerIDs, i)
                end
            end
        end)

        local trig = CreateTrigger() -- Триггер кликов мышью
        local trigSync = CreateTrigger() -- Триггер синха

        for i = 0, bj_MAX_PLAYER_SLOTS - 1 do
            -- Регистрируем события мыши и события синха для каждого игрока
            TriggerRegisterPlayerEvent(trig, Player(i), EVENT_PLAYER_MOUSE_DOWN)
            TriggerRegisterPlayerEvent(trig, Player(i), EVENT_PLAYER_MOUSE_UP)

            -- будем синхронизировать не только клик, но и просто нажатие и отжатие, ибо я хочу использовать BlzFrameSetScale()
            -- чтобы имитировать анимацию кнопок

            BlzTriggerRegisterPlayerSyncEvent(trigSync, Player(i), "mouse_down", false)
            BlzTriggerRegisterPlayerSyncEvent(trigSync, Player(i), "mouse_up", false)
            BlzTriggerRegisterPlayerSyncEvent(trigSync, Player(i), "mouse_click", false)
        end

        TriggerAddCondition(trig, Condition(function()
            if BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_DOWN then
                local t = MUIStuff[GetPlayerId(GetTriggerPlayer())]
                if t.currentFrame then
                    t.pressedFrame = t.currentFrame
                    BlzSendSyncData("mouse_down", BlzFrameGetName(t.pressedFrame)) -- передаём всем имя
                end
            elseif BlzGetTriggerPlayerMouseButton() == MOUSE_BUTTON_TYPE_LEFT and GetTriggerEventId() == EVENT_PLAYER_MOUSE_UP then
                local t = MUIStuff[GetPlayerId(GetTriggerPlayer())] -- получили таблицу для проверяемого игрока, и теперь будем заполнять pressedFrame только для него
                if t.pressedFrame then
                    if t.pressedFrame == t.currentFrame then
                        -- Поймали клик по фрейму
                        BlzSendSyncData("mouse_click", BlzFrameGetName(t.pressedFrame)) -- передаём всем имя
                    else
                        BlzSendSyncData("mouse_up", BlzFrameGetName(t.pressedFrame)) -- передаём всем имя
                    end
                end
                t.pressedFrame = nil
            end
        end))

        TriggerAddCondition(trigSync, Condition(function()
            local prefix = BlzGetTriggerSyncPrefix()
            local data = BlzGetTriggerSyncData()

            if prefix == "mouse_down" then
                local fr = BlzGetFrameByName(data, 0)
                resetFrame(fr) --клик по фрейму блокирует события клавы (забирает фокус), так что такой костылёк может пригодиться, но не обязательно
                pressFrame(fr)
                return
            end

            if prefix == "mouse_up" then
                unpressFrame(BlzGetFrameByName(data, 0))
                return
            end

            if prefix == "mouse_click" then
                -- узнав игрока и фрейм можем запустить скрипт продажи предмета
                local fr = BlzGetFrameByName(data, 0)
                unpressFrame(fr)
                clickToShopFrame(fr, GetTriggerPlayer())
            end
        end))
    end

    function pressFrame(fr)
        BlzFrameSetScale(fr, .9)
    end

    function unpressFrame(fr)
        BlzFrameSetScale(fr, .9)
        TimerStart(CreateTimer(), 1 / 32, false, function()
            BlzFrameSetScale(fr, 1)
            DestroyTimer(GetExpiredTimer())
        end)
    end
end
`
ОЖИДАНИЕ РЕКЛАМЫ...
21
без рефа и без ужоПы бы сделали такое, для 1 .26 а так нет. Жаль!
Ответы (4)
20
SсRealm,
Ага, а ще бы сделали на гуи, без переменных, без монитора, без клавиатуры, без компьютера и без рук. А лучше если бы сделали даже без мыслей.
21
KaneThaumaturge, Зачем так сразу. Хотя по статистике комп работает намного дольше, если рядом нет юзера!))
20
SсRealm, Ну а как по другому, иди омлет приготовь без яиц
27
Ранее это использовал, пока мне Hate не сказал, что это может приводить десинкам. Поэтому, я решил переделать на ивенты, по правилам работать. Ивенты то синхронизируются. Потом сломали. И заново на братно переезжать не хотелось. Это как жил в деревне, а потом переехал в город. А теперь, уволили, откатился обратно в берлогу.

Ну хоть кто то написал. Спасибо)) замотивировал поделать
Ответы (1)
25
MpW, есть такое, видел у тебя где-то в статьях, что раньше так и делали. Назад в прошлое, получается.
По десинкам хз, я тестил в двух окнах, и вроде всё нормально было, если делать через синк триггер, и каждому игроку показывать только свои фреймы. Ну это пример магазина в конце статьи.
Этот комментарий удален
Чтобы оставить комментарий, пожалуйста, войдите на сайт.