-- =========================================================================== -- InvasionTop (CLIENTE) v3 - Ranking de invasiones, diseno con imagenes -- Tecla F4: panel con tres pestanas -- Hoy -> tus kills y puntos de hoy por monstruo -- Kills -> top semanal por cantidad de kills (quien mas participa) -- Puntos -> top semanal por puntos (quien mata lo mas dificil) -- Semana: lunes a domingo. Todo llega del GameServer por paquete (sin web). -- -- Mover: clic sobre la barra del titulo la toma, mueve el mouse, clic otra vez la suelta -- (se guarda en InvasionTop_pos.txt). Clic derecho sobre el titulo vuelve a la posicion original. -- -- Imagenes: copiar los 21 archivos .ozt de ScriptImagesOZT\ a Data\Custom\ScriptImages\ -- Ubicacion: Data\Custom\Script\Scripts\InvasionTop\InvasionTop.lua -- Requiere: InvasionRank.lua v2 en el servidor, System\ScriptCore, System\ProtocolCore. -- Solo ASCII en los textos. -- =========================================================================== local CFG = { Key = Keys.F4, Scene = 5, Title = "RANKING DE INVASIONES", PointsLabel = "Pts", ReqName = "InvTopReq", ResName = "InvTopRes", MinIntervalSec = 3, -- minimo entre dos pedidos AutoSec = 20, -- con el panel abierto se refresca solo cada tanto TimeoutSec = 5, MaxRows = 10, AnimSec = 0.5, -- las barras "crecen" al abrir / cambiar de pestana AllowMove = true, PosFile = "InvasionTop_pos.txt", Logo = true, -- logo del servidor en la cabecera (necesita rk_logo.ozt) MarginRight = 12, -- posicion original: pegado a la derecha... Y = 70, -- ...y a esta altura Scale = 0.85, -- tamano del panel: 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 -- origen del panel: todo se escala desde su esquina local LOGO_H = 58 -- alto extra de la cabecera cuando hay logo local W = 220 -- tamano del panel (unidades del cliente) local H = 326 + (CFG.Logo and LOGO_H or 0) -- Ids de imagen (40100..40106 son de InvasionMap, 40000/40001 de NewWindow) local IMG = { tl = 40110, tm = 40111, tr = 40112, ml = 40113, mm = 40114, mr = 40115, bl = 40116, bm = 40117, br = 40118, head = 40119, line = 40120, tabOn = 40121, tabOff = 40122, barGold = 40123, barGreen = 40124, rowAlt = 40125, strip = 40126, m1 = 40127, m2 = 40128, m3 = 40129, badge = 40130, logo = 40131, } local IMG_FILES = { [IMG.tl] = "rk_tl", [IMG.tm] = "rk_tm", [IMG.tr] = "rk_tr", [IMG.ml] = "rk_ml", [IMG.mm] = "rk_mm", [IMG.mr] = "rk_mr", [IMG.bl] = "rk_bl", [IMG.bm] = "rk_bm", [IMG.br] = "rk_br", [IMG.head] = "rk_head", [IMG.line] = "rk_line", [IMG.tabOn] = "rk_tab_on", [IMG.tabOff] = "rk_tab_off", [IMG.barGold] = "rk_bar_gold", [IMG.barGreen] = "rk_bar_green", [IMG.rowAlt] = "rk_row_alt", [IMG.strip] = "rk_strip", [IMG.m1] = "rk_m1", [IMG.m2] = "rk_m2", [IMG.m3] = "rk_m3", [IMG.badge] = "rk_badge", [IMG.logo] = "rk_logo", } local PROTO_VERSION = 2 local MonsterNames = { [18] = "Gorgon", [25] = "Ice Queen", [42] = "Red Dragon", [43] = "G. Budge", [49] = "Hydra", [53] = "G. Titan", [54] = "G. Soldier", [55] = "Death King", [59] = "Zaikan", [67] = "Metal Balrog", [78] = "G. Goblin", [80] = "G. Lizard", [81] = "G. Vepar", [83] = "G. Wheel", [135] = "White Wizard", [365] = "Pouch", [413] = "Lunar Rabbit", [463] = "Flame Ghost", [493] = "G. D.Knight", [494] = "G. Devil", [495] = "G. Golem", [496] = "G. Crust", [497] = "G. Satyros", [498] = "G. Twin Tail", [499] = "G. Iron Knight", [792] = "Green Goblin", [793] = "Blue Goblin", [794] = "Red Goblin", [795] = "Yellow Goblin", } local State = { open = false, tab = 1, data = nil, waiting = false, failed = false, sentAt = 0, lastReq = 0, lastOk = 0, animT0 = 0, imgLoaded = false, dx = 0, dy = 0, moving = false, grabX = 0, grabY = 0, px = 0, py = 0, -- esquina del panel en el ultimo dibujo } local Hit = { win = {}, head = {}, close = {}, refresh = {}, tab = { {}, {}, {} }, } BridgeFunctionAttach('KeyboardEvent', 'InvTop_Key') BridgeFunctionAttach('MainInterfaceProcThread', 'InvTop_Draw') BridgeFunctionAttach('MainLoader', 'InvTop_LoadImages') BridgeFunctionAttach('UpdateMouseEvent', 'InvTop_Mouse') BridgeFunctionAttach('UpdateKeyEvent', 'InvTop_UpdateKey') -- --------------------------------------------------------------------------- -- Utilidades -- --------------------------------------------------------------------------- local function MyName() local ok, n = pcall(UserGetName) if ok and type(n) == "string" then return n end return "" end -- El mouse viene en pantalla; los rectangulos estan en medidas de diseno (antes de escalar). local function Inside(r, mx, my) if r.x == nil then return false end mx, my = OX + (mx - OX) / SC, OY + (my - OY) / SC return mx >= r.x and mx <= (r.x + r.w) and my >= r.y and my <= (r.y + r.h) end local function Set(r, x, y, w, h) r.x, r.y, r.w, r.h = x, y, w, h end local function Clamp(v, lo, hi) if v < lo then return lo end if v > hi then return hi end return v end local function Cut(s, n) s = tostring(s) if #s > n then return string.sub(s, 1, n) end return s end local function Fmt(n) -- 12345 -> 12,345 local s = tostring(math.floor(n or 0)) local out, c = "", 0 for i = #s, 1, -1 do out = string.sub(s, i, i) .. out c = c + 1 if c % 3 == 0 and i > 1 then out = "," .. out end end return out end 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, State.dy = tonumber(a) or 0, 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() -- --------------------------------------------------------------------------- -- Paquete -- --------------------------------------------------------------------------- local function RB(name, idx) local v = tonumber(GetBytePacket(name, idx)) or 0 return math.floor(v) & 0xFF end local function Parse(name) local idx = 0 local function u8() local v = RB(name, idx); idx = idx + 1; return v end local function u16() local a = u8(); return a | (u8() << 8) end local function u32() local a = u16(); return a | (u16() << 16) end local ver = u8() if ver ~= PROTO_VERSION then Console(2, string.format("[InvasionTop] version inesperada %d (el servidor debe ser InvasionRank v2)", ver)) return nil end local d = { today = {}, byKills = {}, byPoints = {} } local y, m, dd = u8(), u8(), u8() -- Lunes a domingo local t0 = os.time({ year = 2000 + y, month = m, day = dd, hour = 12 }) local e = os.date("*t", t0 + 6 * 86400) d.week = string.format("Semana %02d/%02d - %02d/%02d", dd, m, e.day, e.month) d.rankK, d.rankP = u16(), u16() d.wkP, d.wkK, d.tdP, d.tdK = u32(), u32(), u32(), u32() local nT = u8() if nT > 30 then return nil end for i = 1, nT do local cl, k, p = u16(), u16(), u32() d.today[#d.today + 1] = { name = MonsterNames[cl] or ("Monstruo " .. cl), kills = k, points = p } end local function list(dst) local n = u8() if n > 30 then return false end for i = 1, n do local len = u8() if len > 10 then return false end local chars = {} for c = 1, len do chars[c] = string.char(u8()) end local p, k = u32(), u32() dst[#dst + 1] = { name = table.concat(chars), points = p, kills = k } end return true end if not list(d.byKills) then return nil end if not list(d.byPoints) then return nil end return d end function InvTop_OnPacket(Packet, PacketName) if PacketName ~= CFG.ResName then return false end local ok, res = pcall(Parse, PacketName) if ok and res then State.data, State.failed, State.lastOk = res, false, os.time() State.animT0 = os.clock() else if not ok then Console(2, "[InvasionTop] error leyendo respuesta: " .. tostring(res)) end State.failed = true end State.waiting = false ClearPacket(PacketName) return true end ProtocolFunctions.ClientProtocol(InvTop_OnPacket) local function Request() if GetMainScene() ~= CFG.Scene then return end local now = os.time() if (now - State.lastReq) < CFG.MinIntervalSec then return end State.lastReq, State.sentAt, State.waiting = now, now, true CreatePacket(CFG.ReqName, 1) SetBytePacket(CFG.ReqName, PROTO_VERSION) SendPacket(CFG.ReqName) ClearPacket(CFG.ReqName) end local function OpenWin() if GetMainScene() ~= CFG.Scene then return end State.open = true State.animT0 = os.clock() Request() end local function CloseWin() State.open = false if State.moving then State.moving = false; SavePos() end end -- --------------------------------------------------------------------------- -- Imagenes -- --------------------------------------------------------------------------- local function LoadImagesOnce() if State.imgLoaded then return end State.imgLoaded = true for id, file in pairs(IMG_FILES) do LoadBitmap("Custom\\ScriptImages\\" .. file .. ".tga", id) end end function InvTop_LoadImages() LoadImagesOnce() end 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 Frame(x, y, w, h) local c = 8 Img(IMG.tl, x, y, c, c); Img(IMG.tm, x + c, y, w - 2 * c, c); Img(IMG.tr, x + w - c, y, c, c) Img(IMG.ml, x, y + c, c, h - 2 * c); Img(IMG.mm, x + c, y + c, w - 2 * c, h - 2 * c); Img(IMG.mr, x + w - c, y + c, c, h - 2 * c) Img(IMG.bl, x, y + h - c, c, c); Img(IMG.bm, x + c, y + h - c, w - 2 * c, c); Img(IMG.br, x + w - c, y + h - c, c, c) end -- --------------------------------------------------------------------------- -- Entrada -- --------------------------------------------------------------------------- function InvTop_Key(key) if CheckWindowOpen(UIChatWindow) == 1 then return end if key == CFG.Key then if State.open then CloseWin() else OpenWin() end return end if State.open and key == Keys.Escape then CloseWin() end end function InvTop_Mouse() if not State.open then return end -- Panel tomado: cualquier clic lo suelta if State.moving then if CheckReleasedKey(Keys.LButton) == 1 then State.moving = false; SavePos() end return end local mx, my = MousePosX(), MousePosY() if CheckReleasedKey(Keys.RButton) == 1 and CFG.AllowMove and Inside(Hit.head, mx, my) then State.dx, State.dy = 0, 0 SavePos() return end if CheckReleasedKey(Keys.LButton) ~= 1 then return end if Inside(Hit.close, mx, my) then CloseWin() elseif Inside(Hit.tab[1], mx, my) then State.tab = 1; State.animT0 = os.clock() elseif Inside(Hit.tab[2], mx, my) then State.tab = 2; State.animT0 = os.clock() elseif Inside(Hit.tab[3], mx, my) then State.tab = 3; State.animT0 = os.clock() elseif Inside(Hit.refresh, mx, my) then Request() elseif CFG.AllowMove and Inside(Hit.head, mx, my) then State.moving = true State.grabX, State.grabY = mx - State.dx, my - State.dy end end function InvTop_UpdateKey() if not State.open then return end if CheckWindowOpen(UIChatWindow) == 1 then return end if State.moving or Inside(Hit.win, MousePosX(), MousePosY()) then DisableClickClient() end end -- --------------------------------------------------------------------------- -- Dibujo -- --------------------------------------------------------------------------- local function Ease() -- 0..1 con frenado suave local t = (os.clock() - State.animT0) / CFG.AnimSec if t >= 1 then return 1 end if t <= 0 then return 0 end return 1 - (1 - t) * (1 - t) end local function Text(x, y, s, w, align, r, g, b, a) SetTextColor(r, g, b, a or 255) RenderText5(math.floor(OX + (x - OX) * SC + 0.5), math.floor(OY + (y - OY) * SC + 0.5), s, math.floor(w * SC + 0.5), align) end -- Fila de lista con barra proporcional. rank = nil en la pestana "Hoy". local function DrawRow(x, y, i, rank, label, valueText, frac, isMe, v2text) if i % 2 == 0 then Img(IMG.rowAlt, x + 8, y - 1, W - 16, 16) end if frac > 0 then Img(isMe and IMG.barGreen or IMG.barGold, x + 8, y - 1, math.max(2, math.floor((W - 16) * frac)), 16) end local nameX, nameW = x + 14, 80 if rank then local mid = rank == 1 and IMG.m1 or rank == 2 and IMG.m2 or rank == 3 and IMG.m3 or IMG.badge Img(mid, x + 12, y - 2, 18, 18) if rank <= 3 then Text(x + 12, y + 2, tostring(rank), 18, ALIGN_CENTER, 40, 26, 0) else Text(x + 12, y + 2, tostring(rank), 18, ALIGN_CENTER, 200, 200, 210) end nameX, nameW = x + 36, 84 end local r, g, b = 235, 235, 240 if isMe then r, g, b = 130, 255, 150 elseif rank == 1 then r, g, b = 255, 215, 90 end Text(nameX, y + 2, Cut(label, math.floor(13 * SC + 0.5)), nameW, ALIGN_LEFT, r, g, b) if v2text then Text(x + 92, y + 2, v2text, 62, ALIGN_RIGHT, 190, 190, 200) Text(x + 154, y + 2, valueText, 54, ALIGN_RIGHT, 255, 215, 90) else Text(x + 120, y + 2, valueText, 90, ALIGN_RIGHT, 255, 215, 90) end end function InvTop_Draw() if not State.open then return end if CheckWindowOpen(UIInventory) == 1 or CheckWindowOpen(UICharacter) == 1 then CloseWin() return end LoadImagesOnce() local now = os.time() if State.waiting and (now - State.sentAt) >= CFG.TimeoutSec then State.waiting, State.failed = false, true end if not State.waiting and (now - State.lastReq) >= CFG.AutoSec then Request() end -- Posicion (movible) if State.moving then State.dx = MousePosX() - State.grabX State.dy = MousePosY() - State.grabY end local baseX = GetWideX() - W * SC - CFG.MarginRight State.dx = Clamp(State.dx, -baseX, GetWideX() - W * SC - baseX) State.dy = Clamp(State.dy, -CFG.Y, 470 - H * SC - CFG.Y) local x, y = baseX + State.dx, CFG.Y + State.dy OX, OY = x, y State.px, State.py = x, y local mx, my = MousePosX(), MousePosY() local d = State.data SetFontType(1) SetTextBg(0, 0, 0, 0) -- Marco y cabecera local oy = CFG.Logo and LOGO_H or 0 Frame(x, y, W, H) Img(IMG.head, x + 3, y + 3, W - 6, 32 + oy) Img(IMG.line, x + 3, y + 35 + oy, W - 6, 1) if CFG.Logo then Img(IMG.logo, x + (W - 120) // 2, y + 2, 120, 60) end local cy = y + oy -- origen del contenido (bajo la cabecera) local function T(ty) return cy + ty end Text(x, T(7), CFG.Title, W, ALIGN_CENTER, 255, 215, 90) if d then Text(x, T(21), d.week, W, ALIGN_CENTER, 175, 170, 150) end Set(Hit.head, x, y, W - 24, 36 + oy) Set(Hit.close, x + W - 22, y + 6, 16, 16) local overClose = Inside(Hit.close, mx, my) Text(x + W - 22, y + 8, "x", 16, ALIGN_CENTER, overClose and 255 or 190, overClose and 120 or 190, overClose and 120 or 190) Set(Hit.win, x, y, W, H) -- Resumen Img(IMG.strip, x + 8, cy + 40, W - 16, 34) local colW = (W - 16) // 3 local function Stat3(i, label, value, hot) local sx = x + 8 + (i - 1) * colW Text(sx, T(44), label, colW, ALIGN_CENTER, 160, 150, 120) Text(sx, T(57), value, colW, ALIGN_CENTER, hot and 255 or 255, hot and 215 or 255, hot and 90 or 255) end if State.tab == 1 then Stat3(1, "KILLS HOY", d and Fmt(d.tdK) or "-", false) Stat3(2, "PUNTOS HOY", d and Fmt(d.tdP) or "-", true) Stat3(3, "TIPOS", d and tostring(#d.today) or "-", false) else local rank = d and (State.tab == 2 and d.rankK or d.rankP) or 0 Stat3(1, "POSICION", rank > 0 and ("#" .. rank) or "-", true) Stat3(2, "KILLS SEM.", d and Fmt(d.wkK) or "-", false) Stat3(3, "PUNTOS SEM.", d and Fmt(d.wkP) or "-", false) end -- Pestanas local labels = { "Hoy", "Kills", "Puntos" } local tw = (W - 16 - 8) // 3 for i = 1, 3 do local h = Hit.tab[i] Set(h, x + 8 + (i - 1) * (tw + 4), cy + 80, tw, 22) local on = State.tab == i or Inside(h, mx, my) Img(on and IMG.tabOn or IMG.tabOff, h.x, h.y, h.w, h.h) if State.tab == i then Text(h.x, h.y + 6, labels[i], h.w, ALIGN_CENTER, 255, 245, 210) else Text(h.x, h.y + 6, labels[i], h.w, ALIGN_CENTER, 200, 195, 180) end end -- Encabezado de columnas if State.tab == 1 then Text(x + 14, T(108), "MONSTRUO", 90, ALIGN_LEFT, 160, 150, 120) Text(x + 84, T(108), "KILLS x " .. string.upper(CFG.PointsLabel), 70, ALIGN_RIGHT, 160, 150, 120) Text(x + 154, T(108), "TOTAL", 54, ALIGN_RIGHT, 160, 150, 120) else Text(x + 14, T(108), "JUGADOR", 90, ALIGN_LEFT, 160, 150, 120) Text(x + 120, T(108), State.tab == 2 and "KILLS" or string.upper(CFG.PointsLabel), 90, ALIGN_RIGHT, 160, 150, 120) end Img(IMG.line, x + 8, cy + 120, W - 16, 1) -- Lista local k = Ease() local ry = cy + 125 local rows = 0 if State.tab == 1 then local list = d and d.today or {} local maxv = 1 for _, r in ipairs(list) do if r.points > maxv then maxv = r.points end end for i, r in ipairs(list) do if i > CFG.MaxRows then break end DrawRow(x, ry, i, nil, r.name, Fmt(r.points), (r.points / maxv) * k, false, string.format("%d x %s", r.kills, Fmt(r.kills > 0 and math.floor(r.points / r.kills + 0.5) or 0))) ry = ry + 17; rows = rows + 1 end if rows == 0 then Text(x, cy + 190, d and "Aun no has cazado invasiones hoy" or "Cargando...", W, ALIGN_CENTER, 170, 170, 180) end else local list = d and (State.tab == 2 and d.byKills or d.byPoints) or {} local me = string.lower(MyName()) local maxv = 1 if list[1] then maxv = math.max(1, State.tab == 2 and list[1].kills or list[1].points) end for i, r in ipairs(list) do if i > CFG.MaxRows then break end local v = State.tab == 2 and r.kills or r.points DrawRow(x, ry, i, i, r.name, Fmt(v), (v / maxv) * k, me ~= "" and string.lower(r.name) == me) ry = ry + 17; rows = rows + 1 end if rows == 0 then Text(x, cy + 190, d and "Aun no hay datos esta semana" or "Cargando...", W, ALIGN_CENTER, 170, 170, 180) end end -- Pie Img(IMG.line, x + 8, y + H - 24, W - 16, 1) local status, sr, sg, sb if State.waiting then status, sr, sg, sb = "Cargando...", 255, 215, 90 elseif State.failed then status, sr, sg, sb = (d and "Sin respuesta (datos anteriores)" or "Sin respuesta del servidor"), 255, 120, 120 elseif d then status, sr, sg, sb = string.format("Actualizado hace %ds", now - State.lastOk), 150, 150, 160 else status, sr, sg, sb = "Sin datos", 150, 150, 160 end Text(x + 14, y + H - 17, status, 130, ALIGN_LEFT, sr, sg, sb) Set(Hit.refresh, x + W - 76, y + H - 21, 64, 16) Img(Inside(Hit.refresh, mx, my) and IMG.tabOn or IMG.tabOff, Hit.refresh.x, Hit.refresh.y, 64, 16) Text(Hit.refresh.x, Hit.refresh.y + 3, "Refrescar", 64, ALIGN_CENTER, 235, 230, 215) end Console(1, "[InvasionTop] v3 cargado - F4 abre el ranking")