game_spellbar.lua

main 1041 lines · 36.1 KB Raw
Alex Alex Commit Client Upload Part 2 19/10/2026 00:06
1local spellBarWindow = nil
2local clickedSlot = nil
3local newKey = nil
4local slotHotkey = nil
5local comboLabel = nil
6local spellCooldowns = {} -- Tabela de cooldowns. Estrutura: spellCooldowns["PlayerName"]["nomeSpell"] = endTime
7local comboPreview = nil
8
9local vocation = 0
10local hotkeys = {} -- Tabela para armazenar os hotkeys salvos
11local lastUsedSpell = nil -- Armazena o último feitiço usado (para reverter cooldown se falhar)
12
13-- Variável para o cooldown global atual (será definido conforme a magia usada)
14local globalCooldownEnd = 0
15
16-- Variável para bloquear movimento enquanto a hotkey é usada
17local movementBlocker = nil
18
19----------------------------------------------------------------------
20-- LAYOUT RESPONSIVO (posição, tamanho, quebra de linha) --
21----------------------------------------------------------------------
22local SLOT_COUNT = 10
23local SLOT_SIZE = 36
24local SLOT_HEIGHT = 45 -- slot + hotkey number under it
25local SLOT_SPACING = 10
26local ROW_SPACING = 6
27local DEFAULT_BOTTOM = 90
28
29-- Position relative to the bottom-center of the game screen (like CSS
30-- "position: fixed; bottom: Npx; left: 50%"), so it follows window resizes.
31local barOffset = { x = 0, bottom = DEFAULT_BOTTOM }
32local rootPanel = nil
33
34local function barWidth(columns)
35 return columns * SLOT_SIZE + (columns - 1) * SLOT_SPACING
36end
37
38-- Client option "Show action bar" (client_options, id: actionBar)
39function isBarEnabled()
40 return not modules.client_options or modules.client_options.getOption('actionBar') ~= false
41end
42
43function setBarEnabled(enabled)
44 if not spellBarWindow then return end
45 if enabled and g_game.isOnline() and spellBarWindow:getChildById('slot-1') then
46 spellBarWindow:show()
47 relayout()
48 else
49 spellBarWindow:hide()
50 end
51end
52
53-- Re-anchors the bar to the bottom-center of the game screen, clamped so it
54-- never leaves the screen.
55function applyPosition()
56 if not spellBarWindow then return end
57 local parent = spellBarWindow:getParent()
58 if not parent or parent:getWidth() <= 0 then return end
59
60 local maxX = math.max(0, math.floor((parent:getWidth() - spellBarWindow:getWidth()) / 2))
61 local maxBottom = math.max(0, parent:getHeight() - spellBarWindow:getHeight())
62 local x = math.max(-maxX, math.min(maxX, barOffset.x))
63 local bottom = math.max(0, math.min(maxBottom, barOffset.bottom))
64
65 spellBarWindow:breakAnchors()
66 spellBarWindow:addAnchor(AnchorBottom, 'parent', AnchorBottom)
67 spellBarWindow:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter)
68 spellBarWindow:setMarginBottom(bottom)
69 spellBarWindow:setMarginLeft(math.max(0, x))
70 spellBarWindow:setMarginRight(math.max(0, -x))
71end
72
73-- One row of 10 slots, or two rows of 5 when the game screen is too narrow.
74function relayout()
75 if not spellBarWindow then return end
76 local parent = spellBarWindow:getParent()
77 if not parent or parent:getWidth() <= 0 then return end
78
79 local columns = SLOT_COUNT
80 if parent:getWidth() - 20 < barWidth(SLOT_COUNT) then
81 columns = math.ceil(SLOT_COUNT / 2)
82 end
83 local rows = math.ceil(SLOT_COUNT / columns)
84 spellBarWindow:setSize({ width = barWidth(columns), height = rows * SLOT_HEIGHT + (rows - 1) * ROW_SPACING })
85
86 for i = 1, SLOT_COUNT do
87 local slot = spellBarWindow:getChildById('slot-' .. i)
88 if slot then
89 slot:setMarginLeft(((i - 1) % columns) * (SLOT_SIZE + SLOT_SPACING))
90 slot:setMarginTop(math.floor((i - 1) / columns) * (SLOT_HEIGHT + ROW_SPACING))
91 end
92 end
93 applyPosition()
94end
95
96function resetPosition()
97 barOffset.x = 0
98 barOffset.bottom = DEFAULT_BOTTOM
99 g_settings.set('spellBarOffsetX', barOffset.x)
100 g_settings.set('spellBarOffsetBottom', barOffset.bottom)
101 applyPosition()
102end
103
104-- After a drag, remember where the player put it (relative to bottom-center)
105-- and anchor it again, instead of leaving it at fixed pixels.
106local function onBarDragLeave(widget, droppedWidget, mousePos)
107 local parent = widget:getParent()
108 if not parent then return end
109 barOffset.x = math.floor(widget:getX() + widget:getWidth() / 2 - (parent:getX() + parent:getWidth() / 2))
110 barOffset.bottom = math.floor(parent:getY() + parent:getHeight() - (widget:getY() + widget:getHeight()))
111 g_settings.set('spellBarOffsetX', barOffset.x)
112 g_settings.set('spellBarOffsetBottom', barOffset.bottom)
113 applyPosition()
114end
115
116local function onBarMousePress(widget, mousePos, mouseButton)
117 if mouseButton ~= MouseRightButton then return false end
118 local menu = g_ui.createWidget('PopupMenu')
119 menu:setGameMenu(true)
120 menu:addOption(tr('Reset spell bar position'), resetPosition)
121 menu:display(mousePos)
122 return true
123end
124
125----------------------------------------------------------------------
126-- BLOQUEIO E LIBERAÇÃO DE MOVIMENTO --
127----------------------------------------------------------------------
128function blockMovement()
129 if not movementBlocker then
130 -- Cria um widget invisível que captura o teclado e impede o movimento.
131 -- Usamos o estilo "UIWidget", que é padrão.
132 movementBlocker = g_ui.createWidget('UIWidget', modules.game_interface.getRootPanel())
133 if movementBlocker then
134 movementBlocker:setSize({ width = 1, height = 1 })
135 movementBlocker:setOpacity(0) -- Torna o widget invisível
136 movementBlocker:grabKeyboard()
137 movementBlocker.onKeyDown = function(widget, keyCode, keyboardModifiers)
138 local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers)
139 if keyCombo == "Up" or keyCombo == "Down" or keyCombo == "Left" or keyCombo == "Right" or
140 keyCombo:lower() == "w" or keyCombo:lower() == "a" or keyCombo:lower() == "s" or keyCombo:lower() == "d" then
141 return true -- Bloqueia as teclas de movimento
142 end
143 return false
144 end
145 else
146 print("Failed to create movementBlocker widget")
147 end
148 end
149end
150
151function unblockMovement()
152 if movementBlocker then
153 movementBlocker:destroy()
154 movementBlocker = nil
155 end
156end
157
158----------------------------------------------------------------------
159-- LISTA DE SPELLS --
160----------------------------------------------------------------------
161local spelllist = {
162 [1] = { -- Gon
163 ["Punch"] = {
164 name = '@#$0',
165 exhaustion = 1,
166 globalCooldown = 0.5, -- Cooldown global editável para esta magia
167 icon = 'HXH/gon/1',
168 level = 1,
169 mana = 0,
170 key = "F1",
171 desc = "Gon da um soco rápido no inimigo.",
172 movementBlockTime = 300 -- bloqueia movimento por 500ms
173 },
174 ["Sequence Punch"] = {
175 name = '@#$1',
176 exhaustion = 2,
177 globalCooldown = 0.8,
178 icon = 'HXH/gon/10',
179 level = 5,
180 mana = 0,
181 key = "F2",
182 desc = "Gon atinge o alvo com uma sequencia de socos.",
183 movementBlockTime = 500
184 },
185 ["Jajaken Gu"] = {
186 name = '@#$2',
187 exhaustion = 3,
188 globalCooldown = 1,
189 icon = 'HXH/gon/15',
190 level = 15,
191 mana = 0,
192 key = "F3",
193 desc = "Gon concentra aura em seu punho e desfere um soco de Nen.",
194 movementBlockTime = 800
195 },
196 ["Dash"] = {
197 name = '@#$3',
198 exhaustion = 7,
199 globalCooldown = 1,
200 icon = 'HXH/gon/25',
201 level = 20,
202 mana = 0,
203 key = "F4",
204 desc = "Gon usa seu instinto selvagem para se esquivar.",
205 movementBlockTime = 0
206 },
207 ["Ren"] = {
208 name = '@#$4',
209 exhaustion = 10,
210 globalCooldown = 1,
211 icon = 'HXH/gon/20',
212 level = 25,
213 mana = 0,
214 key = "F5",
215 desc = "Expande e intensifica a aura o máximo possível.",
216 movementBlockTime = 300
217 },
218 ["Rock Smash"] = {
219 name = '@#$5',
220 exhaustion = 20,
221 globalCooldown = 1.5,
222 icon = 'HXH/gon/35',
223 level = 35,
224 mana = 0,
225 key = "F6",
226 desc = "Gon acumula uma grande quantidade de aura em seu punho e atinge o solo.",
227 movementBlockTime = 1600
228 },
229 ["Jajaken Chi"] = {
230 name = '@#$6',
231 exhaustion = 10,
232 globalCooldown = 0.7,
233 icon = 'HXH/gon/50',
234 level = 50,
235 mana = 0,
236 key = "F7",
237 desc = "Gon transforma sua aura em uma lâmina de Nen.",
238 movementBlockTime = 1200
239 },
240 ["Jajaken Pa"] = {
241 name = '@#$7',
242 exhaustion = 15,
243 globalCooldown = 1.3,
244 icon = 'HXH/gon/55',
245 level = 55,
246 mana = 0,
247 key = "F8",
248 desc = "Gon transforma sua aura em uma esfera de Nen e a dispara em direção ao alvo.",
249 movementBlockTime = 2000
250 }
251 },
252
253 [2] = { -- Killua
254 ["Claws"] = {
255 name = 'Claws',
256 exhaustion = 1,
257 globalCooldown = 1,
258 icon = 'HXH/killua/1',
259 level = 1,
260 mana = 0,
261 key = "F1",
262 desc = "Killua ataca com suas mãos afiadas.",
263 movementBlockTime = 300
264 },
265 ["Double Claws"] = {
266 name = 'Double Claws',
267 exhaustion = 5,
268 globalCooldown = 1.2,
269 icon = 'HXH/killua/10',
270 level = 10,
271 mana = 0,
272 key = "F2",
273 desc = "Killua se move rapidamente criando imagens residuais para confundir o inimigo.",
274 movementBlockTime = 400
275 },
276 ["Critical Claws"] = {
277 name = 'Critical Claws',
278 exhaustion = 3,
279 globalCooldown = 1.05,
280 icon = 'HXH/killua/15',
281 level = 15,
282 mana = 0,
283 key = "F3",
284 desc = "Killua libera uma descarga elétrica concentrada em sua palma.",
285 movementBlockTime = 300
286 },
287 ["dark steps"] = {
288 name = 'dark steps',
289 exhaustion = 5,
290 globalCooldown = 2,
291 icon = 'HXH/killua/20',
292 level = 20,
293 mana = 0,
294 key = "F4",
295 desc = "Killua dispara um raio elétrico em seus inimigos.",
296 movementBlockTime = 700
297 },
298 ["Rhythm Echo"] = {
299 name = 'Rhythm Echo',
300 exhaustion = 15,
301 globalCooldown = 1,
302 icon = 'HXH/Killua/dash',
303 level = 25,
304 mana = 0,
305 key = "F5",
306 desc = "Killua atinge sua velocidade máxima com Nen elétrico.",
307 movementBlockTime = 300
308 },
309 ["Kanden shi"] = {
310 name = 'Kanden shi',
311 exhaustion = 10,
312 globalCooldown = 1,
313 icon = 'HXH/Killua/40',
314 level = 40,
315 mana = 0,
316 key = "F6",
317 desc = "Killua atinge sua velocidade máxima com Nen elétrico.",
318 movementBlockTime = 1250
319 }
320 },
321
322 [3] = { -- Kurapika
323 ["basic chain attack"] = {
324 name = 'Basic Chain Attack',
325 exhaustion = 2,
326 globalCooldown = 1,
327 icon = 'HXH/Kurapika_basic',
328 level = 1,
329 mana = 0,
330 key = "Q",
331 desc = "Kurapika ataca com sua corrente básica.",
332 movementBlockTime = 300
333 },
334 ["chain jail"] = {
335 name = 'Chain Jail',
336 exhaustion = 10,
337 globalCooldown = 1,
338 icon = 'HXH/Kurapika1',
339 level = 30,
340 mana = 0,
341 key = "1",
342 desc = "Kurapika prende o alvo com correntes de Nen que restringem seu movimento.",
343 movementBlockTime = 500
344 },
345 ["holy chain"] = {
346 name = 'Holy Chain',
347 exhaustion = 30,
348 globalCooldown = 1,
349 icon = 'HXH/Kurapika3',
350 level = 50,
351 mana = 0,
352 key = "2",
353 desc = "Correntes de cura restauram HP rapidamente.",
354 movementBlockTime = 600
355 },
356 ["judgment chain"] = {
357 name = 'Judgment Chain',
358 exhaustion = 60,
359 globalCooldown = 1,
360 icon = 'HXH/Kurapika2',
361 level = 70,
362 mana = 0,
363 key = "3",
364 desc = "Uma corrente mortal que perfura o coração do inimigo e o condena.",
365 movementBlockTime = 700
366 },
367 ["scarlet eyes mode"] = {
368 name = 'Scarlet Eyes Mode',
369 exhaustion = 90,
370 globalCooldown = 1,
371 icon = 'HXH/Kurapika4',
372 level = 100,
373 mana = 0,
374 key = "4",
375 desc = "Kurapika ativa seus olhos escarlates, aumentando drasticamente seu poder.",
376 movementBlockTime = 800
377 }
378 },
379
380 [4] = { -- Hisoka
381 ["basic card throw"] = {
382 name = 'Basic Card Throw',
383 exhaustion = 2,
384 globalCooldown = 1,
385 icon = 'HXH/Hisoka_basic',
386 level = 1,
387 mana = 0,
388 key = "Q",
389 desc = "Hisoka arremessa uma carta de baralho afiada no inimigo.",
390 movementBlockTime = 300
391 },
392 ["bungee gum trap"] = {
393 name = 'Bungee Gum Trap',
394 exhaustion = 10,
395 globalCooldown = 1,
396 icon = 'HXH/Hisoka1',
397 level = 30,
398 mana = 0,
399 key = "1",
400 desc = "Hisoka prende o inimigo com sua elasticidade especial.",
401 movementBlockTime = 400
402 },
403 ["card barrage"] = {
404 name = 'Card Barrage',
405 exhaustion = 30,
406 globalCooldown = 1,
407 icon = 'HXH/Hisoka2',
408 level = 50,
409 mana = 0,
410 key = "2",
411 desc = "Hisoka lança várias cartas mortais simultaneamente.",
412 movementBlockTime = 500
413 },
414 ["exploding trick"] = {
415 name = 'Exploding Trick',
416 exhaustion = 60,
417 globalCooldown = 1,
418 icon = 'HXH/Hisoka3',
419 level = 75,
420 mana = 0,
421 key = "3",
422 desc = "Hisoka engana o inimigo com uma ilusão antes de um golpe fatal.",
423 movementBlockTime = 600
424 },
425 ["full power mode"] = {
426 name = 'Full Power Mode',
427 exhaustion = 90,
428 globalCooldown = 1,
429 icon = 'HXH/Hisoka4',
430 level = 100,
431 mana = 0,
432 key = "4",
433 desc = "Hisoka libera seu verdadeiro poder, aumentando sua força e velocidade por um tempo limitado.",
434 movementBlockTime = 700
435 }
436 }
437}
438
439-- Define a função global para buscar feitiços
440_G.getSpellByLowerName = function(voc, lowerName)
441 for key, spell in pairs(spelllist[voc]) do
442 if spell.name:lower() == lowerName then
443 return spell
444 end
445 end
446 return nil
447end
448
449----------------------------------------------------------------------
450-- SALVAR/CARREGAR HOTKEYS E COOLDOWNS --
451----------------------------------------------------------------------
452function loadHotkeys()
453 if g_settings then
454 local data = g_settings.get("SpellbarHotkeys")
455 if data and data ~= "" then
456 local success, result = pcall(json.decode, data)
457 if success and result then
458 hotkeys = result
459 end
460 end
461 end
462end
463
464function saveHotkeys()
465 if g_settings then
466 local encoded = json.encode(hotkeys)
467 g_settings.set("SpellbarHotkeys", encoded)
468 end
469end
470
471-- Retorna o nome do player para indexar cooldowns
472local function getPlayerNameForStorage()
473 local player = g_game.getLocalPlayer()
474 if not player then return nil end
475 return player:getName()
476end
477
478-- Carrega cooldowns salvos em g_settings
479local function loadCooldowns()
480 local data = g_settings.get("SpellCooldowns")
481 if not data or data == "" then return end
482
483 local success, result = pcall(json.decode, data)
484 if not success or not result then return end
485
486 local playerName = getPlayerNameForStorage()
487 if not playerName or not result[playerName] then return end
488
489 -- Cria a tabela do player se não existir
490 if not spellCooldowns[playerName] then
491 spellCooldowns[playerName] = {}
492 end
493
494 -- Reaplica o endTime = os.time() + timeLeft
495 for spellName, timeLeft in pairs(result[playerName]) do
496 spellCooldowns[playerName][spellName] = os.time() + timeLeft
497 end
498end
499
500-- Salva cooldowns em g_settings
501local function saveCooldowns()
502 local playerName = getPlayerNameForStorage()
503 if not playerName then return end
504
505 local data = {}
506 -- Lê o que já existe no arquivo (para não sobrescrever outros chars)
507 local oldData = g_settings.get("SpellCooldowns")
508 local success, oldResult = pcall(json.decode, oldData or "")
509 if success and oldResult then
510 data = oldResult
511 end
512
513 -- Cria a tabela do player se não existir
514 if not data[playerName] then
515 data[playerName] = {}
516 end
517
518 -- Pega a tabela de cooldowns do player atual
519 if spellCooldowns[playerName] then
520 for spellName, endTime in pairs(spellCooldowns[playerName]) do
521 local timeLeft = endTime - os.time()
522 if timeLeft > 0 then
523 data[playerName][spellName] = timeLeft
524 else
525 data[playerName][spellName] = nil
526 end
527 end
528 end
529
530 local encoded = json.encode(data)
531 g_settings.set("SpellCooldowns", encoded)
532end
533
534----------------------------------------------------------------------
535-- INICIALIZAÇÃO --
536----------------------------------------------------------------------
537function init()
538 spellBarWindow = g_ui.loadUI('game_spellbar.otui', modules.game_interface.getRootPanel())
539
540 if not spellBarWindow then
541 print("Erro: Falha ao carregar a SpellBar")
542 return
543 end
544
545 spellBarWindow:setDraggable(true)
546 spellBarWindow.onDragLeave = onBarDragLeave
547 spellBarWindow.onMousePress = onBarMousePress
548
549 barOffset.x = tonumber(g_settings.get('spellBarOffsetX')) or 0
550 barOffset.bottom = tonumber(g_settings.get('spellBarOffsetBottom')) or DEFAULT_BOTTOM
551
552 -- follow the game screen size (window resize, side panels, fullscreen)
553 rootPanel = modules.game_interface.getRootPanel()
554 connect(rootPanel, { onGeometryChange = relayout })
555
556 loadHotkeys()
557
558 connect(g_game, {onGameStart = onGameStart, onGameEnd = offline})
559 connect(LocalPlayer, {onLevelChange = onLevelChange, onManaChange = onManaChange})
560 connect(g_game, {onTextMessage = onTextMessage})
561 connect(g_game, 'onTalk', onTalk)
562
563 ProtocolGame.registerExtendedOpcode(193, function(protocol, opcode, buffer) createSpellBar(buffer) end)
564end
565
566function terminate()
567 disconnect(g_game, {onGameStart = onGameStart})
568 disconnect(g_game, {onGameEnd = offline})
569 disconnect(LocalPlayer, {onLevelChange = onLevelChange})
570 disconnect(LocalPlayer, {onManaChange = onManaChange})
571 disconnect(g_game, 'onTalk', onTalk)
572
573 ProtocolGame.unregisterExtendedOpcode(193)
574 if rootPanel then
575 disconnect(rootPanel, { onGeometryChange = relayout })
576 rootPanel = nil
577 end
578 spellBarWindow:destroy()
579end
580
581----------------------------------------------------------------------
582-- CAPTURA MENSAGENS DE ERRO --
583----------------------------------------------------------------------
584function onTextMessage(mode, text)
585 local msgLower = text:lower()
586 if msgLower:find("this action is not permitted in a protection zone") or
587 msgLower:find("you can not cast that here") or
588 msgLower:find("sorry, not possible") or
589 msgLower:find("não pode usar isso aqui") or
590 msgLower:find("you can only use it on creatures") then
591 revertCooldown(lastUsedSpell)
592 lastUsedSpell = nil
593 end
594end
595
596function onTalk(name, level, mode, text, channelId, pos)
597 local msgLower = text:lower()
598 if msgLower:find("this action is not permitted in a protection zone") or
599 msgLower:find("you can not cast that here") or
600 msgLower:find("sorry, not possible") or
601 msgLower:find("não pode usar isso aqui ou sem alvo") then
602 revertCooldown(lastUsedSpell)
603 lastUsedSpell = nil
604 end
605end
606
607----------------------------------------------------------------------
608-- OFFLINE E CRIAÇÃO DA SPELLBAR --
609----------------------------------------------------------------------
610function offline()
611 -- Salva cooldowns antes de destruir a janela
612 saveCooldowns()
613
614 spellBarWindow:hide()
615 for i = 1, 10 do
616 local slot = spellBarWindow:recursiveGetChildById('slot-'..i)
617 if slot ~= nil then
618 slot:destroy()
619 end
620 end
621end
622
623function createSpellBar(buffer)
624 vocation = tonumber(buffer)
625 createSpellsIcons()
626 relayout()
627 if isBarEnabled() then
628 spellBarWindow:show()
629 end
630
631 -- Ao criar a spellbar, carregamos os cooldowns
632 loadCooldowns()
633 onManaChange(g_game.getLocalPlayer(), g_game.getLocalPlayer():getMana(), g_game.getLocalPlayer():getMaxMana())
634end
635
636function onGameStart()
637 scheduleEvent(function()
638 g_game.getProtocolGame():sendExtendedOpcode(193, 'refresh')
639 -- Carrega cooldowns após o login
640 loadCooldowns()
641 end, 10)
642end
643
644----------------------------------------------------------------------
645-- FUNÇÕES DE REVERTER COOLDOWN E TRATAR ERROS --
646----------------------------------------------------------------------
647function revertCooldown(spellName)
648 if not spellName then return end
649 local player = g_game.getLocalPlayer()
650 if not player then return end
651
652 local playerName = getPlayerNameForStorage()
653 if not playerName then return end
654
655 if spellCooldowns[playerName] and spellCooldowns[playerName][spellName] then
656 spellCooldowns[playerName][spellName] = nil
657
658 -- Reseta a barra de progresso
659 local progress = spellBarWindow:recursiveGetChildById('progress-'..spellName)
660 if progress then
661 progress:setPercent(100)
662 progress:setText('')
663 end
664
665 modules.game_textmessage.displayFailureMessage("não pode usar isso aqui ou sem alvo.")
666 end
667end
668
669----------------------------------------------------------------------
670-- MUDANÇA DE LEVEL/MANA E SPELLS ICÔNICOS --
671----------------------------------------------------------------------
672function onLevelChange(localPlayer, value, percent)
673 if spelllist[vocation] then -- Verificar si spelllist[vocation] existe
674 for _, list in pairs(spelllist[vocation]) do
675 local spellWidget = spellBarWindow:recursiveGetChildById('spell-'..list.name:lower())
676 if spellWidget then
677 local progress = spellBarWindow:recursiveGetChildById('progress-'..list.name:lower())
678 if localPlayer:getMana() >= list.mana then
679 if progress:getText() == '' then
680 if localPlayer:getLevel() < list.level then
681 progress:setImageSource('img/lockedicon')
682 progress:setPercent(0)
683 else
684 progress:setImageSource()
685 progress:setPercent(100)
686 end
687 end
688 end
689 end
690 end
691 end
692end
693
694function onManaChange(localPlayer, mana, maxMana)
695 if spelllist[vocation] then -- Verificar si spelllist[vocation] existe
696 for _, list in pairs(spelllist[vocation]) do
697 local spellWidget = spellBarWindow:recursiveGetChildById('spell-'..list.name:lower())
698 if not spellWidget then return end
699 local progress = spellBarWindow:recursiveGetChildById('progress-'..list.name:lower())
700 if localPlayer:getLevel() >= list.level then
701 if progress:getText() == '' then
702 if localPlayer:getMana() < list.mana then
703 progress:setPercent(0)
704 else
705 progress:setPercent(100)
706 end
707 end
708 end
709 end
710 end
711end
712
713function createSpellsIcons()
714 if not g_game.getLocalPlayer() then return end
715
716 if not spelllist[vocation] or type(spelllist[vocation]) ~= "table" then
717 return
718 end
719
720 local spells_icon = {}
721 for _, list in pairs(spelllist[vocation]) do
722 table.insert(spells_icon, {
723 name = list.name,
724 exhaustion = list.exhaustion,
725 globalCooldown = list.globalCooldown,
726 icon = list.icon,
727 level = list.level,
728 mana = list.mana,
729 key = list.key,
730 desc = list.desc
731 })
732 end
733
734 table.sort(spells_icon, function(a, b) return a.level < b.level end)
735
736 -- the server can send the spell list again (refresh): don't stack a second set of slots
737 for i = 1, SLOT_COUNT do
738 local oldSlot = spellBarWindow:getChildById('slot-'..i)
739 if oldSlot then
740 oldSlot:destroy()
741 end
742 end
743
744 -- positions are set by relayout()
745 for i = 1, SLOT_COUNT do
746 local slotSpell = g_ui.createWidget('SlotSpell', spellBarWindow)
747 slotSpell:setId('slot-'..i)
748
749 local spell = spells_icon[i]
750 if spell then
751 local icon = slotSpell:getChildById('spellButton')
752 icon:setId('spell-'..spell.name:lower())
753 icon:setTooltip('\nEnergy: '..spell.mana..'\nExhaustion: '..spell.exhaustion..'\nDescription: '..spell.desc)
754 icon.exhaustion = spell.exhaustion
755 icon:setImageSource('img/vocations/'..spell.icon)
756 icon.lvl = spell.level
757 icon.mana = spell.mana
758 icon.nowexausted = os.time()
759
760 icon.onMousePress = function(widget, mousePos, mouseButton)
761 if mouseButton == MouseLeftButton or mouseButton == MouseRightButton then
762 useSpell(spell.name:lower())
763 end
764 return true
765 end
766
767 local savedKey = hotkeys[spell.name:lower()]
768 if savedKey then
769 icon.key = g_keyboard.bindKeyPress(savedKey, function() useSpell(spell.name:lower()) end, modules.game_interface.getRootPanel())
770 icon.keypress = savedKey
771 else
772 icon.key = g_keyboard.bindKeyPress(spell.key, function() useSpell(spell.name:lower()) end, modules.game_interface.getRootPanel())
773 icon.keypress = spell.key
774 end
775
776 local progress = slotSpell:getChildById('spellProgress')
777 progress:setId('progress-'..spell.name:lower())
778 progress:setText('')
779 progress:setPercent(100)
780
781 if g_game.getLocalPlayer():getLevel() < spell.level then
782 progress:setImageSource('img/lockedicon')
783 progress:setPercent(0)
784 end
785 if g_game.getLocalPlayer():getMana() < spell.mana then
786 progress:setPercent(0)
787 end
788 end
789
790 local label = slotSpell:getChildById('spellHotkey')
791 label:setId('htk-'..i)
792 label:setText(i < 10 and tostring(i) or "0")
793 end
794end
795
796----------------------------------------------------------------------
797-- CHECAR ÁREA PROTEGIDA (OPCIONAL) --
798----------------------------------------------------------------------
799function canCastSpell(spellData)
800 local player = g_game.getLocalPlayer()
801 if player and type(player.isInProtectedArea) == "function" then
802 if player:isInProtectedArea() then
803 return false
804 end
805 end
806 return true
807end
808
809----------------------------------------------------------------------
810-- USO DE SPELL --
811----------------------------------------------------------------------
812function useSpell(getspell)
813 if not g_game.getLocalPlayer() then return end
814 local now = os.time()
815 if now < globalCooldownEnd then
816 modules.game_textmessage.displayFailureMessage("Aguarde " .. (globalCooldownEnd - now) .. "s para usar outra magia.")
817 return false
818 end
819
820 local spellWidget = spellBarWindow:recursiveGetChildById('spell-'..getspell)
821 local progress = spellBarWindow:recursiveGetChildById('progress-'..getspell)
822 if not spellWidget then return end
823 if progress:getText() ~= '' then return end
824 if progress:getPercent() < 100 then return end
825
826 local spellData = getSpellByLowerName(vocation, getspell)
827 if spellData then
828 -- Define o tempo de bloqueio específico para este feitiço (em ms) ou usa 300ms como padrão
829 local blockTime = spellData.movementBlockTime or 300
830 blockMovement()
831 scheduleEvent(function() unblockMovement() end, blockTime)
832
833 local playerName = getPlayerNameForStorage()
834 if not playerName then return end
835
836 if not spellCooldowns[playerName] then
837 spellCooldowns[playerName] = {}
838 end
839
840 if spellCooldowns[playerName][getspell] then
841 local remainingTime = spellCooldowns[playerName][getspell] - now
842 if remainingTime > 0 then
843 modules.game_textmessage.displayFailureMessage("Aguarde ["..remainingTime.."s] para usar esta magia novamente.")
844 return false
845 end
846 end
847
848 lastUsedSpell = getspell
849
850 local individualCooldown = math.max(spellData.exhaustion, 1)
851 spellCooldowns[playerName][getspell] = now + individualCooldown
852
853 local gcool = spellData.globalCooldown or 1
854 globalCooldownEnd = now + gcool
855
856 -- Verificar si spelllist[vocation] existe antes de iterarlo
857 if spelllist[vocation] then
858 for _, spell in pairs(spelllist[vocation]) do
859 local lowerSpellName = spell.name:lower()
860 if not spellCooldowns[playerName][lowerSpellName] or spellCooldowns[playerName][lowerSpellName] < globalCooldownEnd then
861 spellCooldowns[playerName][lowerSpellName] = globalCooldownEnd
862 end
863 end
864 end
865
866 g_game.talk(spellData.name)
867 startDownDelay(getspell)
868 end
869end
870
871----------------------------------------------------------------------
872-- CONTAGEM REGRESSIVA --
873----------------------------------------------------------------------
874function startDownDelay(getspell)
875 local spellWidget = spellBarWindow:recursiveGetChildById('spell-'..getspell)
876 local progress = spellBarWindow:recursiveGetChildById('progress-'..getspell)
877 if not spellWidget then return end
878 progress:setPercent(0)
879
880 local spellData = getSpellByLowerName(vocation, getspell)
881 if not spellData then return end
882
883 local playerName = getPlayerNameForStorage()
884 if not playerName then return end
885
886 spellWidget.nowexausted = os.time() + spellData.exhaustion
887
888 scheduleEvent(function() spellReload(getspell) end, 100)
889end
890
891function spellReload(getspell)
892 local player = g_game.getLocalPlayer()
893 if not player then return end
894
895 local spellWidget = spellBarWindow:recursiveGetChildById('spell-'..getspell)
896 local progress = spellBarWindow:recursiveGetChildById('progress-'..getspell)
897 if not spellWidget then return end
898
899 local spellData = getSpellByLowerName(vocation, getspell)
900 if not spellData then return end
901
902 local playerName = getPlayerNameForStorage()
903 if not playerName then return end
904
905 local exhausted = spellCooldowns[playerName][getspell]
906 if not exhausted then
907 progress:setPercent(100)
908 progress:setText('')
909 return
910 end
911
912 if player:getLevel() < spellData.level then
913 progress:setImageSource('img/lockedicon')
914 progress:setPercent(0)
915 progress:setText('')
916 return
917 else
918 local remainingTime = exhausted - os.time()
919 if remainingTime <= 0 then
920 progress:setPercent(100)
921 progress:setText('')
922 return
923 end
924
925 local perc = math.ceil((spellData.exhaustion - remainingTime) * 100 / spellData.exhaustion)
926 progress:setPercent(perc)
927
928 if remainingTime > 60 then
929 local minutos = math.floor(remainingTime / 60)
930 local segundos = remainingTime % 60
931 progress:setText(minutos .. 'm\n' .. segundos .. 's')
932 else
933 progress:setText(remainingTime .. 's')
934 end
935 end
936
937 scheduleEvent(function() spellReload(getspell) end, 500)
938end
939
940----------------------------------------------------------------------
941-- MUDAR HOTKEY VIA POPUPMENU --
942----------------------------------------------------------------------
943function createButtonKey(slot)
944 local icon = spellBarWindow:recursiveGetChildById('spell-'..slot)
945 local menu = g_ui.createWidget('PopupMenu')
946 menu:setGameMenu(true)
947 menu:addOption(tr('Set Hotkey to this slot'), function() buttonChangeKey() end)
948 menu:display({x = icon:getPosition().x+5, y = icon:getPosition().y-20})
949 clickedSlot = icon
950end
951
952function buttonChangeKey()
953 slotHotkey = g_ui.createWidget('SlotHotkey', rootWidget)
954 slotHotkey:grabKeyboard()
955 comboLabel = slotHotkey:getChildById('comboPreview')
956 comboLabel.keyCombo = ''
957 slotHotkey.onKeyDown = hotkeyCapture
958end
959
960function hotkeyCapture(slotHotkey, keyCode, keyboardModifiers)
961 local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers)
962
963 if keyCombo:match("^%d+$") then
964 local num = tonumber(keyCombo)
965 if num >= 1 and num <= 12 then
966 keyCombo = "F" .. num
967 else
968 g_game.getLocalPlayer():sendCancelMessage("Hotkey inválido! Use apenas números de 1 a 12 ou teclas F1 até F12.")
969 return true
970 end
971 end
972
973 local fNum = keyCombo:match("^F(%d+)$")
974 if not fNum then
975 g_game.getLocalPlayer():sendCancelMessage("Hotkey inválido! Use apenas teclas F1 até F12.")
976 return true
977 end
978
979 local num = tonumber(fNum)
980 if not (num >= 1 and num <= 12) then
981 g_game.getLocalPlayer():sendCancelMessage("Hotkey inválido! Use apenas teclas F1 até F12.")
982 return true
983 end
984
985 comboPreview = slotHotkey:getChildById('comboPreview')
986 comboPreview:setText(tr('Current hotkey to add: %s', keyCombo))
987 comboPreview.keyCombo = keyCombo
988 comboPreview:resizeToText()
989 slotHotkey:getChildById('addButton'):enable()
990 newKey = keyCombo
991 return true
992end
993
994function changeKey()
995 if clickedSlot ~= nil then
996 if newKey ~= 'none' then
997 for _, list in pairs(spelllist[vocation]) do
998 local spellWidget = spellBarWindow:recursiveGetChildById('spell-'..list.name:lower())
999 if spellWidget then
1000 if spellWidget.keypress == newKey or spellWidget.keypress2 == newKey then
1001 return true
1002 end
1003 end
1004 end
1005
1006 local id = string.explode(clickedSlot:getId(), '-')
1007 local spellName = id[2]
1008 local keypress = clickedSlot.keypress2
1009 if keypress ~= 'none' then
1010 clickedSlot.key2 = g_keyboard.unbindKeyPress(keypress, nil, modules.game_interface.getRootPanel())
1011 end
1012 clickedSlot.key2 = g_keyboard.bindKeyPress(newKey, function() useSpell(spellName) end, modules.game_interface.getRootPanel())
1013 clickedSlot.keypress2 = newKey
1014
1015 hotkeys[spellName] = newKey
1016 saveHotkeys()
1017
1018 clickedSlot = nil
1019 end
1020 end
1021end
1022
1023function toggleLock()
1024 if not spellBarWindow then return end
1025
1026 isLocked = not isLocked -- Alterna o estado
1027
1028 spellBarWindow:setDraggable(not isLocked) -- Ativa/desativa movimento
1029
1030 local lockButton = spellBarWindow:recursiveGetChildById('lockButton')
1031 if lockButton then
1032 if isLocked then
1033 lockButton:setText('??') -- ícone de destravado
1034 lockButton:setTooltip(tr('Unlock Spell Bar'))
1035 else
1036 lockButton:setText('??') -- ícone de travado
1037 lockButton:setTooltip(tr('Lock Spell Bar'))
1038 end
1039 end
1040end
1041
Top