-- OT-MMO: mapa del juego (M), estilo WoW con toque propio. El terreno es el minimapa del cliente, ya con el mundo entero -- (data/mapa-mundo.otmm, tools/map-editor/scripts/generar_minimapa.mts); encima van los lugares que manda el servidor -- (server/scripts/mapa.lua, opcode 113 «L|[…]»), los rótulos de cada zona, las marcas de misión de game_misiones -- («!» / «?» sobre el lugar del NPC) y tu posición. -- 23-oct: objetivos de misión encima de los lugares (antes quedaban debajo del ícono del NPC), grupo «Misiones» en la -- lista (clic = marcar la ruta y centrar), zoom de -2 a 3 con botones + y - que se apagan en el límite, rueda a un paso -- por gesto (game_minimap.ruedaConPausa) y el zoom se recuerda entre sesiones. local OPCODE = 113 local IMG = '/modules/game_mapa/images/' local MARCA = { ['!'] = '/modules/game_misiones/images/marca-mision', ['?'] = '/modules/game_misiones/images/marca-entrega' } -- Zonas: nombre por caja (x0, y0, x1, y1, z o nil = cualquiera). La primera que contiene la posición gana. local ZONAS = { { 'Cloaca de Vigilia', 1460, 1360, 1530, 1455, 8 }, { 'Cueva de los Contrabandistas', 1130, 1300, 1330, 1480, 8 }, { 'Fortaleza del Tuerto', 1780, 1150, 1900, 1500 }, { 'Vigilia', 1450, 1280, 1640, 1520 }, { 'Aldea del Molino', 1330, 1280, 1450, 1520 }, { 'Bosque del Tuerto', 1100, 1280, 1330, 1520 }, { 'Tierras Grises', 0, 0, 1100, 1150 }, { 'Mina de Hondahierro', 1580, 1100, 1700, 1240, 8 }, { 'Mina de Hondahierro', 1580, 1100, 1700, 1240, 9 }, { 'Mina de Hondahierro', 1580, 1100, 1700, 1240, 10 }, { 'Tierras Altas', 1330, 1040, 1700, 1262 }, } -- El rótulo de una zona solo se pinta si el servidor mandó algún lugar de ella (áreas sin publicar: sin rótulo). local ROTULOS = { { 'VIGILIA', 1548, 1372, 'Vigilia' }, { 'ALDEA DEL MOLINO', 1395, 1366, 'Aldea del Molino' }, { 'BOSQUE DEL TUERTO', 1232, 1386, 'Bosque del Tuerto' }, { 'TIERRAS ALTAS', 1500, 1150, 'Tierras Altas' } } local GRUPOS = { 'Vigilia', 'Aldea del Molino', 'Bosque del Tuerto', 'Tierras Altas', 'Mina de Hondahierro' } mapaController = Controller:new() local ventana, mapa = nil, nil local lugares = {} -- lo que mandó el servidor local encima = {} -- widgets sobre el mapa (lugares, rótulos, marcas) local marcasW = {} local ZOOM_MIN, ZOOM_MAX, ZOOM_INICIAL = -2, 3, 1 local filasMision = {} -- filas del grupo «Misiones» de la lista function getMapa() return mapa end local function zonaDe(pos) -- dentro de una dungeon, el nombre de la copia (game_plano: plano sin niebla) local plano = modules.game_plano and modules.game_plano.zonaDe and modules.game_plano.zonaDe(pos) if plano then return plano end for _, z in ipairs(ZONAS) do if pos.x >= z[2] and pos.x <= z[4] and pos.y >= z[3] and pos.y <= z[5] and (not z[6] or z[6] == pos.z) then return z[1] end end return 'Tierras sin nombre' end local function escribiendo() local w = rootWidget while w:getFocusedChild() do w = w:getFocusedChild() end return w:getClassName() == 'UITextEdit' and w:isVisible() end -- Lo que va encima del terreno solo vale para su piso: en otro piso el ancla de posición no existe y el widget -- caería en la esquina del mapa. local function verPiso() local c = mapa:getCameraPosition() -- nil hasta la primera vez que se centra if not c then return end local z = c.z for _, w in ipairs(encima) do w:setVisible(w.pos.z == z) end ventana.piso:setText(z == 7 and 'Superficie' or z < 7 and ('Piso ' .. (7 - z) .. ' hacia arriba') or ('Bajo tierra, piso ' .. (z - 7))) end local function poner(w, pos) w.pos = pos mapa:centerInPosition(w, pos) table.insert(encima, w) return w end local function limpiar() for _, w in ipairs(encima) do w:destroy() end encima, marcasW = {}, {} end local function pintarMarcas() for _, w in ipairs(marcasW) do table.removevalue(encima, w) w:destroy() end marcasW = {} local marcas = modules.game_misiones and modules.game_misiones.getMarcas and modules.game_misiones.getMarcas() or {} for _, l in ipairs(lugares) do local m = l.npc ~= '' and marcas[l.npc] if m and MARCA[m] then local w = g_ui.createWidget('MapaMarca', mapa) w:setImageSource(MARCA[m]) w:setMarginBottom(14) -- encima del ícono del lugar w:setTooltip(m == '?' and 'Puedes entregar una misión' or 'Hay una misión para ti') table.insert(marcasW, poner(w, { x = l.x, y = l.y, z = l.z })) end end end local function pintar() limpiar() local conLugar = {} for _, l in ipairs(lugares) do conLugar[zonaDe(l)] = true end for _, r in ipairs(ROTULOS) do if conLugar[r[4]] then local w = g_ui.createWidget('MapaRotulo', mapa) w:setText(r[4]) -- nombre en sentence case (guía B.3: nada de versalitas) poner(w, { x = r[2], y = r[3], z = 7 }) end end for _, l in ipairs(lugares) do local w = g_ui.createWidget('MapaMarca', mapa) w:setImageSource(IMG .. l.icono) w:setTooltip(l.nombre .. '\n' .. l.detalle) poner(w, { x = l.x, y = l.y, z = l.z }) end pintarMarcas() -- objetivos de misión: encima de los lugares if modules.game_misiones and modules.game_misiones.pintarZonas then modules.game_misiones.pintarZonas() end -- lista de la derecha, por zona ventana.lista:destroyChildren() filasMision = {} for _, g in ipairs(GRUPOS) do local titulo = g_ui.createWidget('MapaZona', ventana.lista) titulo:setText(g) for _, l in ipairs(lugares) do if zonaDe(l) == g then local fila = g_ui.createWidget('MapaFila', ventana.lista) fila.icono:setImageSource(IMG .. l.icono) fila.nombre:setText(l.nombre) fila:setTooltip(l.detalle) fila.onClick = function() mapa:setCameraPosition({ x = l.x, y = l.y, z = l.z }) end end end end pintarMisiones() verPiso() end -- Grupo «Misiones» al principio de la lista: una fila por misión activa con el ícono de su objetivo más cercano. -- Clic: marca (o quita) la ruta y centra el mapa en el objetivo. function pintarMisiones() if not ventana then return end for _, w in ipairs(filasMision) do if not w:isDestroyed() then w:destroy() end end filasMision = {} local gm = modules.game_misiones local activas = gm and gm.getActivas and gm.getActivas() or {} if #activas == 0 then return end local ruta = gm.getRuta() local titulo = g_ui.createWidget('MapaZona') titulo:setText('Misiones') titulo:setMarginTop(0) ventana.lista:insertChild(1, titulo) table.insert(filasMision, titulo) for i, m in ipairs(activas) do local d = gm.destinoDe(m) local fila = g_ui.createWidget('MapaFila') ventana.lista:insertChild(i + 1, fila) fila.icono:setImageSource('/modules/game_misiones/images/obj-' .. (d and d.tipo or 'llegar')) fila.nombre:setText(m.nombre) fila:setOn(m.id == ruta) fila.nombre:setColor(m.id == ruta and OtmmoTheme.color.accent or OtmmoTheme.color.text) fila:setTooltip(m.id == ruta and 'Clic: quitar la ruta' or 'Clic: marcar la ruta y centrar el mapa') fila.onClick = function() gm.setRuta(m.id) local destino = gm.destinoDe(m) if destino then mapa:setCameraPosition({ x = destino.x, y = destino.y, z = destino.z }) end end table.insert(filasMision, fila) end local primeraZona = ventana.lista:getChildByIndex(#activas + 2) if primeraZona then primeraZona:setMarginTop(12) end end -- Zoom: botones + y - sobre el mapa, apagados en el límite; se guarda entre sesiones. local function refrescarZoom() if not mapa then return end local z = mapa:getZoom() ventana.zoomMas:setEnabled(z < mapa:getMaxZoom()) ventana.zoomMenos:setEnabled(z > mapa:getMinZoom()) g_settings.set('mapaZoom', z) end function zoomMas() mapa:zoomIn() refrescarZoom() end function zoomMenos() mapa:zoomOut() refrescarZoom() end -- Solo con la ventana abierta: cerrada, la cámara no está puesta y setCrossPosition fallaría. Un error aquí corta la -- cadena de onPositionChange y el minimapa de la esquina deja de seguir al jugador (pasó el 22-oct). local function alMoverse(_, pos) if mapa and pos and ventana:isVisible() and mapa:getCameraPosition() then mapa:setCrossPosition({ x = pos.x, y = pos.y, z = pos.z }) end end function centrar() local yo = g_game.getLocalPlayer() if not yo or not mapa then return end local pos = yo:getPosition() -- dentro de una dungeon, la copia entera encuadrada (game_plano); fuera, centrado en ti if not (modules.game_plano and modules.game_plano.encuadrar and modules.game_plano.encuadrar(mapa)) then mapa:setCameraPosition(pos) end mapa:setCrossPosition({ x = pos.x, y = pos.y, z = pos.z }) ventana.zona:setText(zonaDe(pos)) end function toggle() if not ventana then return end if ventana:isVisible() then ventana:hide() return end ventana:show() ventana:raise() ventana:focus() pintarMarcas() pintarMisiones() refrescarZoom() verPiso() centrar() end local function onOpcode(protocol, opcode, buffer) if buffer:sub(1, 2) ~= 'L|' then return end local ok, datos = pcall(json.decode, buffer:sub(3)) if ok and type(datos) == 'table' then lugares = datos pintar() end end function mapaController:onInit() ventana = g_ui.displayUI('mapa') OtmmoVentanas.recordar(ventana, 'mapa') -- posición entre sesiones (fase 4) ventana:hide() mapa = ventana.mapa for _, id in ipairs({ 'floorUpButton', 'floorDownButton', 'zoomInButton', 'zoomOutButton', 'resetButton' }) do local b = mapa:getChildById(id) if b then b:hide() end end local fijarMin = mapa.setMinZoom or mapa.setMixZoom -- en C++ se enlazó con errata («setMixZoom») fijarMin(mapa, ZOOM_MIN) mapa:setMaxZoom(ZOOM_MAX) local guardado = g_settings.getNumber('mapaZoom', ZOOM_INICIAL) if modules.game_minimap and modules.game_minimap.encajarZoom then modules.game_minimap.encajarZoom(mapa, guardado) modules.game_minimap.ruedaConPausa(mapa, refrescarZoom) else mapa:setZoom(ZOOM_INICIAL) end refrescarZoom() -- el ancla de los widgets no sigue al piso: al cambiar de piso (Ctrl + rueda) se esconden los de otro piso mapa.onCameraPositionChange = function(self, pos) UIMinimap.onCameraPositionChange(self, pos) verPiso() end ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) Keybind.new('Ventanas', 'Mapa', { [CHAT_MODE.ON] = '', [CHAT_MODE.OFF] = 'M' }, '') Keybind.bind('Ventanas', 'Mapa', { { type = KEY_DOWN, callback = function() if not escribiendo() then toggle() end end } }) connect(LocalPlayer, { onPositionChange = alMoverse }) end function mapaController:onGameStart() local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, 'P') end end function mapaController:onGameEnd() if ventana then ventana:hide() end end function mapaController:onTerminate() disconnect(LocalPlayer, { onPositionChange = alMoverse }) ProtocolGame.unregisterExtendedOpcode(OPCODE) Keybind.delete('Ventanas', 'Mapa') limpiar() if ventana then ventana:destroy() end ventana, mapa = nil, nil end