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