646 lines
16 KiB
Lua
646 lines
16 KiB
Lua
--[[
|
|
------------------------------------------------------------------------------
|
|
Simple Tiled Implementation is licensed under the MIT Open Source License.
|
|
(http://www.opensource.org/licenses/mit-license.html)
|
|
------------------------------------------------------------------------------
|
|
|
|
Copyright (c) 2014 Landon Manning - LManning17@gmail.com - LandonManning.com
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
]]--
|
|
|
|
-- Simple Tiled Implementation v0.6.16
|
|
|
|
local bit = require "bit"
|
|
local STI = {}
|
|
local Map = {}
|
|
|
|
function STI.new(map)
|
|
map = map .. ".lua"
|
|
|
|
-- Get path to map
|
|
local path = map:reverse():find("[/\\]") or ""
|
|
if path ~= "" then
|
|
path = map:sub(1, 1 + (#map - path))
|
|
end
|
|
|
|
-- Load map
|
|
map = assert(love.filesystem.load(map), "File not found: " .. map)
|
|
setfenv(map, {})
|
|
map = setmetatable(map(), {__index = Map})
|
|
|
|
map.tiles = {}
|
|
map.drawRange = {
|
|
sx = 1,
|
|
sy = 1,
|
|
ex = map.width,
|
|
ey = map.height,
|
|
}
|
|
|
|
-- Set tiles, images
|
|
local gid = 1
|
|
for i, tileset in ipairs(map.tilesets) do
|
|
local image = STI.formatPath(path..tileset.image)
|
|
tileset.image = love.graphics.newImage(image)
|
|
tileset.image:setFilter("nearest", "nearest")
|
|
gid = map:setTiles(i, tileset, gid)
|
|
end
|
|
|
|
-- Set layers
|
|
for i, layer in ipairs(map.layers) do
|
|
map:setLayer(layer, path)
|
|
end
|
|
|
|
return map
|
|
end
|
|
|
|
function STI.formatPath(path)
|
|
local str = string.split(path, "/")
|
|
|
|
for i, segment in pairs(str) do
|
|
if segment == ".." then
|
|
str[i] = nil
|
|
str[i-1] = nil
|
|
end
|
|
end
|
|
|
|
path = ""
|
|
for _, segment in pairs(str) do
|
|
path = path .. segment .. "/"
|
|
end
|
|
|
|
return string.sub(path, 1, path:len()-1)
|
|
end
|
|
|
|
function Map:setTiles(index, tileset, gid)
|
|
local function getTiles(i, t, m, s)
|
|
i = i - m
|
|
local n = 0
|
|
|
|
while i >= t do
|
|
i = i - t
|
|
if n ~= 0 then i = i - s end
|
|
if i >= 0 then n = n + 1 end
|
|
end
|
|
|
|
return n
|
|
end
|
|
|
|
local quad = love.graphics.newQuad
|
|
local mw = self.tilewidth
|
|
local iw = tileset.imagewidth
|
|
local ih = tileset.imageheight
|
|
local tw = tileset.tilewidth
|
|
local th = tileset.tileheight
|
|
local s = tileset.spacing
|
|
local m = tileset.margin
|
|
local w = getTiles(iw, tw, m, s)
|
|
local h = getTiles(ih, th, m, s)
|
|
|
|
for y = 1, h do
|
|
for x = 1, w do
|
|
local qx = (x - 1) * tw + m + (x - 1) * s
|
|
local qy = (y - 1) * th + m + (y - 1) * s
|
|
local properties
|
|
|
|
for _, tile in pairs(tileset.tiles) do
|
|
if tile.id == gid - tileset.firstgid + 1 then
|
|
properties = tile.properties
|
|
end
|
|
end
|
|
|
|
local tile = {
|
|
gid = gid,
|
|
tileset = index,
|
|
quad = quad(qx, qy, tw, th, iw, ih),
|
|
properties = properties,
|
|
sx = 1,
|
|
sy = 1,
|
|
r = 0,
|
|
offset = {
|
|
x = -mw,
|
|
y = -th,
|
|
},
|
|
}
|
|
|
|
if self.orientation == "isometric" then
|
|
tile.offset.x = -mw / 2
|
|
end
|
|
|
|
--[[ THIS IS A TEMPORARY FIX FOR 0.9.1 ]]--
|
|
if tileset.tileoffset then
|
|
tile.offset.x = tile.offset.x + tileset.tileoffset.x
|
|
tile.offset.y = tile.offset.y + tileset.tileoffset.y
|
|
end
|
|
|
|
self.tiles[gid] = tile
|
|
gid = gid + 1
|
|
end
|
|
end
|
|
|
|
return gid
|
|
end
|
|
|
|
function Map:setLayer(layer, path)
|
|
layer.x = layer.x or 0
|
|
layer.y = layer.y or 0
|
|
layer.update = function(dt) return end
|
|
|
|
if layer.type == "tilelayer" then
|
|
self:setTileData(layer)
|
|
self:setSpriteBatches(layer)
|
|
layer.draw = function() self:drawTileLayer(layer) end
|
|
elseif layer.type == "objectgroup" then
|
|
layer.draw = function() self:drawObjectLayer(layer) end
|
|
elseif layer.type == "imagelayer" then
|
|
layer.draw = function() self:drawImageLayer(layer) end
|
|
|
|
local image = STI.formatPath(path..layer.image)
|
|
if layer.image ~= "" then
|
|
layer.image = love.graphics.newImage(image)
|
|
end
|
|
end
|
|
|
|
self.layers[layer.name] = layer
|
|
end
|
|
|
|
function Map:setTileData(layer)
|
|
local i = 1
|
|
local map = {}
|
|
|
|
for y = 1, layer.height do
|
|
map[y] = {}
|
|
for x = 1, layer.width do
|
|
local gid = layer.data[i]
|
|
|
|
if gid > 0 then
|
|
local tile = self.tiles[gid]
|
|
|
|
if tile then
|
|
map[y][x] = tile
|
|
else
|
|
local flipX = bit.status(gid, 31)
|
|
local flipY = bit.status(gid, 30)
|
|
local flipD = bit.status(gid, 29)
|
|
local realgid = bit.band(gid, bit.bnot(bit.bor(2^31, 2^30, 2^29)))
|
|
local tile = self.tiles[realgid]
|
|
local data = {
|
|
gid = tile.gid,
|
|
tileset = tile.tileset,
|
|
offset = tile.offset,
|
|
quad = tile.quad,
|
|
properties = tile.properties,
|
|
sx = tile.sx,
|
|
sy = tile.sy,
|
|
r = tile.r,
|
|
}
|
|
|
|
if flipX then
|
|
if flipY then
|
|
data.sx = -1
|
|
data.sy = -1
|
|
elseif flipD then
|
|
data.r = math.rad(90)
|
|
else
|
|
data.sx = -1
|
|
end
|
|
elseif flipY then
|
|
if flipD then
|
|
data.r = math.rad(-90)
|
|
else
|
|
data.sy = -1
|
|
end
|
|
elseif flipD then
|
|
data.r = math.rad(90)
|
|
data.sy = -1
|
|
end
|
|
|
|
self.tiles[gid] = data
|
|
map[y][x] = self.tiles[gid]
|
|
end
|
|
end
|
|
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
layer.data = map
|
|
end
|
|
|
|
function Map:setSpriteBatches(layer)
|
|
local newBatch = love.graphics.newSpriteBatch
|
|
local w = love.graphics.getWidth()
|
|
local h = love.graphics.getHeight()
|
|
local tw = self.tilewidth
|
|
local th = self.tileheight
|
|
local bw = math.ceil(w / tw)
|
|
local bh = math.ceil(h / th)
|
|
|
|
-- Minimum of 400 tiles per batch
|
|
if bw < 20 then bw = 20 end
|
|
if bh < 20 then bh = 20 end
|
|
|
|
local size = bw * bh
|
|
local batches = {
|
|
width = bw,
|
|
height = bh,
|
|
data = {},
|
|
}
|
|
|
|
for y = 1, layer.height do
|
|
local by = math.ceil(y / bh)
|
|
|
|
for x = 1, layer.width do
|
|
local tile = layer.data[y][x]
|
|
local bx = math.ceil(x / bw)
|
|
|
|
if tile then
|
|
local ts = tile.tileset
|
|
local image = self.tilesets[tile.tileset].image
|
|
|
|
batches.data[ts] = batches.data[ts] or {}
|
|
batches.data[ts][by] = batches.data[ts][by] or {}
|
|
batches.data[ts][by][bx] = batches.data[ts][by][bx] or newBatch(image, size)
|
|
|
|
local batch = batches.data[ts][by][bx]
|
|
local tx, ty
|
|
|
|
if self.orientation == "orthogonal" then
|
|
tx = x * tw + tile.offset.x
|
|
ty = y * th + tile.offset.y
|
|
|
|
-- Compensation for scale/rotation shift
|
|
if tile.sx < 0 then tx = tx + tw end
|
|
if tile.sy < 0 then ty = ty + th end
|
|
if tile.r > 0 then tx = tx + tw end
|
|
if tile.r < 0 then ty = ty + th end
|
|
elseif self.orientation == "isometric" then
|
|
tx = (x - y) * (tw / 2) + tile.offset.x
|
|
ty = (x + y) * (th / 2) + tile.offset.y
|
|
elseif self.orientation == "staggered" then
|
|
if y % 2 == 0 then
|
|
tx = x * tw + tw / 2 + tile.offset.x
|
|
else
|
|
tx = x * tw + tile.offset.x
|
|
end
|
|
|
|
ty = y * th / 2 + tile.offset.y
|
|
end
|
|
|
|
batch:add(tile.quad, tx, ty, tile.r, tile.sx, tile.sy)
|
|
end
|
|
end
|
|
end
|
|
|
|
layer.batches = batches
|
|
end
|
|
|
|
function Map:setDrawRange(tx, ty, w, h)
|
|
tx = -tx
|
|
ty = -ty
|
|
local tw = self.tilewidth
|
|
local th = self.tileheight
|
|
local sx, sy, ex, ey
|
|
|
|
if self.orientation == "orthogonal" then
|
|
sx = math.ceil(tx / tw)
|
|
sy = math.ceil(ty / th)
|
|
ex = math.ceil(sx + w / tw)
|
|
ey = math.ceil(sy + h / th)
|
|
elseif self.orientation == "isometric" then
|
|
sx = math.ceil(((ty / (th / 2)) + (tx / (tw / 2))) / 2)
|
|
sy = math.ceil(((ty / (th / 2)) - (tx / (tw / 2))) / 2 - h / th)
|
|
ex = math.ceil(sx + (h / th) + (w / tw))
|
|
ey = math.ceil(sy + (h / th) * 2 + (w / tw))
|
|
elseif self.orientation == "staggered" then
|
|
sx = math.ceil(tx / tw - 1)
|
|
sy = math.ceil(ty / th)
|
|
ex = math.ceil(sx + w / tw + 1)
|
|
ey = math.ceil(sy + h / th * 2)
|
|
end
|
|
|
|
self.drawRange = {
|
|
sx = sx,
|
|
sy = sy,
|
|
ex = ex,
|
|
ey = ey,
|
|
}
|
|
end
|
|
|
|
function Map:getCollisionMap(index)
|
|
local layer = assert(self.layers[index], "Layer not found: " .. index)
|
|
|
|
assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
|
|
|
|
local w = self.width
|
|
local h = self.height
|
|
local map = {
|
|
type = layer.type,
|
|
orientation = layer.orientation,
|
|
collision = true,
|
|
opacity = 0.5,
|
|
data = {},
|
|
}
|
|
|
|
for y=1, h do
|
|
map.data[y] = {}
|
|
for x=1, w do
|
|
if layer.data[y][x] == nil then
|
|
map.data[y][x] = 0
|
|
else
|
|
map.data[y][x] = 1
|
|
|
|
ctile = collider:addRectangle((x - 1) * self.tilewidth, (y - 1) * self.tileheight, self.tilewidth, self.tileheight)
|
|
collider:setPassive(ctile)
|
|
end
|
|
end
|
|
end
|
|
|
|
return map
|
|
end
|
|
|
|
function Map:addCustomLayer(name, index)
|
|
local layer = {
|
|
type = "customlayer",
|
|
name = name,
|
|
visible = true,
|
|
opacity = 1,
|
|
properties = {},
|
|
}
|
|
|
|
function layer:draw() return end
|
|
function layer:update(dt) return end
|
|
|
|
table.insert(self.layers, index, layer)
|
|
self.layers[name] = self.layers[index]
|
|
end
|
|
|
|
function Map:convertToCustomLayer(index)
|
|
local layer = assert(self.layers[index], "Layer not found: " .. index)
|
|
|
|
layer.type = "customlayer"
|
|
layer.x = nil
|
|
layer.y = nil
|
|
layer.width = nil
|
|
layer.height = nil
|
|
layer.encoding = nil
|
|
layer.data = nil
|
|
layer.objects = nil
|
|
layer.image = nil
|
|
|
|
function layer:draw() return end
|
|
function layer:update(dt) return end
|
|
end
|
|
|
|
function Map:removeLayer(index)
|
|
local layer = assert(self.layers[index], "Layer not found: " .. index)
|
|
|
|
if type(index) == "string" then
|
|
for i, layer in ipairs(self.layers) do
|
|
if layer.name == index then
|
|
table.remove(self.layers, i)
|
|
table.remove(self.layers, index)
|
|
break
|
|
end
|
|
end
|
|
else
|
|
local name = self.layers[index].name
|
|
table.remove(self.layers, index)
|
|
table.remove(self.layers, name)
|
|
end
|
|
end
|
|
|
|
function Map:update(dt)
|
|
for _, layer in ipairs(self.layers) do
|
|
layer:update(dt)
|
|
end
|
|
end
|
|
|
|
function Map:draw()
|
|
for _, layer in ipairs(self.layers) do
|
|
if layer.visible and layer.opacity > 0 then
|
|
self:drawLayer(layer)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Map:drawLayer(layer)
|
|
love.graphics.setColor(255, 255, 255, 255 * layer.opacity)
|
|
layer:draw()
|
|
love.graphics.setColor(255, 255, 255, 255)
|
|
end
|
|
|
|
function Map:drawTileLayer(layer)
|
|
assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
|
|
|
|
local bw = layer.batches.width
|
|
local bh = layer.batches.height
|
|
local sx = math.ceil((self.drawRange.sx - layer.x / self.tilewidth - 1) / bw)
|
|
local sy = math.ceil((self.drawRange.sy - layer.y / self.tileheight - 1) / bh)
|
|
local ex = math.ceil((self.drawRange.ex - layer.x / self.tilewidth + 1) / bw)
|
|
local ey = math.ceil((self.drawRange.ey - layer.y / self.tileheight + 1) / bh)
|
|
local mx = math.ceil(self.width / bw)
|
|
local my = math.ceil(self.height / bh)
|
|
|
|
for by=sy, ey do
|
|
for bx=sx, ex do
|
|
if bx >= 1 and bx <= mx and by >= 1 and by <= my then
|
|
for _, batches in pairs(layer.batches.data) do
|
|
local batch = batches[by] and batches[by][bx]
|
|
|
|
if batch then
|
|
love.graphics.draw(batch, math.floor(layer.x), math.floor(layer.y))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function Map:drawObjectLayer(layer)
|
|
assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup")
|
|
|
|
local line = { 160, 160, 160, 255 * layer.opacity }
|
|
local fill = { 160, 160, 160, 255 * layer.opacity * 0.2 }
|
|
local shadow = { 0, 0, 0, 255 * layer.opacity }
|
|
|
|
local function drawEllipse(mode, x, y, rx, ry)
|
|
local segments = 100
|
|
local vertices = {}
|
|
|
|
table.insert(vertices, x + rx / 2)
|
|
table.insert(vertices, y + ry / 2)
|
|
|
|
for i=0, segments do
|
|
local angle = (i / segments) * math.pi * 2
|
|
local px = x + rx / 2 + math.cos(angle) * rx / 2
|
|
local py = y + ry / 2 + math.sin(angle) * ry / 2
|
|
|
|
table.insert(vertices, px)
|
|
table.insert(vertices, py)
|
|
end
|
|
|
|
love.graphics.polygon(mode, vertices)
|
|
end
|
|
|
|
for _, object in ipairs(layer.objects) do
|
|
local x = layer.x + object.x
|
|
local y = layer.y + object.y
|
|
|
|
if object.shape == "rectangle" then
|
|
love.graphics.setColor(fill)
|
|
love.graphics.rectangle("fill", x, y, object.width, object.height)
|
|
|
|
love.graphics.setColor(shadow)
|
|
love.graphics.rectangle("line", x+1, y+1, object.width, object.height)
|
|
|
|
love.graphics.setColor(line)
|
|
love.graphics.rectangle("line", x, y, object.width, object.height)
|
|
elseif object.shape == "ellipse" then
|
|
love.graphics.setColor(fill)
|
|
drawEllipse("fill", x, y, object.width, object.height)
|
|
|
|
love.graphics.setColor(shadow)
|
|
drawEllipse("line", x+1, y+1, object.width, object.height)
|
|
|
|
love.graphics.setColor(line)
|
|
drawEllipse("line", x, y, object.width, object.height)
|
|
elseif object.shape == "polygon" then
|
|
local points = {{},{}}
|
|
|
|
for _, point in ipairs(object.polygon) do
|
|
table.insert(points[1], x + point.x)
|
|
table.insert(points[1], y + point.y)
|
|
table.insert(points[2], x + point.x+1)
|
|
table.insert(points[2], y + point.y+1)
|
|
end
|
|
|
|
love.graphics.setColor(fill)
|
|
if not love.math.isConvex(points[1]) then
|
|
local triangles = love.math.triangulate(points[1])
|
|
for _, triangle in ipairs(triangles) do
|
|
love.graphics.polygon("fill", triangle)
|
|
end
|
|
else
|
|
love.graphics.polygon("fill", points[1])
|
|
end
|
|
|
|
love.graphics.setColor(shadow)
|
|
love.graphics.polygon("line", points[2])
|
|
|
|
love.graphics.setColor(line)
|
|
love.graphics.polygon("line", points[1])
|
|
elseif object.shape == "polyline" then
|
|
local points = {{},{}}
|
|
|
|
for _, point in ipairs(object.polyline) do
|
|
table.insert(points[1], x + point.x)
|
|
table.insert(points[1], y + point.y)
|
|
table.insert(points[2], x + point.x+1)
|
|
table.insert(points[2], y + point.y+1)
|
|
end
|
|
|
|
love.graphics.setColor(shadow)
|
|
love.graphics.line(points[2])
|
|
|
|
love.graphics.setColor(line)
|
|
love.graphics.line(points[1])
|
|
end
|
|
end
|
|
end
|
|
|
|
function Map:drawImageLayer(layer)
|
|
assert(layer.type == "imagelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: imagelayer")
|
|
|
|
if layer.image ~= "" then
|
|
love.graphics.draw(layer.image, layer.x, layer.y)
|
|
end
|
|
end
|
|
|
|
function Map:drawCollisionMap(layer)
|
|
assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer")
|
|
assert(layer.collision, "This is not a collision layer")
|
|
|
|
local tw = self.tilewidth
|
|
local th = self.tileheight
|
|
|
|
love.graphics.setColor(255, 255, 255, 255 * layer.opacity)
|
|
|
|
for y=1, self.height do
|
|
for x=1, self.width do
|
|
local tx, ty
|
|
|
|
if self.orientation == "orthogonal" then
|
|
tx = (x - 1) * tw
|
|
ty = (y - 1) * th
|
|
elseif self.orientation == "isometric" then
|
|
tx = (x - y) * (tw / 2) - self.tilewidth / 2
|
|
ty = (x + y) * (th / 2) - self.tileheight
|
|
elseif self.orientation == "staggered" then
|
|
if y % 2 == 0 then
|
|
tx = x * tw + tw / 2 - self.tilewidth
|
|
else
|
|
tx = x * tw - self.tilewidth
|
|
end
|
|
|
|
ty = y * th / 2 - self.tileheight
|
|
end
|
|
|
|
|
|
if layer.data[y][x] == 1 then
|
|
love.graphics.rectangle("fill", tx, ty, tw, th)
|
|
else
|
|
love.graphics.rectangle("line", tx, ty, tw, th)
|
|
end
|
|
end
|
|
end
|
|
|
|
love.graphics.setColor(255, 255, 255, 255)
|
|
end
|
|
|
|
-- http://wiki.interfaceware.com/534.html
|
|
function string.split(s, d)
|
|
local t = {}
|
|
local i = 0
|
|
local f
|
|
local match = '(.-)' .. d .. '()'
|
|
|
|
if string.find(s, d) == nil then
|
|
return {s}
|
|
end
|
|
|
|
for sub, j in string.gmatch(s, match) do
|
|
i = i + 1
|
|
t[i] = sub
|
|
f = j
|
|
end
|
|
|
|
if i ~= 0 then
|
|
t[i+1] = string.sub(s, f)
|
|
end
|
|
|
|
return t
|
|
end
|
|
|
|
function bit.status(num, digit)
|
|
return bit.band(num, bit.lshift(1, digit)) ~= 0
|
|
end
|
|
|
|
return STI
|