Skip to content
Snippets Groups Projects
Commit 6ab55cf6 authored by DarkGod's avatar DarkGod
Browse files

tilemaps support tunneling

parent 89bf7f27
No related branches found
No related tags found
No related merge requests found
......@@ -19,6 +19,7 @@
require "engine.class"
local Map = require "engine.Map"
local BaseGenerator = require "engine.Generator"
local RoomsLoader = require "engine.generator.map.RoomsLoader"
require "engine.Generator"
......@@ -32,8 +33,8 @@ function _M:init(zone, map, level, data)
self.spots = {}
self.mapsize = {self.map.w, self.map.h, w=self.map.w, h=self.map.h}
self.post_gen = {}
self.rooms_positions = {}
self.rooms_registers = {}
self.maps_positions = {}
self.maps_registers = {}
RoomsLoader.init(self, data)
end
......@@ -106,23 +107,15 @@ function _M:generate(lev, old_lev)
local data = self:custom(lev, old_lev)
if self.force_regen then return self:generate(lev, old_lev) end
for id, room in pairs(self.rooms_registers) do
local pos = self.rooms_positions[id]
self:roomPlace(room, id, pos.x - 1, pos.y - 1)
data:applyArea(pos, pos + data:point(room.w - 1, room.h - 1), function(x, y, symb)
if self.map.room_map[x-1][y-1].can_open then
return symb
else
return "⛝" -- Carve out the interrior and all non openings with a special symbol to mark them as needing to NOT be overridden
end
end)
for id, map in pairs(self.maps_registers) do
local pos = self.maps_positions[id]
self.map:import(map, pos.x - 1, pos.y - 1)
end
data:printResult()
data = data:getResult(true)
for i = 0, self.map.w - 1 do
for j = 0, self.map.h - 1 do
if data[j+1][i+1] ~= "" then
if data[j+1][i+1] ~= "" then
self.map(i, j, Map.TERRAIN, self:resolve(data[j+1][i+1] or '#'))
end
end
......@@ -151,6 +144,28 @@ function _M:postGen(fct)
self.post_gen[#self.post_gen+1] = fct
end
function _M:makeTemporaryMap(map_w, map_h, fct)
local old_map = self.level.map
local old_game_level = game.level
game.level = self.level
local tmp_map = Map.new(map_w, map_h)
self.level.map = tmp_map
self.map = tmp_map
local new_data = table.clone(self.data, true)
-- Fake a generator call to init tmp_map.room_map
local ngen = BaseGenerator.new({}, tmp_map, {}, {})
fct(tmp_map, new_data)
game.level = old_game_level
self.map = old_map
self.level.map = old_map
return tmp_map
end
--- Create the stairs inside the level
function _M:makeStairsInside(lev, old_lev, spots)
-- Put down stairs
......
......@@ -25,6 +25,8 @@ local RoomsLoader = require "engine.generator.map.RoomsLoader"
-- @classmod engine.tilemaps.Heightmap
module(..., package.seeall, class.inherit(Tilemap))
local RoomInstance = class.inherit(Tilemap){}
function _M:init(mapscript, rooms_list)
self.mapscript = mapscript
if type(rooms_list) == "string" then rooms_list = {rooms_list} end
......@@ -47,22 +49,84 @@ function _M:generateRoom(temp_symbol, account_for_border)
return nil
end
self.room_next_id = self.room_next_id + 1
if account_for_border == nil then account_for_border = false end
local tm = Tilemap.new({room.w - (account_for_border and 2 or 0), room.h - (account_for_border and 2 or 0)}, temp_symbol or '⍓')
local tm = RoomInstance.new({room.w - (account_for_border and 2 or 0), room.h - (account_for_border and 2 or 0)}, temp_symbol or '⍓')
room.temp_symbol = temp_symbol
mapscript.rooms_registers[id] = room
mapscript.rooms_positions[id] = account_for_border and self:point(0, 0) or self:point(1, 1)
tm.mergedAt = function(self, x, y)
mapscript.rooms_positions[id] = mapscript.rooms_positions[id] + self:point(x - 1, y - 1)
local map = mapscript:makeTemporaryMap(room.w, room.h, function(map)
mapscript:roomPlace(room, id, 0, 0)
end)
mapscript.maps_registers[id] = map
mapscript.maps_positions[id] = account_for_border and self:point(0, 0) or self:point(1, 1)
local exits = { openables={}, doors={} }
local function checkexits(i, j, dir)
if map.room_map[i][j].can_open then
exits.openables[#exits.openables+1] = self:point(i+1, j+1)
end
if map(i, j, map.TERRAIN) and map(i, j, map.TERRAIN).door_opened then
-- Doors "position" is the actual tile right beside it, not the door itself as we dont want to delete the door
local dx, dy = util.coordAddDir(i+1, j+1, dir)
exits.doors[#exits.doors+1] = self:point(dx, dy)
end
end
tm.mapscript = mapscript
tm.exits = exits
tm.room_id = id
print('------------------------------exits')
table.print(room.exits)
print('------------------------------')
for i = 0, map.w - 1 do
checkexits(i, 0, 8)
checkexits(i, map.h - 1, 2)
end
for j = 0, map.h - 1 do
checkexits(0, j, 4)
checkexits(map.w - 1, j, 6)
end
self.room_next_id = self.room_next_id + 1
return tm
end
function RoomInstance:mergedAt(x, y)
Tilemap.mergedAt(self, x, y)
local d = self:point(x - 1, y - 1)
self.mapscript.maps_positions[self.room_id] = self.mapscript.maps_positions[self.room_id] + d
for _, open in pairs(self.exits.openables) do open.x, open.y = open.x + d.x, open.y + d.y end
for _, door in pairs(self.exits.doors) do door.x, door.y = door.x + d.x, door.y + d.y end
end
function RoomInstance:findClosestExit(pos, kind)
local cur_dist = 9999999
local cur_pos = nil
local cur_kind = nil
if not kind or kind == "openable" then
for _, open in pairs(self.exits.openables) do
local dist = core.fov.distance(pos.x, pos.y, open.x, open.y)
if dist < cur_dist then
cur_dist = dist
cur_pos = open
cur_kind = "open"
end
end
end
if not kind or kind == "door" then
for _, door in pairs(self.exits.doors) do
local dist = core.fov.distance(pos.x, pos.y, door.x, door.y)
if dist < cur_dist then
cur_dist = dist
cur_pos = door
cur_kind = "door"
end
end
end
return cur_pos, cur_kind, cur_dist
end
function _M:flip(axis) error("Cannot use :flip() on a Room tilemap") end
function _M:rotate(angle) error("Cannot use :rotate() on a Room tilemap") end
function _M:scale(sx, sy) error("Cannot use :scale() on a Room tilemap") end
......@@ -52,10 +52,15 @@ end
local point_meta = {
__add = function(a, b)
return _M:point(a.x + b.x, a.y + b.y)
if type(b) == "number" then return _M:point(a.x + b, a.y + b)
else return _M:point(a.x + b.x, a.y + b.y) end
end,
__sub = function(a, b)
return _M:point(a.x + b.x, a.y + b.y)
if type(b) == "number" then return _M:point(a.x - b, a.y - b)
else return _M:point(a.x - b.x, a.y - b.y) end
end,
__eq = function(a, b)
return a.x == b.x and a.y == b.y
end,
}
--- Make a point data, can be added
......@@ -79,6 +84,23 @@ function _M:fillAll(fill_with, empty_char)
end
end
--- Check if the given coords are on the map
function _M:isBound(pos, y)
if y then pos = self:point(pos, y) end
if pos.x >= 1 and pos.x <= self.data_w and pos.y >=1 and pos.y <= self.data_h then
return true
else
return false
end
end
--- Draw a single tile
function _M:put(pos, char)
if self:isBound(pos) then
self.data[pos.y][pos.x] = char
end
end
--- Flip the map
function _M:flip(axis)
local ndata = self:makeData(self.data_w, self.data_h, '')
......@@ -568,8 +590,13 @@ function _M:merge(x, y, tm, char_order, empty_char)
x = math.floor(x)
y = math.floor(y)
char_order = table.reverse(char_order or {})
empty_char = empty_char or ' '
empty_char = empty_char or {' '}
if type(empty_char) == "string" then empty_char = {empty_char} end
empty_char = table.reverse(empty_char)
if not tm.data then return end
for i = 1, tm.data_w do
......@@ -577,7 +604,7 @@ function _M:merge(x, y, tm, char_order, empty_char)
local si, sj = i + x - 1, j + y - 1
if si >= 1 and si <= self.data_w and sj >= 1 and sj <= self.data_h then
local c = tm.data[j][i]
if c ~= empty_char then
if not empty_char[c] then
local sc = self.data[sj][si]
local sc_o = char_order[sc] or 0
local c_o = char_order[c] or 0
......@@ -595,3 +622,332 @@ end
--- Does nothing, meant to be superloaded
function _M:mergedAt(x, y)
end
------------------------------------------------------------------------------
-- Simple tunneling
------------------------------------------------------------------------------
function _M:initTunneling()
if not self.tunnels_map then
self.tunnels_map = self:makeData(self.data_w, self.data_h, false)
self.tunnels_next_id = 'a'
end
local id = self.tunnels_next_id
self.tunnels_next_id = string.char(string.byte(self.tunnels_next_id) + 1)
return id
end
--- Random tunnel dir (no diagonals)
function _M:tunnelRandDir(sx, sy)
local dirs = util.primaryDirs() --{4,6,8,2}
return util.dirToCoord(dirs[rng.range(1, #dirs)], sx, sy)
end
--- Find the direction in which to tunnel (no diagonals)
function _M:tunnelDir(x1, y1, x2, y2)
-- HEX TODO ?
local xdir = (x1 == x2) and 0 or ((x1 < x2) and 1 or -1)
local ydir = (y1 == y2) and 0 or ((y1 < y2) and 1 or -1)
if xdir ~= 0 and ydir ~= 0 then
if rng.percent(50) then xdir = 0
else ydir = 0
end
end
return xdir, ydir
end
--- Marks a tunnel as a tunnel and the space behind it
function _M:tunnelMark(x, y, xdir, ydir, id)
x, y = x - xdir, y - ydir
local dir = util.coordToDir(xdir, ydir, x, y)
local sides = util.dirSides(dir, x, y)
local mark_dirs = {dir, sides.left, sides.right}
for i, d in ipairs(mark_dirs) do
local xd, yd = util.dirToCoord(d, x, y)
if self:isBound(x+xd, y+yd) and not self.tunnels_map[y+yd][x+xd] then
self.tunnels_map[y+yd][x+xd] = id
print("mark tunnel", x+xd, y+yd , id)
end
end
if not self.tunnels_map[y][x] then
self.tunnels_map[y][x] = id
print("mark tunnel", x, y , id)
end
end
--- Tunnel between two points
-- @param x1, y1 starting coordinates
-- @param x2, y2 ending coordinates
-- @param id tunnel id
-- @param virtual set true to mark the tunnel without changing terrain
function _M:tunnel(from, to, char, tunnel_through, tunnel_avoid, config, virtual)
local x1, y1, x2, y2 = from.x, from.y, to.x, to.y
config = config or {}
config.tunnel_change = config.tunnel_change or 60
config.tunnel_random = config.tunnel_random or 7
char = char or '.'
tunnel_through = table.reverse(tunnel_through or {'ALL'})
tunnel_avoid = table.reverse(tunnel_avoid or {'⍓'})
local id = self:initTunneling()
if x1 == x2 and y1 == y2 then return end
-- Disable the many prints of tunnelling
-- local print = function()end
local xdir, ydir = self:tunnelDir(x1, y1, x2, y2)
print("tunneling from",x1, y1, "to", x2, y2, "initial dir", xdir, ydir)
local startx, starty = x1, y1
local tun = {}
local tries = 2000
local no_move_tries = 0
while tries > 0 do
if rng.percent(config.tunnel_change) then
if rng.percent(config.tunnel_random) then xdir, ydir = self:tunnelRandDir(x1, x2)
else xdir, ydir = self:tunnelDir(x1, y1, x2, y2)
end
end
local nx, ny = x1 + xdir, y1 + ydir
while true do
if self:isBound(nx, ny) then break end
if rng.percent(config.tunnel_random) then xdir, ydir = self:tunnelRandDir(nx, ny)
else xdir, ydir = self:tunnelDir(x1, y1, x2, y2)
end
nx, ny = x1 + xdir, y1 + ydir
end
print(feat, "try pos", nx, ny, "dir", util.coordToDir(xdir, ydir, nx, ny))
local nc = self.data[ny][nx]
if tunnel_avoid[nc] then
if nx == from.x and ny == from.y then
tun[#tun+1] = {nx,ny}
x1, y1 = nx, ny
print(feat, "accept avoid (start)", nc)
elseif nx == to.x and ny == to.y then
tun[#tun+1] = {nx,ny}
x1, y1 = nx, ny
print(feat, "accept avoid (end)", nc)
else
print(feat, "reject avoid", nc)
if nx == x2 and ny == y2 then -- stop if next to special target
x1, y1 = nx, ny
print(feat, "end adjacent to special target")
end
end
-- elseif nc.can_open ~= nil then
-- if nc.can_open then
-- print(feat, "tunnel crossing can_open", nx,ny)
-- for _, coord in pairs(util.adjacentCoords(nx, ny)) do
-- if self:isBound(coord[1], coord[2]) then
-- self.map.room_map[coord[1]][coord[2]].can_open = false
-- print(feat, "forbidding crossing at ", coord[1], coord[2])
-- end
-- end
-- tun[#tun+1] = {nx,ny,true}
-- x1, y1 = nx, ny
-- print(feat, "accept can_open")
-- else
-- print(feat, "reject can_open")
-- end
elseif self.tunnels_map[ny][nx] then
if no_move_tries >= 15 then
tun[#tun+1] = {nx,ny}
x1, y1 = nx, ny
print(feat, "accept tunnel", nc, id)
else
print(feat, "reject tunnel", nc, id)
end
elseif tunnel_through[nc] or tunnel_through.ALL then
tun[#tun+1] = {nx,ny}
x1, y1 = nx, ny
print(feat, "accept normal", nc)
else
print(feat, "reject normal", nc)
end
if x1 == nx and y1 == ny then
self:tunnelMark(x1, y1, xdir, ydir, id)
no_move_tries = 0
else
no_move_tries = no_move_tries + 1
end
if x1 == x2 and y1 == y2 then print(feat, "done") break end
tries = tries - 1
end
local doors = {}
self.possible_doors = self.possible_doors or {}
for _, t in ipairs(tun) do
local nx, ny = t[1], t[2]
if t[3] and self.data.door then self.possible_doors[#self.possible_doors+1] = t end
if not t[4] and not virtual then
print("=======TUNN", nx, ny)
-- self.map(nx, ny, Map.TERRAIN, self:resolve('=') or self:resolve('.') or self:resolve('floor'))
self:put(self:point(nx, ny), char)
end
end
end
------------------------------------------------------------------------------
-- A* tunneling
------------------------------------------------------------------------------
--- The default heuristic for A*, tries to come close to the straight path
-- @int sx
-- @int sy
-- @int cx
-- @int cy
-- @int tx
-- @int ty
local function heuristicCloserPath(sx, sy, cx, cy, tx, ty)
local h
-- Chebyshev distance
h = math.max(math.abs(tx - cx), math.abs(ty - cy))
-- tie-breaker rule for straighter paths
local dx1 = cx - tx
local dy1 = cy - ty
local dx2 = sx - tx
local dy2 = sy - ty
return h + 0.01*math.abs(dx1*dy2 - dx2*dy1)
end
--- A simple heuristic for A*, using distance
-- @int sx
-- @int sy
-- @int cx
-- @int cy
-- @int tx
-- @int ty
local function heuristicDistance(sx, sy, cx, cy, tx, ty)
return core.fov.distance(cx, cy, tx, ty)
end
--- Converts x & y into a single value
-- @see astarToDouble
-- @int x
-- @int y
function _M:astarToSingle(x, y)
return x + y * self.data_w
end
--- Converts a single value back into x & y
-- @see astarToSingle
-- @int c
function _M:astarToDouble(c)
local y = math.floor(c / self.data_w)
return c - y * self.data_w, y
end
--- Create Path
-- @param came_from
-- @param cur
function _M:astarCreatePath(came_from, cur, id, char)
if not came_from[cur] then return end
local rpath, path = {}, {}
while came_from[cur] do
local x, y = self:astarToDouble(cur)
rpath[#rpath+1] = self:point(x, y)
self.data[y][x] = char
self.tunnels_map[y][x] = id
cur = came_from[cur]
end
for i = #rpath, 1, -1 do path[#path+1] = rpath[i] end
return path
end
--- Compute path from sx/sy to tx/ty
function _M:tunnelAStar(from, to, char, tunnel_through, tunnel_avoid, config)
local sx, sy, tx, ty = from.x, from.y, to.x, to.y
config = config or {}
char = char or '.'
tunnel_through = table.reverse(tunnel_through or {'ALL'})
tunnel_avoid = table.reverse(tunnel_avoid or {'⍓'})
local id = self:initTunneling()
if sx == tx and sy == ty then return end
config.erraticness = config.erraticness or 9
config.tunnel_avoidance = config.tunnel_avoidance or 20
if type(config.forbid_diagonals) == "nil" then config.forbid_diagonals = true end
local heur = heuristic or heuristicCloserPath
local w, h = self.data_w, self.data_h
local start = self:astarToSingle(sx, sy)
local stop = self:astarToSingle(tx, ty)
local open = {[start]=true}
local closed = {}
local g_score = {[start] = 0}
local h_score = {[start] = heur(sx, sy, sx, sy, tx, ty)}
local f_score = {[start] = heur(sx, sy, sx, sy, tx, ty)}
local came_from = {}
if not self:isBound(sx, sy) or not self:isBound(tx, ty) then
print("Astar fail: source/destination unreachable")
return nil
end
local checkPos = function(node, nx, ny)
local npos = self:point(nx, ny)
local nnode = self:astarToSingle(nx, ny)
if self:isBound(nx, ny) then print("---Check", nx, ny,':', self.data[ny][nx], ":", not closed[nnode], self:isBound(nx, ny), (tunnel_through.ALL or tunnel_through[self.data[ny][nx]]), (not tunnel_avoid[self.data[ny][nx]]), (not config.add_check or config.add_check(nx, ny))) end
if not closed[nnode] and self:isBound(nx, ny) and (
(
npos == from or npos == to -- Always allow on start & stop
)
or
(
(tunnel_through.ALL or tunnel_through[self.data[ny][nx]]) and -- Allowed to tunnel in
(not tunnel_avoid[self.data[ny][nx]]) and -- Avoid tunneling in
(not config.add_check or config.add_check(nx, ny)) -- Extra checks
)
) then
local nc = self.data[ny][nx]
local score = 1
score = score + rng.float(0, config.erraticness)
if self.tunnels_map[ny][nx] then score = score + config.tunnel_avoidance end
local tent_g_score = g_score[node] + score -- we can adjust here for difficult passable terrain
local tent_is_better = false
if not open[nnode] then open[nnode] = true; tent_is_better = true
elseif tent_g_score < g_score[nnode] then tent_is_better = true
end
if tent_is_better then
came_from[nnode] = node
g_score[nnode] = tent_g_score
h_score[nnode] = heur(sx, sy, tx, ty, nx, ny)
f_score[nnode] = g_score[nnode] + h_score[nnode]
end
end
end
while next(open) do
-- Find lowest of f_score
local node, lowest = nil, 999999999999999
local n, _ = next(open)
while n do
if f_score[n] < lowest then node = n; lowest = f_score[n] end
n, _ = next(open, n)
end
if node == stop then return self:astarCreatePath(came_from, stop, id, char) end
open[node] = nil
closed[node] = true
local x, y = self:astarToDouble(node)
-- Check sides
for _, coord in pairs(util.adjacentCoords(x, y, forbid_diagonals)) do
checkPos(node, coord[1], coord[2])
end
end
end
......@@ -92,11 +92,6 @@ tm:fillAll()
-- Elimitate the rest
if tm:eliminateByFloodfill{'#', 'T'} < 400 then return self:regenerate() end
self.data.greater_vaults_list = {"32-chambers"}
local proom = Rooms.new(self, "greater_vault"):generateRoom()
tm:carveArea('#', tm:point(28, 1), tm:point(28+proom.data_w+4, 2+proom.data_h+4))
tm:merge(30, 2, proom)
tm:printResult()
-- print('---==============---')
......
......@@ -19,14 +19,18 @@
-- Merge them all
local tm = Tilemap.new(self.mapsize, '#')
tm:carveArea(';', tm:point(1, 1), tm:point(4, 4))
tm:carveArea(';', tm:point(1, 1), tm:point(4, 10))
tm:carveArea('T', tm:point(15, 3), tm:point(15, 16))
tm:carveArea(';', tm:point(30, 1), tm:point(35, 10))
self.data.greater_vaults_list = {"32-chambers"}
local proom = Rooms.new(self, "greater_vault"):generateRoom()
tm:merge(12, 5, proom)
-- tm:tunnel(tm:point(1, 10), tm:point(36, 10), ';', nil, {'T'}, {tunnel_change=60, tunnel_random=5})
-- tm:tunnelAStar(tm:point(1, 10), tm:point(36, 1), '=', nil, {'T'}, {})
tm:tunnelAStar(tm:point(1, 1), tm:point(36, 10), '.', nil, {'T'}, {})
-- tm:tunnelAStar(tm:point(1, 30), tm:point(36, 30), '.', nil, {}, {})
tm:printResult()
-- print('---==============---')
-- local noise = Noise.new(nil, 0.5, 2, 3, 6):make(80, 50, {'T', 'T', '=', '=', '=', ';', ';'})
-- noise:printResult()
......
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009 - 2018 Nicolas Casalini
--
-- 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/>.
--
-- Nicolas Casalini "DarkGod"
-- darkgod@te4.org
-- Merge them all
local tm = Tilemap.new(self.mapsize, '#')
tm:carveArea(';', tm:point(1, 1), tm:point(4, 4))
self.data.greater_vaults_list = {"32-chambers"}
local proom = Rooms.new(self, "oval"):generateRoom()
tm:merge(12, 5, proom)
local pos, kind = proom:findClosestExit(tm:point(1, 1))
if pos then
tm:tunnelAStar(tm:point(1, 1), pos, '.', nil, nil, {erraticness=9})
tm:tunnelAStar(tm:point(1, 4), tm:point(50, 10), ';', nil, nil, {erraticness=9})
-- if kind == "open" then tm:put(pos, '+') end
end
tm:printResult()
print("----------POS")
table.print(pos)
print("----------AKLDZJLD")
table.print(proom.exits)
print("----------")
-- print('---==============---')
-- local noise = Noise.new(nil, 0.5, 2, 3, 6):make(80, 50, {'T', 'T', '=', '=', '=', ';', ';'})
-- noise:printResult()
-- print('---==============---')
-- print('---==============---')
-- local pond = Heightmap.new(1.9, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(30, 30, {';', 'T', '=', '=', ';'})
-- pond:printResult()
-- print('---==============---')
-- print('---==============---')
-- local maze = Maze.new():makeSimple(31, 31, '.', {'#','T'}, true)
-- maze:printResult()
-- print('---==============---')
-- DGDGDGDG: make at least Tilemap handlers for BSP, roomer (single room), roomers and correctly handle up/down stairs
return tm
......@@ -42,7 +42,7 @@ return {
['_'] = "FLOOR", ['O'] = "WALL",
[';'] = "GRASS", ['T'] = "TREE",
['='] = "DEEP_WATER",
mapscript = "!testroom",
mapscript = "!testroom2",
-- mapscript = "!inner_outer",
--]]
--[[
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment