-- OT-MMO: el Gunslinger (vocación 13, clientid 8). Recámara en el HUD y RECARGA ACTIVA (como Gears of War). -- Solo dibuja y pide: el servidor (server/scripts/clases/gunslinger.lua) lleva las balas, la recarga y el tiempo, y -- juzga la segunda pulsación (zona dorada, blanca o fallo) con margen de latencia. Opcode 138: -- cliente «R» empieza a recargar · «C|ms» segunda pulsación (ms desde que empezó la barra, reloj del cliente) · «P» estado -- servidor «S|familia|balas|capacidad|dorada|estado» (estado L listo · R recargando · E encasquillada · - sin arma) -- «Z|familia|dur|blancaIni|blancaFin|doradaIni|doradaFin» · «E|D|ms» dorada · «E|B|ms» blanca · «E|F|ms» fallo -- · «E|N|0» acabó sola · «E|X|0» cancelada · «N|motivo» no se puede -- Tecla R (sin chat) o Alt+R (con chat). Para las demás clases no hace nada. local OPCODE = 138 local VOC = 8 -- clientid de vocations.xml local C = OtmmoTheme.color local ORO, BRASA, HUESO = '#C7A86B', '#D99A45', '#D6D0C2' local DORADO = '#F0C24A' -- recámara dorada (28-oct): oro saturado, el mismo de las balas (arte/gunslinger/hud.py) local TAM = { pistolas = { 7, 16, 2 }, rifle = { 9, 22, 3 }, metralleta = { 5, 11, 1 } } -- ancho, alto, separación local NOMBRE = { pistolas = 'Pistolas', rifle = 'Rifle', metralleta = 'Metralleta' } local ANCHO_PISTA = 220 -- Las zonas y el rayado se anclan al carril (parent): otclient los pone dentro de su padding (4 px). La aguja se ancla -- a pista.left (un hermano, sin padding): sin sumarle esos 4 px iba por detrás de las zonas y el final de cada franja -- (lo que más se pulsa) era ya fallo en el servidor. Aguja de 7 px: su centro está 3 px a la derecha de su borde. local PADDING_PISTA = 4 local ATASCO_MS = 2000 -- server: G.ATASCO local ANCHO_BALAS = 186 -- hueco entre el arma y la cuenta (placa de 284) local SEG_ALTO = 14 -- segmento del cargador de la metralleta gunslingerController = Controller:new() local hud, barra = nil, nil local estado = { fam = nil, balas = 0, cap = 0, dorada = false, st = '-' } local recarga = nil -- { t0, dur, ba, bb, da, db, enviado } local atasco = nil -- { hasta } mientras la barra muestra el encasquillado (un solo estado: cuenta atrás) local ciclo, ocultarBarra = nil, nil local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end local function partir(s) local t = {} for x in ((s or '') .. '|'):gmatch('(.-)|') do t[#t + 1] = x end return t end local function esGunslinger() local p = g_game.getLocalPlayer() return p and p:getVocation() == VOC end local function ahora() return g_clock.millis() end -- Recámara -------------------------------------------------------------------------------------------------------- -- Pistolas y rifle: un cartucho por bala. Metralleta (30, hasta 42 con Tambor Grande): cargador de segmentos en grupos -- de 5, en una sola fila que se lee a 1366 px (antes, 2 filas de cartuchos de 5x11). local function colocar(w, x, y) w:addAnchor(AnchorLeft, 'parent', AnchorLeft) w:addAnchor(AnchorTop, 'parent', AnchorTop) w:setMarginLeft(x) w:setMarginTop(y) end local function pintarBalas() local fam = estado.fam local caja = hud.balas caja:destroyChildren() if not fam then return end local cap, llenas = estado.cap, estado.balas if fam == 'metralleta' then local grupos = math.ceil(cap / 5) local hueco = 3 local w = math.floor((ANCHO_BALAS - (cap - 1) - (grupos - 1) * hueco) / cap) if w < 3 then hueco, w = 2, math.max(2, math.floor((ANCHO_BALAS - (cap - 1) - (grupos - 1) * 2) / cap)) end w = math.min(w, 6) local x = 0 for i = 1, cap do local s = g_ui.createWidget('GunslingerBala', caja) local img = i <= llenas and (estado.dorada and 'seg-dorada' or 'seg-llena') or 'seg-vacia' s:setImageSource('/modules/game_gunslinger/images/' .. img) s:setImageBorder(1) s:setSize({ width = w, height = SEG_ALTO }) colocar(s, x, 1) x = x + w + 1 + ((i % 5 == 0) and hueco or 0) end caja:setWidth(x) caja:setHeight(SEG_ALTO + 2) return end local w, h, sep = unpack(TAM[fam]) for i = 1, cap do local b = g_ui.createWidget('GunslingerBala', caja) b:setImageSource('/modules/game_gunslinger/images/bala-' .. fam) local clip = i <= llenas and (estado.dorada and 2 or 0) or 1 b:setImageClip({ x = clip * w, y = 0, width = w, height = h }) b:setSize({ width = w, height = h }) colocar(b, (i - 1) * (w + sep), 0) end caja:setWidth(cap * (w + sep) - sep) caja:setHeight(h) end local function pintarHud() if not hud then return end if not estado.fam or not esGunslinger() then hud:hide() return end hud:show() hud.arma:setImageSource('/modules/game_gunslinger/images/arma-' .. estado.fam) hud.cuenta:setText(string.format('%d/%d', estado.balas, estado.cap)) pintarBalas() local placa, texto, color = 'placa', NOMBRE[estado.fam], C.text2 local dorada = estado.dorada and estado.balas > 0 if estado.st == 'E' then -- la barra de encima ya cuenta los segundos: aquí solo el canto rojo (un solo mensaje en pantalla) placa = 'placa-atasco' if not (atasco and barra:isVisible()) then texto, color = 'Encasquillada', C.errorText end elseif estado.st == 'R' then texto, color = 'Recargando...', C.text2 elseif estado.balas <= 0 then placa, texto, color = 'placa-vacia', 'Vacía: recarga', BRASA elseif dorada then placa, texto, color = 'placa-dorada', 'Recámara dorada: +20 % de daño', DORADO end hud:setImageSource('/modules/game_gunslinger/images/' .. placa) hud.rombo:setVisible(dorada and estado.st == 'L') hud.estado:setMarginLeft(hud.rombo:isVisible() and 13 or 0) hud.estado:setText(texto) hud.estado:setColor(color) hud.cuenta:setColor(estado.balas <= 0 and BRASA or (dorada and DORADO or C.text)) hud.tecla:setVisible(estado.balas < estado.cap and estado.st == 'L') end -- Barra de recarga activa ---------------------------------------------------------------------------------------- local function px(ms) return math.floor(ANCHO_PISTA * ms / recarga.dur + 0.5) end local function ponerAguja(ms) barra.aguja:setMarginLeft(PADDING_PISTA + math.min(ANCHO_PISTA, px(ms)) - 3) end local function mostrarBarra(dur, ba, bb, da, db) recarga = { t0 = ahora(), dur = dur, ba = ba, bb = bb, da = da, db = db } ocultarBarra = nil atasco = nil barra:setOpacity(1) barra.pista.rayado:hide() barra.pista.blanca:show() barra.pista.dorada:show() barra.aguja:show() barra.pista.blanca:setMarginLeft(px(ba)) barra.pista.blanca:setWidth(math.max(4, px(bb) - px(ba))) barra.pista.dorada:setMarginLeft(px(da) - 1) barra.pista.dorada:setWidth(math.max(4, px(db) - px(da) + 2)) ponerAguja(0) barra.resultado:setText('Pulsa R en la franja') barra.resultado:setColor(C.text2) barra:show() barra:raise() end local function segundos(ms) return (string.format('%.1f', math.max(0, ms) / 1000):gsub('%.', ',')) end -- Encasquillada: un solo estado en la barra. Fuera las zonas y la aguja; el carril rayado en rojo se vacía en 2 s y el -- texto dice cuánto falta. Cuando el servidor da el arma por desatascada (E|N), la barra se va sin decir «Recargada». local function encasquillar() atasco = { hasta = ahora() + ATASCO_MS } barra.pista.blanca:hide() barra.pista.dorada:hide() barra.aguja:hide() barra.destello:hide() barra.pista.rayado:setWidth(ANCHO_PISTA) barra.pista.rayado:show() barra.resultado:setText('Encasquillada: ' .. segundos(ATASCO_MS) .. ' s') barra.resultado:setColor(C.errorText) ocultarBarra = nil end local function resultado(tipo, ms) if tipo == 'N' and atasco then atasco = nil -- fin del encasquillado: se va sin otro mensaje recarga = nil ocultarBarra = ahora() return end if tipo == 'F' then recarga = nil encasquillar() return end local textos = { D = { '¡Recarga dorada!', DORADO }, -- el +20 % ya lo dice la recámara B = { 'Recarga rápida', HUESO }, N = { 'Recargada', C.text2 }, X = { 'Recarga cancelada', C.text2 }, } local t = textos[tipo] or textos.N barra.resultado:setText(t[1]) barra.resultado:setColor(t[2]) if recarga and ms and tonumber(ms) and tonumber(ms) > 0 then ponerAguja(tonumber(ms)) end if tipo == 'D' then barra.destello:show() local paso = 0 local function animar() if not barra or paso > 3 then if barra then barra.destello:hide() end return end barra.destello:setImageClip({ x = 0, y = paso * 16, width = 16, height = 16 }) paso = paso + 1 scheduleEvent(animar, 70) end animar() end recarga = nil ocultarBarra = ahora() + 900 end local function pulsarR() if not esGunslinger() or g_keyboard.escribiendo() then return end if recarga and not recarga.enviado then recarga.enviado = true enviar('C|' .. (ahora() - recarga.t0)) elseif not recarga then enviar('R') end end local function tic() if not barra then return end if atasco then local resto = atasco.hasta - ahora() barra.pista.rayado:setWidth(math.max(0, math.floor(ANCHO_PISTA * resto / ATASCO_MS + 0.5))) barra.resultado:setText('Encasquillada: ' .. segundos(resto) .. ' s') if resto < -1500 then -- no llegó el fin del servidor: no dejar la barra colgada atasco, ocultarBarra = nil, ahora() end elseif recarga then local t = math.min(recarga.dur, ahora() - recarga.t0) if not recarga.enviado then ponerAguja(t) end elseif ocultarBarra and ahora() >= ocultarBarra then local resto = 1 - math.min(1, (ahora() - ocultarBarra) / 250) barra:setOpacity(resto) if resto <= 0 then ocultarBarra = nil barra:hide() end end end local function onOpcode(protocol, opcode, buffer) local m = partir(buffer) local tipo = m[1] if tipo == 'S' then estado.fam = m[2] ~= '-' and m[2] or nil estado.balas, estado.cap = tonumber(m[3]) or 0, tonumber(m[4]) or 0 estado.dorada, estado.st = m[5] == '1', m[6] or 'L' pintarHud() elseif tipo == 'Z' then mostrarBarra(tonumber(m[3]), tonumber(m[4]), tonumber(m[5]), tonumber(m[6]), tonumber(m[7])) elseif tipo == 'E' then if barra:isVisible() or m[2] ~= 'N' then if not barra:isVisible() then barra:show() barra:setOpacity(1) end resultado(m[2], m[3]) end elseif tipo == 'N' then if modules.game_textmessage then modules.game_textmessage.displayFailureMessage(m[2] or '') end end end -- Para las capturas sin servidor (fixture): modules.game_gunslinger.probar(138, 'S|...') function probar(op, texto) if op == OPCODE then onOpcode(nil, op, texto) end end -- Congela la aguja en «ms» (capturas). function congelar(ms) if recarga then recarga.enviado = true ponerAguja(ms) end end function gunslingerController:onInit() local raiz = modules.game_interface.getRootPanel() g_ui.importStyle('gunslinger') hud = g_ui.createWidget('GunslingerHud', raiz) hud:addAnchor(AnchorBottom, 'parent', AnchorBottom) hud:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) hud:setMarginBottom(232) -- 27-oct: a 108 quedaba TAPADA por las barras de hechizos (y 612-660); ahora sobre la barra de nivel/vida hud:hide() barra = g_ui.createWidget('GunslingerRecarga', raiz) barra:addAnchor(AnchorBottom, hud:getId(), AnchorTop) barra:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) barra:setMarginBottom(6) barra:hide() ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) Keybind.new('Combate', 'Recargar (Gunslinger)', { [CHAT_MODE.ON] = 'Alt+R', [CHAT_MODE.OFF] = 'R' }, '') Keybind.bind('Combate', 'Recargar (Gunslinger)', { { type = KEY_DOWN, callback = pulsarR } }) if g_game.isOnline() then self:onGameStart() end end function gunslingerController:onGameStart() if ciclo then removeEvent(ciclo) end ciclo = cycleEvent(tic, 16) if esGunslinger() then enviar('P') end end function gunslingerController:onGameEnd() if ciclo then removeEvent(ciclo) ciclo = nil end recarga, ocultarBarra, atasco = nil, nil, nil estado = { fam = nil, balas = 0, cap = 0, dorada = false, st = '-' } if hud then hud:hide() end if barra then barra:hide() end end function gunslingerController:onTerminate() self:onGameEnd() ProtocolGame.unregisterExtendedOpcode(OPCODE) Keybind.delete('Combate', 'Recargar (Gunslinger)') if barra then barra:destroy() end if hud then hud:destroy() end hud, barra = nil, nil end