Especificações
Nome: Maze Minigame
Eventos: Movement
Testado em: The Forgotten Server 0.3.6 PL1 8.54 (linux-build)
Autor: Skyen Hasus
Copyright © 2011 Skyen Hasus
Licensa: GNU General Public License v3 <http://www.gnu.org/licenses/gpl.html>
Observações:
- Optei por fazer um minigame de labirinto por ser um clássico mundialmente conhecido, e pelo algoritmo de geração ser complexo, mas não difícil, além de poder ser totalmente aplicado em um jogo como o Tibia.
- O script não foi feito sob Programação Funcional, que é o paradigma mais comum usado em scripts para Open Tibia, e sim sob Orientação à Objetos. A classe do minigame é a Maze, contida em data/movements/lib/libmaze.lua
- Os labirintos gerados são únicos, uma vez que usam de pseudo-randomismo (math.random) para serem gerados pelo computador. Todos os labirintos gerados tem solução, pois nenhum bloco do labirinto fica isolado.
- Qualquer tamanho para o labirinto é aceito, porém, quanto maior o labirinto, mais o algoritmo vai demorar para gerar o labirinto. Um labirinto 50x50 foi gerado sem travamentos em minha máquina, com estas especificações:
Sistema Operacional: GNU/Linux Ubuntu 10.10 - Maverick Meerkat
Processador: Intel® Pentium® Dual CPU E2180 @ 2.0 GHz
Memória RAM: 2.0 GB
- O código está todo comentado e foi previamente testado para evitar problemas.
Instalação e configuração
O script é instalado apenas copiando os conteúdos do arquivo zipado para suas respectivas pastas, e modificando o arquivo movements.xml para incluir o conteúdo do arquivo zipado.
O script requer uma leve modificação no mapa, como mostra a imagem abaixo:
O script já vem pré-configurado para o mapa acima.
A área maior é a área onde será gerado o labirinto. Na imagem acima o tamanho dela é de 15x15. Este valor já vem pré-configurado no script, na linha 46 do maze.lua.
maze:set_size(15, 15)
Esta área deve estar preenchida com o No-Logout Zone.
Os tiles destacados em verde na parte de cima devem ter a ActionID 1001 (Este valor pode ser alterado desde que o script maze.lua e o movements.xml também sejam alterados). O ActionID 1001 é o chaveamento para iniciar o labirinto.
O tile destacados em vermelho na parte de baixo deve ter a ActionID 1002 (Este valor pode ser alterado desde que o script maze.lua e o movements.xml também sejam alterados). O ActionID 1002 é o chaveamento para encerrar o labirinto.
Caso você deseje alterar algum dado, estas são as configurações necessárias:
Relocation Position: Posição para onde serão teleportados os itens e players não relacionados ao minigame quando for executada uma limpeza do labirinto.
maze:set_relocation_pos(x, y, z)
Top-Left Position: Posição superior-esquerda do primeiro tile da área do labirinto.
maze:set_topleft_pos(x, y, z)
Exit: Posição da célula do labirinto que será usada como saída. Vale lembrar que esta célula deve estar na parte inferior do labirinto (Isto pode ser alterado mudando a linha 290 do libmaze.lua) e não é contada como uma posição em SQM do Tibia, e sim como número da célula.
maze:set_exit(x, y)
Wall ID: ItemID da parede do labirinto que será gerada.
maze:set_wall(itemid)
Para adicionar uma nova entrada no labirinto, usar:
maze:add_entrance({ tile = {x=?, y=?, z=?}, init = {x=?, y=?, z=?}, exit = {x=?, y=?, z=?}, })
tile: Posição do tile com o ActionID 1001, usado para ativar o labirinto.
init: Posição que o player daquele tile será teleportado assim que o minigame começar.
exit: Posição que o player daquele tile será teleportado assim que o minigame acabar.
Bugs conhecidos e suas soluções:
Estes bugs não podem ser corrigidos via script. Abaixo seguem suas descrições e soluções.
- Se o player fizer logout dentro da área do labirinto, ao fazer login o mesmo não conseguirá sair de dentro. Para isso, marque toda a área do labirinto como No-Logout Zone.
- Se o mundo for No-PVP, os players podem passar por cima de outros (update). Caso um player já esteja sobre um dos tiles com ActionID, se outro player entrar em cima dele, o tile receberá um ID não-correspondente. Para solucionar o problema existe um patch de correção do The Forgotten Server que impossibilita a passagem de um player sobre outro em mundos No-PVP onde os tiles possuem ActionID.
Arquivos
/data/movements/movements.xml
<!-- Maze minigame --> <movevent type="StepIn" actionid="1001;1002" event="script" value="maze.lua"/> <movevent type="StepOut" actionid="1001;1002" event="script" value="maze.lua"/>
/data/movements/lib/libmaze.lua
-- libmaze.lua -- This file is part of Maze minigame -- -- Copyright (C) 2011 Skyen Hasus -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see <http://www.gnu.org/licenses/>. -- Por questões de compatibilidade com diversas distribuições, mantive esta -- biblioteca na mesma pasta do script. -- Criação da classe Maze (Orientação à Objetos) Maze = {} -- Método útil para comparar posições function Maze:compare_pos(pos1, pos2) return pos1.x == pos2.x and pos1.y == pos2.y and pos1.z == pos2.z end -- Inicializa uma nova instância de Maze (Orientação à Objetos) function Maze:new(obj) local obj = obj or {} if not obj.entrances then obj.entrances = {} end if not obj.players then obj.players = {} end return setmetatable(obj, {__index=self}) end -- Métodos de configuração function Maze:add_entrance(template) table.insert(self.entrances, template) end function Maze:add_player(entrance, cid) self.players[cid] = entrance end function Maze:rem_players() for cid, _ in pairs(self.players) do self.players[cid] = nil end end function Maze:set_relocation_pos(x, y, z) self.relpos = {x=x, y=y, z=z} end function Maze:set_topleft_pos(x, y, z) self.topleft = {x=x, y=y, z=z} end function Maze:set_exit(x, y) self.exit = {x=x, y=y} end function Maze:set_wall(id) self.wall_id = id end function Maze:set_size(width, height) self.size = {width=width, height=height} end function Maze:set_area(template) self.area = template end -- Métodos para ler a configuração function Maze:get_entrances() return self.entrances end function Maze:get_entrance(pos) for _, entrance in ipairs(maze:get_entrances()) do if self:compare_pos(pos, entrance.tile) then return entrance end end return false end function Maze:get_players() return self.players end function Maze:get_relocation_pos() return self.relpos end function Maze:get_topleft_pos() return self.topleft end function Maze:get_exit() return self.exit end function Maze:get_wall() return self.wall_id end function Maze:get_size() return self.size end function Maze:get_area() return self.area end -- Métodos úteis para o desenvolvimento do script function Maze:get_top_left(pos) return {x=pos.x-1, y=pos.y-1, z=pos.z, stackpos=pos.stackpos} end function Maze:get_top_right(pos) return {x=pos.x+1, y=pos.y-1, z=pos.z, stackpos=pos.stackpos} end function Maze:get_bottom_left(pos) return {x=pos.x-1, y=pos.y+1, z=pos.z, stackpos=pos.stackpos} end function Maze:get_bottom_right(pos) return {x=pos.x+1, y=pos.y+1, z=pos.z, stackpos=pos.stackpos} end function Maze:get_top(pos) return {x=pos.x, y=pos.y-1, z=pos.z, stackpos=pos.stackpos} end function Maze:get_bottom(pos) return {x=pos.x, y=pos.y+1, z=pos.z, stackpos=pos.stackpos} end function Maze:get_left(pos) return {x=pos.x-1, y=pos.y, z=pos.z, stackpos=pos.stackpos} end function Maze:get_right(pos) return {x=pos.x+1, y=pos.y, z=pos.z, stackpos=pos.stackpos} end -- Método para transformar uma posição do Tibia em células do labirinto function Maze:to_maze(value) return (value-1)/2 end -- Método que verifica se todos os players estão em suas posição e se não -- há nenhum player dentro do labirinto function Maze:is_available() local start = self:get_topleft_pos() local size = self:get_size() for _, entrance in ipairs(self:get_entrances()) do local player = getTopCreature(entrance.tile) if player.uid == 0 or not isPlayer(player.uid) then return false end end for x = start.x, start.x+size.width do for y = start.y, start.y+size.height do local player = getTopCreature({x=x, y=y, z=start.z}) if isCreature(player.uid) then return false end end end return true end -- Método para pegar uma lista de células vizinhas function Maze:get_neighbors(x, y) local neighbors = { {x=x, y=y-1}, {x=x, y=y+1}, {x=x-1, y=y}, {x=x+1, y=y}, } return neighbors end -- Método para determinar se uma posição está dentro de uma área function Maze:is_valid(x, y) local size = self:get_size() local width = self:to_maze(size.width) local height = self:to_maze(size.height) return x >= 1 and x <= width and y >= 1 and y <= height end -- Método para geração de uma área limpa para o labirinto function Maze:generate_area() local size = self:get_size() -- Verifica se a área do labirinto é valida if not ((size.width-1)%2 == 0 and (size.height-1)%2 == 0) then print("Warning: Invalid size for maze area generation!") return false end -- Gera a área e suas respectivas células limpas local area = {} for x = 1, self:to_maze(size.width) do area[x] = {} for y = 1, self:to_maze(size.height) do -- Gera uma nova célula limpa area[x][y] = { visited = false, top = true, bottom = true, left = true, right = true, } end end self:set_area(area) return true end -- Método recursivo para caminhar pela área do labirinto, gerando os caminhos function Maze:handle_cell(x, y) -- Pega a área e tamanho do labirinto e copia em váriaveis locais -- para otimização do código local area = self:get_area() local size = self:get_size() -- Antes de mais nada, marca a célula atual como visitada area[x][y].visited = true; -- Pega uma lista de células vizinhas local nb = self:get_neighbors(x, y) local used = {false, false, false, false} -- Converte o tamanho do labirinto de número de tiles -- para número de células local width = self:to_maze(size.width) local height = self:to_maze(size.height) -- Enquanto a célula atual tiver vizinhas não visitadas, inicie um novo -- caminho pelo labirinto, partindo de uma célula aleatória while not (used[1] and used[2] and used[3] and used[4]) do local c = math.random(1, 4) used[c] = true -- Verifica se a célula vizinha escolhida é válida e ainda não -- foi visitada if self:is_valid(nb[c].x, nb[c].y) and not area[nb[c].x][nb[c].y].visited then -- Abre as paredes entre as duas células if c == 1 then area[x][y].top = false area[nb[c].x][nb[c].y].bottom = false elseif c == 2 then area[x][y].bottom = false area[nb[c].x][nb[c].y].top = false elseif c == 3 then area[x][y].left = false area[nb[c].x][nb[c].y].right = false elseif c == 4 then area[x][y].right = false area[nb[c].x][nb[c].y].left = false end -- Salva as modificações na área e faz a recursão self:set_area(area) self:handle_cell(nb[c].x, nb[c].y) end end -- No fim de tudo, salva a área do labirinto gerado self:set_area(area) end -- Gera um novo labirinto function Maze:generate_maze() local size = self:get_size() local centerx = math.floor(math.ceil(size.width/2)/2) local centery = math.floor(math.ceil(size.height/2)/2) self:handle_cell(centerx, centery); local area = self:get_area() local exit = self:get_exit() area[exit.x][exit.y].bottom = false self:set_area(area) end -- Método útil para limpar a área do labirinto dentro do jogo function Maze:clean(callback, wall, winner) winner = winner or false local start = self:get_topleft_pos() local size = self:get_size() -- Faz uma varredura pela área for x = start.x, start.x + size.width-1 do for y = start.y, start.y + size.height-1 do local pos = {x=x, y=y, z=start.z, stackpos=1} -- Enquanto existirem itens na posição, continue -- a enviá-los para a função callback while getThingFromPos(pos, false).uid ~= 0 do local thing = getThingFromPos(pos, false) if wall and thing.itemid == self:get_wall() then doRemoveThing(thing.uid) else callback(self, thing, winner) end pos.stackpos = pos.stackpos + 1 end end end end -- Método para aplicar uma área de labirinto gerada em uma área do Tibia -- Mesmo que uma área de labirinto seja criada e gerada, nada aparecerá no -- Tibia caso se esta função não for chamada function Maze:apply_maze() local pos = self:get_topleft_pos() local wall = self:get_wall() local area = self:get_area() local size = self:get_size() -- Faz uma varredura pela área for x = 1, self:to_maze(size.width) do for y = 1, self:to_maze(size.height) do -- Pega a célula da posição atual local cell = area[x][y] local rawpos = {x=pos.x+x*2-1, y=pos.y+y*2-1, z=pos.z} rawpos.stackpos = 1 -- Cria as paredes fixas (que não precisam ser geradas) -- em seus respectivos lugares local cpos = self:get_top_left(rawpos) if getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end local cpos = self:get_top_right(rawpos) if getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end local cpos = self:get_bottom_left(rawpos) if getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end local cpos = self:get_bottom_right(rawpos) if getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end -- Cria as paredes geradas em seus respectivos lugares local cpos = self:get_top(rawpos) if cell.top and getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end local cpos = self:get_bottom(rawpos) if cell.bottom and getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end local cpos = self:get_left(rawpos) if cell.left and getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end local cpos = self:get_right(rawpos) if cell.right and getThingFromPos(cpos, false).uid == 0 then doCreateItem(wall, cpos) end end end end
/data/movements/scripts/maze.lua
-- maze.lua -- This file is part of Maze minigame -- -- Copyright (C) 2011 Skyen Hasus -- -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU General Public License as published by -- the Free Software Foundation, either version 3 of the License, or -- at your option) any later version. -- -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU General Public License for more details. -- -- You should have received a copy of the GNU General Public License -- along with this program. If not, see <http://www.gnu.org/licenses/>. -- Ao carregar o script, carregar também a biblioteca dofile(getDataDir() .. "movements/lib/libmaze.lua") -- ----------------------------------------------------------------------------- -- Criação e configuração de um novo labirinto: -- ----------------------------------------------------------------------------- local maze = Maze:new() maze:add_entrance({ tile = {x=1000, y=1016, z=7}, init = {x=1000, y=1018, z=7}, exit = {x=1000, y=1014, z=7}, }) maze:add_entrance({ tile = {x=1012, y=1016, z=7}, init = {x=1012, y=1018, z=7}, exit = {x=1012, y=1014, z=7}, }) maze:set_relocation_pos(1006, 1012, 7) maze:set_topleft_pos(999, 1017, 7) maze:set_exit(4, 7) maze:set_wall(1483) maze:set_size(15, 15) -- ----------------------------------------------------------------------------- -- Gera uma nova semente aleatória, garantindo que cada labirinto seja único math.randomseed(os.time()) -- Função callback para limpeza do labirinto local function clear_thing(maze, thing, winner) local entrances = maze:get_entrances() local players = maze:get_players() if isPlayer(thing.uid) and players[thing.uid] then if winner then doPlayerSendTextMessage(thing.uid, 25, getPlayerName(winner) .. " completed the maze!") end doTeleportThing(thing.uid, entrances[players[thing.uid]].exit) else doTeleportThing(thing.uid, maze:get_relocation_pos()) end end -- Função callback principal do evento StepIn function onStepIn(cid, item, position) if not isPlayer(cid) then return true end doTransformItem(item.uid, item.itemid+1) if item.actionid == 1001 and maze:is_available() then if not maze:generate_area() then return false end maze:generate_maze() maze:clean(clear_thing, true) maze:apply_maze() for id, entrance in ipairs(maze:get_entrances()) do local player = getTopCreature(entrance.tile) doTeleportThing(player.uid, entrance.init) maze:add_player(id, player.uid) end elseif item.actionid == 1002 then local entrances = maze:get_entrances() local players = maze:get_players() doTeleportThing(cid, entrances[players[cid]].exit) doPlayerSendTextMessage(cid, 25, "You completed the maze!") maze:clean(clear_thing, false, cid) maze:rem_players() end return true end -- Função callback principal do evento StepOut function onStepOut(cid, item) if not isPlayer(cid) then return true end doTransformItem(item.uid, item.itemid-1) return true end
------
Isso aew, obrigado a todos que participou do concurso e este foi o grande vencedor.