1
0
Fork 0

Add working, minimally playable prototype.

This commit is contained in:
Gonzalo Delgado 2023-08-18 07:34:37 -03:00
commit 11acc030e5
11 changed files with 3570 additions and 0 deletions

5
conf.lua Normal file
View File

@ -0,0 +1,5 @@
function love.conf(t)
t.version = "11.3"
t.window.fullscreen = true
t.title = "Tortugger"
end

337
inspect.lua Normal file
View File

@ -0,0 +1,337 @@
local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = pcall(require, 'compat53.module'); if p then _tl_compat = m end end; local math = _tl_compat and _tl_compat.math or math; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table
local inspect = {Options = {}, }
inspect._VERSION = 'inspect.lua 3.1.0'
inspect._URL = 'http://github.com/kikito/inspect.lua'
inspect._DESCRIPTION = 'human-readable representations of tables'
inspect._LICENSE = [[
MIT LICENSE
Copyright (c) 2022 Enrique García Cota
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.
]]
inspect.KEY = setmetatable({}, { __tostring = function() return 'inspect.KEY' end })
inspect.METATABLE = setmetatable({}, { __tostring = function() return 'inspect.METATABLE' end })
local tostring = tostring
local rep = string.rep
local match = string.match
local char = string.char
local gsub = string.gsub
local fmt = string.format
local function rawpairs(t)
return next, t, nil
end
local function smartQuote(str)
if match(str, '"') and not match(str, "'") then
return "'" .. str .. "'"
end
return '"' .. gsub(str, '"', '\\"') .. '"'
end
local shortControlCharEscapes = {
["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v", ["\127"] = "\\127",
}
local longControlCharEscapes = { ["\127"] = "\127" }
for i = 0, 31 do
local ch = char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = "\\" .. i
longControlCharEscapes[ch] = fmt("\\%03d", i)
end
end
local function escape(str)
return (gsub(gsub(gsub(str, "\\", "\\\\"),
"(%c)%f[0-9]", longControlCharEscapes),
"%c", shortControlCharEscapes))
end
local function isIdentifier(str)
return type(str) == "string" and not not str:match("^[_%a][_%a%d]*$")
end
local flr = math.floor
local function isSequenceKey(k, sequenceLength)
return type(k) == "number" and
flr(k) == k and
1 <= (k) and
k <= sequenceLength
end
local defaultTypeOrders = {
['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
['function'] = 5, ['userdata'] = 6, ['thread'] = 7,
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
if ta == tb and (ta == 'string' or ta == 'number') then
return (a) < (b)
end
local dta = defaultTypeOrders[ta] or 100
local dtb = defaultTypeOrders[tb] or 100
return dta == dtb and ta < tb or dta < dtb
end
local function getKeys(t)
local seqLen = 1
while rawget(t, seqLen) ~= nil do
seqLen = seqLen + 1
end
seqLen = seqLen - 1
local keys, keysLen = {}, 0
for k in rawpairs(t) do
if not isSequenceKey(k, seqLen) then
keysLen = keysLen + 1
keys[keysLen] = k
end
end
table.sort(keys, sortKeys)
return keys, keysLen, seqLen
end
local function countCycles(x, cycles)
if type(x) == "table" then
if cycles[x] then
cycles[x] = cycles[x] + 1
else
cycles[x] = 1
for k, v in rawpairs(x) do
countCycles(k, cycles)
countCycles(v, cycles)
end
countCycles(getmetatable(x), cycles)
end
end
end
local function makePath(path, a, b)
local newPath = {}
local len = #path
for i = 1, len do newPath[i] = path[i] end
newPath[len + 1] = a
newPath[len + 2] = b
return newPath
end
local function processRecursive(process,
item,
path,
visited)
if item == nil then return nil end
if visited[item] then return visited[item] end
local processed = process(item, path)
if type(processed) == "table" then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k, v in rawpairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= 'table' then mt = nil end
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
local function puts(buf, str)
buf.n = buf.n + 1
buf[buf.n] = str
end
local Inspector = {}
local Inspector_mt = { __index = Inspector }
local function tabify(inspector)
puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
end
function Inspector:getId(v)
local id = self.ids[v]
local ids = self.ids
if not id then
local tv = type(v)
id = (ids[tv] or 0) + 1
ids[v], ids[tv] = id, id
end
return tostring(id)
end
function Inspector:putValue(v)
local buf = self.buf
local tv = type(v)
if tv == 'string' then
puts(buf, smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
tv == 'cdata' or tv == 'ctype' then
puts(buf, tostring(v))
elseif tv == 'table' and not self.ids[v] then
local t = v
if t == inspect.KEY or t == inspect.METATABLE then
puts(buf, tostring(t))
elseif self.level >= self.depth then
puts(buf, '{...}')
else
if self.cycles[t] > 1 then puts(buf, fmt('<%d>', self:getId(t))) end
local keys, keysLen, seqLen = getKeys(t)
puts(buf, '{')
self.level = self.level + 1
for i = 1, seqLen + keysLen do
if i > 1 then puts(buf, ',') end
if i <= seqLen then
puts(buf, ' ')
self:putValue(t[i])
else
local k = keys[i - seqLen]
tabify(self)
if isIdentifier(k) then
puts(buf, k)
else
puts(buf, "[")
self:putValue(k)
puts(buf, "]")
end
puts(buf, ' = ')
self:putValue(t[k])
end
end
local mt = getmetatable(t)
if type(mt) == 'table' then
if seqLen + keysLen > 0 then puts(buf, ',') end
tabify(self)
puts(buf, '<metatable> = ')
self:putValue(mt)
end
self.level = self.level - 1
if keysLen > 0 or type(mt) == 'table' then
tabify(self)
elseif seqLen > 0 then
puts(buf, ' ')
end
puts(buf, '}')
end
else
puts(buf, fmt('<%s %d>', tv, self:getId(v)))
end
end
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or (math.huge)
local newline = options.newline or '\n'
local indent = options.indent or ' '
local process = options.process
if process then
root = processRecursive(process, root, {}, {})
end
local cycles = {}
countCycles(root, cycles)
local inspector = setmetatable({
buf = { n = 0 },
ids = {},
cycles = cycles,
depth = depth,
level = 0,
newline = newline,
indent = indent,
}, Inspector_mt)
inspector:putValue(root)
return table.concat(inspector.buf)
end
setmetatable(inspect, {
__call = function(_, root, options)
return inspect.inspect(root, options)
end,
})
return inspect

230
main.lua Normal file
View File

@ -0,0 +1,230 @@
local sti = require("sti")
local inspect = require("inspect")
love.graphics.setDefaultFilter("nearest", "nearest")
local game = {
SCREEN_WIDTH=320,
SCREEN_HEIGHT=200,
TILE_SIZE=16,
-- NOTE: playfield dimensions in tiles
PLAYFIELD_OFFSET_TOP=1.5,
PLAYFIELD_WIDTH=20,
PLAYFIELD_HEIGHT=10,
VEHICLE_SPEED=10,
FLOATDEV_SPEED=8,
playFieldTiles={},
map=sti("map.lua"),
entities={
vehicles={},
floatdevs={},
},
hazards={},
drawGrid=true,
}
function t2p(tileDim)
return tileDim*game.TILE_SIZE
end
function tilePos2Pixels(tilePos)
return t2p(tilePos.x - 1), t2p(tilePos.y - 1)
end
function screenPixelToTilePos(pixelPos)
return {
x=math.floor(pixelPos.x/game.TILE_SIZE + 1),
y=math.floor((pixelPos.y + t2p(game.PLAYFIELD_OFFSET_TOP))/game.TILE_SIZE)
}
end
local player = {
pos={x=t2p(0), y=t2p(0)},
turtlePos={x=1, y=2},
turtleHit=false,
lives=3,
turtles=3,
dead=false,
deadTimer=2,
}
function resetPlayer()
player.pos = {x=t2p(0), y=t2p(0)}
player.dead = false
end
function love.load()
local desktop_width, desktop_height = love.window.getDesktopDimensions()
game.scaleW = desktop_width / game.SCREEN_WIDTH
game.scaleH = desktop_height / game.SCREEN_HEIGHT
for i, layer in ipairs(game.map.layers) do
if layer.objects then
for j, mapObj in ipairs(layer.objects) do
mapObj.visible = false
local typekey = mapObj.type.."s"
if game.entities[typekey] == nil then
game.entities[typekey] = {}
end
table.insert(
game.entities[typekey],
{
x=mapObj.x,
y=mapObj.y,
w=mapObj.width,
h=mapObj.height,
type=mapObj.type,
}
)
end
end
end
end
function love.keyreleased(key)
if player.dead then
return
end
local newPos = screenPixelToTilePos(player.pos)
local oldPos = screenPixelToTilePos(player.pos)
if key == "up" or key == "w" then
newPos.y = newPos.y - 1
end
if key == "down" or key == "s" then
newPos.y = newPos.y + 1
end
if key == "right" or key == "d" then
newPos.x = newPos.x + 1
end
if key == "left" or key == "a" then
newPos.x = newPos.x - 1
end
player.turtlePos = oldPos
if newPos.x < 1 then
newPos.x = 1
end
if newPos.x > game.PLAYFIELD_WIDTH then
newPos.x = game.PLAYFIELD_WIDTH
end
if newPos.y < 1 then
newPos.y = 1
end
if newPos.y > game.PLAYFIELD_HEIGHT then
newPos.y = game.PLAYFIELD_HEIGHT
end
player.pos.x = t2p(newPos.x - 1)
player.pos.y = t2p(newPos.y - 1)
end
function tilePos2Rect(tilePos)
return {
x=t2p(tilePos.x - 1),
y=t2p(tilePos.y - 1),
w=game.TILE_SIZE,
h=game.TILE_SIZE,
}
end
function rectanglesIntersect(rect1, rect2)
return (rect1.x < rect2.x + rect2.w and
rect1.x + rect1.w > rect2.x and
rect1.y < rect2.y + rect2.h and
rect1.y + rect1.h > rect2.y)
end
function love.update(dt)
player.turtleHit = false
if player.dead then
player.deadTimer = player.deadTimer - dt
if player.deadTimer <= 0 then
player.deadTimer = 2
resetPlayer()
end
else
local playerRect = tilePos2Rect(screenPixelToTilePos(player.pos))
local turtleRect = tilePos2Rect(player.turtlePos)
if player.floating then
player.pos.y = player.pos.y + game.FLOATDEV_SPEED*dt
end
for i, vehicle in ipairs(game.entities.vehicles) do
if rectanglesIntersect(playerRect, vehicle) then
player.dead = true
player.lives = player.lives - 1
end
if rectanglesIntersect(turtleRect, vehicle) then
player.turtleHit = true
end
vehicle.y = vehicle.y + game.VEHICLE_SPEED*dt
if vehicle.y > t2p(game.PLAYFIELD_HEIGHT) then
vehicle.y = -vehicle.h
end
end
player.floating = false
for i, floatdev in ipairs(game.entities.floatdevs) do
if not player.floating and rectanglesIntersect(playerRect, floatdev) then
player.floating = true
end
floatdev.y = floatdev.y + game.FLOATDEV_SPEED*dt
if floatdev.y > t2p(game.PLAYFIELD_HEIGHT) then
floatdev.y = -floatdev.h
end
end
if not player.floating then
for i, water in ipairs(game.entities.waters) do
if not player.dead and rectanglesIntersect(playerRect, water) then
player.dead = true
player.lives = player.lives - 1
end
end
end
end
end
function love.draw()
love.graphics.setColor(1, 1, 1)
game.map:draw(0, t2p(game.PLAYFIELD_OFFSET_TOP), game.scaleW, game.scaleH)
love.graphics.push()
-- <playfield>
love.graphics.scale(game.scaleW, game.scaleH)
love.graphics.translate(0, t2p(game.PLAYFIELD_OFFSET_TOP))
if game.drawGrid then
love.graphics.setColor(0.4, 0.3, 0.1)
for i=0, game.PLAYFIELD_WIDTH do
love.graphics.line(t2p(i), 0, t2p(i), t2p(game.PLAYFIELD_HEIGHT))
end
for i=0, game.PLAYFIELD_HEIGHT do
love.graphics.line(0, t2p(i), t2p(game.PLAYFIELD_WIDTH), t2p(i))
end
end
love.graphics.setColor(0.9, 0.2, 0)
for i, vehicle in ipairs(game.entities.vehicles) do
love.graphics.rectangle("fill", vehicle.x, vehicle.y, vehicle.w, vehicle.h)
end
love.graphics.setColor(0.8, 0.7, 0)
for i, floatdev in ipairs(game.entities.floatdevs) do
love.graphics.rectangle("fill", floatdev.x, floatdev.y, floatdev.w, floatdev.h)
end
if player.dead then
love.graphics.setColor(0.8, 0.1, 0)
else
love.graphics.setColor(0.9, 0.6, 0.1)
end
-- TODO: change this to use player's stored pixel pos
local x, y = player.pos.x, player.pos.y
love.graphics.circle("fill", x + game.TILE_SIZE/2, y + game.TILE_SIZE/2, game.TILE_SIZE/2)
love.graphics.setColor(0.2, 0.1, 0.1)
love.graphics.circle("line", x + game.TILE_SIZE/2, y + game.TILE_SIZE/2, game.TILE_SIZE/2)
x, y = tilePos2Pixels(player.turtlePos)
love.graphics.setColor(0.2, 0.1, 0.1)
love.graphics.circle("line", x + game.TILE_SIZE/2, y + game.TILE_SIZE/2, game.TILE_SIZE/2)
if player.turtleHit then
love.graphics.setColor(0.8, 0.1, 0)
else
love.graphics.setColor(0, 0.7, 0.2)
end
love.graphics.circle("fill", x + game.TILE_SIZE/2, y + game.TILE_SIZE/2, game.TILE_SIZE/2)
-- </playfield>
love.graphics.pop()
end

248
map.lua Normal file
View File

@ -0,0 +1,248 @@
return {
version = "1.5",
luaversion = "5.1",
tiledversion = "1.8.6",
orientation = "orthogonal",
renderorder = "right-down",
width = 20,
height = 10,
tilewidth = 16,
tileheight = 16,
nextlayerid = 4,
nextobjectid = 12,
properties = {},
tilesets = {
{
name = "tiles",
firstgid = 1,
tilewidth = 16,
tileheight = 16,
spacing = 0,
margin = 0,
columns = 10,
image = "tiles.png",
imagewidth = 160,
imageheight = 16,
transparentcolor = "#ff00ff",
objectalignment = "unspecified",
tileoffset = {
x = 0,
y = 0
},
grid = {
orientation = "orthogonal",
width = 16,
height = 16
},
properties = {},
wangsets = {},
tilecount = 10,
tiles = {}
}
},
layers = {
{
type = "tilelayer",
x = 0,
y = 0,
width = 20,
height = 10,
id = 1,
name = "Capa de patrones 1",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
properties = {},
encoding = "lua",
data = {
1, 2, 3, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 3, 7, 3, 4, 6,
1, 2, 7, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 7, 7, 7, 4, 6,
1, 2, 3, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 3, 7, 3, 4, 6,
1, 2, 7, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 7, 7, 7, 4, 6,
1, 2, 3, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 3, 7, 3, 4, 6,
1, 2, 7, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 7, 7, 7, 4, 6,
1, 2, 3, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 3, 7, 3, 4, 6,
1, 2, 7, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 7, 7, 7, 4, 6,
1, 2, 3, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 3, 7, 3, 4, 6,
1, 2, 7, 4, 1, 5, 5, 5, 1, 5, 5, 5, 5, 1, 2, 7, 7, 7, 4, 6
}
},
{
type = "objectgroup",
draworder = "topdown",
id = 3,
name = "hazards",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
properties = {},
objects = {
{
id = 4,
name = "",
type = "water",
shape = "rectangle",
x = 80,
y = -0.25,
width = 48,
height = 160,
rotation = 0,
visible = false,
properties = {}
},
{
id = 5,
name = "",
type = "water",
shape = "rectangle",
x = 144,
y = 0,
width = 64,
height = 160,
rotation = 0,
visible = false,
properties = {}
}
}
},
{
type = "objectgroup",
draworder = "topdown",
id = 2,
name = "entities",
visible = true,
opacity = 1,
offsetx = 0,
offsety = 0,
parallaxx = 1,
parallaxy = 1,
tintcolor = { 170, 0, 0 },
properties = {},
objects = {
{
id = 1,
name = "",
type = "vehicle",
shape = "rectangle",
x = 18,
y = 0,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 8,
name = "",
type = "floatdev",
shape = "rectangle",
x = 82,
y = 0,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 11,
name = "",
type = "floatdev",
shape = "rectangle",
x = 98,
y = 32,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 2,
name = "",
type = "vehicle",
shape = "rectangle",
x = 18,
y = 64,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 7,
name = "",
type = "floatdev",
shape = "rectangle",
x = 82,
y = 64,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 10,
name = "",
type = "floatdev",
shape = "rectangle",
x = 98,
y = 98,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 3,
name = "",
type = "vehicle",
shape = "rectangle",
x = 18,
y = 128,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 6,
name = "",
type = "floatdev",
shape = "rectangle",
x = 82,
y = 128,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
},
{
id = 9,
name = "",
type = "floatdev",
shape = "rectangle",
x = 98,
y = 160,
width = 12,
height = 16,
rotation = 0,
visible = true,
properties = {}
}
}
}
}
}

38
map.tmx Normal file
View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.8" tiledversion="1.8.6" orientation="orthogonal" renderorder="right-down" width="20" height="10" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="12">
<editorsettings>
<export target="map.lua" format="lua"/>
</editorsettings>
<tileset firstgid="1" name="tiles" tilewidth="16" tileheight="16" tilecount="10" columns="10">
<image source="tiles.png" trans="ff00ff" width="160" height="16"/>
</tileset>
<layer id="1" name="Capa de patrones 1" width="20" height="10">
<data encoding="csv">
1,2,3,4,1,5,5,5,1,5,5,5,5,1,2,3,7,3,4,6,
1,2,7,4,1,5,5,5,1,5,5,5,5,1,2,7,7,7,4,6,
1,2,3,4,1,5,5,5,1,5,5,5,5,1,2,3,7,3,4,6,
1,2,7,4,1,5,5,5,1,5,5,5,5,1,2,7,7,7,4,6,
1,2,3,4,1,5,5,5,1,5,5,5,5,1,2,3,7,3,4,6,
1,2,7,4,1,5,5,5,1,5,5,5,5,1,2,7,7,7,4,6,
1,2,3,4,1,5,5,5,1,5,5,5,5,1,2,3,7,3,4,6,
1,2,7,4,1,5,5,5,1,5,5,5,5,1,2,7,7,7,4,6,
1,2,3,4,1,5,5,5,1,5,5,5,5,1,2,3,7,3,4,6,
1,2,7,4,1,5,5,5,1,5,5,5,5,1,2,7,7,7,4,6
</data>
</layer>
<objectgroup id="3" name="hazards">
<object id="4" type="water" x="80" y="-0.25" width="48" height="160"/>
<object id="5" type="water" x="144" y="0" width="64" height="160"/>
</objectgroup>
<objectgroup id="2" name="entities" tintcolor="#aa0000">
<object id="1" type="vehicle" x="18" y="0" width="12" height="16"/>
<object id="8" type="floatdev" x="82" y="0" width="12" height="16"/>
<object id="11" type="floatdev" x="98" y="32" width="12" height="16"/>
<object id="2" type="vehicle" x="18" y="64" width="12" height="16"/>
<object id="7" type="floatdev" x="82" y="64" width="12" height="16"/>
<object id="10" type="floatdev" x="98" y="98" width="12" height="16"/>
<object id="3" type="vehicle" x="18" y="128" width="12" height="16"/>
<object id="6" type="floatdev" x="82" y="128" width="12" height="16"/>
<object id="9" type="floatdev" x="98" y="160" width="12" height="16"/>
</objectgroup>
</map>

159
sti/atlas.lua Normal file
View File

@ -0,0 +1,159 @@
---- Texture atlas complement for the Simple Tiled Implementation
-- @copyright 2022
-- @author Eduardo Hernández coz.eduardo.hernandez@gmail.com
-- @license MIT/X11
local module = {}
--- Create a texture atlas
-- @param files Array with filenames
-- @param sort If "size" will sort by size, or if "id" will sort by id
-- @param ids Array with ids of each file
-- @param pow2 If true, will force a power of 2 size
function module.Atlas( files, sort, ids, pow2 )
local function Node(x, y, w, h)
return {x = x, y = y, w = w, h = h}
end
local function nextpow2( n )
local res = 1
while res <= n do
res = res * 2
end
return res
end
local function loadImgs()
local images = {}
for i = 1, #files do
images[i] = {}
--images[i].name = files[i]
if ids then images[i].id = ids[i] end
images[i].img = love.graphics.newImage( files[i] )
images[i].w = images[i].img:getWidth()
images[i].h = images[i].img:getHeight()
images[i].area = images[i].w * images[i].h
end
if sort == "size" or sort == "id" then
table.sort( images, function( a, b ) return ( a.area > b.area ) end )
end
return images
end
--TODO: understand this func
local function add(root, id, w, h)
if root.left or root.right then
if root.left then
local node = add(root.left, id, w, h)
if node then return node end
end
if root.right then
local node = add(root.right, id, w, h)
if node then return node end
end
return nil
end
if w > root.w or h > root.h then return nil end
local _w, _h = root.w - w, root.h - h
if _w <= _h then
root.left = Node(root.x + w, root.y, _w, h)
root.right = Node(root.x, root.y + h, root.w, _h)
else
root.left = Node(root.x, root.y + h, w, _h)
root.right = Node(root.x + w, root.y, _w, root.h)
end
root.w = w
root.h = h
root.id = id
return root
end
local function unmap(root)
if not root then return {} end
local tree = {}
if root.id then
tree[root.id] = {}
tree[root.id].x, tree[root.id].y = root.x, root.y
end
local left = unmap(root.left)
local right = unmap(root.right)
for k, v in pairs(left) do
tree[k] = {}
tree[k].x, tree[k].y = v.x, v.y
end
for k, v in pairs(right) do
tree[k] = {}
tree[k].x, tree[k].y = v.x, v.y
end
return tree
end
local function bake()
local images = loadImgs()
local root = {}
local w, h = images[1].w, images[1].h
if pow2 then
if w % 1 == 0 then w = nextpow2(w) end
if h % 1 == 0 then h = nextpow2(h) end
end
repeat
local node
root = Node(0, 0, w, h)
for i = 1, #images do
node = add(root, i, images[i].w, images[i].h)
if not node then break end
end
if not node then
if h <= w then
if pow2 then h = h * 2 else h = h + 1 end
else
if pow2 then w = w * 2 else w = w + 1 end
end
else
break
end
until false
local limits = love.graphics.getSystemLimits()
if w > limits.texturesize or h > limits.texturesize then
return "Resulting texture is too large for this system"
end
local coords = unmap(root)
local map = love.graphics.newCanvas(w, h)
love.graphics.setCanvas( map )
-- love.graphics.clear()
for i = 1, #images do
love.graphics.draw(images[i].img, coords[i].x, coords[i].y)
if ids then coords[i].id = images[i].id end
end
love.graphics.setCanvas()
if sort == "ids" then
table.sort( coords, function( a, b ) return ( a.id < b.id ) end )
end
return { image = map, coords = coords }
end
return bake()
end
return module

132
sti/graphics.lua Normal file
View File

@ -0,0 +1,132 @@
local lg = _G.love.graphics
local graphics = { isCreated = lg and true or false }
function graphics.newSpriteBatch(...)
if graphics.isCreated then
return lg.newSpriteBatch(...)
end
end
function graphics.newCanvas(...)
if graphics.isCreated then
return lg.newCanvas(...)
end
end
function graphics.newImage(...)
if graphics.isCreated then
return lg.newImage(...)
end
end
function graphics.newQuad(...)
if graphics.isCreated then
return lg.newQuad(...)
end
end
function graphics.getCanvas(...)
if graphics.isCreated then
return lg.getCanvas(...)
end
end
function graphics.setCanvas(...)
if graphics.isCreated then
return lg.setCanvas(...)
end
end
function graphics.clear(...)
if graphics.isCreated then
return lg.clear(...)
end
end
function graphics.push(...)
if graphics.isCreated then
return lg.push(...)
end
end
function graphics.origin(...)
if graphics.isCreated then
return lg.origin(...)
end
end
function graphics.scale(...)
if graphics.isCreated then
return lg.scale(...)
end
end
function graphics.translate(...)
if graphics.isCreated then
return lg.translate(...)
end
end
function graphics.pop(...)
if graphics.isCreated then
return lg.pop(...)
end
end
function graphics.draw(...)
if graphics.isCreated then
return lg.draw(...)
end
end
function graphics.rectangle(...)
if graphics.isCreated then
return lg.rectangle(...)
end
end
function graphics.getColor(...)
if graphics.isCreated then
return lg.getColor(...)
end
end
function graphics.setColor(...)
if graphics.isCreated then
return lg.setColor(...)
end
end
function graphics.line(...)
if graphics.isCreated then
return lg.line(...)
end
end
function graphics.polygon(...)
if graphics.isCreated then
return lg.polygon(...)
end
end
function graphics.points(...)
if graphics.isCreated then
return lg.points(...)
end
end
function graphics.getWidth()
if graphics.isCreated then
return lg.getWidth()
end
return 0
end
function graphics.getHeight()
if graphics.isCreated then
return lg.getHeight()
end
return 0
end
return graphics

1688
sti/init.lua Normal file

File diff suppressed because it is too large Load Diff

323
sti/plugins/box2d.lua Normal file
View File

@ -0,0 +1,323 @@
--- Box2D plugin for STI
-- @module box2d
-- @author Landon Manning
-- @copyright 2019
-- @license MIT/X11
local love = _G.love
local utils = require((...):gsub('plugins.box2d', 'utils'))
local lg = require((...):gsub('plugins.box2d', 'graphics'))
return {
box2d_LICENSE = "MIT/X11",
box2d_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
box2d_VERSION = "2.3.2.7",
box2d_DESCRIPTION = "Box2D hooks for STI.",
--- Initialize Box2D physics world.
-- @param world The Box2D world to add objects to.
box2d_init = function(map, world)
assert(love.physics, "To use the Box2D plugin, please enable the love.physics module.")
local body = love.physics.newBody(world, map.offsetx, map.offsety)
local collision = {
body = body,
}
local function addObjectToWorld(objshape, vertices, userdata, object)
local shape
if objshape == "polyline" then
if #vertices == 4 then
shape = love.physics.newEdgeShape(unpack(vertices))
else
shape = love.physics.newChainShape(false, unpack(vertices))
end
else
shape = love.physics.newPolygonShape(unpack(vertices))
end
local currentBody = body
--dynamic are objects/players etc.
if userdata.properties.dynamic == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic')
-- static means it shouldn't move. Things like walls/ground.
elseif userdata.properties.static == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'static')
-- kinematic means that the object is static in the game world but effects other bodies
elseif userdata.properties.kinematic == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'kinematic')
end
local fixture = love.physics.newFixture(currentBody, shape)
fixture:setUserData(userdata)
-- Set some custom properties from userdata (or use default set by box2d)
fixture:setFriction(userdata.properties.friction or 0.2)
fixture:setRestitution(userdata.properties.restitution or 0.0)
fixture:setSensor(userdata.properties.sensor or false)
fixture:setFilterData(
userdata.properties.categories or 1,
userdata.properties.mask or 65535,
userdata.properties.group or 0
)
local obj = {
object = object,
body = currentBody,
shape = shape,
fixture = fixture,
}
table.insert(collision, obj)
end
local function getPolygonVertices(object)
local vertices = {}
for _, vertex in ipairs(object.polygon) do
table.insert(vertices, vertex.x)
table.insert(vertices, vertex.y)
end
return vertices
end
local function calculateObjectPosition(object, tile)
local o = {
shape = object.shape,
x = (object.dx or object.x) + map.offsetx,
y = (object.dy or object.y) + map.offsety,
w = object.width,
h = object.height,
polygon = object.polygon or object.polyline or object.ellipse or object.rectangle
}
local userdata = {
object = o,
properties = object.properties
}
o.r = object.rotation or 0
if o.shape == "rectangle" then
local cos = math.cos(math.rad(o.r))
local sin = math.sin(math.rad(o.r))
local oy = 0
if object.gid then
local tileset = map.tilesets[map.tiles[object.gid].tileset]
local lid = object.gid - tileset.firstgid
local t = {}
-- This fixes a height issue
o.y = o.y + map.tiles[object.gid].offset.y
oy = o.h
for _, tt in ipairs(tileset.tiles) do
if tt.id == lid then
t = tt
break
end
end
if t.objectGroup then
for _, obj in ipairs(t.objectGroup.objects) do
-- Every object in the tile
calculateObjectPosition(obj, object)
end
return
else
o.w = map.tiles[object.gid].width
o.h = map.tiles[object.gid].height
end
end
o.polygon = {
{ x=o.x+0, y=o.y+0 },
{ x=o.x+o.w, y=o.y+0 },
{ x=o.x+o.w, y=o.y+o.h },
{ x=o.x+0, y=o.y+o.h }
}
for _, vertex in ipairs(o.polygon) do
vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin, oy)
end
local vertices = getPolygonVertices(o)
addObjectToWorld(o.shape, vertices, userdata, tile or object)
elseif o.shape == "ellipse" then
if not o.polygon then
o.polygon = utils.convert_ellipse_to_polygon(o.x, o.y, o.w, o.h)
end
local vertices = getPolygonVertices(o)
local triangles = love.math.triangulate(vertices)
for _, triangle in ipairs(triangles) do
addObjectToWorld(o.shape, triangle, userdata, tile or object)
end
elseif o.shape == "polygon" then
-- Recalculate collision polygons inside tiles
if tile then
local cos = math.cos(math.rad(o.r))
local sin = math.sin(math.rad(o.r))
for _, vertex in ipairs(o.polygon) do
vertex.x = vertex.x + o.x
vertex.y = vertex.y + o.y
vertex.x, vertex.y = utils.rotate_vertex(map, vertex, o.x, o.y, cos, sin)
end
end
local vertices = getPolygonVertices(o)
local triangles = love.math.triangulate(vertices)
for _, triangle in ipairs(triangles) do
addObjectToWorld(o.shape, triangle, userdata, tile or object)
end
elseif o.shape == "polyline" then
local vertices = getPolygonVertices(o)
addObjectToWorld(o.shape, vertices, userdata, tile or object)
end
end
for _, tile in pairs(map.tiles) do
if map.tileInstances[tile.gid] then
for _, instance in ipairs(map.tileInstances[tile.gid]) do
-- Every object in every instance of a tile
if tile.objectGroup then
for _, object in ipairs(tile.objectGroup.objects) do
if object.properties.collidable == true then
object = utils.deepCopy(object)
object.dx = instance.x + object.x
object.dy = instance.y + object.y
calculateObjectPosition(object, instance)
end
end
end
-- Every instance of a tile
if tile.properties.collidable == true then
local object = {
shape = "rectangle",
x = instance.x,
y = instance.y,
width = map.tilewidth,
height = map.tileheight,
properties = tile.properties
}
calculateObjectPosition(object, instance)
end
end
end
end
for _, layer in ipairs(map.layers) do
-- Entire layer
if layer.properties.collidable == true then
if layer.type == "tilelayer" then
for gid, tiles in pairs(map.tileInstances) do
local tile = map.tiles[gid]
local tileset = map.tilesets[tile.tileset]
for _, instance in ipairs(tiles) do
if instance.layer == layer then
local object = {
shape = "rectangle",
x = instance.x,
y = instance.y,
width = tileset.tilewidth,
height = tileset.tileheight,
properties = tile.properties
}
calculateObjectPosition(object, instance)
end
end
end
elseif layer.type == "objectgroup" then
for _, object in ipairs(layer.objects) do
calculateObjectPosition(object)
end
elseif layer.type == "imagelayer" then
local object = {
shape = "rectangle",
x = layer.x or 0,
y = layer.y or 0,
width = layer.width,
height = layer.height,
properties = layer.properties
}
calculateObjectPosition(object)
end
end
-- Individual objects
if layer.type == "objectgroup" then
for _, object in ipairs(layer.objects) do
if object.properties.collidable == true then
calculateObjectPosition(object)
end
end
end
end
map.box2d_collision = collision
end,
--- Remove Box2D fixtures and shapes from world.
-- @param index The index or name of the layer being removed
box2d_removeLayer = function(map, index)
local layer = assert(map.layers[index], "Layer not found: " .. index)
local collision = map.box2d_collision
-- Remove collision objects
for i = #collision, 1, -1 do
local obj = collision[i]
if obj.object.layer == layer then
obj.fixture:destroy()
table.remove(collision, i)
end
end
end,
--- Draw Box2D physics world.
-- @param tx Translate on X
-- @param ty Translate on Y
-- @param sx Scale on X
-- @param sy Scale on Y
box2d_draw = function(map, tx, ty, sx, sy)
local collision = map.box2d_collision
lg.push()
lg.scale(sx or 1, sy or sx or 1)
lg.translate(math.floor(tx or 0), math.floor(ty or 0))
for _, obj in ipairs(collision) do
local points = {obj.body:getWorldPoints(obj.shape:getPoints())}
local shape_type = obj.shape:getType()
if shape_type == "edge" or shape_type == "chain" then
love.graphics.line(points)
elseif shape_type == "polygon" then
love.graphics.polygon("line", points)
else
error("sti box2d plugin does not support "..shape_type.." shapes")
end
end
lg.pop()
end
}
--- Custom Properties in Tiled are used to tell this plugin what to do.
-- @table Properties
-- @field collidable set to true, can be used on any Layer, Tile, or Object
-- @field sensor set to true, can be used on any Tile or Object that is also collidable
-- @field dynamic set to true, can be used on any Tile or Object
-- @field friction can be used to define the friction of any Object
-- @field restitution can be used to define the restitution of any Object
-- @field categories can be used to set the filter Category of any Object
-- @field mask can be used to set the filter Mask of any Object
-- @field group can be used to set the filter Group of any Object

193
sti/plugins/bump.lua Normal file
View File

@ -0,0 +1,193 @@
--- Bump.lua plugin for STI
-- @module bump.lua
-- @author David Serrano (BobbyJones|FrenchFryLord)
-- @copyright 2019
-- @license MIT/X11
local lg = require((...):gsub('plugins.bump', 'graphics'))
return {
bump_LICENSE = "MIT/X11",
bump_URL = "https://github.com/karai17/Simple-Tiled-Implementation",
bump_VERSION = "3.1.7.1",
bump_DESCRIPTION = "Bump hooks for STI.",
--- Adds each collidable tile to the Bump world.
-- @param world The Bump world to add objects to.
-- @return collidables table containing the handles to the objects in the Bump world.
bump_init = function(map, world)
local collidables = {}
for _, tileset in ipairs(map.tilesets) do
for _, tile in ipairs(tileset.tiles) do
local gid = tileset.firstgid + tile.id
if map.tileInstances[gid] then
for _, instance in ipairs(map.tileInstances[gid]) do
-- Every object in every instance of a tile
if tile.objectGroup then
for _, object in ipairs(tile.objectGroup.objects) do
if object.properties.collidable == true then
local t = {
name = object.name,
type = object.type,
x = instance.x + map.offsetx + object.x,
y = instance.y + map.offsety + object.y,
width = object.width,
height = object.height,
layer = instance.layer,
properties = object.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
end
-- Every instance of a tile
if tile.properties and tile.properties.collidable == true then
local t = {
x = instance.x + map.offsetx,
y = instance.y + map.offsety,
width = map.tilewidth,
height = map.tileheight,
layer = instance.layer,
type = tile.type,
properties = tile.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
end
end
end
for _, layer in ipairs(map.layers) do
-- Entire layer
if layer.properties.collidable == true then
if layer.type == "tilelayer" then
for y, tiles in ipairs(layer.data) do
for x, tile in pairs(tiles) do
if tile.objectGroup then
for _, object in ipairs(tile.objectGroup.objects) do
if object.properties.collidable == true then
local t = {
name = object.name,
type = object.type,
x = ((x-1) * map.tilewidth + tile.offset.x + map.offsetx) + object.x,
y = ((y-1) * map.tileheight + tile.offset.y + map.offsety) + object.y,
width = object.width,
height = object.height,
layer = layer,
properties = object.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
end
local t = {
x = (x-1) * map.tilewidth + tile.offset.x + map.offsetx,
y = (y-1) * map.tileheight + tile.offset.y + map.offsety,
width = tile.width,
height = tile.height,
layer = layer,
type = tile.type,
properties = tile.properties
}
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end
end
elseif layer.type == "imagelayer" then
world:add(layer, layer.x, layer.y, layer.width, layer.height)
table.insert(collidables, layer)
end
end
-- individual collidable objects in a layer that is not "collidable"
-- or whole collidable objects layer
if layer.type == "objectgroup" then
for _, obj in ipairs(layer.objects) do
if layer.properties.collidable == true or obj.properties.collidable == true then
if obj.shape == "rectangle" then
local t = {
name = obj.name,
type = obj.type,
x = obj.x + map.offsetx,
y = obj.y + map.offsety,
width = obj.width,
height = obj.height,
layer = layer,
properties = obj.properties
}
if obj.gid then
t.y = t.y - obj.height
end
world:add(t, t.x, t.y, t.width, t.height)
table.insert(collidables, t)
end -- TODO implement other object shapes?
end
end
end
end
map.bump_world = world
map.bump_collidables = collidables
end,
--- Remove layer
-- @param index to layer to be removed
bump_removeLayer = function(map, index)
local layer = assert(map.layers[index], "Layer not found: " .. index)
local collidables = map.bump_collidables
-- Remove collision objects
for i = #collidables, 1, -1 do
local obj = collidables[i]
if obj.layer == layer
and (
layer.properties.collidable == true
or obj.properties.collidable == true
) then
map.bump_world:remove(obj)
table.remove(collidables, i)
end
end
end,
--- Draw bump collisions world.
-- @param world bump world holding the tiles geometry
-- @param tx Translate on X
-- @param ty Translate on Y
-- @param sx Scale on X
-- @param sy Scale on Y
bump_draw = function(map, tx, ty, sx, sy)
lg.push()
lg.scale(sx or 1, sy or sx or 1)
lg.translate(math.floor(tx or 0), math.floor(ty or 0))
local items = map.bump_world:getItems()
for _, item in ipairs(items) do
lg.rectangle("line", map.bump_world:getRect(item))
end
lg.pop()
end
}
--- Custom Properties in Tiled are used to tell this plugin what to do.
-- @table Properties
-- @field collidable set to true, can be used on any Layer, Tile, or Object

217
sti/utils.lua Normal file
View File

@ -0,0 +1,217 @@
-- Some utility functions that shouldn't be exposed.
local utils = {}
-- https://github.com/stevedonovan/Penlight/blob/master/lua/pl/path.lua#L286
function utils.format_path(path)
local np_gen1,np_gen2 = '[^SEP]+SEP%.%.SEP?','SEP+%.?SEP'
local np_pat1, np_pat2 = np_gen1:gsub('SEP','/'), np_gen2:gsub('SEP','/')
local k
repeat -- /./ -> /
path,k = path:gsub(np_pat2,'/',1)
until k == 0
repeat -- A/../ -> (empty)
path,k = path:gsub(np_pat1,'',1)
until k == 0
if path == '' then path = '.' end
return path
end
-- Compensation for scale/rotation shift
function utils.compensate(tile, tileX, tileY, tileW, tileH)
local compx = 0
local compy = 0
if tile.sx < 0 then compx = tileW end
if tile.sy < 0 then compy = tileH end
if tile.r > 0 then
tileX = tileX + tileH - compy
tileY = tileY + tileH + compx - tileW
elseif tile.r < 0 then
tileX = tileX + compy
tileY = tileY - compx + tileH
else
tileX = tileX + compx
tileY = tileY + compy
end
return tileX, tileY
end
-- Cache images in main STI module
function utils.cache_image(sti, path, image)
image = image or love.graphics.newImage(path)
image:setFilter("nearest", "nearest")
sti.cache[path] = image
end
-- We just don't know.
function utils.get_tiles(imageW, tileW, margin, spacing)
imageW = imageW - margin
local n = 0
while imageW >= tileW do
imageW = imageW - tileW
if n ~= 0 then imageW = imageW - spacing end
if imageW >= 0 then n = n + 1 end
end
return n
end
-- Decompress tile layer data
function utils.get_decompressed_data(data)
local ffi = require "ffi"
local d = {}
local decoded = ffi.cast("uint32_t*", data)
for i = 0, data:len() / ffi.sizeof("uint32_t") do
table.insert(d, tonumber(decoded[i]))
end
return d
end
-- Convert a Tiled ellipse object to a LOVE polygon
function utils.convert_ellipse_to_polygon(x, y, w, h, max_segments)
local ceil = math.ceil
local cos = math.cos
local sin = math.sin
local function calc_segments(segments)
local function vdist(a, b)
local c = {
x = a.x - b.x,
y = a.y - b.y,
}
return c.x * c.x + c.y * c.y
end
segments = segments or 64
local vertices = {}
local v = { 1, 2, ceil(segments/4-1), ceil(segments/4) }
local m
if love and love.physics then
m = love.physics.getMeter()
else
m = 32
end
for _, i in ipairs(v) do
local angle = (i / segments) * math.pi * 2
local px = x + w / 2 + cos(angle) * w / 2
local py = y + h / 2 + sin(angle) * h / 2
table.insert(vertices, { x = px / m, y = py / m })
end
local dist1 = vdist(vertices[1], vertices[2])
local dist2 = vdist(vertices[3], vertices[4])
-- Box2D threshold
if dist1 < 0.0025 or dist2 < 0.0025 then
return calc_segments(segments-2)
end
return segments
end
local segments = calc_segments(max_segments)
local vertices = {}
table.insert(vertices, { x = x + w / 2, y = y + h / 2 })
for i = 0, segments do
local angle = (i / segments) * math.pi * 2
local px = x + w / 2 + cos(angle) * w / 2
local py = y + h / 2 + sin(angle) * h / 2
table.insert(vertices, { x = px, y = py })
end
return vertices
end
function utils.rotate_vertex(map, vertex, x, y, cos, sin, oy)
if map.orientation == "isometric" then
x, y = utils.convert_isometric_to_screen(map, x, y)
vertex.x, vertex.y = utils.convert_isometric_to_screen(map, vertex.x, vertex.y)
end
vertex.x = vertex.x - x
vertex.y = vertex.y - y
return
x + cos * vertex.x - sin * vertex.y,
y + sin * vertex.x + cos * vertex.y - (oy or 0)
end
--- Project isometric position to cartesian position
function utils.convert_isometric_to_screen(map, x, y)
local mapW = map.width
local tileW = map.tilewidth
local tileH = map.tileheight
local tileX = x / tileH
local tileY = y / tileH
local offsetX = mapW * tileW / 2
return
(tileX - tileY) * tileW / 2 + offsetX,
(tileX + tileY) * tileH / 2
end
function utils.hex_to_color(hex)
if hex:sub(1, 1) == "#" then
hex = hex:sub(2)
end
return {
r = tonumber(hex:sub(1, 2), 16) / 255,
g = tonumber(hex:sub(3, 4), 16) / 255,
b = tonumber(hex:sub(5, 6), 16) / 255
}
end
function utils.pixel_function(_, _, r, g, b, a)
local mask = utils._TC
if r == mask.r and
g == mask.g and
b == mask.b then
return r, g, b, 0
end
return r, g, b, a
end
function utils.fix_transparent_color(tileset, path)
local image_data = love.image.newImageData(path)
tileset.image = love.graphics.newImage(image_data)
if tileset.transparentcolor then
utils._TC = utils.hex_to_color(tileset.transparentcolor)
image_data:mapPixel(utils.pixel_function)
tileset.image = love.graphics.newImage(image_data)
end
end
function utils.deepCopy(t)
local copy = {}
for k,v in pairs(t) do
if type(v) == "table" then
v = utils.deepCopy(v)
end
copy[k] = v
end
return copy
end
return utils