Все три с половиной человека, делающие карты на рефе с фреймами знают о неприятном баге с фрейм-ивентами, появившемся ещё в 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) можно отказаться от использования фрейм-ивентов в пользу проверки видимости тултипов для контролируемых фреймов.
Базовый алгоритм:
- К каждому контрольному фрейму нужно привязать свой уникальный тултип. Тултип может быть как настоящим (обводка, всплывающая подсказка и т.д.), так и фейковым (пустой текстовый фрейм, прозрачный бекдроп и т.д.).
- Тултипы и фреймы объединяются в пары ключ-значение.
- В нужный момент проверяется видимость тултипа, если он видим, то можно сделать вывод о том, что курсор находится на фрейме.
Пример кода
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
Ага, а ще бы сделали на гуи, без переменных, без монитора, без клавиатуры, без компьютера и без рук. А лучше если бы сделали даже без мыслей.
По десинкам хз, я тестил в двух окнах, и вроде всё нормально было, если делать через синк триггер, и каждому игроку показывать только свои фреймы. Ну это пример магазина в конце статьи.