-- OT-MMO: tiendas de mercader (servidor: server/scripts/tiendas/tiendas.lua, opcode 141). Solo dibuja y pide: el -- servidor comprueba el puesto, los objetos, el oro y la comisión, y guarda todo. -- servidor -> «P|n^x^y^z^estado^dueno^titulo^tipo^id~...» puestos · «S|n» estás en el puesto libre n (0 = ya no) -- «E|n^oro^puestos^comision^maxV^maxC|v~...|c~...» editor · «V|cab|l~...» una tienda · «G|...» la tuya -- «M|texto» aviso · «X» cerrar todo -- cliente -> «P» · «E|n» · «O|n|tipo|titulo|l~...» · «V|id» · «C|id|linea|n» · «S|id|linea|n» · «Q» · «D» -- Sobre la cabeza del vendedor (o del NPC que lo sustituye sin conexión) va el nombre de su tienda; en los puestos -- libres, «Puesto libre» en el suelo. Clic derecho sobre el vendedor: «Ver tienda» (gameinterface.lua). local OPCODE = 141 local ORO, CLARO, TEXTO, TEXTO2, OK, ERROR = '#C7A86B', '#E8D3A2', '#E2E5E6', '#9AA7AE', '#52A674', '#DC6B6B' tiendasController = Controller:new() local ventana, editor, panelMia, cartel = nil, nil, nil, nil local puestos = {} -- { n, x, y, z, abierta, dueno, titulo, tipo, id } local viendo = nil -- tienda abierta en la ventana { id, tipo, titulo, dueno, horas, comision, oro, mia, lineas } local ed = nil -- editor { n, oro, puestos, comision, maxV, maxC, mochila, catalogo, tipo, lineas } local libre = 0 -- puesto libre en el que está el jugador local marcados = {} -- [id de criatura] = true (llevan el nombre de su tienda) local suelos = {} -- [clave de posición] = pos (llevan «Puesto libre») local mensajeEv, ronda = nil, nil local abiertoEn = nil local function enviar(texto) local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, texto) end end local function partir(s, sep) local t = {} for x in ((s or '') .. sep):gmatch('(.-)' .. sep:gsub('%p', '%%%0')) do t[#t + 1] = x end return t end local function miles(n) local s = tostring(math.floor(tonumber(n) or 0)) return (s:reverse():gsub('(%d%d%d)', '%1 '):reverse():gsub('^ ', '')) end local function limpio(s) return (tostring(s or ''):gsub('[|^~]', '')) end local function numero(w) return tonumber(w:getText()) or 0 end -- Solo cifras en las cajas de números. local function soloCifras(w, alCambiar) w.onTextChange = function(self, t) local s = t:gsub('%D', '') if s ~= t then self:setText(s) return end if alCambiar then alCambiar() end end end local function cerca(p, q, d) return p and q and p.z == q.z and math.max(math.abs(p.x - q.x), math.abs(p.y - q.y)) <= d end local function mensaje(texto, color) if not ventana then return end ventana.mensaje:setText(texto or '') ventana.mensaje:setColor(color or OK) if mensajeEv then removeEvent(mensajeEv) end mensajeEv = scheduleEvent(function() mensajeEv = nil if ventana then ventana.mensaje:setText('') end end, 6000) end local function ponerObjeto(w, id, n) w:setItemId(id) w:setItemCount(n and n > 1 and n or 1) end -- Carteles: el nombre de la tienda sobre el vendedor y «Puesto libre» en el suelo --------------------------------------- local function quitarCarteles() for cid in pairs(marcados) do local c = g_map.getCreatureById(cid) if c then c:setText('', ORO) end end marcados = {} for _, pos in pairs(suelos) do local tile = g_map.getTile(pos) if tile then tile:setText('', ORO) end end suelos = {} end local function pintarCarteles() local yo = g_game.isOnline() and g_game.getLocalPlayer() if not yo then return end local p = yo:getPosition() local siguen = {} for _, s in ipairs(puestos) do local pos = { x = s.x, y = s.y, z = s.z } local tile = cerca(p, pos, 12) and g_map.getTile(pos) if tile then local k = s.x .. ',' .. s.y .. ',' .. s.z if s.abierta then if suelos[k] then tile:setText('', ORO) suelos[k] = nil end for _, c in ipairs(tile:getCreatures()) do if c:getName() == s.dueno then c:setText(s.titulo, ORO) siguen[c:getId()] = true end end elseif tile:getText() ~= 'Puesto libre' then tile:setText('Puesto libre', ORO) suelos[k] = pos end end end -- quien ya no está en su puesto (o cerró) pierde el cartel for cid in pairs(marcados) do if not siguen[cid] then local c = g_map.getCreatureById(cid) if c then c:setText('', ORO) end end end marcados = siguen end -- La tienda que tiene montada esta criatura (para el menú del clic derecho). function tiendaDe(creature) if not creature then return nil end local pos = creature:getPosition() if not pos then return nil end for _, s in ipairs(puestos) do if s.abierta and s.id > 0 and s.x == pos.x and s.y == pos.y and s.z == pos.z and creature:getName() == s.dueno then return s end end return nil end -- La tienda de otro ---------------------------------------------------------------------------------------------------- local function leerVista(buffer) local cab, resto = buffer:sub(3):match('^([^|]*)|?(.*)$') local m = partir(cab, '^') local v = { id = tonumber(m[1]) or 0, tipo = m[2], titulo = m[3] or '', dueno = m[4] or '', horas = tonumber(m[5]) or 0, comision = tonumber(m[6]) or 0, oro = tonumber(m[7]) or 0, mia = m[8] == '1', lineas = {} } for _, l in ipairs(partir(resto, '~')) do local x = partir(l, '^') if tonumber(x[1]) then v.lineas[#v.lineas + 1] = { linea = tonumber(x[1]), id = tonumber(x[2]) or 0, n = tonumber(x[3]) or 0, precio = tonumber(x[4]) or 0, nombre = x[5] or '', extra = tonumber(x[6]) or 0 } end end return v end local function pintarVista() local v = viendo ventana:setText(v.tipo == 'V' and 'Tienda' or 'Tienda de compra') ventana.titulo:setText(v.titulo) local horas = v.horas == 1 and '1 hora' or (v.horas .. ' horas') ventana.sub:setText((v.mia and 'Tu tienda' or ('De ' .. v.dueno)) .. ' · abierta ' .. horas .. ' más') ventana.oro:setText('Tienes ' .. miles(v.oro) .. ' de oro (con el banco)') ventana.lista:destroyChildren() for _, l in ipairs(v.lineas) do local f = g_ui.createWidget('TiendaFila', ventana.lista) ponerObjeto(f.objeto, l.id, v.tipo == 'V' and l.n or 1) local cuantos if v.tipo == 'V' then f.nombre:setText(l.nombre .. (l.extra > 0 and (' · nivel ' .. l.extra) or '')) f.detalle:setText(miles(l.precio) .. ' de oro c/u · quedan ' .. miles(l.n)) f.boton:setText('Comprar') cuantos = 1 else f.nombre:setText(l.nombre) f.detalle:setText('Paga ' .. miles(l.precio) .. ' c/u · busca ' .. miles(l.n) .. ' · tienes ' .. miles(l.extra)) f.boton:setText('Vender') cuantos = math.min(l.n, l.extra) f.boton:setEnabled(l.extra > 0) end f.cuantos:setText(tostring(math.max(1, cuantos))) soloCifras(f.cuantos) if l.n <= 1 and v.tipo == 'V' then f.cuantos:setEnabled(false) end if v.mia then f.boton:setEnabled(false) f.cuantos:setEnabled(false) end f.boton.onClick = function() local n = numero(f.cuantos) if n < 1 then return mensaje('Escribe cuántos.', ERROR) end enviar((v.tipo == 'V' and 'C|' or 'S|') .. v.id .. '|' .. l.linea .. '|' .. n) end end ventana.vacia:setVisible(#v.lineas == 0) ventana.vacia:setText(v.tipo == 'V' and 'Ya no queda nada.' or 'Ya no busca nada.') end function ver(id) enviar('V|' .. tonumber(id)) end function cerrarTienda() if ventana then ventana:hide() end viendo, abiertoEn = nil, nil end -- Montar la tienda propia ---------------------------------------------------------------------------------------------- local function leerEditor(buffer) local partes = partir(buffer:sub(3), '|') local m = partir(partes[1], '^') local e = { n = tonumber(m[1]) or 0, oro = tonumber(m[2]) or 0, puestos = tonumber(m[3]) or 0, comision = tonumber(m[4]) or 0, maxV = tonumber(m[5]) or 8, maxC = tonumber(m[6]) or 6, mochila = {}, catalogo = {}, tipo = 'V', lineas = {} } for _, l in ipairs(partir(partes[2], '~')) do local x = partir(l, '^') if x[1] and x[1] ~= '' then e.mochila[#e.mochila + 1] = { ref = x[1], id = tonumber(x[2]) or 0, max = tonumber(x[3]) or 1, nombre = x[4] or '', tier = tonumber(x[5]) or 0 } end end for _, l in ipairs(partir(partes[3], '~')) do local x = partir(l, '^') if tonumber(x[1]) then e.catalogo[#e.catalogo + 1] = { ref = x[1], id = tonumber(x[1]), max = 1000, nombre = x[2] or '' } end end return e end local function enTienda(ref) for _, l in ipairs(ed.lineas) do if l.ref == ref then return true end end return false end local function pintarResumen() local total, mal = 0, nil for _, l in ipairs(ed.lineas) do if l.cantidad < 1 or l.cantidad > l.max then mal = 'Revisa las cantidades: ' .. l.nombre .. ' (de 1 a ' .. l.max .. ').' end if l.precio < 1 or l.precio > 1000000 then mal = mal or ('Pon un precio a ' .. l.nombre .. ' (de 1 a 1 000 000).') end total = total + l.cantidad * l.precio end local max = ed.tipo == 'V' and ed.maxV or ed.maxC if ed.tipo == 'V' then local neto = total - math.floor(total * ed.comision / 100) -- la comisión se redondea a la baja en cada venta editor.resumen:setText(#ed.lineas .. ' de ' .. max .. ' · si lo vendes todo, al menos ' .. miles(neto) .. ' de oro (la Liga cobra el ' .. ed.comision .. ' %)') else editor.resumen:setText(#ed.lineas .. ' de ' .. max .. ' · reservas ' .. miles(total) .. ' de oro · tienes ' .. miles(ed.oro)) if total > ed.oro then mal = mal or 'No tienes tanto oro, entre lo que llevas y tu banco.' end end if ed.puestos < 1 then mal = 'Necesitas un puesto de mercader. Oren, el banquero de la Plaza del Faro, los vende.' end editor.aviso:setText(mal or '') editor.abrir:setEnabled(not mal and #ed.lineas > 0) end local pintarEditor local function quitarLinea(ref) for i, l in ipairs(ed.lineas) do if l.ref == ref then table.remove(ed.lineas, i) break end end pintarEditor() end local function ponerLinea(o) local max = ed.tipo == 'V' and ed.maxV or ed.maxC if enTienda(o.ref) or #ed.lineas >= max then return end ed.lineas[#ed.lineas + 1] = { ref = o.ref, id = o.id, nombre = o.nombre, max = o.max, cantidad = ed.tipo == 'V' and o.max or 10, precio = 0 } pintarEditor() end pintarEditor = function() local venta = ed.tipo == 'V' editor.tabVenta:setOn(venta) editor.tabVenta.raya:setVisible(venta) editor.tabCompra:setOn(not venta) editor.tabCompra.raya:setVisible(not venta) editor.explica:setText(venta and 'Pones tus objetos con su precio y los demás te los compran.' or 'Pides objetos a un precio; el oro queda reservado hasta que te los venden.') editor.tOrigen:setText(venta and 'Tu mochila (clic para ponerlo a la venta)' or 'Qué quieres comprar (clic para pedirlo)') editor.tLineas:setText(venta and 'En tu tienda' or 'Lo que pides') local max = venta and ed.maxV or ed.maxC editor.origen:destroyChildren() for _, o in ipairs(venta and ed.mochila or ed.catalogo) do local f = g_ui.createWidget('TiendaOrigen', editor.origen) ponerObjeto(f.objeto, o.id, venta and o.max or 1) f.nombre:setText(o.nombre .. ((o.tier or 0) > 0 and (' · nivel ' .. o.tier) or '')) f:setEnabled(not enTienda(o.ref) and #ed.lineas < max) f.onClick = function() ponerLinea(o) end end editor.lineas:destroyChildren() for _, l in ipairs(ed.lineas) do local f = g_ui.createWidget('TiendaLinea', editor.lineas) ponerObjeto(f.objeto, l.id, 1) f.nombre:setText(l.nombre) f.cantidad:setText(tostring(l.cantidad)) f.precio:setText(l.precio > 0 and tostring(l.precio) or '') if venta and l.max <= 1 then f.cantidad:setEnabled(false) end soloCifras(f.cantidad, function() l.cantidad = numero(f.cantidad) pintarResumen() end) soloCifras(f.precio, function() l.precio = numero(f.precio) pintarResumen() end) f.quitar.onClick = function() quitarLinea(l.ref) end end editor.lineasVacia:setVisible(#ed.lineas == 0) editor.lineasVacia:setText(venta and (#ed.mochila == 0 and 'No llevas nada que se pueda vender.' or 'Elige a la izquierda lo que vendes.') or 'Elige a la izquierda lo que quieres comprar.') pintarResumen() end function elegirTipo(tipo) if not ed or ed.tipo == tipo then return end ed.tipo = tipo ed.lineas = {} pintarEditor() end function montar() if libre > 0 then enviar('E|' .. libre) end end function cerrarEditor() if editor then editor:hide() end ed = nil end function abrirTienda() if not ed then return end local refs = {} for _, l in ipairs(ed.lineas) do refs[#refs + 1] = l.ref .. '^' .. l.cantidad .. '^' .. l.precio end enviar('O|' .. ed.n .. '|' .. ed.tipo .. '|' .. limpio(editor.nombreTienda:getText()) .. '|' .. table.concat(refs, '~')) end -- Tu tienda abierta ---------------------------------------------------------------------------------------------------- function cerrarMia() enviar('Q') end function salirYDejar() local caja local function si() enviar('D') if caja then caja:destroy() end end local function no() if caja then caja:destroy() end end caja = displayGeneralBox('Salir y dejar la tienda', 'Tu personaje sale del juego y la tienda sigue abierta hasta que se acabe el tiempo o se venda todo.\nLo que ganes llega por carta de la Liga.', { { text = 'Salir', callback = si }, { text = 'Cancelar', callback = no }, anchor = AnchorHorizontalCenter }, si, no) end local function pintarMia(buffer) local m = partir(buffer:sub(3), '^') if not tonumber(m[1]) or tonumber(m[1]) == 0 then panelMia:hide() return end local tipo, horas = m[2], tonumber(m[4]) or 0 panelMia.tMia:setText(tipo == 'V' and 'Tu tienda' or 'Tu tienda de compra') panelMia.titulo:setText(m[3] or '') panelMia.horas:setText(horas == 1 and 'queda 1 hora' or ('quedan ' .. horas .. ' horas')) panelMia.cuenta:setText(tipo == 'V' and ('Ganado: ' .. miles(m[5]) .. ' de oro') or ('Oro reservado: ' .. miles(m[6]))) panelMia:show() panelMia:raise() -- con la tienda montada, el editor ya no hace falta if editor:isVisible() then cerrarEditor() end cartel:hide() end -- Opcode --------------------------------------------------------------------------------------------------------------- local function onOpcode(protocol, opcode, buffer) local c = buffer:sub(1, 1) if c == 'P' then quitarCarteles() puestos = {} for _, l in ipairs(partir(buffer:sub(3), '~')) do local m = partir(l, '^') if tonumber(m[4]) then puestos[#puestos + 1] = { n = tonumber(m[1]), x = tonumber(m[2]), y = tonumber(m[3]), z = tonumber(m[4]), abierta = m[5] == 'A', dueno = m[6] or '', titulo = m[7] or '', tipo = m[8] or '', id = tonumber(m[9]) or 0 } end end pcall(pintarCarteles) elseif c == 'S' then libre = tonumber(buffer:sub(3)) or 0 if libre > 0 and not panelMia:isVisible() then cartel:show() cartel:raise() else cartel:hide() if libre == 0 and editor:isVisible() then cerrarEditor() end end elseif c == 'E' then ed = leerEditor(buffer) editor.nombreTienda:setText('') pintarEditor() editor:show() editor:raise() editor:focus() editor.nombreTienda:focus() elseif c == 'V' then viendo = leerVista(buffer) pintarVista() if not ventana:isVisible() then ventana:show() local yo = g_game.getLocalPlayer() abiertoEn = yo and yo:getPosition() end ventana:raise() ventana:focus() elseif c == 'G' then pintarMia(buffer) elseif c == 'M' then local t = buffer:sub(3) if editor:isVisible() then editor.aviso:setText(t) elseif ventana:isVisible() then mensaje(t, ERROR) else modules.game_textmessage.displayFailureMessage(t) end elseif c == 'X' then cerrarTienda() cerrarEditor() end end -- Para probar sin servidor: modules.game_tiendas.probar('V|...') function probar(texto) onOpcode(nil, OPCODE, texto) end -- La ventana de una tienda se cierra al alejarse (el servidor también lo comprueba en cada compra). local function vigilar() if ventana and ventana:isVisible() and abiertoEn then local yo = g_game.getLocalPlayer() local p = yo and yo:getPosition() if p and not cerca(p, abiertoEn, 5) then cerrarTienda() end end pcall(pintarCarteles) end function tiendasController:onInit() g_ui.importStyle('/game_viaje/valdren') g_ui.importStyle('tiendas') ventana = g_ui.createWidget('TiendaVer', rootWidget) editor = g_ui.createWidget('TiendaEditor', rootWidget) panelMia = g_ui.createWidget('TiendaMia', rootWidget) panelMia:addAnchor(AnchorTop, 'parent', AnchorTop) panelMia:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) panelMia:setMarginTop(64) cartel = g_ui.createWidget('TiendaCartel', rootWidget) cartel:addAnchor(AnchorTop, 'parent', AnchorTop) cartel:addAnchor(AnchorHorizontalCenter, 'parent', AnchorHorizontalCenter) cartel:setMarginTop(64) if OtmmoVentanas then OtmmoVentanas.recordar(ventana, 'tienda') OtmmoVentanas.recordar(editor, 'tienda-editor') end for _, w in ipairs({ ventana, editor, panelMia, cartel }) do w:hide() end ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) ronda = cycleEvent(vigilar, 500) end function tiendasController:onGameEnd() quitarCarteles() puestos, viendo, ed, libre, abiertoEn = {}, nil, nil, 0, nil for _, w in ipairs({ ventana, editor, panelMia, cartel }) do if w then w:hide() end end end function tiendasController:onTerminate() self:onGameEnd() for _, ev in ipairs({ mensajeEv, ronda }) do if ev then removeEvent(ev) end end ProtocolGame.unregisterExtendedOpcode(OPCODE) for _, w in ipairs({ ventana, editor, panelMia, cartel }) do if w then w:destroy() end end ventana, editor, panelMia, cartel = nil, nil, nil, nil end