streuner-game/src/sti.lua
Sebastian Hugentobler 47525f364d initial commit
2014-04-03 09:15:25 +02:00

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