-- OT-MMO: plano de la dungeon sin niebla (vault OT-MMO/dungeons/fortaleza-v2.md §6, regla D31). -- Al entrar en una copia de dungeon, el minimapa y el mapa (M) enseñan la copia entera, como el mundo con mapa-mundo.otmm, -- con marcas: entrada, hogueras (apagada / encendida), jefes (vivo / tachado) y salida (cerrada / abierta). -- -- Protocolo (opcode 123, server/scripts/dungeon.lua): -- cliente -> servidor V|1 al empezar la partida: «entiendo el plano» (un cliente viejo no lo manda y no -- recibe nada) -- servidor -> cliente E|tipo|ox|oy|lado|zmin|zmax|titulo|marcas al entrar, al reconectar dentro y en cada cambio -- marcas = tipo,x,y,z,estado,nombre;... relativas al origen (ox, oy) del hueco -- S al salir de la copia -- El terreno: data/planos/.otmm (tools/map-editor/scripts/generar_planos.mts) es la plantilla pintada desde (0,0), -- un bloque de 64 x 64 por piso; aqui se le cambia la x/y de cada bloque por el origen del hueco y se carga encima. -- El plano NO se queda en el minimapa personal: al entrar se guarda el minimapa (/minimap.otmm) tal cual estaba y al -- salir (o al guardar en cualquier momento, p. ej. al cerrar la partida) se limpia y se vuelve a cargar ese respaldo y -- mapa-mundo.otmm. Lo visto dentro de la copia tampoco se guarda (son coordenadas lejanas, 4096+). local OPCODE = 123 local TEMPORAL = '/plano-dungeon.otmm' local IMG = '/modules/game_plano/images/' local ICONO = { E = { 'entrada', 'entrada' }, H = { 'hoguera', 'hoguera-on' }, J = { 'jefe', 'jefe-muerto' }, X = { 'salida', 'salida-on' }, } local TEXTO = { H = { 'apagada', 'encendida: aquí reapareces' }, J = { '', 'derrotado' }, X = { 'se abre al vencer al jefe final', 'portal abierto' }, } planoController = Controller:new() local activo = nil -- { tipo, ox, oy, lado, zmin, zmax, titulo, marcas, respaldo } local marcasW = {} -- widgets sobre el minimapa y el mapa M local guardarOriginal = nil local anterior = {} -- onCameraPositionChange de cada mapa antes de envolverlo local anteriorZoom = {} -- onZoomChange, igual local function u16(s, i) return s:byte(i) + s:byte(i + 1) * 256 end local function pu16(v) return string.char(v % 256, math.floor(v / 256) % 256) end -- Cambia la x/y de cada bloque del .otmm (pintado en 0,0) por el origen del hueco. local function reubicar(datos, ox, oy) local inicio = u16(datos, 5) local partes = { datos:sub(1, inicio) } local i = inicio + 1 while i + 6 <= #datos do local x, y, z = u16(datos, i), u16(datos, i + 2), datos:byte(i + 4) if x == 65535 and y == 65535 then break end local len = u16(datos, i + 5) partes[#partes + 1] = pu16(ox + x) .. pu16(oy + y) .. string.char(z) .. pu16(len) .. datos:sub(i + 7, i + 6 + len) i = i + 7 + len end partes[#partes + 1] = pu16(65535) .. pu16(65535) .. string.char(255) return table.concat(partes) end local function mapas() local l = {} local mm = modules.game_minimap and modules.game_minimap.getMiniMapUi and modules.game_minimap.getMiniMapUi() if mm then l[#l + 1] = mm end local mM = modules.game_mapa and modules.game_mapa.getMapa and modules.game_mapa.getMapa() if mM then l[#l + 1] = mM end return l end -- Cada marca solo en su piso; en el minimapa redondo, solo dentro del circulo (los widgets no se recortan al circulo). local function verPiso() for _, w in ipairs(marcasW) do if not w:isDestroyed() then local c = w.mapa:getCameraPosition() local ver = c ~= nil and c.z == w.pos.z if ver and w.mapa.isCircular and w.mapa:isCircular() then local r, p = w.mapa:getPaddingRect(), w.mapa:getTilePoint(w.pos) local cx, cy = r.x + r.width / 2, r.y + r.height / 2 local radio = math.min(r.width, r.height) / 2 - 7 ver = p ~= nil and (p.x - cx) ^ 2 + (p.y - cy) ^ 2 <= radio * radio end w:setVisible(ver) end end end local function quitarMarcas() for _, w in ipairs(marcasW) do if not w:isDestroyed() then w:destroy() end end marcasW = {} end local function pintarMarcas() quitarMarcas() if not activo then return end for _, mapa in ipairs(mapas()) do for _, m in ipairs(activo.marcas) do local clase = m.tipo:sub(1, 1) local ic = ICONO[clase] if ic then local w = g_ui.createWidget('PlanoMarca', mapa) w:setImageSource(IMG .. ic[m.estado and 2 or 1]) local extra = TEXTO[clase] and TEXTO[clase][m.estado and 2 or 1] or '' w:setTooltip(m.nombre .. (extra ~= '' and (' (' .. extra .. ')') or '')) w.pos = { x = activo.ox + m.x, y = activo.oy + m.y, z = m.z } w.mapa = mapa mapa:centerInPosition(w, w.pos) marcasW[#marcasW + 1] = w end end end verPiso() end -- Vuelve el minimapa a como estaba antes de entrar (sin el plano ni lo visto dentro de la copia). local function restaurar() if activo and activo.respaldo then activo.respaldo = false g_minimap.clean() if g_resources.fileExists('/minimap.otmm') then g_minimap.loadOtmm('/minimap.otmm') end if g_resources.fileExists('/mapa-mundo.otmm') then g_minimap.loadOtmm('/mapa-mundo.otmm') end end end local function cargarPlano() local archivo = '/planos/' .. activo.tipo .. '.otmm' if not g_resources.fileExists(archivo) then g_logger.warning('[game_plano] no existe ' .. archivo) return end if not activo.respaldo then guardarOriginal('/minimap.otmm') -- el minimapa personal tal cual estaba, para volver a el al salir activo.respaldo = true end local ok, err = pcall(function() g_resources.writeFileContents(TEMPORAL, reubicar(g_resources.readFileContents(archivo), activo.ox, activo.oy)) g_minimap.loadOtmm(TEMPORAL) g_resources.deleteFile(TEMPORAL) end) if not ok then g_logger.error('[game_plano] ' .. tostring(err)) end end local function partir(s, sep) local l = {} for parte in (s .. sep):gmatch('(.-)' .. sep:gsub('%p', '%%%0')) do l[#l + 1] = parte end return l end local function salir() restaurar() local mM = modules.game_mapa and modules.game_mapa.getMapa and modules.game_mapa.getMapa() if activo and activo.zoomM and mM then mM:setZoom(activo.zoomM) -- el zoom del mapa M que tenia antes de encuadrar la copia end activo = nil quitarMarcas() end local function onOpcode(protocol, opcode, buffer) if buffer == 'S' then return salir() end if buffer:sub(1, 2) ~= 'E|' then return end local c = partir(buffer, '|') local nuevo = { tipo = c[2], ox = tonumber(c[3]), oy = tonumber(c[4]), lado = tonumber(c[5]), zmin = tonumber(c[6]), zmax = tonumber(c[7]), titulo = c[8] or '', marcas = {} } if not (nuevo.tipo and nuevo.ox and nuevo.oy) then return end for _, m in ipairs(partir(c[9] or '', ';')) do local f = partir(m, ',') if #f >= 5 then nuevo.marcas[#nuevo.marcas + 1] = { tipo = f[1], x = tonumber(f[2]), y = tonumber(f[3]), z = tonumber(f[4]), estado = f[5] == '1', nombre = f[6] or '' } end end if activo and (activo.tipo ~= nuevo.tipo or activo.ox ~= nuevo.ox or activo.oy ~= nuevo.oy) then salir() -- otra copia (no deberia pasar sin un S antes) end if not activo then activo = nuevo cargarPlano() else nuevo.respaldo = activo.respaldo nuevo.zoomM = activo.zoomM activo = nuevo end pintarMarcas() end --- Para game_mapa: nombre de la zona si la posicion esta dentro de la copia en la que estas. function zonaDe(pos) if activo and pos.x >= activo.ox and pos.y >= activo.oy and pos.x < activo.ox + (activo.lado or 64) and pos.y < activo.oy + (activo.lado or 64) then return activo.titulo end end --- Para game_mapa (Centrar en mi / abrir el mapa): dentro de una copia, encuadra el hueco entero en el piso en el que --- estas con el zoom mas grande en el que cabe. Devuelve true si lo hizo. function encuadrar(mapa) local yo = g_game.getLocalPlayer() if not activo or not yo or not mapa then return false end local l = activo.lado or 64 activo.zoomM = activo.zoomM or mapa:getZoom() mapa:setCameraPosition({ x = activo.ox + math.floor(l / 2), y = activo.oy + math.floor(l / 2), z = yo:getPosition().z }) local lado = math.min(mapa:getWidth(), mapa:getHeight()) for z = mapa:getMaxZoom(), mapa:getMinZoom(), -1 do mapa:setZoom(z) if mapa:getScale() * l <= lado then break end end verPiso() return true end function getActivo() return activo end local function envolverMapas() for _, mapa in ipairs(mapas()) do if not anterior[mapa] then anterior[mapa] = mapa.onCameraPositionChange or false mapa.onCameraPositionChange = function(self, pos, old) local f = anterior[self] if f then f(self, pos, old) else UIMinimap.onCameraPositionChange(self, pos, old) end verPiso() end anteriorZoom[mapa] = mapa.onZoomChange or false mapa.onZoomChange = function(self, zoom, old) local f = anteriorZoom[self] if f then f(self, zoom, old) else UIMinimap.onZoomChange(self, zoom, old) end verPiso() end end end end function planoController:onInit() g_ui.importStyle('plano') ProtocolGame.registerExtendedOpcode(OPCODE, onOpcode) -- quien guarde el minimapa (game_minimap al cerrar la partida, el mapa de la ciclopedia) guarda SIN el plano guardarOriginal = g_minimap.saveOtmm g_minimap.saveOtmm = function(archivo) restaurar() return guardarOriginal(archivo) end connect(LocalPlayer, { onPositionChange = verPiso }) end function planoController:onGameStart() envolverMapas() local protocol = g_game.getProtocolGame() if protocol then protocol:sendExtendedOpcode(OPCODE, 'V|1') end end function planoController:onGameEnd() salir() end function planoController:onTerminate() salir() disconnect(LocalPlayer, { onPositionChange = verPiso }) ProtocolGame.unregisterExtendedOpcode(OPCODE) if guardarOriginal then g_minimap.saveOtmm = guardarOriginal guardarOriginal = nil end for mapa, f in pairs(anterior) do if not mapa:isDestroyed() then mapa.onCameraPositionChange = f or nil mapa.onZoomChange = anteriorZoom[mapa] or nil end end anterior, anteriorZoom = {}, {} end