local CONFIG = require("Scripts\\InvasionNotifier\\Config") local THEME = require("Scripts\\MasterHub\\Theme") local InvasionNotifier = { Visible = false, Rows = {}, Selected = 1, Mode = "list", Tick = 0, PanelX = nil, PanelY = nil, CompactX = nil, CompactY = nil, Minimized = true, Dragging = false, DragMoved = false, DragOffsetX = 0, DragOffsetY = 0, Buttons = {}, ModelCache = {}, } BridgeFunctionAttach("MainLoader", "InvasionNotifier_Preload") BridgeFunctionAttach("MainInterfaceProcThread", "InvasionNotifier_Render") BridgeFunctionAttach("UpdateMouseEvent", "InvasionNotifier_UpdateMouse") local function clamp(value, minValue, maxValue) value = tonumber(value) or minValue if value < minValue then return minValue end if value > maxValue then return maxValue end return value end local function trimPacketString(value) value = tostring(value or "") value = string.gsub(value, "%z", "") value = string.gsub(value, "%s+$", "") return value end local function drawRect(x, y, w, h, color) if w <= 0 or h <= 0 then return end SetBlend(1) glColor4f(color[1], color[2], color[3], color[4]) DrawBar(x, y, w, h) EndDrawBar() end local function drawSteppedRect(x, y, w, h, color) drawRect(x + 3, y, w - 6, h, color) drawRect(x + 1, y + 2, w - 2, h - 4, color) drawRect(x, y + 4, w, h - 8, color) end local function isMouseOver(x, y, w, h) local mouseX = MousePosX() local mouseY = MousePosY() return mouseX >= x and mouseX <= (x + w) and mouseY >= y and mouseY <= (y + h) end local function blockGameClick() if DisableCLick ~= nil then DisableCLick() elseif DisableClickClient ~= nil then DisableClickClient() end end local function resetMouseLeft() if ResetMouseL ~= nil then ResetMouseL() end end local function usingMasterHub() return (tonumber(CONFIG.UseMasterHub) or 0) == 1 and MasterHub ~= nil end local function mapIsOpen() if CheckWindowOpen == nil then return false end local full = (UIFullMap ~= nil and CheckWindowOpen(UIFullMap)) or 0 local move = (UIMoveList ~= nil and CheckWindowOpen(UIMoveList)) or 0 return full == 1 or move == 1 end local function formatTime(seconds) seconds = math.max(0, tonumber(seconds) or 0) local minutes = math.floor(seconds / 60) local rest = seconds % 60 return string.format("%dm %02ds", minutes, rest) end local function shortText(value, maxLength) value = tostring(value or "") if string.len(value) <= maxLength then return value end return string.sub(value, 1, maxLength - 2) .. ".." end local function detailNeedsBossLine(row) return row ~= nil and row.invasionName ~= "" and row.bossName ~= "" and row.invasionName ~= row.bossName end local function getPanelSize() local panel = CONFIG.Panel or {} local width = tonumber(panel.Width) or 260 local rowHeight = tonumber(panel.RowHeight) or 22 if InvasionNotifier.Mode == "detail" then local row = InvasionNotifier.Rows[InvasionNotifier.Selected] local base = 125 if detailNeedsBossLine(row) then base = base + 15 end return width, base end return width, 36 + (#InvasionNotifier.Rows * rowHeight) + 8 end local function getPanelLayout() local panel = CONFIG.Panel or {} local width, height = getPanelSize() local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 local defaultX = tonumber(panel.PosX) or 18 local defaultY = tonumber(panel.PosY) or 116 InvasionNotifier.PanelX = clamp(tonumber(InvasionNotifier.PanelX) or defaultX, 0, math.max(0, GetWideX() - width)) InvasionNotifier.PanelY = clamp(tonumber(InvasionNotifier.PanelY) or defaultY, 0, math.max(0, screenHeight - height)) return InvasionNotifier.PanelX, InvasionNotifier.PanelY, width, height end local function getCompactLayout() local panel = CONFIG.Panel or {} local width = tonumber(panel.CompactWidth) or 42 local height = tonumber(panel.CompactHeight) or 22 local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 local defaultX = GetWideX() - width - 18 local defaultY = tonumber(panel.PosY) or 116 InvasionNotifier.CompactX = clamp(tonumber(InvasionNotifier.CompactX) or defaultX, 0, math.max(0, GetWideX() - width)) InvasionNotifier.CompactY = clamp(tonumber(InvasionNotifier.CompactY) or defaultY, 0, math.max(0, screenHeight - height)) return InvasionNotifier.CompactX, InvasionNotifier.CompactY, width, height end function InvasionNotifier.Clear() InvasionNotifier.Visible = false InvasionNotifier.Rows = {} InvasionNotifier.Selected = 1 InvasionNotifier.Mode = "list" InvasionNotifier.Tick = 0 InvasionNotifier.Dragging = false InvasionNotifier.DragMoved = false end function InvasionNotifier.ReadRows(packetName) local rows = {} local count = tonumber(GetBytePacket(packetName, -1)) or 0 for i = 1, count do local monsterIndex = tonumber(GetWordPacket(packetName, -1)) or 0 local bossClass = tonumber(GetWordPacket(packetName, -1)) or 0 local map = tonumber(GetWordPacket(packetName, -1)) or 0 local x = tonumber(GetBytePacket(packetName, -1)) or 0 local y = tonumber(GetBytePacket(packetName, -1)) or 0 local remaining = tonumber(GetWordPacket(packetName, -1)) or 0 local duration = tonumber(GetWordPacket(packetName, -1)) or 0 local wcp = tonumber(GetWordPacket(packetName, -1)) or 0 local gp = tonumber(GetWordPacket(packetName, -1)) or 0 local percent = tonumber(GetWordPacket(packetName, -1)) or 0 local attackers = tonumber(GetBytePacket(packetName, -1)) or 0 local invasionName = trimPacketString(GetCharPacketLength(packetName, -1, CONFIG.NameLength)) local bossName = trimPacketString(GetCharPacketLength(packetName, -1, CONFIG.BossLength)) local mapName = trimPacketString(GetCharPacketLength(packetName, -1, CONFIG.MapLength)) table.insert(rows, { monsterIndex = monsterIndex, bossClass = bossClass, map = map, x = x, y = y, remaining = remaining, duration = duration, wcp = wcp, gp = gp, percent = percent, attackers = attackers, invasionName = invasionName, bossName = bossName, mapName = mapName, }) end InvasionNotifier.Rows = rows InvasionNotifier.Selected = clamp(InvasionNotifier.Selected, 1, math.max(1, #rows)) InvasionNotifier.Visible = #rows > 0 InvasionNotifier.Tick = 0 if #rows == 0 then InvasionNotifier.Mode = "list" elseif InvasionNotifier.Mode == "detail" and InvasionNotifier.Rows[InvasionNotifier.Selected] == nil then InvasionNotifier.Mode = "list" end end function InvasionNotifier.Protocol(packet, packetName) if CONFIG.Enable == 0 or packetName ~= CONFIG.PacketName then return false end local action = tonumber(GetBytePacket(packetName, -1)) or 0 if action == 0 then InvasionNotifier.Clear() ClearPacket(packetName) return true end if action == 1 then InvasionNotifier.ReadRows(packetName) end ClearPacket(packetName) return true end function InvasionNotifier.Init() ProtocolFunctions.ClientProtocol(InvasionNotifier.Protocol) end function InvasionNotifier.DrawBadge() local colors = CONFIG.Colors local x, y, w, h = getCompactLayout() local hovered = isMouseOver(x, y, w, h) local border = hovered and colors.BadgeBorderHover or colors.BadgeBorder drawSteppedRect(x + 2, y + 2, w, h, colors.Shadow) drawSteppedRect(x, y, w, h, border) drawSteppedRect(x + 1, y + 1, w - 2, h - 2, colors.BadgeBack) drawRect(x + 6, y + 3, w - 12, 1, colors.BadgeShine) SetFontType(1) SetTextBg(0, 0, 0, 0) SetTextColor(210, 214, 222, 255) RenderText(x, y + 6, "Invasion Activa", w, ALIGN_CENTER) end function InvasionNotifier.DrawButton(id, text, x, y, w, h) local hovered = isMouseOver(x, y, w, h) InvasionNotifier.Buttons[id] = { x = x, y = y, w = w, h = h } drawRect(x, y, w, h, hovered and THEME.Colors.ButtonHover or THEME.Colors.Button) SetTextColor(222, 224, 230, 255) RenderText(x, y + 4, text, w, ALIGN_CENTER) end function InvasionNotifier.DrawFrame(title) local x, y, width, height = getPanelLayout() local closeX = x + width - 17 local closeY = y + 5 InvasionNotifier.Buttons = {} InvasionNotifier.Buttons.header = { x = x, y = y, w = width, h = 24 } THEME.DrawFrame(x, y, width, height, 24) SetFontType(1) SetTextBg(0, 0, 0, 0) THEME.SetTitleColor() RenderText(x, y + 7, title, width, ALIGN_CENTER) InvasionNotifier.DrawButton("close", "x", closeX, closeY, 12, 12) return x, y, width, height end function InvasionNotifier.DrawList() local panel = CONFIG.Panel or {} local rowHeight = tonumber(panel.RowHeight) or 22 local x, y, width = InvasionNotifier.DrawFrame("INVASION ACTIVA") local startY = y + 31 SetFontType(0) for i, row in ipairs(InvasionNotifier.Rows) do local rowY = startY + ((i - 1) * rowHeight) local hovered = isMouseOver(x + 7, rowY, width - 14, rowHeight - 2) local rowColor = hovered and THEME.Colors.RowHover or THEME.Colors.Row InvasionNotifier.Buttons["row" .. i] = { x = x + 7, y = rowY, w = width - 14, h = rowHeight - 2, row = i } drawRect(x + 7, rowY, width - 14, rowHeight - 2, rowColor) SetTextColor(214, 216, 222, 255) RenderText(x + 12, rowY + 4, shortText(row.bossName, 22), 118, ALIGN_LEFT) SetTextColor(150, 190, 214, 255) RenderText(x + 132, rowY + 4, shortText(row.mapName, 13), 66, ALIGN_LEFT) SetTextColor(198, 206, 214, 255) RenderText(x + width - 72, rowY + 4, formatTime(row.remaining), 58, ALIGN_RIGHT) end end function InvasionNotifier.GetBossModel(bossClass) bossClass = tonumber(bossClass) or 0 if bossClass <= 0 or LoadMonster == nil then return nil end -- Recargar en cada frame: abrir el mapa (tecla M) u otros eventos del cliente -- descargan modelos y un handle cacheado quedaria invalido (imagen en blanco). -- LoadMonster devuelve el indice y recarga si hace falta, asi que es seguro. local ok, model = pcall(LoadMonster, bossClass) if ok and model ~= nil and model ~= 0 then return model end return nil end function InvasionNotifier.DrawBossPreview(row, x, y) local preview = CONFIG.Preview or {} if preview.Enable == 0 or row == nil then return end local colors = CONFIG.Colors local width = tonumber(preview.Width) or 70 local height = tonumber(preview.Height) or 84 local bossClass = tonumber(row.bossClass) or 0 local classScale = nil if preview.ScaleByClass ~= nil then classScale = tonumber(preview.ScaleByClass[bossClass]) end local size = classScale or tonumber(preview.Size) or 0.42 local renderX = x + (width / 2) local renderY = y + height - 14 local model = InvasionNotifier.GetBossModel(row.bossClass) drawRect(x, y, width, height, colors.PreviewBack or THEME.Colors.Row) drawRect(x, y, width, 1, THEME.Colors.GoldDark) drawRect(x, y + height - 1, width, 1, THEME.Colors.GoldDark) -- Con el mapa (M) abierto el cliente libera los modelos; no renderizamos -- el modelo esos frames para no perderlo (el recuadro queda dibujado). if mapIsOpen() then return end if model == nil or model == false then SetFontType(0) SetTextColor(150, 190, 214, 210) RenderText(x, y + 35, "BOSS", width, ALIGN_CENTER) return end if tonumber(preview.UseRotation) == 1 and RenderMonsterRotation ~= nil then pcall(RenderMonsterRotation, model, renderX, renderY, size) elseif RenderMonster ~= nil then pcall(RenderMonster, model, renderX, renderY, size) elseif RenderMonsterRotation ~= nil then pcall(RenderMonsterRotation, model, renderX, renderY, size) end end function InvasionNotifier.DrawDetail() local x, y, width, height = InvasionNotifier.DrawFrame("DETALLE INVASION") local row = InvasionNotifier.Rows[InvasionNotifier.Selected] if row == nil then InvasionNotifier.Mode = "list" return end local preview = CONFIG.Preview or {} local previewWidth = tonumber(preview.Width) or 74 local tableX = x + 8 local tableWidth = width - previewWidth - 27 local lineY = y + 28 SetFontType(0) SetTextBg(0, 0, 0, 0) -- Nombre del boss (una sola vez, sin fondo) SetTextColor(222, 236, 248, 255) RenderText(tableX + 3, lineY, shortText(row.bossName, 22), tableWidth, ALIGN_LEFT) lineY = lineY + 16 -- Nombre de la invasion solo si difiere del boss (evita el duplicado) if detailNeedsBossLine(row) then SetTextColor(150, 168, 192, 255) RenderText(tableX + 3, lineY, shortText(row.invasionName, 24), tableWidth, ALIGN_LEFT) lineY = lineY + 15 end -- Datos en tabla de 2 columnas (etiqueta | valor). Filas en zebra por tono, -- sin bordes. La Vida pasa a rojo cuando baja del 25%. local percent = clamp(row.percent, 0, 100) local rowsData = { { "Mapa", string.format("%s (%d, %d)", row.mapName, row.x, row.y) }, { "Vida", string.format("%d%%", percent) }, { "Atacantes", string.format("%d", tonumber(row.attackers) or 0) }, { "Premio", string.format("%d WCP / %d GP", row.wcp, row.gp) }, { "Restante", formatTime(row.remaining) }, } local rh = 15 for i = 1, #rowsData do local ry = lineY + ((i - 1) * rh) local zebra = (i % 2 == 0) and THEME.Colors.RowHover or THEME.Colors.Row drawRect(tableX, ry, tableWidth, rh, zebra) SetTextColor(150, 168, 192, 255) RenderText(tableX + 5, ry + 2, rowsData[i][1], 70, ALIGN_LEFT) if rowsData[i][1] == "Vida" then if percent < 25 then SetTextColor(232, 96, 98, 255) else SetTextColor(120, 210, 240, 255) end else SetTextColor(200, 212, 228, 255) end RenderText(tableX, ry + 2, rowsData[i][2], tableWidth - 6, ALIGN_RIGHT) end -- El modelo 3D se dibuja al final: RenderMonster altera el estado del texto y -- ensuciaba el fondo del primer texto siguiente (el nombre del boss). InvasionNotifier.DrawBossPreview(row, x + width - previewWidth - 9, y + 28) end function InvasionNotifier_UpdateMouse() if CONFIG.Enable == 0 or not InvasionNotifier.Visible then return end local mouseX = MousePosX() local mouseY = MousePosY() local released = CheckReleasedKey(Keys.LButton) == 1 local pressed = CheckPressedKey(Keys.LButton) == 1 if InvasionNotifier.Minimized then if usingMasterHub() then return end local x, y, w, h = getCompactLayout() if InvasionNotifier.Dragging == "compact" then local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 local nextX = clamp(mouseX - InvasionNotifier.DragOffsetX, 0, math.max(0, GetWideX() - w)) local nextY = clamp(mouseY - InvasionNotifier.DragOffsetY, 0, math.max(0, screenHeight - h)) if math.abs(nextX - InvasionNotifier.CompactX) >= 2 or math.abs(nextY - InvasionNotifier.CompactY) >= 2 then InvasionNotifier.DragMoved = true end InvasionNotifier.CompactX = nextX InvasionNotifier.CompactY = nextY if released then if not InvasionNotifier.DragMoved then InvasionNotifier.Minimized = false end InvasionNotifier.Dragging = false InvasionNotifier.DragMoved = false resetMouseLeft() end return end if isMouseOver(x, y, w, h) then blockGameClick() if pressed then InvasionNotifier.Dragging = "compact" InvasionNotifier.DragOffsetX = mouseX - x InvasionNotifier.DragOffsetY = mouseY - y InvasionNotifier.DragMoved = false resetMouseLeft() end end return end local panelX, panelY, panelW, panelH = getPanelLayout() if isMouseOver(panelX, panelY, panelW, panelH) then blockGameClick() end if InvasionNotifier.Dragging == "panel" then local _, _, width, height = getPanelLayout() local screenHeight = tonumber(CONFIG.ScreenHeight) or 480 InvasionNotifier.PanelX = clamp(mouseX - InvasionNotifier.DragOffsetX, 0, math.max(0, GetWideX() - width)) InvasionNotifier.PanelY = clamp(mouseY - InvasionNotifier.DragOffsetY, 0, math.max(0, screenHeight - height)) if released then InvasionNotifier.Dragging = false resetMouseLeft() end return end if pressed and isMouseOver(panelX, panelY, panelW, 24) and not isMouseOver(panelX + panelW - 17, panelY + 5, 12, 12) then InvasionNotifier.Dragging = "panel" InvasionNotifier.DragOffsetX = mouseX - panelX InvasionNotifier.DragOffsetY = mouseY - panelY resetMouseLeft() return end if not released then return end for id, button in pairs(InvasionNotifier.Buttons) do if id ~= "header" and isMouseOver(button.x, button.y, button.w, button.h) then if id == "close" then -- La (x) actua como "volver": en el detalle regresa a la lista -- (si hay varias invasiones); si no, minimiza al hub. if InvasionNotifier.Mode == "detail" and #InvasionNotifier.Rows > 1 then InvasionNotifier.Mode = "list" else InvasionNotifier.Minimized = true end elseif button.row ~= nil then InvasionNotifier.Selected = button.row InvasionNotifier.Mode = "detail" end resetMouseLeft() return end end end function InvasionNotifier_Render() if CONFIG.Enable == 0 or not InvasionNotifier.Visible then return end InvasionNotifier.Tick = InvasionNotifier.Tick + 1 if InvasionNotifier.Tick > (tonumber(CONFIG.HideTicks) or 900) then InvasionNotifier.Clear() return end if InvasionNotifier.Minimized then if not usingMasterHub() then InvasionNotifier.DrawBadge() end elseif InvasionNotifier.Mode == "detail" then InvasionNotifier.DrawDetail() else InvasionNotifier.DrawList() end end function InvasionNotifier_Preload() if LoadMonster == nil then return end -- Precargar los modelos de los bosses de invasion conocidos al iniciar el -- cliente para que queden residentes (mitiga la perdida al abrir el mapa). local preview = CONFIG.Preview or {} for bossClass in pairs(preview.ScaleByClass or {}) do pcall(LoadMonster, bossClass) end end InvasionNotifier.Init() -- ### MASTERHUB REGISTRO ### MasterHubQueue = MasterHubQueue or {} table.insert(MasterHubQueue, { Id = "invasiones", Label = "Invasiones", Order = 10, IsVisible = function() return CONFIG.Enable == 1 end, GetCount = function() if InvasionNotifier.Visible then return #InvasionNotifier.Rows end return 0 end, OnClick = function() if InvasionNotifier.Visible and #InvasionNotifier.Rows > 0 then InvasionNotifier.Minimized = false -- Con una sola invasion activa se abre directo el detalle; -- con 2 o mas se muestra la lista para elegir. if #InvasionNotifier.Rows == 1 then InvasionNotifier.Selected = 1 InvasionNotifier.Mode = "detail" else InvasionNotifier.Mode = "list" end InvasionNotifier.Tick = 0 return nil end return "No hay invasiones activas" end, }) return InvasionNotifier