diff --git a/imperium-porcorum.love/core/callbacks.lua b/imperium-porcorum.love/core/callbacks.lua new file mode 100644 index 0000000..39fe422 --- /dev/null +++ b/imperium-porcorum.love/core/callbacks.lua @@ -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 diff --git a/imperium-porcorum.love/core/debug.lua b/imperium-porcorum.love/core/debug.lua index 9a85811..fe6076d 100644 --- a/imperium-porcorum.love/core/debug.lua +++ b/imperium-porcorum.love/core/debug.lua @@ -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 diff --git a/imperium-porcorum.love/core/init.lua b/imperium-porcorum.love/core/init.lua index 63bba08..1831d01 100644 --- a/imperium-porcorum.love/core/init.lua +++ b/imperium-porcorum.love/core/init.lua @@ -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() diff --git a/imperium-porcorum.love/core/input.lua b/imperium-porcorum.love/core/input.lua index 7513d25..539e8f8 100644 --- a/imperium-porcorum.love/core/input.lua +++ b/imperium-porcorum.love/core/input.lua @@ -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 diff --git a/imperium-porcorum.love/core/lang.lua b/imperium-porcorum.love/core/lang.lua index 1144806..3691707 100644 --- a/imperium-porcorum.love/core/lang.lua +++ b/imperium-porcorum.love/core/lang.lua @@ -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 diff --git a/imperium-porcorum.love/core/libs/binser.lua b/imperium-porcorum.love/core/libs/binser.lua new file mode 100644 index 0000000..5aa1299 --- /dev/null +++ b/imperium-porcorum.love/core/libs/binser.lua @@ -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 +} diff --git a/imperium-porcorum.love/core/libs/classic.lua b/imperium-porcorum.love/core/libs/classic.lua new file mode 100644 index 0000000..cbd6f81 --- /dev/null +++ b/imperium-porcorum.love/core/libs/classic.lua @@ -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 diff --git a/imperium-porcorum.love/core/libs/cscreen.lua b/imperium-porcorum.love/core/libs/cscreen.lua new file mode 100644 index 0000000..579ea0d --- /dev/null +++ b/imperium-porcorum.love/core/libs/cscreen.lua @@ -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 diff --git a/imperium-porcorum.love/core/libs/lovebird.lua b/imperium-porcorum.love/core/libs/lovebird.lua new file mode 100644 index 0000000..8b296eb --- /dev/null +++ b/imperium-porcorum.love/core/libs/lovebird.lua @@ -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"] = [[ + + + + + + + + lovebird + + + + +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + +]] + + +lovebird.pages["buffer"] = [[ ]] + + +lovebird.pages["env.json"] = [[ + +{ + "valid": true, + "path": "", + "vars": [ + + { + "key": "", + "value": , + "type": "", + }, + + ] +} +]] + + + +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", 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 = { + ["<"] = "<", + ["&"] = "&", + ['"'] = """, + ["'"] = "'", +} + +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", "
") + end + if line.type == "input" then + str = '' .. str .. '' + else + if line.type == "error" then + str = '! ' .. str + str = '' .. str .. '' + end + if line.count > 1 then + str = '' .. line.count .. ' ' .. str + end + if lovebird.timestamp then + str = os.date('%H:%M:%S ', line.time) .. + str + end + end + return str + end + lovebird.buffer = table.concat(lovebird.map(lovebird.lines, doline), "
") +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 diff --git a/imperium-porcorum.love/core/modules/assets/animator.lua b/imperium-porcorum.love/core/modules/assets/animator.lua index 7bce90c..1498153 100644 --- a/imperium-porcorum.love/core/modules/assets/animator.lua +++ b/imperium-porcorum.love/core/modules/assets/animator.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/assets/autotile.lua b/imperium-porcorum.love/core/modules/assets/autotile.lua index 4104303..b441ab1 100644 --- a/imperium-porcorum.love/core/modules/assets/autotile.lua +++ b/imperium-porcorum.love/core/modules/assets/autotile.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/assets/background.lua b/imperium-porcorum.love/core/modules/assets/background.lua index 862050b..7a0ac56 100644 --- a/imperium-porcorum.love/core/modules/assets/background.lua +++ b/imperium-porcorum.love/core/modules/assets/background.lua @@ -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 ) diff --git a/imperium-porcorum.love/core/modules/assets/fonts.lua b/imperium-porcorum.love/core/modules/assets/fonts.lua index f2bc7c9..c527144 100644 --- a/imperium-porcorum.love/core/modules/assets/fonts.lua +++ b/imperium-porcorum.love/core/modules/assets/fonts.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/assets/imagefonts.lua b/imperium-porcorum.love/core/modules/assets/imagefonts.lua index 16e0b7a..86f318b 100644 --- a/imperium-porcorum.love/core/modules/assets/imagefonts.lua +++ b/imperium-porcorum.love/core/modules/assets/imagefonts.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/assets/init.lua b/imperium-porcorum.love/core/modules/assets/init.lua index b6e11dc..1634110 100644 --- a/imperium-porcorum.love/core/modules/assets/init.lua +++ b/imperium-porcorum.love/core/modules/assets/init.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/assets/sprites.lua b/imperium-porcorum.love/core/modules/assets/sprites.lua index 326db34..ddf4f89 100644 --- a/imperium-porcorum.love/core/modules/assets/sprites.lua +++ b/imperium-porcorum.love/core/modules/assets/sprites.lua @@ -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) diff --git a/imperium-porcorum.love/core/modules/assets/texture.lua b/imperium-porcorum.love/core/modules/assets/texture.lua new file mode 100644 index 0000000..30901f3 --- /dev/null +++ b/imperium-porcorum.love/core/modules/assets/texture.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/assets/tileset.lua b/imperium-porcorum.love/core/modules/assets/tileset.lua index c86aa44..55d6643 100644 --- a/imperium-porcorum.love/core/modules/assets/tileset.lua +++ b/imperium-porcorum.love/core/modules/assets/tileset.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/flowbox.lua b/imperium-porcorum.love/core/modules/menusystem/flowbox.lua index a120f80..c3d9426 100644 --- a/imperium-porcorum.love/core/modules/menusystem/flowbox.lua +++ b/imperium-porcorum.love/core/modules/menusystem/flowbox.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/grid.lua b/imperium-porcorum.love/core/modules/menusystem/grid.lua index 7dec4b3..e1fd8ed 100644 --- a/imperium-porcorum.love/core/modules/menusystem/grid.lua +++ b/imperium-porcorum.love/core/modules/menusystem/grid.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/hlistbox.lua b/imperium-porcorum.love/core/modules/menusystem/hlistbox.lua new file mode 100644 index 0000000..067de32 --- /dev/null +++ b/imperium-porcorum.love/core/modules/menusystem/hlistbox.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/init.lua b/imperium-porcorum.love/core/modules/menusystem/init.lua index db5ee4d..780796d 100644 --- a/imperium-porcorum.love/core/modules/menusystem/init.lua +++ b/imperium-porcorum.love/core/modules/menusystem/init.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/listbox.lua b/imperium-porcorum.love/core/modules/menusystem/listbox.lua index 944c465..19c2ea2 100644 --- a/imperium-porcorum.love/core/modules/menusystem/listbox.lua +++ b/imperium-porcorum.love/core/modules/menusystem/listbox.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/parent.lua b/imperium-porcorum.love/core/modules/menusystem/parent.lua index a6bb2a5..12777e6 100644 --- a/imperium-porcorum.love/core/modules/menusystem/parent.lua +++ b/imperium-porcorum.love/core/modules/menusystem/parent.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/textmenu.lua b/imperium-porcorum.love/core/modules/menusystem/textmenu.lua deleted file mode 100644 index 6703039..0000000 --- a/imperium-porcorum.love/core/modules/menusystem/textmenu.lua +++ /dev/null @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/widgets/init.lua b/imperium-porcorum.love/core/modules/menusystem/widgets/init.lua index 03006df..9c02e5a 100644 --- a/imperium-porcorum.love/core/modules/menusystem/widgets/init.lua +++ b/imperium-porcorum.love/core/modules/menusystem/widgets/init.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/menusystem/widgets/utils.lua b/imperium-porcorum.love/core/modules/menusystem/widgets/utils.lua index ab4a48c..e69c6d7 100644 --- a/imperium-porcorum.love/core/modules/menusystem/widgets/utils.lua +++ b/imperium-porcorum.love/core/modules/menusystem/widgets/utils.lua @@ -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) diff --git a/imperium-porcorum.love/core/modules/scenes.lua b/imperium-porcorum.love/core/modules/scenes.lua index c87f29e..9da7c83 100644 --- a/imperium-porcorum.love/core/modules/scenes.lua +++ b/imperium-porcorum.love/core/modules/scenes.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/actors/actor2D.lua b/imperium-porcorum.love/core/modules/world/actors/actor2D.lua new file mode 100644 index 0000000..a6613d2 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/actors/actor2D.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/actors/baseactor.lua b/imperium-porcorum.love/core/modules/world/actors/baseactor.lua new file mode 100644 index 0000000..5512449 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/actors/baseactor.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/actors/gfx2D.lua b/imperium-porcorum.love/core/modules/world/actors/gfx2D.lua new file mode 100644 index 0000000..9f3857e --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/actors/gfx2D.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/actors/utils/timer.lua b/imperium-porcorum.love/core/modules/world/actors/utils/timer.lua new file mode 100644 index 0000000..5ba7c3f --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/actors/utils/timer.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/baseworld.lua b/imperium-porcorum.love/core/modules/world/baseworld.lua new file mode 100644 index 0000000..4bf83eb --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/baseworld.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/camera.lua b/imperium-porcorum.love/core/modules/world/camera.lua new file mode 100644 index 0000000..db77abe --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/camera.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/bump-3dpd.lua b/imperium-porcorum.love/core/modules/world/libs/bump-3dpd.lua new file mode 100644 index 0000000..f094f8d --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/bump-3dpd.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/bump.lua b/imperium-porcorum.love/core/modules/world/libs/bump.lua new file mode 100644 index 0000000..66d4cf1 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/bump.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/hump/camera.lua b/imperium-porcorum.love/core/modules/world/libs/hump/camera.lua new file mode 100644 index 0000000..cb86a79 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/hump/camera.lua @@ -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}) diff --git a/imperium-porcorum.love/core/modules/world/libs/sti/graphics.lua b/imperium-porcorum.love/core/modules/world/libs/sti/graphics.lua new file mode 100644 index 0000000..1d73379 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/sti/graphics.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/sti/init.lua b/imperium-porcorum.love/core/modules/world/libs/sti/init.lua new file mode 100644 index 0000000..de1bd16 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/sti/init.lua @@ -0,0 +1,1485 @@ +--- Simple and fast Tiled map loader and renderer. +-- @module sti +-- @author Landon Manning +-- @copyright 2016 +-- @license MIT/X11 + +local STI = { + _LICENSE = "MIT/X11", + _URL = "https://github.com/karai17/Simple-Tiled-Implementation", + _VERSION = "0.18.2.1", + _DESCRIPTION = "Simple Tiled Implementation is a Tiled Map Editor library designed for the *awesome* LÖVE framework.", + cache = {} +} +STI.__index = STI + +local cwd = (...):gsub('%.init$', '') .. "." +local utils = require(cwd .. "utils") +local ceil = math.ceil +local floor = math.floor +local lg = require(cwd .. "graphics") +local Map = {} +Map.__index = Map + +local function new(map, plugins, ox, oy) + local dir = "" + + if type(map) == "table" then + map = setmetatable(map, Map) + else + -- Check for valid map type + local ext = map:sub(-4, -1) + assert(ext == ".lua", string.format( + "Invalid file type: %s. File must be of type: lua.", + ext + )) + + -- Get directory of map + dir = map:reverse():find("[/\\]") or "" + if dir ~= "" then + dir = map:sub(1, 1 + (#map - dir)) + end + + -- Load map + map = setmetatable(assert(love.filesystem.load(map))(), Map) + end + + map:init(dir, plugins, ox, oy) + + return map +end + +--- Instance a new map. +-- @param map Path to the map file or the map table itself +-- @param plugins A list of plugins to load +-- @param ox Offset of map on the X axis (in pixels) +-- @param oy Offset of map on the Y axis (in pixels) +-- @return table The loaded Map +function STI.__call(_, map, plugins, ox, oy) + return new(map, plugins, ox, oy) +end + +--- Flush image cache. +function STI:flush() + self.cache = {} +end + +--- Map object + +--- Instance a new map +-- @param path Path to the map file +-- @param plugins A list of plugins to load +-- @param ox Offset of map on the X axis (in pixels) +-- @param oy Offset of map on the Y axis (in pixels) +function Map:init(path, plugins, ox, oy) + if type(plugins) == "table" then + self:loadPlugins(plugins) + end + + self:resize() + self.objects = {} + self.tiles = {} + self.tileInstances = {} + self.drawRange = { + sx = 1, + sy = 1, + ex = self.width, + ey = self.height, + } + self.offsetx = ox or 0 + self.offsety = oy or 0 + + self.freeBatchSprites = {} + setmetatable(self.freeBatchSprites, { __mode = 'k' }) + + -- Set tiles, images + local gid = 1 + for i, tileset in ipairs(self.tilesets) do + assert(tileset.image, "STI does not support Tile Collections.\nYou need to create a Texture Atlas.") + + -- Cache images + if lg.isCreated then + local formatted_path = utils.format_path(path .. tileset.image) + + if not STI.cache[formatted_path] then + utils.fix_transparent_color(tileset, formatted_path) + utils.cache_image(STI, formatted_path, tileset.image) + else + tileset.image = STI.cache[formatted_path] + end + end + + gid = self:setTiles(i, tileset, gid) + end + + -- Set layers + for _, layer in ipairs(self.layers) do + self:setLayer(layer, path) + end +end + +--- Load plugins +-- @param plugins A list of plugins to load +function Map:loadPlugins(plugins) + for _, plugin in ipairs(plugins) do + local pluginModulePath = cwd .. 'plugins.' .. plugin + local ok, pluginModule = pcall(require, pluginModulePath) + if ok then + for k, func in pairs(pluginModule) do + if not self[k] then + self[k] = func + end + end + end + end +end + +--- Create Tiles +-- @param index Index of the Tileset +-- @param tileset Tileset data +-- @param gid First Global ID in Tileset +-- @return number Next Tileset's first Global ID +function Map:setTiles(index, tileset, gid) + local quad = lg.newQuad + local imageW = tileset.imagewidth + local imageH = tileset.imageheight + local tileW = tileset.tilewidth + local tileH = tileset.tileheight + local margin = tileset.margin + local spacing = tileset.spacing + local w = utils.get_tiles(imageW, tileW, margin, spacing) + local h = utils.get_tiles(imageH, tileH, margin, spacing) + + for y = 1, h do + for x = 1, w do + local id = gid - tileset.firstgid + local quadX = (x - 1) * tileW + margin + (x - 1) * spacing + local quadY = (y - 1) * tileH + margin + (y - 1) * spacing + local properties, terrain, animation, objectGroup + + for _, tile in pairs(tileset.tiles) do + if tile.id == id then + properties = tile.properties + animation = tile.animation + objectGroup = tile.objectGroup + + if tile.terrain then + terrain = {} + + for i = 1, #tile.terrain do + terrain[i] = tileset.terrains[tile.terrain[i] + 1] + end + end + end + end + + local tile = { + id = id, + gid = gid, + tileset = index, + quad = quad( + quadX, quadY, + tileW, tileH, + imageW, imageH + ), + properties = properties or {}, + terrain = terrain, + animation = animation, + objectGroup = objectGroup, + frame = 1, + time = 0, + width = tileW, + height = tileH, + sx = 1, + sy = 1, + r = 0, + offset = tileset.tileoffset, + } + + self.tiles[gid] = tile + gid = gid + 1 + end + end + + return gid +end + +--- Create Layers +-- @param layer Layer data +-- @param path (Optional) Path to an Image Layer's image +function Map:setLayer(layer, path) + if layer.encoding then + if layer.encoding == "base64" then + assert(require "ffi", "Compressed maps require LuaJIT FFI.\nPlease Switch your interperator to LuaJIT or your Tile Layer Format to \"CSV\".") + local fd = love.data.decode("string", "base64", layer.data) + + if not layer.compression then + layer.data = utils.get_decompressed_data(fd) + else + assert(love.data.decompress, "zlib and gzip compression require LOVE 11.0+.\nPlease set your Tile Layer Format to \"Base64 (uncompressed)\" or \"CSV\".") + + if layer.compression == "zlib" then + local data = love.data.decompress("string", "zlib", fd) + layer.data = utils.get_decompressed_data(data) + end + + if layer.compression == "gzip" then + local data = love.data.decompress("string", "gzip", fd) + layer.data = utils.get_decompressed_data(data) + end + end + end + end + + layer.x = (layer.x or 0) + layer.offsetx + self.offsetx + layer.y = (layer.y or 0) + layer.offsety + self.offsety + layer.update = function() end + + if layer.type == "tilelayer" then + self:setTileData(layer) + self:setSpriteBatches(layer) + layer.draw = function() self:drawTileLayer(layer) end + elseif layer.type == "objectgroup" then + self:setObjectData(layer) + self:setObjectCoordinates(layer) + self:setObjectSpriteBatches(layer) + layer.draw = function() self:drawObjectLayer(layer) end + elseif layer.type == "imagelayer" then + layer.draw = function() self:drawImageLayer(layer) end + + if layer.image ~= "" then + local formatted_path = utils.format_path(path .. layer.image) + if not STI.cache[formatted_path] then + utils.cache_image(STI, formatted_path) + end + + layer.image = STI.cache[formatted_path] + layer.width = layer.image:getWidth() + layer.height = layer.image:getHeight() + end + end + + self.layers[layer.name] = layer +end + +--- Add Tiles to Tile Layer +-- @param layer The Tile Layer +function Map:setTileData(layer) + local i = 1 + local map = {} + + for y = 1, layer.height do + map[y] = {} + for x = 1, layer.width do + local gid = layer.data[i] + + if gid > 0 then + map[y][x] = self.tiles[gid] or self:setFlippedGID(gid) + end + + i = i + 1 + end + end + + layer.data = map +end + +--- Add Objects to Layer +-- @param layer The Object Layer +function Map:setObjectData(layer) + for _, object in ipairs(layer.objects) do + object.layer = layer + self.objects[object.id] = object + end +end + +--- Correct position and orientation of Objects in an Object Layer +-- @param layer The Object Layer +function Map:setObjectCoordinates(layer) + for _, object in ipairs(layer.objects) do + local x = layer.x + object.x + local y = layer.y + object.y + local w = object.width + local h = object.height + local r = object.rotation + local cos = math.cos(math.rad(r)) + local sin = math.sin(math.rad(r)) + + if object.shape == "rectangle" and not object.gid then + object.rectangle = {} + + local vertices = { + { x=x, y=y }, + { x=x + w, y=y }, + { x=x + w, y=y + h }, + { x=x, y=y + h }, + } + + for _, vertex in ipairs(vertices) do + vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) + table.insert(object.rectangle, { x = vertex.x, y = vertex.y }) + end + elseif object.shape == "ellipse" then + object.ellipse = {} + local vertices = utils.convert_ellipse_to_polygon(x, y, w, h) + + for _, vertex in ipairs(vertices) do + vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) + table.insert(object.ellipse, { x = vertex.x, y = vertex.y }) + end + elseif object.shape == "polygon" then + for _, vertex in ipairs(object.polygon) do + vertex.x = vertex.x + x + vertex.y = vertex.y + y + vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) + end + elseif object.shape == "polyline" then + for _, vertex in ipairs(object.polyline) do + vertex.x = vertex.x + x + vertex.y = vertex.y + y + vertex.x, vertex.y = utils.rotate_vertex(self, vertex, x, y, cos, sin) + end + end + end +end + +--- Convert tile location to tile instance location +-- @param layer Tile layer +-- @param tile Tile +-- @param x Tile location on X axis (in tiles) +-- @param y Tile location on Y axis (in tiles) +-- @return number Tile instance location on X axis (in pixels) +-- @return number Tile instance location on Y axis (in pixels) +function Map:getLayerTilePosition(layer, tile, x, y) + local tileW = self.tilewidth + local tileH = self.tileheight + local tileX, tileY + + if self.orientation == "orthogonal" then + local tileset = self.tilesets[tile.tileset] + tileX = (x - 1) * tileW + tile.offset.x + tileY = (y - 0) * tileH + tile.offset.y - tileset.tileheight + tileX, tileY = utils.compensate(tile, tileX, tileY, tileW, tileH) + elseif self.orientation == "isometric" then + tileX = (x - y) * (tileW / 2) + tile.offset.x + layer.width * tileW / 2 - self.tilewidth / 2 + tileY = (x + y - 2) * (tileH / 2) + tile.offset.y + else + local sideLen = self.hexsidelength or 0 + if self.staggeraxis == "y" then + if self.staggerindex == "odd" then + if y % 2 == 0 then + tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x + else + tileX = (x - 1) * tileW + tile.offset.x + end + else + if y % 2 == 0 then + tileX = (x - 1) * tileW + tile.offset.x + else + tileX = (x - 1) * tileW + tileW / 2 + tile.offset.x + end + end + + local rowH = tileH - (tileH - sideLen) / 2 + tileY = (y - 1) * rowH + tile.offset.y + else + if self.staggerindex == "odd" then + if x % 2 == 0 then + tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y + else + tileY = (y - 1) * tileH + tile.offset.y + end + else + if x % 2 == 0 then + tileY = (y - 1) * tileH + tile.offset.y + else + tileY = (y - 1) * tileH + tileH / 2 + tile.offset.y + end + end + + local colW = tileW - (tileW - sideLen) / 2 + tileX = (x - 1) * colW + tile.offset.x + end + end + + return tileX, tileY +end + +--- Place new tile instance +-- @param layer Tile layer +-- @param tile Tile +-- @param number Tile location on X axis (in tiles) +-- @param number Tile location on Y axis (in tiles) +function Map:addNewLayerTile(layer, tile, x, y) + local tileset = tile.tileset + local image = self.tilesets[tile.tileset].image + + layer.batches[tileset] = layer.batches[tileset] + or lg.newSpriteBatch(image, layer.width * layer.height) + + local batch = layer.batches[tileset] + local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y) + + local tab = { + layer = layer, + gid = tile.gid, + x = tileX, + y = tileY, + r = tile.r, + oy = 0 + } + + if batch then + tab.batch = batch + tab.id = batch:add(tile.quad, tileX, tileY, tile.r, tile.sx, tile.sy) + end + + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + table.insert(self.tileInstances[tile.gid], tab) +end + +--- Batch Tiles in Tile Layer for improved draw speed +-- @param layer The Tile Layer +function Map:setSpriteBatches(layer) + layer.batches = {} + + if self.orientation == "orthogonal" or self.orientation == "isometric" then + local startX = 1 + local startY = 1 + local endX = layer.width + local endY = layer.height + local incrementX = 1 + local incrementY = 1 + + -- Determine order to add tiles to sprite batch + -- Defaults to right-down + if self.renderorder == "right-up" then + startX, endX, incrementX = startX, endX, 1 + startY, endY, incrementY = endY, startY, -1 + elseif self.renderorder == "left-down" then + startX, endX, incrementX = endX, startX, -1 + startY, endY, incrementY = startY, endY, 1 + elseif self.renderorder == "left-up" then + startX, endX, incrementX = endX, startX, -1 + startY, endY, incrementY = endY, startY, -1 + end + + for y = startY, endY, incrementY do + for x = startX, endX, incrementX do + local tile = layer.data[y][x] + + if tile then + self:addNewLayerTile(layer, tile, x, y) + end + end + end + else + local sideLen = self.hexsidelength or 0 + + if self.staggeraxis == "y" then + for y = 1, layer.height do + for x = 1, layer.width do + local tile = layer.data[y][x] + + if tile then + self:addNewLayerTile(layer, tile, x, y) + end + end + end + else + local i = 0 + local _x + + if self.staggerindex == "odd" then + _x = 1 + else + _x = 2 + end + + while i < layer.width * layer.height do + for _y = 1, layer.height + 0.5, 0.5 do + local y = floor(_y) + + for x = _x, layer.width, 2 do + i = i + 1 + local tile = layer.data[y][x] + + if tile then + self:addNewLayerTile(layer, tile, x, y) + end + end + + if _x == 1 then + _x = 2 + else + _x = 1 + end + end + end + end + end +end + +--- Batch Tiles in Object Layer for improved draw speed +-- @param layer The Object Layer +function Map:setObjectSpriteBatches(layer) + local newBatch = lg.newSpriteBatch + local tileW = self.tilewidth + local tileH = self.tileheight + local batches = {} + + if layer.draworder == "topdown" then + table.sort(layer.objects, function(a, b) + return a.y + a.height < b.y + b.height + end) + end + + for _, object in ipairs(layer.objects) do + if object.gid then + local tile = self.tiles[object.gid] or self:setFlippedGID(object.gid) + local tileset = tile.tileset + local image = self.tilesets[tile.tileset].image + + batches[tileset] = batches[tileset] or newBatch(image) + + local sx = object.width / tile.width + local sy = object.height / tile.height + + local batch = batches[tileset] + local tileX = object.x + tile.offset.x + local tileY = object.y + tile.offset.y - tile.height * sy + local tileR = math.rad(object.rotation) + local oy = 0 + + -- Compensation for scale/rotation shift + if tile.sx == 1 and tile.sy == 1 then + if tileR ~= 0 then + tileY = tileY + tileH + oy = tileH + end + else + if tile.sx < 0 then tileX = tileX + tileW end + if tile.sy < 0 then tileY = tileY + tileH end + if tileR > 0 then tileX = tileX + tileW end + if tileR < 0 then tileY = tileY + tileH end + end + + local tab = { + layer = layer, + gid = tile.gid, + x = tileX, + y = tileY, + r = tileR, + oy = oy + } + + if batch then + tab.batch = batch + tab.id = batch:add(tile.quad, tileX, tileY, tileR, tile.sx * sx, tile.sy * sy, 0, oy) + end + + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + table.insert(self.tileInstances[tile.gid], tab) + end + end + + layer.batches = batches +end + +--- Create a Custom Layer to place userdata in (such as player sprites) +-- @param name Name of Custom Layer +-- @param index Draw order within Layer stack +-- @return table Custom Layer +function Map:addCustomLayer(name, index) + index = index or #self.layers + 1 + local layer = { + type = "customlayer", + name = name, + visible = true, + opacity = 1, + properties = {}, + } + + function layer.draw() end + function layer.update() end + + table.insert(self.layers, index, layer) + self.layers[name] = self.layers[index] + + return layer +end + +--- Convert another Layer into a Custom Layer +-- @param index Index or name of Layer to convert +-- @return table Custom Layer +function Map:convertToCustomLayer(index) + local layer = assert(self.layers[index], "Layer not found: " .. index) + + layer.type = "customlayer" + layer.x = nil + layer.y = nil + layer.width = nil + layer.height = nil + layer.encoding = nil + layer.data = nil + layer.objects = nil + layer.image = nil + + function layer.draw() end + function layer.update() end + + return layer +end + +--- Remove a Layer from the Layer stack +-- @param index Index or name of Layer to convert +function Map:removeLayer(index) + local layer = assert(self.layers[index], "Layer not found: " .. index) + + if type(index) == "string" then + for i, l in ipairs(self.layers) do + if l.name == index then + table.remove(self.layers, i) + self.layers[index] = nil + break + end + end + else + local name = self.layers[index].name + table.remove(self.layers, index) + self.layers[name] = nil + end + + -- Remove tile instances + if layer.batches then + for _, batch in pairs(layer.batches) do + self.freeBatchSprites[batch] = nil + end + + for _, tiles in pairs(self.tileInstances) do + for i = #tiles, 1, -1 do + local tile = tiles[i] + if tile.layer == layer then + table.remove(tiles, i) + end + end + end + end + + -- Remove objects + if layer.objects then + for i, object in pairs(self.objects) do + if object.layer == layer then + self.objects[i] = nil + end + end + end +end + +--- Animate Tiles and update every Layer +-- @param dt Delta Time +function Map:update(dt) + for _, tile in pairs(self.tiles) do + local update = false + + if tile.animation then + tile.time = tile.time + dt * 1000 + + while tile.time > tonumber(tile.animation[tile.frame].duration) do + update = true + tile.time = tile.time - tonumber(tile.animation[tile.frame].duration) + tile.frame = tile.frame + 1 + + if tile.frame > #tile.animation then tile.frame = 1 end + end + + if update and self.tileInstances[tile.gid] then + for _, j in pairs(self.tileInstances[tile.gid]) do + local t = self.tiles[tonumber(tile.animation[tile.frame].tileid) + self.tilesets[tile.tileset].firstgid] + j.batch:set(j.id, t.quad, j.x, j.y, j.r, tile.sx, tile.sy, 0, j.oy) + end + end + end + end + + for _, layer in ipairs(self.layers) do + layer:update(dt) + end +end + +--- Draw every Layer +-- @param tx Translate on X +-- @param ty Translate on Y +-- @param sx Scale on X +-- @param sy Scale on Y +function Map:draw(tx, ty, sx, sy) + local current_canvas = lg.getCanvas() + lg.setCanvas(self.canvas) + lg.clear() + + -- Scale map to 1.0 to draw onto canvas, this fixes tearing issues + -- Map is translated to correct position so the right section is drawn + lg.push() + lg.origin() + lg.translate(math.floor(tx or 0), math.floor(ty or 0)) + + for _, layer in ipairs(self.layers) do + if layer.visible and layer.opacity > 0 then + self:drawLayer(layer) + end + end + + lg.pop() + + -- Draw canvas at 0,0; this fixes scissoring issues + -- Map is scaled to correct scale so the right section is shown + lg.push() + lg.origin() + lg.scale(sx or 1, sy or sx or 1) + + lg.setCanvas(current_canvas) + lg.draw(self.canvas) + + lg.pop() +end + +--- Draw an individual Layer +-- @param layer The Layer to draw +function Map.drawLayer(_, layer) + local r,g,b,a = lg.getColor() + lg.setColor(r, g, b, a * layer.opacity) + layer:draw() + lg.setColor(r,g,b,a) +end + +--- Default draw function for Tile Layers +-- @param layer The Tile Layer to draw +function Map:drawTileLayer(layer) + if type(layer) == "string" or type(layer) == "number" then + layer = self.layers[layer] + end + + assert(layer.type == "tilelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: tilelayer") + + for _, batch in pairs(layer.batches) do + lg.draw(batch, floor(layer.x), floor(layer.y)) + end +end + +--- Default draw function for Object Layers +-- @param layer The Object Layer to draw +function Map:drawObjectLayer(layer) + if type(layer) == "string" or type(layer) == "number" then + layer = self.layers[layer] + end + + assert(layer.type == "objectgroup", "Invalid layer type: " .. layer.type .. ". Layer must be of type: objectgroup") + + local line = { 160, 160, 160, 255 * layer.opacity } + local fill = { 160, 160, 160, 255 * layer.opacity * 0.5 } + local r,g,b,a = lg.getColor() + local reset = { r, g, b, a * layer.opacity } + + local function sortVertices(obj) + local vertex = {} + + for _, v in ipairs(obj) do + table.insert(vertex, v.x) + table.insert(vertex, v.y) + end + + return vertex + end + + local function drawShape(obj, shape) + local vertex = sortVertices(obj) + + if shape == "polyline" then + lg.setColor(line) + lg.line(vertex) + return + elseif shape == "polygon" then + lg.setColor(fill) + if not love.math.isConvex(vertex) then + local triangles = love.math.triangulate(vertex) + for _, triangle in ipairs(triangles) do + lg.polygon("fill", triangle) + end + else + lg.polygon("fill", vertex) + end + else + lg.setColor(fill) + lg.polygon("fill", vertex) + end + + lg.setColor(line) + lg.polygon("line", vertex) + end + + for _, object in ipairs(layer.objects) do + if object.shape == "rectangle" and not object.gid then + drawShape(object.rectangle, "rectangle") + elseif object.shape == "ellipse" then + drawShape(object.ellipse, "ellipse") + elseif object.shape == "polygon" then + drawShape(object.polygon, "polygon") + elseif object.shape == "polyline" then + drawShape(object.polyline, "polyline") + end + end + + lg.setColor(reset) + for _, batch in pairs(layer.batches) do + lg.draw(batch, 0, 0) + end + lg.setColor(r,g,b,a) +end + +--- Default draw function for Image Layers +-- @param layer The Image Layer to draw +function Map:drawImageLayer(layer) + if type(layer) == "string" or type(layer) == "number" then + layer = self.layers[layer] + end + + assert(layer.type == "imagelayer", "Invalid layer type: " .. layer.type .. ". Layer must be of type: imagelayer") + + if layer.image ~= "" then + lg.draw(layer.image, layer.x, layer.y) + end +end + +--- Resize the drawable area of the Map +-- @param w The new width of the drawable area (in pixels) +-- @param h The new Height of the drawable area (in pixels) +function Map:resize(w, h) + if lg.isCreated then + w = w or lg.getWidth() + h = h or lg.getHeight() + + self.canvas = lg.newCanvas(w, h) + self.canvas:setFilter("nearest", "nearest") + end +end + +--- Create flipped or rotated Tiles based on bitop flags +-- @param gid The flagged Global ID +-- @return table Flipped Tile +function Map:setFlippedGID(gid) + local bit31 = 2147483648 + local bit30 = 1073741824 + local bit29 = 536870912 + local flipX = false + local flipY = false + local flipD = false + local realgid = gid + + if realgid >= bit31 then + realgid = realgid - bit31 + flipX = not flipX + end + + if realgid >= bit30 then + realgid = realgid - bit30 + flipY = not flipY + end + + if realgid >= bit29 then + realgid = realgid - bit29 + flipD = not flipD + end + + local tile = self.tiles[realgid] + local data = { + id = tile.id, + gid = gid, + tileset = tile.tileset, + frame = tile.frame, + time = tile.time, + width = tile.width, + height = tile.height, + offset = tile.offset, + quad = tile.quad, + properties = tile.properties, + terrain = tile.terrain, + animation = tile.animation, + sx = tile.sx, + sy = tile.sy, + r = tile.r, + } + + if flipX then + if flipY and flipD then + data.r = math.rad(-90) + data.sy = -1 + elseif flipY then + data.sx = -1 + data.sy = -1 + elseif flipD then + data.r = math.rad(90) + else + data.sx = -1 + end + elseif flipY then + if flipD then + data.r = math.rad(-90) + else + data.sy = -1 + end + elseif flipD then + data.r = math.rad(90) + data.sy = -1 + end + + self.tiles[gid] = data + + return self.tiles[gid] +end + +--- Get custom properties from Layer +-- @param layer The Layer +-- @return table List of properties +function Map:getLayerProperties(layer) + local l = self.layers[layer] + + if not l then + return {} + end + + return l.properties +end + +--- Get custom properties from Tile +-- @param layer The Layer that the Tile belongs to +-- @param x The X axis location of the Tile (in tiles) +-- @param y The Y axis location of the Tile (in tiles) +-- @return table List of properties +function Map:getTileProperties(layer, x, y) + local tile = self.layers[layer].data[y][x] + + if not tile then + return {} + end + + return tile.properties +end + +--- Get custom properties from Object +-- @param layer The Layer that the Object belongs to +-- @param object The index or name of the Object +-- @return table List of properties +function Map:getObjectProperties(layer, object) + local o = self.layers[layer].objects + + if type(object) == "number" then + o = o[object] + else + for _, v in ipairs(o) do + if v.name == object then + o = v + break + end + end + end + + if not o then + return {} + end + + return o.properties +end + +--- Change a tile in a layer to another tile +-- @param layer The Layer that the Tile belongs to +-- @param x The X axis location of the Tile (in tiles) +-- @param y The Y axis location of the Tile (in tiles) +-- @param gid The gid of the new tile +function Map:setLayerTile(layer, x, y, gid) + layer = self.layers[layer] + + layer.data[y] = layer.data[y] or {} + local tile = layer.data[y][x] + local instance + if tile then + local tileX, tileY = self:getLayerTilePosition(layer, tile, x, y) + for _, inst in pairs(self.tileInstances[tile.gid]) do + if inst.x == tileX and inst.y == tileY then + instance = inst + break + end + end + end + + if tile == self.tiles[gid] then + return + end + + tile = self.tiles[gid] + + if instance then + self:swapTile(instance, tile) + else + self:addNewLayerTile(layer, tile, x, y) + end + layer.data[y][x] = tile +end + +--- Swap a tile in a spritebatch +-- @param instance The current Instance object we want to replace +-- @param tile The Tile object we want to use +-- @return none +function Map:swapTile(instance, tile) + -- Update sprite batch + if instance.batch then + if tile then + instance.batch:set( + instance.id, + tile.quad, + instance.x, + instance.y, + tile.r, + tile.sx, + tile.sy + ) + else + instance.batch:set( + instance.id, + instance.x, + instance.y, + 0, + 0) + + self.freeBatchSprites[instance.batch] = + self.freeBatchSprites[instance.batch] or {} + + table.insert(self.freeBatchSprites[instance.batch], instance) + end + end + + -- Remove old tile instance + for i, ins in ipairs(self.tileInstances[instance.gid]) do + if ins.batch == instance.batch and ins.id == instance.id then + table.remove(self.tileInstances[instance.gid], i) + break + end + end + + -- Add new tile instance + if tile then + self.tileInstances[tile.gid] = self.tileInstances[tile.gid] or {} + + local freeBatchSprites = self.freeBatchSprites[instance.batch] + local newInstance + if freeBatchSprites and #freeBatchSprites > 0 then + newInstance = freeBatchSprites[#freeBatchSprites] + freeBatchSprites[#freeBatchSprites] = nil + else + newInstance = {} + end + + newInstance.layer = instance.layer + newInstance.batch = instance.batch + newInstance.id = instance.id + newInstance.gid = tile.gid or 0 + newInstance.x = instance.x + newInstance.y = instance.y + newInstance.r = tile.r or 0 + newInstance.oy = tile.r ~= 0 and tile.height or 0 + table.insert(self.tileInstances[tile.gid], newInstance) + end +end + +--- Convert tile location to pixel location +-- @param x The X axis location of the point (in tiles) +-- @param y The Y axis location of the point (in tiles) +-- @return number The X axis location of the point (in pixels) +-- @return number The Y axis location of the point (in pixels) +function Map:convertTileToPixel(x,y) + if self.orientation == "orthogonal" then + local tileW = self.tilewidth + local tileH = self.tileheight + return + x * tileW, + y * tileH + elseif self.orientation == "isometric" then + local mapH = self.height + local tileW = self.tilewidth + local tileH = self.tileheight + local offsetX = mapH * tileW / 2 + return + (x - y) * tileW / 2 + offsetX, + (x + y) * tileH / 2 + elseif self.orientation == "staggered" or + self.orientation == "hexagonal" then + local tileW = self.tilewidth + local tileH = self.tileheight + local sideLen = self.hexsidelength or 0 + + if self.staggeraxis == "x" then + return + x * tileW, + ceil(y) * (tileH + sideLen) + (ceil(y) % 2 == 0 and tileH or 0) + else + return + ceil(x) * (tileW + sideLen) + (ceil(x) % 2 == 0 and tileW or 0), + y * tileH + end + end +end + +--- Convert pixel location to tile location +-- @param x The X axis location of the point (in pixels) +-- @param y The Y axis location of the point (in pixels) +-- @return number The X axis location of the point (in tiles) +-- @return number The Y axis location of the point (in tiles) +function Map:convertPixelToTile(x, y) + if self.orientation == "orthogonal" then + local tileW = self.tilewidth + local tileH = self.tileheight + return + x / tileW, + y / tileH + elseif self.orientation == "isometric" then + local mapH = self.height + local tileW = self.tilewidth + local tileH = self.tileheight + local offsetX = mapH * tileW / 2 + return + y / tileH + (x - offsetX) / tileW, + y / tileH - (x - offsetX) / tileW + elseif self.orientation == "staggered" then + local staggerX = self.staggeraxis == "x" + local even = self.staggerindex == "even" + + local function topLeft(x, y) + if staggerX then + if ceil(x) % 2 == 1 and even then + return x - 1, y + else + return x - 1, y - 1 + end + else + if ceil(y) % 2 == 1 and even then + return x, y - 1 + else + return x - 1, y - 1 + end + end + end + + local function topRight(x, y) + if staggerX then + if ceil(x) % 2 == 1 and even then + return x + 1, y + else + return x + 1, y - 1 + end + else + if ceil(y) % 2 == 1 and even then + return x + 1, y - 1 + else + return x, y - 1 + end + end + end + + local function bottomLeft(x, y) + if staggerX then + if ceil(x) % 2 == 1 and even then + return x - 1, y + 1 + else + return x - 1, y + end + else + if ceil(y) % 2 == 1 and even then + return x, y + 1 + else + return x - 1, y + 1 + end + end + end + + local function bottomRight(x, y) + if staggerX then + if ceil(x) % 2 == 1 and even then + return x + 1, y + 1 + else + return x + 1, y + end + else + if ceil(y) % 2 == 1 and even then + return x + 1, y + 1 + else + return x, y + 1 + end + end + end + + local tileW = self.tilewidth + local tileH = self.tileheight + + if staggerX then + x = x - (even and tileW / 2 or 0) + else + y = y - (even and tileH / 2 or 0) + end + + local halfH = tileH / 2 + local ratio = tileH / tileW + local referenceX = ceil(x / tileW) + local referenceY = ceil(y / tileH) + local relativeX = x - referenceX * tileW + local relativeY = y - referenceY * tileH + + if (halfH - relativeX * ratio > relativeY) then + return topLeft(referenceX, referenceY) + elseif (-halfH + relativeX * ratio > relativeY) then + return topRight(referenceX, referenceY) + elseif (halfH + relativeX * ratio < relativeY) then + return bottomLeft(referenceX, referenceY) + elseif (halfH * 3 - relativeX * ratio < relativeY) then + return bottomRight(referenceX, referenceY) + end + + return referenceX, referenceY + elseif self.orientation == "hexagonal" then + local staggerX = self.staggeraxis == "x" + local even = self.staggerindex == "even" + local tileW = self.tilewidth + local tileH = self.tileheight + local sideLenX = 0 + local sideLenY = 0 + + if staggerX then + sideLenX = self.hexsidelength + x = x - (even and tileW or (tileW - sideLenX) / 2) + else + sideLenY = self.hexsidelength + y = y - (even and tileH or (tileH - sideLenY) / 2) + end + + local colW = ((tileW - sideLenX) / 2) + sideLenX + local rowH = ((tileH - sideLenY) / 2) + sideLenY + local referenceX = ceil(x) / (colW * 2) + local referenceY = ceil(y) / (rowH * 2) + local relativeX = x - referenceX * colW * 2 + local relativeY = y - referenceY * rowH * 2 + local centers + + if staggerX then + local left = sideLenX / 2 + local centerX = left + colW + local centerY = tileH / 2 + + centers = { + { x = left, y = centerY }, + { x = centerX, y = centerY - rowH }, + { x = centerX, y = centerY + rowH }, + { x = centerX + colW, y = centerY }, + } + else + local top = sideLenY / 2 + local centerX = tileW / 2 + local centerY = top + rowH + + centers = { + { x = centerX, y = top }, + { x = centerX - colW, y = centerY }, + { x = centerX + colW, y = centerY }, + { x = centerX, y = centerY + rowH } + } + end + + local nearest = 0 + local minDist = math.huge + + local function len2(ax, ay) + return ax * ax + ay * ay + end + + for i = 1, 4 do + local dc = len2(centers[i].x - relativeX, centers[i].y - relativeY) + + if dc < minDist then + minDist = dc + nearest = i + end + end + + local offsetsStaggerX = { + { x = 0, y = 0 }, + { x = 1, y = -1 }, + { x = 1, y = 0 }, + { x = 2, y = 0 }, + } + + local offsetsStaggerY = { + { x = 0, y = 0 }, + { x = -1, y = 1 }, + { x = 0, y = 1 }, + { x = 0, y = 2 }, + } + + local offsets = staggerX and offsetsStaggerX or offsetsStaggerY + + return + referenceX + offsets[nearest].x, + referenceY + offsets[nearest].y + end +end + +--- A list of individual layers indexed both by draw order and name +-- @table Map.layers +-- @see TileLayer +-- @see ObjectLayer +-- @see ImageLayer +-- @see CustomLayer + +--- A list of individual tiles indexed by Global ID +-- @table Map.tiles +-- @see Tile +-- @see Map.tileInstances + +--- A list of tile instances indexed by Global ID +-- @table Map.tileInstances +-- @see TileInstance +-- @see Tile +-- @see Map.tiles + +--- A list of no-longer-used batch sprites, indexed by batch +--@table Map.freeBatchSprites + +--- A list of individual objects indexed by Global ID +-- @table Map.objects +-- @see Object + +--- @table TileLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field width Width of layer (in tiles) +-- @field height Height of layer (in tiles) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field data A tileWo dimensional table filled with individual tiles indexed by [y][x] (in tiles) +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers +-- @see Tile + +--- @table ObjectLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field objects List of objects indexed by draw order +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers +-- @see Object + +--- @table ImageLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field image Image to be drawn +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers + +--- Custom Layers are used to place userdata such as sprites within the draw order of the map. +-- @table CustomLayer +-- @field name The name of the layer +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @field visible Toggle if layer is visible or hidden +-- @field opacity Opacity of layer +-- @field properties Custom properties +-- @field update Update function +-- @field draw Draw function +-- @see Map.layers +-- @usage +-- -- Create a Custom Layer +-- local spriteLayer = map:addCustomLayer("Sprite Layer", 3) +-- +-- -- Add data to Custom Layer +-- spriteLayer.sprites = { +-- player = { +-- image = lg.newImage("assets/sprites/player.png"), +-- x = 64, +-- y = 64, +-- r = 0, +-- } +-- } +-- +-- -- Update callback for Custom Layer +-- function spriteLayer:update(dt) +-- for _, sprite in pairs(self.sprites) do +-- sprite.r = sprite.r + math.rad(90 * dt) +-- end +-- end +-- +-- -- Draw callback for Custom Layer +-- function spriteLayer:draw() +-- for _, sprite in pairs(self.sprites) do +-- local x = math.floor(sprite.x) +-- local y = math.floor(sprite.y) +-- local r = sprite.r +-- lg.draw(sprite.image, x, y, r) +-- end +-- end + +--- @table Tile +-- @field id Local ID within Tileset +-- @field gid Global ID +-- @field tileset Tileset ID +-- @field quad Quad object +-- @field properties Custom properties +-- @field terrain Terrain data +-- @field animation Animation data +-- @field frame Current animation frame +-- @field time Time spent on current animation frame +-- @field width Width of tile +-- @field height Height of tile +-- @field sx Scale value on the X axis +-- @field sy Scale value on the Y axis +-- @field r Rotation of tile (in radians) +-- @field offset Offset drawing position +-- @field offset.x Offset value on the X axis +-- @field offset.y Offset value on the Y axis +-- @see Map.tiles + +--- @table TileInstance +-- @field batch Spritebatch the Tile Instance belongs to +-- @field id ID within the spritebatch +-- @field gid Global ID +-- @field x Position on the X axis (in pixels) +-- @field y Position on the Y axis (in pixels) +-- @see Map.tileInstances +-- @see Tile + +--- @table Object +-- @field id Global ID +-- @field name Name of object (non-unique) +-- @field shape Shape of object +-- @field x Position of object on X axis (in pixels) +-- @field y Position of object on Y axis (in pixels) +-- @field width Width of object (in pixels) +-- @field height Heigh tof object (in pixels) +-- @field rotation Rotation of object (in radians) +-- @field visible Toggle if object is visible or hidden +-- @field properties Custom properties +-- @field ellipse List of verticies of specific shape +-- @field rectangle List of verticies of specific shape +-- @field polygon List of verticies of specific shape +-- @field polyline List of verticies of specific shape +-- @see Map.objects + +return setmetatable({}, STI) diff --git a/imperium-porcorum.love/core/modules/world/libs/sti/plugins/box2d.lua b/imperium-porcorum.love/core/modules/world/libs/sti/plugins/box2d.lua new file mode 100644 index 0000000..6d2e1b4 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/sti/plugins/box2d.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/sti/plugins/bump.lua b/imperium-porcorum.love/core/modules/world/libs/sti/plugins/bump.lua new file mode 100644 index 0000000..d69ff26 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/sti/plugins/bump.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/sti/utils.lua b/imperium-porcorum.love/core/modules/world/libs/sti/utils.lua new file mode 100644 index 0000000..9f8839a --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/sti/utils.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/libs/tsort.lua b/imperium-porcorum.love/core/modules/world/libs/tsort.lua new file mode 100644 index 0000000..3f8dfc9 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/libs/tsort.lua @@ -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 diff --git a/imperium-porcorum.love/core/modules/world/world2D.lua b/imperium-porcorum.love/core/modules/world/world2D.lua new file mode 100644 index 0000000..7375c46 --- /dev/null +++ b/imperium-porcorum.love/core/modules/world/world2D.lua @@ -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 diff --git a/imperium-porcorum.love/core/options.lua b/imperium-porcorum.love/core/options.lua index 57111c9..361d211 100644 --- a/imperium-porcorum.love/core/options.lua +++ b/imperium-porcorum.love/core/options.lua @@ -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() diff --git a/imperium-porcorum.love/core/scenemanager.lua b/imperium-porcorum.love/core/scenemanager.lua index 5d403ca..b1a8af6 100644 --- a/imperium-porcorum.love/core/scenemanager.lua +++ b/imperium-porcorum.love/core/scenemanager.lua @@ -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 diff --git a/imperium-porcorum.love/core/screen.lua b/imperium-porcorum.love/core/screen.lua index 4e962a8..ec3e6e0 100644 --- a/imperium-porcorum.love/core/screen.lua +++ b/imperium-porcorum.love/core/screen.lua @@ -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 diff --git a/imperium-porcorum.love/core/utils/filesystem.lua b/imperium-porcorum.love/core/utils/filesystem.lua new file mode 100644 index 0000000..ce99fbb --- /dev/null +++ b/imperium-porcorum.love/core/utils/filesystem.lua @@ -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 diff --git a/imperium-porcorum.love/core/utils/graphics.lua b/imperium-porcorum.love/core/utils/graphics.lua new file mode 100644 index 0000000..925ddbe --- /dev/null +++ b/imperium-porcorum.love/core/utils/graphics.lua @@ -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 diff --git a/imperium-porcorum.love/core/utils/init.lua b/imperium-porcorum.love/core/utils/init.lua new file mode 100644 index 0000000..765016a --- /dev/null +++ b/imperium-porcorum.love/core/utils/init.lua @@ -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") +} diff --git a/imperium-porcorum.love/core/utils/math.lua b/imperium-porcorum.love/core/utils/math.lua new file mode 100644 index 0000000..fea382b --- /dev/null +++ b/imperium-porcorum.love/core/utils/math.lua @@ -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