-- OT-MMO: apuntar como en WoW/FFXIV, sin la ventana Battle (vault OT-MMO/diseno/objetivo.md). -- · Tab / Shift+Tab: el enemigo más cercano en pantalla y, al repetir, el siguiente (Shift+Tab, el anterior). -- Primero los que te atacan a ti o a tu grupo (opcode 155 «P|id,id,...», server/scripts/objetivo.lua). Solo -- monstruos atacables (no habitantes, no invocaciones); jugadores solo si te atacan a ti o a tu grupo o son tus -- rivales en la arena (game_pvp). Esc ya quita el objetivo («Stop All Actions» de game_interface). -- · Aro bajo los pies del objetivo: rojo enemigo, dorado élite o jefe, verde aliado; uno claro y fino bajo la criatura -- que tiene el ratón encima, y uno a trazos azules bajo el foco. Sustituye al cuadrado rojo de Tibia. -- Son efectos adjuntos con textura propia (g_attachedEffects.registerByImage): los dibuja el propio cliente justo -- antes que la criatura (debajo de ella) y la siguen al andar. No hace falta C++. Con el orden de dibujo SECOND -- (el de los bordes) el suelo los tapaba: se quedan en el de siempre (THIRD). -- · Alt+clic: marca un FOCO (marco aparte en game_objetivo); Alt+clic en el vacío o sobre el foco lo quita. -- Antes Alt+clic atacaba (lo mismo que el clic normal): ya no. -- · El «T|...» del mismo opcode (a quién ataca tu objetivo) se pasa a game_objetivo. local OPCODE = 155 local CICLO_MS = 4000 -- Tab seguido dentro de este tiempo = «el siguiente»; si no, vuelve a empezar por el más cercano -- id de efecto -> imagen. 2 tamaños: normal (36 x 18) y grande (56 x 26, criaturas de más de una casilla). local AROS = { rojo = 9101, oro = 9102, verde = 9103, hover = 9104, foco = 9105 } local GRANDE = 5 -- se suma al id local IMG = '/game_apuntar/images/aro-' local MEDIDA = { [false] = { 36, 18 }, [true] = { 56, 26 } } -- ancho y alto de cada imagen apuntarController = Controller:new() local atacan = {} -- [id] = true: te atacan a ti o a tu grupo (servidor) local foco = nil local hover = nil local puestos = {} -- [id de criatura] = { criatura, efecto } local ciclo = { hasta = 0, vistos = {}, historia = {} } local procesarOriginal = nil local mapa = nil -- ¿Te ataca a ti o a tu grupo? (lo dice el servidor; también para otros módulos) function teAtaca(c) return c ~= nil and atacan[c:getId()] == true end local function esAliado(c) if c:isLocalPlayer() then return true end local s = c:getShield() return c:isPlayer() and s >= ShieldBlue and s <= ShieldYellowNoSharedExp end local function esRival(c) return c:isPlayer() and modules.game_pvp and modules.game_pvp.esRival and modules.game_pvp.esRival(c:getName()) end -- ¿Se le puede atacar con Tab? (también decide si el ratón lo ilumina) local function atacable(c) if not c or c:isLocalPlayer() or c:isNpc() or c:isDead() or c:getHealthPercent() <= 0 then return false end if c:isPlayer() then return atacan[c:getId()] == true or esRival(c) == true end local t = c:getType() if t == CreatureTypeSummonOwn then return false end if t == CreatureTypeSummonOther then return atacan[c:getId()] == true -- la invocación de otro solo si va a por ti end return c:isMonster() end -- atacando: es tu objetivo de ataque (no solo al que sigues). Un jugador al que atacas es rojo aunque no sea hostil. local function colorDe(c, atacando) if (modules.game_elite and modules.game_elite.esElite and modules.game_elite.esElite(c)) or (modules.game_objetivo and modules.game_objetivo.esJefe and modules.game_objetivo.esJefe(c)) then return 'oro' end if esAliado(c) or c:getType() == CreatureTypeSummonOwn then return 'verde' -- tu grupo, tu invocación end if c:isPlayer() and not atacando and not atacan[c:getId()] and not esRival(c) then return 'verde' -- un jugador al que sigues y no es hostil end return 'rojo' end local function tipoDe(c) local o = c:getOutfit() return o and o.type and o.type > 0 and g_things.getThingType(o.type, ThingCategoryCreature) or nil end local function esGrande(c) local tt = tipoDe(c) return tt ~= nil and tt:getRealSize() > 44 end -- Desfase del aro (setOffset resta del punto de dibujo de la criatura): centrado bajo lo que se ve de la criatura -- (el mismo recuadro que el cuadrado de Tibia: la casilla menos el desplazamiento del sprite) y a la altura de los pies. local function desfase(c, grande) local tt = tipoDe(c) local dx, dy = tt and tt:getDisplacementX() or 8, tt and tt:getDisplacementY() or 8 local ancho = grande and 64 or 32 local cx, pies = 32 - ancho / 2 - dx, 32 - dy - 4 local m = MEDIDA[grande] return math.floor(m[1] / 2 - cx), math.floor(m[2] / 2 - pies) end -- Qué aro le toca a la criatura: objetivo > foco > ratón local function aroDe(c) local objetivo = g_game.getAttackingCreature() or g_game.getFollowingCreature() local nombre if c == objetivo then nombre = colorDe(c, c == g_game.getAttackingCreature()) elseif c == foco then nombre = 'foco' elseif c == hover then nombre = 'hover' end return nombre and (AROS[nombre] + (esGrande(c) and GRANDE or 0)) or nil end local function poner(c) if not c then return end local id = c:getId() local quiere = aroDe(c) local p = puestos[id] -- comprobado en la criatura: al saltar (teletransporte, cambio de piso) el cliente la vuelve a crear sin efectos if p and p.efecto == quiere and p.criatura == c and c:getAttachedEffectById(quiere) then return end if p then p.criatura:detachEffectById(p.efecto) c:detachEffectById(p.efecto) puestos[id] = nil end if quiere then local e = g_attachedEffects.getById(quiere) if e then e:setOffset(desfase(c, quiere > AROS.foco)) -- los grandes van de 9106 a 9110 c:attachEffect(e) puestos[id] = { criatura = c, efecto = quiere } end end end local function quitarTodos() for _, p in pairs(puestos) do p.criatura:detachEffectById(p.efecto) end puestos = {} end -- Repasa los aros puestos (alguno dejó de tocarle) y los de objetivo, foco y ratón. function refrescar() for _, p in pairs(puestos) do poner(p.criatura) end local objetivo = g_game.getAttackingCreature() or g_game.getFollowingCreature() poner(objetivo) poner(foco) poner(hover) end local function alCambiarObjetivo(c) refrescar() -- el cuadrado rojo de Tibia (game_battle lo pone si su ventana está cerrada): el aro lo sustituye addEvent(function() if c and c.hideStaticSquare then c:hideStaticSquare() end end) end -- Tab ----------------------------------------------------------------------------------------------------------- local function candidatos() local yo = g_game.getLocalPlayer() if not yo or not mapa then return {} end local py = yo:getPosition() local lista = {} for _, c in ipairs(mapa:getSpectators()) do local pos = c:getPosition() if pos and pos.z == py.z and atacable(c) and mapa:isInRange(pos) and c:canBeSeen() then local dx, dy = pos.x - py.x, pos.y - py.y lista[#lista + 1] = { c = c, pri = atacan[c:getId()] and 0 or 1, d = dx * dx + dy * dy, id = c:getId() } end end table.sort(lista, function(a, b) if a.pri ~= b.pri then return a.pri < b.pri end if a.d ~= b.d then return a.d < b.d end return a.id < b.id end) return lista end function siguiente() if not g_game.isOnline() then return end local lista = candidatos() if #lista == 0 then return end local ahora = g_clock.millis() local actual = g_game.getAttackingCreature() if ahora > ciclo.hasta or not actual then ciclo.vistos, ciclo.historia = {}, {} if actual then ciclo.vistos[actual:getId()] = true ciclo.historia[1] = actual:getId() end end local elegido for _, x in ipairs(lista) do if not ciclo.vistos[x.id] then elegido = x.c break end end if not elegido then -- ya pasaste por todos: otra vuelta desde el más cercano ciclo.vistos = {} for _, x in ipairs(lista) do if x.c ~= actual then elegido = x.c break end end end ciclo.hasta = ahora + CICLO_MS if not elegido then return end ciclo.vistos[elegido:getId()] = true table.insert(ciclo.historia, elegido:getId()) g_game.attack(elegido) end function anterior() if not g_game.isOnline() then return end local ahora = g_clock.millis() local h = ciclo.historia if ahora <= ciclo.hasta and #h >= 2 then table.remove(h) -- el de ahora while #h > 0 do local c = g_map.getCreatureById(h[#h]) if c and atacable(c) then ciclo.hasta = ahora + CICLO_MS g_game.attack(c) return end table.remove(h) end end -- sin vuelta empezada: el más lejano (la lista recorrida al revés) local lista = candidatos() local actual = g_game.getAttackingCreature() for i = #lista, 1, -1 do if lista[i].c ~= actual then ciclo.vistos, ciclo.historia = { [lista[i].id] = true }, { lista[i].id } ciclo.hasta = ahora + CICLO_MS g_game.attack(lista[i].c) return end end end -- Foco ---------------------------------------------------------------------------------------------------------- local function ponerFoco(c) local antes = foco foco = c if antes then poner(antes) end poner(foco) if modules.game_objetivo and modules.game_objetivo.ponerFoco then modules.game_objetivo.ponerFoco(foco) end end function quitarFoco() ponerFoco(nil) end -- Alt+clic izquierdo sobre el mapa: foco (sin más teclas). Lo demás sigue a game_interface. local function procesar(menuPosition, mouseButton, autoWalkPos, lookThing, useThing, creatureThing, attackCreature, ...) if mouseButton == MouseLeftButton and g_keyboard.getModifiers() == KeyboardAltModifier then local c = creatureThing or attackCreature if not c or c:isLocalPlayer() or c:isNpc() or c == foco then ponerFoco(nil) else ponerFoco(c) end return true end return procesarOriginal(menuPosition, mouseButton, autoWalkPos, lookThing, useThing, creatureThing, attackCreature, ...) end -- Ratón encima ---------------------------------------------------------------------------------------------------- local function ponerHover(c) if c == hover then return end local antes = hover hover = c if antes then poner(antes) end if hover then poner(hover) end end local function alMoverRaton(widget, pos) local tile = mapa and mapa:getTile(pos) local c = tile and tile:getTopCreature() ponerHover(atacable(c) and c or nil) end local function alCambiarHover(widget, dentro) if not dentro then ponerHover(nil) end end -- Servidor ------------------------------------------------------------------------------------------------------ local function onOpcode(protocol, opcode, buffer) local tipo, resto = buffer:match('^(%a)|(.*)$') if tipo == 'P' then local antes = atacan atacan = {} for id in resto:gmatch('%d+') do atacan[tonumber(id)] = true end -- un jugador que empieza o deja de atacarte cambia de color for id in pairs(antes) do if not atacan[id] and puestos[id] then poner(puestos[id].criatura) end end for id in pairs(atacan) do if not antes[id] and puestos[id] then poner(puestos[id].criatura) end end elseif tipo == 'T' and modules.game_objetivo and modules.game_objetivo.alObjetivoDelObjetivo then local id, nombre = resto:match('^(%d+)|(.*)$') modules.game_objetivo.alObjetivoDelObjetivo(tonumber(id), nombre) end end -- vuelve a entrar en pantalla (o el cliente la recrea): si es el objetivo, el foco o el ratón, su aro otra vez local function alAparecer(c) if puestos[c:getId()] or c == foco or c == g_game.getAttackingCreature() or c == g_game.getFollowingCreature() then poner(c) end end local function alMorir(c) if c == foco then ponerFoco(nil) end if c == hover then hover = nil end local p = puestos[c:getId()] if p then c:detachEffectById(p.efecto) puestos[c:getId()] = nil end end local function registrarAros() for nombre, id in pairs(AROS) do for grande, suma in pairs({ [false] = 0, [true] = GRANDE }) do local e = g_attachedEffects.registerByImage(id + suma, 'Aro ' .. nombre, IMG .. nombre .. (grande and '-grande' or '') .. '.png', false) if e then e:setCanDrawOnUI(false) -- ni en el retrato del marco de objetivo ni en otras ventanas e:setPermanent(true) -- si no, el cliente lo quita cada vez que el servidor vuelve a mandar la criatura end end end end local function quitarAros() for _, id in pairs(AROS) do g_attachedEffects.remove(id) g_attachedEffects.remove(id + GRANDE) end end function apuntarController:onInit() registrarAros() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) procesarOriginal = modules.game_interface.processMouseAction modules.game_interface.processMouseAction = procesar connect(g_game, { onAttackingCreatureChange = alCambiarObjetivo, onFollowingCreatureChange = alCambiarObjetivo }) connect(Creature, { onDeath = alMorir, onAppear = alAparecer }) Keybind.new('Combate', 'Objetivo siguiente', 'Tab', '') Keybind.bind('Combate', 'Objetivo siguiente', { { type = KEY_PRESS, callback = siguiente } }, modules.game_interface.getRootPanel()) Keybind.new('Combate', 'Objetivo anterior', 'Shift+Tab', '') Keybind.bind('Combate', 'Objetivo anterior', { { type = KEY_PRESS, callback = anterior } }, modules.game_interface.getRootPanel()) Keybind.new('Combate', 'Quitar foco', '', '') Keybind.bind('Combate', 'Quitar foco', { { type = KEY_DOWN, callback = quitarFoco } }, modules.game_interface.getRootPanel()) if g_game.isOnline() then self:onGameStart() end end function apuntarController:onGameStart() mapa = modules.game_interface.getMapPanel() connect(mapa, { onMouseMove = alMoverRaton, onHoverChange = alCambiarHover }) refrescar() local protocolo = g_game.getProtocolGame() if protocolo then protocolo:sendExtendedOpcode(OPCODE, 'R') -- que el servidor lo vuelva a mandar todo (si no, solo manda cambios) end end function apuntarController:onGameEnd() if mapa then disconnect(mapa, { onMouseMove = alMoverRaton, onHoverChange = alCambiarHover }) end quitarTodos() atacan, foco, hover = {}, nil, nil ciclo = { hasta = 0, vistos = {}, historia = {} } end function apuntarController:onTerminate() self:onGameEnd() ProtocolGame.unregisterExtendedOpcode(OPCODE) if procesarOriginal then modules.game_interface.processMouseAction = procesarOriginal procesarOriginal = nil end disconnect(g_game, { onAttackingCreatureChange = alCambiarObjetivo, onFollowingCreatureChange = alCambiarObjetivo }) disconnect(Creature, { onDeath = alMorir, onAppear = alAparecer }) Keybind.delete('Combate', 'Objetivo siguiente') Keybind.delete('Combate', 'Objetivo anterior') Keybind.delete('Combate', 'Quitar foco') quitarAros() if modules.game_objetivo and modules.game_objetivo.ponerFoco then modules.game_objetivo.ponerFoco(nil) end mapa = nil end