-- =========================================================================== -- InvasionMap (CLIENTE) v5 - HUD con tarjetas, avisos y posicion movible -- Una tarjeta por invasion activa, bajo los botones superiores: -- -- | Red Dragon Lorencia 4 Devias 4 Noria 3 -- | 11/15 vivos [=======] [=======] [==== ] -- -- Cada mapa lleva una barrita con la proporcion de monstruos que quedan; un mapa -- limpio se muestra en gris. Si no hay invasion activa no dibuja nada. -- F9 oculta / muestra. -- Avisos: una invasion nueva (o un mapa que se limpia) hace parpadear su tarjeta; con el HUD -- oculto aparece un aviso breve bajo los botones ("Red Dragon comenzo (F9)"). -- Mover: clic sobre una tarjeta la toma, mueve el mouse, clic otra vez la suelta; la posicion -- se guarda en InvasionMap_pos.txt. Clic derecho sobre una tarjeta vuelve a la posicion por defecto. -- -- Imagenes: copiar la carpeta ScriptImages\ (8 archivos .tga) a Data\Custom\ScriptImages\ -- -- Ubicacion: Data\Custom\Script\Scripts\InvasionMap\InvasionMap.lua -- Carga: requirefolder('Scripts\\InvasionMap') en el ScriptMain del cliente. -- Requiere: System\ScriptCore, System\ProtocolCore, Util\Defines, Util\KeysValue -- y el InvasionMap.lua v2 del servidor (paquete version 2). -- IMPORTANTE: TesteProtocol debe estar desactivado (limpia todos los paquetes). -- Solo ASCII en los textos. -- =========================================================================== local CFG = { Key = Keys.F9, -- oculta / muestra el HUD StartVisible = false, -- false = empieza oculto y F9 lo muestra; true = empieza visible PollHiddenSec = 15, -- con el HUD oculto sigue consultando (para avisar), mas espaciado AlertSec = 6, -- cuanto dura el aviso cuando el HUD esta oculto FlashSec = 4, -- cuanto parpadea una tarjeta nueva / un mapa que se limpia AllowMove = true, -- clic en una tarjeta = tomarla, clic otra vez = soltarla (se guarda) PosFile = "InvasionMap_pos.txt", -- se crea junto al juego; borrarlo = posicion por defecto ReqName = "InvMapReq", ResName = "InvMapRes", PollSec = 8, -- cada cuantos segundos pide datos Scene = 5, -- GetMainScene() == 5: dentro del juego MaxRows = 60, -- Posicion: centrado en pantalla, bajo los botones. Ajusta si no calza. OffsetX = 0, -- + derecha / - izquierda Y = 40, -- borde superior de la primera tarjeta CardGap = 4, -- espacio entre tarjetas -- true = tarjetas con imagenes (copiar ScriptImages\*.tga al cliente). -- false = solo texto sobre fondo oscuro (como la version anterior). UseImages = true, BgAlpha = 150, -- fondo del texto cuando UseImages = false -- Medidas de la tarjeta (unidades del cliente) NameW = 76, -- columna con nombre de la invasion y total ChipW = 60, -- ancho de cada mapa ChipsPerRow = 4, -- mapas por fila antes de bajar a otra Scale = 0.85, -- tamano del HUD: 1.0 = original, menor = mas chico (0.7 a 1.0) } local SC = math.max(0.6, math.min(1.2, CFG.Scale or 1.0)) local OX, OY = 0, 0 -- ancla del HUD (centro arriba): todo se escala desde ahi local function CS(n) return math.floor(n * SC + 0.5) end -- letras por nombre (el texto no se achica) -- Ids de las imagenes propias (Data\Custom\ScriptImages\). 40000/40001 ya los usa NewWindow. local IMG = { CardL = 40100, CardM = 40101, CardR = 40102, Accent = 40103, BarBg = 40104, BarFill = 40105, BarGray = 40106, } local IMG_FILES = { [IMG.CardL] = "inv_card_l", [IMG.CardM] = "inv_card_m", [IMG.CardR] = "inv_card_r", [IMG.Accent] = "inv_accent", [IMG.BarBg] = "inv_bar_bg", [IMG.BarFill] = "inv_bar_fill", [IMG.BarGray] = "inv_bar_gray", } local PROTO_VERSION = 2 local InvNames = { [0] = "Skeleton King", [1] = "Red Dragon", [2] = "Golden Dragon", [3] = "White Wizard", [4] = "New Year", [5] = "Rabbits", [6] = "Summer", [12] = "Goblins", [14] = "Mini Bosses", } -- Nombre corto del monstruo, usado cuando cada jefe esta en su propio mapa (ej. Mini Bosses). local ClassShort = { [25] = "Ice Queen", [18] = "Gorgon", [67] = "M.Balrog", [49] = "Hydra", [59] = "Zaikan", } local MapNames = { [0] = "Lorencia", [1] = "Dungeon", [2] = "Devias", [3] = "Noria", [4] = "L.Tower", [5] = "Exile", [7] = "Atlans", [8] = "Tarkan", [10] = "Icarus", [30] = "Valley of Loren", [33] = "Aida", [34] = "Crywolf", [37] = "Kanturu", [38] = "Relic", [51] = "Elbeland", [56] = "Swamp", [57] = "Raklion", [62] = "Santa Village", [63] = "Vulcanus", [64] = "Duel Arena", [79] = "Loren Market", [80] = "Karutan", } local State = { visible = CFG.StartVisible, cards = {}, -- { name=, nameCut=, totalText=, alive=, total=, maps={ {name=,nameCut=,countText=,alive=,total=} } } lastReq = 0, hasData = false, imgLoaded = false, dx = 0, dy = 0, -- desplazamiento elegido por el jugador moving = false, grabX = 0, grabY = 0, rects = {}, -- rectangulos de las tarjetas del ultimo dibujo (para clics) baseline = false, -- ya hubo una primera lectura (para no avisar lo que ya estaba) prev = {}, -- [invasion] = { [mapa] = vivos } de la lectura anterior flash = {}, -- [clave] = os.clock() hasta el que parpadea toasts = {}, -- { text=, untilT= } } BridgeFunctionAttach('KeyboardEvent', 'InvMap_Key') BridgeFunctionAttach('MainInterfaceProcThread', 'InvMap_Draw') BridgeFunctionAttach('MainLoader', 'InvMap_LoadImages') BridgeFunctionAttach('UpdateMouseEvent', 'InvMap_Mouse') BridgeFunctionAttach('UpdateKeyEvent', 'InvMap_UpdateKey') -- --------------------------------------------------------------------------- -- Posicion guardada -- --------------------------------------------------------------------------- local function LoadPos() local ok, f = pcall(io.open, CFG.PosFile, "r") if ok and f then local a, b = f:read("*l"), f:read("*l") f:close() State.dx = tonumber(a) or 0 State.dy = tonumber(b) or 0 end end local function SavePos() local ok, f = pcall(io.open, CFG.PosFile, "w") if ok and f then f:write(tostring(State.dx), "\n", tostring(State.dy), "\n") f:close() end end LoadPos() -- --------------------------------------------------------------------------- -- Lectura -- --------------------------------------------------------------------------- local function ReadByte(name, idx) local v = tonumber(GetBytePacket(name, idx)) or 0 return math.floor(v) & 0xFF end local function MapName(id) return MapNames[id] or string.format("Mapa %d", id) end local function Cut(str, n) str = tostring(str) if #str > n then return string.sub(str, 1, n) end return str end -- Convierte la respuesta en tarjetas listas para dibujar (textos ya preparados: -- el dibujo se repite cada frame y no debe armar cadenas). local function ParseResponse(name) local version = ReadByte(name, 0) local nInv = ReadByte(name, 1) if version ~= PROTO_VERSION or nInv > 20 then Console(2, string.format("[InvasionMap] formato inesperado: version=%d invasiones=%d (el servidor debe ser v2)", version, nInv)) return nil end local cards, idx, total = {}, 2, 0 for i = 1, nInv do local id = ReadByte(name, idx) local n = ReadByte(name, idx + 3) -- (idx+1, idx+2 = tiempo restante, no se usa) idx = idx + 4 total = total + n if total > CFG.MaxRows then return nil end -- Filas recibidas local raw = {} for k = 1, n do raw[k] = { class = ReadByte(name, idx) | (ReadByte(name, idx + 1) << 8), map = ReadByte(name, idx + 2), alive = ReadByte(name, idx + 3), total = ReadByte(name, idx + 4), } idx = idx + 5 end -- Si cada jefe esta en su propio mapa (clases y mapas todos distintos), se muestra -- por jefe: "Ice Queen / Devias". Si no, se suma por mapa. local perBoss = (n > 1) local seenC, seenM = {}, {} for _, r in ipairs(raw) do if seenC[r.class] or seenM[r.map] then perBoss = false end seenC[r.class] = true; seenM[r.map] = true end local order = {} if perBoss then for _, r in ipairs(raw) do order[#order + 1] = { name = ClassShort[r.class] or ("Monstruo " .. r.class), sub = MapName(r.map), alive = r.alive, total = r.total, } end else local byMap = {} for _, r in ipairs(raw) do local m = byMap[r.map] if m == nil then m = { name = MapName(r.map), alive = 0, total = 0 } byMap[r.map] = m order[#order + 1] = m end m.alive = m.alive + r.alive m.total = m.total + r.total end end local card = { name = InvNames[id] or ("Invasion " .. id), alive = 0, total = 0, maps = order, twoLine = perBoss } for _, m in ipairs(order) do card.alive = card.alive + m.alive card.total = card.total + m.total m.nameCut = Cut(m.name, CS(9)) m.subCut = m.sub and Cut(m.sub, CS(10)) or nil m.countText = tostring(m.alive) end card.nameCut = Cut(card.name, CS(13)) card.totalText = string.format("%d/%d vivos", card.alive, card.total) cards[#cards + 1] = card end return cards end -- Compara con la lectura anterior y programa parpadeos / avisos. local function DetectEvents(cards) local now = os.clock() local nowT = os.time() local seen, newPrev = {}, {} for _, c in ipairs(cards) do seen[c.name] = true local maps = {} for _, m in ipairs(c.maps) do maps[m.name] = m.alive end newPrev[c.name] = maps if State.baseline then local old = State.prev[c.name] if old == nil then State.flash["card:" .. c.name] = now + CFG.FlashSec State.toasts[#State.toasts + 1] = { text = c.name .. " comenzo", untilT = nowT + CFG.AlertSec } else for _, m in ipairs(c.maps) do if (old[m.name] or 0) > 0 and m.alive == 0 then State.flash["map:" .. c.name .. ":" .. m.name] = now + CFG.FlashSec end end end end end if State.baseline then for name in pairs(State.prev) do if not seen[name] then State.toasts[#State.toasts + 1] = { text = name .. " finalizo", untilT = nowT + CFG.AlertSec } end end end State.prev = newPrev State.baseline = true while #State.toasts > 3 do table.remove(State.toasts, 1) end end function InvMap_OnPacket(Packet, PacketName) if PacketName ~= CFG.ResName then return false end local ok, result = pcall(ParseResponse, PacketName) if ok and result then State.cards = result State.hasData = true local ok2, err = pcall(DetectEvents, result) if not ok2 then Console(2, "[InvasionMap] error en avisos: " .. tostring(err)) end elseif not ok then Console(2, "[InvasionMap] error leyendo respuesta: " .. tostring(result)) end ClearPacket(PacketName) return true end ProtocolFunctions.ClientProtocol(InvMap_OnPacket) -- --------------------------------------------------------------------------- -- Pedido y teclado -- --------------------------------------------------------------------------- local function Request() if GetMainScene() ~= CFG.Scene then return end local now = os.time() local gap = State.visible and CFG.PollSec or CFG.PollHiddenSec if (now - State.lastReq) < gap then return end State.lastReq = now CreatePacket(CFG.ReqName, 1) SetBytePacket(CFG.ReqName, PROTO_VERSION) SendPacket(CFG.ReqName) ClearPacket(CFG.ReqName) end function InvMap_Key(key) if CheckWindowOpen(UIChatWindow) == 1 then return end if key == CFG.Key then State.visible = not State.visible State.moving = false State.lastReq = 0 -- al mostrarlo, refresca de inmediato end end -- --------------------------------------------------------------------------- -- Imagenes -- --------------------------------------------------------------------------- local function LoadImagesOnce() if State.imgLoaded or not CFG.UseImages then return end State.imgLoaded = true for id, file in pairs(IMG_FILES) do LoadBitmap("Custom\\ScriptImages\\" .. file .. ".tga", id) end end function InvMap_LoadImages() LoadImagesOnce() end -- --------------------------------------------------------------------------- -- Dibujo -- --------------------------------------------------------------------------- local function Img(id, x, y, w, h) RenderImage2(id, math.floor(OX + (x - OX) * SC + 0.5), math.floor(OY + (y - OY) * SC + 0.5), math.floor(w * SC + 0.5), math.floor(h * SC + 0.5), 0.0, 0.0, 1.0, 1.0, 1, 1, 1.0) end local function Txt(x, y, str, w, align) RenderText5(math.floor(OX + (x - OX) * SC + 0.5), math.floor(OY + (y - OY) * SC + 0.5), str, math.floor(w * SC + 0.5), align) end local function CardSize(card) local nChips = #card.maps local perRow = math.min(nChips, CFG.ChipsPerRow) if perRow < 1 then perRow = 1 end local rows = math.ceil(nChips / CFG.ChipsPerRow) if rows < 1 then rows = 1 end local w = 14 + CFG.NameW + perRow * (CFG.ChipW + 3) + 4 local rowH = card.twoLine and 27 or 18 local h = 8 + rows * rowH + 2 if h < 28 then h = 28 end return w, h, rows end local function Frame(x, y, w, h) if CFG.UseImages then Img(IMG.CardL, x, y, 16, h) Img(IMG.CardM, x + 16, y, w - 32, h) Img(IMG.CardR, x + w - 16, y, 16, h) SetTextBg(0, 0, 0, 0) else SetTextBg(0, 0, 0, CFG.BgAlpha) end end local function DrawCard(card, x, y, clock) local w, h = CardSize(card) Frame(x, y, w, h) if CFG.UseImages then Img(IMG.Accent, x + 5, y + 6, 3, h - 12) end -- Parpadeo de invasion nueva (4 veces por segundo) local blink = false local fl = State.flash["card:" .. card.name] if fl and clock < fl then blink = (math.floor(clock * 4) % 2 == 0) end if blink then SetTextColor(255, 255, 255, 255) else SetTextColor(255, 215, 0, 255) end Txt(x + 12, y + 5, card.nameCut, CFG.NameW, ALIGN_LEFT) if fl and clock < fl then SetTextColor(120, 255, 140, 255) Txt(x + 12, y + 17, "NUEVA", CFG.NameW, ALIGN_LEFT) else SetTextColor(190, 190, 190, 255) Txt(x + 12, y + 17, card.totalText, CFG.NameW, ALIGN_LEFT) end local x0 = x + 14 + CFG.NameW for i, m in ipairs(card.maps) do local col = (i - 1) % CFG.ChipsPerRow local row = (i - 1) // CFG.ChipsPerRow local cx = x0 + col * (CFG.ChipW + 3) local rowH = card.twoLine and 27 or 18 local cy = y + 5 + row * rowH local cleared = (m.alive == 0) local mf = State.flash["map:" .. card.name .. ":" .. m.name] local mblink = mf and clock < mf and (math.floor(clock * 4) % 2 == 0) if mblink then SetTextColor(255, 90, 90, 255) elseif cleared then SetTextColor(135, 135, 142, 255) else SetTextColor(235, 235, 235, 255) end Txt(cx, cy, m.nameCut, CFG.ChipW - 16, ALIGN_LEFT) local barY = cy + 11 if m.subCut then SetTextColor(150, 150, 160, 255) Txt(cx, cy + 10, m.subCut, CFG.ChipW - 4, ALIGN_LEFT) barY = cy + 21 end if mblink then SetTextColor(255, 90, 90, 255) elseif cleared then SetTextColor(135, 135, 142, 255) else SetTextColor(235, 235, 235, 255) end if cleared then SetTextColor(135, 135, 142, 255) else SetTextColor(120, 255, 140, 255) end Txt(cx + CFG.ChipW - 20, cy, mblink and "OK" or m.countText, 18, ALIGN_RIGHT) if CFG.UseImages then local bw = CFG.ChipW - 4 Img(IMG.BarBg, cx, barY, bw, 3) if m.total > 0 and m.alive > 0 then Img(IMG.BarFill, cx, barY, math.max(2, math.floor(bw * m.alive / m.total)), 3) end end end return w, h end -- Aviso corto cuando el HUD esta oculto. local function DrawToasts(cx, y) local nowT = os.time() local n = 0 for i = #State.toasts, 1, -1 do if State.toasts[i].untilT <= nowT then table.remove(State.toasts, i) end end for _, t in ipairs(State.toasts) do local w, h = 200, 20 local x = cx - w // 2 Frame(x, y, w, h) SetTextColor(255, 215, 0, 255) Txt(x, y + 5, t.text .. " (F9)", w, ALIGN_CENTER) y = y + h + CFG.CardGap n = n + 1 end return n end local function Clamp(v, lo, hi) if v < lo then return lo end if v > hi then return hi end return v end function InvMap_Draw() if GetMainScene() ~= CFG.Scene then return end LoadImagesOnce() -- por si MainLoader no ejecuto este script Request() -- oculto tambien consulta (mas espaciado) para poder avisar SetFontType(1) local baseX = (GetWideX() // 2) + CFG.OffsetX local clock = os.clock() OX, OY = baseX + State.dx, CFG.Y + State.dy if not State.visible then State.rects = {} DrawToasts(baseX + State.dx, CFG.Y + State.dy) return end -- Tarjeta tomada: sigue al mouse if State.moving then State.dx = MousePosX() - State.grabX State.dy = MousePosY() - State.grabY State.dx = Clamp(State.dx, -(GetWideX() // 2), GetWideX() // 2) State.dy = Clamp(State.dy, -CFG.Y, 380) end if #State.cards == 0 then State.rects = {} -- Sin invasiones: un cartel breve para que se note que F9 funciona local w, h = 200, 20 local x = baseX + State.dx - w // 2 Frame(x, CFG.Y + State.dy, w, h) SetTextColor(190, 190, 190, 255) Txt(x, CFG.Y + State.dy + 5, State.hasData and "Sin invasiones activas" or "Consultando al servidor...", w, ALIGN_CENTER) return end local cx = baseX + State.dx local y = CFG.Y + State.dy local rects = {} for _, card in ipairs(State.cards) do local w0 = CardSize(card) local w, h = DrawCard(card, cx - w0 // 2, y, clock) rects[#rects + 1] = { x = OX + (cx - w0 // 2 - OX) * SC, y = OY + (y - OY) * SC, w = w * SC, h = h * SC } -- en pantalla y = y + h + CFG.CardGap end State.rects = rects -- Ayuda al pasar el mouse if CFG.AllowMove then local mx, my = MousePosX(), MousePosY() for _, r in ipairs(rects) do if mx >= r.x and mx <= r.x + r.w and my >= r.y and my <= r.y + r.h then DrawTooltip(mx + 12, my + 14, State.moving and "Clic: soltar aqui" or "Clic: mover | Clic derecho: restablecer") break end end end end -- --------------------------------------------------------------------------- -- Mouse -- --------------------------------------------------------------------------- local function OverCard(mx, my) for _, r in ipairs(State.rects) do if mx >= r.x and mx <= r.x + r.w and my >= r.y and my <= r.y + r.h then return r end end return nil end function InvMap_Mouse() if not State.visible or not CFG.AllowMove then return end local mx, my = MousePosX(), MousePosY() if CheckReleasedKey(Keys.LButton) == 1 then if State.moving then State.moving = false SavePos() elseif OverCard(mx, my) then State.moving = true State.grabX = mx - State.dx State.grabY = my - State.dy end elseif CheckReleasedKey(Keys.RButton) == 1 and OverCard(mx, my) then State.moving = false State.dx, State.dy = 0, 0 SavePos() end end -- Mientras el mouse esta sobre una tarjeta (o se esta moviendo) el clic no mueve al personaje. function InvMap_UpdateKey() if not State.visible or not CFG.AllowMove then return end if CheckWindowOpen(UIChatWindow) == 1 then return end if State.moving or OverCard(MousePosX(), MousePosY()) then DisableClickClient() end end Console(1, "[InvasionMap] v5 cargado - F9 oculta/muestra el HUD")