-- ============================================================================ -- ItemFusion · Interfaz (CLIENTE) -- -- Ventana propia (se dibuja por frame, no es una CustomWindow nativa -- -- confirmado que OpenCustomWindow no tiene gancho LUA para pintar adentro). -- Se ancla al lado de donde abre el inventario, mismo truco que -- Scripts/NewWindow del Louis default (abre UIInventory + posiciona a su -- izquierda). -- -- Flujo: click en un slot vacio -> se "arma" (le avisa al server) -> -- el jugador hace click derecho (usar) sobre un item de su inventario -- real -> el server lo captura sin tocarlo y nos manda sus datos por -- paquete -> se dibuja el icono + preview. Con los 2 slots llenos y del -- mismo tipo, el boton "Fusionar" se habilita. -- -- El marco externo es el panel nativo de MU. La ornamentacion interna usa -- un solo atlas PNG cargado igual que el NewWindow default de Louis: -- LoadPngImage + RenderImage2. Si el atlas no carga, cae al dibujo DrawBar -- actual para no dejar la ventana inutilizable. Los ICONOS de item usan -- CreateItem (renderer nativo). El preview usa ShowDescriptionComplete, -- el tooltip nativo real del juego. -- ============================================================================ local CONFIG_MODULE = "Scripts\\ItemFusion\\Config" local CONFIG_FILE = "Data\\Custom\\Script\\Scripts\\ItemFusion\\Config.lua" local CONFIG = require(CONFIG_MODULE) ItemFusion = { Open = false, SlotA = nil, -- { Index, Level, Option1, Option2, Option3, NewOption, Socket1..5, SocketBonus } SlotB = nil, Jewels = {}, -- caja de jewels: 12 ranuras genericas, [1..12] = data del item Armed = nil, -- "A" | "B" | nil (esperando que el jugador use un item) Status = nil, -- texto de estado (ultimo resultado / hint) StatusColor = nil, StatusAt = 0, LastResult = nil, LastResultFrames = 0, Processing = 0, ProcessingTotal = 0, WaitingResult = false, PendingResult = nil, ConfirmPending = false, AltDown = false, -- ultimo estado conocido de Alt (ver UpdateModifierState) CtrlDown = false, -- usado solo para bloquear Ctrl + click derecho con la ventana abierta } -- Codigos de ranura del protocolo. TIENEN que ser identicos a los del script -- del SERVIDOR (Scripts/ItemFusion/ItemFusion.lua): si no coinciden, la joya -- se dibuja en la ranura equivocada o se ignora, y no salta ningun error. -- 0 = cuadro A 1 = cuadro B 2..13 = caja de jewels (12 ranuras) local SLOT_A, SLOT_B = 0, 1 local SLOT_JEWEL_FIRST = 2 local function JewelSlots() local jcfg = CONFIG.Jewels if type(jcfg) ~= "table" then return 12 end return tonumber(jcfg.Slots) or 12 end local function SlotKey(code) code = tonumber(code) if code == SLOT_A then return "A" end if code == SLOT_B then return "B" end if code ~= nil and code >= SLOT_JEWEL_FIRST and code <= (SLOT_JEWEL_FIRST + JewelSlots() - 1) then return "Jewel", code - SLOT_JEWEL_FIRST + 1 end return nil end -- --------------------------------------------------------------------------- -- Puntos de las joyas (espejo de la logica del servidor, ver CONFIG.Jewels). -- Solo se usa para el estimado que se muestra en pantalla; el resultado real -- lo decide el servidor. -- --------------------------------------------------------------------------- local function JewelEntry(itemIndex) local jcfg = CONFIG.Jewels if type(jcfg) ~= "table" or type(jcfg.Accepted) ~= "table" then return nil end itemIndex = tonumber(itemIndex) if itemIndex == nil then return nil end return jcfg.Accepted[itemIndex] end -- Unidades: una joya suelta vale 1, un bundle vale -- BundleQtyPerLevel * (Level + 1) -> Level 0 = x10, 1 = x20, 2 = x30. local function JewelUnits(itemIndex, itemLevel) local entry = JewelEntry(itemIndex) if entry == nil then return 0 end if tonumber(entry.Bundle) ~= 1 then return 1 end local jcfg = CONFIG.Jewels or {} local perLevel = tonumber(jcfg.BundleQtyPerLevel) or 10 local level = math.floor(tonumber(itemLevel) or 0) if level < 0 then level = 0 end return perLevel * (level + 1) end -- Points es cuanto suma CADA UNIDAD a la probabilidad. local function JewelPoints(itemIndex, itemLevel) local entry = JewelEntry(itemIndex) if entry == nil then return 0 end return (tonumber(entry.Points) or 0) * JewelUnits(itemIndex, itemLevel) end -- Total de puntos puestos en la caja ahora mismo. local function LocalJewelPoints() local total = 0 for i = 1, JewelSlots() do local data = ItemFusion.Jewels[i] if data ~= nil then total = total + JewelPoints(data.Index, data.Level) end end return total end -- Unidades puestas por familia (la joya suelta y su bundle comparten Family, -- asi que las dos cuentan para el mismo minimo). local function LocalFamilyUnits() local acc = {} for i = 1, JewelSlots() do local data = ItemFusion.Jewels[i] if data ~= nil then local entry = JewelEntry(data.Index) if entry ~= nil and entry.Family ~= nil then acc[entry.Family] = (acc[entry.Family] or 0) + JewelUnits(data.Index, data.Level) end end end return acc end -- Lista de { Family, Puestas, Pedidas, Ok } para pintar el estado del minimo. local function LocalMinimums() local out = {} local mins = (CONFIG.Jewels or {}).Minimums if type(mins) ~= "table" then return out end local have = LocalFamilyUnits() for family, required in pairs(mins) do local need = tonumber(required) or 0 local got = tonumber(have[family]) or 0 table.insert(out, { Family = family, Got = got, Need = need, Ok = got >= need }) end -- orden estable, si no cada frame podrian salir en distinto orden table.sort(out, function(a, b) return tostring(a.Family) < tostring(b.Family) end) return out end local function LocalMinimumsOk() for _, m in ipairs(LocalMinimums()) do if not m.Ok then return false end end return true end local SendAction local uiAtlasLoaded = false local uiAtlasTried = false local renderedAtlasLoaded = false local renderedAtlasTried = false local configReloadFrames = 0 local configReloadError = nil -- Tamaño REAL de cada atlas ya cargado, medido con GetImageWidth/Height. -- Sirve para no depender de que Width/Height del Config coincidan con el -- archivo: si no coinciden, el recorte sale de cualquier lado. -- Caso real (2026-08-02): se amplio el atlas de 512x256 a 512x512 y se -- actualizo el Config, pero en el cliente todavia estaba el PNG viejo -> los -- efectos (que viven en la mitad de abajo, v=0.5) caian justo sobre los -- botones. Midiendo la textura, eso no puede volver a pasar. local atlasRealSize = {} -- [id] = { w, h } local function ConfigTable(name) local value = CONFIG[name] if type(value) == "table" then return value end return {} end local function TableValue(t, key) if type(t) ~= "table" then return nil end local value = t[key] if type(value) == "table" then return value end return nil end local function Num(t, key, defaultValue) if type(t) ~= "table" then return defaultValue end local value = tonumber(t[key]) if value == nil then return defaultValue end return value end local function LayoutEntry(name) return TableValue(ConfigTable("LayoutTuning"), name) or {} end local function LayoutSlot(baseX, baseY, name, defX, defY, defSize) local entry = LayoutEntry(name) return baseX + Num(entry, "X", defX), baseY + Num(entry, "Y", defY), Num(entry, "Size", defSize) end local function RenderGroup(name) name = tostring(name or "") if name == "ItemA" or name == "ItemB" then return "Main" end if string.sub(name, 1, 5) == "Jewel" then return "Jewel" end if name == "Result" then return "Result" end return "Default" end local function RenderEntry(name) local tuning = ConfigTable("RenderTuning") return TableValue(tuning, name) or TableValue(tuning, RenderGroup(name)) or TableValue(tuning, "Default") or {} end -- Respaldo si el Config no trae RenderTuning. Alineado con los valores nuevos -- (ver el comentario de RenderTuning en Config.lua): son offsets de CENTRADO, -- no compensacion de desfase como eran los viejos. local function RenderDefaults(name, size) local group = RenderGroup(name) if group == "Main" then return -2, -4, 4, 4 end if group == "Jewel" then return -2, -2, 4, 4 end if group == "Result" then return -2, -4, 4, 4 end local itemPad = size <= 30 and 1 or math.floor(size * 0.08) return itemPad, itemPad, -(itemPad * 2), -(itemPad * 2) end local function AtlasSignature(cfg, key) if type(cfg) ~= "table" then return "" end local atlas = cfg[key] if type(atlas) ~= "table" then return "" end return table.concat({ tostring(atlas.Enable), tostring(atlas.Id), tostring(atlas.Path), tostring(atlas.Width), tostring(atlas.Height) }, "|") end local function ResetAtlasLoadersIfNeeded(oldConfig, newConfig) if AtlasSignature(oldConfig, "UiAtlas") ~= AtlasSignature(newConfig, "UiAtlas") then uiAtlasLoaded = false uiAtlasTried = false end if AtlasSignature(oldConfig, "RenderedAtlas") ~= AtlasSignature(newConfig, "RenderedAtlas") then renderedAtlasLoaded = false renderedAtlasTried = false end end local function LoadConfigFresh() if package ~= nil and package.loaded ~= nil then package.loaded[CONFIG_MODULE] = nil end if dofile ~= nil then local ok, fresh = pcall(dofile, CONFIG_FILE) if ok and type(fresh) == "table" then if package ~= nil and package.loaded ~= nil then package.loaded[CONFIG_MODULE] = fresh end return true, fresh end end local ok, fresh = pcall(require, CONFIG_MODULE) if ok and type(fresh) == "table" then return true, fresh end return false, fresh end local function HotReloadConfig() local cfg = ConfigTable("HotReload") if tonumber(cfg.Enable) ~= 1 then return end if configReloadFrames > 0 then configReloadFrames = configReloadFrames - 1 return end local interval = tonumber(cfg.Frames) or 20 if interval < 1 then interval = 20 end configReloadFrames = interval local ok, fresh = LoadConfigFresh() if ok then ResetAtlasLoadersIfNeeded(CONFIG, fresh) CONFIG = fresh configReloadError = nil elseif Console ~= nil then local msg = tostring(fresh) if configReloadError ~= msg then configReloadError = msg Console(1, "[ItemFusion] Config hot-reload fallo, se mantiene el anterior: " .. msg) end end end local function MeasureAtlas(id) if id == nil then return end local w, h if GetImageWidth ~= nil then local ok, v = pcall(GetImageWidth, id) if ok then w = tonumber(v) end end if GetImageHeight ~= nil then local ok, v = pcall(GetImageHeight, id) if ok then h = tonumber(v) end end if w ~= nil and h ~= nil and w > 1 and h > 1 then atlasRealSize[id] = { w, h } end end -- Devuelve el tamaño a usar para pasar pixeles a UV: el medido si se pudo, -- si no el declarado en el Config. local function AtlasSize(atlas, defW, defH) local id = tonumber(atlas.Id) local real = (id ~= nil) and atlasRealSize[id] or nil if real ~= nil then return real[1], real[2] end return (tonumber(atlas.Width) or defW), (tonumber(atlas.Height) or defH) end local function LoadUiAtlas() if uiAtlasLoaded then return true end if uiAtlasTried then return false end uiAtlasTried = true local atlas = CONFIG.UiAtlas or {} if tonumber(atlas.Enable) ~= 1 then return false end if LoadPngImage == nil or atlas.Id == nil or atlas.Path == nil then return false end local ok, result = pcall(LoadPngImage, tonumber(atlas.Id), tostring(atlas.Path)) uiAtlasLoaded = ok and (result == true or tonumber(result) == 1) if uiAtlasLoaded then MeasureAtlas(tonumber(atlas.Id)) -- Avisar si el archivo no es del tamaño que declara el Config: casi -- siempre significa que quedo una version vieja del PNG en el cliente. local rw, rh = AtlasSize(atlas, 0, 0) local cw, ch = tonumber(atlas.Width) or 0, tonumber(atlas.Height) or 0 if Console ~= nil and rw > 0 and (rw ~= cw or rh ~= ch) then Console(1, string.format( "[ItemFusion] OJO: el atlas mide %dx%d pero el Config dice %dx%d. Se usa el real (PNG desactualizado?)", rw, rh, cw, ch)) end elseif Console ~= nil then Console(1, "[ItemFusion] No se pudo cargar el atlas PNG: " .. tostring(atlas.Path)) end return uiAtlasLoaded end local function LoadRenderedAtlas() if renderedAtlasLoaded then return true end if renderedAtlasTried then return false end renderedAtlasTried = true local atlas = CONFIG.RenderedAtlas or {} if tonumber(atlas.Enable) ~= 1 then return false end if LoadPngImage == nil or atlas.Id == nil or atlas.Path == nil then return false end -- Hoy UiAtlas y RenderedAtlas son el MISMO archivo con el mismo Id (una -- sola textura para todo). Si ya se cargo, no volver a cargarla. local ui = CONFIG.UiAtlas or {} if uiAtlasLoaded and tonumber(ui.Id) == tonumber(atlas.Id) then renderedAtlasLoaded = true return true end local ok, result = pcall(LoadPngImage, tonumber(atlas.Id), tostring(atlas.Path)) renderedAtlasLoaded = ok and (result == true or tonumber(result) == 1) if renderedAtlasLoaded then MeasureAtlas(tonumber(atlas.Id)) end if not renderedAtlasLoaded and Console ~= nil then Console(1, "[ItemFusion] No se pudo cargar el atlas render PNG: " .. tostring(atlas.Path)) end return renderedAtlasLoaded end function ItemFusion_MainLoader() LoadUiAtlas() LoadRenderedAtlas() end BridgeFunctionAttach("MainLoader", "ItemFusion_MainLoader") -- --------------------------------------------------------------------------- -- Dibujo base (mismo primitivo que Theme.lua de MasterHub: SetBlend + -- glColor4f + DrawBar + EndDrawBar). Colores en 0-255 para que sea comodo -- de leer/ajustar, se convierten a 0-1 aca adentro. -- --------------------------------------------------------------------------- local function Rect(x, y, w, h, color, alphaMul) if w <= 0 or h <= 0 or color == nil then return end SetBlend(1) glColor4f(color[1] / 255, color[2] / 255, color[3] / 255, (color[4] / 255) * (alphaMul or 1)) DrawBar(x, y, w, h) EndDrawBar() end local function Text(x, y, value, w, align, color, font) if value == nil then return end SetFontType(font or 0) if SetTextBg ~= nil then pcall(SetTextBg, 0, 0, 0, 0) end if color ~= nil then SetTextColor(color[1], color[2], color[3], color[4] or 255) end if RenderText5 ~= nil and pcall(RenderText5, x, y, tostring(value), w, align or 0) then return end RenderText(x, y, tostring(value), w, align or 0) end local function DrawSprite(spriteName, x, y, w, h, alpha) if RenderImage2 == nil or not LoadUiAtlas() then return false end local atlas = CONFIG.UiAtlas or {} local sprites = atlas.Sprites or {} local sprite = sprites[spriteName] if sprite == nil then return false end -- Tamaño MEDIDO de la textura, no el declarado (ver MeasureAtlas). local atlasW, atlasH = AtlasSize(atlas, 512, 512) local sx = tonumber(sprite[1]) or 0 local sy = tonumber(sprite[2]) or 0 local sw = tonumber(sprite[3]) or w local sh = tonumber(sprite[4]) or h -- Si el sprite cae fuera de la textura real, no dibujar: mejor no mostrar -- nada que mostrar un recorte equivocado (era lo que pasaba con los -- efectos: caian sobre los botones). if sx + sw > atlasW or sy + sh > atlasH then return false end if SetBlend ~= nil then pcall(SetBlend, 1) end if glColor4f ~= nil then pcall(glColor4f, 1.0, 1.0, 1.0, alpha or 1.0) end local ok = pcall(RenderImage2, tonumber(atlas.Id), x, y, w, h, sx / atlasW, sy / atlasH, sw / atlasW, sh / atlasH, 1, 1, alpha or 1.0) return ok end local function DrawRenderedAsset(name, x, y, w, h, alpha) if RenderImage2 == nil or not LoadRenderedAtlas() then return false end local atlas = CONFIG.RenderedAtlas or {} local sprites = atlas.Sprites or {} local sprite = sprites[name] if sprite == nil or atlas.Id == nil then return false end local atlasW, atlasH = AtlasSize(atlas, 512, 1024) local sx = tonumber(sprite[1]) or 0 local sy = tonumber(sprite[2]) or 0 local sw = tonumber(sprite[3]) or w local sh = tonumber(sprite[4]) or h if sx + sw > atlasW or sy + sh > atlasH then return false end if SetBlend ~= nil then pcall(SetBlend, 1) end if glColor4f ~= nil then pcall(glColor4f, 1.0, 1.0, 1.0, alpha or 1.0) end return pcall(RenderImage2, tonumber(atlas.Id), x, y, w, h, sx / atlasW, sy / atlasH, sw / atlasW, sh / atlasH, 1, 1, alpha or 1.0) end local function DrawRenderedSlot(x, y, size, hover) return DrawRenderedAsset("Slot", x, y, size, size, hover and 1.0 or 0.96) end -- =========================================================================== -- EFECTOS -- -- Todo se anima con un contador de frames que baja en cada render: no hay -- timers ni corrutinas. FX.Tick es un contador que sube siempre y se usa -- para las animaciones ciclicas (latido de la runa, barrido del boton). -- -- Cada efecto se dibuja con un sprite del atlas (FxGlow/FxRing/...) sobre -- el elemento correspondiente. Si el atlas no cargo, DrawSprite devuelve -- false y simplemente no se ve el efecto -- la ventana sigue funcionando. -- =========================================================================== local FX = { Tick = 0, PlaceA = 0, -- frames restantes del pulso al poner item en A PlaceB = 0, Success = 0, -- frames restantes de la animacion de exito Fail = 0, -- frames restantes del parpadeo de error Sparks = nil, -- posiciones de las chispas del exito } local function FxOn() local f = CONFIG.Fx or {} return tonumber(f.Enable) == 1 end local function FxCfg(key, def) local f = CONFIG.Fx or {} local v = tonumber(f[key]) if v == nil then return def end return v end local function Clamp01(t) if t < 0 then t = 0 elseif t > 1 then t = 1 end return t end -- Curvas suaves para que los sprites entren/salgan sin verse mecanicos. local function EaseOut(t) t = Clamp01(t) return 1 - (1 - t) * (1 - t) end local function EaseInOut(t) t = Clamp01(t) return 0.5 - math.cos(t * 3.1416) * 0.5 end local function FxPlaySound(key) local f = CONFIG.Fx or {} local id = tonumber(f[key]) if id ~= nil and PlaySound ~= nil then pcall(PlaySound, id) end end -- Se llama cuando el servidor confirma que un item entro en un cuadro. function ItemFusion_FxPlace(letter) if not FxOn() then return end local n = FxCfg("PlaceFrames", 26) if letter == "B" then FX.PlaceB = n else FX.PlaceA = n end FxPlaySound("SoundPlace") end function ItemFusion_FxSuccess() if not FxOn() then return end FX.Success = FxCfg("SuccessFrames", 46) -- Chispas con direccion, tamano y delay propios: evita el estallido rigido. local n = FxCfg("SuccessSparks", 7) FX.Sparks = {} for i = 1, n do local ang = ((i - 1) / n) * 6.2832 + (((i * 11) % 9) - 4) * 0.035 table.insert(FX.Sparks, { a = ang, d = 0.70 + ((i * 37) % 48) / 100, delay = ((i * 13) % 22) / 100, scale = 0.76 + ((i * 17) % 32) / 100, }) end FxPlaySound("SoundSuccess") end function ItemFusion_FxFail() if not FxOn() then return end FX.Fail = FxCfg("FailFrames", 22) FxPlaySound("SoundFail") end local function ClearAllClientSlots() ItemFusion.SlotA = nil ItemFusion.SlotB = nil ItemFusion.Jewels = {} end local function ItemFusion_ApplyResult(result) if result == nil then return end ItemFusion.WaitingResult = false ItemFusion.Processing = 0 ItemFusion.ProcessingTotal = 0 ItemFusion.PendingResult = nil ItemFusion.ConfirmPending = false if result.Ok == 1 then local data = result.Data ItemFusion.Status = "Fusion completa." ItemFusion.StatusColor = CONFIG.Colors.Success ClearAllClientSlots() ItemFusion.LastResult = data ItemFusion.LastResultFrames = FxCfg("ResultHoldFrames", 150) ItemFusion_FxSuccess() else local reason = tonumber(result.Reason) or 4 local msgs = { [0] = "No hay espacio en el inventario.", [1] = "Los items no son del mismo tipo.", [2] = "Uno de los items ya no esta ahi.", [3] = "Faltan items para fusionar.", [4] = "Error al fusionar, proba de nuevo.", [5] = "Es el mismo item en los 2 slots, usá uno diferente.", [6] = "La fusion fallo.", [7] = "Faltan jewels en la caja.", } ItemFusion.Status = msgs[reason] or msgs[4] ItemFusion.StatusColor = CONFIG.Colors.Error if reason == 6 then ClearAllClientSlots() ItemFusion.LastResult = nil ItemFusion.LastResultFrames = 0 end ItemFusion_FxFail() end ItemFusion.StatusAt = os.time() end -- Baja los contadores. Se llama UNA vez por frame desde el render. local function FxUpdate() FX.Tick = FX.Tick + 1 if ItemFusion.Processing ~= nil and ItemFusion.Processing > 0 then ItemFusion.Processing = ItemFusion.Processing - 1 if ItemFusion.Processing == 0 then if ItemFusion.PendingResult ~= nil then ItemFusion_ApplyResult(ItemFusion.PendingResult) elseif ItemFusion.ConfirmPending then ItemFusion.ConfirmPending = false ItemFusion.Status = "Confirmando..." ItemFusion.StatusColor = CONFIG.Colors.TitleText if SendAction ~= nil then SendAction(5) end end end end if FX.PlaceA > 0 then FX.PlaceA = FX.PlaceA - 1 end if FX.PlaceB > 0 then FX.PlaceB = FX.PlaceB - 1 end if FX.Success > 0 then FX.Success = FX.Success - 1 if FX.Success == 0 then FX.Sparks = nil end end if FX.Fail > 0 then FX.Fail = FX.Fail - 1 end if ItemFusion.LastResultFrames ~= nil and ItemFusion.LastResultFrames > 0 then ItemFusion.LastResultFrames = ItemFusion.LastResultFrames - 1 if ItemFusion.LastResultFrames == 0 then ItemFusion.LastResult = nil end end end -- Halo controlado sobre un cuadro de item, con intensidad 0..1 local function FxSlotGlow(x, y, size, intensity) if intensity <= 0 then return end intensity = Clamp01(intensity) local grow = 6 + 12 * intensity DrawSprite("FxBox", x - grow / 2, y - grow / 2, size + grow, size + grow, intensity * 0.34) end -- Pulso al colocar: onda corta + chispa leve, sin lavar el icono. local function FxDrawPlace(x, y, size, frames) if frames <= 0 then return end local total = FxCfg("PlaceFrames", 26) local left = Clamp01(frames / total) -- 1 -> 0 local age = 1 - left -- 0 -> 1 local pulse = math.sin(age * 3.1416) local soft = left * (0.60 + pulse * 0.40) FxSlotGlow(x, y, size, soft) local ring = size * (0.74 + EaseOut(age) * 0.58) DrawSprite("FxRing", x + size / 2 - ring / 2, y + size / 2 - ring / 2, ring, ring, 0.30 * left) local flash = 1 - Clamp01(age / 0.36) if flash > 0 then local s = size * (0.22 + EaseInOut(age / 0.36) * 0.32) DrawSprite("FxStar", x + size / 2 - s / 2, y + size / 2 - s / 2, s, s, flash * 0.48) end end -- Latido continuo de la runa cuando los dos cuadros estan listos local function FxDrawRunePulse(x, y, size) if FxCfg("RunePulse", 1) ~= 1 then return end local period = FxCfg("RunePeriod", 70) local phase = (FX.Tick % period) / period local wave = EaseInOut((math.sin(phase * 6.2832) + 1) / 2) local grow = 8 + wave * 10 DrawSprite("FxRing", x - grow / 2, y - grow / 2, size + grow, size + grow, 0.22 + wave * 0.18) end -- Brillo que cruza el boton de izquierda a derecha local function FxDrawButtonSweep(x, y, w, h) if FxCfg("ButtonSweep", 1) ~= 1 then return end local period = FxCfg("SweepPeriod", 95) local phase = (FX.Tick % period) / period -- solo se ve durante el primer 35% del ciclo: pasa y descansa if phase > 0.35 then return end local t = phase / 0.35 local sw = h * 1.9 local sx = x - sw + (w + sw * 2) * t -- se atenua en los bordes para que no aparezca/desaparezca de golpe local fade = 1 - math.abs(t * 2 - 1) DrawSprite("FxSweep", sx, y, sw, h, 0.55 * fade) end local function FxDrawProcessing(cx, cy, size, barX, barY, barW, barH, progress) if not ItemFusion.WaitingResult then return end progress = Clamp01(progress or 0) local phase = (FX.Tick % 96) / 96 local breathe = 0.5 + math.sin(phase * 6.2832) * 0.5 local ring = size * (0.95 + breathe * 0.22) DrawSprite("FxRing", cx - ring / 2, cy - ring / 2, ring, ring, 0.22 + breathe * 0.16) for i = 1, 6 do local a = phase * 6.2832 + i * 1.0472 local dist = size * (0.55 + (i % 2) * 0.12) local s = size * (0.12 + ((i * 7) % 5) * 0.01) DrawSprite("FxStar", cx + math.cos(a) * dist - s / 2, cy + math.sin(a) * dist * 0.68 - s / 2, s, s, 0.26 + breathe * 0.22) end Rect(barX, barY, barW, barH, { 9, 7, 7, 190 }) Rect(barX + 1, barY + 1, barW - 2, barH - 2, { 40, 18, 18, 210 }) Rect(barX + 1, barY + 1, (barW - 2) * progress, barH - 2, { 174, 118, 66, 235 }) Rect(barX + 1, barY, barW - 2, 1, { 235, 206, 142, 190 }) end -- Fusion exitosa: capas escalonadas, mas ceremonial que explosiva. local function FxDrawSuccess(cx, cy, size) if FX.Success <= 0 then return end local total = FxCfg("SuccessFrames", 46) local t = 1 - (FX.Success / total) -- 0 -> 1 local fade = 1 - EaseInOut(t) local ring = size * (0.82 + EaseOut(t) * 1.70) DrawSprite("FxRing", cx - ring / 2, cy - ring / 2, ring, ring, fade * 0.72) local outer = size * (1.05 + EaseOut(t) * 2.20) DrawSprite("FxRing", cx - outer / 2, cy - outer / 2, outer, outer, fade * 0.24) local bloom = size * (1.10 + math.sin(Clamp01(t) * 3.1416) * 0.36) DrawSprite("FxGlow", cx - bloom / 2, cy - bloom / 2, bloom, bloom, fade * 0.34) local beamFade = 1 - Clamp01(t / 0.58) local beamH = size * (1.70 + EaseOut(t) * 0.95) local beamW = size * (0.16 + math.sin(t * 3.1416) * 0.14) * beamFade if beamW > 2 then DrawSprite("FxBeam", cx - beamW / 2, cy - beamH / 2, beamW, beamH, beamFade * 0.52) end local flash = 1 - Clamp01(t / 0.24) if flash > 0 then local s = size * (0.38 + EaseOut(t / 0.24) * 0.78) DrawSprite("FxStar", cx - s / 2, cy - s / 2, s, s, flash * 0.62) end if FX.Sparks ~= nil then for _, sp in ipairs(FX.Sparks) do local st = (t - sp.delay) / (1 - sp.delay) if st > 0 then st = Clamp01(st) local drift = size * (0.22 + EaseOut(st) * 1.35) * sp.d local curve = math.sin(st * 3.1416) * 0.18 local ang = sp.a + curve local ss = size * 0.30 * sp.scale * (1 - st) if ss > 2 then DrawSprite("FxStar", cx + math.cos(ang) * drift - ss / 2, cy + math.sin(ang) * drift - ss / 2, ss, ss, (1 - st) * 0.82) end end end end end -- Parpadeo rojo del panel al fallar local function FxDrawFail(x, y, w, h) if FX.Fail <= 0 then return end local total = FxCfg("FailFrames", 22) local t = FX.Fail / total -- dos parpadeos local blink = math.abs(math.sin(t * 6.2832)) Rect(x, y, w, h, { 190, 40, 40, 255 }, 0.20 * t * blink) end local function SoftRect(x, y, w, h, color, alphaMul) if w <= 4 or h <= 4 then Rect(x, y, w, h, color, alphaMul) return end Rect(x + 2, y, w - 4, 1, color, alphaMul) Rect(x + 1, y + 1, w - 2, 1, color, alphaMul) Rect(x, y + 2, w, h - 4, color, alphaMul) Rect(x + 1, y + h - 2, w - 2, 1, color, alphaMul) Rect(x + 2, y + h - 1, w - 4, 1, color, alphaMul) end local function SoftFrame(x, y, w, h, bg, border, highlight, shadow) SoftRect(x + 1, y + 1, w, h, { 0, 0, 0, 105 }) SoftRect(x, y, w, h, bg) Rect(x + 2, y, w - 4, 1, highlight or border) Rect(x + 1, y + 1, w - 2, 1, border) Rect(x, y + 2, 1, h - 4, border) Rect(x + w - 1, y + 2, 1, h - 4, shadow or border) Rect(x + 1, y + h - 2, w - 2, 1, shadow or border) Rect(x + 2, y + h - 1, w - 4, 1, { 0, 0, 0, 180 }) end local function MetalBox(x, y, size, bg, hover) local steelHi = hover and { 188, 150, 150, 255 } or { 154, 156, 150, 255 } local steelMid = hover and { 98, 45, 48, 255 } or { 74, 74, 72, 255 } local steelDark = { 16, 17, 17, 255 } local accent = hover and CONFIG.Colors.SlotBorderOn or CONFIG.Colors.BorderGold SoftRect(x + 2, y + 3, size, size, { 0, 0, 0, 82 }) SoftRect(x, y, size, size, { 18, 20, 20, 245 }) SoftRect(x + 1, y + 1, size - 2, size - 2, steelMid) SoftRect(x + 3, y + 3, size - 6, size - 6, { 8, 9, 9, 245 }) SoftRect(x + 6, y + 6, size - 12, size - 12, bg) Rect(x + 5, y + 3, size - 10, 1, steelHi) Rect(x + 5, y + size - 4, size - 10, 1, steelDark) Rect(x + 3, y + 5, 1, size - 10, steelHi) Rect(x + size - 4, y + 5, 1, size - 10, steelDark) Rect(x + 8, y + 8, size - 16, size - 16, { 0, 0, 0, 82 }) -- Esquinas tipo garra: detalle medieval nítido sin pernos gigantes. Rect(x + 2, y + 2, 9, 1, accent) Rect(x + 2, y + 2, 1, 9, accent) Rect(x + size - 11, y + 2, 9, 1, accent) Rect(x + size - 3, y + 2, 1, 9, accent) Rect(x + 2, y + size - 3, 9, 1, steelHi) Rect(x + 2, y + size - 11, 1, 9, steelHi) Rect(x + size - 11, y + size - 3, 9, 1, steelDark) Rect(x + size - 3, y + size - 11, 1, 9, steelDark) Rect(x + size / 2 - 2, y + 2, 4, 1, CONFIG.Colors.TitleText, 0.45) end local function MetalButton(x, y, w, h, hover, disabled) local C = CONFIG.Colors local sprite = disabled and "ButtonOff" or "Button" if DrawRenderedAsset(sprite, x, y, w, h, 1.0) then return end if DrawSprite(sprite, x, y, w, h, 1.0) then return end local bg = disabled and C.BtnBgOff or (hover and C.BtnBgHover or C.BtnBg) local edge = hover and C.BorderGold or C.BtnBorder SoftRect(x + 1, y + 1, w, h, { 0, 0, 0, 55 }) SoftRect(x, y, w, h, bg, disabled and 0.82 or 1.0) Rect(x + 3, y, w - 6, 1, { 154, 126, 126, 230 }) Rect(x + 3, y + h - 1, w - 6, 1, { 10, 10, 10, 245 }) Rect(x, y + 3, 1, h - 6, { 118, 76, 78, 225 }) Rect(x + w - 1, y + 3, 1, h - 6, { 18, 18, 18, 245 }) Rect(x + 5, y + 5, w - 10, 1, { 108, 110, 104, 145 }) Rect(x + 5, y + h - 6, w - 10, 1, { 0, 0, 0, 130 }) Rect(x + 3, y + 3, 8, 1, edge) Rect(x + 3, y + 3, 1, 8, edge) Rect(x + w - 11, y + 3, 8, 1, edge) Rect(x + w - 4, y + 3, 1, 8, edge) Rect(x + 3, y + h - 4, 8, 1, C.BorderGoldLo) Rect(x + w - 11, y + h - 4, 8, 1, C.BorderGoldLo) Rect(x + w / 2 - 9, y + 2, 18, 1, C.TitleText, 0.28) end local function DecorLine(x, y, w, color) if w < 110 then local C = CONFIG.Colors local c = color or C.BorderGoldLo Rect(x, y, w / 2 - 5, 1, c) Rect(x + w / 2 + 5, y, w / 2 - 5, 1, c) Rect(x + w / 2 - 2, y - 2, 4, 4, C.BorderGold) Rect(x + w / 2 - 1, y - 1, 2, 2, C.TitleText, 0.45) return end if DrawRenderedAsset("Divider", x, y - 12, w, 24, 1.0) then return end if w >= 120 and DrawSprite("Divider", x, y - 5, w, 12, 1.0) then return end local C = CONFIG.Colors local c = color or C.BorderGoldLo local cx = x + w / 2 Rect(x, y, (w - 12) / 2, 1, c) Rect(cx + 6, y, (w - 12) / 2, 1, c) Rect(cx - 1, y - 3, 2, 7, C.BorderGoldLo) Rect(cx - 3, y - 1, 6, 3, C.BorderGold, 0.9) Rect(cx - 1, y - 1, 2, 3, C.TitleText, 0.38) end -- Marco tipo "placa de metal con borde dorado": sombra + panel + doble -- borde (dorado + sombra dorada 1px adentro) para dar sensacion de relieve. local function FramePlate(x, y, w, h, bg) local C = CONFIG.Colors Rect(x + 2, y + 2, w, h, { 0, 0, 0, 120 }) -- sombra Rect(x, y, w, h, bg or C.PanelBg) -- placa Rect(x, y, w, 1, C.BorderGold) -- borde sup Rect(x, y + h - 1, w, 1, C.BorderGoldLo) -- borde inf (sombra) Rect(x, y, 1, h, C.BorderGold) -- borde izq Rect(x + w - 1, y, 1, h, C.BorderGoldLo) -- borde der (sombra) end local function NativeMuPanel(x, y, w, h) if RenderImage == nil then return false end pcall(RenderImage, 31322, x, y, w, h) pcall(RenderImage, 31353, x, y, w, 64) pcall(RenderImage, 31355, x, y + 64, 21, h - 109) pcall(RenderImage, 31356, x + w - 21, y + 64, 21, h - 109) pcall(RenderImage, 31357, x, y + h - 45, w, 45) return true end local function MouseIn(x, y, w, h) return (tonumber(CheckMouseIn(x, y, w, h)) or 0) ~= 0 end -- --------------------------------------------------------------------------- -- Paquetes -- --------------------------------------------------------------------------- function SendAction(action) local name = CONFIG.PacketName CreatePacket(name, CONFIG.PacketHead) SetBytePacket(name, action) if GetPlayerIndex ~= nil then SendPacket(name, GetPlayerIndex()) else SendPacket(name, 0) end ClearPacket(name) end -- Accion 8: "poner ESTE slot del inventario en una ranura de la ventana". -- Es el camino natural (agarrar el item y soltarlo en el cuadro), no necesita -- armar nada previo. local function SendPlaceItem(slotCode, invSlot) local name = CONFIG.PacketName CreatePacket(name, CONFIG.PacketHead) SetBytePacket(name, 8) SetBytePacket(name, tonumber(slotCode) or SLOT_A) SetBytePacket(name, invSlot) if GetPlayerIndex ~= nil then SendPacket(name, GetPlayerIndex()) else SendPacket(name, 0) end ClearPacket(name) end local function SendClearSlot(slotCode) local name = CONFIG.PacketName CreatePacket(name, CONFIG.PacketHead) SetBytePacket(name, 13) SetBytePacket(name, tonumber(slotCode) or SLOT_A) if GetPlayerIndex ~= nil then SendPacket(name, GetPlayerIndex()) else SendPacket(name, 0) end ClearPacket(name) end -- --------------------------------------------------------------------------- -- Item "agarrado" con el mouse (el que va pegado al cursor tras hacerle click -- en el inventario). Funciones nativas de Update 41+ -- documentadas en -- 05. Documentación LUA/Client/Functions.lua, no usadas por ningun otro -- script del proyecto todavia, asi que se llaman con guardas por si en esta -- version del cliente no existen o devuelven basura. -- -- Devuelve: slotInventario, itemIndex -- o nil si no hay item agarrado. -- --------------------------------------------------------------------------- -- Bloqueo del click hacia el mundo/juego. MISMO patron ya probado en -- BossDamage / InvasionNotifier / KanturuTopPVM / MasterHub de este proyecto: -- el nombre real de la funcion es DisableCLick (asi, con la L mayuscula), -- DisableClickClient es solo un alias que puede no existir. ItemFusion usaba -- unicamente el alias -- por eso el click se filtraba al juego y el cliente lo -- interpretaba como "tirar el item al piso", disparando la proteccion nativa -- "no estas habilitado para soltar un item tan costoso" (reportado 2026-08-01). local function BlockGameClick() if DisableCLick ~= nil then DisableCLick() elseif DisableClickClient ~= nil then DisableClickClient() end end -- Consume el click izquierdo para que el cliente no lo vuelva a procesar -- despues de que nosotros ya lo usamos. local function ResetMouseLeft() if ResetMouseL ~= nil then ResetMouseL() end end local function ResetMouseRight() if ResetMouseR ~= nil then ResetMouseR() end end local function GetHeldItem() if GetInventoryMouseItemIndex == nil then return nil end local idx = tonumber(GetInventoryMouseItemIndex()) if idx == nil or idx <= 0 then return nil end -- Hay 2 getters de slot documentados y no esta claro cual es cual; -- probamos el mas especifico primero y caemos al otro. local slot = nil if GetInventoryMouseItemSlot ~= nil then slot = tonumber(GetInventoryMouseItemSlot()) end if (slot == nil or slot < 0) and GetInventoryMouseSlot ~= nil then slot = tonumber(GetInventoryMouseSlot()) end if slot == nil or slot < 0 then return nil end return slot, idx end local function ReadSlotFromPacket(name) return { Index = tonumber(GetWordPacket(name, -1)) or 0, Level = tonumber(GetBytePacket(name, -1)) or 0, Option1 = tonumber(GetBytePacket(name, -1)) or 0, Option2 = tonumber(GetBytePacket(name, -1)) or 0, Option3 = tonumber(GetBytePacket(name, -1)) or 0, NewOption = tonumber(GetBytePacket(name, -1)) or 0, Socket1 = tonumber(GetBytePacket(name, -1)) or 255, Socket2 = tonumber(GetBytePacket(name, -1)) or 255, Socket3 = tonumber(GetBytePacket(name, -1)) or 255, Socket4 = tonumber(GetBytePacket(name, -1)) or 255, Socket5 = tonumber(GetBytePacket(name, -1)) or 255, SocketBonus = tonumber(GetBytePacket(name, -1)) or 255, } end local function SetClientSlot(slotCode, data) local kind, pos = SlotKey(slotCode) if kind == "A" then ItemFusion.SlotA = data elseif kind == "B" then ItemFusion.SlotB = data elseif kind == "Jewel" then ItemFusion.Jewels[pos] = data end end local function ClearClientSlot(slotCode) local kind, pos = SlotKey(slotCode) if kind == "A" then ItemFusion.SlotA = nil elseif kind == "B" then ItemFusion.SlotB = nil elseif kind == "Jewel" then ItemFusion.Jewels[pos] = nil end end function ItemFusion_Protocol(packet, packetName) if tonumber(CONFIG.Enable) ~= 1 or packetName ~= CONFIG.PacketName then return false end local action = tonumber(GetBytePacket(packetName, -1)) or 0 if action == 10 then local slotCode = tonumber(GetBytePacket(packetName, -1)) or SLOT_A local data = ReadSlotFromPacket(packetName) SetClientSlot(slotCode, data) ItemFusion.Status = nil ItemFusion.LastResult = nil ItemFusion.LastResultFrames = 0 local kind = SlotKey(slotCode) if kind == "A" or kind == "B" then ItemFusion_FxPlace(kind) end elseif action == 11 then local slotCode = tonumber(GetBytePacket(packetName, -1)) or SLOT_A ClearClientSlot(slotCode) elseif action == 12 then local ok = tonumber(GetBytePacket(packetName, -1)) or 0 local result = { Ok = ok } if ok == 1 then result.Data = ReadSlotFromPacket(packetName) else local reason = tonumber(GetBytePacket(packetName, -1)) or 4 result.Reason = reason if false then local msgs = { [0] = "No hay espacio en el inventario.", [1] = "Los items no son del mismo tipo.", [2] = "Uno de los items ya no esta ahi.", [3] = "Faltan items para fusionar.", [4] = "Error al fusionar, proba de nuevo.", [5] = "Es el mismo item en los 2 slots, usá uno diferente.", } ItemFusion.Status = msgs[reason] or msgs[4] ItemFusion.StatusColor = CONFIG.Colors.Error ItemFusion_FxFail() -- parpadeo rojo del panel end end if ok ~= 1 and result.Reason == nil then result.Reason = 4 end if ItemFusion.WaitingResult and (tonumber(ItemFusion.Processing) or 0) > 0 then ItemFusion.PendingResult = result else ItemFusion_ApplyResult(result) end end ClearPacket(packetName) return true end local itemFusionProtocolRegistered = false local function RegisterItemFusionProtocol() if itemFusionProtocolRegistered then return end if ProtocolFunctions == nil or type(ProtocolFunctions.ClientProtocol) ~= "function" then return end ProtocolFunctions.ClientProtocol(ItemFusion_Protocol) itemFusionProtocolRegistered = true end RegisterItemFusionProtocol() -- --------------------------------------------------------------------------- -- Preview local (cosmetica -- el server vuelve a calcular todo en serio) -- --------------------------------------------------------------------------- -- Cuantas opciones excelentes tiene (bits prendidos del bitmask NewOption). -- Va ACA arriba y no mas abajo porque lo usa CombineExcellent: en LUA un -- "local function" solo existe de su declaracion hacia adelante. local function CountBits(v) local n = tonumber(v) or 0 local c = 0 for _ = 1, 8 do if n % 2 == 1 then c = c + 1 end n = math.floor(n / 2) end return c end local function combineByRule(rule, a, b) a = tonumber(a) or 0 b = tonumber(b) or 0 if rule == "or" then return (a ~= 0 or b ~= 0) and 1 or 0 elseif rule == "or_bits" then local result, bit = 0, 1 for i = 1, 8 do local abit = math.floor(a / bit) % 2 local bbit = math.floor(b / bit) % 2 if abit == 1 or bbit == 1 then result = result + bit end bit = bit * 2 end return result else return math.max(a, b) end end -- --------------------------------------------------------------------------- -- Opciones excelentes con TOPE. -- -- Hay 6 opciones posibles (bits 1,2,4,8,16,32 -> full = 63) y cambian de -- significado segun el tipo de item, pero siempre son 6. -- CONFIG.MaxExcellentOptions (5 por defecto) evita que la fusion entregue -- el full exe: siempre queda una opcion por debajo. -- -- Dos reglas, en este orden: -- -- 1) NUNCA EMPEORAR. El tope limita la GANANCIA, no lo que ya entro. Si uno -- de los dos items ya venia con mas opciones que el tope, el resultado -- conserva esa cantidad. Sin esto, meter un item full a la fusion se lo -- degradaria a 5 -- o sea, el jugador pagaria por empeorar su item. -- -- 2) PRIORIDAD AL CUADRO A. Si hay que descartar, se conservan TODAS las de -- A y se rellena con las de B empezando por los bits mas ALTOS. Segun -- Data/Item/ExcellentOptionRate.txt los bits altos son los raros (en -- armas el 32 = Excellent Damage Rate tiene rate 5, contra 35 de los -- bits 1 y 2), asi que lo que se cae es la opcion mas comun de B. -- El jugador controla el resultado eligiendo que item pone primero. -- --------------------------------------------------------------------------- -- De mas valioso a menos (ver ExcellentOptionRate.txt). local EXCELLENT_BITS = { 32, 16, 8, 4, 2, 1 } local function HasBit(value, bit) return math.floor((tonumber(value) or 0) / bit) % 2 == 1 end local function CombineExcellent(a, b) a = tonumber(a) or 0 b = tonumber(b) or 0 local both = combineByRule("or_bits", a, b) -- Regla 1: piso = lo que ya traia el mejor de los dos. local floorCount = math.max(CountBits(a), CountBits(b)) local maxCount = tonumber(CONFIG.MaxExcellentOptions) or 5 if maxCount < floorCount then maxCount = floorCount end if CountBits(both) <= maxCount then return both end -- Regla 2: arranca con A completo y suma de B hasta llegar al tope. local out = a for _, bit in ipairs(EXCELLENT_BITS) do if CountBits(out) >= maxCount then break end if HasBit(b, bit) and not HasBit(out, bit) then out = out + bit end end return out end local function LocalPreview() local A, B = ItemFusion.SlotA, ItemFusion.SlotB if A == nil or B == nil or A.Index ~= B.Index then return nil end local locked = tonumber(CONFIG.LockedSocket) or 255 local maxSocket = tonumber(CONFIG.MaxSocket) or 5 local filled = {} for _, d in ipairs({ A, B }) do local s = { d.Socket1, d.Socket2, d.Socket3, d.Socket4, d.Socket5 } for _, v in ipairs(s) do if v ~= nil and v ~= locked and #filled < maxSocket then table.insert(filled, v) end end end local sockets = {} for i = 1, 5 do sockets[i] = filled[i] or locked end local rules = CONFIG.Rules or {} return { Index = A.Index, Level = combineByRule(rules.Level or "max", A.Level, B.Level), Option1 = combineByRule(rules.Luck or "or", A.Option1, B.Option1), Option2 = combineByRule(rules.Skill or "or", A.Option2, B.Option2), Option3 = combineByRule(rules.Option3 or "max", A.Option3, B.Option3), NewOption = CombineExcellent(A.NewOption, B.NewOption), Socket1 = sockets[1], Socket2 = sockets[2], Socket3 = sockets[3], Socket4 = sockets[4], Socket5 = sockets[5], SocketCount = #filled, } end -- --------------------------------------------------------------------------- -- Abrir / cerrar (mismo anclaje que Scripts/NewWindow del Louis default: -- se apoya en UIInventory para aparecer "al lado" de el). -- --------------------------------------------------------------------------- function ItemFusion.OpenWindow() if CheckWindowOpen(UITrade) == 1 then return end if CheckWindowOpen(UIWarehouse) == 1 then return end if CheckWindowOpen(UIShop) == 1 then return end if CheckWindowOpen(UIStore) == 1 then return end OpenWindow(UIInventory) ItemFusion.Open = true ItemFusion.LastResult = nil ItemFusion.LastResultFrames = 0 ItemFusion.WaitingResult = false ItemFusion.Processing = 0 ItemFusion.ProcessingTotal = 0 ItemFusion.PendingResult = nil ItemFusion.ConfirmPending = false ItemFusion.AltDown = false ItemFusion.CtrlDown = false ClearAllClientSlots() RegisterItemFusionProtocol() -- Avisar al servidor: habilita la captura por click derecho. IMPORTANTE: -- sin este aviso (o si falta el de cierre) el servidor le robaria el click -- derecho de equipar al jugador aunque la ventana este cerrada. SendAction(9) end function ItemFusion.CloseWindow() ItemFusion.Open = false ItemFusion.Armed = nil ItemFusion.ConfirmPending = false ClearAllClientSlots() -- Cortar modificadores explicitamente: si la ventana se cierra justo despues -- de un pulso, la ventana de captura del servidor seguiria viva un rato y le -- robaria un click derecho de equipar al jugador. ItemFusion.AltDown = false ItemFusion.CtrlDown = false SendAction(11) -- corta la ventana de captura de modificadores SendAction(6) -- cierra la sesion end -- --------------------------------------------------------------------------- -- Estado de Alt/Ctrl -> servidor. -- -- El click derecho sobre equipo ES el gesto de equipar: son el MISMO evento, -- no se pueden separar mirando solo el click (por eso antes se robaba los -- equipados legitimos, reportado 2026-08-01). La unica forma de desambiguar -- es un modificador: Alt + click derecho = fusion, click derecho solo = -- equipar normal. -- -- Se consulta desde el RENDER (MainInterfaceProcThread), que corre todos los -- frames. Antes se consultaba desde UpdateKeyEvent, que no garantiza correr -- siempre -- parte de por que "fallaba mucho". -- --------------------------------------------------------------------------- -- Frames que espera antes de reenviar avisos de modificadores. local MOD_RESEND_FRAMES = 10 local MOD_UI_FRAMES = 70 local altSendCooldown = 0 local altUiFrames = 0 local ctrlBlockCooldown = 0 local ctrlBlockUiFrames = 0 local function KeyPulse(key) if key == nil then return false end -- OJO: ninguna de las dos da un estado SOSTENIDO mientras mantenes la -- tecla -- las dos PULSAN (confirmado en el log del 2026-08-01: llegaban -- pares 10/11 decenas de veces por segundo). CheckPressedKey pulsa al -- apretar; CheckIsRepeatKey pulsa con el auto-repeat del teclado. Por eso -- esto se llama "pulso" y no "esta apretada": cada pulso RENUEVA una -- ventana de tiempo del lado servidor, no prende/apaga un booleano. if CheckIsRepeatKey ~= nil and (tonumber(CheckIsRepeatKey(key)) or 0) ~= 0 then return true end if CheckPressedKey ~= nil and (tonumber(CheckPressedKey(key)) or 0) ~= 0 then return true end return false end local function TouchAlt() altUiFrames = MOD_UI_FRAMES if altSendCooldown <= 0 then SendAction(10) -- renovar la ventana de captura Alt en el servidor altSendCooldown = MOD_RESEND_FRAMES end end local function TouchCtrlBlock() ctrlBlockUiFrames = MOD_UI_FRAMES if ctrlBlockCooldown <= 0 then SendAction(14) -- bloquear Ctrl + click derecho mientras ItemFusion esta abierta ctrlBlockCooldown = MOD_RESEND_FRAMES end end local function UpdateModifierState() if Keys ~= nil and KeyPulse(Keys.Alt) then TouchAlt() end if Keys ~= nil and KeyPulse(Keys.Control) then TouchCtrlBlock() end -- NO se manda "modificador soltado" por deteccion: no hay forma confiable de -- saber cuando se solto (ver arriba), y el aviso adelantado era lo que -- cortaba la captura antes de tiempo. La ventana vence sola en el -- servidor; el 11 solo se manda al cerrar la ventana. if altSendCooldown > 0 then altSendCooldown = altSendCooldown - 1 end if altUiFrames > 0 then altUiFrames = altUiFrames - 1 end if ctrlBlockCooldown > 0 then ctrlBlockCooldown = ctrlBlockCooldown - 1 end if ctrlBlockUiFrames > 0 then ctrlBlockUiFrames = ctrlBlockUiFrames - 1 end ItemFusion.AltDown = (altUiFrames > 0) ItemFusion.CtrlDown = (ctrlBlockUiFrames > 0) end local function TouchModifierKey(key) if Keys == nil or key == nil then return false end if key == Keys.Alt then TouchAlt() ItemFusion.AltDown = true return false end if key == Keys.Control then TouchCtrlBlock() ItemFusion.CtrlDown = true return true end return false end local function CtrlBlockNow() if ItemFusion.CtrlDown then return true end if Keys ~= nil and KeyPulse(Keys.Control) then TouchCtrlBlock() ItemFusion.CtrlDown = true return true end return false end -- --------------------------------------------------------------------------- -- Helpers del panel de comparacion -- --------------------------------------------------------------------------- -- OJO: todas LOCALES a proposito. Con nombres tan genericos (CountBits, -- YesNo...) como globales, se pisarian con las de cualquier otro script del -- cliente que use el mismo nombre. local function CountSockets(d) if d == nil then return 0 end local locked = tonumber(CONFIG.LockedSocket) or 255 local n = 0 for _, v in ipairs({ d.Socket1, d.Socket2, d.Socket3, d.Socket4, d.Socket5 }) do if v ~= nil and tonumber(v) ~= locked then n = n + 1 end end return n end local function Clamp(value, minValue, maxValue) value = tonumber(value) or 0 minValue = tonumber(minValue) or value maxValue = tonumber(maxValue) or value if value < minValue then return minValue end if value > maxValue then return maxValue end return value end local function LocalVipMult() local cfg = CONFIG.SuccessRate or {} local accountLevel = 0 if GetViewAccountLevel ~= nil then accountLevel = tonumber(GetViewAccountLevel()) or 0 end if cfg.VipBonusMult ~= nil and cfg.VipBonusMult[accountLevel] ~= nil then return tonumber(cfg.VipBonusMult[accountLevel]) or 1 end return 1 end -- Base del item YA penalizada, o sea de donde arranca antes de las jewels. local function LocalBaseAfterPenalty(preview) local cfg = CONFIG.SuccessRate or {} local base = tonumber(cfg.DefaultBaseRate) or 45 if preview == nil then return base end local level = math.floor(tonumber(preview.Level) or 0) if level < 0 then level = 0 elseif level > 15 then level = 15 end local levelPenalty = 0 if cfg.LevelPenalty ~= nil and cfg.LevelPenalty[level] ~= nil then levelPenalty = tonumber(cfg.LevelPenalty[level]) or 0 end local exePenalty = CountBits(preview.NewOption) * (tonumber(cfg.ExcellentPenalty) or 0) local socketPenalty = CountSockets(preview) * (tonumber(cfg.SocketPenalty) or 0) return base - levelPenalty - exePenalty - socketPenalty end -- MISMO orden que el servidor (ver GetFusionRate alla): -- base -> se restan penalidades -> se suman jewels (el VIP multiplica solo -- el bonus) -> tope. Si esto se desincroniza, la ventana muestra una -- probabilidad y el servidor entrega otra. local function LocalSuccessRate(preview) if preview == nil then return nil end local cfg = CONFIG.SuccessRate or {} if tonumber(cfg.Enable) ~= 1 then return 100 end local rate = LocalBaseAfterPenalty(preview) + LocalJewelPoints() * LocalVipMult() return Clamp(rate, tonumber(cfg.MinRate) or 0, tonumber(cfg.MaxRate) or 100) end -- Bonus de las jewels PARA MOSTRAR: se recorta a lo que realmente entra -- debajo del tope. Sin recortar aparecian cosas como "Jewels +113 pct" con -- el exito clavado en 85, que confunde en vez de informar. Asi la cuenta que -- ve el jugador siempre cierra: base penalizada + jewels = exito. local function LocalJewelBonus(preview) local cfg = CONFIG.SuccessRate or {} local bonus = LocalJewelPoints() * LocalVipMult() local room = (tonumber(cfg.MaxRate) or 100) - LocalBaseAfterPenalty(preview) if room < 0 then room = 0 end if bonus > room then bonus = room end return bonus end local function YesNo1(v) return ((tonumber(v) or 0) ~= 0) and "Si" or "No" end local function YesNo(a, b) return YesNo1(a) .. " " .. YesNo1(b) end -- --------------------------------------------------------------------------- -- Render -- --------------------------------------------------------------------------- local function DrawSlot(x, y, size, data, armed, label, frameDrawn, renderKind) local C = CONFIG.Colors local hover = MouseIn(x, y, size, size) local filled = (data ~= nil and tonumber(data.Index) ~= nil and tonumber(data.Index) > 0) local sprite = "Slot" if filled and (hover or armed) then sprite = "SlotFilledHover" elseif filled then sprite = "SlotFilled" elseif hover or armed then sprite = "SlotHover" end if frameDrawn then if hover or armed then local inset = size <= 30 and 2 or 10 Rect(x + inset, y + inset, size - inset * 2, size - inset * 2, C.SlotBgHover, 0.35) end elseif not DrawRenderedSlot(x, y, size, hover or armed) then if not DrawSprite(sprite, x, y, size, size, 1.0) then MetalBox(x, y, size, hover and C.SlotBgHover or C.SlotBg, hover or armed) end end if filled then local exc = (tonumber(data.NewOption) or 0) ~= 0 and 1 or 0 local defX, defY, defW, defH = RenderDefaults(renderKind, size) local tune = RenderEntry(renderKind) local itemX = x + Num(tune, "X", defX) local itemY = y + Num(tune, "Y", defY) local itemW = size + Num(tune, "W", defW) local itemH = size + Num(tune, "H", defH) CreateItem(itemX, itemY, itemW, itemH, data.Index, data.Level, exc, 0, 0) if hover then local name = GetCompleteNameByIndex(data.Index, data.Level, data.NewOption) DrawTooltip(x, y + size + 4, tostring(name or "?")) end elseif label ~= nil and label ~= "" then Text(x + 1, y + size / 2 - 6, label or "+", size, 3, { 0, 0, 0, 220 }, 1) Text(x, y + size / 2 - 7, label or "+", size, 3, C.TitleText, 1) end end function ItemFusion_Render() if not ItemFusion.Open then return end HotReloadConfig() -- Si el jugador cierra el inventario, esta ventana se va con el. Hay que -- avisarle al servidor (CloseWindow manda la accion 6), si no quedaria -- capturando el click derecho de equipar con la ventana ya invisible. if CheckWindowOpen(UIInventory) ~= 1 then ItemFusion.CloseWindow() return end -- Sondeo de modificadores en cada frame (este bridge si corre siempre). UpdateModifierState() -- Avanza los contadores de los efectos (una sola vez por frame). FxUpdate() local ok, err = pcall(function() local C = CONFIG.Colors local W = CONFIG.Window.W local H = CONFIG.Window.H local headerH = CONFIG.Window.HeaderH local iconSize = CONFIG.SlotIconSize local x = GetWideX() - (tonumber(CONFIG.Window.AnchorOffsetX) or 360) local y = 0 -- ---------------- Fondo / marco ---------------- local fullPanel = DrawRenderedAsset("Panel", x, y, W, H, 1.0) if not fullPanel then FramePlate(x, y, W, H) -- underlay seguro NativeMuPanel(x, y, W, H) end -- ---------------- Titulo ---------------- if not fullPanel then SetFontType(1) SetTextColor(C.TitleText[1], C.TitleText[2], C.TitleText[3], C.TitleText[4]) RenderText(x, y + 13, "FUSION DE ITEMS", W, 3) end -- ---------------- Boton cerrar ---------------- local closeCfg = LayoutEntry("Close") local closeSize = Num(closeCfg, "Size", 20) local closeX, closeY = x + Num(closeCfg, "X", W - 27), y + Num(closeCfg, "Y", 4) local closeHover = MouseIn(closeX, closeY, closeSize, closeSize) -- El resaltado del hover sale del atlas de UI: el atlas del panel no -- trae sprite de cerrar (la region que se declaraba estaba vacia). if fullPanel then if closeHover then DrawSprite("CloseHover", closeX, closeY, closeSize, closeSize, 1.0) end elseif not DrawRenderedAsset(closeHover and "CloseHover" or "Close", closeX, closeY, closeSize, closeSize, 1.0) then DrawSprite(closeHover and "CloseHover" or "Close", closeX, closeY, closeSize, closeSize, 1.0) end if closeHover then DrawTooltip(closeX - 24, closeY + 18, "Cerrar") end -- ---------------- Cuadros A / B con la runa al medio ---------------- local slotAx, slotAy, slotASize = LayoutSlot(x, y, "ItemA", 32, 52, iconSize) local slotBx, slotBy, slotBSize = LayoutSlot(x, y, "ItemB", 116, 52, iconSize) local gap = slotBx - slotAx - slotASize if not fullPanel then Text(slotAx - 6, slotAy - 17, "ITEM A", slotASize + 12, 3, C.BodyText, 0) Text(slotBx - 6, slotBy - 17, "ITEM B", slotBSize + 12, 3, C.BodyText, 0) DecorLine(slotAx - 6, slotAy - 5, slotASize + 12, C.BorderGoldLo) DecorLine(slotBx - 6, slotBy - 5, slotBSize + 12, C.BorderGoldLo) end DrawSlot(slotAx, slotAy, slotASize, ItemFusion.SlotA, false, "", fullPanel, "ItemA") DrawSlot(slotBx, slotBy, slotBSize, ItemFusion.SlotB, false, "", fullPanel, "ItemB") -- EFECTO: pulso al colocar un item (encima del cuadro ya dibujado) FxDrawPlace(slotAx, slotAy, slotASize, FX.PlaceA) FxDrawPlace(slotBx, slotBy, slotBSize, FX.PlaceB) -- Caja de jewels: 12 ranuras en 2 filas de 6. Las posiciones reales -- salen del Config (Jewel1..Jewel12); los defaults de aca son solo el -- respaldo por si falta la entrada, y reproducen el mismo reparto. local jewelSlots = {} local jewelCount = JewelSlots() local perRow = tonumber((CONFIG.Jewels or {}).Columns) or 7 local jewelBoxEmpty = true for i = 1, jewelCount do local col = (i - 1) % perRow local row = math.floor((i - 1) / perRow) local defX = 26 + col * 20 local defY = 141 + row * 20 -- paso 20 = 18 de ranura + 2 de aire local sx, sy, slotSize = LayoutSlot(x, y, "Jewel" .. tostring(i), defX, defY, 20) jewelSlots[i] = { X = sx, Y = sy, Size = slotSize, Code = SLOT_JEWEL_FIRST + i - 1 } if ItemFusion.Jewels[i] ~= nil then jewelBoxEmpty = false end end -- PLACEHOLDER de la caja vacia. Se dibuja ANTES de las ranuras (para que -- cualquier icono quede por encima) y solo mientras no haya ni una joya -- puesta: apenas entra la primera, el texto desaparece y no estorba. -- Color tenue a proposito -- es una ayuda, no un cartel. if jewelBoxEmpty then local hint = ConfigTable("JewelBoxHint") local lines = hint.Lines if type(lines) == "table" and #lines > 0 then local boxY = y + Num(hint, "Y", 138) local lineH = Num(hint, "LineHeight", 12) -- Centrado vertical dentro del interior de la caja (48 px de alto). local startY = boxY + (48 - (#lines * lineH)) / 2 for i = 1, #lines do Text(x, startY + (i - 1) * lineH, tostring(lines[i]), W, 3, C.HintText or C.DimText, 0) end end end for i = 1, jewelCount do local s = jewelSlots[i] DrawSlot(s.X, s.Y, s.Size, ItemFusion.Jewels[i], false, "", fullPanel, "Jewel" .. tostring(i)) end local plusCfg = LayoutEntry("Plus") local plusSize = Num(plusCfg, "Size", 18) local plusX = x + Num(plusCfg, "X", (slotAx - x) + slotASize + (gap - plusSize) / 2) local plusY = y + Num(plusCfg, "Y", (slotAy - y) + (slotASize - plusSize) / 2) -- EFECTO: la runa/plus late cuando los dos cuadros estan listos if ItemFusion.SlotA ~= nil and ItemFusion.SlotB ~= nil then FxDrawRunePulse(plusX, plusY, 18) end if not fullPanel then if not DrawRenderedAsset("Plus", plusX, plusY, plusSize, plusSize, 1.0) and not DrawSprite("Plus", plusX + 1, plusY + 1, plusSize - 2, plusSize - 2, 1.0) then Text(slotAx + slotASize, slotAy + 18, "+", gap, 3, C.BorderGold, 1) end end -- ---------------- Hint de Alt ---------------- -- Se ENCIENDE en dorado mientras Alt este detectado: es la señal de que -- el cliente lo lee bien (si no se ilumina, el problema es la tecla). SetFontType(1) local hintY = y + 193 if ItemFusion.AltDown then Text(x, hintY, "ALT activo", W, 3, C.TitleText, 0) end local preview = LocalPreview() local resultPreview = preview if resultPreview == nil and ItemFusion.LastResultFrames ~= nil and ItemFusion.LastResultFrames > 0 then resultPreview = ItemFusion.LastResult end local processing = ItemFusion.WaitingResult == true local minimums = LocalMinimums() local jewelsOk = LocalMinimumsOk() local jewelBonus = LocalJewelBonus(preview) local successRate = LocalSuccessRate(preview) local canFuse = preview ~= nil and jewelsOk and not processing -- ---------------- Separador ---------------- local divY = y + 202 if not fullPanel then DecorLine(x + 18, divY + 4, W - 36, C.BorderGoldLo) end -- ---------------- Resultado limpio ---------------- local panelY = divY + 14 Text(x, panelY, "RESULTADO DE LA FUSION", W, 3, C.TitleText, 1) local lineY = panelY + 18 if processing then Text(x, lineY, "Fusionando...", W, 3, C.TitleText, 0) Text(x, lineY + 14, "Espera el resultado", W, 3, C.DimText, 0) elseif preview ~= nil then -- OJO: aca el % es solo TEXTO DE PANTALLA del cliente, no pasa por -- ninguna funcion del engine, asi que es seguro. En el SERVIDOR en -- cambio un % literal en MessageSend mata el GS (ver SafeMessage). Text(x, lineY, string.format("Exito %d%% Jewels +%d%%", math.floor((successRate or 0) + 0.5), math.floor(jewelBonus + 0.5)), W, 3, C.TitleText, 0) -- Los minimos, uno al lado del otro. Cada contador se topea en lo -- que se pide (sin topear quedaba "350/90", que se lee como error); -- lo que sobra ya se ve arriba en "Jewels +X%". -- Rojo el que falta, verde el que ya esta cubierto. local minX = x local anchoCada = W / math.max(1, #minimums) for i, m in ipairs(minimums) do local got = m.Got if got > m.Need then got = m.Need end Text(minX + (i - 1) * anchoCada, lineY + 14, string.format("%s %d/%d", tostring(m.Family), math.floor(got), math.floor(m.Need)), anchoCada, 3, m.Ok and C.Success or C.Error, 0) end elseif resultPreview ~= nil then Text(x, lineY, string.format("Nivel +%d Sockets %d/5", tonumber(resultPreview.Level) or 0, tonumber(resultPreview.SocketCount) or 0), W, 3, C.BodyText, 0) Text(x, lineY + 14, string.format("Suerte %s Excelente %d", YesNo1(resultPreview.Option1), CountBits(resultPreview.NewOption)), W, 3, C.DimText, 0) else Text(x, lineY + 2, "Pone dos items iguales", W, 3, C.DimText, 0) Text(x, lineY + 17, "para ver el resultado", W, 3, C.DimText, 0) end -- ---------------- Cuadro del resultado ---------------- local resultDefaultSize = iconSize local previewX, previewY, resultSize = LayoutSlot(x, y, "Result", (W - resultDefaultSize) / 2, 286, resultDefaultSize) DrawSlot(previewX, previewY, resultSize, resultPreview, false, "", fullPanel, "Result") -- ---------------- Boton Fusionar ---------------- local btnCfg = LayoutEntry("Button") local btnW, btnH = Num(btnCfg, "W", 128), Num(btnCfg, "H", 32) local btnX = x + Num(btnCfg, "X", (W - btnW) / 2) local btnY = y + Num(btnCfg, "Y", 358) canFuse = preview ~= nil and jewelsOk and not processing local btnHover = canFuse and MouseIn(btnX, btnY, btnW, btnH) MetalButton(btnX, btnY, btnW, btnH, btnHover, not canFuse) if processing then Text(btnX, btnY + 11, "FUSIONANDO", btnW, 3, C.TitleText, 1) elseif canFuse then Text(btnX, btnY + 11, "FUSIONAR", btnW, 3, C.TitleText, 1) else Text(btnX, btnY + 11, "FUSIONAR", btnW, 3, C.DimText, 1) end local procTotal = tonumber(ItemFusion.ProcessingTotal) or FxCfg("ProcessingFrames", 120) local procLeft = tonumber(ItemFusion.Processing) or 0 local procProgress = 1 - Clamp01(procLeft / math.max(procTotal, 1)) FxDrawProcessing(previewX + resultSize / 2, previewY + resultSize / 2, resultSize, btnX, btnY + btnH + 5, btnW, 5, procProgress) -- EFECTO: fusion exitosa, centrado en el cuadro de resultado. -- Se dibuja despues del boton para que quede por encima de todo. FxDrawSuccess(previewX + resultSize / 2, previewY + resultSize / 2, resultSize) FxDrawFail(previewX, previewY, resultSize, resultSize) -- El tooltip nativo debe quedar por encima del boton y del resto del UI. if resultPreview ~= nil and MouseIn(previewX, previewY, resultSize, resultSize) then ShowDescriptionComplete( previewX, previewY + resultSize + 4, resultPreview.Index, resultPreview.Level, 255, resultPreview.Option1, resultPreview.Option2, resultPreview.Option3, resultPreview.NewOption, 0, 0, resultPreview.SocketCount, resultPreview.Socket1, resultPreview.Socket2, resultPreview.Socket3, resultPreview.Socket4, resultPreview.Socket5 ) end -- ---------------- Mensaje de estado ---------------- if ItemFusion.Status ~= nil and not processing then local col = ItemFusion.StatusColor or C.BodyText Text(x, btnY + btnH + 8, ItemFusion.Status, W, 3, col, 0) end -- EFECTO: parpadeo rojo sobre TODO el panel al fallar. Ultimo de todo. FxDrawFail(x, y, W, H) ItemFusion._layout = { WinX = x, WinY = y, WinW = W, WinH = H, CloseX = closeX, CloseY = closeY, CloseSize = closeSize, SlotAx = slotAx, SlotAy = slotAy, SlotASize = slotASize, SlotBx = slotBx, SlotBy = slotBy, SlotBSize = slotBSize, SlotY = slotAy, IconSize = iconSize, BtnX = btnX, BtnY = btnY, BtnW = btnW, BtnH = btnH, CanFuse = canFuse, JewelSlots = jewelSlots, } end) if not ok then Console(1, "[ItemFusion] Error de render: " .. tostring(err)) end end BridgeFunctionAttach("MainInterfaceProcThread", "ItemFusion_Render") -- --------------------------------------------------------------------------- -- Mouse / teclado -- --------------------------------------------------------------------------- function ItemFusion_UpdateMouse() if not ItemFusion.Open then return end local L = ItemFusion._layout if L == nil then return end -- Mientras el mouse este ENCIMA de la ventana, bloquear el click hacia el -- juego en TODO frame (no solo al soltar). Sin esto el cliente procesa el -- soltar-sobre-la-ventana como un intento de tirar el item al piso y salta -- "no estas habilitado para soltar un item tan costoso". local overWindow = MouseIn(L.WinX, L.WinY, L.WinW, L.WinH) if overWindow then BlockGameClick() end if CtrlBlockNow() and MouseRButton ~= nil and (tonumber(MouseRButton()) or 0) ~= 0 then BlockGameClick() end if CheckReleasedKey(Keys.LButton) == 1 then if MouseIn(L.CloseX, L.CloseY, L.CloseSize, L.CloseSize) then ItemFusion.CloseWindow() return end if ItemFusion.WaitingResult then ResetMouseLeft() return end -- ------------------------------------------------------------------ -- Los items se ponen con ALT + CLICK DERECHO en el inventario (lo maneja el -- servidor, ver ItemFusion_OnUserItemMove) y el cuadro se elige solo: -- primero A, despues B. Aca el click izquierdo sobre un cuadro solo -- sirve para SACAR el item que ya esta puesto. -- -- Se mantiene ademas el soltar-con-el-mouse (agarrar el item y soltarlo -- encima del cuadro) como gesto alternativo. -- ------------------------------------------------------------------ local heldSlot = GetHeldItem() if MouseIn(L.SlotAx, L.SlotAy or L.SlotY, L.SlotASize or L.IconSize, L.SlotASize or L.IconSize) then if heldSlot ~= nil then SendPlaceItem(SLOT_A, heldSlot) ResetMouseLeft() -- consumir el click, que el cliente no lo reprocese elseif ItemFusion.SlotA ~= nil then ItemFusion.SlotA = nil SendAction(3) end return end if MouseIn(L.SlotBx, L.SlotBy or L.SlotY, L.SlotBSize or L.IconSize, L.SlotBSize or L.IconSize) then if heldSlot ~= nil then SendPlaceItem(SLOT_B, heldSlot) ResetMouseLeft() -- consumir el click, que el cliente no lo reprocese elseif ItemFusion.SlotB ~= nil then ItemFusion.SlotB = nil SendAction(4) end return end -- Caja de jewels: click con una joya agarrada la pone en ESA ranura; -- click sobre una ranura ya llena la vacia. for i = 1, JewelSlots() do local slot = L.JewelSlots and L.JewelSlots[i] if slot ~= nil and MouseIn(slot.X, slot.Y, slot.Size, slot.Size) then if heldSlot ~= nil then SendPlaceItem(slot.Code, heldSlot) ResetMouseLeft() elseif ItemFusion.Jewels[i] ~= nil then ItemFusion.Jewels[i] = nil SendClearSlot(slot.Code) end return end end if L.CanFuse and MouseIn(L.BtnX, L.BtnY, L.BtnW, L.BtnH) then local frames = FxCfg("ProcessingFrames", 120) ItemFusion.WaitingResult = true ItemFusion.Processing = frames ItemFusion.ProcessingTotal = frames ItemFusion.PendingResult = nil ItemFusion.ConfirmPending = true ItemFusion.LastResult = nil ItemFusion.LastResultFrames = 0 ItemFusion.Status = "Fusionando..." ItemFusion.StatusColor = CONFIG.Colors.TitleText ResetMouseLeft() return end end end BridgeFunctionAttach("UpdateMouseEvent", "ItemFusion_UpdateMouse") function ItemFusion_RightClickEvent() if not ItemFusion.Open then return end if CtrlBlockNow() then BlockGameClick() ResetMouseRight() end end BridgeFunctionAttach("RightClickEvent", "ItemFusion_RightClickEvent") function ItemFusion_UpdateKey() if not ItemFusion.Open then return end if CheckWindowOpen(UIChatWindow) == 1 then return end -- ESC cierra la ventana. Se sondea ACA, por frame, y no solo en el -- KeyboardEvent, porque ESC es una tecla que el cliente usa para su propio -- menu y no hay garantia de que el bridge de teclado la deje pasar. Este -- camino usa CheckPressedKey, el mismo que ya funciona para Alt y Ctrl. -- -- Si el menu nativo de ESC igual se abre encima, poner CloseOnEscape = 0 -- en el Config y cerrar con la tecla de abrir (L) o con la X. if tonumber(CONFIG.CloseOnEscape) ~= 0 and Keys ~= nil and KeyPulse(Keys.Escape) then ItemFusion.CloseWindow() return end -- Evita que el click se traspase al mundo (el personaje caminando) -- mientras la ventana esta abierta -- mismo patron de Scripts/NewWindow. BlockGameClick() end BridgeFunctionAttach("UpdateKeyEvent", "ItemFusion_UpdateKey") function ItemFusion_KeyListener(key) if CheckWindowOpen(UIChatWindow) == 1 then return false end local openKeyName = tostring(CONFIG.OpenKey or "F") local openKey = Keys[openKeyName] if openKey ~= nil and key == openKey then if ItemFusion.Open then ItemFusion.CloseWindow() else ItemFusion.OpenWindow() end return true end if not ItemFusion.Open then return false end if TouchModifierKey(key) then return true end -- Segundo camino para ESC (el principal es el sondeo por frame de -- ItemFusion_UpdateKey). Se dejan los dos porque no esta garantizado que -- el cliente propague ESC a este bridge; CloseWindow es idempotente, asi -- que si llegan los dos no pasa nada. if tonumber(CONFIG.CloseOnEscape) ~= 0 and Keys.Escape ~= nil and key == Keys.Escape then ItemFusion.CloseWindow() return true end end BridgeFunctionAttach("KeyboardEvent", "ItemFusion_KeyListener") -- --------------------------------------------------------------------------- -- MASTERHUB REGISTRO -- entrada en la pestana lateral (reemplaza el comando -- /fusion, que se saco). Abre/cierra igual que la tecla L. -- --------------------------------------------------------------------------- local itemFusionHubEntry = { Id = "itemfusion", Label = "Fusion de Items", Order = 25, IsVisible = function() return tonumber(CONFIG.Enable) == 1 end, OnClick = function() if ItemFusion.Open then ItemFusion.CloseWindow() else ItemFusion.OpenWindow() end return nil end, } if MasterHub ~= nil and MasterHub.Register ~= nil then MasterHub.Register(itemFusionHubEntry) else MasterHubQueue = MasterHubQueue or {} table.insert(MasterHubQueue, itemFusionHubEntry) end