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

mapscript update with binpacking algorithm and more utilities

parent 137bbb0c
No related branches found
No related tags found
No related merge requests found
Pipeline #
......@@ -123,7 +123,7 @@ function _M:custom(lev, old_lev)
if ret then
return ret
elseif self.force_regen then
elseif self.force_regen or (self.force_redo and next(self.maps_registers)) then
return nil
elseif self.force_redo then
return self:custom(lev, old_lev)
......@@ -144,7 +144,7 @@ function _M:generate(lev, old_lev)
end
-- We do it AFTER importing submaps to ensure entities on them are correctly released
if self.force_regen then self.level.force_recreate = true return false end
if self.force_regen or (self.force_redo and next(self.maps_registers)) then self.level.force_recreate = true return false end
if not self.entrance_pos then self.entrance_pos = data:locateTile('<') end
if not self.exit_pos then self.exit_pos = data:locateTile('>') end
......
......@@ -165,8 +165,8 @@ function _M:mstEdges(fatten)
return edges
end
function _M:mergedAt(x, y)
Tilemap.mergedAt(self, x, y)
function _M:mergedAt(x, y, into)
Tilemap.mergedAt(self, x, y, tino)
for idx, room in ipairs(self.rooms) do
room:translate(self.merged_pos - 1)
......
......@@ -124,14 +124,21 @@ function RoomInstance:discard()
end end
end
function RoomInstance:mergedAt(x, y)
Tilemap.mergedAt(self, x, y)
function RoomInstance:mergedAt(x, y, into)
local function translate(map, x, y, into)
local d = self:point(x, 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
return true
end
Tilemap.mergedAt(self, x, y, into)
local d = self:point(x - 1, y - 1)
-- Tell the tilemap we merge into to keep translating the map positions if it is itself on_merged_at
into.on_merged_at[#into.on_merged_at+1] = translate
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
-- And translate right now too
translate(self, x, y, into)
end
function RoomInstance:findExits(pos, kind)
......
......@@ -30,12 +30,17 @@ function _M:init(size, fill_with)
self:setSize(size[1], size[2], fill_with)
end
self.merged_pos = self:point(1, 1)
self.on_merged_at = {}
end
function _M:getSize()
return self.data_w, self.data_h
end
function _M:area()
return self.data_w * self.data_h
end
function _M:setSize(w, h, fill_with)
self.data_w = math.floor(w)
self.data_h = math.floor(h)
......@@ -587,6 +592,10 @@ group_meta = {
bounds = function(g)
return _M:pointsBoundingRectangle(g.list)
end,
size = function(g)
local from, to = g:bounds()
return to - from + 1
end,
submap = function(g, map)
local Proxy = require "engine.tilemaps.Proxy"
......@@ -1116,11 +1125,16 @@ function _M:merge(x, y, tm, char_order, empty_char)
end
end
end
tm:mergedAt(x, y)
tm:mergedAt(x, y, self)
end
function _M:mergedAt(x, y)
function _M:mergedAt(x, y, into)
self.merged_pos = self.merged_pos + self:point(x, y) - 1
for _, fct in ipairs(self.on_merged_at) do
if fct(self, x, y, into) then
into.on_merged_at[#into.on_merged_at+1] = fct
end
end
end
function _M:findExits(pos, kind, exitable_chars)
......@@ -1573,3 +1587,102 @@ function _M:tunnelAStar(from, to, char, tunnel_through, tunnel_avoid, config)
end
end
end
-------------------------------------------- Binpacker
local binpack_meta
--- Make a point data, can be added
function _M:binpack(margin_x, margin_y, maps)
margin_x, margin_y = margin_x or 1, margin_y or 1
local binpack = require('binpack')
local bp = binpack(self.data_w, self.data_h)
local maps = maps or {}
local g = {into=self, maps={}, bp=bp, default_margin_x=margin_x, default_margin_y=margin_y}
for _, m in ipairs(maps) do
g.maps[m] = {
margin_x = margin_x,
margin_y = margin_y,
}
end
setmetatable(g, binpack_meta)
return g
end
binpack_meta = {
__tostring = function(self)
return ("Binpacker (%d maps)"):format(#self.maps)
end,
__index = {
add = function(self, map, margin_x, margin_y)
margin_x, margin_y = margin_x or self.default_margin_x, margin_y or self.default_margin_y
self.maps[map] = {
margin_x = margin_x,
margin_y = margin_y,
}
return self
end,
compute = function(self, sort)
self.bp:clear(self.into.data_w, self.into.data_h)
local list = {}
for map, params in pairs(self.maps) do
params.computed, params.pos = nil, nil
list[#list+1] = map
end
if sort == "random" then
table.shuffle(list)
elseif sort == false then
table.sort(list, function(a, b) return a:area() > b:area() end)
elseif sort == true then
table.sort(list, function(a, b) return a:area() < b:area() end)
elseif type(sort) == "function" then
table.sort(list, sort)
end
for _, map in ipairs(list) do
local params = self.maps[map]
local coords = self.bp:insert(map.data_w + 2 * params.margin_x, map.data_h + 2 * params.margin_y)
if coords then
params.computed = true
params.pos = _M:point(coords.x + 1 + params.margin_x, coords.y + 1 + params.margin_y)
end
end
return self
end,
resolve = function(self)
for map, params in pairs(self.maps) do
if not params.computed then
local coords = self.bp:insert(map.data_w + 2 * params.margin_x, map.data_h + 2 * params.margin_y)
if not coords then return false end
params.computed = true
params.pos = _M:point(coords.x + 1 + params.margin_x, coords.y + 1 + params.margin_y)
end
end
return true
end,
merge = function(self)
self.merged_maps = {}
for map, params in pairs(self.maps) do if params.computed then
if map.build then map:build() end
self.into:merge(params.pos, map)
self.merged_maps[#self.merged_maps+1] = map
end end
end,
hasMerged = function(map)
return self.maps[map] and self.maps[map].computed and self.maps[map].pos
end,
getMerged = function(self)
return self.merged_maps
end,
getNotMerged = function(self)
local list = {}
for map, params in pairs(self.maps) do if not params.computed then
list[#list+1] = map
end end
return list
end,
iteratorMerged = function(self)
return ipairs(self.merged_maps)
end,
},
}
......@@ -31,13 +31,13 @@ if #rooms <= 1 then return true end -- Easy !
local mstrun = MST.new()
-- Generate all possible edges
for i, room in ipairs(rooms) do
for i, room in ipairs(rooms) do if not room.do_not_connect then
local c = room:centerPoint()
for j, proom in ipairs(rooms) do if proom ~= room then
local c1, c2 = room:centerPoint(), proom:centerPoint()
mstrun:edge(room, proom, core.fov.distance(c1.x, c1.y, c2.x, c2.y))
end end
end
end end
-- Compute!
mstrun:run()
......
......@@ -23,21 +23,22 @@ local BSP = require "engine.tilemaps.BSP"
local tm = Tilemap.new(self.mapsize, '=', 1)
local bsp = BSP.new(10, 10, 2):make(50, 50, nil, '=')
local bsp = BSP.new(7, 7, 3):make(50, 50, nil, '=')
for _, room in ipairs(bsp.rooms) do
local pond = Heightmap.new(1.6, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(room.map.data_w, room.map.data_h, {' ', ' ', ';', ';', 'T', ';', ';', ';'})
local room_size = room:size()
local pond = Heightmap.new(1.6, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(room_size.x, room_size.y, {' ', ' ', ';', ';', 'T', ';', ';', ';'})
-- Ensure exit from the lake to exterrior
local pond_exit = pond:findRandomExit(pond:centerPoint(), nil, {';'})
pond:tunnelAStar(pond:centerPoint(), pond_exit, ';', {'T'}, {}, {erraticness=9})
-- If lake is big enough and we find a spot, place it
if pond:eliminateByFloodfill{'T', ' '} < 8 then return self:regenerate() end
if pond:eliminateByFloodfill{'T', ' '} < 8 then return self:redo() end
room.map:merge(1, 1, pond)
tm:merge(room:submap(tm).merged_pos, pond)
end
tm:merge(1, 1, bsp)
-- tm:merge(1, 1, bsp)
-- if tm:eliminateByFloodfill{'T','#'} < 800 then return self:regenerate() end
-- if tm:eliminateByFloodfill{'T','#'} < 800 then return self:redo() end
return tm
......@@ -22,70 +22,33 @@
local tm = Tilemap.new(self.mapsize, '#')
-- self.data.greater_vaults_list = {"32-chambers"}
local room_factory = Rooms.new(self, "random_room")
local sroom_factory = Rooms.new(self, "simple")
local rroom_factory = Rooms.new(self, "random_room")
local vault_factory = Rooms.new(self, "greater_vault")
local nb_vault = 1
local binpack = require('binpack')
local bp = binpack(tm.data_w, tm.data_h)
local nb_vault = 0
local rooms = {}
for i = 1, 5 do
local proom = (nb_vault > 0 and vault_factory or room_factory):generateRoom()
local pos = proom and tm:findRandomArea(nil, tm.data_size, proom.data_w, proom.data_h, '#', 1)
if pos then
tm:merge(pos, proom:build())
rooms[#rooms+1] = proom
local max_area = tm.data_w * tm.data_h
local cur_area = 0
while cur_area < max_area do
local proom = (nb_vault > 0 and vault_factory or (rng.percent(50) and sroom_factory or rroom_factory)):generateRoom()
if proom then
nb_vault = nb_vault - 1
cur_area = cur_area + (proom.data_w+2) * (proom.data_h+2)
if cur_area > max_area then break end
local coords = bp:insert(proom.data_w+2, proom.data_h+2)
if not coords then break end
-- tm:carveArea('.', tm:point(coords.x+2,coords.y+2), tm:point(coords.x+2,coords.y+2) + tm:point(coords.w-1, coords.h-1) - 1)
tm:merge(tm:point(coords.x+2,coords.y+2), proom:build())
rooms[#rooms+1] = proom
end
end
local up_stairs = true
-- for i = 1, 20 do
-- -- Make a little lake
-- local r = rng.range(7, 15)
-- local pond = Heightmap.new(1.6, {up_left=0, down_left=0, up_right=0, down_right=0, middle=1}):make(r, r, {' ', ' ', ';', ';', 'T', '=', '=', up_stairs and '<' or '='})
-- -- Ensure exit from the lake to exterrior
-- local pond_exit = pond:findRandomExit(pond:centerPoint(), nil, {';'})
-- pond:tunnelAStar(pond:centerPoint(), pond_exit, ';', {'T'}, {}, {erraticness=9})
-- -- If lake is big enough and we find a spot, place it
-- if pond:eliminateByFloodfill{'T', ' '} > 8 then
-- local pos = tm:findRandomArea(nil, tm.data_size, pond.data_w, pond.data_h, '#', 1)
-- if pos then
-- tm:merge(pos, pond)
-- rooms[#rooms+1] = pond
-- up_stairs = false
-- end
-- end
-- end
if not loadMapScript("lib/connect_rooms_multi", {map=tm, rooms=rooms, door_chance=60, edges_surplus=0}) then return self:redo() end
-- loadMapScript("lib/connect_rooms_multi", {map=tm, rooms=rooms})
self:setEntrance(tm:locateTile('<'))
self:setExit(rooms[#rooms]:centerPoint()) tm:put(rooms[#rooms]:centerPoint(), '>')
-- Elimitate the rest
-- if tm:eliminateByFloodfill{'#', 'T'} < 600 then return self:redo() end
--local spot = tm:point(1, 1)
--loadMapScript("lib/subvault", {map=tm, spot=spot, char="V", greater_vaults_list={"living-weapons"}})
game.log("!!! %d", #rooms)
if not loadMapScript("lib/connect_rooms_multi", {map=tm, rooms=rooms, door_chance=60, edges_surplus=3}) then return self:redo() end
tm:printResult()
-- 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
-- ToME - Tales of Maj'Eyal
-- Copyright (C) 2009 - 2019 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
-- rng.seed(2)
tm:erase('#')
-- self.data.greater_vaults_list = {"32-chambers"}
local sroom_factory = Rooms.new(self, "simple")
local rroom_factory = Rooms.new(self, "random_room")
local vault_factory = Rooms.new(self, "lesser_vault")
local bp = tm:binpack()
local nb_vault = 1
local nb_rooms = 0
while true do
local proom
if nb_rooms < 15 then proom = (nb_vault > 0 and vault_factory or (rng.percent(50) and sroom_factory or rroom_factory)):generateRoom()
else break
-- else proom = Tilemap.new({rng.range(3, 7), rng.range(3, 7)}, '#') proom.do_not_connect = true
end
if proom then
nb_rooms = nb_rooms + 1
nb_vault = nb_vault - 1
if not bp:add(proom):resolve() then break end
end
end
bp:compute("random")
bp:merge()
game.log("!!! %d + %d", #bp:getMerged(), #bp:getNotMerged())
if not loadMapScript("lib/connect_rooms_multi", {map=tm, rooms=bp:getMerged(), door_chance=60, edges_surplus=3, erraticness=1}) then return self:redo() end
tm:printResult()
return tm
......@@ -25,7 +25,7 @@ return {
decay = {300, 800},
actor_adjust_level = function(zone, level, e) return zone.base_level + e:getRankLevelAdjust() + level.level-1 + rng.range(-1,2) end,
width = 50, height = 50,
all_remembered = true,
-- all_remembered = true,
all_lited = true,
no_level_connectivity = true,
......@@ -42,12 +42,12 @@ return {
['_'] = "FLOOR", ['O'] = "WALL",
[';'] = "GRASS", ['T'] = "TREE",
['='] = "DEEP_WATER",
-- mapscript = "!rooms_test",
mapscript = "!rooms_test2",
-- greater_vaults_list = {"portal-vault"},
-- mapscript = "!bsp_islands",
-- mapscript = "!cavernous_forest",
-- mapscript = "!testroom2",
mapscript = "!inner_outer",
-- mapscript = "!inner_outer",
--]]
--[[
class = "engine.generator.map.Hexacle",
......
-- binpack.lua
--
-- Based on SharpFont BinPacker.cs (c) 2015 Michael Popoloski, licensed under MIT
-- https://github.com/MikePopoloski/SharpFont/blob/master/SharpFont/Internal/BinPacker.cs
--
-- Uses MAXRECTS method developed by Jukka Jylänki http://clb.demon.fi/files/RectangleBinPack.pdf
--
-- Does not support rotating rectangles for better fitting.
-- Does not support dynamic spritesheet sizes; you must pick a size up-front
--
-- Ported to Lua by leafi. Licensed under MIT, as that is how the original source was licensed.
-- !!! binpack_instance:insert(w,h) returns nil instead of empty rectangle if placement failed, unlike original code !!!
--
-- API EXAMPLE:
--[[
-- binpack_example.lua
local binpack_new = require('binpack')
local bp = binpack_new(2048, 2048)
local rect1 = bp:insert(32, 64)
-- (rect1 has .x, .y, .w, .h, :clone(), :contains(rect), .right, .bottom)
print('rect1:', rect1) -- rect1: {x=0,y=0,w=32,h=64}
local rect2 = bp:insert(100, 101)
print('rect2:', rect2) -- rect2: {x=0,y=64,w=100,h=101}
print('\n20 250x200 rects: '); for i = 1,20 do print(bp:insert(250, 200)) end
print('\nClearing; changing binpack instance to 1024x1024')
bp:clear(1024, 1024) -- you MUST provide w,h again
print('10 370x430 rects in 1024x1024:'); for i = 1,10 do print(bp:insert(370, 430)) end
-- you can call binpack_new(w, h) again if you want 2 binpackers simultaneously. it's not an issue.
--]]
--
-- (Another potentially interesting rect packer to port would be one of the Java ports
-- of stb_rect_pack.h. stb_rect_pack is public-domain - but is written in C - but surely
-- a Java conversion would solve most of the problems already.
-- stb_rect_pack uses Skyline Bottom-Left.)
--
--
-- Rectangle implementation... don't be afraid to delete & make binpacker use your rectangle type instead!
--
-- (api: fields: .x, .y, .w, .h, read-only: .right, .bottom, func: :clone(), :contains(another_rect))
--
local rect_mt = {}
function rect_mt.__eq(a, b)
return a.x == b.x and a.y == b.y and a.w == b.w and a.h == b.h
end
function rect_mt.__index(rect, key)
return rect_mt[key] or (rect_mt['get_' .. key] and rect_mt['get_' .. key](rect) or nil)
end
function rect_mt.__newindex(rect, key, value)
if rect_mt['get_' .. key] then error(key .. ' property/field alias is read-only. change fields instead') end
return rawset(rect, key, value)
end
function rect_mt.__tostring(self)
return '{x=' .. self.x .. ',y=' .. self.y .. ',w=' .. self.w .. ',h=' .. self.h .. '}'
end
function rect_mt.clone(self)
local nr = {x=self.x, y=self.y, w=self.w, h=self.h}
setmetatable(nr, rect_mt)
return nr
end
function rect_mt.contains(self, r)
return r.x >= self.x and r.y >= self.y and r.right <= self.right and r.bottom <= self.bottom
end
function rect_mt.empty(self)
return self.w == 0 or self.h == 0
end
function rect_mt.get_right(self)
return self.x + self.w
end
function rect_mt.get_bottom(self)
return self.y + self.h
end
function rect_mt.get_width(self) return self.w end
function rect_mt.get_height(self) return self.h end
function rect_mt.get_left(self) return self.x end
function rect_mt.get_top(self) return self.y end
local function new_rect(x, y, w, h)
local rect = {x=x or 0, y=y or 0, w=w or 0, h=h or 0}
setmetatable(rect, rect_mt)
return rect
end
--
-- BinPacker class
--
-- Lua 5.3 introduces real 64-bit integers. This probably isn't needed - who has spritesheets with lengths this big?! - but...
local binpacker_insert_prelude = (math.maxinteger and math.tointeger) and (function(self,w,h)
-- a.k.a. Lua 5.3+ path; we have the integer type
-- coerce to integers, just in case...
w = math.tointeger(math.ceil(w))
h = math.tointeger(math.ceil(h))
if not w or not h then error('binpack:insert(w,h) had either w or h not coercable to integer (Lua 5.3+ path)') end
return w, h, math.maxinteger
end) or (function(self,w,h)
-- a.k.a. Lua <= 5.2 path
-- best-effort coerce to integers. you'll be fine if your 'integers' already fit within like 52 bits(?) or something.
w = math.ceil(w)
h = math.ceil(h)
return w, h, math.huge
end)
local binpacker_funcs = {
clear = function(self, w, h)
self.freelist = {new_rect(0, 0, math.floor(w), math.floor(h))}
end,
insert = function(self, w, h)
local maxvalue
w, h, maxvalue = binpacker_insert_prelude(self, w, h)
local bestNode = new_rect()
local bestShortFit = maxvalue
local bestLongFit = maxvalue
local count = #self.freelist
for i=1,count do
-- try to place the rect
local rect = self.freelist[i]
if not (rect.w < w or rect.h < h) then
local leftoverX = math.abs(rect.w - w)
local leftoverY = math.abs(rect.h - h)
local shortFit = math.min(leftoverX, leftoverY)
local longFit = math.max(leftoverX, leftoverY)
if shortFit < bestShortFit or (shortFit == bestShortFit and longFit < bestLongFit) then
bestNode.x = rect.x
bestNode.y = rect.y
bestNode.w = w
bestNode.h = h
bestShortFit = shortFit
bestLongFit = longFit
end
end -- end if
end -- end for
-- !!! returns 'nil' for failed placement unlike empty rectangle in original C# code
if bestNode.h == 0 then return nil end
-- split out free areas into smaller ones
local i = 1
while i <= count do
if self:_splitFreeNode(self.freelist[i], bestNode) then
table.remove(self.freelist, i)
i = i - 1
count = count - 1
end
i = i + 1
end
-- prune the freelist
i = 1
while i <= #self.freelist do
local j = i + 1
while j <= #self.freelist do
local idata = self.freelist[i]
local jdata = self.freelist[j]
if jdata:contains(idata) then
table.remove(self.freelist, i)
i = i - 1
break
end
if idata:contains(jdata) then
table.remove(self.freelist, j)
j = j - 1
end
j = j + 1
end
i = i + 1
end
-- !!! returns 'nil' for failed placement unlike empty rectangle in original C# code
return bestNode.w > 0 and bestNode or nil
end,
_splitFreeNode = function(self, freeNode, usedNode)
-- test if the rects even intersect
local insideX = usedNode.x < freeNode.right and usedNode.right > freeNode.x
local insideY = usedNode.y < freeNode.bottom and usedNode.bottom > freeNode.y
if not insideX or not insideY then return false end
if insideX then
-- new node at the top side of the used node
if usedNode.y > freeNode.y and usedNode.y < freeNode.bottom then
local newNode = freeNode:clone()
newNode.h = usedNode.y - newNode.y
self.freelist[#self.freelist+1] = newNode
end
-- new node at the bottom side of the used node
if usedNode.bottom < freeNode.bottom then
local newNode = freeNode:clone()
newNode.y = usedNode.bottom
newNode.h = freeNode.bottom - usedNode.bottom
self.freelist[#self.freelist+1] = newNode
end
end
if insideY then
-- new node at the left side of the used node
if usedNode.x > freeNode.x and usedNode.x < freeNode.right then
local newNode = freeNode:clone()
newNode.w = usedNode.x - newNode.x
self.freelist[#self.freelist+1] = newNode
end
-- new node at the right side of the used node
if usedNode.right < freeNode.right then
local newNode = freeNode:clone()
newNode.x = usedNode.right
newNode.w = freeNode.right - usedNode.right
self.freelist[#self.freelist+1] = newNode
end
end
return true
end
}
local binpacker_mt = {__index=binpacker_funcs}
local function binpacker_new(w, h)
if not w or not h then error('must provide w,h to binpack new') end
local binpacker = {freelist={new_rect(0, 0, math.floor(w), math.floor(h))}}
setmetatable(binpacker, binpacker_mt)
return binpacker
end
return binpacker_new
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