mirror of
https://github.com/Reuh/ubiquitousse.git
synced 2025-10-27 09:09:30 +00:00
Code reorganization, added uqt.ecs, removed LÖVE duplicates (uqt.audio, uqt.draw, uqt.filesystem)
This commit is contained in:
parent
523c5d36c0
commit
16e533d176
28 changed files with 2544 additions and 2107 deletions
91
asset.lua
91
asset.lua
|
|
@ -1,91 +0,0 @@
|
||||||
-- ubiquitousse.asset
|
|
||||||
|
|
||||||
-- The asset cache. Each cached asset is indexed with a string key "type.assetName".
|
|
||||||
local cache = setmetatable({}, { __mode = "v" }) -- weak values
|
|
||||||
|
|
||||||
--- Asset manager. Loads asset and cache them.
|
|
||||||
-- This file has no dependicy to either ubiquitousse or a ubiquitousse backend.
|
|
||||||
-- This only provides a streamlined way to handle asset, and doesn't handle the actual file loading/object creation itself; you are expected to provide your own asset loaders.
|
|
||||||
-- See asset.load for more details. Hopefully this will allow you to use asset which are more game-specific than "image" or "audio".
|
|
||||||
local asset
|
|
||||||
asset = setmetatable({
|
|
||||||
--- A prefix for asset names
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
prefix = "",
|
|
||||||
|
|
||||||
--- Load (and cache) an asset.
|
|
||||||
-- Asset name are similar to Lua module names (directory separator is the dot . and no extention should be specified).
|
|
||||||
-- To load an asset, ubiquitousse will, in this order:
|
|
||||||
-- * try to load the directory loader: a file named loader.lua in the same directory as the asset we are trying to load
|
|
||||||
-- * try to load the asset-specific loader: a file in the same directory and with the same name (except with the .lua extension) as the asset we are trying to load
|
|
||||||
-- Loaders should return either:
|
|
||||||
-- * the new asset
|
|
||||||
-- * nil, message if there was an error loading the asset
|
|
||||||
-- These loaders have acces to the following variables:
|
|
||||||
-- * directory: the asset directory (including prefix)
|
|
||||||
-- * name: the asset name (directory information removed)
|
|
||||||
-- * asset: the asset data. May be nil if this is the first loader to run.
|
|
||||||
-- @tparam assetName string the asset's full name
|
|
||||||
-- @return the asset
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
load = function(assetName)
|
|
||||||
if not cache[assetName] then
|
|
||||||
-- Get directory and name
|
|
||||||
local path, name = assetName:match("^([^.]+)%.(.+)$")
|
|
||||||
if not path then
|
|
||||||
path, name = "", assetName
|
|
||||||
end
|
|
||||||
local dir = (asset.prefix..path):gsub("%.", "/")
|
|
||||||
|
|
||||||
-- Setup env
|
|
||||||
local oName, oAsset, oDirectory = _G.name, _G.asset, _G.directory
|
|
||||||
_G.name, _G.asset, _G.directory = name, nil, dir
|
|
||||||
|
|
||||||
-- Load
|
|
||||||
local err = ("couldn't load asset %q:"):format(assetName)
|
|
||||||
|
|
||||||
-- Asset directory loader
|
|
||||||
local f = io.open(dir.."/loader.lua")
|
|
||||||
if f then
|
|
||||||
f:close()
|
|
||||||
local r, msg = dofile(dir.."/loader.lua")
|
|
||||||
if r ~= nil then
|
|
||||||
_G.asset = r
|
|
||||||
else
|
|
||||||
err = err .. ("\n\t* directory loader %q failed to load the asset: %s"):format(dir.."/loader.lua", msg)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
err = err .. ("\n\t* no directory loader %q found"):format(dir.."/loader.lua")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Asset specific loader
|
|
||||||
local f = io.open(dir.."/"..name..".lua")
|
|
||||||
if f then
|
|
||||||
f:close()
|
|
||||||
local r, msg = dofile(dir.."/"..name..".lua")
|
|
||||||
if r ~= nil then
|
|
||||||
_G.asset = r
|
|
||||||
else
|
|
||||||
err = err .. ("\n\t* asset specific loader %q failed to load the asset: %s"):format(dir.."/"..name..".lua", msg)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
err = err .. ("\n\t* no asset specific loader %q found"):format(dir.."/"..name..".lua")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Done
|
|
||||||
cache[assetName] = assert(_G.asset, err)
|
|
||||||
|
|
||||||
-- Restore env
|
|
||||||
_G.name, _G.asset, _G.directory = oName, oAsset, oDirectory
|
|
||||||
end
|
|
||||||
|
|
||||||
return cache[assetName]
|
|
||||||
end,
|
|
||||||
}, {
|
|
||||||
--- asset(...) is a shortcut for asset.load(...)
|
|
||||||
__call = function(self, ...)
|
|
||||||
return asset.load(...)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
return asset
|
|
||||||
86
asset/asset.lua
Normal file
86
asset/asset.lua
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
--- ubiquitousse.asset
|
||||||
|
-- No dependencies.
|
||||||
|
|
||||||
|
--- Asset manager. Loads asset and cache them.
|
||||||
|
-- This file has no dependency to either ubiquitousse or a ubiquitousse backend.
|
||||||
|
-- This only provides a streamlined way to handle asset, and doesn't handle the actual file loading/object creation itself; you are expected to provide your own asset loaders.
|
||||||
|
-- See the __call method for more details on how assets are loaded. Hopefully this will allow you to use asset which are more game-specific than "image" or "audio".
|
||||||
|
local asset_mt = {
|
||||||
|
--- Load (and cache) an asset.
|
||||||
|
-- Asset name are similar to Lua module names (directory separator is the dot . and no extention should be specified).
|
||||||
|
-- To load an asset, ubiquitousse will try every loaders in the loader list with a name that prefix the asset name.
|
||||||
|
-- The first value returned will be used as the asset value.
|
||||||
|
-- Loaders are called with the arguments:
|
||||||
|
-- * path: the asset full path, except extension
|
||||||
|
-- * ...: other arguments given after the asset name. Can only be number and strings.
|
||||||
|
-- @tparam assetName string the asset's full name
|
||||||
|
-- @tparam ... number/string other arguments for the asset loader
|
||||||
|
-- @return the asset
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
__call = function(self, assetName, ...)
|
||||||
|
local cache = self.cache
|
||||||
|
local hash = table.concat({assetName, ...}, ".")
|
||||||
|
|
||||||
|
if not cache[hash] then
|
||||||
|
for prefix, fn in pairs(self.loaders) do
|
||||||
|
if assetName:match("^"..prefix) then
|
||||||
|
cache[hash] = fn((self.prefix..assetName):gsub("%.", "/"), ...)
|
||||||
|
if cache[hash] then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert(cache[hash], ("couldn't load asset %q"):format(assetName))
|
||||||
|
end
|
||||||
|
|
||||||
|
return cache[hash]
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Preload a list of assets.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
load = function(self, list)
|
||||||
|
for _, asset in ipairs(list) do
|
||||||
|
self(asset)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Allow loaded assets to be garbage collected.
|
||||||
|
-- Only useful if the caching mode is set to "manual" duritng creation.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
clear = function(self)
|
||||||
|
self.cache = {}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
asset_mt.__index = asset_mt
|
||||||
|
|
||||||
|
local asset = {
|
||||||
|
--- Create a new asset manager.
|
||||||
|
-- If the caching "mode" is set to auto (default), the asset manager will allow assets to be automaticaly garbage collected by Lua.
|
||||||
|
-- If set to "manual", the assets will not be garbage collected unless the clear method is called.
|
||||||
|
-- "manual" mode is useful if you have assets that are particularly slow to load and you want full control on when they are loaded and unloaded (typically a loading screen).
|
||||||
|
-- @tparam directory string the directory in which the assets will be loaded
|
||||||
|
-- @tparam loaders table loaders table: {prefix = function, ...}
|
||||||
|
-- @tparam mode string[opt="auto"] caching mode
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
new = function(dir, loaders, mode)
|
||||||
|
local cache = {}
|
||||||
|
if mode == nil or mode == "auto" then
|
||||||
|
setmetatable(cache, { __mode = "v" })
|
||||||
|
end
|
||||||
|
return setmetatable({
|
||||||
|
--- A prefix for asset names
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
prefix = dir..".",
|
||||||
|
|
||||||
|
--- The asset cache. Each cached asset is indexed with a string key "type.assetName".
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
cache = cache,
|
||||||
|
|
||||||
|
--- The loaders table.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
loaders = loaders
|
||||||
|
}, asset_mt)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
return asset
|
||||||
1
asset/init.lua
Normal file
1
asset/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
return require((...)..".asset")
|
||||||
12
audio.lua
12
audio.lua
|
|
@ -1,12 +0,0 @@
|
||||||
-- ubiquitousse.audio
|
|
||||||
|
|
||||||
--- Audio functions.
|
|
||||||
return {
|
|
||||||
--- Loads an audio file and returns the corresponding audio object.
|
|
||||||
-- TODO: audio object doc & API
|
|
||||||
-- @impl backend
|
|
||||||
load = function(filepath) end,
|
|
||||||
|
|
||||||
-- TODO: doc
|
|
||||||
available = false
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +1,10 @@
|
||||||
--- ctrµLua backend 0.0.1 for Ubiquitousse.
|
|
||||||
-- Provides a partial Ubiquitousse API. Still a lot to implement.
|
|
||||||
-- Made for some ctrµLua version and Ubiquitousse 0.0.1.
|
|
||||||
-- See `ubiquitousse` for Ubiquitousse API.
|
|
||||||
|
|
||||||
-- General
|
|
||||||
local version = "0.0.1"
|
|
||||||
|
|
||||||
-- Require stuff
|
|
||||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||||
local ctr = require("ctr")
|
local ctr = require("ctr")
|
||||||
local gfx = require("ctr.gfx")
|
local gfx = require("ctr.gfx")
|
||||||
local hid = require("ctr.hid")
|
|
||||||
|
|
||||||
-- Version compatibility warning
|
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
||||||
do
|
|
||||||
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
|
||||||
if actualVersion ~= expectedVersion then
|
if actualVersion ~= expectedVersion then
|
||||||
local txt = ("Ubiquitousse ctrµLua backend version "..version.." was made for %s %s but %s is used!\nThings may not work as expected.")
|
local txt = ("Ubiquitousse ctrµLua backend was made for %s %s but %s is used!\nThings may not work as expected.")
|
||||||
:format(stuffName, expectedVersion, actualVersion)
|
:format(stuffName, expectedVersion, actualVersion)
|
||||||
print(txt)
|
print(txt)
|
||||||
for _=0,300 do
|
for _=0,300 do
|
||||||
|
|
@ -26,309 +14,6 @@ do
|
||||||
gfx.render()
|
gfx.render()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
checkCompat("ctrµLua", "v1.0", ctr.version) -- not really a version, just get the latest build
|
|
||||||
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Redefine all functions in tbl which also are in toAdd, so when used they call the old function (in tbl) and then the new (in toAdd).
|
|
||||||
-- Functions with names prefixed by a exclamation mark will overwrite the old function.
|
|
||||||
local function add(tbl, toAdd)
|
|
||||||
for k,v in pairs(toAdd) do
|
|
||||||
local old = tbl[k]
|
|
||||||
if k:sub(1,1) == "!" then
|
|
||||||
tbl[k] = v
|
|
||||||
else
|
|
||||||
tbl[k] = function(...)
|
|
||||||
old(...)
|
|
||||||
return v(...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt
|
|
||||||
uqt.backend = "ctrulua"
|
|
||||||
|
|
||||||
-- uqt.event: TODO
|
|
||||||
if uqt.event then
|
|
||||||
error("uqt.event: NYI")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.draw: TODO
|
|
||||||
if uqt.draw then
|
|
||||||
error("uqt.draw: NYI")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.audio: TODO
|
|
||||||
if uqt.audio then
|
|
||||||
error("uqt.audio: NYI")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.time
|
|
||||||
if uqt.time then
|
|
||||||
add(uqt.time, {
|
|
||||||
get = ctr.time
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.input
|
|
||||||
if uqt.input then
|
|
||||||
local keys = {}
|
|
||||||
local touchX, touchY, dTouchX, dTouchY
|
|
||||||
add(uqt.input, {
|
|
||||||
update = function()
|
|
||||||
hid.read()
|
|
||||||
|
|
||||||
keys = hid.keys()
|
|
||||||
|
|
||||||
local nTouchX, nTouchY = hid.touch()
|
|
||||||
dTouchX, dTouchY = nTouchX - touchX, nTouchY - touchY
|
|
||||||
touchX, touchY = nTouchX, nTouchY
|
|
||||||
end,
|
|
||||||
|
|
||||||
buttonDetector = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Keys
|
|
||||||
if id:match("^key%.") then
|
|
||||||
local key = id:match("^key%.(.+)$")
|
|
||||||
table.insert(ret, function()
|
|
||||||
return keys.held[key]
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
error("Unknown button identifier: "..id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return table.unpack(ret)
|
|
||||||
end,
|
|
||||||
|
|
||||||
axisDetector = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Binary axis
|
|
||||||
if id:match(".+%,.+") then
|
|
||||||
local d1, d2 = uqt.input.buttonDetector(id:match("^(.+)%,(.+)$"))
|
|
||||||
table.insert(ret, function()
|
|
||||||
local b1, b2 = d1(), d2()
|
|
||||||
if b1 and b2 then return 0
|
|
||||||
elseif b1 then return -1
|
|
||||||
elseif b2 then return 1
|
|
||||||
else return 0 end
|
|
||||||
end)
|
|
||||||
-- Touch movement
|
|
||||||
elseif id:match("^touch%.move%.") then
|
|
||||||
local axis, threshold = id:match("^touch%.move%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^touch%.move%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local val, raw, max
|
|
||||||
if axis == "x" then
|
|
||||||
raw, max = dTouchX, gfx.BOTTOM_WIDTH
|
|
||||||
elseif axis == "y" then
|
|
||||||
raw, max = dTouchY, gfx.BOTTOM_HEIGHT
|
|
||||||
end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- Touch position
|
|
||||||
elseif id:match("^touch%.position%.") then
|
|
||||||
local axis, threshold = id:match("^touch%.position%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^touch%.position%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local val, raw, max
|
|
||||||
if axis == "x" then
|
|
||||||
max = gfx.BOTTOM_WIDTH / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1])
|
|
||||||
raw = touchX - max
|
|
||||||
elseif axis == "y" then
|
|
||||||
max = gfx.BOTTOM_HEIGHT / 2
|
|
||||||
raw = touchY - max
|
|
||||||
end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- Circle pad axis
|
|
||||||
elseif id:match("^circle%.") then
|
|
||||||
local axis, threshold = id:match("^circle%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^circle%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local x, y = hid.circle()
|
|
||||||
local val, raw, max = 0, 0, 156
|
|
||||||
if axis == "x" then raw = x
|
|
||||||
elseif axis == "y" then raw = y end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- C-Stick axis
|
|
||||||
elseif id:match("^cstick%.") then
|
|
||||||
local axis, threshold = id:match("^cstick%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^cstick%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local x, y = hid.cstick()
|
|
||||||
local val, raw, max = 0, 0, 146
|
|
||||||
if axis == "x" then raw = x
|
|
||||||
elseif axis == "y" then raw = y end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- Accelerometer axis
|
|
||||||
elseif id:match("^accel%.") then
|
|
||||||
local axis, threshold = id:match("^accel%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^accel%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local x, y, z = hid.accel()
|
|
||||||
local val, raw, max = 0, 0, 32768 -- no idea actually, but it's a s16
|
|
||||||
if axis == "x" then raw = x
|
|
||||||
elseif axis == "y" then raw = y
|
|
||||||
elseif axis == "z" then raw = z end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- Gyroscope axis
|
|
||||||
elseif id:match("^gyro%.") then
|
|
||||||
local axis, threshold = id:match("^gyro%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^gyro%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local roll, pitch, yaw = hid.gyro()
|
|
||||||
local val, raw, max = 0, 0, 32768 -- no idea actually, but it's a s16
|
|
||||||
if axis == "roll" then raw = roll
|
|
||||||
elseif axis == "pitch" then raw = pitch
|
|
||||||
elseif axis == "yaw" then raw = yaw end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
error("Unknown axis identifier: "..id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return table.unpack(ret)
|
|
||||||
end,
|
|
||||||
|
|
||||||
buttonsInUse = function(threshold)
|
|
||||||
local r = {}
|
|
||||||
for key, held in pairs(keys.held) do
|
|
||||||
if held then table.insert(r, "key."..key) end
|
|
||||||
end
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
axesInUse = function(threshold)
|
|
||||||
local r = {}
|
|
||||||
threshold = threshold or 0.5
|
|
||||||
|
|
||||||
if math.abs(touchX) / gfx.BOTTOM_WIDTH > threshold then table.insert(r, "touch.position.x%"..threshold) end
|
|
||||||
if math.abs(touchY) / gfx.BOTTOM_HEIGHT > threshold then table.insert(r, "touch.position.y%"..threshold) end
|
|
||||||
|
|
||||||
if math.abs(dTouchX) / gfx.BOTTOM_WIDTH > threshold then table.insert(r, "touch.move.x%"..threshold) end
|
|
||||||
if math.abs(dTouchY) / gfx.BOTTOM_HEIGHT > threshold then table.insert(r, "touch.move.y%"..threshold) end
|
|
||||||
|
|
||||||
local circleX, circleY = hid.circle()
|
|
||||||
if math.abs(circleX) / 156 > threshold then table.insert(r, "circle.x%"..threshold) end
|
|
||||||
if math.abs(circleY) / 156 > threshold then table.insert(r, "circle.y%"..threshold) end
|
|
||||||
|
|
||||||
if ctr.apt.isNew3DS() then
|
|
||||||
local cstickX, cstickY = hid.cstick()
|
|
||||||
if math.abs(cstickY) / 146 > threshold then table.insert(r, "cstick.y%"..threshold) end
|
|
||||||
if math.abs(cstickX) / 146 > threshold then table.insert(r, "cstick.x%"..threshold) end
|
|
||||||
end
|
|
||||||
|
|
||||||
local accelX, accelY, accelZ = hid.accel()
|
|
||||||
if math.abs(accelX) / 32768 > threshold then table.insert(r, "accel.x%"..threshold) end
|
|
||||||
if math.abs(accelY) / 32768 > threshold then table.insert(r, "accel.y%"..threshold) end
|
|
||||||
if math.abs(accelZ) / 32768 > threshold then table.insert(r, "accel.z%"..threshold) end
|
|
||||||
|
|
||||||
-- no gyro, because it is always in use
|
|
||||||
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
buttonName = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Key
|
|
||||||
if id:match("^key%.") then
|
|
||||||
local key = id:match("^key%.(.+)$")
|
|
||||||
table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key")
|
|
||||||
else
|
|
||||||
table.insert(ret, id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return table.unpack(ret)
|
|
||||||
end,
|
|
||||||
|
|
||||||
axisName = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Binary axis
|
|
||||||
if id:match(".+%,.+") then
|
|
||||||
local b1, b2 = uqt.input.buttonName(id:match("^(.+)%,(.+)$"))
|
|
||||||
table.insert(ret, b1.." / "..b2)
|
|
||||||
-- Touch movement
|
|
||||||
elseif id:match("^touch%.move%.") then
|
|
||||||
local axis, threshold = id:match("^touch%.move%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^touch%.move%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, ("Touch %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
-- Touch position
|
|
||||||
elseif id:match("^touch%.position%.") then
|
|
||||||
local axis, threshold = id:match("^touch%.position%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^touch%.position%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, ("Touch %s position (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
-- Circle pad axis
|
|
||||||
elseif id:match("^circle%.") then
|
|
||||||
local axis, threshold = id:match("^circle%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^circle%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
if axis == "x" then
|
|
||||||
table.insert(ret, ("Circle pad horizontal axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
|
||||||
elseif axis == "y" then
|
|
||||||
table.insert(ret, ("Circle pad vertical axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
|
||||||
else
|
|
||||||
table.insert(ret, ("Circle pad %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
end
|
|
||||||
-- C-Stick axis
|
|
||||||
elseif id:match("^cstick%.") then
|
|
||||||
local axis, threshold = id:match("^cstick%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^cstick%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
if axis == "x" then
|
|
||||||
table.insert(ret, ("C-Stick horizontal axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
|
||||||
elseif axis == "y" then
|
|
||||||
table.insert(ret, ("C-Stick vertical axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
|
||||||
else
|
|
||||||
table.insert(ret, ("C-Stick %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
end
|
|
||||||
-- Accelerometer axis
|
|
||||||
elseif id:match("^accel%.") then
|
|
||||||
local axis, threshold = id:match("^accel%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^accel%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, ("Accelerometer %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
-- Gyroscope axis
|
|
||||||
elseif id:match("^gyro%.") then
|
|
||||||
local axis, threshold = id:match("^gyro%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^gyro%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, ("Gyroscope %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
else
|
|
||||||
table.insert(ret, id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return table.unpack(ret)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Defaults
|
|
||||||
uqt.input.default.pointer:bind(
|
|
||||||
{ "absolute", "key.left,key.right", "key.up,key.down" },
|
|
||||||
{ "absolute", "circle.x", "circle.y" }
|
|
||||||
)
|
|
||||||
uqt.input.default.confirm:bind("key.a")
|
|
||||||
uqt.input.default.cancel:bind("key.b")
|
|
||||||
end
|
end
|
||||||
|
checkCompat("ctrµLua", "v1.0", ctr.version) -- not really a version, just get the latest build
|
||||||
|
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
||||||
|
|
|
||||||
515
backend/love.lua
515
backend/love.lua
|
|
@ -1,518 +1,11 @@
|
||||||
--- Löve backend 0.0.1 for Ubiquitousse.
|
|
||||||
-- Provides all the Ubiquitousse API on a Löve environment.
|
|
||||||
-- Made for Löve 11.1.0 and Ubiquitousse 0.0.1.
|
|
||||||
-- See `ubiquitousse` for Ubiquitousse API.
|
|
||||||
|
|
||||||
-- Config
|
|
||||||
local useScancodes = true -- Use ScanCodes (layout independant input) instead of KeyConstants (layout dependant) for keyboard input
|
|
||||||
local displayKeyConstant = true -- If using ScanCodes, sets this to true so the backend returns the layout-dependant KeyConstant
|
|
||||||
-- instead of the raw ScanCode when getting the display name. If set to false and using ScanCodes,
|
|
||||||
-- the user will see keys that don't match what's actually written on his keyboard, which is confusing.
|
|
||||||
|
|
||||||
-- General
|
|
||||||
local version = "0.0.1"
|
|
||||||
|
|
||||||
-- Require stuff
|
|
||||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
||||||
local m = uqt.module
|
|
||||||
|
|
||||||
-- Version compatibility warning
|
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
||||||
do
|
|
||||||
local function checkCompat(stuffName, expectedVersion, actualVersion)
|
|
||||||
if actualVersion ~= expectedVersion then
|
if actualVersion ~= expectedVersion then
|
||||||
local txt = ("Ubiquitousse Löve backend version "..version.." was made for %s %s but %s is used!\nThings may not work as expected.")
|
local txt = ("Ubiquitousse Löve backend was made for %s %s but %s is used!\nThings may not work as expected.")
|
||||||
:format(stuffName, expectedVersion, actualVersion)
|
:format(stuffName, expectedVersion, actualVersion)
|
||||||
print(txt)
|
print(txt)
|
||||||
love.window.showMessageBox("Warning", txt, "warning")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
checkCompat("Löve", "11.1.0", ("%s.%s.%s"):format(love.getVersion()))
|
|
||||||
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Redefine all functions in tbl which also are in toAdd, so when used they call the old function (in tbl) and then the new (in toAdd).
|
|
||||||
-- Functions with names prefixed by a exclamation mark will overwrite the old function.
|
|
||||||
local function add(tbl, toAdd)
|
|
||||||
for k,v in pairs(toAdd) do
|
|
||||||
local old = tbl[k]
|
|
||||||
if k:sub(1,1) == "!" then
|
|
||||||
tbl[k] = v
|
|
||||||
else
|
|
||||||
tbl[k] = function(...)
|
|
||||||
old(...)
|
|
||||||
return v(...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
checkCompat("Löve", "11.3.0", ("%s.%s.%s"):format(love.getVersion()))
|
||||||
-- uqt
|
checkCompat("Ubiquitousse", "0.0.1", uqt.version)
|
||||||
uqt.backend = "love"
|
|
||||||
|
|
||||||
-- uqt.event
|
|
||||||
if m.event then
|
|
||||||
local updateDefault = uqt.event.update
|
|
||||||
uqt.event.update = function() end
|
|
||||||
function love.update(dt)
|
|
||||||
-- Stuff defined in ubiquitousse.lua
|
|
||||||
updateDefault(dt*1000)
|
|
||||||
|
|
||||||
-- Callback
|
|
||||||
uqt.event.update(dt)
|
|
||||||
end
|
|
||||||
|
|
||||||
local drawDefault = uqt.event.draw
|
|
||||||
uqt.event.draw = function() end
|
|
||||||
function love.draw()
|
|
||||||
if m.draw then
|
|
||||||
love.graphics.push()
|
|
||||||
|
|
||||||
-- Resize type
|
|
||||||
local winW, winH = love.graphics.getWidth(), love.graphics.getHeight()
|
|
||||||
local gameW, gameH = uqt.draw.params.width, uqt.draw.params.height
|
|
||||||
if uqt.draw.params.resizeType == "auto" then
|
|
||||||
love.graphics.scale(winW/gameW, winH/gameH)
|
|
||||||
elseif uqt.draw.params.resizeType == "center" then
|
|
||||||
love.graphics.translate(math.floor(winW/2-gameW/2), math.floor(winH/2-gameH/2))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Stuff defined in ubiquitousse.lua
|
|
||||||
drawDefault()
|
|
||||||
|
|
||||||
-- Callback
|
|
||||||
uqt.event.draw()
|
|
||||||
|
|
||||||
if m.draw then
|
|
||||||
love.graphics.pop()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.draw
|
|
||||||
if m.draw then
|
|
||||||
local defaultFont = love.graphics.getFont()
|
|
||||||
add(uqt.draw, {
|
|
||||||
init = function(params)
|
|
||||||
local p = uqt.draw.params
|
|
||||||
love.window.setTitle(p.title)
|
|
||||||
love.window.setMode(p.width, p.height, {
|
|
||||||
resizable = p.resizable
|
|
||||||
})
|
|
||||||
end,
|
|
||||||
fps = function()
|
|
||||||
return love.timer.getFPS()
|
|
||||||
end,
|
|
||||||
color = function(r, g, b, a)
|
|
||||||
love.graphics.setColor(r, g, b, a)
|
|
||||||
end,
|
|
||||||
text = function(x, y, text)
|
|
||||||
love.graphics.setFont(defaultFont)
|
|
||||||
love.graphics.print(text, x, y)
|
|
||||||
end,
|
|
||||||
point = function(x, y, ...)
|
|
||||||
love.graphics.points(x, y, ...)
|
|
||||||
end,
|
|
||||||
lineWidth = function(width)
|
|
||||||
love.graphics.setLineWidth(width)
|
|
||||||
end,
|
|
||||||
line = function(x1, y1, x2, y2, ...)
|
|
||||||
love.graphics.line(x1, y1, x2, y2, ...)
|
|
||||||
end,
|
|
||||||
polygon = function(...)
|
|
||||||
love.graphics.polygon("fill", ...)
|
|
||||||
end,
|
|
||||||
linedPolygon = function(...)
|
|
||||||
love.graphics.polygon("line", ...)
|
|
||||||
end,
|
|
||||||
["!rectangle"] = function(x, y, width, height)
|
|
||||||
love.graphics.rectangle("fill", x, y, width, height)
|
|
||||||
end,
|
|
||||||
["!linedRectangle"] = function(x, y, width, height)
|
|
||||||
love.graphics.rectangle("line", x, y, width, height)
|
|
||||||
end,
|
|
||||||
circle = function(x, y, radius)
|
|
||||||
love.graphics.circle("fill", x, y, radius)
|
|
||||||
end,
|
|
||||||
linedCircle = function(x, y, radius)
|
|
||||||
love.graphics.circle("line", x, y, radius)
|
|
||||||
end,
|
|
||||||
scissor = function(x, y, width, height)
|
|
||||||
love.graphics.setScissor(x, y, width, height)
|
|
||||||
end,
|
|
||||||
-- TODO: cf draw.lua
|
|
||||||
image = function(filename)
|
|
||||||
local img = love.graphics.newImage(filename)
|
|
||||||
return {
|
|
||||||
width = img:getWidth(),
|
|
||||||
height = img:getHeight(),
|
|
||||||
draw = function(self, x, y, r, sx, sy, ox, oy)
|
|
||||||
love.graphics.draw(img, x, y, r, sx, sy, ox, oy)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
font = function(filename, size)
|
|
||||||
local fnt = love.graphics.newFont(filename, size)
|
|
||||||
return {
|
|
||||||
width = function(self, text)
|
|
||||||
return fnt:getWidth(text)
|
|
||||||
end,
|
|
||||||
draw = function(self, text, x, y, r, sx, sy, ox, oy)
|
|
||||||
love.graphics.setFont(fnt)
|
|
||||||
love.graphics.print(text, x, y, r, sx, sy, ox, oy)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
function love.resize(width, height)
|
|
||||||
if uqt.draw.params.resizeType == "none" then
|
|
||||||
uqt.draw.width = width
|
|
||||||
uqt.draw.height = height
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif m.input then -- fields required by uqt.input
|
|
||||||
uqt.draw = {
|
|
||||||
width = love.graphics.getWidth(),
|
|
||||||
height = love.graphics.getHeight()
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.audio
|
|
||||||
if m.audio then
|
|
||||||
add(uqt.audio, {
|
|
||||||
-- TODO: cf audio.lua
|
|
||||||
load = function(filepath)
|
|
||||||
local audio = love.audio.newSource(filepath)
|
|
||||||
return {
|
|
||||||
play = function(self)
|
|
||||||
audio:play()
|
|
||||||
end
|
|
||||||
}
|
|
||||||
end
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.time
|
|
||||||
if m.time then
|
|
||||||
add(uqt.time, {
|
|
||||||
get = function()
|
|
||||||
return love.timer.getTime() * 1000
|
|
||||||
end
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
-- uqt.input
|
|
||||||
if m.input then
|
|
||||||
local buttonsInUse = {}
|
|
||||||
local axesInUse = {}
|
|
||||||
function love.keypressed(key, scancode, isrepeat)
|
|
||||||
if useScancodes then key = scancode end
|
|
||||||
buttonsInUse["keyboard."..key] = true
|
|
||||||
end
|
|
||||||
function love.keyreleased(key, scancode)
|
|
||||||
if useScancodes then key = scancode end
|
|
||||||
buttonsInUse["keyboard."..key] = nil
|
|
||||||
end
|
|
||||||
function love.mousepressed(x, y, button, istouch)
|
|
||||||
buttonsInUse["mouse."..button] = true
|
|
||||||
end
|
|
||||||
function love.mousereleased(x, y, button, istouch)
|
|
||||||
buttonsInUse["mouse."..button] = nil
|
|
||||||
end
|
|
||||||
function love.wheelmoved(x, y)
|
|
||||||
if y > 0 then
|
|
||||||
buttonsInUse["mouse.wheel.up"] = true
|
|
||||||
elseif y < 0 then
|
|
||||||
buttonsInUse["mouse.wheel.down"] = true
|
|
||||||
end
|
|
||||||
if x > 0 then
|
|
||||||
buttonsInUse["mouse.wheel.right"] = true
|
|
||||||
elseif x < 0 then
|
|
||||||
buttonsInUse["mouse.wheel.left"] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
function love.mousemoved(x, y, dx, dy)
|
|
||||||
if dx ~= 0 then axesInUse["mouse.move.x"] = dx/love.graphics.getWidth() end
|
|
||||||
if dy ~= 0 then axesInUse["mouse.move.y"] = dy/love.graphics.getHeight() end
|
|
||||||
end
|
|
||||||
function love.gamepadpressed(joystick, button)
|
|
||||||
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = true
|
|
||||||
end
|
|
||||||
function love.gamepadreleased(joystick, button)
|
|
||||||
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = nil
|
|
||||||
end
|
|
||||||
function love.gamepadaxis(joystick, axis, value)
|
|
||||||
if value ~= 0 then
|
|
||||||
axesInUse["gamepad.axis."..joystick:getID().."."..axis] = value
|
|
||||||
else
|
|
||||||
axesInUse["gamepad.axis."..joystick:getID().."."..axis] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
love.mouse.setVisible(false)
|
|
||||||
|
|
||||||
add(uqt.input, {
|
|
||||||
-- love.wheelmoved doesn't trigger when the wheel stop moving, so we need to clear up our stuff at each update
|
|
||||||
update = function()
|
|
||||||
buttonsInUse["mouse.wheel.up"] = nil
|
|
||||||
buttonsInUse["mouse.wheel.down"] = nil
|
|
||||||
buttonsInUse["mouse.wheel.right"] = nil
|
|
||||||
buttonsInUse["mouse.wheel.left"] = nil
|
|
||||||
-- Same for mouse axis
|
|
||||||
axesInUse["mouse.move.x"] = nil
|
|
||||||
axesInUse["mouse.move.y"] = nil
|
|
||||||
end,
|
|
||||||
|
|
||||||
buttonDetector = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Keyboard
|
|
||||||
if id:match("^keyboard%.") then
|
|
||||||
local key = id:match("^keyboard%.(.+)$")
|
|
||||||
table.insert(ret, function()
|
|
||||||
return useScancodes and love.keyboard.isScancodeDown(key) or love.keyboard.isDown(key)
|
|
||||||
end)
|
|
||||||
-- Mouse wheel
|
|
||||||
elseif id:match("^mouse%.wheel%.") then
|
|
||||||
local key = id:match("^mouse%.wheel%.(.+)$")
|
|
||||||
table.insert(ret, function()
|
|
||||||
return buttonsInUse["mouse.wheel."..key]
|
|
||||||
end)
|
|
||||||
-- Mouse
|
|
||||||
elseif id:match("^mouse%.") then
|
|
||||||
local key = id:match("^mouse%.(.+)$")
|
|
||||||
table.insert(ret, function()
|
|
||||||
return love.mouse.isDown(key)
|
|
||||||
end)
|
|
||||||
-- Gamepad button
|
|
||||||
elseif id:match("^gamepad%.button%.") then
|
|
||||||
local gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
|
|
||||||
gid = tonumber(gid)
|
|
||||||
table.insert(ret, function()
|
|
||||||
local gamepad
|
|
||||||
for _,j in ipairs(love.joystick.getJoysticks()) do
|
|
||||||
if j:getID() == gid then gamepad = j end
|
|
||||||
end
|
|
||||||
return gamepad and gamepad:isGamepadDown(key)
|
|
||||||
end)
|
|
||||||
-- Gamepad axis
|
|
||||||
elseif id:match("^gamepad%.axis%.") then
|
|
||||||
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
|
||||||
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
|
||||||
gid = tonumber(gid)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local gamepad
|
|
||||||
for _,j in ipairs(love.joystick.getJoysticks()) do
|
|
||||||
if j:getID() == gid then gamepad = j end
|
|
||||||
end
|
|
||||||
if not gamepad then
|
|
||||||
return false
|
|
||||||
else
|
|
||||||
local val = gamepad:getGamepadAxis(axis)
|
|
||||||
return (math.abs(val) > math.abs(threshold)) and ((val < 0) == (threshold < 0))
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
error("Unknown button identifier: "..id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return unpack(ret)
|
|
||||||
end,
|
|
||||||
|
|
||||||
axisDetector = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Binary axis
|
|
||||||
if id:match(".+%,.+") then
|
|
||||||
local d1, d2 = uqt.input.buttonDetector(id:match("^(.+)%,(.+)$"))
|
|
||||||
table.insert(ret, function()
|
|
||||||
local b1, b2 = d1(), d2()
|
|
||||||
if b1 and b2 then return 0
|
|
||||||
elseif b1 then return -1
|
|
||||||
elseif b2 then return 1
|
|
||||||
else return 0 end
|
|
||||||
end)
|
|
||||||
-- Mouse movement
|
|
||||||
elseif id:match("^mouse%.move%.") then
|
|
||||||
local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local val, raw, max = axesInUse["mouse.move."..axis] or 0, 0, 1
|
|
||||||
if axis == "x" then
|
|
||||||
raw, max = val * love.graphics.getWidth(), love.graphics.getWidth()
|
|
||||||
elseif axis == "y" then
|
|
||||||
raw, max = val * love.graphics.getHeight(), love.graphics.getHeight()
|
|
||||||
end
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- Mouse position
|
|
||||||
elseif id:match("^mouse%.position%.") then
|
|
||||||
local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local val, raw, max = 0, 0, 1
|
|
||||||
if axis == "x" then
|
|
||||||
max = love.graphics.getWidth() / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1])
|
|
||||||
raw = love.mouse.getX() - max
|
|
||||||
elseif axis == "y" then
|
|
||||||
max = love.graphics.getHeight() / 2
|
|
||||||
raw = love.mouse.getY() - max
|
|
||||||
end
|
|
||||||
val = raw / max
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
|
||||||
end)
|
|
||||||
-- Gamepad axis
|
|
||||||
elseif id:match("^gamepad%.axis%.") then
|
|
||||||
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
|
||||||
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
|
||||||
gid = tonumber(gid)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, function()
|
|
||||||
local gamepad
|
|
||||||
for _,j in ipairs(love.joystick.getJoysticks()) do
|
|
||||||
if j:getID() == gid then gamepad = j end
|
|
||||||
end
|
|
||||||
if not gamepad then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
local val = gamepad:getGamepadAxis(axis)
|
|
||||||
return math.abs(val) > math.abs(threshold) and val or 0
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
error("Unknown axis identifier: "..id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return unpack(ret)
|
|
||||||
end,
|
|
||||||
|
|
||||||
buttonsInUse = function(threshold)
|
|
||||||
local r = {}
|
|
||||||
threshold = threshold or 0.5
|
|
||||||
for b in pairs(buttonsInUse) do
|
|
||||||
table.insert(r, b)
|
|
||||||
end
|
|
||||||
for b,v in pairs(axesInUse) do
|
|
||||||
if math.abs(v) > threshold then
|
|
||||||
table.insert(r, b.."%"..(v < 0 and -threshold or threshold))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
axesInUse = function(threshold)
|
|
||||||
local r = {}
|
|
||||||
threshold = threshold or 0.5
|
|
||||||
for b,v in pairs(axesInUse) do
|
|
||||||
if math.abs(v) > threshold then
|
|
||||||
table.insert(r, b.."%"..threshold)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
buttonName = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Keyboard
|
|
||||||
if id:match("^keyboard%.") then
|
|
||||||
local key = id:match("^keyboard%.(.+)$")
|
|
||||||
if useScancodes and displayKeyConstant then key = love.keyboard.getKeyFromScancode(key) end
|
|
||||||
table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key")
|
|
||||||
-- Mouse wheel
|
|
||||||
elseif id:match("^mouse%.wheel%.") then
|
|
||||||
local key = id:match("^mouse%.wheel%.(.+)$")
|
|
||||||
table.insert(ret, "Mouse wheel "..key)
|
|
||||||
-- Mouse
|
|
||||||
elseif id:match("^mouse%.") then
|
|
||||||
local key = id:match("^mouse%.(.+)$")
|
|
||||||
table.insert(ret, "Mouse "..key)
|
|
||||||
-- Gamepad button
|
|
||||||
elseif id:match("^gamepad%.button%.") then
|
|
||||||
local gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
|
|
||||||
table.insert(ret, "Gamepad "..gid.." button "..key)
|
|
||||||
-- Gamepad axis
|
|
||||||
elseif id:match("^gamepad%.axis%.") then
|
|
||||||
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
|
||||||
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
if axis == "rightx" then
|
|
||||||
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
|
||||||
elseif axis == "righty" then
|
|
||||||
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
|
||||||
elseif axis == "leftx" then
|
|
||||||
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
|
||||||
elseif axis == "lefty" then
|
|
||||||
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
|
||||||
else
|
|
||||||
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
table.insert(ret, id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return unpack(ret)
|
|
||||||
end,
|
|
||||||
|
|
||||||
axisName = function(...)
|
|
||||||
local ret = {}
|
|
||||||
for _,id in ipairs({...}) do
|
|
||||||
-- Binary axis
|
|
||||||
if id:match(".+%,.+") then
|
|
||||||
local b1, b2 = uqt.input.buttonName(id:match("^(.+)%,(.+)$"))
|
|
||||||
table.insert(ret, b1.." / "..b2)
|
|
||||||
-- Mouse movement
|
|
||||||
elseif id:match("^mouse%.move%.") then
|
|
||||||
local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, ("Mouse %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
-- Mouse position
|
|
||||||
elseif id:match("^mouse%.position%.") then
|
|
||||||
local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$")
|
|
||||||
if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
table.insert(ret, ("Mouse %s position (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
|
||||||
-- Gamepad axis
|
|
||||||
elseif id:match("^gamepad%.axis%.") then
|
|
||||||
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
|
||||||
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
|
||||||
threshold = tonumber(threshold) or 0
|
|
||||||
if axis == "rightx" then
|
|
||||||
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
|
||||||
elseif axis == "righty" then
|
|
||||||
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
|
||||||
elseif axis == "leftx" then
|
|
||||||
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
|
||||||
elseif axis == "lefty" then
|
|
||||||
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
|
||||||
else
|
|
||||||
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
|
|
||||||
end
|
|
||||||
else
|
|
||||||
table.insert(ret, id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return unpack(ret)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Defaults
|
|
||||||
uqt.input.default.pointer:bind(
|
|
||||||
{ "absolute", "keyboard.left,keyboard.right", "keyboard.up,keyboard.down" },
|
|
||||||
{ "absolute", "keyboard.a,keyboard.d", "keyboard.w,keyboard.s" },
|
|
||||||
{ "absolute", "gamepad.axis.1.leftx", "gamepad.axis.1.lefty" },
|
|
||||||
{ "absolute", "gamepad.button.1.dpleft,gamepad.button.1.dpright", "gamepad.button.1.dpup,gamepad.button.1.dpdown"}
|
|
||||||
)
|
|
||||||
uqt.input.default.confirm:bind(
|
|
||||||
"keyboard.enter", "keyboard.space", "keyboard.lshift", "keyboard.e",
|
|
||||||
"gamepad.button.1.a"
|
|
||||||
)
|
|
||||||
uqt.input.default.cancel:bind(
|
|
||||||
"keyboard.escape", "keyboard.backspace",
|
|
||||||
"gamepad.button.1.b"
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
|
||||||
169
draw.lua
169
draw.lua
|
|
@ -1,169 +0,0 @@
|
||||||
-- ubiquitousse.draw
|
|
||||||
|
|
||||||
--- The drawing functions: everything that affect the display/window.
|
|
||||||
-- The coordinate system used is:
|
|
||||||
--
|
|
||||||
-- (0,0) +---> (x)
|
|
||||||
-- |
|
|
||||||
-- |
|
|
||||||
-- v (y)
|
|
||||||
--
|
|
||||||
-- x and y values can be float, so make sure to perform math.floor if your engine only support
|
|
||||||
-- integer coordinates.
|
|
||||||
--
|
|
||||||
-- Mostly plagiarized from the Löve API, with some parts from ctrµLua.
|
|
||||||
local draw
|
|
||||||
draw = {
|
|
||||||
--- Initial game view paramters (some defaults).
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
params = {
|
|
||||||
title = "Ubiquitousse Game",
|
|
||||||
width = 800,
|
|
||||||
height = 600,
|
|
||||||
resizable = false,
|
|
||||||
resizeType = "auto"
|
|
||||||
},
|
|
||||||
|
|
||||||
--- Setup the intial game view parameters.
|
|
||||||
-- If a parmeter is not set, a default value will be used.
|
|
||||||
-- This function is expected to be only called once, before doing any drawing operation.
|
|
||||||
-- @tparam table params the game parameters
|
|
||||||
-- @usage -- Default values:
|
|
||||||
-- ubiquitousse.init {
|
|
||||||
-- title = "Ubiquitousse Game", -- usually window title
|
|
||||||
-- width = 800, -- in px
|
|
||||||
-- height = 600, -- in px
|
|
||||||
-- resizable = false, -- can the game be resized?
|
|
||||||
-- resizeType = "auto" -- how to act on resize: "none" to do nothing (0,0 will be top-left)
|
|
||||||
-- "center" to autocenter (0,0 will be at windowWidth/2-gameWidth/2,windowHeight/2-gameHeight/2)
|
|
||||||
-- "auto" to automatically resize to the window size (coordinate system won't change)
|
|
||||||
-- }
|
|
||||||
-- @impl mixed
|
|
||||||
init = function(params)
|
|
||||||
for k, v in pairs(params) do
|
|
||||||
draw.params[k] = v
|
|
||||||
end
|
|
||||||
draw.width = params.width
|
|
||||||
draw.height = params.height
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Return the number of frames per second.
|
|
||||||
-- @treturn number the current FPS
|
|
||||||
-- @impl backend
|
|
||||||
fps = function() end,
|
|
||||||
|
|
||||||
--- Sets the drawing color
|
|
||||||
-- @tparam number r the red component (0-1)
|
|
||||||
-- @tparam number g the green component (0-1)
|
|
||||||
-- @tparam number b the blue component (0-1)
|
|
||||||
-- @tparam[opt=1] number a the alpha (opacity) component (0-1)
|
|
||||||
-- @impl backend
|
|
||||||
color = function(r, g, b, a) end,
|
|
||||||
|
|
||||||
--- Draws some text.
|
|
||||||
-- @tparam number x x top-left coordinate of the text
|
|
||||||
-- @tparam number y y top-left coordinate of the text
|
|
||||||
-- @tparam string text the text to draw. UTF-8 format, convert if needed.
|
|
||||||
-- @impl backend
|
|
||||||
text = function(x, y, text) end,
|
|
||||||
|
|
||||||
--- Draws a point.
|
|
||||||
-- @tparam number x point x coordinate
|
|
||||||
-- @tparam number y point y coordinate
|
|
||||||
-- @tparam number ... other vertices to draw other points
|
|
||||||
-- @impl backend
|
|
||||||
point = function(x, y, ...) end,
|
|
||||||
|
|
||||||
--- Sets the width.
|
|
||||||
-- @tparam number width the line width
|
|
||||||
-- @impl backend
|
|
||||||
lineWidth = function(width) end,
|
|
||||||
|
|
||||||
--- Draws a line.
|
|
||||||
-- @tparam number x1 line start x coordinate
|
|
||||||
-- @tparam number y1 line start y coordinate
|
|
||||||
-- @tparam number x2 line end x coordinate
|
|
||||||
-- @tparam number y2 line end y coordinate
|
|
||||||
-- @tparam number ... other vertices to continue drawing a polyline
|
|
||||||
-- @impl backend
|
|
||||||
line = function(x1, y1, x2, y2, ...) end,
|
|
||||||
|
|
||||||
--- Draws a filled polygon.
|
|
||||||
-- @tparam number x1,y1,x2,y2... the vertices of the polygon
|
|
||||||
-- @impl backend
|
|
||||||
polygon = function(...) end,
|
|
||||||
|
|
||||||
--- Draws a polygon outline.
|
|
||||||
-- @tparam number x1,y1,x2,y2... the vertices of the polygon
|
|
||||||
-- @impl backend
|
|
||||||
linedPolygon = function(...) end,
|
|
||||||
|
|
||||||
--- Draws a filled rectangle.
|
|
||||||
-- @tparam number x rectangle top-left x coordinate
|
|
||||||
-- @tparam number y rectangle top-left x coordinate
|
|
||||||
-- @tparam number width rectangle width
|
|
||||||
-- @tparam number height rectangle height
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
rectangle = function(x, y, width, height)
|
|
||||||
draw.polygon(x, y, x + width, y, x + width, y + height, x, y + height)
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Draws a rectangle outline.
|
|
||||||
-- @tparam number x rectangle top-left x coordinate
|
|
||||||
-- @tparam number y rectangle top-left x coordinate
|
|
||||||
-- @tparam number width rectangle width
|
|
||||||
-- @tparam number height rectangle height
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
linedRectangle = function(x, y, width, height)
|
|
||||||
draw.linedPolygon(x, y, x + width, y, x + width, y + height, x, y + height)
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Draws a filled circle.
|
|
||||||
-- @tparam number x center x coordinate
|
|
||||||
-- @tparam number y center x coordinate
|
|
||||||
-- @tparam number radius circle radius
|
|
||||||
-- @impl backend
|
|
||||||
circle = function(x, y, radius) end,
|
|
||||||
|
|
||||||
--- Draws a circle outline.
|
|
||||||
-- @tparam number x center x coordinate
|
|
||||||
-- @tparam number y center x coordinate
|
|
||||||
-- @tparam number radius circle radius
|
|
||||||
-- @impl backend
|
|
||||||
linedCircle = function(x, y, radius) end,
|
|
||||||
|
|
||||||
--- Enables the scissor test.
|
|
||||||
-- When enabled, every pixel drawn outside of the scissor rectangle is discarded.
|
|
||||||
-- When called withou arguments, it disables the scissor test.
|
|
||||||
-- @tparam number x rectangle top-left x coordinate
|
|
||||||
-- @tparam number y rectangle top-left x coordinate
|
|
||||||
-- @tparam number width rectangle width
|
|
||||||
-- @tparam number height rectangle height
|
|
||||||
-- @impl backend
|
|
||||||
scissor = function(x, y, width, height) end,
|
|
||||||
|
|
||||||
--- The drawing area width, in pixels.
|
|
||||||
-- @requiredby input
|
|
||||||
-- @impl backend
|
|
||||||
width = 800,
|
|
||||||
|
|
||||||
--- The drawing area height, in pixels.
|
|
||||||
-- @requiredby input
|
|
||||||
-- @impl backend
|
|
||||||
height = 600,
|
|
||||||
|
|
||||||
-- TODO: doc & api
|
|
||||||
push = function() end,
|
|
||||||
pop = function() end,
|
|
||||||
translate = function(x, y) end,
|
|
||||||
rotate = function(angle) end,
|
|
||||||
scale = function(sx, sy) end,
|
|
||||||
font = function(filename) end,
|
|
||||||
image = function(filename) end,
|
|
||||||
}
|
|
||||||
|
|
||||||
-- TODO: canvas stuff ; also make everything here actually be shortcut to draw to the game's framebuffer.
|
|
||||||
-- TODO: add software implementations of everything.
|
|
||||||
-- TODO: add function to draw a message (used eg for the error message when there is a version mismatch)
|
|
||||||
|
|
||||||
return draw
|
|
||||||
308
ecs/ecs.can
Normal file
308
ecs/ecs.can
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
--- ubiquitousse.ecs
|
||||||
|
-- No dependency.
|
||||||
|
|
||||||
|
--- Entity Component System library, inspired by the excellent tiny-ecs. Main differences include:
|
||||||
|
-- * ability to nest systems;
|
||||||
|
-- * instanciation of systems for each world;
|
||||||
|
-- * adding and removing entities is done instantaneously.
|
||||||
|
|
||||||
|
-- TODO: Implement a skip list for faster search.
|
||||||
|
|
||||||
|
--- Recursively remove subsystems from a system.
|
||||||
|
let recDestroySystems = (system)
|
||||||
|
for i=#system.systems, 1, -1 do
|
||||||
|
let s = system.systems[i]
|
||||||
|
recDestroySystems(s)
|
||||||
|
system.systems[i] = nil
|
||||||
|
if s.name then
|
||||||
|
system.world.s[s.name] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
--- Recursively call :clear and :onRemoveFromWorld to a list of systems in a world.
|
||||||
|
let recCallOnRemoveFromWorld = (world, systems)
|
||||||
|
for _, s in ipairs(systems) do
|
||||||
|
s:clear()
|
||||||
|
recCallOnRemoveFromWorld(world, s.systems)
|
||||||
|
s:onRemoveFromWorld(world)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Iterate through the next entity, based on state s: { previousLinkedListItem }
|
||||||
|
let nextEntity = (s)
|
||||||
|
if s[1] then
|
||||||
|
let var = s[1][1]
|
||||||
|
s[1] = s[1][2]
|
||||||
|
return var
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- System fields and methods.
|
||||||
|
-- When they are added to a world, a new, per-world self table is created and used for every method call (which we call "instancied system").
|
||||||
|
-- Instancied systems can be retrieved in system.s or system.systems.
|
||||||
|
-- Oh, the "world" is just the top-level system.
|
||||||
|
let system_mt = {
|
||||||
|
--- Read-only after creation system options ---
|
||||||
|
-- I mean, you can try to change them afterwards. But, heh.
|
||||||
|
|
||||||
|
--- Name of the system (optional).
|
||||||
|
-- Used to create a field with the system's name in world.system.
|
||||||
|
name = nil,
|
||||||
|
|
||||||
|
--- List of subsystems.
|
||||||
|
-- On a instancied system, this is a list of the same subsystems, but instancied for this world.
|
||||||
|
systems = nil,
|
||||||
|
|
||||||
|
--- Returns true if the entity should be added to this system (and therefore its subsystems).
|
||||||
|
-- By default, rejects everything.
|
||||||
|
filter = :(e) return false end,
|
||||||
|
--- Returns true if e1 <= e2.
|
||||||
|
compare = :(e1, e2) return true end,
|
||||||
|
|
||||||
|
--- Modifiable system options ---
|
||||||
|
|
||||||
|
--- Called when adding an entity to the system.
|
||||||
|
onAdd = :(e) end,
|
||||||
|
--- Called when removing an entity from the system.
|
||||||
|
onRemove = :(e) end,
|
||||||
|
--- Called when the system is added to a world.
|
||||||
|
onAddToWorld = :(world) end,
|
||||||
|
--- Called when the system is removed from a world (i.e., the world is destroyed).
|
||||||
|
onRemoveFromWorld = :(world) end,
|
||||||
|
--- Called when updating the system.
|
||||||
|
onUpdate = :(dt) end,
|
||||||
|
--- Called when drawing the system.
|
||||||
|
onDraw = :() end,
|
||||||
|
--- Called when updating the system, for every entity the system contains.
|
||||||
|
process = :(e, dt) end,
|
||||||
|
--- Called when drawing the system, for every entity the system contains.
|
||||||
|
render = :(e) end,
|
||||||
|
|
||||||
|
--- If set, the system will only update every interval seconds.
|
||||||
|
interval = nil,
|
||||||
|
--- The system and its susbsystems will only update if this is true.
|
||||||
|
active = true,
|
||||||
|
--- The system and its subsystems will only draw if this is true.
|
||||||
|
visible = true,
|
||||||
|
|
||||||
|
--- Read-only system options ---
|
||||||
|
|
||||||
|
--- The world the system belongs to.
|
||||||
|
world = nil,
|
||||||
|
--- Number of entities in the system.
|
||||||
|
entityCount = 0,
|
||||||
|
--- Map of named systems in the world (not only subsystems).
|
||||||
|
s = nil,
|
||||||
|
|
||||||
|
--- Private fields ---
|
||||||
|
|
||||||
|
--- First element of the linked list of entities.
|
||||||
|
_first = nil,
|
||||||
|
--- Amount of time waited since last update (if interval is set).
|
||||||
|
_waited = 0,
|
||||||
|
|
||||||
|
--- Methods ---
|
||||||
|
|
||||||
|
--- Add entities to the system and its subsystems.
|
||||||
|
-- If this is called on a subsystem instead of the world, be warned that this will bypass all the parent's systems filters.
|
||||||
|
-- Since :remove will not search for entities in systems where they should have been filtered out, the added entities will not be removed
|
||||||
|
-- when calling :remove on a parent system or the world. The entity can only be removed by calling :remove on the system :add was called on.
|
||||||
|
add = :(...)
|
||||||
|
for _, e in ipairs({...}) do
|
||||||
|
if @filter(e) then
|
||||||
|
if @_first == nil then
|
||||||
|
@_first = { e, nil }
|
||||||
|
elseif @compare(e, @_first[1]) then
|
||||||
|
@_first = { e, @_first }
|
||||||
|
else
|
||||||
|
let entity = @_first
|
||||||
|
while entity[2] ~= nil do
|
||||||
|
if @compare(e, entity[2][1]) then
|
||||||
|
entity[2] = { e, entity[2] }
|
||||||
|
break
|
||||||
|
end
|
||||||
|
entity = entity[2]
|
||||||
|
end
|
||||||
|
if entity[2] == nil then
|
||||||
|
entity[2] = { e, nil }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, s in ipairs(@systems) do
|
||||||
|
s:add(e)
|
||||||
|
end
|
||||||
|
@entityCount += 1
|
||||||
|
@onAdd(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ...
|
||||||
|
end,
|
||||||
|
--- Remove entities to the system and its subsystems.
|
||||||
|
-- If you intend to call this on a subsystem instead of the world, please read the warning in :add.
|
||||||
|
remove = :(...)
|
||||||
|
for _, e in ipairs({...}) do
|
||||||
|
if @filter(e) then
|
||||||
|
let found = false
|
||||||
|
if @_first == nil then
|
||||||
|
return
|
||||||
|
elseif @_first[1] == e then
|
||||||
|
@_first = @_first[2]
|
||||||
|
found = true
|
||||||
|
else
|
||||||
|
let entity = @_first
|
||||||
|
while entity[2] ~= nil do
|
||||||
|
if entity[2][1] == e then
|
||||||
|
entity[2] = entity[2][2]
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
entity = entity[2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if found then
|
||||||
|
for _, s in ipairs(@systems) do
|
||||||
|
s:remove(e)
|
||||||
|
end
|
||||||
|
@entityCount -= 1
|
||||||
|
@onRemove(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns an iterator that iterate through the entties in this system.
|
||||||
|
iter = :()
|
||||||
|
return nextEntity, { @_first }
|
||||||
|
end,
|
||||||
|
--- Remove every entity from the system and its subsystems.
|
||||||
|
clear = :()
|
||||||
|
for e in @iter() do
|
||||||
|
@remove(e)
|
||||||
|
end
|
||||||
|
for _, s in ipairs(@systems) do
|
||||||
|
s:clear()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Try to update the system and its subsystems. Should be called on every game update.
|
||||||
|
update = :(dt)
|
||||||
|
if @active then
|
||||||
|
if @interval then
|
||||||
|
@_waited += dt
|
||||||
|
if @_waited < @interval then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, s in ipairs(@systems) do
|
||||||
|
s:update(dt)
|
||||||
|
end
|
||||||
|
if @process ~= system_mt.process then
|
||||||
|
for e in @iter() do
|
||||||
|
@process(e, dt)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@onUpdate(dt)
|
||||||
|
if @interval then
|
||||||
|
@_waited = 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Try to draw the system and its subsystems. Should be called on every game draw.
|
||||||
|
draw = :()
|
||||||
|
if @visible then
|
||||||
|
for _, s in ipairs(@systems) do
|
||||||
|
s:draw()
|
||||||
|
end
|
||||||
|
if @render ~= system_mt.render then
|
||||||
|
for e in @iter() do
|
||||||
|
@render(e)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@onDraw()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Remove all the entities and subsystems in this system.
|
||||||
|
destroy = :()
|
||||||
|
recCallOnRemoveFromWorld(@world, { @ })
|
||||||
|
recDestroySystems({ systems = { @ } })
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
--- Recursively instanciate a list of systems for a world:
|
||||||
|
-- * create their self table with instance fields set
|
||||||
|
-- * create a field with their name in world.s (if name defined)
|
||||||
|
let recInstanciateSystems = (world, systems)
|
||||||
|
let t = {}
|
||||||
|
for _, s in ipairs(systems) do
|
||||||
|
table.insert(t, setmetatable({
|
||||||
|
systems = recInstanciateSystems(world, s.systems or {}),
|
||||||
|
world = world,
|
||||||
|
s = world.s
|
||||||
|
}, {
|
||||||
|
__index = :(k)
|
||||||
|
if s[k] ~= nil then
|
||||||
|
return s[k]
|
||||||
|
else
|
||||||
|
return system_mt[k]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}))
|
||||||
|
let system = t[#t]
|
||||||
|
if s.name then
|
||||||
|
world.s[s.name] = system
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end
|
||||||
|
--- Recursively call :onAddToWorld to a list of systems in a world.
|
||||||
|
let recCallOnAddToWorld = (world, systems)
|
||||||
|
for _, s in ipairs(systems) do
|
||||||
|
recCallOnAddToWorld(world, s.systems)
|
||||||
|
s:onAddToWorld(world)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Create and returns a world system based on a list of systems.
|
||||||
|
-- The systems will be instancied for this world.
|
||||||
|
let world = (...)
|
||||||
|
let world = setmetatable({
|
||||||
|
filter = (e) return true end,
|
||||||
|
s = {}
|
||||||
|
}, { __index = system_mt })
|
||||||
|
world.world = world
|
||||||
|
world.systems = recInstanciateSystems(world, {...})
|
||||||
|
recCallOnAddToWorld(world, world.systems)
|
||||||
|
return world
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a filter that returns true if, for every argument, a field with the same name exists in the entity.
|
||||||
|
let all = (...)
|
||||||
|
let l = {...}
|
||||||
|
return function(s, e)
|
||||||
|
for _, k in ipairs(l) do
|
||||||
|
if e[k] == nil then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Returns a filter that returns true if one of the arguments if the name of a field in the entity.
|
||||||
|
let any = (...)
|
||||||
|
let l = {...}
|
||||||
|
return function(s, e)
|
||||||
|
for _, k in ipairs(l) do
|
||||||
|
if e[k] ~= nil then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- ECS module.
|
||||||
|
return {
|
||||||
|
world = world,
|
||||||
|
all = all,
|
||||||
|
any = any
|
||||||
|
}
|
||||||
1
ecs/init.lua
Normal file
1
ecs/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
return require((...)..".ecs")
|
||||||
29
event.lua
29
event.lua
|
|
@ -1,29 +0,0 @@
|
||||||
-- ubiquitousse.event
|
|
||||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
|
||||||
local m = uqt.module
|
|
||||||
|
|
||||||
--- The events: callback functions that will be called when something interesting occurs.
|
|
||||||
-- Theses are expected to be redefined in the game.
|
|
||||||
-- For backend writers: if they already contain code, then this code has to be called on each call, even
|
|
||||||
-- if the user manually redefines them.
|
|
||||||
-- @usage -- in the game's code
|
|
||||||
-- ubiquitousse.event.draw = function()
|
|
||||||
-- ubiquitousse.draw.text(5, 5, "Hello world")
|
|
||||||
-- end
|
|
||||||
return {
|
|
||||||
--- Called each time the game loop is ran. Don't draw here.
|
|
||||||
-- @tparam number dt time since last call, in miliseconds
|
|
||||||
-- @impl mixed
|
|
||||||
update = function(dt)
|
|
||||||
if m.input then uqt.input.update(dt) end
|
|
||||||
if m.time then uqt.time.update(dt) end
|
|
||||||
if m.scene then uqt.scene.update(dt) end
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Called each time the game expect a new frame to be drawn.
|
|
||||||
-- The screen is expected to be cleared since last frame.
|
|
||||||
-- @impl backend
|
|
||||||
draw = function()
|
|
||||||
if m.scene then uqt.scene.draw() end
|
|
||||||
end
|
|
||||||
}
|
|
||||||
123
init.lua
123
init.lua
|
|
@ -1,13 +1,45 @@
|
||||||
-- ubiquitousse
|
-- ubiquitousse
|
||||||
|
|
||||||
--- Ubiquitousse Game Engine.
|
--- Ubiquitousse Game Framework.
|
||||||
-- Main module, containing the main things.
|
-- Main module, which will try to load every other Ubiquitousse module when required and provide a few convenience functions.
|
||||||
-- The API exposed here is the Ubiquitousse API.
|
|
||||||
-- It is as the name does not imply anymore abstract, and must be implemented in a backend, such as ubiquitousse.love.
|
|
||||||
-- When required, this file will try to autodetect the engine it is running on, and load a correct backend.
|
|
||||||
--
|
--
|
||||||
-- Ubiquitousse may or may not be used as a full game engine. You can delete the modules files you don't need and Ubiquitousse
|
-- Ubiquitousse may or may not be used in its totality. You can delete the modules directories you don't need and Ubiquitousse
|
||||||
-- should adapt accordingly.
|
-- should adapt accordingly. You can also simply copy the modules directories you need and use them directly, without using this
|
||||||
|
-- file at all.
|
||||||
|
-- However, some modules may provide more feature when other modules are available.
|
||||||
|
-- These dependencies are written at the top of every main module file.
|
||||||
|
--
|
||||||
|
-- Ubiquitousse's goal is to run everywhere with the least porting effort possible, so Ubiquitousse tries to only use features that
|
||||||
|
-- are almost sure to be available everywhere.
|
||||||
|
--
|
||||||
|
-- Some Ubiquitousse modules require functions that are not in the Lua standard library, and must therefore be implemented in a backend,
|
||||||
|
-- such as ubiquitousse.love. When required, modules will try to autodetect the engine it is running on, and load a correct backend.
|
||||||
|
--
|
||||||
|
-- Most Ubiquitousse module backends require a few things to be fully implemented:
|
||||||
|
-- * The backend needs to have access to some kind of main loop, or at least a function called very often (may or may not be the
|
||||||
|
-- same as the redraw screen callback).
|
||||||
|
-- * Some way of measuring time (preferably with millisecond-precision).
|
||||||
|
-- * Some kind of filesystem.
|
||||||
|
-- * Lua 5.1, 5.2, 5.3 or LuaJit.
|
||||||
|
-- * Other requirement for specific modules should be described in the module's documentation.
|
||||||
|
--
|
||||||
|
-- Units used in the API documentation:
|
||||||
|
-- * All distances are expressed in pixels (px)
|
||||||
|
-- * All durations are expressed in milliseconds (ms)
|
||||||
|
-- These units are only used to make writing documentation easier; you can use other units if you want, as long as you're consistent.
|
||||||
|
--
|
||||||
|
-- Style:
|
||||||
|
-- * tabs for indentation, spaces for esthetic whitespace (notably in comments)
|
||||||
|
-- * no globals
|
||||||
|
-- * UPPERCASE for constants (or maybe not).
|
||||||
|
-- * CamelCase for class names.
|
||||||
|
-- * lowerCamelCase is expected for everything else.
|
||||||
|
--
|
||||||
|
-- Implementation levels:
|
||||||
|
-- * backend: nothing defined in Ubiquitousse, must be implemented in backend
|
||||||
|
-- * mixed: partly implemented in Ubiquitousse but must be complemeted in backend.
|
||||||
|
-- * ubiquitousse: fully-working version in Ubiquitousse, may or may not be redefined in backend
|
||||||
|
-- The implementation level is indicated using the "@impl level" annotation.
|
||||||
--
|
--
|
||||||
-- For backend writers:
|
-- For backend writers:
|
||||||
-- If a function defined here already contains some code, this means this code is mandatory and you must put/call
|
-- If a function defined here already contains some code, this means this code is mandatory and you must put/call
|
||||||
|
|
@ -20,50 +52,8 @@
|
||||||
-- between the different versions, so it's up to you to handle that in your game (or ignore the problem and sticks to your
|
-- between the different versions, so it's up to you to handle that in your game (or ignore the problem and sticks to your
|
||||||
-- main's backend Lua version).
|
-- main's backend Lua version).
|
||||||
--
|
--
|
||||||
-- Ubiquitousse's goal is to run everywhere with the least porting effort possible.
|
|
||||||
-- To achieve this, the engine needs to stay simple, and only provide features that are almost sure to be
|
|
||||||
-- available everywhere, so writing a backend should be straighforward.
|
|
||||||
--
|
|
||||||
-- However, a full Ubiquitousse backend still have a few requirement about the destination platform:
|
|
||||||
-- * The backend needs to have access to some kind of main loop, or at least a function called very often (may or may not be the
|
|
||||||
-- same as the redraw screen callback).
|
|
||||||
-- * A 2D matrix graphic output with 32bit RGB color depth.
|
|
||||||
-- * Inputs which match ubiquitousse.input.default (a pointing/4 direction input, a confirm button, and a cancel button).
|
|
||||||
-- * Some way of measuring time with millisecond-precision.
|
|
||||||
-- * Some kind of filesystem.
|
|
||||||
-- * An available audio output would be preferable but optional.
|
|
||||||
-- * Lua 5.1, 5.2, 5.3 or LuaJit.
|
|
||||||
--
|
|
||||||
-- Regarding data formats, Ubiquitousse implementations expect and recommend:
|
|
||||||
-- * For images, PNG support is expected.
|
|
||||||
-- * For audio files, OGG Vorbis support is expected.
|
|
||||||
-- * For fonts, TTF support is expected.
|
|
||||||
-- Theses formats are respected for the reference implementations, but Ubiquitousse may provide a script to
|
|
||||||
-- automatically convert data formats from a project at some point.
|
|
||||||
--
|
|
||||||
-- Units used in the API:
|
|
||||||
-- * All distances are expressed in pixels (px)
|
|
||||||
-- * All durations are expressed in milliseconds (ms)
|
|
||||||
--
|
|
||||||
-- Style:
|
|
||||||
-- * tabs for indentation, spaces for esthetic whitespace (notably in comments)
|
|
||||||
-- * no globals
|
|
||||||
-- * UPPERCASE for constants (or maybe not).
|
|
||||||
-- * CamelCase for class names.
|
|
||||||
-- * lowerCamelCase is expected for everything else.
|
|
||||||
--
|
|
||||||
-- Implementation levels:
|
|
||||||
-- * backend: nothing defined in Ubiquitousse, must be implemented in backend
|
|
||||||
-- * mixed: partly implemented in Ubiquitousse but must be complemeted in backend
|
|
||||||
-- * ubiquitousse: fully-working version in Ubiquitousse, may or may not be redefined in backend
|
|
||||||
-- The implementation level is indicated using the "@impl level" annotation.
|
|
||||||
--
|
|
||||||
-- Some Ubiquitousse modules require parts of other modules to work. Because every module should work when all the others are
|
|
||||||
-- disabled, the backend may need to provide defaults values for a few fields in disabled modules required by an enabled one.
|
|
||||||
-- Thoses fields are indicated with "@requiredby module" annotations.
|
|
||||||
--
|
|
||||||
-- Regarding the documentation: Ubiquitousse used LDoc/LuaDoc styled-comments, but since LDoc hates me and my code, the
|
-- Regarding the documentation: Ubiquitousse used LDoc/LuaDoc styled-comments, but since LDoc hates me and my code, the
|
||||||
-- generated result is complete garbage, so please read the documentation directly in the comments here.
|
-- generated result is complete garbage, so please read the documentation directly in the comments here until fix this.
|
||||||
-- Stuff you're interested in starts with triple - (e.g., "--- This functions saves the world").
|
-- Stuff you're interested in starts with triple - (e.g., "--- This functions saves the world").
|
||||||
--
|
--
|
||||||
-- @usage local ubiquitousse = require("ubiquitousse")
|
-- @usage local ubiquitousse = require("ubiquitousse")
|
||||||
|
|
@ -76,34 +66,31 @@ ubiquitousse = {
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
version = "0.0.1",
|
version = "0.0.1",
|
||||||
|
|
||||||
--- Table of enabled modules.
|
--- Should be called each time the game loop is ran; will update every loaded Ubiquitousse module that needs it.
|
||||||
-- @impl ubiquitousse
|
-- @tparam number dt time since last call, in miliseconds
|
||||||
module = {
|
-- @impl mixed
|
||||||
time = false,
|
update = function(dt)
|
||||||
draw = false,
|
if ubiquitousse.time then ubiquitousse.time.update(dt) end
|
||||||
audio = false,
|
if ubiquitousse.scene then ubiquitousse.scene.update(dt) end
|
||||||
input = false,
|
if ubiquitousse.input then ubiquitousse.input.update(dt) end
|
||||||
scene = false,
|
end,
|
||||||
event = false,
|
|
||||||
asset = false,
|
|
||||||
util = false
|
|
||||||
},
|
|
||||||
|
|
||||||
--- Backend name.
|
--- Should be called each time the game expect a new frame to be drawn; will draw every loaded Ubiquitousse module that needs it
|
||||||
-- For consistency, only use lowercase letters [a-z] (no special char)
|
-- The screen is expected to be cleared since last frame.
|
||||||
-- @impl backend
|
-- @impl mixed
|
||||||
backend = "unknown"
|
draw = function()
|
||||||
|
if ubiquitousse.scene then ubiquitousse.scene.draw() end
|
||||||
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
-- We're going to require modules requiring Ubiquitousse, so to avoid stack overflows we already register the ubiquitousse package
|
-- We're going to require modules requiring Ubiquitousse, so to avoid stack overflows we already register the ubiquitousse package
|
||||||
package.loaded[p] = ubiquitousse
|
package.loaded[p] = ubiquitousse
|
||||||
|
|
||||||
-- Require external submodules
|
-- Require external submodules
|
||||||
for m in pairs(ubiquitousse.module) do
|
for _, m in ipairs{"asset", "ecs", "input", "scene", "time", "util"} do
|
||||||
local s, t = pcall(require, p.."."..m)
|
local s, t = pcall(require, p.."."..m)
|
||||||
if s then
|
if s then
|
||||||
ubiquitousse[m] = t
|
ubiquitousse[m] = t
|
||||||
ubiquitousse.module[m] = true
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
489
input.lua
489
input.lua
|
|
@ -1,489 +0,0 @@
|
||||||
-- ubiquitousse.input
|
|
||||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
|
||||||
|
|
||||||
--- Used to store inputs which were updated this frame
|
|
||||||
-- { Input: true, ... }
|
|
||||||
-- This table is for internal use and shouldn't be used from an external script.
|
|
||||||
local updated = {}
|
|
||||||
|
|
||||||
--- Input stuff
|
|
||||||
-- Inspired by Tactile by Andrew Minnich (https://github.com/tesselode/tactile).
|
|
||||||
-- I don't think I need to include the license since it's just inspiration, but for the sake of information, Tactile is under the MIT license.
|
|
||||||
-- Ubiquitousse considers two input methods, called buttons (binary input) and axes (analog input).
|
|
||||||
local input
|
|
||||||
input = {
|
|
||||||
---------------------------------
|
|
||||||
--- Detectors (input sources) ---
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
-- Buttons detectors --
|
|
||||||
-- A button detector is a function which returns true (pressed) or false (unpressed).
|
|
||||||
-- Any fuction which returns a boolean can be used as a button detector, but you will probably want to get the data from an HID.
|
|
||||||
-- Ubiquitousse being platform-agnostic, it doesn't suppose there is a keyboard and mouse available for example, and all HID buttons are identified using
|
|
||||||
-- an identifier string, which depends of the backend. Identifier strings should be of the format "source1.source2.[...].button", for example "keyboard.up"
|
|
||||||
-- or "gamepad.button.1.a" for the A-button of the first gamepad.
|
|
||||||
-- If the button is actually an axis (ie, the button is pressed if the axis value passes a certain threshold), the threshold should be in the end of the
|
|
||||||
-- identifier, preceded by a % : for example "gamepad.axis.1.leftx%-0.5" should return true when the left-stick of the first gamepad is moved to the right
|
|
||||||
-- by more of 50%. The negative threshold value means that the button will be pressed only when the axis has a negative value (in the example, it won't be
|
|
||||||
-- pressed when the axis is moved to the right).
|
|
||||||
|
|
||||||
--- Makes a new button detector(s) from the identifier(s) string.
|
|
||||||
-- The function may error if the identifier is incorrect.
|
|
||||||
-- @tparam string button identifier, depends on the platform Ubiquitousse is running on (multiple parameters)
|
|
||||||
-- @treturn each button detector (multiple-returns)
|
|
||||||
-- @impl backend
|
|
||||||
buttonDetector = function(...) end,
|
|
||||||
|
|
||||||
-- Axis detectors --
|
|
||||||
-- Similar to buttons detectors, but returns a number between -1 and 1.
|
|
||||||
-- Threshold value can be used similarly with %.
|
|
||||||
-- Axis detectors should support "binary axis", ie an axis defined by two buttons: if the 1rst button is pressed, value will be -1, if the 2nd is pressed it will be 1
|
|
||||||
-- and if none or the both are pressed, the value will be 0. This kind of axis identifier should have an identifier like "button1,button2" (comma-separated).
|
|
||||||
-- Axis detectors may also optionally return after the number between -1 and 1 the raw value and max value. The raw value is between -max and +max.
|
|
||||||
|
|
||||||
--- Makes a new axis detector(s) from the identifier(s) string.
|
|
||||||
-- @tparam string axis identifier, depends on the platform Ubiquitousse is running on (multiple parameters)
|
|
||||||
-- @treturn each axis detector (multiple-returns)
|
|
||||||
-- @impl backend
|
|
||||||
axisDetector = function(...) end,
|
|
||||||
|
|
||||||
------------------------------------------
|
|
||||||
--- Inputs (the thing you want to use) ---
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
-- Buttons inputs --
|
|
||||||
-- Button input is a container for buttons detector. A button will be pressed when one of its detectors returns true.
|
|
||||||
-- Inputs also knows if the button was just pressed or released.
|
|
||||||
-- @tparam ButtonDetectors ... all the buttons detectors or buttons identifiers
|
|
||||||
-- @tretrun ButtonInput the object
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
button = function(...)
|
|
||||||
local r -- object
|
|
||||||
local detectors = {} -- detectors list
|
|
||||||
local state = "none" -- current state (none, pressed, down, released)
|
|
||||||
local function update() -- update button state
|
|
||||||
if not updated[r] then
|
|
||||||
local down = false
|
|
||||||
for _,d in pairs(detectors) do
|
|
||||||
if d() then
|
|
||||||
down = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if down then
|
|
||||||
if state == "none" or state == "released" then
|
|
||||||
state = "pressed"
|
|
||||||
else
|
|
||||||
state = "down"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if state == "down" or state == "pressed" then
|
|
||||||
state = "released"
|
|
||||||
else
|
|
||||||
state = "none"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
updated[r] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- Object
|
|
||||||
r = {
|
|
||||||
--- Returns a new ButtonInput with the same properties.
|
|
||||||
-- @treturn ButtonInput the cloned object
|
|
||||||
clone = function(self)
|
|
||||||
local clone = input.button()
|
|
||||||
for name, detector in pairs(detectors) do
|
|
||||||
if type(name) == "string" then
|
|
||||||
clone:bind(name)
|
|
||||||
else
|
|
||||||
clone:bind(detector)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return clone
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Bind new ButtonDetector(s) to this input.
|
|
||||||
-- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to add
|
|
||||||
-- @treturn ButtonInput this ButtonInput object
|
|
||||||
bind = function(self, ...)
|
|
||||||
for _,d in ipairs({...}) do
|
|
||||||
if type(d) == "string" then
|
|
||||||
detectors[d] = input.buttonDetector(d)
|
|
||||||
elseif type(d) == "function" then
|
|
||||||
detectors[d] = d
|
|
||||||
else
|
|
||||||
error("Not a valid button detector")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
--- Unbind ButtonDetector(s).
|
|
||||||
-- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to remove
|
|
||||||
-- @treturn ButtonInput this ButtonInput object
|
|
||||||
unbind = function(self, ...)
|
|
||||||
for _,d in ipairs({...}) do
|
|
||||||
detectors[d] = nil
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Returns true if the input was just pressed.
|
|
||||||
-- @treturn boolean true if the input was pressed, false otherwise
|
|
||||||
pressed = function(self)
|
|
||||||
update()
|
|
||||||
return state == "pressed"
|
|
||||||
end,
|
|
||||||
--- Returns true if the input is down.
|
|
||||||
-- @treturn boolean true if the input is currently down, false otherwise
|
|
||||||
down = function(self)
|
|
||||||
update()
|
|
||||||
return state == "down" or state == "pressed"
|
|
||||||
end,
|
|
||||||
--- Returns true if the input was just released.
|
|
||||||
-- @treturn boolean true if the input was released, false otherwise
|
|
||||||
released = function(self)
|
|
||||||
update()
|
|
||||||
return state == "released"
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
r:bind(...)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Axis inputs --
|
|
||||||
-- Axis input is a container for axes detector. An axis input will return the value of the axis detector the most far away from their center (0).
|
|
||||||
-- Axis input provide a threshold setting ; every axis which has a distance to the center below th threshold will be ignored.
|
|
||||||
-- @tparam AxisDetectors ... all the axis detectors or axis identifiers
|
|
||||||
-- @tretrun AxisInput the object
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
axis = function(...)
|
|
||||||
local r -- object
|
|
||||||
local detectors = {} -- detectors list
|
|
||||||
local value, raw, max = 0, 0, 1 -- current value between -1 and 1, raw value between -max and +max and maximum for raw values
|
|
||||||
local threshold = 0.5 -- ie., the deadzone
|
|
||||||
local function update() -- update axis state
|
|
||||||
if not updated[r] then
|
|
||||||
value = 0
|
|
||||||
for _,d in pairs(detectors) do
|
|
||||||
local v, r, m = d() -- v[-1,1], r[-m,+m]
|
|
||||||
if math.abs(v) > math.abs(value) then
|
|
||||||
value, raw, max = v, r or v, m or 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
updated[r] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- Object
|
|
||||||
r = {
|
|
||||||
--- Returns a new AxisInput with the same properties.
|
|
||||||
-- @treturn AxisInput the cloned object
|
|
||||||
clone = function(self)
|
|
||||||
local clone = input.axis()
|
|
||||||
for name, detector in pairs(detectors) do
|
|
||||||
if type(name) == "string" then
|
|
||||||
clone:bind(name)
|
|
||||||
else
|
|
||||||
clone:bind(detector)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
clone:threshold(threshold)
|
|
||||||
return clone
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Bind new AxisDetector(s) to this input.
|
|
||||||
-- @tparam AxisDetectors ... axis detectors or axis identifiers to add
|
|
||||||
-- @treturn AxisInput this AxisInput object
|
|
||||||
bind = function(self, ...)
|
|
||||||
for _,d in ipairs({...}) do
|
|
||||||
if type(d) == "string" then
|
|
||||||
detectors[d] = input.axisDetector(d)
|
|
||||||
elseif type(d) == "function" then
|
|
||||||
detectors[d] = d
|
|
||||||
else
|
|
||||||
error("Not a valid axis detector")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
--- Unbind AxisDetector(s).
|
|
||||||
-- @tparam AxisDetectors ... axis detectors or axis identifiers to remove
|
|
||||||
-- @treturn AxisInput this AxisInput object
|
|
||||||
unbind = function(self, ...)
|
|
||||||
for _,d in ipairs({...}) do
|
|
||||||
detectors[d] = nil
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Sets the default detection threshold (deadzone).
|
|
||||||
-- @tparam number new the new detection threshold
|
|
||||||
-- @treturn AxisInput this AxisInput object
|
|
||||||
threshold = function(self, new)
|
|
||||||
threshold = tonumber(new)
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Returns the value of the input (between -1 and 1).
|
|
||||||
-- @tparam[opt=default threshold] number threshold value to use
|
|
||||||
-- @treturn number the input value
|
|
||||||
value = function(self, curThreshold)
|
|
||||||
update()
|
|
||||||
return math.abs(value) > math.abs(curThreshold or threshold) and value or 0
|
|
||||||
end,
|
|
||||||
--- Returns the raw value of the input (between -max and +max).
|
|
||||||
-- @tparam[opt=default threshold*max] number raw threshold value to use
|
|
||||||
-- @treturn number the input raw value
|
|
||||||
raw = function(self, rawThreshold)
|
|
||||||
update()
|
|
||||||
return math.abs(raw) > math.abs(rawThreshold or threshold*max) and raw or 0
|
|
||||||
end,
|
|
||||||
--- Return the raw max of the input.
|
|
||||||
-- @treturn number the input raw max
|
|
||||||
max = function(self)
|
|
||||||
update()
|
|
||||||
return max
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- The associated button pressed when the axis reaches a positive value.
|
|
||||||
positive = input.button(function() return r:value() > 0 end),
|
|
||||||
--- The associated button pressed when the axis reaches a negative value.
|
|
||||||
negative = input.button(function() return r:value() < 0 end)
|
|
||||||
}
|
|
||||||
r:bind(...)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- Pointer inputs --
|
|
||||||
-- Pointer inputs are container for two axes input, in order to represent a two-dimensionnal pointing device, e.g. a mouse or a stick.
|
|
||||||
-- Each pointer detector is a table with 3 fields: mode(string), XAxis(axis), YAxis(axis). mode can either be "relative" or "absolute".
|
|
||||||
-- In relative mode, the pointer will return the movement since last update (for example to move a mouse pointer with a stick).
|
|
||||||
-- In absolute mode, the pointer will return the pointer position directly deduced of the current axes position.
|
|
||||||
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode
|
|
||||||
-- @tretrun PointerInput the object
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
pointer = function(...)
|
|
||||||
local draw = uqt.draw -- requires width and height
|
|
||||||
local pointers = {} -- pointers list
|
|
||||||
local x, y = 0, 0 -- pointer position
|
|
||||||
local width, height = 1, 1 -- half-dimensions of the movement area
|
|
||||||
local offsetX, offsetY = 0, 0 -- offsets
|
|
||||||
local xSpeed, ySpeed = 1, 1 -- speed (pixels/milisecond); for relative mode
|
|
||||||
local r -- object
|
|
||||||
local function update()
|
|
||||||
if not updated[r] then
|
|
||||||
local width, height = width or draw.width/2, height or draw.height/2
|
|
||||||
local newX, newY = x, y
|
|
||||||
local maxMovX, maxMovY = 0, 0 -- the maxium axis movement in a direction (used to determine which axes have the priority) (absolute value)
|
|
||||||
for _, pointer in ipairs(pointers) do
|
|
||||||
local mode, xAxis, yAxis = unpack(pointer)
|
|
||||||
if mode == "relative" then
|
|
||||||
local movX, movY = math.abs(xAxis:value()), math.abs(yAxis:value())
|
|
||||||
if movX > maxMovX then
|
|
||||||
newX = x + (xSpeed and (xAxis:value() * xSpeed * uqt.time.dt) or xAxis:raw())
|
|
||||||
maxMovX = movX
|
|
||||||
end
|
|
||||||
if movY > maxMovY then
|
|
||||||
newY = y + (ySpeed and (yAxis:value() * ySpeed * uqt.time.dt) or yAxis:raw())
|
|
||||||
maxMovY = movY
|
|
||||||
end
|
|
||||||
elseif mode == "absolute" then
|
|
||||||
if not pointer.previous then pointer.previous = { x = xAxis:value(), y = yAxis:value() } end -- last frame position (to calculate movement/delta)
|
|
||||||
local movX, movY = math.abs(xAxis:value() - pointer.previous.x), math.abs(yAxis:value() - pointer.previous.y)
|
|
||||||
pointer.previous = { x = xAxis:value(), y = yAxis:value() }
|
|
||||||
if movX > maxMovX then
|
|
||||||
newX = xAxis:value() * width
|
|
||||||
maxMovX = movX
|
|
||||||
end
|
|
||||||
if movY > maxMovY then
|
|
||||||
newY = yAxis:value() * height
|
|
||||||
maxMovY = movY
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
x, y = math.min(math.abs(newX), width) * (newX < 0 and -1 or 1), math.min(math.abs(newY), height) * (newY < 0 and -1 or 1)
|
|
||||||
updated[r] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
r = {
|
|
||||||
--- Returns a new PointerInput with the same properties.
|
|
||||||
-- @treturn PointerInput the cloned object
|
|
||||||
clone = function(self)
|
|
||||||
return input.pointer(unpack(pointers))
|
|
||||||
:dimensions(width, height)
|
|
||||||
:offset(offsetX, offsetY)
|
|
||||||
:speed(xSpeed, ySpeed)
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Bind new axis couples to this input.
|
|
||||||
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode
|
|
||||||
-- @treturn PointerInput this PointerInput object
|
|
||||||
bind = function(self, ...)
|
|
||||||
for _,p in ipairs({...}) do
|
|
||||||
if type(p) == "table" then
|
|
||||||
if type(p[2]) == "string" then p[2] = input.axis(input.axisDetector(p[2])) end
|
|
||||||
if type(p[3]) == "string" then p[3] = input.axis(input.axisDetector(p[3])) end
|
|
||||||
table.insert(pointers, p)
|
|
||||||
else
|
|
||||||
error("Pointer must be a table")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
--- Unbind axis couples.
|
|
||||||
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to remove
|
|
||||||
-- @treturn PointerInput this PointerInput object
|
|
||||||
unbind = function(self, ...)
|
|
||||||
for _,p in ipairs({...}) do
|
|
||||||
for i,pointer in ipairs(pointers) do
|
|
||||||
if pointer == p then
|
|
||||||
table.remove(pointers, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Set the moving area half-dimensions.
|
|
||||||
-- Call without argument to use half the window dimensions.
|
|
||||||
-- It's the half dimensions because axes values goes from -1 to 1, so theses dimensions only
|
|
||||||
-- covers values from x=0,y=0 to x=1,y=1. The full moving area will be 4*newWidth*newHeight.
|
|
||||||
-- @tparam number newWidth new width
|
|
||||||
-- @tparam number newHeight new height
|
|
||||||
-- @treturn PointerInput this PointerInput object
|
|
||||||
dimensions = function(self, newWidth, newHeight)
|
|
||||||
width, height = newWidth, newHeight
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
--- Set the moving area coordinates offset.
|
|
||||||
-- The offset is a value automatically added to the x and y values when using the x() and y() methods.
|
|
||||||
-- Call without argument to automatically offset so 0,0 <= x(),y() <= width,height, i.e. offset to width,height.
|
|
||||||
-- @tparam number newOffX new X offset
|
|
||||||
-- @tparam number newOffY new Y offset
|
|
||||||
-- @treturn PointerInput this PointerInput object
|
|
||||||
offset = function(self, newOffX, newOffY)
|
|
||||||
offsetX, offsetY = newOffX, newOffY
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
--- Set maximal speed (pixels-per-milisecond)
|
|
||||||
-- Only used in relative mode.
|
|
||||||
-- Calls without argument to use the raw data and don't apply a speed modifier.
|
|
||||||
-- @tparam number newXSpeed new X speed
|
|
||||||
-- @tparam number newYSpeed new Y speed
|
|
||||||
-- @treturn PointerInput this PointerInput object
|
|
||||||
speed = function(self, newXSpeed, newYSpeed)
|
|
||||||
xSpeed, ySpeed = newXSpeed, newYSpeed or newXSpeed
|
|
||||||
return self
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Returns the current X value of the pointer.
|
|
||||||
-- @treturn number X value
|
|
||||||
x = function()
|
|
||||||
update()
|
|
||||||
return x + (offsetX or width or draw.width/2)
|
|
||||||
end,
|
|
||||||
--- Returns the current Y value of the pointer.
|
|
||||||
-- @treturn number Y value
|
|
||||||
y = function()
|
|
||||||
update()
|
|
||||||
return y + (offsetY or height or draw.height/2)
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- The associated horizontal axis.
|
|
||||||
horizontal = input.axis(function()
|
|
||||||
local h = r:x()
|
|
||||||
return h/width, h, width
|
|
||||||
end),
|
|
||||||
--- The associated vertical axis.
|
|
||||||
vertical = input.axis(function()
|
|
||||||
local v = r:y()
|
|
||||||
return v/height, v, height
|
|
||||||
end),
|
|
||||||
|
|
||||||
--- The associated button pressed when the pointer goes to the right.
|
|
||||||
right = nil,
|
|
||||||
--- The associated button pressed when the pointer goes to the left.
|
|
||||||
left = nil,
|
|
||||||
--- The associated button pressed when the pointer points up.
|
|
||||||
up = nil,
|
|
||||||
--- The associated button pressed when the pointer points down.
|
|
||||||
down = nil
|
|
||||||
}
|
|
||||||
r.right, r.left = r.horizontal.positive, r.horizontal.negative
|
|
||||||
r.up, r.down = r.vertical.negative, r.vertical.positive
|
|
||||||
r:bind(...)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
------------------------------
|
|
||||||
--- Input detection helpers --
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
--- Returns a list of the buttons currently in use, identified by their string button identifier.
|
|
||||||
-- This may also returns "axis threshold" buttons if an axis passes the threshold.
|
|
||||||
-- @treturn table<string> buttons identifiers list
|
|
||||||
-- @treturn[opt=0.5] number threshold the threshold to detect axes as button
|
|
||||||
-- @impl backend
|
|
||||||
buttonsInUse = function(threshold) end,
|
|
||||||
|
|
||||||
--- Returns a list of the axes currently in use, identified by their string axis identifier
|
|
||||||
-- @treturn table<string> axes identifiers list
|
|
||||||
-- @treturn[opt=0.5] number threshold the threshold to detect axes
|
|
||||||
-- @impl backend
|
|
||||||
axesInUse = function(threshold) end,
|
|
||||||
|
|
||||||
--- Returns a nice name for the button identifier.
|
|
||||||
-- Can be locale-depedant and stuff, it's only for display.
|
|
||||||
-- May returns the raw identifier if you're lazy.
|
|
||||||
-- @tparam string... button identifier string(s)
|
|
||||||
-- @treturn string... the displayable names
|
|
||||||
-- @impl backend
|
|
||||||
buttonName = function(...) end,
|
|
||||||
|
|
||||||
--- Returns a nice name for the axis identifier.
|
|
||||||
-- Can be locale-depedant and stuff, it's only for display.
|
|
||||||
-- May returns the raw identifier if you're lazy.
|
|
||||||
-- @tparam string... axis identifier string(s)
|
|
||||||
-- @treturn string... the displayable names
|
|
||||||
-- @impl backend
|
|
||||||
axisName = function(...) end,
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
--- Other stuff ---
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
--- Some default inputs.
|
|
||||||
-- The backend should bind detectors to thoses inputs.
|
|
||||||
-- These are used to provide some common input default detectors to allow to start a game quickly on
|
|
||||||
-- any platform without having to configure the keys.
|
|
||||||
-- If some key function in your game match one of theses defaults, using it instead of creating a new
|
|
||||||
-- input would be a good idea.
|
|
||||||
-- @impl mixed
|
|
||||||
default = {
|
|
||||||
pointer = nil, -- Pointer: used to move and select. Example binds: arrow keys, WASD, stick.
|
|
||||||
confirm = nil, -- Button: used to confirm something. Example binds: Enter, A button.
|
|
||||||
cancel = nil -- Button: used to cancel something. Example binds: Escape, B button.
|
|
||||||
},
|
|
||||||
|
|
||||||
--- Update all the Inputs.
|
|
||||||
-- Supposed to be called in ubiquitousse.event.update.
|
|
||||||
-- The backend can hook into this function to to its input-related updates.
|
|
||||||
-- @tparam numder dt the delta-time
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
update = function(dt)
|
|
||||||
updated = {}
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Create default inputs
|
|
||||||
input.default.pointer = input.pointer()
|
|
||||||
input.default.confirm = input.button()
|
|
||||||
input.default.cancel = input.button()
|
|
||||||
|
|
||||||
-- TODO: some key selection helper? Will be backend-implemented, to account for all the possible input methods.
|
|
||||||
-- TODO: some way to list all possible input / outputs, or make the *inUse make some separation between inputs indiscutitably in use and those who are incertain.
|
|
||||||
-- TODO: outputs ! (rumble, lights, I don't know)
|
|
||||||
-- TODO: other, optional, default/generic inputs, and a way to know if they are binded.
|
|
||||||
-- TODO: multiplayer input helpers? something like getting the same input for different players, or default inputs for different players
|
|
||||||
|
|
||||||
return input
|
|
||||||
269
input/backend/ctrulua.lua
Normal file
269
input/backend/ctrulua.lua
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
local input = require((...):match("^(.-%.)backend").."input")
|
||||||
|
|
||||||
|
local gfx = require("ctr.gfx")
|
||||||
|
local hid = require("ctr.hid")
|
||||||
|
|
||||||
|
local keys = {}
|
||||||
|
local touchX, touchY, dTouchX, dTouchY
|
||||||
|
|
||||||
|
local oUpdate = input.update
|
||||||
|
input.update = function(dt)
|
||||||
|
hid.read()
|
||||||
|
|
||||||
|
keys = hid.keys()
|
||||||
|
|
||||||
|
local nTouchX, nTouchY = hid.touch()
|
||||||
|
dTouchX, dTouchY = nTouchX - touchX, nTouchY - touchY
|
||||||
|
touchX, touchY = nTouchX, nTouchY
|
||||||
|
|
||||||
|
oUpdate(dt)
|
||||||
|
end
|
||||||
|
|
||||||
|
input.buttonDetector = function(...)
|
||||||
|
local ret = {}
|
||||||
|
for _,id in ipairs({...}) do
|
||||||
|
-- Keys
|
||||||
|
if id:match("^key%.") then
|
||||||
|
local key = id:match("^key%.(.+)$")
|
||||||
|
table.insert(ret, function()
|
||||||
|
return keys.held[key]
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
error("Unknown button identifier: "..id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
input.axisDetector = function(...)
|
||||||
|
local ret = {}
|
||||||
|
for _,id in ipairs({...}) do
|
||||||
|
-- Binary axis
|
||||||
|
if id:match(".+%,.+") then
|
||||||
|
local d1, d2 = input.buttonDetector(id:match("^(.+)%,(.+)$"))
|
||||||
|
table.insert(ret, function()
|
||||||
|
local b1, b2 = d1(), d2()
|
||||||
|
if b1 and b2 then return 0
|
||||||
|
elseif b1 then return -1
|
||||||
|
elseif b2 then return 1
|
||||||
|
else return 0 end
|
||||||
|
end)
|
||||||
|
-- Touch movement
|
||||||
|
elseif id:match("^touch%.move%.") then
|
||||||
|
local axis, threshold = id:match("^touch%.move%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^touch%.move%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, function()
|
||||||
|
local val, raw, max
|
||||||
|
if axis == "x" then
|
||||||
|
raw, max = dTouchX, gfx.BOTTOM_WIDTH
|
||||||
|
elseif axis == "y" then
|
||||||
|
raw, max = dTouchY, gfx.BOTTOM_HEIGHT
|
||||||
|
end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end)
|
||||||
|
-- Touch position
|
||||||
|
elseif id:match("^touch%.position%.") then
|
||||||
|
local axis, threshold = id:match("^touch%.position%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^touch%.position%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, function()
|
||||||
|
local val, raw, max
|
||||||
|
if axis == "x" then
|
||||||
|
max = gfx.BOTTOM_WIDTH / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1])
|
||||||
|
raw = touchX - max
|
||||||
|
elseif axis == "y" then
|
||||||
|
max = gfx.BOTTOM_HEIGHT / 2
|
||||||
|
raw = touchY - max
|
||||||
|
end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end)
|
||||||
|
-- Circle pad axis
|
||||||
|
elseif id:match("^circle%.") then
|
||||||
|
local axis, threshold = id:match("^circle%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^circle%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, function()
|
||||||
|
local x, y = hid.circle()
|
||||||
|
local val, raw, max = 0, 0, 156
|
||||||
|
if axis == "x" then raw = x
|
||||||
|
elseif axis == "y" then raw = y end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end)
|
||||||
|
-- C-Stick axis
|
||||||
|
elseif id:match("^cstick%.") then
|
||||||
|
local axis, threshold = id:match("^cstick%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^cstick%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, function()
|
||||||
|
local x, y = hid.cstick()
|
||||||
|
local val, raw, max = 0, 0, 146
|
||||||
|
if axis == "x" then raw = x
|
||||||
|
elseif axis == "y" then raw = y end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end)
|
||||||
|
-- Accelerometer axis
|
||||||
|
elseif id:match("^accel%.") then
|
||||||
|
local axis, threshold = id:match("^accel%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^accel%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, function()
|
||||||
|
local x, y, z = hid.accel()
|
||||||
|
local val, raw, max = 0, 0, 32768 -- no idea actually, but it's a s16
|
||||||
|
if axis == "x" then raw = x
|
||||||
|
elseif axis == "y" then raw = y
|
||||||
|
elseif axis == "z" then raw = z end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end)
|
||||||
|
-- Gyroscope axis
|
||||||
|
elseif id:match("^gyro%.") then
|
||||||
|
local axis, threshold = id:match("^gyro%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^gyro%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, function()
|
||||||
|
local roll, pitch, yaw = hid.gyro()
|
||||||
|
local val, raw, max = 0, 0, 32768 -- no idea actually, but it's a s16
|
||||||
|
if axis == "roll" then raw = roll
|
||||||
|
elseif axis == "pitch" then raw = pitch
|
||||||
|
elseif axis == "yaw" then raw = yaw end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
error("Unknown axis identifier: "..id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
input.buttonsInUse = function(threshold)
|
||||||
|
local r = {}
|
||||||
|
for key, held in pairs(keys.held) do
|
||||||
|
if held then table.insert(r, "key."..key) end
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
|
||||||
|
input.axesInUse = function(threshold)
|
||||||
|
local r = {}
|
||||||
|
threshold = threshold or 0.5
|
||||||
|
|
||||||
|
if math.abs(touchX) / gfx.BOTTOM_WIDTH > threshold then table.insert(r, "touch.position.x%"..threshold) end
|
||||||
|
if math.abs(touchY) / gfx.BOTTOM_HEIGHT > threshold then table.insert(r, "touch.position.y%"..threshold) end
|
||||||
|
|
||||||
|
if math.abs(dTouchX) / gfx.BOTTOM_WIDTH > threshold then table.insert(r, "touch.move.x%"..threshold) end
|
||||||
|
if math.abs(dTouchY) / gfx.BOTTOM_HEIGHT > threshold then table.insert(r, "touch.move.y%"..threshold) end
|
||||||
|
|
||||||
|
local circleX, circleY = hid.circle()
|
||||||
|
if math.abs(circleX) / 156 > threshold then table.insert(r, "circle.x%"..threshold) end
|
||||||
|
if math.abs(circleY) / 156 > threshold then table.insert(r, "circle.y%"..threshold) end
|
||||||
|
|
||||||
|
if ctr.apt.isNew3DS() then
|
||||||
|
local cstickX, cstickY = hid.cstick()
|
||||||
|
if math.abs(cstickY) / 146 > threshold then table.insert(r, "cstick.y%"..threshold) end
|
||||||
|
if math.abs(cstickX) / 146 > threshold then table.insert(r, "cstick.x%"..threshold) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local accelX, accelY, accelZ = hid.accel()
|
||||||
|
if math.abs(accelX) / 32768 > threshold then table.insert(r, "accel.x%"..threshold) end
|
||||||
|
if math.abs(accelY) / 32768 > threshold then table.insert(r, "accel.y%"..threshold) end
|
||||||
|
if math.abs(accelZ) / 32768 > threshold then table.insert(r, "accel.z%"..threshold) end
|
||||||
|
|
||||||
|
-- no gyro, because it is always in use
|
||||||
|
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
|
||||||
|
input.buttonName = function(...)
|
||||||
|
local ret = {}
|
||||||
|
for _,id in ipairs({...}) do
|
||||||
|
-- Key
|
||||||
|
if id:match("^key%.") then
|
||||||
|
local key = id:match("^key%.(.+)$")
|
||||||
|
table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key")
|
||||||
|
else
|
||||||
|
table.insert(ret, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
input.axisName = function(...)
|
||||||
|
local ret = {}
|
||||||
|
for _,id in ipairs({...}) do
|
||||||
|
-- Binary axis
|
||||||
|
if id:match(".+%,.+") then
|
||||||
|
local b1, b2 = input.buttonName(id:match("^(.+)%,(.+)$"))
|
||||||
|
table.insert(ret, b1.." / "..b2)
|
||||||
|
-- Touch movement
|
||||||
|
elseif id:match("^touch%.move%.") then
|
||||||
|
local axis, threshold = id:match("^touch%.move%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^touch%.move%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, ("Touch %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
-- Touch position
|
||||||
|
elseif id:match("^touch%.position%.") then
|
||||||
|
local axis, threshold = id:match("^touch%.position%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^touch%.position%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, ("Touch %s position (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
-- Circle pad axis
|
||||||
|
elseif id:match("^circle%.") then
|
||||||
|
local axis, threshold = id:match("^circle%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^circle%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
if axis == "x" then
|
||||||
|
table.insert(ret, ("Circle pad horizontal axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
||||||
|
elseif axis == "y" then
|
||||||
|
table.insert(ret, ("Circle pad vertical axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
||||||
|
else
|
||||||
|
table.insert(ret, ("Circle pad %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
end
|
||||||
|
-- C-Stick axis
|
||||||
|
elseif id:match("^cstick%.") then
|
||||||
|
local axis, threshold = id:match("^cstick%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^cstick%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
if axis == "x" then
|
||||||
|
table.insert(ret, ("C-Stick horizontal axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
||||||
|
elseif axis == "y" then
|
||||||
|
table.insert(ret, ("C-Stick vertical axis (deadzone %s%%)"):format(math.abs(threshold*100)))
|
||||||
|
else
|
||||||
|
table.insert(ret, ("C-Stick %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
end
|
||||||
|
-- Accelerometer axis
|
||||||
|
elseif id:match("^accel%.") then
|
||||||
|
local axis, threshold = id:match("^accel%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^accel%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, ("Accelerometer %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
-- Gyroscope axis
|
||||||
|
elseif id:match("^gyro%.") then
|
||||||
|
local axis, threshold = id:match("^gyro%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^gyro%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, ("Gyroscope %s axis (deadzone %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
else
|
||||||
|
table.insert(ret, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Size
|
||||||
|
input.screenWidth, input.screenHeight = gfx.TOP_WIDTH, gfx.TOP_HEIGHT
|
||||||
|
|
||||||
|
-- Defaults
|
||||||
|
input.default.pointer:bind(
|
||||||
|
{ "absolute", "key.left,key.right", "key.up,key.down" },
|
||||||
|
{ "absolute", "circle.x", "circle.y" }
|
||||||
|
)
|
||||||
|
input.default.confirm:bind("key.a")
|
||||||
|
input.default.cancel:bind("key.b")
|
||||||
|
|
||||||
|
return input
|
||||||
319
input/backend/love.lua
Normal file
319
input/backend/love.lua
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
local input = require((...):match("^(.-%.)backend").."input")
|
||||||
|
|
||||||
|
-- Config --
|
||||||
|
|
||||||
|
-- Use ScanCodes (layout independant input) instead of KeyConstants (layout dependant) for keyboard input
|
||||||
|
local useScancodes = true
|
||||||
|
-- If using ScanCodes, sets this to true so the backend returns the layout-dependant KeyConstant
|
||||||
|
-- instead of the raw ScanCode when getting the display name. If set to false and using ScanCodes,
|
||||||
|
-- the user will see keys that don't match what's actually written on his keyboard, which is confusing.
|
||||||
|
local displayKeyConstant = true
|
||||||
|
|
||||||
|
-- Setup
|
||||||
|
love.mouse.setVisible(false)
|
||||||
|
|
||||||
|
-- Button detection
|
||||||
|
-- FIXME love callbacks do something cleaner
|
||||||
|
local buttonsInUse = {}
|
||||||
|
local axesInUse = {}
|
||||||
|
function love.keypressed(key, scancode, isrepeat)
|
||||||
|
if useScancodes then key = scancode end
|
||||||
|
buttonsInUse["keyboard."..key] = true
|
||||||
|
end
|
||||||
|
function love.keyreleased(key, scancode)
|
||||||
|
if useScancodes then key = scancode end
|
||||||
|
buttonsInUse["keyboard."..key] = nil
|
||||||
|
end
|
||||||
|
function love.mousepressed(x, y, button, istouch)
|
||||||
|
buttonsInUse["mouse."..button] = true
|
||||||
|
end
|
||||||
|
function love.mousereleased(x, y, button, istouch)
|
||||||
|
buttonsInUse["mouse."..button] = nil
|
||||||
|
end
|
||||||
|
function love.wheelmoved(x, y)
|
||||||
|
if y > 0 then
|
||||||
|
buttonsInUse["mouse.wheel.up"] = true
|
||||||
|
elseif y < 0 then
|
||||||
|
buttonsInUse["mouse.wheel.down"] = true
|
||||||
|
end
|
||||||
|
if x > 0 then
|
||||||
|
buttonsInUse["mouse.wheel.right"] = true
|
||||||
|
elseif x < 0 then
|
||||||
|
buttonsInUse["mouse.wheel.left"] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function love.mousemoved(x, y, dx, dy)
|
||||||
|
if dx ~= 0 then axesInUse["mouse.move.x"] = dx/love.graphics.getWidth() end
|
||||||
|
if dy ~= 0 then axesInUse["mouse.move.y"] = dy/love.graphics.getHeight() end
|
||||||
|
end
|
||||||
|
function love.gamepadpressed(joystick, button)
|
||||||
|
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = true
|
||||||
|
end
|
||||||
|
function love.gamepadreleased(joystick, button)
|
||||||
|
buttonsInUse["gamepad.button."..joystick:getID().."."..button] = nil
|
||||||
|
end
|
||||||
|
function love.gamepadaxis(joystick, axis, value)
|
||||||
|
if value ~= 0 then
|
||||||
|
axesInUse["gamepad.axis."..joystick:getID().."."..axis] = value
|
||||||
|
else
|
||||||
|
axesInUse["gamepad.axis."..joystick:getID().."."..axis] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Windows size
|
||||||
|
input.drawWidth, input.drawHeight = love.graphics.getWidth(), love.graphics.getHeight()
|
||||||
|
function love.resize(width, height)
|
||||||
|
input.drawWidth = width
|
||||||
|
input.drawHeight = height
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Update
|
||||||
|
local oUpdate = input.update
|
||||||
|
input.update = function(dt)
|
||||||
|
-- love.wheelmoved doesn't trigger when the wheel stop moving, so we need to clear up our stuff at each update
|
||||||
|
buttonsInUse["mouse.wheel.up"] = nil
|
||||||
|
buttonsInUse["mouse.wheel.down"] = nil
|
||||||
|
buttonsInUse["mouse.wheel.right"] = nil
|
||||||
|
buttonsInUse["mouse.wheel.left"] = nil
|
||||||
|
-- Same for mouse axis
|
||||||
|
axesInUse["mouse.move.x"] = nil
|
||||||
|
axesInUse["mouse.move.y"] = nil
|
||||||
|
|
||||||
|
oUpdate(dt)
|
||||||
|
end
|
||||||
|
|
||||||
|
input.basicButtonDetector = function(id)
|
||||||
|
-- Keyboard
|
||||||
|
if id:match("^keyboard%.") then
|
||||||
|
local key = id:match("^keyboard%.(.+)$")
|
||||||
|
return function()
|
||||||
|
return useScancodes and love.keyboard.isScancodeDown(key) or love.keyboard.isDown(key)
|
||||||
|
end
|
||||||
|
-- Mouse wheel
|
||||||
|
elseif id:match("^mouse%.wheel%.") then
|
||||||
|
local key = id:match("^mouse%.wheel%.(.+)$")
|
||||||
|
return function()
|
||||||
|
return buttonsInUse["mouse.wheel."..key]
|
||||||
|
end
|
||||||
|
-- Mouse
|
||||||
|
elseif id:match("^mouse%.") then
|
||||||
|
local key = id:match("^mouse%.(.+)$")
|
||||||
|
return function()
|
||||||
|
return love.mouse.isDown(key)
|
||||||
|
end
|
||||||
|
-- Gamepad button
|
||||||
|
elseif id:match("^gamepad%.button%.") then
|
||||||
|
local gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
|
||||||
|
gid = tonumber(gid)
|
||||||
|
return function()
|
||||||
|
local gamepad
|
||||||
|
for _,j in ipairs(love.joystick.getJoysticks()) do
|
||||||
|
if j:getID() == gid then gamepad = j end
|
||||||
|
end
|
||||||
|
return gamepad and gamepad:isGamepadDown(key)
|
||||||
|
end
|
||||||
|
-- Gamepad axis
|
||||||
|
elseif id:match("^gamepad%.axis%.") then
|
||||||
|
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
||||||
|
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
||||||
|
gid = tonumber(gid)
|
||||||
|
threshold = tonumber(threshold) or 0.1
|
||||||
|
return function()
|
||||||
|
local gamepad
|
||||||
|
for _,j in ipairs(love.joystick.getJoysticks()) do
|
||||||
|
if j:getID() == gid then gamepad = j end
|
||||||
|
end
|
||||||
|
if not gamepad then
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
local val = gamepad:getGamepadAxis(axis)
|
||||||
|
return (math.abs(val) > math.abs(threshold)) and ((val < 0) == (threshold < 0))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error("Unknown button identifier: "..id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
input.basicAxisDetector = function(id)
|
||||||
|
-- Mouse movement
|
||||||
|
if id:match("^mouse%.move%.") then
|
||||||
|
local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
return function()
|
||||||
|
local val, raw, max = axesInUse["mouse.move."..axis] or 0, 0, 1
|
||||||
|
if axis == "x" then
|
||||||
|
raw, max = val * love.graphics.getWidth(), love.graphics.getWidth()
|
||||||
|
elseif axis == "y" then
|
||||||
|
raw, max = val * love.graphics.getHeight(), love.graphics.getHeight()
|
||||||
|
end
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end
|
||||||
|
-- Mouse position
|
||||||
|
elseif id:match("^mouse%.position%.") then
|
||||||
|
local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
return function()
|
||||||
|
local val, raw, max = 0, 0, 1
|
||||||
|
if axis == "x" then
|
||||||
|
max = love.graphics.getWidth() / 2 -- /2 because x=0,y=0 is the center of the screen (an axis value is in [-1,1])
|
||||||
|
raw = love.mouse.getX() - max
|
||||||
|
elseif axis == "y" then
|
||||||
|
max = love.graphics.getHeight() / 2
|
||||||
|
raw = love.mouse.getY() - max
|
||||||
|
end
|
||||||
|
val = raw / max
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0, raw, max
|
||||||
|
end
|
||||||
|
-- Gamepad axis
|
||||||
|
elseif id:match("^gamepad%.axis%.") then
|
||||||
|
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
||||||
|
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
||||||
|
gid = tonumber(gid)
|
||||||
|
threshold = tonumber(threshold) or 0.1
|
||||||
|
return function()
|
||||||
|
local gamepad
|
||||||
|
for _,j in ipairs(love.joystick.getJoysticks()) do
|
||||||
|
if j:getID() == gid then gamepad = j end
|
||||||
|
end
|
||||||
|
if not gamepad then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
local val = gamepad:getGamepadAxis(axis)
|
||||||
|
return math.abs(val) > math.abs(threshold) and val or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error("Unknown axis identifier: "..id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
input.buttonsInUse = function(threshold)
|
||||||
|
local r = {}
|
||||||
|
threshold = threshold or 0.5
|
||||||
|
for b in pairs(buttonsInUse) do
|
||||||
|
table.insert(r, b)
|
||||||
|
end
|
||||||
|
for b,v in pairs(axesInUse) do
|
||||||
|
if math.abs(v) > threshold then
|
||||||
|
table.insert(r, b.."%"..(v < 0 and -threshold or threshold))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
|
||||||
|
input.axesInUse = function(threshold)
|
||||||
|
local r = {}
|
||||||
|
threshold = threshold or 0.5
|
||||||
|
for b,v in pairs(axesInUse) do
|
||||||
|
if math.abs(v) > threshold then
|
||||||
|
table.insert(r, b.."%"..threshold)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
|
||||||
|
input.buttonName = function(...)
|
||||||
|
local ret = {}
|
||||||
|
for _,id in ipairs({...}) do
|
||||||
|
-- Keyboard
|
||||||
|
if id:match("^keyboard%.") then
|
||||||
|
local key = id:match("^keyboard%.(.+)$")
|
||||||
|
if useScancodes and displayKeyConstant then key = love.keyboard.getKeyFromScancode(key) end
|
||||||
|
table.insert(ret, key:sub(1,1):upper()..key:sub(2).." key")
|
||||||
|
-- Mouse wheel
|
||||||
|
elseif id:match("^mouse%.wheel%.") then
|
||||||
|
local key = id:match("^mouse%.wheel%.(.+)$")
|
||||||
|
table.insert(ret, "Mouse wheel "..key)
|
||||||
|
-- Mouse
|
||||||
|
elseif id:match("^mouse%.") then
|
||||||
|
local key = id:match("^mouse%.(.+)$")
|
||||||
|
table.insert(ret, "Mouse "..key)
|
||||||
|
-- Gamepad button
|
||||||
|
elseif id:match("^gamepad%.button%.") then
|
||||||
|
local gid, key = id:match("^gamepad%.button%.(.+)%.(.+)$")
|
||||||
|
table.insert(ret, "Gamepad "..gid.." button "..key)
|
||||||
|
-- Gamepad axis
|
||||||
|
elseif id:match("^gamepad%.axis%.") then
|
||||||
|
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
||||||
|
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0.1
|
||||||
|
if axis == "rightx" then
|
||||||
|
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
||||||
|
elseif axis == "righty" then
|
||||||
|
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
||||||
|
elseif axis == "leftx" then
|
||||||
|
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
||||||
|
elseif axis == "lefty" then
|
||||||
|
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
||||||
|
else
|
||||||
|
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(ret, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
input.axisName = function(...)
|
||||||
|
local ret = {}
|
||||||
|
for _,id in ipairs({...}) do
|
||||||
|
-- Binary axis
|
||||||
|
if id:match(".+%,.+") then
|
||||||
|
local b1, b2 = input.buttonName(id:match("^(.+)%,(.+)$"))
|
||||||
|
table.insert(ret, b1.." / "..b2)
|
||||||
|
-- Mouse movement
|
||||||
|
elseif id:match("^mouse%.move%.") then
|
||||||
|
local axis, threshold = id:match("^mouse%.move%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^mouse%.move%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, ("Mouse %s movement (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
-- Mouse position
|
||||||
|
elseif id:match("^mouse%.position%.") then
|
||||||
|
local axis, threshold = id:match("^mouse%.position%.(.+)%%(.+)$")
|
||||||
|
if not axis then axis = id:match("^mouse%.position%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0
|
||||||
|
table.insert(ret, ("Mouse %s position (threshold %s%%)"):format(axis, math.abs(threshold*100)))
|
||||||
|
-- Gamepad axis
|
||||||
|
elseif id:match("^gamepad%.axis%.") then
|
||||||
|
local gid, axis, threshold = id:match("^gamepad%.axis%.(.+)%.(.+)%%(.+)$")
|
||||||
|
if not gid then gid, axis = id:match("^gamepad%.axis%.(.+)%.(.+)$") end -- no threshold (=0)
|
||||||
|
threshold = tonumber(threshold) or 0.1
|
||||||
|
if axis == "rightx" then
|
||||||
|
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
||||||
|
elseif axis == "righty" then
|
||||||
|
table.insert(ret, ("Gamepad %s right stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
||||||
|
elseif axis == "leftx" then
|
||||||
|
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "right" or "left", math.abs(threshold*100)))
|
||||||
|
elseif axis == "lefty" then
|
||||||
|
table.insert(ret, ("Gamepad %s left stick %s (deadzone %s%%)"):format(gid, threshold >= 0 and "down" or "up", math.abs(threshold*100)))
|
||||||
|
else
|
||||||
|
table.insert(ret, ("Gamepad %s axis %s (deadzone %s%%)"):format(gid, axis, math.abs(threshold*100)))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(ret, id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return unpack(ret)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default inputs.
|
||||||
|
input.default.pointer:bind(
|
||||||
|
{ "absolute", { "keyboard.left", "keyboard.right" }, { "keyboard.up", "keyboard.down" } },
|
||||||
|
{ "absolute", { "keyboard.a", "keyboard.d" }, { "keyboard.w", "keyboard.s" } },
|
||||||
|
{ "absolute", "gamepad.axis.1.leftx", "gamepad.axis.1.lefty" },
|
||||||
|
{ "absolute", { "gamepad.button.1.dpleft", "gamepad.button.1.dpright" }, { "gamepad.button.1.dpup", "gamepad.button.1.dpdown" } }
|
||||||
|
)
|
||||||
|
input.default.confirm:bind(
|
||||||
|
"keyboard.return", "keyboard.space", "keyboard.lshift", "keyboard.e",
|
||||||
|
"gamepad.button.1.a"
|
||||||
|
)
|
||||||
|
input.default.cancel:bind(
|
||||||
|
"keyboard.escape", "keyboard.backspace",
|
||||||
|
"gamepad.button.1.b"
|
||||||
|
)
|
||||||
|
|
||||||
|
return input
|
||||||
14
input/init.lua
Normal file
14
input/init.lua
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
local input
|
||||||
|
|
||||||
|
local p = ...
|
||||||
|
if love then
|
||||||
|
input = require(p..".backend.love")
|
||||||
|
elseif package.loaded["ctr"] then
|
||||||
|
input = require(p..".backend.ctrulua")
|
||||||
|
elseif package.loaded["libretro"] then
|
||||||
|
error("NYI")
|
||||||
|
else
|
||||||
|
error("no backend for ubiquitousse.input")
|
||||||
|
end
|
||||||
|
|
||||||
|
return input
|
||||||
711
input/input.lua
Normal file
711
input/input.lua
Normal file
|
|
@ -0,0 +1,711 @@
|
||||||
|
--- ubiquitousse.input
|
||||||
|
-- Depends on a backend.
|
||||||
|
|
||||||
|
-- TODO: some key selection helper? Will be backend-implemented, to account for all the possible input methods.
|
||||||
|
-- TODO: some way to list all possible input / outputs, or make the *inUse make some separation between inputs indiscutitably in use and those who are incertain.
|
||||||
|
-- TODO: outputs! (rumble, lights, I don't know)
|
||||||
|
-- TODO: other, optional, default/generic inputs, and a way to know if they are binded.
|
||||||
|
-- TODO: multiplayer input helpers? something like getting the same input for different players, or default inputs for different players
|
||||||
|
|
||||||
|
local input
|
||||||
|
local sqrt = math.sqrt
|
||||||
|
local unpack = table.unpack or unpack
|
||||||
|
local dt = 0
|
||||||
|
|
||||||
|
--- Used to store inputs which were updated this frame
|
||||||
|
-- { Input: true, ... }
|
||||||
|
-- This table is for internal use and shouldn't be used from an external script.
|
||||||
|
local updated = {}
|
||||||
|
|
||||||
|
--- ButtonInput methods
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
local button_mt = {
|
||||||
|
--- Returns a new ButtonInput with the same properties.
|
||||||
|
-- @treturn ButtonInput the cloned object
|
||||||
|
clone = function(self)
|
||||||
|
return input.button(unpack(self.detectors))
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Bind new ButtonDetector(s) to this input.
|
||||||
|
-- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to add
|
||||||
|
-- @treturn ButtonInput this ButtonInput object
|
||||||
|
bind = function(self, ...)
|
||||||
|
for _, d in ipairs({...}) do
|
||||||
|
table.insert(self.detectors, input.buttonDetector(d))
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
--- Unbind ButtonDetector(s).
|
||||||
|
-- @tparam ButtonDetectors ... buttons detectors or buttons identifiers to remove
|
||||||
|
-- @treturn ButtonInput this ButtonInput object
|
||||||
|
unbind = function(self, ...)
|
||||||
|
for _, d in ipairs({...}) do
|
||||||
|
for i, detector in ipairs(self.detectors) do
|
||||||
|
if detector == d then
|
||||||
|
table.remove(self.detectors, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
--- Unbind all ButtonDetector(s).
|
||||||
|
-- @treturn ButtonInput this ButtonInput object
|
||||||
|
clear = function(self)
|
||||||
|
self.detectors = {}
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Grabs the input.
|
||||||
|
-- This function returns a new input object which mirrors the current object, except it will grab every new input.
|
||||||
|
-- This means any new button press/down/release will only be visible to the new object; the button will always appear unpressed for the initial object.
|
||||||
|
-- This is useful for contextual input, for example if you want to display a menu without pausing the game: the menu
|
||||||
|
-- can grab relevant inputs while it is open, so they don't trigger any action in the rest of the game.
|
||||||
|
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
|
||||||
|
-- @treturn ButtonInput the new input object which is grabbing the input
|
||||||
|
grab = function(self)
|
||||||
|
local grabbed = setmetatable({}, { __index = self, __newindex = self })
|
||||||
|
table.insert(self.grabStack, grabbed)
|
||||||
|
self.grabbing = grabbed
|
||||||
|
return grabbed
|
||||||
|
end,
|
||||||
|
--- Release the input that was grabbed by this object.
|
||||||
|
-- Input will be given back to the previous object.
|
||||||
|
-- @treturn ButtonInput this ButtonInput object
|
||||||
|
release = function(self)
|
||||||
|
local grabStack = self.grabStack
|
||||||
|
for i, v in ipairs(grabStack) do
|
||||||
|
if v == self then
|
||||||
|
table.remove(grabStack, i)
|
||||||
|
self.grabbing = grabStack[#grabStack]
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
end
|
||||||
|
error("This object is currently not grabbing this input")
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Returns true if the input was just pressed.
|
||||||
|
-- @treturn boolean true if the input was pressed, false otherwise
|
||||||
|
pressed = function(self)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
return self.state == "pressed"
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns true if the input was just released.
|
||||||
|
-- @treturn boolean true if the input was released, false otherwise
|
||||||
|
released = function(self)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
return self.state == "released"
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns true if the input is down.
|
||||||
|
-- @treturn boolean true if the input is currently down, false otherwise
|
||||||
|
down = function(self)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
local state = self.state
|
||||||
|
return state == "down" or state == "pressed"
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns true if the input is up.
|
||||||
|
-- @treturn boolean true if the input is currently up, false otherwise
|
||||||
|
up = function(self)
|
||||||
|
return not self:down()
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Update button state.
|
||||||
|
-- Automatically called, don't call unless you know what you're doing.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
update = function(self)
|
||||||
|
if not updated[self] then
|
||||||
|
local down = false
|
||||||
|
for _, d in ipairs(self.detectors) do
|
||||||
|
if d() then
|
||||||
|
down = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local state = self.state
|
||||||
|
if down then
|
||||||
|
if state == "none" or state == "released" then
|
||||||
|
self.state = "pressed"
|
||||||
|
else
|
||||||
|
self.state = "down"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if state == "down" or state == "pressed" then
|
||||||
|
self.state = "released"
|
||||||
|
else
|
||||||
|
self.state = "none"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
updated[self] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
button_mt.__index = button_mt
|
||||||
|
|
||||||
|
--- AxisInput methods
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
local axis_mt = {
|
||||||
|
--- Returns a new AxisInput with the same properties.
|
||||||
|
-- @treturn AxisInput the cloned object
|
||||||
|
clone = function(self)
|
||||||
|
return input.axis(unpack(self.detectors))
|
||||||
|
:threshold(self.threshold)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Bind new AxisDetector(s) to this input.
|
||||||
|
-- @tparam AxisDetectors ... axis detectors or axis identifiers to add
|
||||||
|
-- @treturn AxisInput this AxisInput object
|
||||||
|
bind = function(self, ...)
|
||||||
|
for _,d in ipairs({...}) do
|
||||||
|
table.insert(self.detectors, input.axisDetector(d))
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
--- Unbind AxisDetector(s).
|
||||||
|
-- @tparam AxisDetectors ... axis detectors or axis identifiers to remove
|
||||||
|
-- @treturn AxisInput this AxisInput object
|
||||||
|
unbind = button_mt.unbind,
|
||||||
|
--- Unbind all AxisDetector(s).
|
||||||
|
-- @treturn AxisInput this AxisInput object
|
||||||
|
clear = button_mt.clear,
|
||||||
|
|
||||||
|
--- Grabs the input.
|
||||||
|
-- This function returns a new input object which mirrors the current object, except it will grab every new input.
|
||||||
|
-- This means any value change will only be visible to the new object; the axis will always appear to be at 0 for the initial object.
|
||||||
|
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
|
||||||
|
-- @treturn AxisInput the new input object which is grabbing the input
|
||||||
|
grab = function(self)
|
||||||
|
local grabbed
|
||||||
|
grabbed = setmetatable({
|
||||||
|
positive = input.button(function() return grabbed:value() > 0 end),
|
||||||
|
negative = input.button(function() return grabbed:value() < 0 end)
|
||||||
|
}, { __index = self, __newindex = self })
|
||||||
|
table.insert(self.grabStack, grabbed)
|
||||||
|
self.grabbing = grabbed
|
||||||
|
return grabbed
|
||||||
|
end,
|
||||||
|
--- Release the input that was grabbed by this object.
|
||||||
|
-- Input will be given back to the previous object.
|
||||||
|
-- @treturn AxisInput this AxisInput object
|
||||||
|
release = button_mt.release,
|
||||||
|
|
||||||
|
--- Sets the default detection threshold (deadzone).
|
||||||
|
-- @tparam number new the new detection threshold
|
||||||
|
-- @treturn AxisInput this AxisInput object
|
||||||
|
threshold = function(self, new)
|
||||||
|
self.threshold = tonumber(new)
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Returns the value of the input (between -1 and 1).
|
||||||
|
-- @tparam[opt=default threshold] number threshold value to use
|
||||||
|
-- @treturn number the input value
|
||||||
|
value = function(self, curThreshold)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
local val = self.val
|
||||||
|
return math.abs(val) > math.abs(curThreshold or self.threshold) and val or 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns the change in value of the input since last update (between -2 and 2).
|
||||||
|
-- @treturn number the value delta
|
||||||
|
delta = function(self)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
return self.dval
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns the raw value of the input (between -max and +max).
|
||||||
|
-- @tparam[opt=default threshold*max] number raw threshold value to use
|
||||||
|
-- @treturn number the input raw value
|
||||||
|
raw = function(self, rawThreshold)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
local raw = self.raw
|
||||||
|
return math.abs(raw) > math.abs(rawThreshold or self.threshold*self.max) and raw or 0
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Return the raw max of the input.
|
||||||
|
-- @treturn number the input raw max
|
||||||
|
max = function(self)
|
||||||
|
self:update()
|
||||||
|
return self.max
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- The associated button pressed when the axis reaches a positive value.
|
||||||
|
positive = nil,
|
||||||
|
--- The associated button pressed when the axis reaches a negative value.
|
||||||
|
negative = nil,
|
||||||
|
|
||||||
|
--- Update axis state.
|
||||||
|
-- Automatically called, don't call unless you know what you're doing.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
update = function(self)
|
||||||
|
if not updated[self] then
|
||||||
|
local val, raw, max = 0, 0, 1
|
||||||
|
for _, d in ipairs(self.detectors) do
|
||||||
|
local v, r, m = d() -- v[-1,1], r[-m,+m]
|
||||||
|
if math.abs(v) > math.abs(val) then
|
||||||
|
val, raw, max = v, r or v, m or 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.dval = val - self.val
|
||||||
|
self.val, self.raw, self.max = val, raw, max
|
||||||
|
updated[self] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
axis_mt.__index = axis_mt
|
||||||
|
|
||||||
|
--- PointerInput methods
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
local pointer_mt = {
|
||||||
|
--- Returns a new PointerInput with the same properties.
|
||||||
|
-- @treturn PointerInput the cloned object
|
||||||
|
clone = function(self)
|
||||||
|
return input.pointer(unpack(self.detectors))
|
||||||
|
:dimensions(self.width, self.height)
|
||||||
|
:offset(self.offsetX, self.offsetY)
|
||||||
|
:speed(self.xSpeed, self.ySpeed)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Bind new axis couples to this input.
|
||||||
|
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
bind = function(self, ...)
|
||||||
|
for _, p in ipairs({...}) do
|
||||||
|
if type(p) == "table" then
|
||||||
|
local h, v = p[2], p[3]
|
||||||
|
if getmetatable(h) ~= axis_mt then
|
||||||
|
h = input.axis(h)
|
||||||
|
end
|
||||||
|
if getmetatable(v) ~= axis_mt then
|
||||||
|
v = input.axis(v)
|
||||||
|
end
|
||||||
|
table.insert(self.detectors, { p[1], h, v })
|
||||||
|
else
|
||||||
|
error("Pointer detector must be a table")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
--- Unbind axis couple(s).
|
||||||
|
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to remove
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
unbind = button_mt.unbind,
|
||||||
|
--- Unbind all axis couple(s).
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
clear = button_mt.clear,
|
||||||
|
|
||||||
|
--- Grabs the input.
|
||||||
|
-- This function returns a new input object which mirrors the current object, except it will grab every new input.
|
||||||
|
-- This means any value change will only be visible to the new object; the pointer will always appear to be at offsetX,offsetY for the initial object.
|
||||||
|
-- An input can be grabbed several times; the one which grabbed it last will be the active one.
|
||||||
|
-- @treturn PointerInput the new input object which is grabbing the input
|
||||||
|
grab = function(self)
|
||||||
|
local grabbed
|
||||||
|
grabbed = {
|
||||||
|
horizontal = input.axis(function()
|
||||||
|
local h = grabbed:x()
|
||||||
|
local width = grabbed.width
|
||||||
|
return h/width, h, width
|
||||||
|
end),
|
||||||
|
vertical = input.axis(function()
|
||||||
|
local v = grabbed:y()
|
||||||
|
local height = grabbed.height
|
||||||
|
return v/height, v, height
|
||||||
|
end)
|
||||||
|
}
|
||||||
|
grabbed.right, grabbed.left = grabbed.horizontal.positive, grabbed.horizontal.negative
|
||||||
|
grabbed.up, grabbed.down = grabbed.vertical.negative, grabbed.vertical.positive
|
||||||
|
setmetatable(grabbed, { __index = self, __newindex = self })
|
||||||
|
table.insert(self.grabStack, grabbed)
|
||||||
|
self.grabbing = grabbed
|
||||||
|
return grabbed
|
||||||
|
end,
|
||||||
|
--- Release the input that was grabbed by this object.
|
||||||
|
-- Input will be given back to the previous object.
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
release = button_mt.release,
|
||||||
|
|
||||||
|
--- Set the moving area half-dimensions.
|
||||||
|
-- Call without argument to use half the window dimensions.
|
||||||
|
-- It's the half dimensions because axes values goes from -1 to 1, so theses dimensions only
|
||||||
|
-- covers values from x=0,y=0 to x=1,y=1. The full moving area will be 4*newWidth*newHeight.
|
||||||
|
-- @tparam number newWidth new width
|
||||||
|
-- @tparam number newHeight new height
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
dimensions = function(self, newWidth, newHeight)
|
||||||
|
self.width, self.height = newWidth, newHeight
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
--- Set the moving area coordinates offset.
|
||||||
|
-- The offset is a value automatically added to the x and y values when using the x() and y() methods.
|
||||||
|
-- Call without argument to automatically offset so 0,0 <= x(),y() <= width,height, i.e. offset to width,height.
|
||||||
|
-- @tparam number newOffX new X offset
|
||||||
|
-- @tparam number newOffY new Y offset
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
offset = function(self, newOffX, newOffY)
|
||||||
|
self.offsetX, self.offsetY = newOffX, newOffY
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
--- Set maximal speed (pixels-per-milisecond)
|
||||||
|
-- Only used in relative mode.
|
||||||
|
-- Calls without argument to use the raw data and don't apply a speed modifier.
|
||||||
|
-- @tparam number newXSpeed new X speed
|
||||||
|
-- @tparam number newYSpeed new Y speed
|
||||||
|
-- @treturn PointerInput this PointerInput object
|
||||||
|
speed = function(self, newXSpeed, newYSpeed)
|
||||||
|
self.xSpeed, self.ySpeed = newXSpeed, newYSpeed or newXSpeed
|
||||||
|
return self
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Returns the current X value of the pointer.
|
||||||
|
-- @treturn number X value
|
||||||
|
x = function(self)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
return self.valX + (self.offsetX or self.width or input.drawWidth/2)
|
||||||
|
else
|
||||||
|
return self.offsetX or self.width or input.drawWidth/2
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
--- Returns the current Y value of the pointer.
|
||||||
|
-- @treturn number Y value
|
||||||
|
y = function(self)
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
return self.valY + (self.offsetY or self.height or input.drawHeight/2)
|
||||||
|
else
|
||||||
|
return self.offsetY or self.height or input.drawHeight/2
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Returns the X and Y value of the pointer, clamped.
|
||||||
|
-- They are clamped to stay in the ellipse touching all 4 sides of the dimension rectangle, i.e. the
|
||||||
|
-- (x,y) vector's magnitude reached its maximum either in (0,height) or (width,0).
|
||||||
|
-- Typically, this is used with square dimensions for player movements: when moving diagonally, the magnitude
|
||||||
|
-- will be the same as when moving horiontally or vertically, thus avoiding faster diagonal movement, A.K.A. "straferunning".
|
||||||
|
-- If you're not conviced by my overly complicated explanation: just use this to retrieve x and y for movement and everything
|
||||||
|
-- will be fine.
|
||||||
|
-- @treturn number X value
|
||||||
|
-- @treturn number Y value
|
||||||
|
clamped = function(self)
|
||||||
|
local width, height = self.width, self.height
|
||||||
|
if self.grabbing == self then
|
||||||
|
self:update()
|
||||||
|
local x, y = self.valX, self.valY
|
||||||
|
local cx, cy = x, y
|
||||||
|
local normalizedMagnitude = (x*x)/(width*width) + (y*y)/(height*height) -- go back to a unit circle
|
||||||
|
if normalizedMagnitude > 1 then
|
||||||
|
local magnitude = sqrt(x*x + y*y)
|
||||||
|
cx, cy = cx / magnitude * width, cy / magnitude * height
|
||||||
|
end
|
||||||
|
return cx + (self.offsetX or width or input.drawWidth/2), cy + (self.offsetY or height or input.drawHeight/2)
|
||||||
|
else
|
||||||
|
return self.offsetX or width or input.drawWidth/2, self.offsetY or height or input.drawHeight/2
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- The associated horizontal axis.
|
||||||
|
horizontal = nil,
|
||||||
|
--- The associated vertical axis.
|
||||||
|
vertical = nil,
|
||||||
|
|
||||||
|
--- The associated button pressed when the pointer goes to the right.
|
||||||
|
right = nil,
|
||||||
|
--- The associated button pressed when the pointer goes to the left.
|
||||||
|
left = nil,
|
||||||
|
--- The associated button pressed when the pointer points up.
|
||||||
|
up = nil,
|
||||||
|
--- The associated button pressed when the pointer points down.
|
||||||
|
down = nil,
|
||||||
|
|
||||||
|
--- Update pointer state.
|
||||||
|
-- Automatically called, don't call unless you know what you're doing.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
update = function(self)
|
||||||
|
if not updated[self] then
|
||||||
|
local x, y = self.valX, self.valY
|
||||||
|
local xSpeed, ySpeed = self.xSpeed, self.ySpeed
|
||||||
|
local width, height = self.width or input.drawWidth/2, self.height or input.drawHeight/2
|
||||||
|
local newX, newY = x, y
|
||||||
|
local maxMovX, maxMovY = 0, 0 -- the maxium axis movement in a direction (used to determine which axes have the priority) (absolute value)
|
||||||
|
for _, pointer in ipairs(self.detectors) do
|
||||||
|
local mode, xAxis, yAxis = unpack(pointer)
|
||||||
|
if mode == "relative" then
|
||||||
|
local movX, movY = math.abs(xAxis:value()), math.abs(yAxis:value())
|
||||||
|
if movX > maxMovX then
|
||||||
|
newX = x + (xSpeed and (xAxis:value() * xSpeed * dt) or xAxis:raw())
|
||||||
|
maxMovX = movX
|
||||||
|
end
|
||||||
|
if movY > maxMovY then
|
||||||
|
newY = y + (ySpeed and (yAxis:value() * ySpeed * dt) or yAxis:raw())
|
||||||
|
maxMovY = movY
|
||||||
|
end
|
||||||
|
elseif mode == "absolute" then
|
||||||
|
local movX, movY = math.abs(xAxis:delta()), math.abs(yAxis:delta())
|
||||||
|
if movX > maxMovX then
|
||||||
|
newX = xAxis:value() * width
|
||||||
|
maxMovX = movX
|
||||||
|
end
|
||||||
|
if movY > maxMovY then
|
||||||
|
newY = yAxis:value() * height
|
||||||
|
maxMovY = movY
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.valX, self.valY = math.min(math.abs(newX), width) * (newX < 0 and -1 or 1), math.min(math.abs(newY), height) * (newY < 0 and -1 or 1)
|
||||||
|
updated[self] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
pointer_mt.__index = pointer_mt
|
||||||
|
|
||||||
|
--- Input stuff
|
||||||
|
-- Inspired by Tactile by Andrew Minnich (https://github.com/tesselode/tactile), under the MIT license.
|
||||||
|
-- Ubiquitousse considers two basic input methods, called buttons (binary input) and axes (analog input).
|
||||||
|
input = {
|
||||||
|
---------------------------------
|
||||||
|
--- Detectors (input sources) ---
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
-- Buttons detectors --
|
||||||
|
-- A button detector is a function which returns true (pressed) or false (unpressed).
|
||||||
|
-- All buttons are identified using an identifier string, which depends on the backend. The presence of eg., a mouse or keyboard is not assumed.
|
||||||
|
-- Some identifier strings conventions: (not used internally by Ubiquitousse, but it's nice to have some consistency between backends)
|
||||||
|
-- They should be in the format "source1.source2.[...].button", for example "keyboard.up" or "gamepad.button.1.a" for the A-button of the first gamepad.
|
||||||
|
-- If the button is actually an axis (ie, the button is pressed if the axis value passes a certain threshold), the threshold should be in the end of the
|
||||||
|
-- identifier, preceded by a % : for example "gamepad.axis.1.leftx%-0.5" should return true when the left-stick of the first gamepad is moved to the right
|
||||||
|
-- by more of 50%. The negative threshold value means that the button will be pressed only when the axis has a negative value (in the example, it won't be
|
||||||
|
-- pressed when the axis is moved to the right).
|
||||||
|
|
||||||
|
--- Makes a new button detector from a identifier string.
|
||||||
|
-- The function may error if the identifier is incorrect.
|
||||||
|
-- @tparam string button identifier, depends on the platform Ubiquitousse is running on
|
||||||
|
-- @treturn the new button detector
|
||||||
|
-- @impl backend
|
||||||
|
basicButtonDetector = function(str) end,
|
||||||
|
|
||||||
|
--- Make a new button detector from a detector function of string.
|
||||||
|
-- @tparam string, function button identifier
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
buttonDetector = function(obj)
|
||||||
|
if type(obj) == "function" then
|
||||||
|
return obj
|
||||||
|
elseif type(obj) == "string" then
|
||||||
|
return input.basicButtonDetector(obj)
|
||||||
|
end
|
||||||
|
error(("Not a valid button detector: %s"):format(obj))
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- Axis detectors --
|
||||||
|
-- Similar to buttons detectors, but returns a number between -1 and 1.
|
||||||
|
-- Threshold value can be used similarly with %.
|
||||||
|
-- Axis detectors can also be defined by two buttons: if the 1rst button is pressed, value will be -1, if the 2nd is pressed it will be 1
|
||||||
|
-- and if none or the both are pressed, the value will be 0. This kind of axis identifier is a table {"button1", "button2"}.
|
||||||
|
-- Axis detectors may also optionally return after the number between -1 and 1 the raw value and max value. The raw value is between -max and +max.
|
||||||
|
|
||||||
|
--- Makes a new axis detector from a identifier string.
|
||||||
|
-- The function may error if the identifier is incorrect.
|
||||||
|
-- @tparam string axis identifier, depends on the platform Ubiquitousse is running on
|
||||||
|
-- @treturn the new axis detector
|
||||||
|
-- @impl backend
|
||||||
|
basicAxisDetector = function(str) end,
|
||||||
|
|
||||||
|
--- Make a new axis detector from a detector function, string, or a couple of buttons.
|
||||||
|
-- @tparam string, function or table axis identifier
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
axisDetector = function(obj)
|
||||||
|
if type(obj) == "function" then
|
||||||
|
return obj
|
||||||
|
elseif type(obj) == "string" then
|
||||||
|
return input.basicAxisDetector(obj)
|
||||||
|
elseif type(obj) == "table" then
|
||||||
|
local b1, b2 = input.buttonDetector(obj[1]), input.buttonDetector(obj[2])
|
||||||
|
return function()
|
||||||
|
local d1, d2 = b1(), b2()
|
||||||
|
if d1 and d2 then return 0
|
||||||
|
elseif d1 then return -1
|
||||||
|
elseif d2 then return 1
|
||||||
|
else return 0 end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
error(("Not a valid axis detector: %s"):format(obj))
|
||||||
|
end,
|
||||||
|
|
||||||
|
------------------------------------------
|
||||||
|
--- Inputs (the thing you want to use) ---
|
||||||
|
------------------------------------------
|
||||||
|
|
||||||
|
-- Buttons inputs --
|
||||||
|
-- Button input is a container for buttons detector. A button will be pressed when one of its detectors returns true.
|
||||||
|
-- Inputs also knows if the button was just pressed or released.
|
||||||
|
-- @tparam ButtonDetectors ... all the buttons detectors or buttons identifiers
|
||||||
|
-- @tretrun ButtonInput the object
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
button = function(...)
|
||||||
|
local r = setmetatable({
|
||||||
|
grabStack = {}, -- grabbers stack, last element is the object currently grabbing this input
|
||||||
|
grabbing = nil, -- object currently grabbing this input
|
||||||
|
detectors = {}, -- detectors list
|
||||||
|
state = "none" -- current state (none, pressed, down, released)
|
||||||
|
}, button_mt)
|
||||||
|
table.insert(r.grabStack, r)
|
||||||
|
r.grabbing = r
|
||||||
|
r:bind(...)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- Axis inputs --
|
||||||
|
-- Axis input is a container for axes detector. An axis input will return the value of the axis detector the most far away from their center (0).
|
||||||
|
-- Axis input provide a threshold setting ; every axis which has a distance to the center below the threshold (none by default) will be ignored.
|
||||||
|
-- @tparam AxisDetectors ... all the axis detectors or axis identifiers
|
||||||
|
-- @tretrun AxisInput the object
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
axis = function(...)
|
||||||
|
local r = setmetatable({
|
||||||
|
grabStack = {}, -- grabbers stack, last element is the object currently grabbing this input
|
||||||
|
grabbing = nil, -- object currently grabbing this input
|
||||||
|
detectors = {}, -- detectors list
|
||||||
|
val = 0, -- current value between -1 and 1
|
||||||
|
dval = 0, -- change between -2 and 2
|
||||||
|
raw = 0, -- raw value between -max and +max
|
||||||
|
max = 1, -- maximum for raw values
|
||||||
|
threshold = 0 -- ie., the deadzone
|
||||||
|
}, axis_mt)
|
||||||
|
table.insert(r.grabStack, r)
|
||||||
|
r.grabbing = r
|
||||||
|
r:bind(...)
|
||||||
|
r.positive = input.button(function() return r:value() > 0 end)
|
||||||
|
r.negative = input.button(function() return r:value() < 0 end)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- Pointer inputs --
|
||||||
|
-- Pointer inputs are container for two axes input, in order to represent a two-dimensionnal pointing device, e.g. a mouse or a stick.
|
||||||
|
-- Each pointer detector is a table with 3 fields: mode(string), XAxis(axis), YAxis(axis). mode can either be "relative" or "absolute".
|
||||||
|
-- In relative mode, the pointer will return the movement since last update (for example to move a mouse pointer with a stick).
|
||||||
|
-- In absolute mode, the pointer will return the pointer position directly deduced of the current axes position.
|
||||||
|
-- @tparam table{mode,XAxis,YAxis} ... couples of axis detectors, axis identifiers or axis input to add and in which mode
|
||||||
|
-- @tretrun PointerInput the object
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
pointer = function(...)
|
||||||
|
local r = setmetatable({
|
||||||
|
grabStack = {}, -- grabbers stack, first element is the object currently grabbing this input
|
||||||
|
grabbing = nil, -- object currently grabbing this input
|
||||||
|
detectors = {}, -- pointers list (composite detectors)
|
||||||
|
valX = 0, valY = 0, -- pointer position
|
||||||
|
width = 1, height = 1, -- half-dimensions of the movement area
|
||||||
|
offsetX = 0, offsetY = 0, -- offsets
|
||||||
|
xSpeed = 1, ySpeed = 1, -- speed (pixels/milisecond); for relative mode
|
||||||
|
}, pointer_mt)
|
||||||
|
table.insert(r.grabStack, r)
|
||||||
|
r.grabbing = r
|
||||||
|
r:bind(...)
|
||||||
|
r.horizontal = input.axis(function()
|
||||||
|
local h = r:x()
|
||||||
|
local width = r.width
|
||||||
|
return h/width, h, width
|
||||||
|
end)
|
||||||
|
r.vertical = input.axis(function()
|
||||||
|
local v = r:y()
|
||||||
|
local height = r.height
|
||||||
|
return v/height, v, height
|
||||||
|
end)
|
||||||
|
r.right, r.left = r.horizontal.positive, r.horizontal.negative
|
||||||
|
r.up, r.down = r.vertical.negative, r.vertical.positive
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
------------------------------
|
||||||
|
--- Input detection helpers --
|
||||||
|
------------------------------
|
||||||
|
-- TODO: make this better
|
||||||
|
|
||||||
|
--- Returns a list of the buttons currently in use, identified by their string button identifier.
|
||||||
|
-- This may also returns "axis threshold" buttons if an axis passes the threshold.
|
||||||
|
-- @treturn table<string> buttons identifiers list
|
||||||
|
-- @treturn[opt=0.5] number threshold the threshold to detect axes as button
|
||||||
|
-- @impl backend
|
||||||
|
buttonsInUse = function(threshold) end,
|
||||||
|
|
||||||
|
--- Returns a list of the axes currently in use, identified by their string axis identifier
|
||||||
|
-- @treturn table<string> axes identifiers list
|
||||||
|
-- @treturn[opt=0.5] number threshold the threshold to detect axes
|
||||||
|
-- @impl backend
|
||||||
|
axesInUse = function(threshold) end,
|
||||||
|
|
||||||
|
--- Returns a nice name for the button identifier.
|
||||||
|
-- Can be locale-depedant and stuff, it's only for display.
|
||||||
|
-- May returns the raw identifier if you're lazy.
|
||||||
|
-- @tparam string... button identifier string(s)
|
||||||
|
-- @treturn string... the displayable names
|
||||||
|
-- @impl backend
|
||||||
|
buttonName = function(...) end,
|
||||||
|
|
||||||
|
--- Returns a nice name for the axis identifier.
|
||||||
|
-- Can be locale-depedant and stuff, it's only for display.
|
||||||
|
-- May returns the raw identifier if you're lazy.
|
||||||
|
-- @tparam string... axis identifier string(s)
|
||||||
|
-- @treturn string... the displayable names
|
||||||
|
-- @impl backend
|
||||||
|
axisName = function(...) end,
|
||||||
|
|
||||||
|
-------------------
|
||||||
|
--- Other stuff ---
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
--- Some default inputs.
|
||||||
|
-- The backend should bind detectors to thoses inputs (don't recreate them).
|
||||||
|
-- These are used to provide some common input default detectors to allow to start a game quickly on
|
||||||
|
-- any platform without having to configure the keys.
|
||||||
|
-- If some key function in your game match one of theses defaults, using it instead of creating a new
|
||||||
|
-- input would be a good idea.
|
||||||
|
-- @impl mixed
|
||||||
|
default = {
|
||||||
|
pointer = nil, -- Pointer: used to move and select. Example binds: arrow keys, WASD, stick.
|
||||||
|
confirm = nil, -- Button: used to confirm something. Example binds: Enter, A button.
|
||||||
|
cancel = nil -- Button: used to cancel something. Example binds: Escape, B button.
|
||||||
|
},
|
||||||
|
|
||||||
|
--- Draw area dimensions.
|
||||||
|
-- Used for pointers.
|
||||||
|
-- @impl backend
|
||||||
|
drawWidth = 1,
|
||||||
|
drawHeight = 1,
|
||||||
|
|
||||||
|
--- Update all the Inputs.
|
||||||
|
-- Should be called at every game update; called by ubiquitousse.update.
|
||||||
|
-- The backend can hook into this function to to its input-related updates.
|
||||||
|
-- @tparam numder dt the delta-time
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
update = function(newDt)
|
||||||
|
dt = newDt
|
||||||
|
updated = {}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Create default inputs
|
||||||
|
input.default.pointer = input.pointer()
|
||||||
|
input.default.confirm = input.button()
|
||||||
|
input.default.cancel = input.button()
|
||||||
|
|
||||||
|
return input
|
||||||
1
scene/init.lua
Normal file
1
scene/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
return require((...)..".scene")
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
-- ubiquitousse.scene
|
--- ubiquitousse.scene
|
||||||
local uqt = require((...):match("^(.-ubiquitousse)%."))
|
-- Optional dependencies: ubiquitousse.time (to provide each scene a time registry)
|
||||||
local m = uqt.module
|
local loaded, time = pcall(require, (...):match("^.-ubiquitousse%.").."time")
|
||||||
|
if not loaded then time = nil end
|
||||||
|
|
||||||
--- Scene management.
|
--- Scene management.
|
||||||
-- You can use use scenes to seperate the different states of your game: for example, a menu scene and a game scene.
|
-- You can use use scenes to seperate the different states of your game: for example, a menu scene and a game scene.
|
||||||
|
|
@ -14,6 +15,7 @@ local m = uqt.module
|
||||||
-- of this is that you can load assets, libraries, etc. outside of the enter callback, so they can be cached and not reloaded each time
|
-- of this is that you can load assets, libraries, etc. outside of the enter callback, so they can be cached and not reloaded each time
|
||||||
-- the scene is entered, but all the other scene initialization should be done in the enter callback, since it won't be executed on
|
-- the scene is entered, but all the other scene initialization should be done in the enter callback, since it won't be executed on
|
||||||
-- each enter otherwise.
|
-- each enter otherwise.
|
||||||
|
-- FIXME: actually more useful to never cache?
|
||||||
-- The expected code-organisation is:
|
-- The expected code-organisation is:
|
||||||
-- * each scene is in a file, identified by its module name (same identifier used by Lua's require)
|
-- * each scene is in a file, identified by its module name (same identifier used by Lua's require)
|
||||||
-- * each scene file create a new scene table using ubiquitousse.scene.new and returns it at the end of the file
|
-- * each scene file create a new scene table using ubiquitousse.scene.new and returns it at the end of the file
|
||||||
|
|
@ -26,26 +28,42 @@ scene = setmetatable({
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
current = nil,
|
current = nil,
|
||||||
|
|
||||||
|
--- Shortcut for scene.current.time.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
time = nil,
|
||||||
|
|
||||||
--- The scene stack: list of scene, from the farest one to the nearest.
|
--- The scene stack: list of scene, from the farest one to the nearest.
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
stack = {},
|
stack = {},
|
||||||
|
|
||||||
--- A prefix for scene modules names
|
--- A prefix for scene modules names.
|
||||||
|
-- Will search in the "scene" directory by default. Redefine it to fit your own ridiculous filesystem.
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
prefix = "",
|
prefix = "scene.",
|
||||||
|
|
||||||
--- Function which load a scene file
|
--- Function which load a scene file.
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
load = function(scenePath)
|
load = function(sceneModule)
|
||||||
|
local scenePath = sceneModule:gsub("%.", "/")
|
||||||
for path in package.path:gmatch("[^;]+") do
|
for path in package.path:gmatch("[^;]+") do
|
||||||
path = path:gsub("%?", (scenePath:gsub("%.", "/")))
|
path = path:gsub("%?", scenePath)
|
||||||
local f = io.open(path)
|
local f = io.open(path)
|
||||||
if f then
|
if f then
|
||||||
f:close()
|
f:close()
|
||||||
return dofile(path)
|
return dofile(path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
error("can't find scene "..tostring(scenePath))
|
if package.loaded["candran"] then -- Candran support
|
||||||
|
for path in package.path:gsub("%.lua", ".can"):gmatch("[^;]+") do
|
||||||
|
path = path:gsub("%?", scenePath)
|
||||||
|
local f = io.open(path)
|
||||||
|
if f then
|
||||||
|
f:close()
|
||||||
|
return package.loaded["candran"].dofile(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
error("can't find scene "..tostring(sceneModule))
|
||||||
end,
|
end,
|
||||||
|
|
||||||
--- Creates and returns a new Scene object.
|
--- Creates and returns a new Scene object.
|
||||||
|
|
@ -55,7 +73,7 @@ scene = setmetatable({
|
||||||
return {
|
return {
|
||||||
name = name or "unamed", -- The scene name.
|
name = name or "unamed", -- The scene name.
|
||||||
|
|
||||||
time = m.time and uqt.time.new(), -- Scene-specific TimerRegistry, if uqt.time is enabled.
|
time = time and time.new(), -- Scene-specific TimerRegistry, if uqt.time is available.
|
||||||
|
|
||||||
enter = function(self, ...) end, -- Called when entering a scene.
|
enter = function(self, ...) end, -- Called when entering a scene.
|
||||||
exit = function(self) end, -- Called when exiting a scene, and not expecting to come back (scene may be unloaded).
|
exit = function(self) end, -- Called when exiting a scene, and not expecting to come back (scene may be unloaded).
|
||||||
|
|
@ -63,22 +81,24 @@ scene = setmetatable({
|
||||||
suspend = function(self) end, -- Called when suspending a scene, and expecting to come back (scene won't be unloaded).
|
suspend = function(self) end, -- Called when suspending a scene, and expecting to come back (scene won't be unloaded).
|
||||||
resume = function(self) end, -- Called when resuming a suspended scene (after calling suspend).
|
resume = function(self) end, -- Called when resuming a suspended scene (after calling suspend).
|
||||||
|
|
||||||
update = function(self, dt, ...) end, -- Called on each ubiquitousse.event.update on the current scene.
|
update = function(self, dt, ...) end, -- Called on each update on the current scene.
|
||||||
draw = function(self, ...) end -- Called on each ubiquitousse.event.draw on the current scene.
|
draw = function(self, ...) end -- Called on each draw on the current scene.
|
||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
|
-- TODO: handle love.quit / exit all scenes in stack
|
||||||
|
|
||||||
--- Switch to a new scene.
|
--- Switch to a new scene.
|
||||||
-- The new scene will be loaded and the current scene will be replaced by the new one,
|
-- The new scene will be loaded and the current scene will be replaced by the new one,
|
||||||
-- then the previous scene exit function will be called, then the enter callback is called on the new scence.
|
-- then the previous scene exit function will be called, then the enter callback is called on the new scence.
|
||||||
-- Then the stack is changed to replace the old scene with the new one.
|
-- Then the stack is changed to replace the old scene with the new one.
|
||||||
-- @tparam string scenePath the new scene module name
|
-- @tparam string/table scenePath the new scene module name, or the scene table directly
|
||||||
-- @param ... arguments to pass to the scene's enter function
|
-- @param ... arguments to pass to the scene's enter function
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
switch = function(scenePath, ...)
|
switch = function(scenePath, ...)
|
||||||
local previous = scene.current
|
local previous = scene.current
|
||||||
scene.current = scene.load(scene.prefix..scenePath)
|
scene.current = type(scenePath) == "string" and scene.load(scene.prefix..scenePath) or scenePath
|
||||||
scene.current.name = scene.current.name or scenePath
|
scene.time = scene.current.time
|
||||||
|
scene.current.name = scene.current.name or tostring(scenePath)
|
||||||
if previous then previous:exit() end
|
if previous then previous:exit() end
|
||||||
scene.current:enter(...)
|
scene.current:enter(...)
|
||||||
scene.stack[math.max(#scene.stack, 1)] = scene.current
|
scene.stack[math.max(#scene.stack, 1)] = scene.current
|
||||||
|
|
@ -88,13 +108,14 @@ scene = setmetatable({
|
||||||
-- Similar to ubiquitousse.scene.switch, except suspend is called on the current scene instead of exit,
|
-- Similar to ubiquitousse.scene.switch, except suspend is called on the current scene instead of exit,
|
||||||
-- and the current scene is not replaced: when the new scene call ubiquitousse.scene.pop, the old scene
|
-- and the current scene is not replaced: when the new scene call ubiquitousse.scene.pop, the old scene
|
||||||
-- will be reused.
|
-- will be reused.
|
||||||
-- @tparam string scenePath the new scene module name
|
-- @tparam string/table scenePath the new scene module name, or the scene table directly
|
||||||
-- @param ... arguments to pass to the scene's enter function
|
-- @param ... arguments to pass to the scene's enter function
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
push = function(scenePath, ...)
|
push = function(scenePath, ...)
|
||||||
local previous = scene.current
|
local previous = scene.current
|
||||||
scene.current = scene.load(scene.prefix..scenePath)
|
scene.current = type(scenePath) == "string" and scene.load(scene.prefix..scenePath) or scenePath
|
||||||
scene.current.name = scene.current.name or scenePath
|
scene.time = scene.current.time
|
||||||
|
scene.current.name = scene.current.name or tostring(scenePath)
|
||||||
if previous then previous:suspend() end
|
if previous then previous:suspend() end
|
||||||
scene.current:enter(...)
|
scene.current:enter(...)
|
||||||
table.insert(scene.stack, scene.current)
|
table.insert(scene.stack, scene.current)
|
||||||
|
|
@ -107,25 +128,26 @@ scene = setmetatable({
|
||||||
pop = function()
|
pop = function()
|
||||||
local previous = scene.current
|
local previous = scene.current
|
||||||
scene.current = scene.stack[#scene.stack-1]
|
scene.current = scene.stack[#scene.stack-1]
|
||||||
|
scene.time = scene.current.time
|
||||||
if previous then previous:exit() end
|
if previous then previous:exit() end
|
||||||
if scene.current then scene.current:resume() end
|
if scene.current then scene.current:resume() end
|
||||||
table.remove(scene.stack)
|
table.remove(scene.stack)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
--- Update the current scene.
|
--- Update the current scene.
|
||||||
-- Should be called in ubiquitousse.event.update.
|
-- Should be called at every game update; called by ubiquitousse.update.
|
||||||
-- @tparam number dt the delta-time (milisecond)
|
-- @tparam number dt the delta-time (milisecond)
|
||||||
-- @param ... arguments to pass to the scene's update function after dt
|
-- @param ... arguments to pass to the scene's update function after dt
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
update = function(dt, ...)
|
update = function(dt, ...)
|
||||||
if scene.current then
|
if scene.current then
|
||||||
if m.time then scene.current.time.update(dt) end
|
if time then scene.current.time:update(dt) end
|
||||||
scene.current:update(dt, ...)
|
scene.current:update(dt, ...)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
|
|
||||||
--- Draw the current scene.
|
--- Draw the current scene.
|
||||||
-- Should be called in ubiquitousse.event.draw.
|
-- Should be called every time the game is draw; called by ubiquitousse.draw.
|
||||||
-- @param ... arguments to pass to the scene's draw function
|
-- @param ... arguments to pass to the scene's draw function
|
||||||
-- @impl ubiquitousse
|
-- @impl ubiquitousse
|
||||||
draw = function(...)
|
draw = function(...)
|
||||||
333
time.lua
333
time.lua
|
|
@ -1,333 +0,0 @@
|
||||||
-- ubiquitousse.time
|
|
||||||
local ease = require((...):match("^(.-ubiquitousse)%.")..".lib.easing")
|
|
||||||
|
|
||||||
--- Returns true if all the values in the list are true ; functions in the list will be called and the test will be performed on their return value.
|
|
||||||
-- Returns default if the list is empty.
|
|
||||||
local function all(list, default)
|
|
||||||
if #list == 0 then
|
|
||||||
return default
|
|
||||||
else
|
|
||||||
local r = true
|
|
||||||
for _,v in ipairs(list) do
|
|
||||||
if type(v) == "function" then
|
|
||||||
r = r and v()
|
|
||||||
else
|
|
||||||
r = r and v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Time related functions
|
|
||||||
local function newTimerRegistry()
|
|
||||||
--- Used to store all the functions delayed with ubiquitousse.time.delay
|
|
||||||
-- The default implementation use the structure {<key: function> = <value: data table>, ...}
|
|
||||||
-- This table is for internal use and shouldn't be used from an external script.
|
|
||||||
local delayed = {}
|
|
||||||
|
|
||||||
-- Used to calculate the deltatime
|
|
||||||
local lastTime
|
|
||||||
|
|
||||||
local registry
|
|
||||||
registry = {
|
|
||||||
--- Creates and return a new TimerRegistry.
|
|
||||||
-- A TimerRegistry is a separate ubiquitousse.time instance: its TimedFunctions will be independant
|
|
||||||
-- from the one registered using ubiquitousse.time.run (the global TimerRegistry). If you use the scene
|
|
||||||
-- system, a scene-specific TimerRegistry is available at ubiquitousse.scene.current.time.
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
new = function()
|
|
||||||
local new = newTimerRegistry()
|
|
||||||
new.get = registry.get
|
|
||||||
return new
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Returns the number of miliseconds elapsed since some point in time.
|
|
||||||
-- This point is fixed but undetermined, so this function should only be used to calculate durations.
|
|
||||||
-- Should at least have millisecond-precision, but can be more precise if available.
|
|
||||||
-- @impl backend
|
|
||||||
get = function() end,
|
|
||||||
|
|
||||||
--- Update all the TimedFunctions calls.
|
|
||||||
-- Supposed to be called in ubiquitousse.event.update.
|
|
||||||
-- @tparam[opt=calculate here] number dt the delta-time (time spent since last time the function was called) (miliseconds)
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
update = function(dt)
|
|
||||||
if dt then
|
|
||||||
registry.dt = dt
|
|
||||||
else
|
|
||||||
if lastTime then
|
|
||||||
local newTime = registry.get()
|
|
||||||
registry.dt = newTime - lastTime
|
|
||||||
lastTime = newTime
|
|
||||||
else
|
|
||||||
lastTime = registry.get()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local done = {} -- functions done running
|
|
||||||
|
|
||||||
local d = delayed
|
|
||||||
for func, t in pairs(d) do
|
|
||||||
if t and all(t.initWhen, true) then
|
|
||||||
t.initWhen = {}
|
|
||||||
local co = t.coroutine
|
|
||||||
t.after = t.after - dt
|
|
||||||
if t.forceStart or (t.after <= 0 and all(t.startWhen, true)) then
|
|
||||||
t.startWhen = {}
|
|
||||||
d[func] = false -- niling here cause the next pair iteration to error
|
|
||||||
table.insert(done, func)
|
|
||||||
if not co then
|
|
||||||
co = coroutine.create(func)
|
|
||||||
t.coroutine = co
|
|
||||||
t.started = registry.get()
|
|
||||||
if t.times > 0 then t.times = t.times - 1 end
|
|
||||||
for _, f in ipairs(t.onStart) do f(t.object) end
|
|
||||||
end
|
|
||||||
assert(coroutine.resume(co, function(delay)
|
|
||||||
t.after = delay or 0
|
|
||||||
d[func] = t
|
|
||||||
coroutine.yield()
|
|
||||||
end, dt))
|
|
||||||
for _, f in ipairs(t.onUpdate) do f(t.object) end
|
|
||||||
if all(t.stopWhen, false) then t.forceStop = true end
|
|
||||||
if t.forceStop or coroutine.status(co) == "dead" then
|
|
||||||
if t.forceStop
|
|
||||||
or (t.during >= 0 and t.started + t.during < registry.get())
|
|
||||||
or (t.times == 0)
|
|
||||||
or (not all(t.repeatWhile, true))
|
|
||||||
or (t.every == -1 and t.times == -1 and t.during == -1 and #t.repeatWhile == 0) -- no repeat
|
|
||||||
then
|
|
||||||
for _, f in ipairs(t.onEnd) do f(t.object) end
|
|
||||||
else
|
|
||||||
if t.times > 0 then t.times = t.times - 1 end
|
|
||||||
t.after = t.every
|
|
||||||
t.coroutine = coroutine.create(func)
|
|
||||||
d[func] = t
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, func in ipairs(done) do
|
|
||||||
if not d[func] then
|
|
||||||
d[func] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Schedule a function to run.
|
|
||||||
-- The function will receive as first parameter the wait(time) function, which will pause the function execution for time miliseconds.
|
|
||||||
-- @tparam[opt] function func the function to schedule
|
|
||||||
-- @treturn TimedFunction the object
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
run = function(func)
|
|
||||||
-- Creates empty function (the TimedFunction may be used for time measure or stuff like that which doesn't need a specific function)
|
|
||||||
func = func or function() end
|
|
||||||
|
|
||||||
-- Since delayed functions can end in any order, it doesn't really make sense to use a integer-keyed list.
|
|
||||||
-- Using the function as the key works and it's unique.
|
|
||||||
delayed[func] = {
|
|
||||||
object = nil,
|
|
||||||
coroutine = nil,
|
|
||||||
started = 0,
|
|
||||||
|
|
||||||
after = -1,
|
|
||||||
every = -1,
|
|
||||||
times = -1,
|
|
||||||
during = -1,
|
|
||||||
|
|
||||||
initWhen = {},
|
|
||||||
startWhen = {},
|
|
||||||
repeatWhile = {},
|
|
||||||
stopWhen = {},
|
|
||||||
|
|
||||||
forceStart = false,
|
|
||||||
forceStop = false,
|
|
||||||
|
|
||||||
onStart = {},
|
|
||||||
onUpdate = {},
|
|
||||||
onEnd = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
local t = delayed[func] -- internal data
|
|
||||||
local r -- external interface
|
|
||||||
r = {
|
|
||||||
--- Timed conditions ---
|
|
||||||
--- Wait time milliseconds before running the function.
|
|
||||||
after = function(_, time)
|
|
||||||
t.after = time
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- Run the function every time millisecond.
|
|
||||||
every = function(_, time)
|
|
||||||
t.every = time
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- The function will not execute more than count times.
|
|
||||||
times = function(_, count)
|
|
||||||
t.times = count
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- The TimedFunction will be active for a time duration.
|
|
||||||
during = function(_, time)
|
|
||||||
t.during = time
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Function conditions ---
|
|
||||||
--- Starts the function execution when func() returns true. Checked before the "after" condition,
|
|
||||||
-- meaning the "after" countdown starts when func() returns true.
|
|
||||||
-- If multiple init functions are added, init will trigger only when all of them returns true.
|
|
||||||
initWhen = function(_, func)
|
|
||||||
table.insert(t.initWhen, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- Starts the function execution when func() returns true. Checked after the "after" condition.
|
|
||||||
-- If multiple start functions are added, start will trigger only when all of them returns true.
|
|
||||||
startWhen = function(_, func)
|
|
||||||
table.insert(t.startWhen, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- When the functions ends, the execution won't stop and will repeat as long as func() returns true.
|
|
||||||
-- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat.
|
|
||||||
-- If multiple repeat functions are added, a repeat will trigger only when all of them returns true.
|
|
||||||
repeatWhile = function(_, func)
|
|
||||||
table.insert(t.repeatWhile, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- Stops the function execution when func() returns true. Checked before all timed conditions.
|
|
||||||
-- If multiple stop functions are added, stop will trigger only when all of them returns true.
|
|
||||||
stopWhen = function(_, func)
|
|
||||||
table.insert(t.stopWhen, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Conditions override ---
|
|
||||||
--- Force the function to start its execution.
|
|
||||||
start = function(_)
|
|
||||||
t.forceStart = true
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- Force the function to stop its execution.
|
|
||||||
stop = function(_)
|
|
||||||
t.forceStop = true
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Callbacks functions ---
|
|
||||||
--- Will execute func(self) when the function execution start.
|
|
||||||
onStart = function(_, func)
|
|
||||||
table.insert(t.onStart, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- Will execute func(self) each frame the main function is run..
|
|
||||||
onUpdate = function(_, func)
|
|
||||||
table.insert(t.onUpdate, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
--- Will execute func(self) when the function execution end.
|
|
||||||
onEnd = function(_, func)
|
|
||||||
table.insert(t.onEnd, func)
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Chaining ---
|
|
||||||
--- Creates another TimedFunction which will be initialized when the current one ends.
|
|
||||||
-- Returns the new TimedFunction.
|
|
||||||
chain = function(_, func)
|
|
||||||
local done = false
|
|
||||||
r:onEnd(function() done = true end)
|
|
||||||
return registry.run(func)
|
|
||||||
:initWhen(function() return done end)
|
|
||||||
end
|
|
||||||
}
|
|
||||||
t.object = r
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Tween some numeric values.
|
|
||||||
-- @tparam number duration tween duration (miliseconds)
|
|
||||||
-- @tparam table tbl the table containing the values to tween
|
|
||||||
-- @tparam table to the new values
|
|
||||||
-- @tparam[opt="linear"] string/function method tweening method (string name or the actual function(time, start, change, duration))
|
|
||||||
-- @treturn TimedFunction the object. A duration is already defined, and the :chain methods takes the same arguments as tween (and creates a tween).
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
tween = function(duration, tbl, to, method)
|
|
||||||
method = method or "linear"
|
|
||||||
method = type(method) == "string" and ease[method] or method
|
|
||||||
|
|
||||||
local time = 0 -- tweening time elapsed
|
|
||||||
local from = {} -- initial state
|
|
||||||
|
|
||||||
local function update(tbl_, from_, to_) -- apply the method to tbl_ recursively (doesn't handle cycles)
|
|
||||||
for k, v in pairs(to_) do
|
|
||||||
if type(v) == "table" then
|
|
||||||
update(tbl_[k], from_[k], to_[k])
|
|
||||||
else
|
|
||||||
if time < duration then
|
|
||||||
tbl_[k] = method(time, from_[k], v - from_[k], duration)
|
|
||||||
else
|
|
||||||
tbl_[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local r = registry.run(function(wait, dt)
|
|
||||||
time = time + dt
|
|
||||||
update(tbl, from, to)
|
|
||||||
end):during(duration)
|
|
||||||
:onStart(function()
|
|
||||||
local function copy(stencil, source, dest) -- copy initial state recursively
|
|
||||||
for k, v in pairs(stencil) do
|
|
||||||
if type(v) == "table" then
|
|
||||||
if not dest[k] then dest[k] = {} end
|
|
||||||
copy(stencil[k], source[k], dest[k])
|
|
||||||
else
|
|
||||||
dest[k] = source[k]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
copy(to, tbl, from)
|
|
||||||
end)
|
|
||||||
|
|
||||||
--- Creates another tween which will be initialized when the current one ends.
|
|
||||||
-- If tbl_ and/or method_ are not specified, the values from the current tween will be used.
|
|
||||||
-- Returns the new tween.
|
|
||||||
r.chain = function(_, duration_, tbl_, to_, method_)
|
|
||||||
if not method_ and to_ then
|
|
||||||
if type(to_) == "string" then
|
|
||||||
tbl_, to_, method_ = tbl, tbl_, to_
|
|
||||||
else
|
|
||||||
method_ = method
|
|
||||||
end
|
|
||||||
elseif not method_ and not to_ then
|
|
||||||
tbl_, to_, method_ = tbl, tbl_, method
|
|
||||||
end
|
|
||||||
|
|
||||||
local done = false
|
|
||||||
r:onEnd(function() done = true end)
|
|
||||||
return registry.tween(duration_, tbl_, to_, method_)
|
|
||||||
:initWhen(function() return done end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return r
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Cancels all the running TimedFunctions.
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
clear = function()
|
|
||||||
delayed = {}
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Time since last update (miliseconds).
|
|
||||||
-- @impl ubiquitousse
|
|
||||||
dt = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return registry
|
|
||||||
end
|
|
||||||
|
|
||||||
return newTimerRegistry()
|
|
||||||
6
time/backend/ctrulua.lua
Normal file
6
time/backend/ctrulua.lua
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
local time = require((...):match("^(.-%.)backend").."time")
|
||||||
|
local ctr = require("ctr")
|
||||||
|
|
||||||
|
time.get = ctr.time
|
||||||
|
|
||||||
|
return time
|
||||||
7
time/backend/love.lua
Normal file
7
time/backend/love.lua
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
local time = require((...):match("^(.-%.)backend").."time")
|
||||||
|
|
||||||
|
time.get = function()
|
||||||
|
return love.timer.getTime() * 1000
|
||||||
|
end
|
||||||
|
|
||||||
|
return time
|
||||||
14
time/init.lua
Normal file
14
time/init.lua
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
local time
|
||||||
|
|
||||||
|
local p = ...
|
||||||
|
if love then
|
||||||
|
time = require(p..".backend.love")
|
||||||
|
elseif package.loaded["ctr"] then
|
||||||
|
time = require(p..".backend.ctrulua")
|
||||||
|
elseif package.loaded["libretro"] then
|
||||||
|
error("NYI")
|
||||||
|
else
|
||||||
|
error("no backend for ubiquitousse.time")
|
||||||
|
end
|
||||||
|
|
||||||
|
return time
|
||||||
350
time/time.lua
Normal file
350
time/time.lua
Normal file
|
|
@ -0,0 +1,350 @@
|
||||||
|
--- ubiquitousse.time
|
||||||
|
-- Depends on a backend.
|
||||||
|
|
||||||
|
local ease = require((...):match("^.-time")..".easing")
|
||||||
|
local time
|
||||||
|
|
||||||
|
--- Returns true if all the values in the list are true ; functions in the list will be called and the test will be performed on their return value.
|
||||||
|
-- Returns default if the list is empty.
|
||||||
|
local function all(list, default)
|
||||||
|
if #list == 0 then
|
||||||
|
return default
|
||||||
|
else
|
||||||
|
local r = true
|
||||||
|
for _,v in ipairs(list) do
|
||||||
|
if type(v) == "function" then
|
||||||
|
r = r and v()
|
||||||
|
else
|
||||||
|
r = r and v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Registry methods.
|
||||||
|
local registry_mt = {
|
||||||
|
--- Update all the TimedFunctions calls.
|
||||||
|
-- Should be called at every game update; called by ubiquitousse.update.
|
||||||
|
-- @tparam[opt=calculate here] number dt the delta-time (time spent since last time the function was called) (miliseconds)
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
update = function(self, dt)
|
||||||
|
local currentTime = time.get()
|
||||||
|
|
||||||
|
if not dt then
|
||||||
|
dt = currentTime - self.lastTime
|
||||||
|
self.lastTime = currentTime
|
||||||
|
end
|
||||||
|
self.dt = dt
|
||||||
|
|
||||||
|
local done = {} -- functions done running
|
||||||
|
|
||||||
|
local d = self.delayed
|
||||||
|
for func, t in pairs(d) do
|
||||||
|
if t and all(t.initWhen, true) then
|
||||||
|
t.initWhen = {}
|
||||||
|
local co = t.coroutine
|
||||||
|
t.after = t.after - dt
|
||||||
|
if t.forceStart or (t.after <= 0 and all(t.startWhen, true)) then
|
||||||
|
t.startWhen = {}
|
||||||
|
d[func] = false -- niling here cause the next pair iteration to error
|
||||||
|
table.insert(done, func)
|
||||||
|
if not co then
|
||||||
|
co = coroutine.create(func)
|
||||||
|
t.coroutine = co
|
||||||
|
t.started = currentTime
|
||||||
|
if t.times > 0 then t.times = t.times - 1 end
|
||||||
|
for _, f in ipairs(t.onStart) do f(t.object) end
|
||||||
|
end
|
||||||
|
assert(coroutine.resume(co, function(delay)
|
||||||
|
t.after = delay or 0
|
||||||
|
d[func] = t
|
||||||
|
coroutine.yield()
|
||||||
|
end, dt))
|
||||||
|
for _, f in ipairs(t.onUpdate) do f(t.object) end
|
||||||
|
if all(t.stopWhen, false) then t.forceStop = true end
|
||||||
|
if t.forceStop or coroutine.status(co) == "dead" then
|
||||||
|
if t.forceStop
|
||||||
|
or (t.during >= 0 and t.started + t.during < currentTime)
|
||||||
|
or (t.times == 0)
|
||||||
|
or (not all(t.repeatWhile, true))
|
||||||
|
or (t.every == -1 and t.times == -1 and t.during == -1 and #t.repeatWhile == 0) -- no repeat
|
||||||
|
then
|
||||||
|
for _, f in ipairs(t.onEnd) do f(t.object) end
|
||||||
|
else
|
||||||
|
if t.times > 0 then t.times = t.times - 1 end
|
||||||
|
t.after = t.every
|
||||||
|
t.coroutine = coroutine.create(func)
|
||||||
|
d[func] = t
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, func in ipairs(done) do
|
||||||
|
if not d[func] then
|
||||||
|
d[func] = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Schedule a function to run.
|
||||||
|
-- The function will receive as first parameter the wait(time) function, which will pause the function execution for time miliseconds.
|
||||||
|
-- As a second parameter, the function will receive the delta time (dt).
|
||||||
|
-- @tparam[opt] function func the function to schedule
|
||||||
|
-- @treturn TimedFunction the object
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
run = function(self, func)
|
||||||
|
-- Creates empty function (the TimedFunction may be used for time measure or stuff like that which doesn't need a specific function)
|
||||||
|
func = func or function() end
|
||||||
|
|
||||||
|
-- Since delayed functions can end in any order, it doesn't really make sense to use a integer-keyed list.
|
||||||
|
-- Using the function as the key works and it's unique.
|
||||||
|
self.delayed[func] = {
|
||||||
|
object = nil,
|
||||||
|
coroutine = nil,
|
||||||
|
started = 0,
|
||||||
|
|
||||||
|
after = -1,
|
||||||
|
every = -1,
|
||||||
|
times = -1,
|
||||||
|
during = -1,
|
||||||
|
|
||||||
|
initWhen = {},
|
||||||
|
startWhen = {},
|
||||||
|
repeatWhile = {},
|
||||||
|
stopWhen = {},
|
||||||
|
|
||||||
|
forceStart = false,
|
||||||
|
forceStop = false,
|
||||||
|
|
||||||
|
onStart = {},
|
||||||
|
onUpdate = {},
|
||||||
|
onEnd = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
local t = self.delayed[func] -- internal data
|
||||||
|
local r -- external interface
|
||||||
|
r = {
|
||||||
|
--- Timed conditions ---
|
||||||
|
--- Wait time milliseconds before running the function.
|
||||||
|
after = function(_, time)
|
||||||
|
t.after = time
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- Run the function every time millisecond.
|
||||||
|
every = function(_, time)
|
||||||
|
t.every = time
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- The function will not execute more than count times.
|
||||||
|
times = function(_, count)
|
||||||
|
t.times = count
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- The TimedFunction will be active for a time duration.
|
||||||
|
during = function(_, time)
|
||||||
|
t.during = time
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Function conditions ---
|
||||||
|
--- Starts the function execution when func() returns true. Checked before the "after" condition,
|
||||||
|
-- meaning the "after" countdown starts when func() returns true.
|
||||||
|
-- If multiple init functions are added, init will trigger only when all of them returns true.
|
||||||
|
initWhen = function(_, func)
|
||||||
|
table.insert(t.initWhen, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- Starts the function execution when func() returns true. Checked after the "after" condition.
|
||||||
|
-- If multiple start functions are added, start will trigger only when all of them returns true.
|
||||||
|
startWhen = function(_, func)
|
||||||
|
table.insert(t.startWhen, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- When the functions ends, the execution won't stop and will repeat as long as func() returns true.
|
||||||
|
-- Will cancel timed repeat conditions if false but needs other timed repeat conditions to be true to create a new repeat.
|
||||||
|
-- If multiple repeat functions are added, a repeat will trigger only when all of them returns true.
|
||||||
|
repeatWhile = function(_, func)
|
||||||
|
table.insert(t.repeatWhile, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- Stops the function execution when func() returns true. Checked before all timed conditions.
|
||||||
|
-- If multiple stop functions are added, stop will trigger only when all of them returns true.
|
||||||
|
stopWhen = function(_, func)
|
||||||
|
table.insert(t.stopWhen, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Conditions override ---
|
||||||
|
--- Force the function to start its execution.
|
||||||
|
start = function(_)
|
||||||
|
t.forceStart = true
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- Force the function to stop its execution.
|
||||||
|
stop = function(_)
|
||||||
|
t.forceStop = true
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Callbacks functions ---
|
||||||
|
--- Will execute func(self) when the function execution start.
|
||||||
|
onStart = function(_, func)
|
||||||
|
table.insert(t.onStart, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- Will execute func(self) each frame the main function is run.
|
||||||
|
onUpdate = function(_, func)
|
||||||
|
table.insert(t.onUpdate, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
--- Will execute func(self) when the function execution end.
|
||||||
|
onEnd = function(_, func)
|
||||||
|
table.insert(t.onEnd, func)
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Chaining ---
|
||||||
|
--- Creates another TimedFunction which will be initialized when the current one ends.
|
||||||
|
-- Returns the new TimedFunction.
|
||||||
|
chain = function(_, func)
|
||||||
|
local done = false
|
||||||
|
r:onEnd(function() done = true end)
|
||||||
|
return self:run(func)
|
||||||
|
:initWhen(function() return done end)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
t.object = r
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Tween some numeric values.
|
||||||
|
-- @tparam number duration tween duration (miliseconds)
|
||||||
|
-- @tparam table tbl the table containing the values to tween
|
||||||
|
-- @tparam table to the new values
|
||||||
|
-- @tparam[opt="linear"] string/function method tweening method (string name or the actual function(time, start, change, duration))
|
||||||
|
-- @treturn TimedFunction the object. A duration is already defined, and the :chain methods takes the same arguments as tween (and creates a tween).
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
tween = function(self, duration, tbl, to, method)
|
||||||
|
method = method or "linear"
|
||||||
|
method = type(method) == "string" and ease[method] or method
|
||||||
|
|
||||||
|
local time = 0 -- tweening time elapsed
|
||||||
|
local from = {} -- initial state
|
||||||
|
|
||||||
|
local function update(tbl_, from_, to_) -- apply the method to tbl_ recursively (doesn't handle cycles)
|
||||||
|
for k, v in pairs(to_) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
update(tbl_[k], from_[k], to_[k])
|
||||||
|
else
|
||||||
|
if time < duration then
|
||||||
|
tbl_[k] = method(time, from_[k], v - from_[k], duration)
|
||||||
|
else
|
||||||
|
tbl_[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local r = self:run(function(wait, dt)
|
||||||
|
time = time + dt
|
||||||
|
update(tbl, from, to)
|
||||||
|
end):during(duration)
|
||||||
|
:onStart(function()
|
||||||
|
local function copy(stencil, source, dest) -- copy initial state recursively
|
||||||
|
for k, v in pairs(stencil) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
if not dest[k] then dest[k] = {} end
|
||||||
|
copy(stencil[k], source[k], dest[k])
|
||||||
|
else
|
||||||
|
dest[k] = source[k]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
copy(to, tbl, from)
|
||||||
|
end)
|
||||||
|
|
||||||
|
--- Creates another tween which will be initialized when the current one ends.
|
||||||
|
-- If tbl_ and/or method_ are not specified, the values from the current tween will be used.
|
||||||
|
-- Returns the new tween.
|
||||||
|
r.chain = function(_, duration_, tbl_, to_, method_)
|
||||||
|
if not method_ and to_ then
|
||||||
|
if type(to_) == "string" then
|
||||||
|
tbl_, to_, method_ = tbl, tbl_, to_
|
||||||
|
else
|
||||||
|
method_ = method
|
||||||
|
end
|
||||||
|
elseif not method_ and not to_ then
|
||||||
|
tbl_, to_, method_ = tbl, tbl_, method
|
||||||
|
end
|
||||||
|
|
||||||
|
local done = false
|
||||||
|
r:onEnd(function() done = true end)
|
||||||
|
return self:tween(duration_, tbl_, to_, method_)
|
||||||
|
:initWhen(function() return done end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Cancels all the running TimedFunctions.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
clear = function(self)
|
||||||
|
self.delayed = {}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
registry_mt.__index = registry_mt
|
||||||
|
|
||||||
|
--- Time related functions
|
||||||
|
time = {
|
||||||
|
--- Creates and return a new TimerRegistry.
|
||||||
|
-- A TimerRegistry is a separate ubiquitousse.time instance: its TimedFunctions will be independant
|
||||||
|
-- from the one registered using ubiquitousse.time.run (the global TimerRegistry). If you use the scene
|
||||||
|
-- system, a scene-specific TimerRegistry is available at ubiquitousse.scene.current.time.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
new = function()
|
||||||
|
return setmetatable({
|
||||||
|
--- Used to store all the functions delayed with ubiquitousse.time.delay
|
||||||
|
-- The default implementation use the structure {<key: function> = <value: data table>, ...}
|
||||||
|
-- This table is for internal use and shouldn't be used from an external script.
|
||||||
|
delayed = {},
|
||||||
|
|
||||||
|
-- Used to calculate the deltatime
|
||||||
|
lastTime = time.get(),
|
||||||
|
|
||||||
|
--- Time since last timer update (miliseconds).
|
||||||
|
dt = 0
|
||||||
|
}, registry_mt)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Returns the number of miliseconds elapsed since some point in time.
|
||||||
|
-- This point is fixed but undetermined, so this function should only be used to calculate durations.
|
||||||
|
-- Should at least have millisecond-precision, but can be more precise if available.
|
||||||
|
-- @impl backend
|
||||||
|
get = function() end,
|
||||||
|
|
||||||
|
--- Time since last update (miliseconds).
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
dt = 0,
|
||||||
|
|
||||||
|
--- Global TimerRegistry.
|
||||||
|
-- @impl ubiquitousse
|
||||||
|
delayed = {},
|
||||||
|
lastTime = 0,
|
||||||
|
update = function(...)
|
||||||
|
return registry_mt.update(time, ...)
|
||||||
|
end,
|
||||||
|
run = function(...)
|
||||||
|
return registry_mt.run(time, ...)
|
||||||
|
end,
|
||||||
|
tween = function(...)
|
||||||
|
return registry_mt.tween(time, ...)
|
||||||
|
end,
|
||||||
|
clear = function(...)
|
||||||
|
return registry_mt.clear(time, ...)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
return time
|
||||||
5
todo.txt
5
todo.txt
|
|
@ -1,9 +1,10 @@
|
||||||
Ubiquitousse, also known as "The World's Best Video Game Engine Of All Time", despite its perfectness, is still not perfect.
|
Ubiquitousse, also known as "The World's Best Video Game Framework Of All Time", despite its perfectness, is still not perfect.
|
||||||
More specifically, what is lacking to officially turn Ubiquitousse into a sacred text, is:
|
More specifically, what is lacking to officially turn Ubiquitousse into a sacred text, is:
|
||||||
- An i18n API. While some languages are clearly superior to others, the general consensus seems to think otherwise. Ubiquitousse
|
- An i18n API. While some languages are clearly superior to others, the general consensus seems to think otherwise. Ubiquitousse
|
||||||
should be able to get an ordered list of prefered languages and provide translation helpers. See The Pong.
|
should be able to get an ordered list of prefered languages and provide translation helpers. See The Pong.
|
||||||
- Some API are still lacking an API and/or implementation. Search "TODO" for more information.
|
- Some API are still lacking an API and/or implementation. Search "TODO" for more information.
|
||||||
- A filesystem API, to access the game's filesystem. May also rewrite Lua's io functions, see the next item.
|
- A filesystem API, to access the game's filesystem. May also rewrite Lua's io functions, see the next item. Though the LÖVE API is fine.
|
||||||
|
- An audio and draw API. There were one but it was simply plagiarized from LÖVE, so, meh. Might just provide LÖVE API compat layers for other backends in the end.
|
||||||
- A sandboxing system. Ubiquitousse should be able to run in a Ubiquitousse game safely. Since Ubiquitousse can run itself, it will
|
- A sandboxing system. Ubiquitousse should be able to run in a Ubiquitousse game safely. Since Ubiquitousse can run itself, it will
|
||||||
then seems reasonable to claim Ubiquitousse run the universe. See World Domination for Dummies.
|
then seems reasonable to claim Ubiquitousse run the universe. See World Domination for Dummies.
|
||||||
- A libretro backend, so Ubiquitousse really become ubiquitous. If you didn't know, it's the goal.
|
- A libretro backend, so Ubiquitousse really become ubiquitous. If you didn't know, it's the goal.
|
||||||
|
|
|
||||||
52
util.lua
52
util.lua
|
|
@ -1,52 +0,0 @@
|
||||||
-- ubiquitousse.util
|
|
||||||
|
|
||||||
--- Various functions useful for game developement.
|
|
||||||
-- No dependicy on either ubiquitousse or a ubiquitousse backend.
|
|
||||||
local util
|
|
||||||
util = {
|
|
||||||
--- AABB collision check.
|
|
||||||
-- @tparam number x1 first rectangle top-left x coordinate
|
|
||||||
-- @tparam number y1 first rectangle top-left y coordinate
|
|
||||||
-- @tparam number w1 first rectangle width
|
|
||||||
-- @tparam number h1 first rectangle height
|
|
||||||
-- @tparam number x2 second rectangle top-left x coordinate
|
|
||||||
-- @tparam number y2 second rectangle top-left y coordinate
|
|
||||||
-- @tparam number w2 second rectangle width
|
|
||||||
-- @tparam number h2 second rectangle height
|
|
||||||
-- @treturn true if the objects collide, false otherwise
|
|
||||||
aabb = function(x1, y1, w1, h1, x2, y2, w2, h2)
|
|
||||||
if w1 < 0 then x1 = x1 + w1; w1 = -w1 end
|
|
||||||
if h1 < 0 then y1 = y1 + h1; h1 = -h1 end
|
|
||||||
if w2 < 0 then x2 = x2 + w2; w2 = -w2 end
|
|
||||||
if h2 < 0 then y2 = y2 + h2; h2 = -h2 end
|
|
||||||
return x1 + w1 >= x2 and x1 <= x2 + w2 and
|
|
||||||
y1 + h1 >= y2 and y1 <= y2 + h2
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Remove the first occurence of an element in a table.
|
|
||||||
-- @tparam table t the table
|
|
||||||
-- @param x the element to remove
|
|
||||||
-- @return x
|
|
||||||
remove = function(t, x)
|
|
||||||
for i, v in ipairs(t) do
|
|
||||||
if v == x then
|
|
||||||
table.remove(t, i)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return x
|
|
||||||
end,
|
|
||||||
|
|
||||||
--- Returns a new table where the keys and values have been inverted.
|
|
||||||
-- @tparam table t the table
|
|
||||||
-- @treturn table the inverted table
|
|
||||||
invert = function(t)
|
|
||||||
local r = {}
|
|
||||||
for k, v in pairs(t) do
|
|
||||||
r[v] = k
|
|
||||||
end
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
}
|
|
||||||
|
|
||||||
return util
|
|
||||||
1
util/init.lua
Normal file
1
util/init.lua
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
return require((...)..".util")
|
||||||
336
util/util.lua
Normal file
336
util/util.lua
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
--- ubiquitousse.util
|
||||||
|
-- No dependency.
|
||||||
|
|
||||||
|
--- Various functions useful for game developement.
|
||||||
|
local util, group_mt
|
||||||
|
util = {
|
||||||
|
-------------------
|
||||||
|
--- Basic maths ---
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
--- AABB collision check.
|
||||||
|
-- @tparam number x1 first rectangle top-left x coordinate
|
||||||
|
-- @tparam number y1 first rectangle top-left y coordinate
|
||||||
|
-- @tparam number w1 first rectangle width
|
||||||
|
-- @tparam number h1 first rectangle height
|
||||||
|
-- @tparam number x2 second rectangle top-left x coordinate
|
||||||
|
-- @tparam number y2 second rectangle top-left y coordinate
|
||||||
|
-- @tparam number w2 second rectangle width
|
||||||
|
-- @tparam number h2 second rectangle height
|
||||||
|
-- @treturn true if the objects collide, false otherwise
|
||||||
|
aabb = function(x1, y1, w1, h1, x2, y2, w2, h2)
|
||||||
|
if w1 < 0 then x1 = x1 + w1; w1 = -w1 end
|
||||||
|
if h1 < 0 then y1 = y1 + h1; h1 = -h1 end
|
||||||
|
if w2 < 0 then x2 = x2 + w2; w2 = -w2 end
|
||||||
|
if h2 < 0 then y2 = y2 + h2; h2 = -h2 end
|
||||||
|
return x1 + w1 >= x2 and x1 <= x2 + w2 and
|
||||||
|
y1 + h1 >= y2 and y1 <= y2 + h2
|
||||||
|
end,
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
--- List operations ---
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
--- Remove the first occurence of an element in a list.
|
||||||
|
-- @tparam table t the list
|
||||||
|
-- @param x the element to remove
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @return x
|
||||||
|
remove = function(t, x, n)
|
||||||
|
n = n or #t
|
||||||
|
for i=1, n do
|
||||||
|
if t[i] == x then
|
||||||
|
table.remove(t, i)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return x
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Extract the list of elements with a specific key from a list of tables
|
||||||
|
-- @tparam table t the list of tables
|
||||||
|
-- @param k the chosen key
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @treturn the extracted table
|
||||||
|
extract = function(t, k, n)
|
||||||
|
n = n or #t
|
||||||
|
local r = {}
|
||||||
|
for i=1, n do
|
||||||
|
r[i] = t[i][k]
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Applies a function to every item in list t.
|
||||||
|
-- The function receive two argument: the value and then the key.
|
||||||
|
-- @tparam table t initial list
|
||||||
|
-- @tparam function fn the function to apply
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @treturn table the initial list
|
||||||
|
each = function(t, fn, n)
|
||||||
|
n = n or #t
|
||||||
|
for i=1, n do
|
||||||
|
fn(t[i], i)
|
||||||
|
end
|
||||||
|
return t
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Applies a function to every item in list t and returns the associated new list.
|
||||||
|
-- The function receive two argument: the value and then the key.
|
||||||
|
-- @tparam table t initial list
|
||||||
|
-- @tparam function fn the function to apply
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @treturn table the new list
|
||||||
|
map = function(t, fn, n)
|
||||||
|
n = n or #t
|
||||||
|
local r = {}
|
||||||
|
for i=1, n do
|
||||||
|
r[i] = fn(t[i], i)
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Test if all the values in the list are true. Optionnaly applies a function to get the truthness.
|
||||||
|
-- The function receive two argument: the value and then the key.
|
||||||
|
-- @tparam table t initial list
|
||||||
|
-- @tparam function fn the function to apply
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @treturn boolean result
|
||||||
|
all = function(t, fn, n)
|
||||||
|
n = n or #t
|
||||||
|
fn = fn or function(v) return v end
|
||||||
|
local r = true
|
||||||
|
for i=1, n do
|
||||||
|
r = r and fn(t[i], i)
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Test if at least one value in the list is true. Optionnaly applies a function to get the truthness.
|
||||||
|
-- The function receive two argument: the value and then the key.
|
||||||
|
-- @tparam table t initial list
|
||||||
|
-- @tparam function fn the function to apply
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @treturn boolean result
|
||||||
|
any = function(t, fn, n)
|
||||||
|
n = n or #t
|
||||||
|
fn = fn or function(v) return v end
|
||||||
|
for i=1, n do
|
||||||
|
if fn(t[i], i) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end,
|
||||||
|
|
||||||
|
-----------------------------
|
||||||
|
--- Dictionary operations ---
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
--- Returns a new table where the keys and values have been inverted.
|
||||||
|
-- @tparam table t the table
|
||||||
|
-- @treturn table the inverted table
|
||||||
|
invert = function(t)
|
||||||
|
local r = {}
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
r[v] = k
|
||||||
|
end
|
||||||
|
return r
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Perform a deep copy of a table.
|
||||||
|
-- The copied table will keep the share the same metatable as the original table.
|
||||||
|
-- Note this uses pairs() to perform the copy, which will honor the __pairs methamethod if present.
|
||||||
|
-- @tparam table t the table
|
||||||
|
-- @treturn table the copied table
|
||||||
|
copy = function(t, cache)
|
||||||
|
if cache == nil then cache = {} end
|
||||||
|
local r = {}
|
||||||
|
cache[t] = r
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
if type(v) == "table" then
|
||||||
|
r[k] = cache[v] or util.copy(v, cache)
|
||||||
|
else
|
||||||
|
r[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return setmetatable(r, getmetatable(t))
|
||||||
|
end,
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
--- Random and UUID ---
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
--- Generate a UUID v4.
|
||||||
|
-- @treturn string the UUID in its canonical representation
|
||||||
|
uuid4 = function()
|
||||||
|
return ("xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx") -- version 4
|
||||||
|
:gsub("N", math.random(0x8, 0xb)) -- variant 1
|
||||||
|
:gsub("x", function() return ("%x"):format(math.random(0x0, 0xf)) end) -- random hexadecimal digit
|
||||||
|
end,
|
||||||
|
|
||||||
|
-----------------------
|
||||||
|
--- Object grouping ---
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
--- Groups objects in a meta-object-proxy-thingy.
|
||||||
|
-- Works great with Lua 5.2+. LuaJit requires to be built with Lua 5.2 compatibility enabled to support group comparaison.
|
||||||
|
-- @tparam table list of objects
|
||||||
|
-- @tparam[opt=#t] number n the number of expected elements in the list, including nil values
|
||||||
|
-- @tparam[opt=nil] table p list of parents. Used to find the first arguments of method calls.
|
||||||
|
-- @treturn group object
|
||||||
|
group = function(t, n, p)
|
||||||
|
n = n or #t
|
||||||
|
return setmetatable({ _n = n, _t = t, _p = p or false }, group_mt)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
group_mt = {
|
||||||
|
--- Everything but comparaison: returns a new group
|
||||||
|
__add = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v + other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self + v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v + other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__sub = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v - other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self - v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v - other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__mul = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v * other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self * v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v * other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__div = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v / other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self / v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v / other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__mod = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v % other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self % v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v % other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__pow = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v ^ other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self ^ v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v ^ other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__unm = function(self)
|
||||||
|
return util.group(util.map(self._t, function(v) return -v end, self._n), self._n)
|
||||||
|
end,
|
||||||
|
__concat = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v .. other._t[i] end, self._n), self._n)
|
||||||
|
else
|
||||||
|
return util.group(util.map(other._t, function(v) return self .. v end, self._n), self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.group(util.map(self._t, function(v) return v .. other end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__len = function(self)
|
||||||
|
return util.group(util.map(self._t, function(v) return #v end, self._n), self._n)
|
||||||
|
end,
|
||||||
|
__index = function(self, k)
|
||||||
|
return util.group(util.extract(self._t, k, self._n), self._n, self._t)
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Comparaison: returns true if true for every object of the group
|
||||||
|
__eq = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.all(self._t, function(v, i) return v == other._t[i] end, self._n)
|
||||||
|
else
|
||||||
|
return util.all(other._t, function(v) return self == v end, self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.all(self._t, function(v) return v == other end, self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__lt = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.all(self._t, function(v, i) return v < other._t[i] end, self._n)
|
||||||
|
else
|
||||||
|
return util.all(other._t, function(v) return self < v end, self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.all(self._t, function(v) return v < other end, self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__le = function(self, other)
|
||||||
|
if getmetatable(other) == group_mt then
|
||||||
|
if getmetatable(self) == group_mt then
|
||||||
|
return util.all(self._t, function(v, i) return v <= other._t[i] end, self._n)
|
||||||
|
else
|
||||||
|
return util.all(other._t, function(v) return self <= v end, self._n)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
return util.all(self._t, function(v) return v <= other end, self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Special cases
|
||||||
|
__newindex = function(self, k, v)
|
||||||
|
if getmetatable(v) == group_mt then -- unpack
|
||||||
|
util.each(self._t, function(t, i) t[k] = v._t[i] end, self._n)
|
||||||
|
else
|
||||||
|
util.each(self._t, function(t) t[k] = v end, self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
__call = function(self, selfArg, ...)
|
||||||
|
if getmetatable(selfArg) == group_mt and self._p then -- method call
|
||||||
|
local a = {...}
|
||||||
|
return util.group(util.map(self._t, function(v, i) return v(self._p[i], unpack(a)) end, self._n), self._n)
|
||||||
|
else
|
||||||
|
local a = {selfArg, ...}
|
||||||
|
return util.group(util.map(self._t, function(v) return v(unpack(a)) end, self._n), self._n)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
--- Full-blown debugger
|
||||||
|
__tostring = function(self)
|
||||||
|
return ("group{%s}"):format(table.concat(util.map(self._t, tostring, self._n), ", "))
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
return util
|
||||||
Loading…
Add table
Add a link
Reference in a new issue