improvement(gamecore): update gamecore to 0.5.0

This commit is contained in:
Kazhnuz 2019-06-16 17:38:35 +02:00
parent 4d772de731
commit 14ff853498
51 changed files with 9245 additions and 488 deletions

View File

@ -0,0 +1,46 @@
-- callbacks.lua :: load the callbacks from love2D
--[[
Copyright © 2019 Kazhnuz
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.
]]
function love.update(dt)
core:update(dt)
end
function love.draw()
core:draw()
end
function love.mousemoved(x, y, dx, dy)
core:mousemoved(x, y, dx, dy)
end
function love.mousepressed( x, y, button, istouch )
core:mousepressed(x, y, button, istouch)
end
function love.keypressed( key, scancode, isrepeat )
core:keypressed( key, scancode, isrepeat )
end
function love.keyreleased(key)
core:keyreleased( key )
end

View File

@ -23,7 +23,8 @@
local DebugSystem = Object:extend()
local lovebird = require("libs.lovebird")
local cwd = (...):gsub('%.debug$', '') .. "."
local lovebird = require(cwd .. "libs.lovebird")
function DebugSystem:new(controller, active)
self.controller = controller

View File

@ -23,15 +23,29 @@
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local cwd = (...):gsub('%.init$', '') .. "."
-- GLOBAL UTILS/FUNCTION LOADING
-- Load in the global namespace utilities that'll need to be reusable everywhere
-- in the game
Object = require(cwd .. "libs.classic")
utils = require(cwd .. "utils")
local CoreSystem = Object:extend()
local DebugSystem = require "core.debug"
local DebugSystem = require(cwd .. "debug")
local Options = require "core.options"
local Input = require "core.input"
local Screen = require "core.screen"
local Lang = require "core.lang"
local SceneManager= require "core.scenemanager"
local Options = require(cwd .. "options")
local Input = require(cwd .. "input")
local Screen = require(cwd .. "screen")
local Lang = require(cwd .. "lang")
local SceneManager = require(cwd .. "scenemanager")
require(cwd .. "callbacks")
-- INIT FUNCTIONS
-- Initialize and configure the core object
function CoreSystem:new()
self.debug = DebugSystem(self)
@ -39,8 +53,12 @@ function CoreSystem:new()
self.input = Input(self)
self.screen = Screen(self)
self.scenemanager = SceneManager(self)
self.lang = Lang(self)
end
-- MOUSE FUNCTIONS
-- get directly the mouse when needed
function CoreSystem:mousemoved(x, y, dx, dy)
local x, y = self.screen:project(x, y)
local dx, dy = self.screen:project(dx, dy)
@ -52,6 +70,20 @@ function CoreSystem:mousepressed( x, y, button, istouch )
self.scenemanager:mousepressed( x, y, button, istouch )
end
-- KEYBOARD FUNCTIONS
-- get directly the keyboard when needed
function CoreSystem:keypressed( key, scancode, isrepeat )
self.scenemanager:keypressed( key, scancode, isrepeat )
end
function CoreSystem:keyreleased( key )
self.scenemanager:keyreleased( key )
end
-- UPDATE FUNCTIONS
-- Load every sytem every update functions of the scene and objects
function CoreSystem:update(dt)
self.debug:update(dt)
self.input:update(dt)
@ -59,10 +91,16 @@ function CoreSystem:update(dt)
self.scenemanager:update(dt)
end
-- DRAW FUNCTIONS
-- Draw the whole game
function CoreSystem:draw()
self.scenemanager:draw()
end
-- EXIT FUNCTIONS
-- Quit the game
function CoreSystem:exit()
self.options:save()
love.event.quit()

View File

@ -24,21 +24,32 @@
local InputManager = Object:extend()
-- INIT FUNCTIONS
-- Initialize and configure the controller system
function InputManager:new(controller)
self.controller = controller
self.data = self.controller.options.data.input[1]
self.data = self.controller.options:getInputData()
self.keys = self:getKeyList()
self.fakekeys = self:getKeyList()
self:initKeys()
end
function InputManager:isDown(padkey)
function InputManager:initKeys()
self.fakekeys = self:getKeyList(1)
self.sources = self:getSources()
self.fakesources = self:getSources()
end
-- INFO FUNCTIONS
-- Get functions from the controller object
function InputManager:isDown(sourceid, padkey)
local isdown = false
if self.data.type == "keyboard" then
local key = self.data.keys[padkey]
if self.data[sourceid].type == "keyboard" then
local key = self.data[sourceid].keys[padkey]
isdown = love.keyboard.isDown(key)
if isdown then
end
else
print("Warning: unsupported input device")
end
@ -46,72 +57,93 @@ function InputManager:isDown(padkey)
return isdown
end
function InputManager:getKeyList()
function InputManager:getSources()
local sources = {}
for i,v in ipairs(self.data) do
sources[i] = {}
sources[i].keys = self:getKeyList(i)
end
return sources
end
function InputManager:getKeyList(sourceid)
local keys = {}
for k,v in pairs(self.data.keys) do
keys[k] = {}
keys[k].isDown = false
keys[k].isPressed = false
keys[k].isReleased = false
keys[k].test = "ok"
if self.data[sourceid] ~= nil then
for k,v in pairs(self.data[sourceid].keys) do
keys[k] = {}
keys[k].isDown = false
keys[k].isPressed = false
keys[k].isReleased = false
keys[k].test = "ok"
end
end
return keys
end
function InputManager:translateAction(key)
--TODO:depreciated function
local padkey = ""
for k,v in pairs(self.data.keys) do
if v == key then padkey = k end
end
return padkey
end
function InputManager:getKey(padkey)
function InputManager:getKey(sourceid, padkey)
local padkey = padkey
for k,v in pairs(self.data.keys) do
for k,v in pairs(self.data[sourceid].keys) do
if (k == padkey) then key = v end
end
return key
end
-- KEY MANAGEMENT FUNCTIONS
-- Manage pressed keys
function InputManager:flushKeys()
self.keys = {}
for k,v in pairs(self.data.keys) do
self.keys[k] = {}
self.keys[k].isDown = false
self.keys[k].isPressed = false
self.keys[k].isReleased = false
self.keys[k].test = "ok"
for i,v in ipairs(self.sources) do
self:flushSourceKeys(i)
end
end
function InputManager:update(dt)
for k,v in pairs(self.keys) do
local isDown = self:isDown(k)
function InputManager:flushSourceKeys(sourceid)
self.keys = {}
for k,v in pairs(self.sources[sourceid].keys) do
v = {}
v.isDown = false
v.isPressed = false
v.isReleased = false
end
end
function InputManager:checkKeys(sourceid)
for k,v in pairs(self.sources[sourceid].keys) do
local isDown = self:isDown(sourceid, k)
if (isDown) then
if not (self.keys[k].isDown) then
self.keys[k].isDown = true
self.keys[k].isPressed = true
self.keys[k].isReleased = false
if not (self.sources[sourceid].keys[k].isDown) then
self.sources[sourceid].keys[k].isDown = true
self.sources[sourceid].keys[k].isPressed = true
self.sources[sourceid].keys[k].isReleased = false
else
if (self.keys[k].isPressed) then
self.keys[k].isPressed = false
if (self.sources[sourceid].keys[k].isPressed) then
self.sources[sourceid].keys[k].isPressed = false
end
end
else
if (self.keys[k].isDown) then
self.keys[k].isDown = false
self.keys[k].isPressed = false
self.keys[k].isReleased = true
if (self.sources[sourceid].keys[k].isDown) then
self.sources[sourceid].keys[k].isDown = false
self.sources[sourceid].keys[k].isPressed = false
self.sources[sourceid].keys[k].isReleased = true
else
if (self.keys[k].isReleased) then
self.keys[k].isReleased = false
if (self.sources[sourceid].keys[k].isReleased) then
self.sources[sourceid].keys[k].isReleased = false
end
end
end
end
end
-- UPDATE FUNCTIONS
-- Check every step pressed keys
function InputManager:update(dt)
for i,v in ipairs(self.sources) do
self:checkKeys(i)
end
end
return InputManager

View File

@ -23,30 +23,107 @@
]]
local LanguageManager = Object:extend()
local langs = require "datas.languages"
local TRANSLATION_PATH = "datas/languages/"
-- INIT FUNCTIONS
-- Initialize and configure the translation system
function LanguageManager:new(controller)
self.controller = controller
self:setLang(self.controller.options.data.language)
end
function LanguageManager:getStringList(library, file)
return require(self.lang .. "." .. library .. "." .. file)
end
function LanguageManager:getLangName(lang)
local langnames = langs.available_langs
return langnames[lang]
end
function LanguageManager:getCurrentLangName()
local langnames = langs.available_langs
return langnames[self.lang]
self.data = self:getTranslationData()
self:setLang(self.controller.options.data.language.current)
end
function LanguageManager:setLang(lang)
self.controller.options.data.language = lang
self.lang = self.controller.options.data.language
self.controller.options.data.language.current = lang
self.lang = self.controller.options.data.language.current
end
-- INFO FUNCTIONS
-- Get informations from the translation manager
function LanguageManager:getCurrentLang()
return self.data.language.current
end
function LanguageManager:getDefaultLang()
return self.data.language.default
end
function LanguageManager:getTranslationData()
return self.controller.options.data.language
end
function LanguageManager:getLangMetadata(lang)
local langfilepath = self.data.path .. lang
return require(langfilepath)
end
function LanguageManager:getLangName(lang)
local metadata = self:getLangMetadata(lang)
return metadata.name
end
function LanguageManager:getCurrentLangName()
return self:getLangName(self.data.current)
end
function LanguageManager:isLangAvailable(lang)
local isAvailable = false
for i,v in ipairs(self.data.available) do
if v == lang then
isAvailable = true
end
end
return isAvailable
end
-- TRANSLATION FUNCTIONS
-- get the translation of a string
function LanguageManager:getTranslationStringList(lang, library)
local _path = self.data.path .. lang .. "/" .. library
local fileinfo = love.filesystem.getInfo(_path .. ".lua")
local list = nil
if fileinfo ~= nil then
list = require(_path)
else
print("WARNING: file " .. _path .. " do not exists")
end
return list
end
function LanguageManager:translateFromLang(lang, library, stringToTranslate)
local _stringlist = self:getTranslationStringList(lang, library)
if _stringlist == nil then
return nil
else
return _stringlist[stringToTranslate]
end
end
function LanguageManager:translate(library, string)
local translation = self:translateFromLang(self.data.current, library, string)
if (translation == nil) then
translation = self:translateFromLang(self.data.default, library, string)
end
if (translation == nil) then
translation = string
print("WARNING: no translation path found for " .. string .. " in " .. library)
end
return translation
end
return LanguageManager

View File

@ -0,0 +1,687 @@
-- binser.lua
--[[
Copyright (c) 2016 Calvin Rose
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.
]]
local assert = assert
local error = error
local select = select
local pairs = pairs
local getmetatable = getmetatable
local setmetatable = setmetatable
local tonumber = tonumber
local type = type
local loadstring = loadstring or load
local concat = table.concat
local char = string.char
local byte = string.byte
local format = string.format
local sub = string.sub
local dump = string.dump
local floor = math.floor
local frexp = math.frexp
local unpack = unpack or table.unpack
-- Lua 5.3 frexp polyfill
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
if not frexp then
local log, abs, floor = math.log, math.abs, math.floor
local log2 = log(2)
frexp = function(x)
if x == 0 then return 0, 0 end
local e = floor(log(abs(x)) / log2 + 1)
return x / 2 ^ e, e
end
end
-- NIL = 202
-- FLOAT = 203
-- TRUE = 204
-- FALSE = 205
-- STRING = 206
-- TABLE = 207
-- REFERENCE = 208
-- CONSTRUCTOR = 209
-- FUNCTION = 210
-- RESOURCE = 211
-- INT64 = 212
local mts = {}
local ids = {}
local serializers = {}
local deserializers = {}
local resources = {}
local resources_by_name = {}
local function pack(...)
return {...}, select("#", ...)
end
local function not_array_index(x, len)
return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
end
local function type_check(x, tp, name)
assert(type(x) == tp,
format("Expected parameter %q to be of type %q.", name, tp))
end
local bigIntSupport = false
local isInteger
if math.type then -- Detect Lua 5.3
local mtype = math.type
bigIntSupport = loadstring[[
local char = string.char
return function(n)
local nn = n < 0 and -(n + 1) or n
local b1 = nn // 0x100000000000000
local b2 = nn // 0x1000000000000 % 0x100
local b3 = nn // 0x10000000000 % 0x100
local b4 = nn // 0x100000000 % 0x100
local b5 = nn // 0x1000000 % 0x100
local b6 = nn // 0x10000 % 0x100
local b7 = nn // 0x100 % 0x100
local b8 = nn % 0x100
if n < 0 then
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
end]]()
isInteger = function(x)
return mtype(x) == 'integer'
end
else
isInteger = function(x)
return floor(x) == x
end
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
local function number_to_str(n)
if isInteger(n) then -- int
if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
return char(n + 27)
elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
n = n + 8192
return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
elseif bigIntSupport then
return bigIntSupport(n)
end
end
local sign = 0
if n < 0.0 then
sign = 0x80
n = -n
end
local m, e = frexp(n) -- mantissa, exponent
if m ~= m then
return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
elseif m == 1/0 then
if sign == 0 then
return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
else
return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
end
end
e = e + 0x3FE
if e < 1 then -- denormalized numbers
m = m * 2 ^ (52 + e)
e = 0
else
m = (m * 2 - 1) * 2 ^ 52
end
return char(203,
sign + floor(e / 0x10),
(e % 0x10) * 0x10 + floor(m / 0x1000000000000),
floor(m / 0x10000000000) % 0x100,
floor(m / 0x100000000) % 0x100,
floor(m / 0x1000000) % 0x100,
floor(m / 0x10000) % 0x100,
floor(m / 0x100) % 0x100,
m % 0x100)
end
-- Copyright (C) 2012-2015 Francois Perrad.
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
local function number_from_str(str, index)
local b = byte(str, index)
if b < 128 then
return b - 27, index + 1
elseif b < 192 then
return byte(str, index + 1) + 0x100 * (b - 128) - 8192, index + 2
end
local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
if b == 212 then
local flip = b1 >= 128
if flip then -- negative
b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
end
local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
if flip then
return (-n) - 1, index + 9
else
return n, index + 9
end
end
local sign = b1 > 0x7F and -1 or 1
local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
local n
if e == 0 then
if m == 0 then
n = sign * 0.0
else
n = sign * (m / 2 ^ 52) * 2 ^ -1022
end
elseif e == 0x7FF then
if m == 0 then
n = sign * (1/0)
else
n = 0.0/0.0
end
else
n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
end
return n, index + 9
end
local types = {}
types["nil"] = function(x, visited, accum)
accum[#accum + 1] = "\202"
end
function types.number(x, visited, accum)
accum[#accum + 1] = number_to_str(x)
end
function types.boolean(x, visited, accum)
accum[#accum + 1] = x and "\204" or "\205"
end
function types.string(x, visited, accum)
local alen = #accum
if visited[x] then
accum[alen + 1] = "\208"
accum[alen + 2] = number_to_str(visited[x])
else
visited[x] = visited.next
visited.next = visited.next + 1
accum[alen + 1] = "\206"
accum[alen + 2] = number_to_str(#x)
accum[alen + 3] = x
end
end
local function check_custom_type(x, visited, accum)
local res = resources[x]
if res then
accum[#accum + 1] = "\211"
types[type(res)](res, visited, accum)
return true
end
local mt = getmetatable(x)
local id = mt and ids[mt]
if id then
if x == visited.temp then
error("Infinite loop in constructor.")
end
visited.temp = x
accum[#accum + 1] = "\209"
types[type(id)](id, visited, accum)
local args, len = pack(serializers[id](x))
accum[#accum + 1] = number_to_str(len)
for i = 1, len do
local arg = args[i]
types[type(arg)](arg, visited, accum)
end
visited[x] = visited.next
visited.next = visited.next + 1
return true
end
end
function types.userdata(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
error("Cannot serialize this userdata.")
end
end
function types.table(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited.next
visited.next = visited.next + 1
local xlen = #x
accum[#accum + 1] = "\207"
accum[#accum + 1] = number_to_str(xlen)
for i = 1, xlen do
local v = x[i]
types[type(v)](v, visited, accum)
end
local key_count = 0
for k in pairs(x) do
if not_array_index(k, xlen) then
key_count = key_count + 1
end
end
accum[#accum + 1] = number_to_str(key_count)
for k, v in pairs(x) do
if not_array_index(k, xlen) then
types[type(k)](k, visited, accum)
types[type(v)](v, visited, accum)
end
end
end
end
types["function"] = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, accum) then return end
visited[x] = visited.next
visited.next = visited.next + 1
local str = dump(x)
accum[#accum + 1] = "\210"
accum[#accum + 1] = number_to_str(#str)
accum[#accum + 1] = str
end
end
types.cdata = function(x, visited, accum)
if visited[x] then
accum[#accum + 1] = "\208"
accum[#accum + 1] = number_to_str(visited[x])
else
if check_custom_type(x, visited, #accum) then return end
error("Cannot serialize this cdata.")
end
end
types.thread = function() error("Cannot serialize threads.") end
local function deserialize_value(str, index, visited)
local t = byte(str, index)
if not t then return end
if t < 128 then
return t - 27, index + 1
elseif t < 192 then
return byte(str, index + 1) + 0x100 * (t - 128) - 8192, index + 2
elseif t == 202 then
return nil, index + 1
elseif t == 203 then
return number_from_str(str, index)
elseif t == 204 then
return true, index + 1
elseif t == 205 then
return false, index + 1
elseif t == 206 then
local length, dataindex = deserialize_value(str, index + 1, visited)
local nextindex = dataindex + length
local substr = sub(str, dataindex, nextindex - 1)
visited[#visited + 1] = substr
return substr, nextindex
elseif t == 207 then
local count, nextindex = number_from_str(str, index + 1)
local ret = {}
visited[#visited + 1] = ret
for i = 1, count do
ret[i], nextindex = deserialize_value(str, nextindex, visited)
end
count, nextindex = number_from_str(str, nextindex)
for i = 1, count do
local k, v
k, nextindex = deserialize_value(str, nextindex, visited)
v, nextindex = deserialize_value(str, nextindex, visited)
ret[k] = v
end
return ret, nextindex
elseif t == 208 then
local ref, nextindex = number_from_str(str, index + 1)
return visited[ref], nextindex
elseif t == 209 then
local count
local name, nextindex = deserialize_value(str, index + 1, visited)
count, nextindex = number_from_str(str, nextindex)
local args = {}
for i = 1, count do
args[i], nextindex = deserialize_value(str, nextindex, visited)
end
local ret = deserializers[name](unpack(args))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 210 then
local length, dataindex = deserialize_value(str, index + 1, visited)
local nextindex = dataindex + length
local ret = loadstring(sub(str, dataindex, nextindex - 1))
visited[#visited + 1] = ret
return ret, nextindex
elseif t == 211 then
local res, nextindex = deserialize_value(str, index + 1, visited)
return resources_by_name[res], nextindex
elseif t == 212 then
return number_from_str(str, index)
else
error("Could not deserialize type byte " .. t .. ".")
end
end
local function serialize(...)
local visited = {next = 1}
local accum = {}
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
return concat(accum)
end
local function make_file_writer(file)
return setmetatable({}, {
__newindex = function(_, _, v)
file:write(v)
end
})
end
local function serialize_to_file(path, mode, ...)
local file, err = io.open(path, mode)
assert(file, err)
local visited = {next = 1}
local accum = make_file_writer(file)
for i = 1, select("#", ...) do
local x = select(i, ...)
types[type(x)](x, visited, accum)
end
-- flush the writer
file:flush()
file:close()
end
local function writeFile(path, ...)
return serialize_to_file(path, "wb", ...)
end
local function appendFile(path, ...)
return serialize_to_file(path, "ab", ...)
end
local function deserialize(str, index)
assert(type(str) == "string", "Expected string to deserialize.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while index do
val, index = deserialize_value(str, index, visited)
if index then
len = len + 1
vals[len] = val
end
end
return vals, len
end
local function deserializeN(str, n, index)
assert(type(str) == "string", "Expected string to deserialize.")
n = n or 1
assert(type(n) == "number", "Expected a number for parameter n.")
assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
local vals = {}
index = index or 1
local visited = {}
local len = 0
local val
while index and len < n do
val, index = deserialize_value(str, index, visited)
if index then
len = len + 1
vals[len] = val
end
end
vals[len + 1] = index
return unpack(vals, 1, n + 1)
end
local function readFile(path)
local file, err = io.open(path, "rb")
assert(file, err)
local str = file:read("*all")
file:close()
return deserialize(str)
end
local function default_deserialize(metatable)
return function(...)
local ret = {}
for i = 1, select("#", ...), 2 do
ret[select(i, ...)] = select(i + 1, ...)
end
return setmetatable(ret, metatable)
end
end
local function default_serialize(x)
assert(type(x) == "table",
"Default serialization for custom types only works for tables.")
local args = {}
local len = 0
for k, v in pairs(x) do
args[len + 1], args[len + 2] = k, v
len = len + 2
end
return unpack(args, 1, len)
end
-- Templating
local function normalize_template(template)
local ret = {}
for i = 1, #template do
ret[i] = template[i]
end
local non_array_part = {}
-- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
-- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
-- in templates. Looking for way around this.
for k in pairs(template) do
if not_array_index(k, #template) then
non_array_part[#non_array_part + 1] = k
end
end
table.sort(non_array_part)
for i = 1, #non_array_part do
local name = non_array_part[i]
ret[#ret + 1] = {name, normalize_template(template[name])}
end
return ret
end
local function templatepart_serialize(part, argaccum, x, len)
local extras = {}
local extracount = 0
for k, v in pairs(x) do
extras[k] = v
extracount = extracount + 1
end
for i = 1, #part do
extracount = extracount - 1
if type(part[i]) == "table" then
extras[part[i][1]] = nil
len = templatepart_serialize(part[i][2], argaccum, x[part[i][1]], len)
else
extras[part[i]] = nil
len = len + 1
argaccum[len] = x[part[i]]
end
end
if extracount > 0 then
argaccum[len + 1] = extras
else
argaccum[len + 1] = nil
end
return len + 1
end
local function templatepart_deserialize(ret, part, values, vindex)
for i = 1, #part do
local name = part[i]
if type(name) == "table" then
local newret = {}
ret[name[1]] = newret
vindex = templatepart_deserialize(newret, name[2], values, vindex)
else
ret[name] = values[vindex]
vindex = vindex + 1
end
end
local extras = values[vindex]
if extras then
for k, v in pairs(extras) do
ret[k] = v
end
end
return vindex + 1
end
local function template_serializer_and_deserializer(metatable, template)
return function(x)
argaccum = {}
local len = templatepart_serialize(template, argaccum, x, 0)
return unpack(argaccum, 1, len)
end, function(...)
local ret = {}
local len = select("#", ...)
local args = {...}
templatepart_deserialize(ret, template, args, 1)
return setmetatable(ret, metatable)
end
end
local function register(metatable, name, serialize, deserialize)
name = name or metatable.name
serialize = serialize or metatable._serialize
deserialize = deserialize or metatable._deserialize
if not serialize then
if metatable._template then
local t = normalize_template(metatable._template)
serialize, deserialize = template_serializer_and_deserializer(metatable, t)
elseif not deserialize then
serialize = default_serialize
deserialize = default_deserialize(metatable)
else
serialize = metatable
end
end
type_check(metatable, "table", "metatable")
type_check(name, "string", "name")
type_check(serialize, "function", "serialize")
type_check(deserialize, "function", "deserialize")
assert(not ids[metatable], "Metatable already registered.")
assert(not mts[name], ("Name %q already registered."):format(name))
mts[name] = metatable
ids[metatable] = name
serializers[name] = serialize
deserializers[name] = deserialize
return metatable
end
local function unregister(item)
local name, metatable
if type(item) == "string" then -- assume name
name, metatable = item, mts[item]
else -- assume metatable
name, metatable = ids[item], item
end
type_check(name, "string", "name")
type_check(metatable, "table", "metatable")
mts[name] = nil
ids[metatable] = nil
serializers[name] = nil
deserializers[name] = nil
return metatable
end
local function registerClass(class, name)
name = name or class.name
if class.__instanceDict then -- middleclass
register(class.__instanceDict, name)
else -- assume 30log or similar library
register(class, name)
end
return class
end
local function registerResource(resource, name)
type_check(name, "string", "name")
assert(not resources[resource],
"Resource already registered.")
assert(not resources_by_name[name],
format("Resource %q already exists.", name))
resources_by_name[name] = resource
resources[resource] = name
return resource
end
local function unregisterResource(name)
type_check(name, "string", "name")
assert(resources_by_name[name], format("Resource %q does not exist.", name))
local resource = resources_by_name[name]
resources_by_name[name] = nil
resources[resource] = nil
return resource
end
return {
-- aliases
s = serialize,
d = deserialize,
dn = deserializeN,
r = readFile,
w = writeFile,
a = appendFile,
serialize = serialize,
deserialize = deserialize,
deserializeN = deserializeN,
readFile = readFile,
writeFile = writeFile,
appendFile = appendFile,
register = register,
unregister = unregister,
registerResource = registerResource,
unregisterResource = unregisterResource,
registerClass = registerClass
}

View File

@ -0,0 +1,68 @@
--
-- classic
--
-- Copyright (c) 2014, rxi
--
-- This module is free software; you can redistribute it and/or modify it under
-- the terms of the MIT license. See LICENSE for details.
--
local Object = {}
Object.__index = Object
function Object:new()
end
function Object:extend()
local cls = {}
for k, v in pairs(self) do
if k:find("__") == 1 then
cls[k] = v
end
end
cls.__index = cls
cls.super = self
setmetatable(cls, self)
return cls
end
function Object:implement(...)
for _, cls in pairs({...}) do
for k, v in pairs(cls) do
if self[k] == nil and type(v) == "function" then
self[k] = v
end
end
end
end
function Object:is(T)
local mt = getmetatable(self)
while mt do
if mt == T then
return true
end
mt = getmetatable(mt)
end
return false
end
function Object:__tostring()
return "Object"
end
function Object:__call(...)
local obj = setmetatable({}, self)
obj:new(...)
return obj
end
return Object

View File

@ -0,0 +1,99 @@
--[[
CScreen v1.3 by CodeNMore
A simple way to make resolution-independent Love2D games
Tested for LOVE 0.10.1
See: https://github.com/CodeNMore/CScreen
Zlib License:
Copyright (c) 2016 CodeNMore
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from
the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software in
a product, an acknowledgment in the product documentation would be appreciated
but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
--]]
local CScreen = {}
local rx, ry, ctr = 800, 600, true
local rxv, ryv, fsv, fsvr = 800, 600, 1.0, 1.0
local tx, ty, rwf, rhf = 0, 0, 800, 600
local cr, cg, cb, ca = 0, 0, 0, 255
-- Initializes CScreen with the initial size values
function CScreen.init(tw, th, cntr)
rx = tw or 800
ry = th or 600
ctr = cntr or false
CScreen.update(love.graphics.getWidth(), love.graphics.getHeight())
end
-- Draws letterbox borders
function CScreen.cease()
if ctr then
local pr, pg, pb, pa = love.graphics.getColor()
love.graphics.setColor(cr, cg, cb, ca)
love.graphics.scale(fsvr, fsvr)
if tx ~= 0 then
love.graphics.rectangle("fill", -tx, 0, tx, rhf)
love.graphics.rectangle("fill", rxv, 0, tx, rhf)
elseif ty ~= 0 then
love.graphics.rectangle("fill", 0, -ty, rwf, ty)
love.graphics.rectangle("fill", 0, ryv, rwf, ty)
end
love.graphics.setColor(pr, pg, pb, pa)
end
end
-- Scales and centers all graphics properly
function CScreen.apply()
if ctr then
love.graphics.translate(tx, ty)
end
love.graphics.scale(fsv, fsv)
end
-- Updates CScreen when the window size changes
function CScreen.update(w, h)
local sx = w / rx
local sy = h / ry
fsv = math.min(sx, sy)
fsvr = 1 / fsv
-- Centering
if ctr and fsv == sx then -- Vertically
tx = 0
ty = (h / 2) - (ry * fsv / 2)
elseif ctr and fsv == sy then -- Horizontally
ty = 0
tx = (w / 2) - (rx * fsv / 2)
end
-- Variable sets
rwf = w
rhf = h
rxv = rx * fsv
ryv = ry * fsv
end
-- Convert from window coordinates to target coordinates
function CScreen.project(x, y)
return math.floor((x - tx) / fsv), math.floor((y - ty) / fsv)
end
-- Change letterbox color
function CScreen.setColor(r, g, b, a)
cr = r
cg = g
cb = b
ca = a
end
-- Return the table for use
return CScreen

View File

@ -0,0 +1,737 @@
--
-- lovebird
--
-- Copyright (c) 2017 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local socket = require "socket"
local lovebird = { _version = "0.4.3" }
lovebird.loadstring = loadstring or load
lovebird.inited = false
lovebird.host = "*"
lovebird.buffer = ""
lovebird.lines = {}
lovebird.connections = {}
lovebird.pages = {}
lovebird.wrapprint = true
lovebird.timestamp = true
lovebird.allowhtml = false
lovebird.echoinput = true
lovebird.port = 8000
lovebird.whitelist = { "127.0.0.1" }
lovebird.maxlines = 200
lovebird.updateinterval = .5
lovebird.pages["index"] = [[
<?lua
-- Handle console input
if req.parsedbody.input then
local str = req.parsedbody.input
if lovebird.echoinput then
lovebird.pushline({ type = 'input', str = str })
end
if str:find("^=") then
str = "print(" .. str:sub(2) .. ")"
end
xpcall(function() assert(lovebird.loadstring(str, "input"))() end,
lovebird.onerror)
end
?>
<!doctype html>
<html>
<head>
<meta http-equiv="x-ua-compatible" content="IE=Edge"/>
<meta charset="utf-8">
<title>lovebird</title>
<style>
body {
margin: 0px;
font-size: 14px;
font-family: helvetica, verdana, sans;
background: #FFFFFF;
}
form {
margin-bottom: 0px;
}
.timestamp {
color: #909090;
padding-right: 4px;
}
.repeatcount {
color: #F0F0F0;
background: #505050;
font-size: 11px;
font-weight: bold;
text-align: center;
padding-left: 4px;
padding-right: 4px;
padding-top: 0px;
padding-bottom: 0px;
border-radius: 7px;
display: inline-block;
}
.errormarker {
color: #F0F0F0;
background: #8E0000;
font-size: 11px;
font-weight: bold;
text-align: center;
border-radius: 8px;
width: 17px;
padding-top: 0px;
padding-bottom: 0px;
display: inline-block;
}
.greybordered {
margin: 12px;
background: #F0F0F0;
border: 1px solid #E0E0E0;
border-radius: 3px;
}
.inputline {
font-family: mono, courier;
font-size: 13px;
color: #606060;
}
.inputline:before {
content: '\00B7\00B7\00B7';
padding-right: 5px;
}
.errorline {
color: #8E0000;
}
#header {
background: #101010;
height: 25px;
color: #F0F0F0;
padding: 9px
}
#title {
float: left;
font-size: 20px;
}
#title a {
color: #F0F0F0;
text-decoration: none;
}
#title a:hover {
color: #FFFFFF;
}
#version {
font-size: 10px;
}
#status {
float: right;
font-size: 14px;
padding-top: 4px;
}
#main a {
color: #000000;
text-decoration: none;
background: #E0E0E0;
border: 1px solid #D0D0D0;
border-radius: 3px;
padding-left: 2px;
padding-right: 2px;
display: inline-block;
}
#main a:hover {
background: #D0D0D0;
border: 1px solid #C0C0C0;
}
#console {
position: absolute;
top: 40px; bottom: 0px; left: 0px; right: 312px;
}
#input {
position: absolute;
margin: 10px;
bottom: 0px; left: 0px; right: 0px;
}
#inputbox {
width: 100%;
font-family: mono, courier;
font-size: 13px;
}
#output {
overflow-y: scroll;
position: absolute;
margin: 10px;
line-height: 17px;
top: 0px; bottom: 36px; left: 0px; right: 0px;
}
#env {
position: absolute;
top: 40px; bottom: 0px; right: 0px;
width: 300px;
}
#envheader {
padding: 5px;
background: #E0E0E0;
}
#envvars {
position: absolute;
left: 0px; right: 0px; top: 25px; bottom: 0px;
margin: 10px;
overflow-y: scroll;
font-size: 12px;
}
</style>
</head>
<body>
<div id="header">
<div id="title">
<a href="https://github.com/rxi/lovebird">lovebird</a>
<span id="version"><?lua echo(lovebird._version) ?></span>
</div>
<div id="status"></div>
</div>
<div id="main">
<div id="console" class="greybordered">
<div id="output"> <?lua echo(lovebird.buffer) ?> </div>
<div id="input">
<form method="post"
onkeydown="return onInputKeyDown(event);"
onsubmit="onInputSubmit(); return false;">
<input id="inputbox" name="input" type="text"
autocomplete="off"></input>
</form>
</div>
</div>
<div id="env" class="greybordered">
<div id="envheader"></div>
<div id="envvars"></div>
</div>
</div>
<script>
document.getElementById("inputbox").focus();
var changeFavicon = function(href) {
var old = document.getElementById("favicon");
if (old) document.head.removeChild(old);
var link = document.createElement("link");
link.id = "favicon";
link.rel = "shortcut icon";
link.href = href;
document.head.appendChild(link);
}
var truncate = function(str, len) {
if (str.length <= len) return str;
return str.substring(0, len - 3) + "...";
}
var geturl = function(url, onComplete, onFail) {
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState != 4) return;
if (req.status == 200) {
if (onComplete) onComplete(req.responseText);
} else {
if (onFail) onFail(req.responseText);
}
}
url += (url.indexOf("?") > -1 ? "&_=" : "?_=") + Math.random();
req.open("GET", url, true);
req.send();
}
var divContentCache = {}
var updateDivContent = function(id, content) {
if (divContentCache[id] != content) {
document.getElementById(id).innerHTML = content;
divContentCache[id] = content
return true;
}
return false;
}
var onInputSubmit = function() {
var b = document.getElementById("inputbox");
var req = new XMLHttpRequest();
req.open("POST", "/", true);
req.send("input=" + encodeURIComponent(b.value));
/* Do input history */
if (b.value && inputHistory[0] != b.value) {
inputHistory.unshift(b.value);
}
inputHistory.index = -1;
/* Reset */
b.value = "";
refreshOutput();
}
/* Input box history */
var inputHistory = [];
inputHistory.index = 0;
var onInputKeyDown = function(e) {
var key = e.which || e.keyCode;
if (key != 38 && key != 40) return true;
var b = document.getElementById("inputbox");
if (key == 38 && inputHistory.index < inputHistory.length - 1) {
/* Up key */
inputHistory.index++;
}
if (key == 40 && inputHistory.index >= 0) {
/* Down key */
inputHistory.index--;
}
b.value = inputHistory[inputHistory.index] || "";
b.selectionStart = b.value.length;
return false;
}
/* Output buffer and status */
var refreshOutput = function() {
geturl("/buffer", function(text) {
updateDivContent("status", "connected &#9679;");
if (updateDivContent("output", text)) {
var div = document.getElementById("output");
div.scrollTop = div.scrollHeight;
}
/* Update favicon */
changeFavicon("data:image/png;base64," +
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAP1BMVEUAAAAAAAAAAAD////19fUO"+
"Dg7v7+/h4eGzs7MlJSUeHh7n5+fY2NjJycnGxsa3t7eioqKfn5+QkJCHh4d+fn7zU+b5AAAAAnRS"+
"TlPlAFWaypEAAABRSURBVBjTfc9HDoAwDERRQ+w0ern/WQkZaUBC4e/mrWzppH9VJjbjZg1Ii2rM"+
"DyR1JZ8J0dVWggIGggcEwgbYCRbuPRqgyjHNpzUP+39GPu9fgloC5L9DO0sAAAAASUVORK5CYII="
);
},
function(text) {
updateDivContent("status", "disconnected &#9675;");
/* Update favicon */
changeFavicon("data:image/png;base64," +
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAYFBMVEUAAAAAAAAAAADZ2dm4uLgM"+
"DAz29vbz8/Pv7+/h4eHIyMiwsLBtbW0lJSUeHh4QEBDn5+fS0tLDw8O0tLSioqKfn5+QkJCHh4d+"+
"fn5ycnJmZmZgYGBXV1dLS0tFRUUGBgZ0He44AAAAAnRSTlPlAFWaypEAAABeSURBVBjTfY9HDoAw"+
"DAQD6Z3ey/9/iXMxkVDYw0g7F3tJReosUKHnwY4pCM+EtOEVXrb7wVRA0dMbaAcUwiVeDQq1Jp4a"+
"xUg5kE0ooqZu68Di2Tgbs/DiY/9jyGf+AyFKBAK7KD2TAAAAAElFTkSuQmCC"
);
});
}
setInterval(refreshOutput,
<?lua echo(lovebird.updateinterval) ?> * 1000);
/* Environment variable view */
var envPath = "";
var refreshEnv = function() {
geturl("/env.json?p=" + envPath, function(text) {
var json = eval("(" + text + ")");
/* Header */
var html = "<a href='#' onclick=\"setEnvPath('')\">env</a>";
var acc = "";
var p = json.path != "" ? json.path.split(".") : [];
for (var i = 0; i < p.length; i++) {
acc += "." + p[i];
html += " <a href='#' onclick=\"setEnvPath('" + acc + "')\">" +
truncate(p[i], 10) + "</a>";
}
updateDivContent("envheader", html);
/* Handle invalid table path */
if (!json.valid) {
updateDivContent("envvars", "Bad path");
return;
}
/* Variables */
var html = "<table>";
for (var i = 0; json.vars[i]; i++) {
var x = json.vars[i];
var fullpath = (json.path + "." + x.key).replace(/^\./, "");
var k = truncate(x.key, 15);
if (x.type == "table") {
k = "<a href='#' onclick=\"setEnvPath('" + fullpath + "')\">" +
k + "</a>";
}
var v = "<a href='#' onclick=\"insertVar('" +
fullpath.replace(/\.(-?[0-9]+)/g, "[$1]") +
"');\">" + x.value + "</a>"
html += "<tr><td>" + k + "</td><td>" + v + "</td></tr>";
}
html += "</table>";
updateDivContent("envvars", html);
});
}
var setEnvPath = function(p) {
envPath = p;
refreshEnv();
}
var insertVar = function(p) {
var b = document.getElementById("inputbox");
b.value += p;
b.focus();
}
setInterval(refreshEnv, <?lua echo(lovebird.updateinterval) ?> * 1000);
</script>
</body>
</html>
]]
lovebird.pages["buffer"] = [[ <?lua echo(lovebird.buffer) ?> ]]
lovebird.pages["env.json"] = [[
<?lua
local t = _G
local p = req.parsedurl.query.p or ""
p = p:gsub("%.+", "."):match("^[%.]*(.*)[%.]*$")
if p ~= "" then
for x in p:gmatch("[^%.]+") do
t = t[x] or t[tonumber(x)]
-- Return early if path does not exist
if type(t) ~= "table" then
echo('{ "valid": false, "path": ' .. string.format("%q", p) .. ' }')
return
end
end
end
?>
{
"valid": true,
"path": "<?lua echo(p) ?>",
"vars": [
<?lua
local keys = {}
for k in pairs(t) do
if type(k) == "number" or type(k) == "string" then
table.insert(keys, k)
end
end
table.sort(keys, lovebird.compare)
for _, k in pairs(keys) do
local v = t[k]
?>
{
"key": "<?lua echo(k) ?>",
"value": <?lua echo(
string.format("%q",
lovebird.truncate(
lovebird.htmlescape(
tostring(v)), 26))) ?>,
"type": "<?lua echo(type(v)) ?>",
},
<?lua end ?>
]
}
]]
function lovebird.init()
-- Init server
lovebird.server = assert(socket.bind(lovebird.host, lovebird.port))
lovebird.addr, lovebird.port = lovebird.server:getsockname()
lovebird.server:settimeout(0)
-- Wrap print
lovebird.origprint = print
if lovebird.wrapprint then
local oldprint = print
print = function(...)
oldprint(...)
lovebird.print(...)
end
end
-- Compile page templates
for k, page in pairs(lovebird.pages) do
lovebird.pages[k] = lovebird.template(page, "lovebird, req",
"pages." .. k)
end
lovebird.inited = true
end
function lovebird.template(str, params, chunkname)
params = params and ("," .. params) or ""
local f = function(x) return string.format(" echo(%q)", x) end
str = ("?>"..str.."<?lua"):gsub("%?>(.-)<%?lua", f)
str = "local echo " .. params .. " = ..." .. str
local fn = assert(lovebird.loadstring(str, chunkname))
return function(...)
local output = {}
local echo = function(str) table.insert(output, str) end
fn(echo, ...)
return table.concat(lovebird.map(output, tostring))
end
end
function lovebird.map(t, fn)
local res = {}
for k, v in pairs(t) do res[k] = fn(v) end
return res
end
function lovebird.trace(...)
local str = "[lovebird] " .. table.concat(lovebird.map({...}, tostring), " ")
print(str)
if not lovebird.wrapprint then lovebird.print(str) end
end
function lovebird.unescape(str)
local f = function(x) return string.char(tonumber("0x"..x)) end
return (str:gsub("%+", " "):gsub("%%(..)", f))
end
function lovebird.parseurl(url)
local res = {}
res.path, res.search = url:match("/([^%?]*)%??(.*)")
res.query = {}
for k, v in res.search:gmatch("([^&^?]-)=([^&^#]*)") do
res.query[k] = lovebird.unescape(v)
end
return res
end
local htmlescapemap = {
["<"] = "&lt;",
["&"] = "&amp;",
['"'] = "&quot;",
["'"] = "&#039;",
}
function lovebird.htmlescape(str)
return ( str:gsub("[<&\"']", htmlescapemap) )
end
function lovebird.truncate(str, len)
if #str <= len then
return str
end
return str:sub(1, len - 3) .. "..."
end
function lovebird.compare(a, b)
local na, nb = tonumber(a), tonumber(b)
if na then
if nb then return na < nb end
return false
elseif nb then
return true
end
return tostring(a) < tostring(b)
end
function lovebird.checkwhitelist(addr)
if lovebird.whitelist == nil then return true end
for _, a in pairs(lovebird.whitelist) do
local ptn = "^" .. a:gsub("%.", "%%."):gsub("%*", "%%d*") .. "$"
if addr:match(ptn) then return true end
end
return false
end
function lovebird.clear()
lovebird.lines = {}
lovebird.buffer = ""
end
function lovebird.pushline(line)
line.time = os.time()
line.count = 1
table.insert(lovebird.lines, line)
if #lovebird.lines > lovebird.maxlines then
table.remove(lovebird.lines, 1)
end
lovebird.recalcbuffer()
end
function lovebird.recalcbuffer()
local function doline(line)
local str = line.str
if not lovebird.allowhtml then
str = lovebird.htmlescape(line.str):gsub("\n", "<br>")
end
if line.type == "input" then
str = '<span class="inputline">' .. str .. '</span>'
else
if line.type == "error" then
str = '<span class="errormarker">!</span> ' .. str
str = '<span class="errorline">' .. str .. '</span>'
end
if line.count > 1 then
str = '<span class="repeatcount">' .. line.count .. '</span> ' .. str
end
if lovebird.timestamp then
str = os.date('<span class="timestamp">%H:%M:%S</span> ', line.time) ..
str
end
end
return str
end
lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "<br>")
end
function lovebird.print(...)
local t = {}
for i = 1, select("#", ...) do
table.insert(t, tostring(select(i, ...)))
end
local str = table.concat(t, " ")
local last = lovebird.lines[#lovebird.lines]
if last and str == last.str then
-- Update last line if this line is a duplicate of it
last.time = os.time()
last.count = last.count + 1
lovebird.recalcbuffer()
else
-- Create new line
lovebird.pushline({ type = "output", str = str })
end
end
function lovebird.onerror(err)
lovebird.pushline({ type = "error", str = err })
if lovebird.wrapprint then
lovebird.origprint("[lovebird] ERROR: " .. err)
end
end
function lovebird.onrequest(req, client)
local page = req.parsedurl.path
page = page ~= "" and page or "index"
-- Handle "page not found"
if not lovebird.pages[page] then
return "HTTP/1.1 404\r\nContent-Length: 8\r\n\r\nBad page"
end
-- Handle page
local str
xpcall(function()
local data = lovebird.pages[page](lovebird, req)
local contenttype = "text/html"
if string.match(page, "%.json$") then
contenttype = "application/json"
end
str = "HTTP/1.1 200 OK\r\n" ..
"Content-Type: " .. contenttype .. "\r\n" ..
"Content-Length: " .. #data .. "\r\n" ..
"\r\n" .. data
end, lovebird.onerror)
return str
end
function lovebird.receive(client, pattern)
while 1 do
local data, msg = client:receive(pattern)
if not data then
if msg == "timeout" then
-- Wait for more data
coroutine.yield(true)
else
-- Disconnected -- yielding nil means we're done
coroutine.yield(nil)
end
else
return data
end
end
end
function lovebird.send(client, data)
local idx = 1
while idx < #data do
local res, msg = client:send(data, idx)
if not res and msg == "closed" then
-- Handle disconnect
coroutine.yield(nil)
else
idx = idx + res
coroutine.yield(true)
end
end
end
function lovebird.onconnect(client)
-- Create request table
local requestptn = "(%S*)%s*(%S*)%s*(%S*)"
local req = {}
req.socket = client
req.addr, req.port = client:getsockname()
req.request = lovebird.receive(client, "*l")
req.method, req.url, req.proto = req.request:match(requestptn)
req.headers = {}
while 1 do
local line, msg = lovebird.receive(client, "*l")
if not line or #line == 0 then break end
local k, v = line:match("(.-):%s*(.*)$")
req.headers[k] = v
end
if req.headers["Content-Length"] then
req.body = lovebird.receive(client, req.headers["Content-Length"])
end
-- Parse body
req.parsedbody = {}
if req.body then
for k, v in req.body:gmatch("([^&]-)=([^&^#]*)") do
req.parsedbody[k] = lovebird.unescape(v)
end
end
-- Parse request line's url
req.parsedurl = lovebird.parseurl(req.url)
-- Handle request; get data to send and send
local data = lovebird.onrequest(req)
lovebird.send(client, data)
-- Clear up
client:close()
end
function lovebird.update()
if not lovebird.inited then lovebird.init() end
-- Handle new connections
while 1 do
-- Accept new connections
local client = lovebird.server:accept()
if not client then break end
client:settimeout(0)
local addr = client:getsockname()
if lovebird.checkwhitelist(addr) then
-- Connection okay -- create and add coroutine to set
local conn = coroutine.wrap(function()
xpcall(function() lovebird.onconnect(client) end, function() end)
end)
lovebird.connections[conn] = true
else
-- Reject connection not on whitelist
lovebird.trace("got non-whitelisted connection attempt: ", addr)
client:close()
end
end
-- Handle existing connections
for conn in pairs(lovebird.connections) do
-- Resume coroutine, remove if it has finished
local status = conn()
if status == nil then
lovebird.connections[conn] = nil
end
end
end
return lovebird

View File

@ -24,6 +24,9 @@
local Animator = Object:extend()
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Animator:new(sprite)
self.sprite = sprite
self.frame = 1
@ -40,6 +43,9 @@ function Animator:setCustomSpeed(customSpeed)
self.customSpeed = customSpeed or 0
end
-- UPDATE FUNCTIONS
-- Update the animation of the animator
function Animator:update(dt)
if (self.currentAnimation == "") then
print("warning: no current animation data")
@ -61,21 +67,8 @@ function Animator:update(dt)
end
end
function Animator:getAnimationDuration(animation)
return (self.animationData.endAt - self.animationData.startAt) / self.animationData.speed
end
function Animator:getFrame()
return self.frame
end
function Animator:animationExist(name)
return (self.sprite.data.animations[self.currentAnimation] ~= nil)
end
function Animator:draw(x, y, r, sx, sy, ox, oy, kx, ky)
self.sprite:drawFrame(self.frame, x, y, r, sx, sy, ox, oy, kx, ky)
end
-- ANIMATION HANDLING FUNCTIONS
-- Change the animation of the animator
function Animator:changeAnimation(name, restart)
-- Force restart if animation name is different
@ -98,4 +91,34 @@ function Animator:changeToDefaultAnimation(restart)
self:changeAnimation(self.sprite.data.metadata.defaultAnim, restart)
end
-- INFO FUNCTIONS
-- get information with these functions
function Animator:getAnimationDuration(animation)
return (self.animationData.endAt - self.animationData.startAt) / self.animationData.speed
end
function Animator:getFrame()
return self.frame
end
function Animator:animationExist(name)
return (self.sprite.data.animations[self.currentAnimation] ~= nil)
end
function Animator:getDimensions()
return self.sprite:getDimensions()
end
-- DRAW FUNCTIONS
-- Draw animations using these functions
function Animator:draw(x, y, r, sx, sy, ox, oy, kx, ky)
self.sprite:drawFrame(self.frame, x, y, r, sx, sy, ox, oy, kx, ky)
end
function Animator:drawMask(x, y, r, sx, sy, ox, oy, kx, ky)
self.sprite:drawFrameMask(self.frame, x, y, r, sx, sy, ox, oy, kx, ky)
end
return Animator

View File

@ -23,9 +23,13 @@
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local Tileset = require "core.modules.assets.tileset"
local cwd = (...):gsub('%.autotile$', '') .. "."
local Tileset = require(cwd .. "tileset")
local Autotile = Object:extend()
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Autotile:new(filepath)
self.tileset = Tileset(filepath)
@ -35,6 +39,9 @@ function Autotile:new(filepath)
self.tilesize = self.metadata.width
end
-- DRAW FUNCTIONS
-- Draw tileset using these functions
function Autotile:drawtile(i, j, x, y, r, sx, sy, ox, oy, kx, ky)
local i = i or 1
local j = j or 1

View File

@ -24,6 +24,9 @@
local Background = Object:extend()
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Background:new(filepath)
self.image = love.graphics.newImage(filepath)
self.batch = love.graphics.newSpriteBatch(self.image , 1000 )

View File

@ -25,6 +25,7 @@
local Font = Object:extend()
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Font:new(filename, size)
@ -70,7 +71,8 @@ function Font:setLineHeight(height)
self.font:setLineHeight(height)
end
-- get information functions
-- INFO FUNCTIONS
-- get information with these functions
function Font:getHeight()
local font = self.font
@ -91,7 +93,8 @@ function Font:getColor()
return self.color
end
-- print functions
-- DRAW FUNCTIONS
-- print text using theses functions
function Font:draw(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
-- draw text with color and effect applied
@ -124,6 +127,7 @@ function Font:printf(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
end
-- FILTER SYSTEM
-- With these filter, you can apply custom effects to the fonts
function Font:applyFilter(text, x, y, limit, align, r, sx, sy, ox, oy, kx, ky)
if self.filter == "shadow" then

View File

@ -1,6 +1,35 @@
local Font = require "core.modules.assets.fonts"
-- assets/fonts :: the imagefonts object, which are basically a bitmap version
-- of the font object.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.imagefonts$', '') .. "."
local Font = require(cwd.. "fonts")
local ImageFont = Font:extend()
-- INIT FUNCTIONS
-- Initilizing and configuring option
function ImageFont:new(filename, extraspacing)
local data = require(filename)
local extraspacing = extraspacing or data.extraspacing or 1

View File

@ -24,51 +24,38 @@
local Assets = Object:extend()
local Sprite = require "core.modules.assets.sprites"
local Font = require "core.modules.assets.fonts"
local ImageFont = require "core.modules.assets.imagefonts"
local cwd = (...):gsub('%.init$', '') .. "."
local Tileset = require "core.modules.assets.tileset"
local Autotile = require "core.modules.assets.autotile"
local Background = require "core.modules.assets.background"
local Texture = require(cwd .. "texture")
local Sprite = require(cwd .. "sprites")
local Font = require(cwd .. "fonts")
local ImageFont = require(cwd .. "imagefonts")
local Tileset = require(cwd .. "tileset")
local Autotile = require(cwd .. "autotile")
local Background = require(cwd .. "background")
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Assets:new()
self.sprites = {}
self.sfx = {}
self.fonts = {}
self.music = nil
self:clearBackgrounds()
self:clearFonts()
self:clearAutotile()
self:clearTileset()
self.images = {}
self:clear()
self.isActive = true
end
function Assets:init()
self.sprites = {}
self.sfx = {}
self.fonts = {}
self.music = nil
self.backgrounds= {}
self:clearFonts()
self.images = {}
end
function Assets:clear()
-- TODO: destroy individually each texture/image when assets are cleared
self.sprites = {}
self.sfx = {}
self.fonts = {}
self.music = nil
self.backgrounds= {}
self:clearSprites()
self:clearSFX()
self:clearFonts()
self:resetMusic()
self:clearBackgrounds()
self:clearFonts()
self:clearTileset()
self.images = {}
self:clearImages()
end
function Assets:update(dt)
@ -77,7 +64,87 @@ function Assets:update(dt)
end
end
-- SFX et Musique
-- IMPORT FUNCTIONS
-- Easilly import assets
function Assets:batchImport(datafile)
local datas = require(datafile)
for asset_type, assets in pairs(datas) do
if (asset_type == "autotiles") then
self:importAutotiles(assets)
elseif (asset_type == "backgrounds") then
self:importBackgrounds(assets)
elseif (asset_type == "fonts") then
self:importFonts(assets)
elseif (asset_type == "imagefonts") then
self:importImageFonts(assets)
elseif (asset_type == "images") then
self:importTextures(assets)
elseif (asset_type == "sprites") then
self:importSprites(assets)
elseif (asset_type == "textures") then
self:importTextures(assets)
elseif (asset_type == "tilesets") then
self:importTilesets(assets)
elseif (asset_type == "sfx") then
self:importSFX(assets)
else
print("Unkown asset type : " .. asset_type)
end
end
end
function Assets:importAutotiles(assets)
for i, asset in ipairs(assets) do
self:addAutotile(asset[1], asset[2])
end
end
function Assets:importBackgrounds(assets)
for i, asset in ipairs(assets) do
self:addBackground(asset[1], asset[2])
end
end
function Assets:importFonts(assets)
for i, asset in ipairs(assets) do
self:addFont(asset[1], asset[2], asset[3])
end
end
function Assets:importImageFonts(assets)
for i, asset in ipairs(assets) do
self:addImageFont(asset[1], asset[2], asset[3])
end
end
function Assets:importSprites(assets)
for i, asset in ipairs(assets) do
self:addSprite(asset[1], asset[2])
end
end
function Assets:importTextures(assets)
for i, asset in ipairs(assets) do
self:addImage(asset[1], asset[2])
end
end
function Assets:importTilesets(assets)
for i, asset in ipairs(assets) do
self:addTileset(asset[1], asset[2])
end
end
function Assets:importSFX(assets)
for i, asset in ipairs(assets) do
self:addSFX(asset[1], asset[2])
end
end
-- SFX & MUSICS
-- Handle sound effects and musics
function Assets:addSFX(name, filepath)
self:newSFX(name, filepath)
@ -92,14 +159,6 @@ function Assets:clearSFX()
self.sfx = {}
end
function Assets:setMusic(filename)
if filename ~= nil then
love.audio.stop( )
self.music = love.audio.newSource(filename, "stream" )
self.music:setVolume(core.options.data.audio.music / 100)
end
end
function Assets:playSFX(filename)
if not (self.sfx[filename] == nil) then
self.sfx[filename]:stop()
@ -108,9 +167,11 @@ function Assets:playSFX(filename)
end
end
function Assets:playMusic()
if not (self.music == nil) then
love.audio.play(self.music)
function Assets:setMusic(filename)
if filename ~= nil then
love.audio.stop( )
self.music = love.audio.newSource(filename, "stream" )
self.music:setVolume(core.options.data.audio.music / 100)
end
end
@ -118,49 +179,63 @@ function Assets:silence()
love.audio.stop()
end
-- Background --
function Assets:resetMusic()
self.music = nil
end
function Assets:playMusic()
if not (self.music == nil) then
love.audio.play(self.music)
end
end
-- IMAGES FUNCTIONS
-- Create directly texture items
function Assets:addImage(name, filename)
self.images[name] = love.graphics.newImage(filename)
self.images[name] = Texture(filename)
end
function Assets:drawImage(name, x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(self.images[name], x, y, r, sx, sy, ox, oy, kx, ky)
self.images[name]:draw(x, y, r, sx, sy, ox, oy, kx, ky)
end
-- Images --
function Assets:clearImages()
self.images = {}
end
-- BACKGROUNDS FUNCTIONS
-- Automatic tiling texture
function Assets:addBackground(name, filepath)
-- TODO: rework entirely background to work at any size
self.backgrounds[name] = Background(filepath)
end
function Assets:clearBackgrounds()
self.backgrounds = {}
end
function Assets:addBackground(name, filepath)
self.backgrounds[name] = Background(filepath)
end
-- SPRITES --
-- SPRITES FUNCTIONS
-- Animated tileset
function Assets:addSprite(name, filepath)
self.sprites[name] = Sprite(filepath)
end
function Assets:clearSprites()
self.sprites = {}
end
function Assets:animationsUpdate(dt)
for i,v in pairs(self.sprites) do
v:update(dt)
end
end
-- FONTS --
function Assets:clearFonts()
self.fonts = {}
function Assets:clearSprites()
self.sprites = {}
end
-- FONTS FUNCTIONS
-- Handles fonts and imagesfonts
function Assets:addFont(key, filename, size)
local font = Font(filename, size)
self.fonts[key] = font
@ -175,7 +250,12 @@ function Assets:getFont(filename)
return self.fonts[filename]
end
-- Tileset
function Assets:clearFonts()
self.fonts = {}
end
-- TILESET FUNCTIONS
-- Automatically create quads for a texture
function Assets:addTileset(name, filepath)
self.tileset[name] = Tileset(filepath)
@ -185,7 +265,8 @@ function Assets:clearTileset()
self.tileset = {}
end
-- Autotile
-- AUTOTILE FUNCTIONS
-- Automatically draw tiles
function Assets:addAutotile(name, tilesize)
self.autotile[name] = Autotile(name, tilesize)
@ -195,4 +276,19 @@ function Assets:clearAutotile()
self.autotile = {}
end
-- ACTIVITY FUNCTIONS
-- Handle activity
function Assets:setActivity(activity)
self.isActive = activity
end
function Assets:switchActivity()
self.isActive = (self.isActive == false)
end
function Assets:getActivity()
return self.isActive
end
return Assets

View File

@ -24,8 +24,13 @@
]]
local Sprite = Object:extend()
local Animator = require("core.modules.assets.animator")
local Tileset = require("core.modules.assets.tileset")
local cwd = (...):gsub('%.sprites$', '') .. "."
local Animator = require(cwd .. "animator")
local Tileset = require(cwd .. "tileset")
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Sprite:new(filepath)
self.tileset = Tileset(filepath)
@ -58,18 +63,36 @@ function Sprite:changeAnimation(name, restart)
self.animator:changeAnimation(name, restart)
end
-- INFO FUNCTIONS
-- get information with these functions
function Sprite:animationExist(name)
return self.animator:animationExist(name)
end
function Sprite:getDimensions()
return self.tileset:getDimensions()
end
-- DRAW FUNCTIONS
-- Draw sprites using these functions
function Sprite:drawAnimation(x, y, r, sx, sy, ox, oy, kx, ky)
self.animator:draw(x, y, r, sx, sy, ox, oy, kx, ky)
end
function Sprite:drawAnimationMask(x, y, r, sx, sy, ox, oy, kx, ky)
self.animator:drawMask(x, y, r, sx, sy, ox, oy, kx, ky)
end
function Sprite:drawFrame(frame, x, y, r, sx, sy, ox, oy, kx, ky)
self.tileset:drawTile(frame, x, y, r, sx, sy, ox, oy, kx, ky)
end
function Sprite:drawFrameMask(frame, x, y, r, sx, sy, ox, oy, kx, ky)
self.tileset:drawTileMask(frame, x, y, r, sx, sy, ox, oy, kx, ky)
end
function Sprite:drawPart(x, y, w, h, r, sx, sy, ox, oy, kx, ky)
local w = math.floor(w)
local h = math.floor(h)

View File

@ -0,0 +1,74 @@
-- assets/texture :: the texture object, essentially used to be able to draw easily
-- the mask of the texture (used for stuff like flashing sprite, etc)
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Texture = Object:extend()
local function getMask(x, y, r, g, b, a)
-- template for defining your own pixel mapping function
-- perform computations giving the new values for r, g, b and a
-- ...
return 1, 1, 1, a
end
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Texture:new(filename)
self.imageData = love.image.newImageData(filename)
local maskData = self.imageData:clone()
maskData:mapPixel( getMask )
self.image = love.graphics.newImage( self.imageData )
self.mask = love.graphics.newImage( maskData )
end
-- INFO FUNCTIONS
-- get information with these functions
function Texture:getDimensions()
return self.image:getDimensions()
end
-- DRAW FUNCTIONS
-- Draw texture using these functions
function Texture:draw(x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(self.image, x, y, r, sx, sy, ox, oy, kx, ky)
end
function Texture:drawQuad(quad, x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(self.image, quad, x, y, r, sx, sy, ox, oy, kx, ky)
end
function Texture:drawMask(x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(self.mask, x, y, r, sx, sy, ox, oy, kx, ky)
end
function Texture:drawMaskQuad(quad, x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(self.mask, quad, x, y, r, sx, sy, ox, oy, kx, ky)
end
return Texture

View File

@ -27,9 +27,15 @@
]]
local Tileset = Object:extend()
local cwd = (...):gsub('%.tileset$', '') .. "."
local Texture = require(cwd .. "texture")
-- INIT FUNCTIONS
-- Initilizing and configuring option
function Tileset:new(filepath)
self.texture = love.graphics.newImage(filepath .. ".png")
self.texture = Texture(filepath .. ".png")
local data = require(filepath)
self.metadata = data.metadata
@ -63,6 +69,9 @@ function Tileset:createQuads()
end
-- INFO FUNCTIONS
-- get information with these functions
function Tileset:getTileID_Grid(x, y)
local n = (y - 1) * self.gridWidth + x
@ -77,13 +86,35 @@ function Tileset:getTile(n)
return self.quads[n]
end
function Tileset:getDimensions()
return self.width, self.height
end
-- DRAW FUNCTIONS
-- Draw tileset using these functions
function Tileset:drawTile_Grid(i, j, x, y, r, sx, sy, ox, oy, kx, ky)
local tileID = self:getTileID_Grid(i, j)
love.graphics.draw(self.texture, self.quads[tileID], x, y, r, sx, sy, ox, oy, kx, ky)
local ox = ox or self.metadata.ox
local oy = oy or self.metadata.oy
self.texture:drawQuad(self.quads[tileID], x, y, r, sx, sy, ox, oy, kx, ky)
end
function Tileset:drawTile(id, x, y, r, sx, sy, ox, oy, kx, ky)
love.graphics.draw(self.texture, self.quads[id], x, y, r, sx, sy, ox, oy, kx, ky)
local ox = ox or self.metadata.ox
local oy = oy or self.metadata.oy
self.texture:drawQuad(self.quads[id], x, y, r, sx, sy, ox, oy, kx, ky)
end
function Tileset:drawTileMask_Grid(i, j, x, y, r, sx, sy, ox, oy, kx, ky)
local tileID = self:getTileID_Grid(i, j)
local ox = ox or self.metadata.ox
local oy = oy or self.metadata.oy
self.texture:drawMaskQuad(self.quads[tileID], x, y, r, sx, sy, ox, oy, kx, ky)
end
function Tileset:drawTileMask(id, x, y, r, sx, sy, ox, oy, kx, ky)
self.texture:drawMaskQuad(self.quads[id], x, y, r, sx, sy, ox, oy, kx, ky)
end
return Tileset

View File

@ -1,10 +1,36 @@
local cwd = (...):gsub('%.flowbox$', '') .. "."
local Menu = require(cwd .. "parent")
-- flowbox :: flexible box menu, that handle in grid the widgets
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.flowbox$', '') .. "."
local Menu = require(cwd .. "parent")
local FlowBox = Menu:extend()
local menuutils = require(cwd .. "widgets.utils")
-- INIT FUNCTIONS
-- Initialize and configure the flowbox
function FlowBox:new(menusystem, name, x, y, w, h, slots_hor, slots_vert)
self.view = {}
self.view.slotNumber = slots_hor * slots_vert
@ -19,6 +45,9 @@ function FlowBox:new(menusystem, name, x, y, w, h, slots_hor, slots_vert)
-- soit un multiple du nombre de slot et de leur dimensions
end
-- UPDATE FUNCTIONS
-- Update the menu and its view
function FlowBox:updateWidgetSize()
self.widget.h = math.floor( self.h / self.view.lineNumber )
self.widget.w = math.floor( self.w / self.view.colNumber )
@ -47,6 +76,9 @@ function FlowBox:updateView()
self.view.firstSlot = beginline * self.view.colNumber + 1
end
-- INFO FUNCTIONS
-- Get informations
function FlowBox:getCoord(id_selected)
id_selected = id_selected - 1 -- On simplifie les calcul en prenant 0 comme départ
local line, col
@ -55,6 +87,9 @@ function FlowBox:getCoord(id_selected)
return col, line
end
-- CURSOR FUNCTIONS
-- Handle the cursor in a 2D menu
function FlowBox:moveCursor(new_col, new_line)
local col, line = self:getCoord(self.widget.selected)
local lastcol, lastline = self:getCoord(#self.widget.list)
@ -93,6 +128,9 @@ function FlowBox:moveCursor(new_col, new_line)
self.widget.selected = (new_line * self.view.colNumber) + new_col + 1
end
-- KEYS FUNCTIONS
-- Handle the keyboard/controller inputs
function FlowBox:keyreleased(key, code)
local col, line = self:getCoord(self.widget.selected)
if key == 'left' then
@ -113,11 +151,14 @@ function FlowBox:keyreleased(key, code)
if key == "A" then
if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
self.widget.list[self.widget.selected]:action()
self.widget.list[self.widget.selected]:action("key")
end
end
end
-- MOUSE FUNCTIONS
-- Handle the mouse/touch pointer
function FlowBox:mousemoved(x, y)
local col, line = self:getCoord(self.widget.selected)
local begincol, beginline = self:getCoord(self.view.firstSlot)
@ -145,11 +186,14 @@ function FlowBox:mousepressed(x, y, button, isTouch)
if widget_selected >= 1 and widget_selected <= #self.widget.list then
self.widget.selected = widget_selected
self:getFocus()
self.widget.list[self.widget.selected]:action()
self.widget.list[self.widget.selected]:action("pointer")
end
end
-- DRAW FUNCTIONS
-- Draw the menu and its content
function FlowBox:draw()
self:updateView()
local widgety = self.y

View File

@ -1,10 +1,36 @@
local cwd = (...):gsub('%.grid$', '') .. "."
local Menu = require(cwd .. "parent")
-- grid :: a menu with arbitrary widget placement and size on a grid.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.grid$', '') .. "."
local Menu = require(cwd .. "parent")
local GridBox = Menu:extend()
local menuutils = require(cwd .. "widgets.utils")
-- INIT FUNCTIONS
-- Initialize and configure the menu
function GridBox:new(menusystem, name, x, y, w, h, colNumber, lineNumber)
self.view = {}
self.view.slotNumber = colNumber * lineNumber
@ -40,6 +66,9 @@ function GridBox:updateWidgetSize()
self.widget.w = math.floor( self.w / self.view.colNumber )
end
-- INFO FUNCTIONS
-- Get the info of the widgets
function GridBox:getWidgetSize(id)
local slot = self:getWidgetSlot(id)
if slot == 0 then
@ -106,10 +135,16 @@ function GridBox:getWidgetAtPoint(x, y)
return widgetID
end
-- UPDATE FUNCTIONS
-- Update the Grid and its view
function GridBox:update(dt)
self.view.firstSlot = 1
end
-- KEYS FUNCTIONS
-- Handle the keyboard/manette functions
function GridBox:keyreleased(key, code)
slotID = self:getWidgetSlot(self.widget.selected)
local col, line = self.cursor.x, self.cursor.y
@ -130,7 +165,7 @@ function GridBox:keyreleased(key, code)
end
if key == "A" and self.widget.selected <= #self.widget.list then
self.widget.list[self.widget.selected]:action()
self.widget.list[self.widget.selected]:action("key")
end
end
@ -176,6 +211,9 @@ function GridBox:moveLine(direction)
end
end
-- MOUSE FUNCTIONS
-- Handle the mouse and activate the widgets with it
function GridBox:mousemoved(x, y)
local widgetID = self:getWidgetAtPoint(x, y)
@ -201,11 +239,14 @@ function GridBox:mousepressed(x, y, button, isTouch)
self:getFocus()
if #self.widget.list > 0 and self.widget.selected > 1 and self.widget.selected <= #self.widget.list then
self.widget.list[self.widget.selected]:action()
self.widget.list[self.widget.selected]:action("pointer")
end
end
end
-- DRAW FUNCTIONS
-- Draw the menu and its content
function GridBox:draw()
for i,v in ipairs(self.slots) do

View File

@ -0,0 +1,148 @@
-- hlistbox : add an horizontal list of widgets.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.hlistbox$', '') .. "."
local Menu = require(cwd .. "parent")
local HListBox = Menu:extend()
local menuutils = require(cwd .. "widgets.utils")
-- INIT FUNCTIONS
-- Initialize and configure functions.
function HListBox:new(menusystem, name, x, y, w, h, slotNumber)
self.view = {}
self.view.slotNumber = slotNumber
self.view.firstSlot = 1
HListBox.super.new(self, menusystem, name, x, y, w, h)
self.w = slotNumber * self.widget.w -- On fait en sorte que la hauteur
-- soit un multiple du nombre de slot et de leur hauteur
end
-- UPDATE FUNCTIONS
-- Update the menu every step.
function HListBox:updateWidgetSize()
self.widget.h = self.h
self.widget.w = math.floor( self.w / self.view.slotNumber )
end
function HListBox:update(dt)
self:updateView()
end
function HListBox:updateView()
if self.widget.selected < self.view.firstSlot then
self.view.firstSlot = self.widget.selected
end
if self.widget.selected > self.view.firstSlot + self.view.slotNumber - 1 then
self.view.firstSlot = self.widget.selected - self.view.slotNumber + 1
end
if self.view.firstSlot < 1 then
self.view.firstSlot = 1
end
end
-- KEYBOARD FUNCTIONS
-- Handle key check.
function HListBox:keyreleased(key, code)
if key == 'left' then
self:moveCursor(self.widget.selected - 1)
end
if key == 'right' then
self:moveCursor(self.widget.selected + 1)
end
if key == "A" then
if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
self.widget.list[self.widget.selected]:action("key")
end
end
if key == "B" then
if (self.widget.cancel >= 1 and self.widget.cancel <= #self.widget.list) then
self.widget.list[self.widget.cancel]:action("key")
end
end
end
-- MOUSE FUNCTIONS
-- Click and stuff like that.
function HListBox:mousemoved(x, y)
local widget_selected = self.view.firstSlot + math.floor(x / self.widget.w)
if widget_selected >= 1 and widget_selected <= #self.widget.list then
self.widget.selected = widget_selected
self:getFocus()
end
end
function HListBox:mousepressed(x, y, button, isTouch)
local widget_selected = self.view.firstSlot + math.floor(x / self.widget.w)
if widget_selected >= 1 and widget_selected <= #self.widget.list then
self.widget.selected = widget_selected
self:getFocus()
if #self.widget.list > 0 then
self.widget.list[self.widget.selected]:action("pointer")
end
end
end
-- DRAW FUNCTIONS
-- Draw the menu and its content
function HListBox:draw()
self:updateView()
local widgetx = self.x
for i,v in ipairs(self.widget.list) do
if (i >= self.view.firstSlot) and (i < self.view.firstSlot + self.view.slotNumber) then
v:draw(widgetx, self.y, self.widget.w, self.h)
if self.widget.selected == i and self:haveFocus() == true then
v:drawSelected(widgetx, self.y, self.widget.w, self.h)
else
v:draw(widgetx, self.y, self.widget.w, self.h)
end
widgetx = widgetx + self.widget.w
end
end
end
function HListBox:drawCursor()
self:updateView()
if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
local w, h = self:getWidgetSize()
local x = (self.widget.selected - self.view.firstSlot) * w
menuutils.drawCursor(self.x + x,self.y, w, h)
end
end
return HListBox

View File

@ -1,54 +1,163 @@
local MenuSystem = Object:extend()
-- menusystem :: the controller of the menu system. This object handle the
-- different menu objects
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.init$', '') .. "."
local MenuSystem = Object:extend()
-- Load the differents menu object to get an easy access
MenuSystem.Parent = require(cwd .. "parent")
MenuSystem.ListBox = require(cwd .. "listbox")
MenuSystem.FlowBox = require(cwd .. "flowbox")
MenuSystem.Grid = require(cwd .. "grid")
MenuSystem.TextMenu = require(cwd .. "textmenu")
-- load widgets object
MenuSystem.Widget = require(cwd .. "widgets")
-- INIT FUNCTIONS
-- Initialize and configure the menu controller
--local VirtualPad = require "modules.virtualpad"
function MenuSystem:new()
function MenuSystem:new(scene)
self.scene = scene
self.menus = {}
self.focusedMenu = ""
self.isActive = true
self.lockWorld = false
self.lockAssets = false
end
function MenuSystem:reset()
self.menus = {}
end
-- ACTIVATION FUNCTIONS
-- Activate and deactivate the whole menusystem
function MenuSystem:activate()
self.isActive = true
if (self.lockWorld) then
if (self.scene.world ~= nil) then
self.scene.world:setActivity(false)
end
end
if (self.lockAssets) then
if (self.scene.assets ~= nil) then
self.scene.assets:setActivity(false)
end
end
end
function MenuSystem:deactivate()
self.isActive = false
if (self.lockWorld) then
if (self.scene.world ~= nil) then
self.scene.world:setActivity(true)
end
end
if (self.lockAssets) then
if (self.scene.assets ~= nil) then
self.scene.assets:setActivity(true)
end
end
end
function MenuSystem:getActiveState()
return self.isActive
end
function MenuSystem:lockWorldWhenActive(state)
self.lockWorld = state
end
function MenuSystem:lockAssetsWhenActive(state)
self.lockAssets = state
end
-- MENUS FUNCTIONS
-- Controle the menus of the menusystem
function MenuSystem:addMenu(name, menu)
self.menus[name] = menu
end
function MenuSystem:update(dt)
self:removeDestroyedMenus()
for k,v in pairs(self.menus) do
v:update(dt)
v:updateWidgets(dt)
end
function MenuSystem:menuExist(name)
return (self.menus[name] ~= nil)
end
if self.menus[self.focusedMenu] ~= nil then
-- Only check buttons if the current focused menu is actually active
if self.menus[self.focusedMenu].isActive then
for k,v in pairs(self.keys) do
if self.keys[k].isPressed then
self.menus[self.focusedMenu]:keyreleased(k)
end
end
function MenuSystem:switchMenu(menu)
for k,v in pairs(self.menus) do
if k == menu then
v:getFocus()
v:setVisibility(true)
v:setActivity(true)
else
v:setVisibility(false)
v:setActivity(false)
end
end
end
function MenuSystem:lockMenu(menu, lock)
local lock = lock or true
if self:menuExist(menu) then
self.menus[menu]:lock(lock)
end
end
function MenuSystem:lockMenuVisibility(menu, lock)
local lock = lock or true
if self:menuExist(menu) then
self.menus[menu]:lockVisibility(lock)
end
end
function MenuSystem:setMenuActivity(menu, activity)
local activity = activity or true
if self:menuExist(menu) then
self.menus[menu]:setActivity(activity)
if activity == true then
-- if we make the menu active, he have to be visible too
self.menus[menu]:setVisibility(true)
end
end
end
function MenuSystem:setMenuVisibility(menu, visibility)
local visibility = visibility or true
if self:menuExist(menu) then
self.menus[menu]:setVisibility(visibility)
end
end
function MenuSystem:setAllMenuVisibility(visibility)
for k,v in pairs(self.menus) do
v.isVisible = visibility
v:setVisibility(visibility)
end
end
@ -67,45 +176,110 @@ function MenuSystem:removeDestroyedMenus()
end
end
function MenuSystem:keyreleased(key, code)
-- TODO:depreciated function
-- SOUND FUNCTIONS
-- Add sounds to every menus
function MenuSystem:setSoundFromSceneAssets(soundname)
for k,v in pairs(self.menus) do
v:setSoundFromSceneAssets(soundname)
end
end
function MenuSystem:mousemoved(x, y, dx, dy)
function MenuSystem:setSound(soundasset)
for k,v in pairs(self.menus) do
if v.isActive then
if (x > v.x) and (x < v.x + v.w) and (y > v.y) and (y < v.y + v.h) then
v:mousemoved(x - v.x, y - v.y)
break;
v:setSound(soundasset)
end
end
-- UPDATE FUNCTIONS
-- Update the menus of the menusystem
function MenuSystem:update(dt)
if (self.isActive) then
self:removeDestroyedMenus()
for k,v in pairs(self.menus) do
v:update(dt)
v:updateWidgets(dt)
end
if self.menus[self.focusedMenu] ~= nil then
-- Only check buttons if the current focused menu is actually active
if self.menus[self.focusedMenu].isActive then
for k,v in pairs(self.keys) do
if self.keys[k].isPressed then
self.menus[self.focusedMenu]:keyreleased(k)
end
end
end
end
end
end
-- MOUSE FUNCTIONS
-- Send mouse inputs to the menu
function MenuSystem:mousemoved(x, y, dx, dy)
if (self.isActive) then
for k,v in pairs(self.menus) do
if v.isActive then
if (x > v.x) and (x < v.x + v.w) and (y > v.y) and (y < v.y + v.h) then
v:mousemoved(x - v.x, y - v.y)
break;
end
end
end
end
end
function MenuSystem:mousepressed( x, y, button, istouch )
for k,v in pairs(self.menus) do
if v.isActive then
if (x > v.x) and (x < v.x + v.w) and (y > v.y) and (y < v.y + v.h) then
v:mousepressed(x - v.x, y - v.y, button, istouch )
break;
if (self.isActive) then
for k,v in pairs(self.menus) do
if v.isActive then
if (x > v.x) and (x < v.x + v.w) and (y > v.y) and (y < v.y + v.h) then
v:mousepressed(x - v.x, y - v.y, button, istouch )
break;
end
end
end
end
end
function MenuSystem:draw(dt) -- On dessine les entitées
-- DRAW FUNCTIONS
-- All functions to draw the menus of the menusystem
function MenuSystem:getDrawList()
local drawList = {}
for k,v in pairs(self.menus) do
if (v.isVisible) then
v:draw(dt)
local drawObject = {}
drawObject.name = k
drawObject.depth = v.depth
table.insert(drawList, drawObject)
end
table.sort(drawList, function(a,b) return a.depth > b.depth end)
return drawList
end
function MenuSystem:draw(dt)
if (self.isActive) then
-- Draw all the menus
self.drawList = self:getDrawList()
for i,v in ipairs(self.drawList) do
local v2 = self.menus[v.name]
if (v2.isVisible) then
v2:draw(dt)
end
end
if self.menus[self.focusedMenu] ~= nil then
if (self.menus[self.focusedMenu].isVisible) then
self.menus[self.focusedMenu]:drawCursor()
end
end
end
if self.menus[self.focusedMenu] ~= nil then
if (self.menus[self.focusedMenu].isVisible) then
self.menus[self.focusedMenu]:drawCursor()
end
end
end
return MenuSystem

View File

@ -1,9 +1,35 @@
-- listbox : add a vertical list of widgets.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.listbox$', '') .. "."
local Menu = require(cwd .. "parent")
local ListBox = Menu:extend()
local menuutils = require(cwd .. "widgets.utils")
local ListBox = Menu:extend()
-- INIT FUNCTIONS
-- Initialize and configure functions.
function ListBox:new(menusystem, name, x, y, w, h, slotNumber)
self.view = {}
@ -14,6 +40,9 @@ function ListBox:new(menusystem, name, x, y, w, h, slotNumber)
-- soit un multiple du nombre de slot et de leur hauteur
end
-- UPDATE FUNCTIONS
-- Update the menu every step.
function ListBox:updateWidgetSize()
self.widget.h = math.floor( self.h / self.view.slotNumber )
self.widget.w = self.w
@ -36,6 +65,9 @@ function ListBox:updateView()
end
end
-- KEYBOARD FUNCTIONS
-- Handle input from keyboard/controllers.
function ListBox:keyreleased(key, code)
if key == 'up' then
@ -48,18 +80,21 @@ function ListBox:keyreleased(key, code)
if key == "A" then
if (self.widget.selected >= 1 and self.widget.selected <= #self.widget.list) then
self.widget.list[self.widget.selected]:action()
self.widget.list[self.widget.selected]:action("key")
end
end
if key == "B" then
if (self.widget.cancel >= 1 and self.widget.cancel <= #self.widget.list) then
self.widget.list[self.widget.cancel]:action()
self.widget.list[self.widget.cancel]:action("key")
end
end
end
-- MOUSE FUNCTIONS
-- Handle input from pointers.
function ListBox:mousemoved(x, y)
local widget_selected = self.view.firstSlot + math.floor(y / self.widget.h)
@ -76,12 +111,15 @@ function ListBox:mousepressed(x, y, button, isTouch)
self.widget.selected = widget_selected
self:getFocus()
if #self.widget.list > 0 then
self.widget.list[self.widget.selected]:action()
self.widget.list[self.widget.selected]:action("pointer")
end
end
end
-- DRAW FUNCTIONS
-- draw the menu and the rest of content.
function ListBox:draw()
self:updateView()
local widgety = self.y

View File

@ -1,5 +1,39 @@
-- parent.lua : The parent of the functions.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Menu = Object:extend()
local function updateWidgetByOrder(a, b)
if a.order ~= b.order then
return a.order < b.order
else
return a.creationID < b.creationID
end
end
-- INIT FUNCTIONS
-- Initialize and configure functions.
function Menu:new(menusystem, name, x, y, w, h)
self.menusystem = menusystem
self.name = name
@ -16,17 +50,49 @@ function Menu:new(menusystem, name, x, y, w, h)
self.widget.cancel = 0
self:updateWidgetSize()
self.isDestroyed = false
self.isVisible = true
self.isActive = true
self.isDestroyed = false
self.isVisible = true
self.isActive = true
self.isLocked = false
self.isAlwaysVisible = false
self.sound = {}
self.sound.asset = nil
self.sound.active = false
self.depth = 0
self:resetSound()
self:register()
end
function Menu:setDepth(depth)
self.depth = depth or 0
end
function Menu:setVisibility(visibility)
if self.isLocked == false and self.isAlwaysVisible == false then
-- if the menu is locked (thus is always active), it should also
-- be always visible.
self.isVisible = visibility
else
self.isVisible = true
end
end
function Menu:setActivity(activity)
if self.isLocked == false then
self.isActive = activity
else
self.isActive = true
end
end
function Menu:lock(lock)
self.isLocked = lock
end
function Menu:lockVisibility(lock)
self.isAlwaysVisible = lock
end
function Menu:getFocus()
self.menusystem.focusedMenu = self.name
end
@ -52,14 +118,17 @@ function Menu:getWidgetSize(id)
return self.widget.w, self.widget.h
end
function Menu:cancelAction()
if (self.widget.cancel ~= 0) then
self.widget.list[self.widget.cancel]:action()
end
function Menu:getWidgetNumber()
return #self.widget.list
end
function Menu:update(dt)
-- Cette fonction ne contient rien par défaut
-- ACTION FUNCTIONS
-- Send actions to the widgets
function Menu:cancelAction()
if (self.widget.cancel ~= 0) then
self.widget.list[self.widget.cancel]:action("key")
end
end
function Menu:clear()
@ -80,6 +149,20 @@ function Menu:destroy()
self.destroyed = true
end
function Menu:updateWidgetsOrder()
table.sort(self.widget.list, updateWidgetByOrder)
end
-- UPDATE FUNCTIONS
-- Update the menu every game update
function Menu:update(dt)
-- Cette fonction ne contient rien par défaut
end
-- DRAW FUNCTIONS
-- Draw the menu and its content
function Menu:draw()
-- nothing here
end
@ -92,10 +175,16 @@ function Menu:drawCanvas()
end
-- KEYBOARD FUNCTIONS
-- Handle key press
function Menu:keyreleased(key)
-- Cette fonction ne contient rien par défaut
end
-- MOUSE FUNCTIONS
-- Handle pointers (clic/touch)
function Menu:mousemoved(x, y)
-- Cette fonction ne contient rien par défaut
end
@ -104,12 +193,16 @@ function Menu:mousepressed( x, y, button, istouch )
-- Cette fonction ne contient rien par défaut
end
-- WIDGET FUNCTIONS
-- Handle widgets of the functions
function Menu:addWidget(newwidget)
if #self.widget.list == 0 then
self.widget.selected = 1
end
table.insert(self.widget.list, newwidget)
self:updateWidgetsID()
self:updateWidgetsOrder()
end
function Menu:updateWidgets(dt)
@ -134,12 +227,15 @@ function Menu:removeDestroyedWidgets() -- On retire les widgets marquées comme
end
end
-- CURSOR FUNCTIONS
-- Set or move the cursor of the menu
function Menu:setCursor(cursorid)
self.widget.selected = cursorid --math.max(1, math.min(cursorid, #self.widget.list))
end
function Menu:moveCursor(new_selected)
self:playSelectSound()
self:playNavigationSound()
if new_selected < 1 then
self.widget.selected = #self.widget.list + new_selected
else
@ -151,12 +247,25 @@ function Menu:moveCursor(new_selected)
end
end
-- SOUND FUNCTION
-- Handle SFX
function Menu:resetSound()
self.sound = {}
self.sound.active = false
self.sound.asset = nil
end
function Menu:setSoundFromSceneAssets(name)
self:setSound(self.menusystem.scene.assets.sfx[name])
end
function Menu:setSound(soundasset)
self.sound.active = true
self.sound.asset = soundasset
end
function Menu:playSelectSound()
function Menu:playNavigationSound()
if self.sound.active == true then
love.audio.stop( self.sound.asset )
self.sound.asset:setVolume(core.options.data.audio.sfx / 100)
@ -164,6 +273,9 @@ function Menu:playSelectSound()
end
end
-- VIEW FUNCTIONS
-- Handle the view of the menu
function Menu:resetView()
-- ne sert à rien ici, c'est juste pour éviter des crash
end

View File

@ -1,221 +0,0 @@
local cwd = (...):gsub('%.textmenu$', '') .. "."
local Menu = require(cwd .. "parent")
local TextMenu = Menu:extend()
function TextMenu:new(menusystem, name, x, y, font, slots)
self.slots = slots
TextMenu.super.new(self, menusystem, name, x, y, 0, 0)
self.ox = x
self.oy = y
self.font = font
self.align = "left"
self.begin = 1
self:getBoundingBox()
end
function TextMenu:getBoundingBox()
self:setWidthAuto()
self.widget.h = self.font:getHeight()
self.h = self.widget.h * self.slots
if self.align == "right" then
self.x = self.ox - self.w
elseif self.align == "center" then
self.x = self.ox - self.w / 2
else
self.x = self.ox
end
self.y = self.oy
end
function TextMenu:centerText()
self:setAlign("center")
end
function TextMenu:setAlign(align)
self.align = align
self.font:setAlign("center")
end
function TextMenu:update(dt)
self:getBoundingBox()
if self.widget.selected ~= 0 then
if self.widget.selected < self.begin then
self.begin = self.widget.selected
end
if self.widget.selected > self.begin + self.slots - 1 then
self.begin = self.widget.selected - self.slots + 1
end
end
if self.begin < 1 then
self.begin = 1
end
end
function TextMenu:setWidthAuto()
local width = self.w
for i,v in ipairs(self.widget.list) do
local stringWidth = self.font:getWidth(v.beginlabel .. v.label .. v.endlabel)
width = math.max(stringWidth, width)
end
if width ~= self.w then
self.canvas.needRedraw = true
end
self.w = width
end
function TextMenu:getWidth()
self:setWidthAuto()
return self.w
end
function TextMenu:getHeight()
return self.h
end
function TextMenu:keyreleased(key, code)
if key == 'up' then
self:moveCursor(self.widget.selected - 1)
end
if key == 'down' then
self:moveCursor(self.widget.selected + 1)
end
if key == "A" then
if (self.widget.selected > 0) and (self.widget.selected <= #self.widget.list) then
self.widget.list[self.widget.selected]:action()
end
end
if key == "B" then
self:cancelAction()
end
end
function TextMenu:mousemoved(x, y)
local selectedPrevous = self.widget.selected
self.widget.selected = self.begin + math.floor(y / self.widget.h)
if self.widget.selected < 1 then
self.widget.selected = 1
end
if self.widget.selected > #self.widget.list then
self.widget.selected = #self.widget.list
end
if self.widget.selected ~= selectedPrevious then
self.canvas.needRedraw = true
end
end
function TextMenu:mousepressed(x, y, button, isTouch)
self.widget.selected = self.begin + math.floor(y / self.widget.h)
if self.widget.selected < 1 then
self.widget.selected = 1
end
if self.widget.selected > #self.widget.list then
self.widget.selected = #self.widget.list
end
if #self.widget.list > 0 then
self.widget.list[self.widget.selected]:action()
end
if self.widget.selected ~= selectedPrevious then
self.canvas.needRedraw = true
end
end
function TextMenu:drawCanvas()
print("redraw menu")
self.canvas.texture = love.graphics.newCanvas(self.w, self.h)
love.graphics.setCanvas( self.canvas.texture )
local ox, x
local widgety = 0
local ox = self.w / 2
local x = 0
self.font:set()
for i, v in ipairs(self.widget.list) do
if (i >= self.begin) and (i < self.begin + self.slots) then
self:drawWidget(i, widgety)
widgety = widgety + self.widget.h
end
end
utils.draw.resetColor()
love.graphics.setCanvas( )
end
function TextMenu:drawWidget(widgetID, y)
local widget = self.widget.list[widgetID]
print(widget)
if widget.canvas.needRedraw == true then
self:drawWidgetCanvas(widget)
widget.canvas.needRedraw = false
end
if self.widget.selected == widgetID and self.focus == true then
love.graphics.setColor(1, 1, 0, 1)
else
love.graphics.setColor(1, 1, 1, 1)
end
love.graphics.draw(widget.canvas.texture, 0, y)
end
function TextMenu:drawWidgetCanvas(widget)
widget.canvas.texture = love.graphics.newCanvas(self.w, self.widget.h)
love.graphics.setCanvas( widget.canvas.texture )
self.font:draw(widget.label, math.floor(self.w / 2), 0, -1, self.align)
self.font:draw(widget.beginlabel, math.floor(0), 0, -1, "left")
self.font:draw(widget.endlabel, math.floor(self.w), 0, -1, "right")
love.graphics.setCanvas( self.canvas.texture )
end
function TextMenu:resetView()
self.begin = 1
self.canvas.needRedraw = true
end
function Menu:moveView(begin, absolute)
--local absolute = absolute or true
self.widget.selected = 0
if (absolute) then
self.begin = begin
else
self.begin = self.begin + begin
end
-- ne sert à rien ici, c'est juste pour éviter des crash
self.canvas.needRedraw = true
end
function Menu:getView()
return self.begin
end
function Menu:isViewAtBeggining()
return (self.begin <= 1)
end
function Menu:isViewAtEnd()
return ((self.begin + self.slots) > (#self.widget.list))
end
return TextMenu

View File

@ -1,8 +1,34 @@
-- widgets :: basic widget object
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Widget = {}
BaseWidget = Object:extend()
TextWidget = BaseWidget:extend()
-- INIT FUNCTIONS
-- Initialize and configure the widget
function BaseWidget:new(menu)
self.menu = menu
@ -11,27 +37,37 @@ function BaseWidget:new(menu)
self.selection_margin = 0
self.margin = 2
self:register()
self.canvas = {}
self.canvas.texture = nil
self.canvas.needRedraw = true
self.order = 0
self:register()
end
function BaseWidget:register()
self.creationID = self.menu:getWidgetNumber()
self.menu:addWidget(self)
end
function BaseWidget:redrawCanvas()
self.width, self.height = self.menu:getWidgetSize(self.id)
self.canvas.texture = love.graphics.newCanvas(self.width, self.height)
love.graphics.setCanvas( self.canvas.texture )
local canvas = love.graphics.newCanvas(self.width, self.height)
love.graphics.setCanvas( canvas )
self:drawCanvas()
self.canvas.needRedraw = false
love.graphics.setCanvas( )
local imageData = canvas:newImageData( )
self.canvas.texture = love.graphics.newImage( imageData )
canvas:release( )
imageData:release( )
end
function BaseWidget:invalidateCanvas()
self.canvas.needRedraw = true
end
function BaseWidget:drawCanvas()
@ -50,6 +86,9 @@ function BaseWidget:selectAction()
-- Do nothing
end
-- DRAW WIDGETS
-- Draw the widget
function BaseWidget:draw(x, y)
if self.canvas.texture ~= nil then
utils.graphics.resetColor()
@ -61,6 +100,9 @@ function BaseWidget:drawSelected(x,y,w,h)
self:draw(x, y, w, h)
end
-- UPDATE FUNCTIONS
-- Update the widget
function BaseWidget:update(dt)
if (self.canvas.needRedraw) then
self:redrawCanvas()
@ -68,7 +110,10 @@ function BaseWidget:update(dt)
-- N/A
end
function BaseWidget:action()
-- ACTION FUNCTION
-- Functions to handle actions and selection.
function BaseWidget:action(source)
--self:destroy()
end
@ -76,6 +121,7 @@ function BaseWidget:destroy()
self.destroyed = true
end
-- TEXT WIDGET
-- Simple text widget
function TextWidget:new(menu, font, label)
@ -91,7 +137,7 @@ function TextWidget:drawCanvas()
self.font:draw(self.label, w, h, -1, "center")
end
-- Add the widget as subvariable to the returned table
Widget.Base = BaseWidget
Widget.Text = TextWidget

View File

@ -1,8 +1,31 @@
-- widgets/utils :: basic utility functions for the widgets
--[[
Copyright © 2019 Kazhnuz
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.
]]
local menuUtils = {}
function menuUtils.drawCursor(x, y, w, h)
love.graphics.setColor(0,0,0)
love.graphics.rectangle("fill", x, y, 4, 8)
love.graphics.rectangle("fill", x, y, 8, 4)

View File

@ -22,27 +22,64 @@
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
local cwd = (...):gsub('%.scenes$', '') .. "."
local Scene = Object:extend()
local Assets = require "core.modules.assets"
local MenuSystem = require "core.modules.menusystem"
local Assets = require(cwd .. "assets")
local MenuSystem = require(cwd .. "menusystem")
-- INIT FUNCTIONS
-- Initialize and configure the scene
function Scene:new()
self.mouse = {}
self.mouse.x, self.mouse.y = core.screen:getMousePosition()
self.assets = Assets()
self.menusystem = MenuSystem()
self.keys = core.input:getKeyList()
self.menusystem = MenuSystem(self)
self.sources = core.input:getSources()
self.inputLocked = false
self.inputLockedTimer = 0
self:initWorld()
self:register()
end
function Scene:register()
core.scenemanager.currentScene = self
core.scenemanager:setScene(self)
end
function Scene:clear()
-- TODO: send automatic cleanups to the different elements of the scene
end
-- UPDATE FUNCTIONS
-- Handle stuff that happens every steps
function Scene:updateStart(dt)
end
function Scene:update(dt)
-- Empty function, is just here to avoid crash
end
function Scene:updateEnd(dt)
end
function Scene:updateWorld(dt)
if (self.world ~= nil) and (self.world.isActive) then
self.world:update(dt)
end
end
-- MOUSE FUNCTIONS
-- Make the scene support the pointer
function Scene:mousemoved(x, y, dx, dy)
-- Empty function, is just here to avoid crash
end
@ -51,17 +88,84 @@ function Scene:mousepressed( x, y, button, istouch )
-- Empty function, is just here to avoid crash
end
-- WORLD FUNCTIONS
-- Basic functions to manage the world
function Scene:initWorld()
self.world = nil
end
function Scene:registerWorld(world)
self.world = world
end
-- KEYBOARD FUNCTIONS
-- Add send keys functions to the scene
function Scene:keypressed( key, scancode, isrepeat )
end
function Scene:keyreleased( key )
end
-- DRAW FUNCTIONS
-- Draw the scene and its content
function Scene:drawStart()
end
function Scene:draw()
end
function Scene:clear()
function Scene:drawEnd()
end
function Scene:drawWorld(dt)
if (self.world ~= nil) then
self.world:draw()
end
end
-- INPUT FUNCTIONS
-- Handle inputs from keyboard/controllers
function Scene:setKeys()
if (self.inputLocked) then
self.sources = core.input.fakesources
self.inputLockedTimer = self.inputLockedTimer - 1
if (self.inputLockedTimer <= 0 ) then
self.inputLocked = false
end
else
self.sources = core.input.sources
end
self.menusystem.keys = self.sources[1].keys
end
function Scene:getKeys(sourceid)
if sourceid == nil then
print("WARNING", "no sourceid detected, will default to 1")
end
local sourceid = sourceid or 1
if (self.inputLocked) then
return self.sources[sourceid].keys
else
return core.input.fakekeys
end
end
function Scene:flushKeys()
core.input:flushKeys()
self.keys = core.input.keys
self.sources = core.input:getSources()
self.inputLockedTimer = 1
self.inputLocked = true
end
return Scene

View File

@ -0,0 +1,202 @@
-- actor2D.lua :: the implementation of a 2D actor. It contain every element
-- needed to create your own 2D actors.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.actor2D$', '') .. "."
local BaseActor = require(cwd .. "baseactor")
local Actor2D = BaseActor:extend()
-- INIT FUNCTIONS
-- Initialise the actor and its base functions
function Actor2D:new(world, type, x, y, w, h, isSolid)
self:initHitbox(x, y, w, h)
Actor2D.super.new(self, world, type, isSolid)
end
-- MOVEMENT FUNCTIONS
-- Basic functions from the movement.
function Actor2D:initMovement()
self.xsp = 0
self.ysp = 0
self.xfrc = 0
self.yfrc = 0
end
function Actor2D:autoMove(dt)
self.onGround = false
self:applyGravity(dt)
local newx, newy, cols, colNumber = self:move(self.x + self.xsp * dt, self.y + self.ysp * dt)
-- apply after the movement the friction, until the player stop
-- note: the friction is applied according to the delta time,
-- thus the friction should be how much speed is substracted in 1 second
self:solveAllCollisions(cols)
self.xsp = utils.math.toZero(self.xsp, self.xfrc * dt)
self.ysp = utils.math.toZero(self.ysp, self.yfrc * dt)
end
function Actor2D:solveAllCollisions(cols)
for i, col in ipairs(cols) do
self:collisionResponse(col)
if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
self:changeSpeedToCollisionNormal(col.normal.x, col.normal.y)
end
end
end
function Actor2D:collisionResponse(collision)
-- here come the response to the collision
end
function Actor2D:changeSpeedToCollisionNormal(nx, ny)
local xsp, ysp = self.xsp, self.ysp
if (nx < 0 and xsp > 0) or (nx > 0 and xsp < 0) then
xsp = -xsp * self.bounceFactor
end
if (ny < 0 and ysp > 0) or (ny > 0 and ysp < 0) then
ysp = -ysp * self.bounceFactor
end
self.xsp, self.ysp = xsp, ysp
end
function Actor2D:checkGroundX()
local dx, dy = self.x + utils.math.sign(self.xgrav), self.y
local newx, newy, cols, colNumber = self:checkCollision(dx, dy)
for i, col in ipairs(cols) do
if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
if not (self.ygrav == 0) then
if col.normal.x ~= utils.math.sign(self.xgrav) then self.onGround = true end
end
end
end
end
function Actor2D:checkGroundY()
local dx, dy = self.x, self.y + utils.math.sign(self.ygrav)
local newx, newy, cols, colNumber = self:checkCollision(dx, dy)
for i, col in ipairs(cols) do
if (col.type == "touch") or (col.type == "bounce") or (col.type == "slide") then
if not (self.ygrav == 0) then
if col.normal.y ~= utils.math.sign(self.ygrav) then self.onGround = true end
end
end
end
end
function Actor2D:move(dx, dy)
local cols, colNumber = {}, 0
if (self.isDestroyed == false) then
self.x, self.y, cols, colNumber = self.world:moveActor(self, dx, dy, self.filter)
end
return self.x, self.y, cols, colNumber
end
function Actor2D:checkCollision(dx, dy)
local x, y, cols, colNumber = dx, dy, {}, 0
if (self.isDestroyed == false) then
x, y, cols, colNumber = self.world:moveActor(self, dx, dy, self.filter)
end
return self.x, self.y, cols, colNumber
end
function Actor2D:initGravity()
local xgrav, ygrav
if (self.world.gravity.isDefault) then
self.xgrav = self.world.gravity.xgrav
self.ygrav = self.world.gravity.ygrav
else
self.xgrav = 0
self.ygrav = 0
end
self.onGround = false
end
function Actor2D:setXGravity(grav)
self.xgrav = grav
end
function Actor2D:setYGravity(grav)
self.ygrav = grav
end
function Actor2D:applyGravity(dt)
self.xsp = self.xsp + self.xgrav * dt
self.ysp = self.ysp + self.ygrav * dt
if utils.math.sign(self.ysp) == utils.math.sign(self.ygrav) then
self:checkGroundY( )
end
if utils.math.sign(self.xsp) == utils.math.sign(self.xgrav) then
self:checkGroundX( )
end
end
-- COORDINATE FUNCTIONS
-- Functions related to coordinate and hitbox
function Actor2D:initHitbox(x, y, w, h)
self.x = x or 0
self.y = y or 0
self.w = w or 0
self.h = h or 0
end
function Actor2D:getCenter()
return (self.x + (self.w / 2)), (self.y + (self.h / 2))
end
function Actor2D:getViewCenter()
return self:getCenter()
end
function Actor2D:drawHitbox()
local x, y = math.floor(self.x), math.floor(self.y)
love.graphics.setColor(self.debug.r, self.debug.g, self.debug.b, 1)
utils.graphics.box(x, y, self.w, self.h)
end
-- DRAW FUNCTIONS
-- Draw the actors.
function Actor2D:draw()
self:drawStart()
local x, y = math.floor(self.x), math.floor(self.y)
self:drawSprite(x, y)
self:drawEnd()
end
return Actor2D

View File

@ -0,0 +1,253 @@
-- actor2D.lua :: the global implementation of an actor. Basically, it abstract
-- everything that isn't 2D or 3D related to the actor system.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.baseactor$', '') .. "."
local BaseActor = Object:extend()
local Timer = require(cwd .. "utils.timer")
-- INIT FUNCTIONS
-- Initialise the actor and its base functions
function BaseActor:new(world, type, isSolid)
self.type = type or ""
self.isSolid = isSolid or false
self.depth = 0
self:setManagers(world)
self:initKeys()
self:initTimers()
self:setSprite()
self:initPhysics()
self:setDebugColor(1, 1, 1)
self:register()
end
function BaseActor:setManagers(world)
self.world = world
self.scene = world.scene
self.obj = world.obj
self.assets = self.scene.assets
end
function BaseActor:setDebugColor(r,g,b)
self.debug = {}
self.debug.r = r
self.debug.g = g
self.debug.b = b
end
function BaseActor:register()
self.world:registerActor(self)
self.isDestroyed = false
end
function BaseActor:destroy()
self.world:removeActor(self)
self.isDestroyed = true
end
-- PHYSICS INITIALISATION
-- Basic initialization of the physic systems
function BaseActor:initPhysics()
self:initMovement()
self:initGravity()
self:setBounceFactor()
self:setFilter()
end
function BaseActor:setBounceFactor(newBounceFactor)
self.bounceFactor = newBounceFactor or 0
end
function BaseActor:setFilter()
-- Init the bump filter
self.filter = function(item, other)
if (other.isSolid) then
return "slide"
else
return "cross"
end
end
end
function BaseActor:initMovement( )
-- Empty placeholder function
end
function BaseActor:initGravity( )
-- Empty placeholder function
end
-- UPDATE FUNCTIONS
-- Theses functions are activated every steps
function BaseActor:updateStart(dt)
end
function BaseActor:update(dt)
self:updateStart(dt)
self:updateTimers(dt)
self:autoMove(dt)
self:updateSprite(dt)
self:updateEnd(dt)
end
function BaseActor:updateEnd(dt)
end
function BaseActor:autoMove(dt)
-- The base actor don't have coordinate
-- so the autoMove is only usefull to its
-- 2D and 3D childrens
end
-- INPUT FUNCTIONS
-- get input from the world object
function BaseActor:initKeys()
self.keys = core.input.fakekeys
end
function BaseActor:getInput(keys)
self.keys = keys or core.input.fakekeys
end
-- TIMER FUNCTIONS
-- Control the integrated timers of the actor
function BaseActor:initTimers()
self.timers = {}
end
function BaseActor:addTimer(name, t)
self.timers[name] = Timer(self, name, t)
end
function BaseActor:updateTimers(dt)
for k,v in pairs(self.timers) do
v:update(dt)
end
end
function BaseActor:timerResponse(name)
-- here come the timer responses
end
-- DRAW FUNCTIONS
-- Draw the actors.
function BaseActor:drawStart()
end
function BaseActor:draw()
self:drawStart()
self:drawEnd()
end
function BaseActor:drawEnd()
end
function BaseActor:drawHUD(id, height, width)
end
-- SPRITES FUNCTIONS
-- Handle the sprite of the actor
function BaseActor:setSprite(spritename, ox, oy)
self.sprite = {}
self.sprite.name = spritename or nil
self.sprite.ox = ox or 0
self.sprite.oy = oy or 0
self.sprite.sx = 1
self.sprite.sy = 1
self.sprite.exist = (spritename ~= nil)
self.sprite.clone = nil
end
function BaseActor:cloneSprite()
if self.sprite.name ~= nil then
self.sprite.clone = self.assets.sprites[self.sprite.name]:clone()
end
end
function BaseActor:changeAnimation(animation, restart)
if (self.sprite.clone == nil) then
self.assets.sprites[self.sprite.name]:changeAnimation(animation, restart)
else
self.sprite.clone:changeAnimation(animation, restart)
end
end
function BaseActor:setCustomSpeed(customSpeed)
if (self.sprite.clone == nil) then
self.assets.sprites[self.sprite.name]:setCustomSpeed(customSpeed)
else
self.sprite.clone:setCustomSpeed(customSpeed)
end
end
function BaseActor:updateSprite(dt)
if (self.sprite.clone ~= nil) then
self.sprite.clone:update(dt)
end
end
function BaseActor:setSpriteScallingX(sx)
local sx = sx or 1
self.sprite.sx = sx
end
function BaseActor:setSpriteScallingY(sy)
local sy = sy or 1
self.sprite.sy = sy
end
function BaseActor:drawSprite(x, y, r, sx, sy, ox, oy, kx, ky)
if (self.sprite.name ~= nil) then
local x = x + self.sprite.ox
local y = y + self.sprite.oy
local sx = sx or self.sprite.sx
local sy = sy or self.sprite.sy
if (self.sprite.clone ~= nil) then
self.sprite.clone:draw(x, y, r, sx, sy, ox, oy, kx, ky)
else
self.assets.sprites[self.sprite.name]:drawAnimation(x, y, r, sx, sy, ox, oy, kx, ky)
end
end
end
return BaseActor

View File

@ -0,0 +1,46 @@
-- gfx.lua :: a basic 2D GFX.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.gfx2D$', '') .. "."
local Actor2D = require(cwd .. "actor2D")
local GFX = Actor2D:extend()
function GFX:new(world, x, y, spritename)
local width, height = world.scene.assets.sprites[spritename]:getDimensions()
GFX.super.new(self, world, "gfx", x - (width/2), y - (height/2), width, height)
self:setSprite(spritename)
self:cloneSprite()
local duration = self.sprite.clone:getAnimationDuration()
self:addTimer("destroy", duration)
self.depth = -100
end
function GFX:timerResponse(name)
if (name == "destroy") then
self:destroy()
end
end
return GFX

View File

@ -0,0 +1,44 @@
-- timer.lua :: a basic implementation of a timer for the actor system.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Timer = Object:extend()
function Timer:new(actor, name, t)
self.time = t
self.actor = actor
self.name = name
end
function Timer:update(dt)
self.time = self.time - dt
if (self.time <= 0) then
self:finish()
end
end
function Timer:finish()
self.actor:timerResponse(self.name)
self.actor.timers[self.name] = nil
end
return Timer

View File

@ -0,0 +1,437 @@
-- baseworld.lua :: the base world object, that contain just a fast implementation
-- of a 2D world. It doesn't support collision and stuff.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.baseworld$', '') .. "."
local BaseWorld = Object:extend()
local Sti = require(cwd .. "libs.sti")
local CameraSystem = require(cwd .. "camera")
local PADDING_VALUE = 10/100
-- INIT FUNCTIONS
-- All functions to init the world and the map
function BaseWorld:new(scene, actorlist, mapfile)
self.scene = scene
self.actorlist = actorlist
self.mapfile = mapfile
self.cameras = CameraSystem(self)
self:initActors()
self:initPlayers()
self:setActorList(self.actorlist)
self:initMap(self.mapfile)
self:setGravity()
self:register()
self.isActive = true
end
function BaseWorld:setActorList(actorlist)
if actorlist == nil then
error("FATAL: You must set an actorlist to your world")
else
self.obj = require(actorlist)
end
end
function BaseWorld:initMap(mapfile)
self.haveMap = false
self.haveBackgroundColor = false
self.backcolor = {128, 128, 128}
self.mapfile = mapfile
end
function BaseWorld:setGravity(xgrav, ygrav, isDefault)
local xgrav = xgrav or 0
local ygrav = ygrav or 0
local isDefault = isDefault or 0
self.gravity = {}
self.gravity.xgrav, self.gravity.ygrav = xgrav, ygrav
self.gravity.isDefault = isDefault
end
function BaseWorld:register()
self.scene:registerWorld(self)
end
function BaseWorld:reset()
self:initActors()
self:initPlayers()
self:initMap(self.mapfile)
self.cameras:initViews()
collectgarbage()
self:loadMap()
end
-- ACTOR MANAGEMENT FUNCTIONS
-- Basic function to handle actors
function BaseWorld:initActors( )
self.actors = {}
self.currentCreationID = 0
end
function BaseWorld:newActor(name, x, y)
self.obj.index[name](self, x, y)
end
function BaseWorld:newCollision(name, x, y, w, h)
self.obj.collisions[name](self, x, y, w, h)
end
function BaseWorld:registerActor(actor)
actor.creationID = self.currentCreationID
self.currentCreationID = self.currentCreationID + 1
table.insert(self.actors, actor)
end
function BaseWorld:removeActor(actor)
for i,v in ipairs(self.actors) do
if v == actor then
table.remove(self.actors, i)
end
end
end
function BaseWorld:moveActor(actor, x, y, filter)
-- as the baseworld have no collision function, we return empty collision
-- datas, but from the same type than bump2D will return
return x, y, {}, 0
end
function BaseWorld:checkCollision(actor, x, y, filter)
-- as the baseworld have no collision function, we return empty collision
-- datas, but from the same type than bump2D will return
return x, y, {}, 0
end
function BaseWorld:queryRect(x, y, w, h)
local query = {}
local x2, y2 = x + w, y + h
for i,v in ipairs(self.actors) do
if (v.x >= x) and (v.x + v.w >= x1) and
(v.y >= y) and (v.y + v.h >= y1) then
table.insert(query, v)
end
end
return v
end
function BaseWorld:countActors()
return #self.actors
end
function BaseWorld:getActors()
return self.actors
end
function BaseWorld:getVisibleActors(id)
local camx, camy, camw, camh = self.cameras:getViewCoordinate(id)
local paddingw = camw * PADDING_VALUE
local paddingh = camh * PADDING_VALUE
local x = camx - paddingw
local y = camy - paddingh
local w = camw + paddingw * 2
local h = camh + paddingh * 2
return self:queryRect(x, y, w, h)
end
-- INFO FUNCTIONS
-- Give infos about the world
function BaseWorld:isActorIndexed(name)
return (self.obj.index[name] ~= nil)
end
function BaseWorld:isCollisionIndexed(name)
return (self.obj.collisions[name] ~= nil)
end
-- PLAYER MANAGEMENT FUNCTIONS
-- Basic function to handle player actors
function BaseWorld:loadMap()
local mapfile = self.mapfile
if mapfile == nil then
self.haveMap = false
self.haveBackgroundColor = false
self.backcolor = {128, 128, 128}
else
self.haveMap = true
self.map = Sti(mapfile)
self.haveBackgroundColor = true
self.backcolor = self.map.backgroundcolor or {128, 128, 128}
self:loadMapObjects()
end
end
function BaseWorld:initPlayers()
self.players = {}
self.playerNumber = 1
end
function BaseWorld:setPlayerNumber(playerNumber)
self.playerNumber = playerNumber or 1
end
function BaseWorld:addPlayer(actor, sourceid, haveCam)
local player = {}
player.actor = actor
player.sourceid = sourceid or 1
table.insert(self.players, player)
if (haveCam) then
local xx, yy = player.actor:getViewCenter()
self.cameras:addView(xx, yy, player.actor)
end
end
function BaseWorld:sendInputToPlayers(actor)
for i,v in ipairs(self.players) do
--TODO: make the player get from a selected source inputs
local keys = self.scene.sources[v.sourceid].keys
v.actor:getInput(keys)
end
end
function BaseWorld:removePlayer(actor)
for i,v in ipairs(self.players) do
if (v.actor == actor) then
table.remove(self.players, i)
end
end
end
-- MAP FUNCTIONS
-- All map wrappers
function BaseWorld:loadMapObjects()
self:loadMapCollisions()
self:loadMapPlayers()
self:loadMapActors()
end
function BaseWorld:loadMapCollisions()
for k, objectlayer in pairs(self.map.layers) do
if self:isCollisionIndexed(objectlayer.name) then
print("DEBUG: loading actors in " .. objectlayer.name .. " collision layer")
for k, object in pairs(objectlayer.objects) do
self:newCollision(objectlayer.name, object.x, object.y, object.width, object.height)
end
self.map:removeLayer(objectlayer.name)
end
end
end
function BaseWorld:loadMapActors()
for k, objectlayer in pairs(self.map.layers) do
if self:isActorIndexed(objectlayer.name) then
print("DEBUG: loading actors in " .. objectlayer.name .. " actor layer")
for k, object in pairs(objectlayer.objects) do
if (object.properties.batchActor) then
self:batchActor(objectlayer.name, object)
else
self:newActor(objectlayer.name, object.x, object.y)
end
end
self.map:removeLayer(objectlayer.name)
end
end
end
function BaseWorld:batchActor(name, object)
local gwidth = object.properties.gwidth or self.map.tilewidth
local gheight = object.properties.gheight or self.map.tileheight
local x = object.x
local y = object.y
local w = object.width
local h = object.height
local cellHor = math.ceil(w / gwidth)
local cellVert = math.ceil(h / gheight)
for i=1, cellHor do
for j=1, cellVert do
self:newActor(name, x + (i-1)*gwidth, y + (j-1)*gheight)
end
end
end
function BaseWorld:loadMapPlayers()
for k, objectlayer in pairs(self.map.layers) do
if (objectlayer.name == "player") then
print("DEBUG: loading actors in player layer")
local i = 1
for k, object in pairs(objectlayer.objects) do
if (i <= self.playerNumber) then
-- TODO: don't hardcode camera handling
self:addPlayer(self.obj.Player(self, object.x, object.y), i, true)
end
i = i + 1
end
self.map:removeLayer(objectlayer.name)
end
end
end
function BaseWorld:getDimensions()
if self.haveMap then
return self.map.width * self.map.tilewidth,
self.map.height * self.map.tileheight
else
return core.screen:getDimensions()
end
end
function BaseWorld:setBackgroundColor(r, g, b)
self.backcolor = {r, g, b}
self.haveBackgroundColor = true
end
function BaseWorld:removeBackgroundColor()
self.haveBackgroundColor = false
end
function BaseWorld:getBackgroundColor()
return self.backcolor[1]/256, self.backcolor[2]/256, self.backcolor[3]/256
end
-- Lock MANAGEMENT FUNCTIONS
-- Basic function to handle the lock
function BaseWorld:setActivity(activity)
self.isActive = activity
end
function BaseWorld:switchActivity()
self.isActive = (self.isActive == false)
end
function BaseWorld:getActivity()
return self.isActive
end
-- UPDATE FUNCTIONS
-- All update functions
function BaseWorld:update(dt)
self:updateMap(dt)
self:sendInputToPlayers(dt)
self:updateActors(dt)
self.cameras:update(dt)
end
function BaseWorld:updateActors(dt)
local actors = self:getActors()
for i,v in ipairs(actors) do
v:update(dt)
end
end
function BaseWorld:updateMap(dt)
if self.haveMap then
self.map:update(dt)
end
end
-- DRAW FUNCTIONS
-- All function to draw the map, world and actors
function BaseWorld:draw(dt)
self:drawBackgroundColor()
local camNumber = self.cameras:getViewNumber()
if (camNumber == 0) then
self:drawMap()
self:drawActors()
else
for i=1, camNumber do
self.cameras:attachView(i)
self:drawMap(i)
self:drawActors(i)
self.cameras:detachView(i)
self.cameras:drawHUD(i)
end
end
end
function BaseWorld:drawActors(id)
local actors
if (id == nil) then
actors = self:getActors()
else
actors = self:getVisibleActors(id)
end
table.sort(actors, function(a,b)
if (a.depth == b.depth) then
return a.creationID < b.creationID
else
return a.depth > b.depth
end
end)
for i,v in ipairs(actors) do
v:draw()
end
end
function BaseWorld:drawMap(id)
local tx, ty, scale = 0, 0, 1
if id ~= nil then
-- Du à la manière dont fonctionne STI, on est obligé de récupérer les info
-- de position de camera pour afficher la carte par rapport à ces infos
tx, ty = self.cameras:getViewCoordinate(id)
scale = self.cameras:getViewScale(id) or 1
local vx, vy = self.cameras:getOnScreenViewRelativePosition(id)
tx = math.floor(tx - math.abs(vx))
ty = math.floor(ty - math.abs(vy))
end
if self.haveMap then
self.map:draw(-tx, -ty, scale, scale)
end
end
function BaseWorld:drawBackgroundColor()
if self.haveBackgroundColor then
local r, g, b = self.backcolor[1], self.backcolor[2], self.backcolor[3]
love.graphics.setColor(r/256, g/256, b/256)
love.graphics.rectangle("fill", 0, 0, 480, 272)
utils.graphics.resetColor()
end
end
return BaseWorld

View File

@ -0,0 +1,337 @@
-- camera.lua :: a basic camera adapted to the asset/world system.
-- Use hump.camera as the view backend.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.camera$', '') .. "."
local CameraSystem = Object:extend()
local View = require(cwd .. "libs.hump.camera")
local SPLITSCREEN_ISVERTICAL = false
local SCREEN_LIMIT = 4
-- INIT FUNCTIONS
-- Initialize the camera system
function CameraSystem:new(world)
self.scene = world.scene
self.world = world
self.verticalSplit = SPLITSCREEN_ISVERTICAL
self:initViews()
end
function CameraSystem:initViews()
self.views = {}
self.views.list = {}
self.views.basewidth, self.views.baseheight = core.screen:getDimensions()
self.views.width, self.views.height = self:getViewsDimensions()
self.views.posList = {}
self.views.posList.dual = {}
self.views.posList.multi = {}
if (self.verticalSplit) then
self.views.posList.dual[1] = {}
self.views.posList.dual[1].x = 0
self.views.posList.dual[1].y = (self.views.baseheight/4)
self.views.posList.dual[2] = {}
self.views.posList.dual[2].x = 0
self.views.posList.dual[2].y = -(self.views.baseheight/4)
else
self.views.posList.dual[1] = {}
self.views.posList.dual[1].x = -(self.views.basewidth/4)
self.views.posList.dual[1].y = 0
self.views.posList.dual[2] = {}
self.views.posList.dual[2].x = (self.views.basewidth/4)
self.views.posList.dual[2].y = 0
end
self.views.posList.multi[1] = {}
self.views.posList.multi[1].x = -(self.views.basewidth /4)
self.views.posList.multi[1].y = (self.views.baseheight/4)
self.views.posList.multi[2] = {}
self.views.posList.multi[2].x = (self.views.basewidth /4)
self.views.posList.multi[2].y = (self.views.baseheight/4)
self.views.posList.multi[3] = {}
self.views.posList.multi[3].x = -(self.views.basewidth /4)
self.views.posList.multi[3].y = -(self.views.baseheight/4)
self.views.posList.multi[4] = {}
self.views.posList.multi[4].x = (self.views.basewidth /4)
self.views.posList.multi[4].y = -(self.views.baseheight/4)
end
-- INFO FUNCTIONS
-- Get informations from the camera system
function CameraSystem:getViewNumber()
return #self.views.list
end
function CameraSystem:haveView()
return (self:getViewNumber() == 0)
end
function CameraSystem:getViewsDimensions(viewNumber)
local basewidth, baseheight = self.views.basewidth, self.views.baseheight
local viewnumber = viewNumber or self:getViewNumber()
if (viewnumber <= 1) then
return basewidth, baseheight
elseif (viewnumber == 2) then
if (self.verticalSplit) then
return (basewidth), (baseheight/2)
else
return (basewidth/2), (baseheight)
end
else
return (basewidth/2), (baseheight/2)
end
end
function CameraSystem:recalculateViewsPositions()
if #self.views.list == 1 then
self.views.list[1].pos.onScreen.x = 0
self.views.list[1].pos.onScreen.y = 0
else
for i,v in ipairs(self.views.list) do
local x, y = self:getViewPositions(i)
self.views.list[i].pos.onScreen.x = x
self.views.list[i].pos.onScreen.y = y
end
end
end
function CameraSystem:getViewPositions(id)
local viewNumber = #self.views.list
if (viewNumber == 2) and ((id == 1) or (id == 2)) then
return self.views.posList.dual[id].x, self.views.posList.dual[id].y
elseif (viewNumber > 2) then
return self.views.posList.multi[id].x, self.views.posList.multi[id].y
end
end
-- WRAPPER and UTILS
-- Access data from the views
function CameraSystem:addView(x, y, target)
if (#self.views.list < SCREEN_LIMIT) then
local id = #self.views.list + 1
local view = {}
view.pos = {}
view.pos.x = x or 0
view.pos.y = y or 0
view.pos.onScreen = {}
view.cam = View(view.pos.x, view.pos.y, 1, 0, true)
-- TODO: add a target system in order to make a camera able
-- to target a specific object
view.target = target
table.insert(self.views.list, view)
self.views.width, self.views.height = self:getViewsDimensions()
self:recalculateViewsPositions()
end
end
function CameraSystem:getView(id)
return self.views.list[id]
end
function CameraSystem:getViewCam(id)
local view = self:getView(id)
return view.cam
end
function CameraSystem:attachView(id)
if (id ~= nil) then
local cam = self:getViewCam(id)
local viewx, viewy, vieww, viewh = self:getOnScreenViewCoordinate(id)
cam:attach()
love.graphics.setScissor(viewx, viewy, vieww, viewh)
end
end
function CameraSystem:detachView(id)
if (id ~= nil) then
local cam = self:getViewCam(id)
love.graphics.setScissor( )
cam:detach()
end
end
function CameraSystem:getViewCoordinate(id)
local cam = self:getViewCam(id)
local camx, camy, camw, camh
camx = cam.x - (self.views.width/2)
camy = cam.y - (self.views.height/2)
camw = self.views.width
camh = self.views.height
return camx, camy, camw, camh
end
function CameraSystem:getOnScreenViewCoordinate(id)
local view = self:getView(id)
local viewx, viewy, vieww, viewh
local basex, basey = (self.views.basewidth / 2), (self.views.baseheight / 2)
viewx = (basex) + view.pos.onScreen.x - (self.views.width / 2)
viewy = (basey) + view.pos.onScreen.y - (self.views.height / 2)
vieww = self.views.width
viewh = self.views.height
return viewx, viewy, vieww, viewh
end
function CameraSystem:getOnScreenViewRelativePosition(id)
local view = self:getView(id)
local viewx, viewy
local basex, basey = (self.views.basewidth / 2), (self.views.baseheight / 2)
viewx = view.pos.onScreen.x
viewy = view.pos.onScreen.y
return viewx, viewy
end
function CameraSystem:getOnScreenViewCenter(id)
local view = self:getView(id)
local viewx, viewy
local basex, basey = (self.views.basewidth / 2), (self.views.baseheight / 2)
viewx = (basex) + view.pos.onScreen.x
viewy = (basey) + view.pos.onScreen.y
return viewx, viewy
end
function CameraSystem:getViewScale(id)
local cam = self:getViewCam(id)
return cam.scale
end
function CameraSystem:limitView(id)
local viewx, viewy, vieww, viewh = self:getViewCoordinate(id)
local worldw, worldh = self.world:getDimensions()
local posx = self.views.list[id].pos.x
local posy = self.views.list[id].pos.y
local minx = self.views.width / 2
local miny = self.views.height / 2
local maxx = worldw - minx
local maxy = worldh - miny
self.views.list[id].pos.x = utils.math.between(posx, minx, maxx)
self.views.list[id].pos.y = utils.math.between(posy, miny, maxy)
self:computeCamPosition(id)
end
-- UPDATE and MOVE functions
-- Move and update the camera system
function CameraSystem:update(dt)
for i,v in ipairs(self.views.list) do
self:followActor(i)
end
end
function CameraSystem:moveView(id, x, y)
self.views.list[id].pos.x = x
self.views.list[id].pos.y = y
self:computeCamPosition(id)
self:limitView(id)
end
function CameraSystem:computeCamPosition(id)
local decalx = self.views.list[id].pos.onScreen.x
local decaly = self.views.list[id].pos.onScreen.y
local realx = self.views.list[id].pos.x
local realy = self.views.list[id].pos.y
self.views.list[id].cam.x = realx - decalx
self.views.list[id].cam.y = realy + decaly
-- FIXME: this workaround certainly will cause some problem but that's the only
-- solution we seem to have right now
-- We invert the y decalage for the camera in order to work around a problem
-- that invert the y between it and the clipping.
end
function CameraSystem:followActor(id)
local view = self:getView(id)
if view.target ~= nil then
local x, y = view.target:getViewCenter()
x = math.floor(x)
y = math.floor(y)
self:moveView(id, x, y)
end
end
-- DRAW FUNCTIONS
-- Basic callback to draw stuff
function CameraSystem:drawDebugViewBox(id)
local viewx, viewy, vieww, viewh = self:getOnScreenViewCoordinate(id)
utils.graphics.box(viewx, viewy, vieww, viewh)
local xx, yy = self:getOnScreenViewCenter(id)
love.graphics.line(xx-3, yy, xx+3, yy)
love.graphics.line(xx, yy-3, xx, yy+3)
local string = id .. " x:" .. viewx .. " y:" .. viewy
love.graphics.print(string, viewx + 4, viewy + 4)
print(viewy)
end
function CameraSystem:drawHUD(id)
local viewx, viewy, vieww, viewh = self:getOnScreenViewCoordinate(id)
local view = self:getView(id)
local string2 = id .. " (" .. viewx .. ":" .. (viewh-viewy) .. ") "
love.graphics.setScissor(viewx, viewh-viewy, vieww, viewh)
love.graphics.translate(viewx, viewh-viewy)
view.target:drawHUD(id, vieww, viewh)
love.graphics.translate(-viewx, -(viewh-viewy))
love.graphics.setScissor( )
end
return CameraSystem

View File

@ -0,0 +1,947 @@
local bump = {
_VERSION = 'bump-3dpd v0.2.0',
_URL = 'https://github.com/oniietzschan/bump-3dpd',
_DESCRIPTION = 'A 3D collision detection library for Lua.',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 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.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsCube(x,y,z,w,h,d)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertType('number', z, 'z')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
assertIsPositiveNumber(d, 'd')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Cube functions
------------------------------------------
local function cube_getNearestCorner(x,y,z,w,h,d, px, py, pz)
return nearest(px, x, x + w),
nearest(py, y, y + h),
nearest(pz, z, z + d)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the cube
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1,x2,y2,z2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx = x2 - x1
local dy = y2 - y1
local dz = z2 - z1
local nx, ny, nz
local nx1, ny1, nz1, nx2, ny2, nz2 = 0,0,0,0,0,0
local p, q, r
for side = 1,6 do
if side == 1 then -- Left
nx,ny,nz,p,q = -1, 0, 0, -dx, x1 - x
elseif side == 2 then -- Right
nx,ny,nz,p,q = 1, 0, 0, dx, x + w - x1
elseif side == 3 then -- Top
nx,ny,nz,p,q = 0, -1, 0, -dy, y1 - y
elseif side == 4 then -- Bottom
nx,ny,nz,p,q = 0, 1, 0, dy, y + h - y1
elseif side == 5 then -- Front
nx,ny,nz,p,q = 0, 0, -1, -dz, z1 - z
else -- Back
nx,ny,nz,p,q = 0, 0, 1, dz, z + d - z1
end
if p == 0 then
if q <= 0 then
return nil
end
else
r = q / p
if p < 0 then
if r > ti2 then
return nil
elseif r > ti1 then
ti1, nx1,ny1,nz1 = r, nx,ny,nz
end
else -- p > 0
if r < ti1 then
return nil
elseif r < ti2 then
ti2, nx2,ny2,nz2 = r,nx,ny,nz
end
end
end
end
return ti1,ti2, nx1,ny1,nz1, nx2,ny2,nz2
end
-- Calculates the minkowsky difference between 2 cubes, which is another cube
local function cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
return x2 - x1 - w1,
y2 - y1 - h1,
z2 - z1 - d1,
w1 + w2,
h1 + h2,
d1 + d2
end
local function cube_containsPoint(x,y,z,w,h,d, px,py,pz)
return px - x > DELTA
and py - y > DELTA
and pz - z > DELTA
and x + w - px > DELTA
and y + h - py > DELTA
and z + d - pz > DELTA
end
local function cube_isIntersecting(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
return x1 < x2 + w2 and x2 < x1 + w1 and
y1 < y2 + h2 and y2 < y1 + h1 and
z1 < z2 + d2 and z2 < z1 + d1
end
local function cube_getCubeDistance(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
local dz = z1 - z2 + (d1 - d2)/2
return (dx * dx) + (dy * dy) + (dz * dz)
end
local function cube_detectCollision(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2, goalX, goalY, goalZ)
goalX = goalX or x1
goalY = goalY or y1
goalZ = goalZ or z1
local dx = goalX - x1
local dy = goalY - y1
local dz = goalZ - z1
local x,y,z,w,h,d = cube_getDiff(x1,y1,z1,w1,h1,d1, x2,y2,z2,w2,h2,d2)
local overlaps, ti, nx, ny, nz
if cube_containsPoint(x,y,z,w,h,d, 0,0,0) then -- item was intersecting other
local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
-- Volume of intersection:
local wi = min(w1, abs(px))
local hi = min(h1, abs(py))
local di = min(d1, abs(pz))
ti = wi * hi * di * -1 -- ti is the negative volume of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1,nz1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, math.huge)
-- item tunnels into other
if ti1
and ti1 < 1
and (abs(ti1 - ti2) >= DELTA) -- special case for cube going through another cube's corner
and (0 < ti1 + DELTA
or 0 == ti1 and ti2 > 0)
then
ti, nx, ny, nz = ti1, nx1, ny1, nz1
overlaps = false
end
end
if not ti then
return
end
local tx, ty, tz
if overlaps then
if dx == 0 and dy == 0 and dz == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py, pz = cube_getNearestCorner(x,y,z,w,h,d, 0,0,0)
if abs(px) <= abs(py) and abs(px) <= abs(pz) then
-- X axis has minimum displacement
py, pz = 0, 0
elseif abs(py) <= abs(pz) then
-- Y axis has minimum displacement
px, pz = 0, 0
else
-- Z axis has minimum displacement
px, py = 0, 0
end
nx, ny, nz = sign(px), sign(py), sign(pz)
tx = x1 + px
ty = y1 + py
tz = z1 + pz
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny,nz = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, 0,0,0,dx,dy,dz, -math.huge, 1)
if not ti1 then
return
end
tx = x1 + dx * ti1
ty = y1 + dy * ti1
tz = z1 + dz * ti1
end
else -- tunnel
tx = x1 + dx * ti
ty = y1 + dy * ti
tz = z1 + dz * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy, z = dz},
normal = {x = nx, y = ny, z = nz},
touch = {x = tx, y = ty, z = tz},
itemCube = {x = x1, y = y1, z = z1, w = w1, h = h1, d = d1},
otherCube = {x = x2, y = y2, z = z2, w = w2, h = h2, d = d2},
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy, cz)
return (cx - 1) * cellSize,
(cy - 1) * cellSize,
(cz - 1) * cellSize
end
local function grid_toCell(cellSize, x, y, z)
return floor(x / cellSize) + 1,
floor(y / cellSize) + 1,
floor(z / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,z1,x2,y2,z2, f)
local cx1, cy1, cz1 = grid_toCell(cellSize, x1, y1, z1)
local cx2, cy2, cz2 = grid_toCell(cellSize, x2, y2, z2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local stepZ, dz, tz = grid_traverse_initStep(cellSize, cz1, z1, z2)
local cx, cy, cz = cx1, cy1, cz1
f(cx, cy, cz)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) + abs(cz - cz2) > 1 do
if tx < ty and tx < tz then -- tx is smallest
tx = tx + dx
cx = cx + stepX
f(cx, cy, cz)
elseif ty < tz then -- ty is smallest
-- Addition: include both cells when going through corners
if tx == ty then
f(cx + stepX, cy, cz)
end
ty = ty + dy
cy = cy + stepY
f(cx, cy, cz)
else -- tz is smallest
-- Addition: include both cells when going through corners
if tx == tz then
f(cx + stepX, cy, cz)
end
if ty == tz then
f(cx, cy + stepY, cz)
end
tz = tz + dz
cz = cz + stepZ
f(cx, cy, cz)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 or cz ~= cz2 then
f(cx2, cy2, cz2)
end
end
local function grid_toCellCube(cellSize, x,y,z,w,h,d)
local cx,cy,cz = grid_toCell(cellSize, x, y, z)
local cx2 = ceil((x + w) / cellSize)
local cy2 = ceil((y + h) / cellSize)
local cz2 = ceil((z + d) / cellSize)
return cx,
cy,
cz,
cx2 - cx + 1,
cy2 - cy + 1,
cz2 - cz + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
return col.touch.x, col.touch.y, col.touch.z, {}, 0
end
local cross = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
return goalX, goalY, goalZ, cols, len
end
local slide = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
goalX = goalX or x
goalY = goalY or y
goalZ = goalZ or z
local tch, move = col.touch, col.move
if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
if col.normal.x ~= 0 then
goalX = tch.x
end
if col.normal.y ~= 0 then
goalY = tch.y
end
if col.normal.z ~= 0 then
goalZ = tch.z
end
end
col.slide = {x = goalX, y = goalY, z = goalZ}
x, y, z = tch.x, tch.y, tch.z
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
return goalX, goalY, goalZ, cols, len
end
local bounce = function(world, col, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
goalX = goalX or x
goalY = goalY or y
goalZ = goalZ or z
local tch, move = col.touch, col.move
local tx, ty, tz = tch.x, tch.y, tch.z
local bx, by, bz = tx, ty, tz
if move.x ~= 0 or move.y ~= 0 or move.z ~= 0 then
local bnx = goalX - tx
local bny = goalY - ty
local bnz = goalZ - tz
if col.normal.x ~= 0 then
bnx = -bnx
end
if col.normal.y ~= 0 then
bny = -bny
end
if col.normal.z ~= 0 then
bnz = -bnz
end
bx = tx + bnx
by = ty + bny
bz = tz + bnz
end
col.bounce = {x = bx, y = by, z = bz}
x, y, z = tch.x, tch.y, tch.z
goalX, goalY, goalZ = bx, by, bz
local cols, len = world:project(col.item, x,y,z,w,h,d, goalX, goalY, goalZ, filter)
return goalX, goalY, goalZ, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b)
return a.weight < b.weight
end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemCube, a.otherCube, b.otherCube
local ad = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, ar.x,ar.y,ar.z,ar.w,ar.h,ar.d)
local bd = cube_getCubeDistance(ir.x,ir.y,ir.z,ir.w,ir.h,ir.d, br.x,br.y,br.z,br.w,br.h,br.d)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy, cz)
self.cells[cz] = self.cells[cz] or {}
self.cells[cz][cy] = self.cells[cz][cy] or setmetatable({}, {__mode = 'v'})
if self.cells[cz][cy][cx] == nil then
self.cells[cz][cy][cx] = {
itemCount = 0,
x = cx,
y = cy,
z = cz,
items = setmetatable({}, {__mode = 'k'})
}
end
local cell = self.cells[cz][cy][cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy, cz)
if not self.cells[cz]
or not self.cells[cz][cy]
or not self.cells[cz][cy][cx]
or not self.cells[cz][cy][cx].items[item]
then
return false
end
local cell = self.cells[cz][cy][cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellCube(self, cx,cy,cz, cw,ch,cd)
local items_dict = {}
for z = cz, cz + cd - 1 do
local plane = self.cells[z]
if plane then
for y = cy, cy + ch - 1 do
local row = plane[y]
if row then
for x = cx, cx + cw - 1 do
local cell = row[x]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,z1,x2,y2,z2, function(cx, cy, cz)
local plane = self.cells[cz]
if not plane then
return
end
local row = plane[cy]
if not row then
return
end
local cell = row[cx]
if not cell or visited[cell] then
return
end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1,z1, x2,y2,z2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,z1,x2,y2,z2)
local cell, cube, x,y,z,w,h,d, ti1, ti2, tii0,tii1
local visited, itemInfo, itemInfoLen = {}, {}, 0
for i = 1, len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
cube = self.cubes[item]
x, y, z, w, h, d = cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
ti1, ti2 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0, tii1 = cube_getSegmentIntersectionIndices(x,y,z,w,h,d, x1,y1,z1, x2,y2,z2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0, tii1)}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
local cols, len = {}, 0
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then
return false
end
return filter(itm, other)
end
local projected_cols, projected_len = self:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, goalZ, projected_cols, projected_len = response(
self,
col,
x, y, z, w, h, d,
goalX, goalY, goalZ,
visitedFilter
)
end
return goalX, goalY, goalZ, cols, len
end
function World:project(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
assertIsCube(x, y, z, w, h, d)
goalX = goalX or x
goalY = goalY or y
goalZ = goalZ or z
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then
visited[item] = true
end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding cube of the whole movement. Conditional to building a queryPolygon method
local tx = min(goalX, x)
local ty = min(goalY, y)
local tz = min(goalZ, z)
local tx2 = max(goalX + w, x + w)
local ty2 = max(goalY + h, y + h)
local tz2 = max(goalZ + d, z + d)
local tw = tx2 - tx
local th = ty2 - ty
local td = tz2 - tz
local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, tx,ty,tz, tw,th,td)
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
for other,_ in pairs(dictItemsInCellCube) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,oz,ow,oh,od = self:getCube(other)
local col = cube_detectCollision(x,y,z,w,h,d, ox,oy,oz,ow,oh,od, goalX, goalY, goalZ)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _, plane in pairs(self.cells) do
for _, row in pairs(plane) do
for _,_ in pairs(row) do
count = count + 1
end
end
end
return count
end
function World:hasItem(item)
return not not self.cubes[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.cubes) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.cubes) do len = len + 1 end
return len
end
function World:getCube(item)
local cube = self.cubes[item]
if not cube then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its cube. Use world:add(item, x,y,z,w,h,d) to add it first.')
end
return cube.x, cube.y, cube.z, cube.w, cube.h, cube.d
end
function World:toWorld(cx, cy, cz)
return grid_toWorld(self.cellSize, cx, cy, cz)
end
function World:toCell(x,y,z)
return grid_toCell(self.cellSize, x, y, z)
end
-- Query methods
function World:queryCube(x,y,z,w,h,d, filter)
assertIsCube(x,y,z,w,h,d)
local cx,cy,cz,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz,cw,ch,cd)
local items, len = {}, 0
local cube
for item,_ in pairs(dictItemsInCellCube) do
cube = self.cubes[item]
if (not filter or filter(item))
and cube_isIntersecting(x,y,z,w,h,d, cube.x, cube.y, cube.z, cube.w, cube.h, cube.d)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y,z, filter)
local cx,cy,cz = self:toCell(x,y,z)
local dictItemsInCellCube = getDictItemsInCellCube(self, cx,cy,cz, 1,1,1)
local items, len = {}, 0
local cube
for item,_ in pairs(dictItemsInCellCube) do
cube = self.cubes[item]
if (not filter or filter(item))
and cube_containsPoint(cube.x, cube.y, cube.z, cube.w, cube.h, cube.d, x, y, z)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, z1, x2, y2, z2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
local items = {}
for i = 1, len do
items[i] = itemInfo[i].item
end
return items, len
end
-- function World:querySegmentWithCoords(x1, y1, z1, x2, y2, z2, filter)
-- local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, z1, x2, y2, z2, filter)
-- local dx, dy, dz = x2 - x1, y2 - y1, z2 - z1
-- local info, ti1, ti2
-- for i=1, len do
-- info = itemInfo[i]
-- ti1 = info.ti1
-- ti2 = info.ti2
-- info.weight = nil
-- info.x1 = x1 + dx * ti1
-- info.y1 = y1 + dy * ti1
-- info.x2 = x1 + dx * ti2
-- info.y2 = y1 + dy * ti2
-- end
-- return itemInfo, len
-- end
--- Main methods
function World:add(item, x,y,z,w,h,d)
local cube = self.cubes[item]
if cube then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsCube(x,y,z,w,h,d)
self.cubes[item] = {x=x,y=y,z=z,w=w,h=h,d=d}
local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
for cz = cs, cs + cd - 1 do
for cy = ct, ct + ch - 1 do
for cx = cl, cl + cw - 1 do
addItemToCell(self, item, cx, cy, cz)
end
end
end
return item
end
function World:remove(item)
local x,y,z,w,h,d = self:getCube(item)
self.cubes[item] = nil
local cl,ct,cs,cw,ch,cd = grid_toCellCube(self.cellSize, x,y,z,w,h,d)
for cz = cs, cs + cd - 1 do
for cy = ct, ct + ch - 1 do
for cx = cl, cl + cw - 1 do
removeItemFromCell(self, item, cx, cy, cz)
end
end
end
end
function World:update(item, x2,y2,z2,w2,h2,d2)
local x1,y1,z1, w1,h1,d1 = self:getCube(item)
w2 = w2 or w1
h2 = h2 or h1
d2 = d2 or d1
assertIsCube(x2,y2,z2,w2,h2,d2)
if x1 == x2 and y1 == y2 and z1 == z2 and w1 == w2 and h1 == h2 and d1 == d2 then
return
end
local cl1,ct1,cs1,cw1,ch1,cd1 = grid_toCellCube(self.cellSize, x1,y1,z1, w1,h1,d1)
local cl2,ct2,cs2,cw2,ch2,cd2 = grid_toCellCube(self.cellSize, x2,y2,z2, w2,h2,d2)
if cl1 ~= cl2 or ct1 ~= ct2 or cs1 ~= cs2 or cw1 ~= cw2 or ch1 ~= ch2 or cd1 ~= cd2 then
local cr1 = cl1 + cw1 - 1
local cr2 = cl2 + cw2 - 1
local cb1 = ct1 + ch1 - 1
local cb2 = ct2 + ch2 - 1
local css1 = cs1 + cd1 - 1
local css2 = cs2 + cd2 - 1
local cyOut, czOut
for cz = cs1, css1 do
czOut = cz < cs2 or cz > css2
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if czOut or cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy, cz)
end
end
end
end
for cz = cs2, css2 do
czOut = cz < cs1 or cz > css1
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if czOut or cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy, cz)
end
end
end
end
end
local cube = self.cubes[item]
cube.x, cube.y, cube.z, cube.w, cube.h, cube.d = x2, y2, z2, w2, h2, d2
end
function World:move(item, goalX, goalY, goalZ, filter)
local actualX, actualY, actualZ, cols, len = self:check(item, goalX, goalY, goalZ, filter)
self:update(item, actualX, actualY, actualZ)
return actualX, actualY, actualZ, cols, len
end
function World:check(item, goalX, goalY, goalZ, filter)
local x,y,z,w,h,d = self:getCube(item)
return self:projectMove(item, x,y,z,w,h,d, goalX,goalY,goalZ, filter)
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
cubes = {},
cells = {},
nonEmptyCells = {},
responses = {},
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.cube = {
getNearestCorner = cube_getNearestCorner,
getSegmentIntersectionIndices = cube_getSegmentIntersectionIndices,
getDiff = cube_getDiff,
containsPoint = cube_containsPoint,
isIntersecting = cube_isIntersecting,
getCubeDistance = cube_getCubeDistance,
detectCollision = cube_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

View File

@ -0,0 +1,769 @@
local bump = {
_VERSION = 'bump v3.1.7',
_URL = 'https://github.com/kikito/bump.lua',
_DESCRIPTION = 'A collision detection library for Lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 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.
]]
}
------------------------------------------
-- Auxiliary functions
------------------------------------------
local DELTA = 1e-10 -- floating-point margin of error
local abs, floor, ceil, min, max = math.abs, math.floor, math.ceil, math.min, math.max
local function sign(x)
if x > 0 then return 1 end
if x == 0 then return 0 end
return -1
end
local function nearest(x, a, b)
if abs(a - x) < abs(b - x) then return a else return b end
end
local function assertType(desiredType, value, name)
if type(value) ~= desiredType then
error(name .. ' must be a ' .. desiredType .. ', but was ' .. tostring(value) .. '(a ' .. type(value) .. ')')
end
end
local function assertIsPositiveNumber(value, name)
if type(value) ~= 'number' or value <= 0 then
error(name .. ' must be a positive integer, but was ' .. tostring(value) .. '(' .. type(value) .. ')')
end
end
local function assertIsRect(x,y,w,h)
assertType('number', x, 'x')
assertType('number', y, 'y')
assertIsPositiveNumber(w, 'w')
assertIsPositiveNumber(h, 'h')
end
local defaultFilter = function()
return 'slide'
end
------------------------------------------
-- Rectangle functions
------------------------------------------
local function rect_getNearestCorner(x,y,w,h, px, py)
return nearest(px, x, x+w), nearest(py, y, y+h)
end
-- This is a generalized implementation of the liang-barsky algorithm, which also returns
-- the normals of the sides where the segment intersects.
-- Returns nil if the segment never touches the rect
-- Notice that normals are only guaranteed to be accurate when initially ti1, ti2 == -math.huge, math.huge
local function rect_getSegmentIntersectionIndices(x,y,w,h, x1,y1,x2,y2, ti1,ti2)
ti1, ti2 = ti1 or 0, ti2 or 1
local dx, dy = x2-x1, y2-y1
local nx, ny
local nx1, ny1, nx2, ny2 = 0,0,0,0
local p, q, r
for side = 1,4 do
if side == 1 then nx,ny,p,q = -1, 0, -dx, x1 - x -- left
elseif side == 2 then nx,ny,p,q = 1, 0, dx, x + w - x1 -- right
elseif side == 3 then nx,ny,p,q = 0, -1, -dy, y1 - y -- top
else nx,ny,p,q = 0, 1, dy, y + h - y1 -- bottom
end
if p == 0 then
if q <= 0 then return nil end
else
r = q / p
if p < 0 then
if r > ti2 then return nil
elseif r > ti1 then ti1,nx1,ny1 = r,nx,ny
end
else -- p > 0
if r < ti1 then return nil
elseif r < ti2 then ti2,nx2,ny2 = r,nx,ny
end
end
end
end
return ti1,ti2, nx1,ny1, nx2,ny2
end
-- Calculates the minkowsky difference between 2 rects, which is another rect
local function rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
return x2 - x1 - w1,
y2 - y1 - h1,
w1 + w2,
h1 + h2
end
local function rect_containsPoint(x,y,w,h, px,py)
return px - x > DELTA and py - y > DELTA and
x + w - px > DELTA and y + h - py > DELTA
end
local function rect_isIntersecting(x1,y1,w1,h1, x2,y2,w2,h2)
return x1 < x2+w2 and x2 < x1+w1 and
y1 < y2+h2 and y2 < y1+h1
end
local function rect_getSquareDistance(x1,y1,w1,h1, x2,y2,w2,h2)
local dx = x1 - x2 + (w1 - w2)/2
local dy = y1 - y2 + (h1 - h2)/2
return dx*dx + dy*dy
end
local function rect_detectCollision(x1,y1,w1,h1, x2,y2,w2,h2, goalX, goalY)
goalX = goalX or x1
goalY = goalY or y1
local dx, dy = goalX - x1, goalY - y1
local x,y,w,h = rect_getDiff(x1,y1,w1,h1, x2,y2,w2,h2)
local overlaps, ti, nx, ny
if rect_containsPoint(x,y,w,h, 0,0) then -- item was intersecting other
local px, py = rect_getNearestCorner(x,y,w,h, 0, 0)
local wi, hi = min(w1, abs(px)), min(h1, abs(py)) -- area of intersection
ti = -wi * hi -- ti is the negative area of intersection
overlaps = true
else
local ti1,ti2,nx1,ny1 = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, math.huge)
-- item tunnels into other
if ti1
and ti1 < 1
and (abs(ti1 - ti2) >= DELTA) -- special case for rect going through another rect's corner
and (0 < ti1 + DELTA
or 0 == ti1 and ti2 > 0)
then
ti, nx, ny = ti1, nx1, ny1
overlaps = false
end
end
if not ti then return end
local tx, ty
if overlaps then
if dx == 0 and dy == 0 then
-- intersecting and not moving - use minimum displacement vector
local px, py = rect_getNearestCorner(x,y,w,h, 0,0)
if abs(px) < abs(py) then py = 0 else px = 0 end
nx, ny = sign(px), sign(py)
tx, ty = x1 + px, y1 + py
else
-- intersecting and moving - move in the opposite direction
local ti1, _
ti1,_,nx,ny = rect_getSegmentIntersectionIndices(x,y,w,h, 0,0,dx,dy, -math.huge, 1)
if not ti1 then return end
tx, ty = x1 + dx * ti1, y1 + dy * ti1
end
else -- tunnel
tx, ty = x1 + dx * ti, y1 + dy * ti
end
return {
overlaps = overlaps,
ti = ti,
move = {x = dx, y = dy},
normal = {x = nx, y = ny},
touch = {x = tx, y = ty},
itemRect = {x = x1, y = y1, w = w1, h = h1},
otherRect = {x = x2, y = y2, w = w2, h = h2}
}
end
------------------------------------------
-- Grid functions
------------------------------------------
local function grid_toWorld(cellSize, cx, cy)
return (cx - 1)*cellSize, (cy-1)*cellSize
end
local function grid_toCell(cellSize, x, y)
return floor(x / cellSize) + 1, floor(y / cellSize) + 1
end
-- grid_traverse* functions are based on "A Fast Voxel Traversal Algorithm for Ray Tracing",
-- by John Amanides and Andrew Woo - http://www.cse.yorku.ca/~amana/research/grid.pdf
-- It has been modified to include both cells when the ray "touches a grid corner",
-- and with a different exit condition
local function grid_traverse_initStep(cellSize, ct, t1, t2)
local v = t2 - t1
if v > 0 then
return 1, cellSize / v, ((ct + v) * cellSize - t1) / v
elseif v < 0 then
return -1, -cellSize / v, ((ct + v - 1) * cellSize - t1) / v
else
return 0, math.huge, math.huge
end
end
local function grid_traverse(cellSize, x1,y1,x2,y2, f)
local cx1,cy1 = grid_toCell(cellSize, x1,y1)
local cx2,cy2 = grid_toCell(cellSize, x2,y2)
local stepX, dx, tx = grid_traverse_initStep(cellSize, cx1, x1, x2)
local stepY, dy, ty = grid_traverse_initStep(cellSize, cy1, y1, y2)
local cx,cy = cx1,cy1
f(cx, cy)
-- The default implementation had an infinite loop problem when
-- approaching the last cell in some occassions. We finish iterating
-- when we are *next* to the last cell
while abs(cx - cx2) + abs(cy - cy2) > 1 do
if tx < ty then
tx, cx = tx + dx, cx + stepX
f(cx, cy)
else
-- Addition: include both cells when going through corners
if tx == ty then f(cx + stepX, cy) end
ty, cy = ty + dy, cy + stepY
f(cx, cy)
end
end
-- If we have not arrived to the last cell, use it
if cx ~= cx2 or cy ~= cy2 then f(cx2, cy2) end
end
local function grid_toCellRect(cellSize, x,y,w,h)
local cx,cy = grid_toCell(cellSize, x, y)
local cr,cb = ceil((x+w) / cellSize), ceil((y+h) / cellSize)
return cx, cy, cr - cx + 1, cb - cy + 1
end
------------------------------------------
-- Responses
------------------------------------------
local touch = function(world, col, x,y,w,h, goalX, goalY, filter)
return col.touch.x, col.touch.y, {}, 0
end
local cross = function(world, col, x,y,w,h, goalX, goalY, filter)
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local slide = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
if move.x ~= 0 or move.y ~= 0 then
if col.normal.x ~= 0 then
goalX = tch.x
else
goalY = tch.y
end
end
col.slide = {x = goalX, y = goalY}
x,y = tch.x, tch.y
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
local bounce = function(world, col, x,y,w,h, goalX, goalY, filter)
goalX = goalX or x
goalY = goalY or y
local tch, move = col.touch, col.move
local tx, ty = tch.x, tch.y
local bx, by = tx, ty
if move.x ~= 0 or move.y ~= 0 then
local bnx, bny = goalX - tx, goalY - ty
if col.normal.x == 0 then bny = -bny else bnx = -bnx end
bx, by = tx + bnx, ty + bny
end
col.bounce = {x = bx, y = by}
x,y = tch.x, tch.y
goalX, goalY = bx, by
local cols, len = world:project(col.item, x,y,w,h, goalX, goalY, filter)
return goalX, goalY, cols, len
end
------------------------------------------
-- World
------------------------------------------
local World = {}
local World_mt = {__index = World}
-- Private functions and methods
local function sortByWeight(a,b) return a.weight < b.weight end
local function sortByTiAndDistance(a,b)
if a.ti == b.ti then
local ir, ar, br = a.itemRect, a.otherRect, b.otherRect
local ad = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, ar.x,ar.y,ar.w,ar.h)
local bd = rect_getSquareDistance(ir.x,ir.y,ir.w,ir.h, br.x,br.y,br.w,br.h)
return ad < bd
end
return a.ti < b.ti
end
local function addItemToCell(self, item, cx, cy)
self.rows[cy] = self.rows[cy] or setmetatable({}, {__mode = 'v'})
local row = self.rows[cy]
row[cx] = row[cx] or {itemCount = 0, x = cx, y = cy, items = setmetatable({}, {__mode = 'k'})}
local cell = row[cx]
self.nonEmptyCells[cell] = true
if not cell.items[item] then
cell.items[item] = true
cell.itemCount = cell.itemCount + 1
end
end
local function removeItemFromCell(self, item, cx, cy)
local row = self.rows[cy]
if not row or not row[cx] or not row[cx].items[item] then return false end
local cell = row[cx]
cell.items[item] = nil
cell.itemCount = cell.itemCount - 1
if cell.itemCount == 0 then
self.nonEmptyCells[cell] = nil
end
return true
end
local function getDictItemsInCellRect(self, cl,ct,cw,ch)
local items_dict = {}
for cy=ct,ct+ch-1 do
local row = self.rows[cy]
if row then
for cx=cl,cl+cw-1 do
local cell = row[cx]
if cell and cell.itemCount > 0 then -- no cell.itemCount > 1 because tunneling
for item,_ in pairs(cell.items) do
items_dict[item] = true
end
end
end
end
end
return items_dict
end
local function getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cells, cellsLen, visited = {}, 0, {}
grid_traverse(self.cellSize, x1,y1,x2,y2, function(cx, cy)
local row = self.rows[cy]
if not row then return end
local cell = row[cx]
if not cell or visited[cell] then return end
visited[cell] = true
cellsLen = cellsLen + 1
cells[cellsLen] = cell
end)
return cells, cellsLen
end
local function getInfoAboutItemsTouchedBySegment(self, x1,y1, x2,y2, filter)
local cells, len = getCellsTouchedBySegment(self, x1,y1,x2,y2)
local cell, rect, l,t,w,h, ti1,ti2, tii0,tii1
local visited, itemInfo, itemInfoLen = {},{},0
for i=1,len do
cell = cells[i]
for item in pairs(cell.items) do
if not visited[item] then
visited[item] = true
if (not filter or filter(item)) then
rect = self.rects[item]
l,t,w,h = rect.x,rect.y,rect.w,rect.h
ti1,ti2 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, 0, 1)
if ti1 and ((0 < ti1 and ti1 < 1) or (0 < ti2 and ti2 < 1)) then
-- the sorting is according to the t of an infinite line, not the segment
tii0,tii1 = rect_getSegmentIntersectionIndices(l,t,w,h, x1,y1, x2,y2, -math.huge, math.huge)
itemInfoLen = itemInfoLen + 1
itemInfo[itemInfoLen] = {item = item, ti1 = ti1, ti2 = ti2, weight = min(tii0,tii1)}
end
end
end
end
end
table.sort(itemInfo, sortByWeight)
return itemInfo, itemInfoLen
end
local function getResponseByName(self, name)
local response = self.responses[name]
if not response then
error(('Unknown collision type: %s (%s)'):format(name, type(name)))
end
return response
end
-- Misc Public Methods
function World:addResponse(name, response)
self.responses[name] = response
end
function World:project(item, x,y,w,h, goalX, goalY, filter)
assertIsRect(x,y,w,h)
goalX = goalX or x
goalY = goalY or y
filter = filter or defaultFilter
local collisions, len = {}, 0
local visited = {}
if item ~= nil then visited[item] = true end
-- This could probably be done with less cells using a polygon raster over the cells instead of a
-- bounding rect of the whole movement. Conditional to building a queryPolygon method
local tl, tt = min(goalX, x), min(goalY, y)
local tr, tb = max(goalX + w, x+w), max(goalY + h, y+h)
local tw, th = tr-tl, tb-tt
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, tl,tt,tw,th)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
for other,_ in pairs(dictItemsInCellRect) do
if not visited[other] then
visited[other] = true
local responseName = filter(item, other)
if responseName then
local ox,oy,ow,oh = self:getRect(other)
local col = rect_detectCollision(x,y,w,h, ox,oy,ow,oh, goalX, goalY)
if col then
col.other = other
col.item = item
col.type = responseName
len = len + 1
collisions[len] = col
end
end
end
end
table.sort(collisions, sortByTiAndDistance)
return collisions, len
end
function World:countCells()
local count = 0
for _,row in pairs(self.rows) do
for _,_ in pairs(row) do
count = count + 1
end
end
return count
end
function World:hasItem(item)
return not not self.rects[item]
end
function World:getItems()
local items, len = {}, 0
for item,_ in pairs(self.rects) do
len = len + 1
items[len] = item
end
return items, len
end
function World:countItems()
local len = 0
for _ in pairs(self.rects) do len = len + 1 end
return len
end
function World:getRect(item)
local rect = self.rects[item]
if not rect then
error('Item ' .. tostring(item) .. ' must be added to the world before getting its rect. Use world:add(item, x,y,w,h) to add it first.')
end
return rect.x, rect.y, rect.w, rect.h
end
function World:toWorld(cx, cy)
return grid_toWorld(self.cellSize, cx, cy)
end
function World:toCell(x,y)
return grid_toCell(self.cellSize, x, y)
end
--- Query methods
function World:queryRect(x,y,w,h, filter)
assertIsRect(x,y,w,h)
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
local dictItemsInCellRect = getDictItemsInCellRect(self, cl,ct,cw,ch)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_isIntersecting(x,y,w,h, rect.x, rect.y, rect.w, rect.h)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:queryPoint(x,y, filter)
local cx,cy = self:toCell(x,y)
local dictItemsInCellRect = getDictItemsInCellRect(self, cx,cy,1,1)
local items, len = {}, 0
local rect
for item,_ in pairs(dictItemsInCellRect) do
rect = self.rects[item]
if (not filter or filter(item))
and rect_containsPoint(rect.x, rect.y, rect.w, rect.h, x, y)
then
len = len + 1
items[len] = item
end
end
return items, len
end
function World:querySegment(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local items = {}
for i=1, len do
items[i] = itemInfo[i].item
end
return items, len
end
function World:querySegmentWithCoords(x1, y1, x2, y2, filter)
local itemInfo, len = getInfoAboutItemsTouchedBySegment(self, x1, y1, x2, y2, filter)
local dx, dy = x2-x1, y2-y1
local info, ti1, ti2
for i=1, len do
info = itemInfo[i]
ti1 = info.ti1
ti2 = info.ti2
info.weight = nil
info.x1 = x1 + dx * ti1
info.y1 = y1 + dy * ti1
info.x2 = x1 + dx * ti2
info.y2 = y1 + dy * ti2
end
return itemInfo, len
end
--- Main methods
function World:add(item, x,y,w,h)
local rect = self.rects[item]
if rect then
error('Item ' .. tostring(item) .. ' added to the world twice.')
end
assertIsRect(x,y,w,h)
self.rects[item] = {x=x,y=y,w=w,h=h}
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
addItemToCell(self, item, cx, cy)
end
end
return item
end
function World:remove(item)
local x,y,w,h = self:getRect(item)
self.rects[item] = nil
local cl,ct,cw,ch = grid_toCellRect(self.cellSize, x,y,w,h)
for cy = ct, ct+ch-1 do
for cx = cl, cl+cw-1 do
removeItemFromCell(self, item, cx, cy)
end
end
end
function World:update(item, x2,y2,w2,h2)
local x1,y1,w1,h1 = self:getRect(item)
w2,h2 = w2 or w1, h2 or h1
assertIsRect(x2,y2,w2,h2)
if x1 ~= x2 or y1 ~= y2 or w1 ~= w2 or h1 ~= h2 then
local cellSize = self.cellSize
local cl1,ct1,cw1,ch1 = grid_toCellRect(cellSize, x1,y1,w1,h1)
local cl2,ct2,cw2,ch2 = grid_toCellRect(cellSize, x2,y2,w2,h2)
if cl1 ~= cl2 or ct1 ~= ct2 or cw1 ~= cw2 or ch1 ~= ch2 then
local cr1, cb1 = cl1+cw1-1, ct1+ch1-1
local cr2, cb2 = cl2+cw2-1, ct2+ch2-1
local cyOut
for cy = ct1, cb1 do
cyOut = cy < ct2 or cy > cb2
for cx = cl1, cr1 do
if cyOut or cx < cl2 or cx > cr2 then
removeItemFromCell(self, item, cx, cy)
end
end
end
for cy = ct2, cb2 do
cyOut = cy < ct1 or cy > cb1
for cx = cl2, cr2 do
if cyOut or cx < cl1 or cx > cr1 then
addItemToCell(self, item, cx, cy)
end
end
end
end
local rect = self.rects[item]
rect.x, rect.y, rect.w, rect.h = x2,y2,w2,h2
end
end
function World:move(item, goalX, goalY, filter)
local actualX, actualY, cols, len = self:check(item, goalX, goalY, filter)
self:update(item, actualX, actualY)
return actualX, actualY, cols, len
end
function World:check(item, goalX, goalY, filter)
filter = filter or defaultFilter
local visited = {[item] = true}
local visitedFilter = function(itm, other)
if visited[other] then return false end
return filter(itm, other)
end
local cols, len = {}, 0
local x,y,w,h = self:getRect(item)
local projected_cols, projected_len = self:project(item, x,y,w,h, goalX,goalY, visitedFilter)
while projected_len > 0 do
local col = projected_cols[1]
len = len + 1
cols[len] = col
visited[col.other] = true
local response = getResponseByName(self, col.type)
goalX, goalY, projected_cols, projected_len = response(
self,
col,
x, y, w, h,
goalX, goalY,
visitedFilter
)
end
return goalX, goalY, cols, len
end
-- Public library functions
bump.newWorld = function(cellSize)
cellSize = cellSize or 64
assertIsPositiveNumber(cellSize, 'cellSize')
local world = setmetatable({
cellSize = cellSize,
rects = {},
rows = {},
nonEmptyCells = {},
responses = {}
}, World_mt)
world:addResponse('touch', touch)
world:addResponse('cross', cross)
world:addResponse('slide', slide)
world:addResponse('bounce', bounce)
return world
end
bump.rect = {
getNearestCorner = rect_getNearestCorner,
getSegmentIntersectionIndices = rect_getSegmentIntersectionIndices,
getDiff = rect_getDiff,
containsPoint = rect_containsPoint,
isIntersecting = rect_isIntersecting,
getSquareDistance = rect_getSquareDistance,
detectCollision = rect_detectCollision
}
bump.responses = {
touch = touch,
cross = cross,
slide = slide,
bounce = bounce
}
return bump

View File

@ -0,0 +1,216 @@
--[[
Copyright (c) 2010-2015 Matthias Richter
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.
Except as contained in this notice, the name(s) of the above copyright holders
shall not be used in advertising or otherwise to promote the sale, use or
other dealings in this Software without prior written authorization.
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.
]]--
local _PATH = (...):match('^(.*[%./])[^%.%/]+$') or ''
local cos, sin = math.cos, math.sin
local camera = {}
camera.__index = camera
-- Movement interpolators (for camera locking/windowing)
camera.smooth = {}
function camera.smooth.none()
return function(dx,dy) return dx,dy end
end
function camera.smooth.linear(speed)
assert(type(speed) == "number", "Invalid parameter: speed = "..tostring(speed))
return function(dx,dy, s)
-- normalize direction
local d = math.sqrt(dx*dx+dy*dy)
local dts = math.min((s or speed) * love.timer.getDelta(), d) -- prevent overshooting the goal
if d > 0 then
dx,dy = dx/d, dy/d
end
return dx*dts, dy*dts
end
end
function camera.smooth.damped(stiffness)
assert(type(stiffness) == "number", "Invalid parameter: stiffness = "..tostring(stiffness))
return function(dx,dy, s)
local dts = love.timer.getDelta() * (s or stiffness)
return dx*dts, dy*dts
end
end
local function new(x,y, zoom, rot, smoother)
x,y = x or love.graphics.getWidth()/2, y or love.graphics.getHeight()/2
zoom = zoom or 1
rot = rot or 0
smoother = smoother or camera.smooth.none() -- for locking, see below
return setmetatable({x = x, y = y, scale = zoom, rot = rot, smoother = smoother}, camera)
end
function camera:lookAt(x,y)
self.x, self.y = x, y
return self
end
function camera:move(dx,dy)
self.x, self.y = self.x + dx, self.y + dy
return self
end
function camera:position()
return self.x, self.y
end
function camera:rotate(phi)
self.rot = self.rot + phi
return self
end
function camera:rotateTo(phi)
self.rot = phi
return self
end
function camera:zoom(mul)
self.scale = self.scale * mul
return self
end
function camera:zoomTo(zoom)
self.scale = zoom
return self
end
function camera:attach(x,y,w,h, noclip)
x,y = x or 0, y or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
self._sx,self._sy,self._sw,self._sh = love.graphics.getScissor()
if not noclip then
love.graphics.setScissor(x,y,w,h)
end
local cx,cy = x+w/2, y+h/2
love.graphics.push()
love.graphics.translate(cx, cy)
love.graphics.scale(self.scale)
love.graphics.rotate(self.rot)
love.graphics.translate(-self.x, -self.y)
end
function camera:detach()
love.graphics.pop()
love.graphics.setScissor(self._sx,self._sy,self._sw,self._sh)
end
function camera:draw(...)
local x,y,w,h,noclip,func
local nargs = select("#", ...)
if nargs == 1 then
func = ...
elseif nargs == 5 then
x,y,w,h,func = ...
elseif nargs == 6 then
x,y,w,h,noclip,func = ...
else
error("Invalid arguments to camera:draw()")
end
self:attach(x,y,w,h,noclip)
func()
self:detach()
end
-- world coordinates to camera coordinates
function camera:cameraCoords(x,y, ox,oy,w,h)
ox, oy = ox or 0, oy or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
-- x,y = ((x,y) - (self.x, self.y)):rotated(self.rot) * self.scale + center
local c,s = cos(self.rot), sin(self.rot)
x,y = x - self.x, y - self.y
x,y = c*x - s*y, s*x + c*y
return x*self.scale + w/2 + ox, y*self.scale + h/2 + oy
end
-- camera coordinates to world coordinates
function camera:worldCoords(x,y, ox,oy,w,h)
ox, oy = ox or 0, oy or 0
w,h = w or love.graphics.getWidth(), h or love.graphics.getHeight()
-- x,y = (((x,y) - center) / self.scale):rotated(-self.rot) + (self.x,self.y)
local c,s = cos(-self.rot), sin(-self.rot)
x,y = (x - w/2 - ox) / self.scale, (y - h/2 - oy) / self.scale
x,y = c*x - s*y, s*x + c*y
return x+self.x, y+self.y
end
function camera:mousePosition(ox,oy,w,h)
local mx,my = love.mouse.getPosition()
return self:worldCoords(mx,my, ox,oy,w,h)
end
-- camera scrolling utilities
function camera:lockX(x, smoother, ...)
local dx, dy = (smoother or self.smoother)(x - self.x, self.y, ...)
self.x = self.x + dx
return self
end
function camera:lockY(y, smoother, ...)
local dx, dy = (smoother or self.smoother)(self.x, y - self.y, ...)
self.y = self.y + dy
return self
end
function camera:lockPosition(x,y, smoother, ...)
return self:move((smoother or self.smoother)(x - self.x, y - self.y, ...))
end
function camera:lockWindow(x, y, x_min, x_max, y_min, y_max, smoother, ...)
-- figure out displacement in camera coordinates
x,y = self:cameraCoords(x,y)
local dx, dy = 0,0
if x < x_min then
dx = x - x_min
elseif x > x_max then
dx = x - x_max
end
if y < y_min then
dy = y - y_min
elseif y > y_max then
dy = y - y_max
end
-- transform displacement to movement in world coordinates
local c,s = cos(-self.rot), sin(-self.rot)
dx,dy = (c*dx - s*dy) / self.scale, (s*dx + c*dy) / self.scale
-- move
self:move((smoother or self.smoother)(dx,dy,...))
end
-- the module
return setmetatable({new = new, smooth = camera.smooth},
{__call = function(_, ...) return new(...) end})

View File

@ -0,0 +1,128 @@
local lg = 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.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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,303 @@
--- Box2D plugin for STI
-- @module box2d
-- @author Landon Manning
-- @copyright 2017
-- @license MIT/X11
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.6",
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
if userdata.properties.dynamic == true then
currentBody = love.physics.newBody(world, map.offsetx, map.offsety, 'dynamic')
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
}
if o.shape == "rectangle" then
o.r = object.rotation or 0
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 = tileset.tileheight
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
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.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

View File

@ -0,0 +1,194 @@
--- Bump.lua plugin for STI
-- @module bump.lua
-- @author David Serrano (BobbyJones|FrenchFryLord)
-- @copyright 2016
-- @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.6.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,
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,
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_collidables = collidables
end,
--- Remove layer
-- @param index to layer to be removed
-- @param world bump world the holds the tiles
-- @param tx Translate on X
-- @param ty Translate on Y
-- @param sx Scale on X
-- @param sy Scale on Y
bump_removeLayer = function(map, index, world)
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
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, world, 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))
for _, collidable in pairs(map.bump_collidables) do
lg.rectangle("line", world:getRect(collidable))
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

View File

@ -0,0 +1,206 @@
-- 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,'/')
until k == 0
repeat -- A/../ -> (empty)
path,k = path:gsub(np_pat1,'')
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)
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
end
--- Project isometric position to cartesian position
function utils.convert_isometric_to_screen(map, x, y)
local mapH = map.height
local tileW = map.tilewidth
local tileH = map.tileheight
local tileX = x / tileH
local tileY = y / tileH
local offsetX = mapH * 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
return utils

View File

@ -0,0 +1,84 @@
-- See: https://github.com/bungle/lua-resty-tsort
--
-- Copyright (c) 2016, Aapo Talvensaari
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without modification,
-- are permitted provided that the following conditions are met:
--
-- * Redistributions of source code must retain the above copyright notice, this
-- list of conditions and the following disclaimer.
--
-- * Redistributions in binary form must reproduce the above copyright notice, this
-- list of conditions and the following disclaimer in the documentation and/or
-- other materials provided with the distribution.
--
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-- ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
local setmetatable = setmetatable
local pairs = pairs
local type = type
local function visit(k, n, m, s)
if m[k] == 0 then return 1 end
if m[k] == 1 then return end
m[k] = 0
local f = n[k]
for i=1, #f do
if visit(f[i], n, m, s) then return 1 end
end
m[k] = 1
s[#s+1] = k
end
local tsort = {}
tsort.__index = tsort
function tsort.new()
return setmetatable({ n = {} }, tsort)
end
function tsort:add(...)
local p = { ... }
local c = #p
if c == 0 then return self end
if c == 1 then
p = p[1]
if type(p) == "table" then
c = #p
else
p = { p }
end
end
local n = self.n
for i=1, c do
local f = p[i]
if n[f] == nil then n[f] = {} end
end
for i=2, c, 1 do
local f = p[i]
local t = p[i-1]
local o = n[f]
o[#o+1] = t
end
return self
end
function tsort:sort()
local n = self.n
local s = {}
local m = {}
for k in pairs(n) do
if m[k] == nil then
if visit(k, n, m, s) then
return nil, "There is a circular dependency in the graph. It is not possible to derive a topological sort."
end
end
end
return s
end
return tsort

View File

@ -0,0 +1,75 @@
-- world2D.lua :: a basic 2D world based on bump2D.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.world2D$', '') .. "."
local BaseWorld = require(cwd .. "baseworld")
local World2D = BaseWorld:extend()
local Sti = require(cwd .. "libs.sti")
local Bump = require(cwd .. "libs.bump")
local CameraSystem = require(cwd .. "camera")
function World2D:new(scene, actorlist, mapfile)
World2D.super.new(self, scene, actorlist, mapfile)
end
-- ACTORS FUNCTIONS
-- Wrappers around Bump2D functions
function World2D:initActors()
self.currentCreationID = 0
self.actors = Bump.newWorld(50)
end
function World2D:registerActor(actor)
actor.creationID = self.currentCreationID
self.currentCreationID = self.currentCreationID + 1
return self.actors:add(actor, actor.x, actor.y, actor.w, actor.h)
end
function World2D:removeActor(actor)
return self.actors:remove(actor)
end
function World2D:moveActor(actor, x, y, filter)
return self.actors:move(actor, x, y, filter)
end
function World2D:checkCollision(actor, x, y, filter)
return self.actors:check(actor, x, y, filter)
end
function World2D:queryRect(x, y, w, h)
return self.actors:queryRect(x, y, w, h)
end
function World2D:countActors()
return self.actors:countItems()
end
function World2D:getActors()
return self.actors:getItems()
end
return World2D

View File

@ -24,12 +24,19 @@
local OptionsManager = Object:extend()
local binser = require "libs.binser"
local cwd = (...):gsub('%.options$', '') .. "."
local binser = require(cwd .. "libs.binser")
function OptionsManager:new()
local TRANSLATION_PATH = "datas/languages/"
-- INIT FUNCTIONS
-- Initialize and configure the game options
function OptionsManager:new(controller)
-- We begin by creating an empty data table before reading the data.
self.data = {}
self:read()
self.controller = controller
end
function OptionsManager:reset()
@ -42,16 +49,19 @@ function OptionsManager:reset()
self.data.video.fullscreen = false
-- We load the default files
self.data.input = require "datas.inputs"
self.data.input = self:getInputDefaultData()
-- TODO: have a way to auto-load a language according to the OS ?
self.data.language = "en"
self.data.language = self:getTranslationDefaultData()
self.data.audio = {}
self.data.audio.music = 100
self.data.audio.sfx = 100
end
-- INFO FUNCTIONS
-- Get informations from the option managers
function OptionsManager:getFile(absolute)
local dir = ""
if absolute then
@ -66,6 +76,71 @@ function OptionsManager:getFile(absolute)
return filepath
end
function OptionsManager:getInputDefaultData()
local _path = "datas/inputs.lua"
local datas = {}
local fileinfo = love.filesystem.getInfo(_path)
if fileinfo ~= nil then
datas = require "datas.inputs"
else
datas = {}
end
return datas
end
function OptionsManager:getPlayerInputData(id)
local _playerInputData = self.data.input[id]
if _playerInputData == nil then
_playerInputData = {}
_playerInputData.keys = {}
end
return _playerInputData
end
function OptionsManager:getInputData()
return self.data.input
end
function OptionsManager:setInputKey(sourceid, padkey, key)
if self.data.input[sourceid] ~= nil then
if self.data.input[sourceid].keys[padkey] ~= nil then
self.data.input[sourceid].keys[padkey] = key
end
end
end
-- Lang data
function OptionsManager:getTranslationDefaultData()
local _path = TRANSLATION_PATH .. "init.lua"
local fileinfo = love.filesystem.getInfo(_path)
local datas = nil
if fileinfo ~= nil then
lang = require(TRANSLATION_PATH)
lang.current = lang.default or "en"
lang.path = TRANSLATION_PATH
end
return lang
end
function OptionsManager:setLanguage(lang)
if (self.controller.lang:isLangAvailable(lang)) then
self.data.language.current = lang
self.controller.lang:getTranslationData()
end
end
-- DATA HANDLING FUNCTIONS
-- Save and get data from the savefile
-- FIXME: maybe subclass a special module for that ?
function OptionsManager:write()
local data = self:getData()

View File

@ -25,43 +25,96 @@
local SceneManager = Object:extend()
-- INIT FUNCTIONS
-- Initialize and configure the scene manager
function SceneManager:new(controller)
self.controller = controller
self.currentScene = nil
self.storage = {}
end
function SceneManager:update(dt)
if (self.currentScene ~= nil) then
local keys = self.controller.input.keys
self.currentScene.keys = keys
self.currentScene.menusystem.keys = keys
self.currentScene.assets:update(dt)
self.currentScene.menusystem:update(dt)
self.currentScene:update(dt)
function SceneManager:setScene(scene)
self.currentScene = scene
end
function SceneManager:storeCurrentScene(name)
self.storage[name] = self.currentScene
end
function SceneManager:setStoredScene(name)
local storedScene = self.storage[name]
if storedScene ~= nil then
self.currentScene = storedScene
self.storage[name] = nil
end
end
function SceneManager:mousemoved(x, y, dx, dy)
self.currentScene.mouse.x,
self.currentScene.mouse.y = x, y
self.currentScene:mousemoved(x, y, dx, dy)
self.currentScene.menusystem:mousemoved(x, y, dx, dy)
end
function SceneManager:mousepressed( x, y, button, istouch )
self.currentScene:mousepressed( x, y, button, istouch )
self.currentScene.menusystem:mousepressed( x, y, button, istouch )
function SceneManager:clearStorage()
self.storage = {}
end
function SceneManager:clearScene()
self.currentScene = nil
end
-- UPDATE FUNCTIONS
-- Update the current scene and its subobjects
function SceneManager:update(dt)
if (self.currentScene ~= nil) then
self.currentScene:updateStart(dt)
self.currentScene:setKeys()
self.currentScene.assets:update(dt)
self.currentScene.menusystem:update(dt)
self.currentScene:updateWorld(dt)
self.currentScene:update(dt)
self.currentScene:updateEnd(dt)
end
end
-- MOUSE FUNCTIONS
-- Send pointer data to the scene
function SceneManager:mousemoved(x, y, dx, dy)
if (self.currentScene ~= nil) then
self.currentScene.mouse.x,
self.currentScene.mouse.y = x, y
self.currentScene:mousemoved(x, y, dx, dy)
self.currentScene.menusystem:mousemoved(x, y, dx, dy)
end
end
function SceneManager:mousepressed( x, y, button, istouch )
if (self.currentScene ~= nil) then
self.currentScene:mousepressed( x, y, button, istouch )
self.currentScene.menusystem:mousepressed( x, y, button, istouch )
end
end
-- KEYBOARD FUNCTIONS
-- Add send keys functions to the scene
function SceneManager:keypressed( key, scancode, isrepeat )
self.currentScene:keypressed( key, scancode, isrepeat )
end
function SceneManager:keyreleased( key )
self.currentScene:keyreleased( key )
end
-- DRAW FUNCTIONS
-- Draw the current scene
function SceneManager:draw()
self.controller.screen:apply()
if (self.currentScene ~= nil) then
self.currentScene:draw(dt)
self.currentScene:drawStart()
self.currentScene:drawWorld()
self.currentScene:draw()
self.currentScene.menusystem:draw()
self.currentScene:drawEnd()
end
self.controller.screen:cease()
end

View File

@ -24,7 +24,11 @@
local ScreenManager = Object:extend()
local CScreen = require "libs.cscreen"
local cwd = (...):gsub('%.screen$', '') .. "."
local CScreen = require(cwd .. "libs.cscreen")
-- INIT FUNCTIONS
-- Initialize and configure the screen manager
function ScreenManager:new(controller)
self.controller = controller
@ -51,6 +55,9 @@ function ScreenManager:applySettings()
CScreen.update(width, height)
end
-- POINTER FUNCTIONS
-- Translate the pointer according to the screen coordinates
function ScreenManager:project(x, y)
return CScreen.project(x, y)
end
@ -59,10 +66,16 @@ function ScreenManager:getMousePosition()
return CScreen.project(love.mouse.getX(), love.mouse.getY())
end
-- INFO FUNCTIONS
-- Get screen informations
function ScreenManager:getDimensions()
return self.width, self.height
end
-- DRAW FUNCTIONS
-- Apply draw functions to the scene
function ScreenManager:apply()
CScreen.apply()
end

View File

@ -0,0 +1,39 @@
-- loveutils.filesystem : functions to handle filesystem.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Filesystem = {}
function Filesystem.exists(filepath)
local info = love.filesystem.getInfo( filepath )
local exists = false
if (info == nil) then
exists = false
else
exists = true
end
return exists
end
return Filesystem

View File

@ -0,0 +1,97 @@
-- loveutils.graphics : a set of useful functions for love2D. Aim to reduce
-- boilerplate in love2D by creating usefull function to handle these roles.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Graphics = {}
-- COLOR FUNCTIONS
-- Handle colors and scene colors
function Graphics.resetColor()
love.graphics.setColor(1,1,1,1)
end
-- PRINT TEXT
-- Functions to draw text on screen
function Graphics.print(text, x, y, align, r, sx, sy, ox, oy, kx, ky)
local width
local font = love.graphics.getFont()
width = font:getWidth(text)
if align == "center" then
width = (width/2)
elseif align == "right" then
width = width
else
width = 0
end
love.graphics.print(text, x - (width), y, r, sx, sy, ox, oy, kx, ky)
end
function Graphics.printWithSpacing(text, spacing, align, x, y, r, sx, sy, ox, oy, kx, ky)
-- DO NOT USE THIS FUNCTION IN A "UPDATE" FUNCTION !
-- it's pretty heavy to use as it use a loop to get every character in a text
local font = love.graphics.getFont()
local xx = 0
local lenght = string.len(text)
local basewidth = font:getWidth(text)
local width = basewidth + (spacing * lenght)
if align == "center" then
width = (width/2)
elseif align == "right" then
width = width
else
width = 0
end
for i=1, lenght do
local char = string.sub(text, i, i)
pos = math.floor(x + xx - width)
love.graphics.print(char, pos, y)
xx = xx + font:getWidth(char) + spacing
end
end
-- PLACEHOLDER GRAPHICS FUNCTIONS
-- Ready-to-use placeolder and stuff
function Graphics.box(x, y, w, h)
local x = math.floor(x)
local y = math.floor(y)
local w = math.floor(w)
local h = math.floor(h)
local a = a or 1
local r, g, b, a = love.graphics.getColor( )
love.graphics.setColor(r, g, b, 0.3 * a)
love.graphics.rectangle("fill", x, y, w, h)
love.graphics.setColor(r, g, b, a)
love.graphics.rectangle("line", x, y, w, h)
end
return Graphics

View File

@ -0,0 +1,31 @@
-- loveutils : a set of basic functions and utility for love2D.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local cwd = (...):gsub('%.init$', '') .. "."
-- load the different elements from loveutils
return {
math = require(cwd .. "math"),
graphics = require(cwd .. "graphics"),
filesystem = require(cwd .. "filesystem")
}

View File

@ -0,0 +1,139 @@
-- loveutils.math : easy to use functions for mathematics and geometry.
--[[
Copyright © 2019 Kazhnuz
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.
]]
local Math = {}
-- ALGEBRA FUNCTIONS
-- Simple yet usefull functions not supported by base love2D
function Math.sign(x)
if (x < 0) then
return -1
elseif (x > 0) then
return 1
else
return 0
end
end
function Math.round(num)
return math.floor(num + 0.5)
end
function Math.toZero(num, sub)
local sub = math.floor(sub)
if math.abs(num) < sub then
return 0
else
return num - (sub * Math.sign(num))
end
end
function Math.between(num, value1, value2)
local min = math.min(value1, value2)
local max = math.max(value1, value2)
return math.min(math.max(num, min), max)
end
-- VECTOR/DIRECTION functions
-- Easy-to-use function to handle point and motion
function Math.vector(x1, y1, x2, y2)
local vecx, vecy
vecx = x2 - x1
vexy = y2 - y1
return vecx, vecy
end
function Math.getMiddlePoint(x1, y1, x2, y2)
local newx, newy, vecx, vecy
vecx = math.max(x1, x2) - math.min(x1, x2)
vecy = math.max(y1, y2) - math.min(y1, y2)
newx = math.min(x1, x2) + (vecx / 2)
newy = math.min(y1, y2) + (vecy / 2)
return newx, newy
end
function Math.pointDistance(x1, y1, x2, y2)
local vecx, vecy
vecx = math.max(x1, x2) - math.min(x1, x2)
vecy = math.max(y1, y2) - math.min(y1, y2)
return math.sqrt(vecx^2 + vecy^2)
end
function Math.pointDirection(x1,y1,x2,y2)
local vecx, vecy, angle
vecy = y2 - y1
vecx = x2 - x1
angle = math.atan2(vecy, vecx)
return angle
end
-- STRING FUNCTIONS
-- Transform into string numbers
function Math.numberToString(x, length)
local length = length or 1
local string = ""
local x = x
if (x >= math.pow(10, length)) then
x = unitsNumber*10 - 1
string = string .. x
else
for i=1, (length-1) do
if (x < math.pow(10, length-i)) then
string = string .. "0"
end
end
string = string .. x
end
return string
end
-- COORDINATE FUNCTIONS
-- Easy computation on coordinate
function Math.floorCoord(x, y)
return math.floor(x), math.floor(y)
end
function Math.pixeliseCoord(x, y, factor)
x, y = Math.floorCoord(x / factor, y / factor)
x = x * factor
y = y * factor
return x, y
end
return Math