First project-specific commit.

Hardly a game yet, but extremely basic ball movements work!
This commit is contained in:
Sage Vaillancourt 2025-03-18 00:08:44 -04:00
parent 4134634584
commit a2d796900f
27 changed files with 5539 additions and 4470 deletions

View File

@ -1,4 +1,4 @@
std = "lua54+love"
stds.project = {
globals = {"tiny"},
}
}

BIN
assets/images/Ball.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

BIN
assets/images/Flag.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 901 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 889 B

BIN
assets/images/Grass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

BIN
assets/images/SandTrap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

View File

@ -5,4 +5,5 @@ require("../systems/decay")
require("../systems/draw")
require("../systems/gravity")
require("../systems/input")
require("../systems/rounds")
require("../systems/velocity")

View File

@ -1,11 +1,47 @@
-- GENERATED FILE - DO NOT EDIT
-- Instead, edit the source file directly: assets.lua2p.
-- luacheck: ignore
---@type love.Texture
Ball = love.graphics.newImage("assets/images/Ball.png")
-- luacheck: ignore
---@type love.Texture
Flag = love.graphics.newImage("assets/images/Flag.png")
-- luacheck: ignore
---@type love.Texture
GolferRightActivated = love.graphics.newImage("assets/images/GolferRightActivated.png")
-- luacheck: ignore
---@type love.Texture
GolferRight = love.graphics.newImage("assets/images/GolferRight.png")
-- luacheck: ignore
---@type love.Texture
Grass = love.graphics.newImage("assets/images/Grass.png")
-- luacheck: ignore
---@type love.Texture
SandTrap = love.graphics.newImage("assets/images/SandTrap.png")
-- luacheck: ignore
---@type love.Texture
SmallSandTrapActivated = love.graphics.newImage("assets/images/SmallSandTrapActivated.png")
-- luacheck: ignore
---@type love.Texture
SmallSandTrap = love.graphics.newImage("assets/images/SmallSandTrap.png")
-- luacheck: ignore
---@type love.Texture
StartButton = love.graphics.newImage("assets/images/StartButton.png")
-- luacheck: ignore
---@type FontData
---@type fun(fontSize: number | nil): love.Font
EtBt7001Z0xa = function(fontSize)
return love.graphics.newFont("assets/fonts/EtBt7001Z0xa.ttf", fontSize)
end

View File

@ -25,9 +25,11 @@ function generatedFileWarning()
return "-- GENERATED FILE - DO NOT EDIT\n-- Instead, edit the source file directly: assets.lua2p."
end)!!(generatedFileWarning())
!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'Image'))
!!(dirLookup('assets/sounds', 'wav', 'love.sound.newSoundData', 'SoundData'))
!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'SoundData'))
!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'FontData', function(varName, newFunc, file)
!!(dirLookup('assets/images', 'png', 'love.graphics.newImage', 'love.Texture'))
!!(dirLookup('assets/sounds', 'ogg', 'love.audio.newSource', 'love.Source', function(varName, newFunc, file)
return varName .. ' = ' .. newFunc .. '("' .. file .. '", "static")'
end))
!!(dirLookup('assets/music', 'wav', 'love.sound.newSoundData', 'love.SoundData'))
!!(dirLookup('assets/fonts', 'ttf', 'love.graphics.newFont', 'fun(fontSize: number | nil): love.Font', function(varName, newFunc, file)
return varName .. ' = function(fontSize)\n return ' .. newFunc .. '("' .. file .. '", fontSize)\nend'
end))

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#!/bin/sh
_=[[
_ = [[
exec lua "$0" "$@"
]]and nil
]] and nil
--==============================================================
--=
--= LuaPreprocess command line program
@ -175,35 +175,35 @@ Handler messages:
]]
--==============================================================
local startTime = os.time()
local startTime = os.time()
local startClock = os.clock()
local args = arg
if not args[0] then error("Expected to run from the Lua interpreter.") end
if not args[0] then
error("Expected to run from the Lua interpreter.")
end
local pp = dofile((args[0]:gsub("[^/\\]+$", "preprocess.lua")))
-- From args:
local addLineNumbers = false
local addLineNumbers = false
local allowBacktickStrings = false
local allowJitSyntax = false
local canOutputNil = true
local customData = nil
local fastStrings = false
local hasOutputExtension = false
local hasOutputPaths = false
local isDebug = false
local outputExtension = "lua"
local outputMeta = false -- flag|path
local processingInfoPath = ""
local silent = false
local validate = true
local macroPrefix = ""
local macroSuffix = ""
local releaseMode = false
local maxLogLevel = "trace"
local allowJitSyntax = false
local canOutputNil = true
local customData = nil
local fastStrings = false
local hasOutputExtension = false
local hasOutputPaths = false
local isDebug = false
local outputExtension = "lua"
local outputMeta = false -- flag|path
local processingInfoPath = ""
local silent = false
local validate = true
local macroPrefix = ""
local macroSuffix = ""
local releaseMode = false
local maxLogLevel = "trace"
local strictMacroArguments = true
--==============================================================
@ -212,46 +212,50 @@ local strictMacroArguments = true
local F = string.format
local function formatBytes(n)
if n >= 1024*1024*1024 then
return F("%.2f GiB", n/(1024*1024*1024))
elseif n >= 1024*1024 then
return F("%.2f MiB", n/(1024*1024))
elseif n >= 1024 then
return F("%.2f KiB", n/(1024))
elseif n == 1 then
return F("1 byte", n)
else
return F("%d bytes", n)
end
if n >= 1024 * 1024 * 1024 then
return F("%.2f GiB", n / (1024 * 1024 * 1024))
elseif n >= 1024 * 1024 then
return F("%.2f MiB", n / (1024 * 1024))
elseif n >= 1024 then
return F("%.2f KiB", n / 1024)
elseif n == 1 then
return F("1 byte", n)
else
return F("%d bytes", n)
end
end
local function printfNoise(s, ...)
print(s:format(...))
print(s:format(...))
end
local function printError(s)
io.stderr:write(s, "\n")
io.stderr:write(s, "\n")
end
local function printfError(s, ...)
io.stderr:write(s:format(...), "\n")
io.stderr:write(s:format(...), "\n")
end
local function errorLine(err)
printError(pp.tryToFormatError(err))
os.exit(1)
printError(pp.tryToFormatError(err))
os.exit(1)
end
local loadLuaFile = (
(_VERSION >= "Lua 5.2" or jit) and function(path, env)
return loadfile(path, "bt", env)
end
or function(path, env)
local chunk, err = loadfile(path)
if not chunk then return nil, err end
(_VERSION >= "Lua 5.2" or jit) and function(path, env)
return loadfile(path, "bt", env)
end
or function(path, env)
local chunk, err = loadfile(path)
if not chunk then
return nil, err
end
if env then setfenv(chunk, env) end
if env then
setfenv(chunk, env)
end
return chunk
end
return chunk
end
)
--==============================================================
@ -264,367 +268,339 @@ io.stderr:setvbuf("no")
math.randomseed(os.time()) -- In case math.random() is used anywhere.
math.random() -- Must kickstart...
local processOptions = true
local processOptions = true
local messageHandlerPath = ""
local pathsIn = {}
local pathsOut = {}
local pathsIn = {}
local pathsOut = {}
for _, arg in ipairs(args) do
if processOptions and (arg:find"^%-%-?help$" or arg == "/?" or arg:find"^/[Hh][Ee][Ll][Pp]$") then
print("LuaPreprocess v"..pp.VERSION)
print((help:gsub("\t", " ")))
os.exit()
if processOptions and (arg:find("^%-%-?help$") or arg == "/?" or arg:find("^/[Hh][Ee][Ll][Pp]$")) then
print("LuaPreprocess v" .. pp.VERSION)
print((help:gsub("\t", " ")))
os.exit()
elseif not (processOptions and arg:find("^%-.")) then
local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn
table.insert(paths, arg)
elseif not (processOptions and arg:find"^%-.") then
local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn
table.insert(paths, arg)
if arg == "-" and (not hasOutputPaths or paths == pathsOut) then
silent = true
end
elseif arg == "--" then
processOptions = false
elseif arg:find"^%-%-data=" or arg:find"^%-d=" then
customData = arg:gsub("^.-=", "")
elseif arg == "--backtickstrings" then
allowBacktickStrings = true
elseif arg == "--debug" then
isDebug = true
outputMeta = outputMeta or true
elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then
messageHandlerPath = arg:gsub("^.-=", "")
elseif arg == "--jitsyntax" then
allowJitSyntax = true
elseif arg == "--linenumbers" then
addLineNumbers = true
elseif arg == "--meta" then
outputMeta = true
elseif arg:find"^%-%-meta=" then
outputMeta = arg:gsub("^.-=", "")
elseif arg == "--nonil" then
canOutputNil = false
elseif arg == "--novalidate" then
validate = false
elseif arg:find"^%-%-outputextension=" then
if hasOutputPaths then
errorLine("Cannot specify both --outputextension and --outputpaths")
end
hasOutputExtension = true
outputExtension = arg:gsub("^.-=", "")
elseif arg == "--outputpaths" or arg == "-o" then
if hasOutputExtension then
errorLine("Cannot specify both --outputpaths and --outputextension")
elseif pathsIn[1] then
errorLine(arg.." must appear before any input path.")
end
hasOutputPaths = true
elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then
processingInfoPath = arg:gsub("^.-=", "")
elseif arg == "--silent" then
silent = true
elseif arg == "--faststrings" then
fastStrings = true
elseif arg == "--nogc" then
collectgarbage("stop")
elseif arg:find"^%-%-macroprefix=" then
macroPrefix = arg:gsub("^.-=", "")
elseif arg:find"^%-%-macrosuffix=" then
macroSuffix = arg:gsub("^.-=", "")
elseif arg == "--release" then
releaseMode = true
elseif arg:find"^%-%-loglevel=" then
maxLogLevel = arg:gsub("^.-=", "")
elseif arg == "--version" then
io.stdout:write(pp.VERSION)
os.exit()
elseif arg == "--nostrictmacroarguments" then
strictMacroArguments = false
else
errorLine("Unknown option '"..arg:gsub("=.*", "").."'.")
end
if arg == "-" and (not hasOutputPaths or paths == pathsOut) then
silent = true
end
elseif arg == "--" then
processOptions = false
elseif arg:find("^%-%-data=") or arg:find("^%-d=") then
customData = arg:gsub("^.-=", "")
elseif arg == "--backtickstrings" then
allowBacktickStrings = true
elseif arg == "--debug" then
isDebug = true
outputMeta = outputMeta or true
elseif arg:find("^%-%-handler=") or arg:find("^%-h=") then
messageHandlerPath = arg:gsub("^.-=", "")
elseif arg == "--jitsyntax" then
allowJitSyntax = true
elseif arg == "--linenumbers" then
addLineNumbers = true
elseif arg == "--meta" then
outputMeta = true
elseif arg:find("^%-%-meta=") then
outputMeta = arg:gsub("^.-=", "")
elseif arg == "--nonil" then
canOutputNil = false
elseif arg == "--novalidate" then
validate = false
elseif arg:find("^%-%-outputextension=") then
if hasOutputPaths then
errorLine("Cannot specify both --outputextension and --outputpaths")
end
hasOutputExtension = true
outputExtension = arg:gsub("^.-=", "")
elseif arg == "--outputpaths" or arg == "-o" then
if hasOutputExtension then
errorLine("Cannot specify both --outputpaths and --outputextension")
elseif pathsIn[1] then
errorLine(arg .. " must appear before any input path.")
end
hasOutputPaths = true
elseif arg:find("^%-%-saveinfo=") or arg:find("^%-i=") then
processingInfoPath = arg:gsub("^.-=", "")
elseif arg == "--silent" then
silent = true
elseif arg == "--faststrings" then
fastStrings = true
elseif arg == "--nogc" then
collectgarbage("stop")
elseif arg:find("^%-%-macroprefix=") then
macroPrefix = arg:gsub("^.-=", "")
elseif arg:find("^%-%-macrosuffix=") then
macroSuffix = arg:gsub("^.-=", "")
elseif arg == "--release" then
releaseMode = true
elseif arg:find("^%-%-loglevel=") then
maxLogLevel = arg:gsub("^.-=", "")
elseif arg == "--version" then
io.stdout:write(pp.VERSION)
os.exit()
elseif arg == "--nostrictmacroarguments" then
strictMacroArguments = false
else
errorLine("Unknown option '" .. arg:gsub("=.*", "") .. "'.")
end
end
if silent then
printfNoise = function()end
printfNoise = function() end
end
local header = "= LuaPreprocess v"..pp.VERSION..os.date(", %Y-%m-%d %H:%M:%S =", startTime)
local header = "= LuaPreprocess v" .. pp.VERSION .. os.date(", %Y-%m-%d %H:%M:%S =", startTime)
printfNoise(("="):rep(#header))
printfNoise("%s", header)
printfNoise(("="):rep(#header))
if hasOutputPaths and #pathsOut < #pathsIn then
errorLine("Missing output path for "..pathsIn[#pathsIn])
errorLine("Missing output path for " .. pathsIn[#pathsIn])
end
-- Prepare metaEnvironment.
pp.metaEnvironment.dataFromCommandLine = customData -- May be nil.
-- Load message handler.
local messageHandler = nil
local function hasMessageHandler(message)
if not messageHandler then
return false
elseif type(messageHandler) == "function" then
return true
elseif type(messageHandler) == "table" then
return messageHandler[message] ~= nil
else
assert(false)
end
if not messageHandler then
return false
elseif type(messageHandler) == "function" then
return true
elseif type(messageHandler) == "table" then
return messageHandler[message] ~= nil
else
assert(false)
end
end
local function sendMessage(message, ...)
if not messageHandler then
return
if not messageHandler then
return
elseif type(messageHandler) == "function" then
local returnValues = pp.pack(messageHandler(message, ...))
return pp.unpack(returnValues, 1, returnValues.n)
elseif type(messageHandler) == "table" then
local _messageHandler = messageHandler[message]
if not _messageHandler then
return
end
elseif type(messageHandler) == "function" then
local returnValues = pp.pack(messageHandler(message, ...))
return pp.unpack(returnValues, 1, returnValues.n)
elseif type(messageHandler) == "table" then
local _messageHandler = messageHandler[message]
if not _messageHandler then return end
local returnValues = pp.pack(_messageHandler(...))
return pp.unpack(returnValues, 1, returnValues.n)
else
assert(false)
end
local returnValues = pp.pack(_messageHandler(...))
return pp.unpack(returnValues, 1, returnValues.n)
else
assert(false)
end
end
if messageHandlerPath ~= "" then
-- Make the message handler and the metaprogram share the same environment.
-- This way the message handler can easily define globals that the metaprogram uses.
local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment)
if not mainChunk then
errorLine("Could not load message handler...\n"..pp.tryToFormatError(err))
end
-- Make the message handler and the metaprogram share the same environment.
-- This way the message handler can easily define globals that the metaprogram uses.
local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment)
if not mainChunk then
errorLine("Could not load message handler...\n" .. pp.tryToFormatError(err))
end
messageHandler = mainChunk()
messageHandler = mainChunk()
if type(messageHandler) == "function" then
-- void
elseif type(messageHandler) == "table" then
for message, _messageHandler in pairs(messageHandler) do
if type(message) ~= "string" then
errorLine(messageHandlerPath..": Table of handlers must only contain messages as keys.")
elseif type(_messageHandler) ~= "function" then
errorLine(messageHandlerPath..": Table of handlers must only contain functions as values.")
end
end
else
errorLine(messageHandlerPath..": File did not return a table or a function.")
end
if type(messageHandler) == "function" then
-- void
elseif type(messageHandler) == "table" then
for message, _messageHandler in pairs(messageHandler) do
if type(message) ~= "string" then
errorLine(messageHandlerPath .. ": Table of handlers must only contain messages as keys.")
elseif type(_messageHandler) ~= "function" then
errorLine(messageHandlerPath .. ": Table of handlers must only contain functions as values.")
end
end
else
errorLine(messageHandlerPath .. ": File did not return a table or a function.")
end
end
-- Init stuff.
sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better?
if not hasOutputPaths then
for i, pathIn in ipairs(pathsIn) do
pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "").."."..outputExtension
end
for i, pathIn in ipairs(pathsIn) do
pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "") .. "." .. outputExtension
end
end
if not pathsIn[1] then
errorLine("No path(s) specified.")
errorLine("No path(s) specified.")
elseif #pathsIn ~= #pathsOut then
errorLine(F("Number of input and output paths differ. (%d in, %d out)", #pathsIn, #pathsOut))
errorLine(F("Number of input and output paths differ. (%d in, %d out)", #pathsIn, #pathsOut))
end
local pathsSetIn = {}
local pathsSetIn = {}
local pathsSetOut = {}
for i = 1, #pathsIn do
if pathsSetIn [pathsIn [i]] then errorLine("Duplicate input path: " ..pathsIn [i]) end
if pathsSetOut[pathsOut[i]] then errorLine("Duplicate output path: "..pathsOut[i]) end
if pathsSetIn[pathsIn[i]] then
errorLine("Duplicate input path: " .. pathsIn[i])
end
if pathsSetOut[pathsOut[i]] then
errorLine("Duplicate output path: " .. pathsOut[i])
end
pathsSetIn [pathsIn [i]] = true
pathsSetOut[pathsOut[i]] = true
pathsSetIn[pathsIn[i]] = true
pathsSetOut[pathsOut[i]] = true
if pathsIn [i] ~= "-" and pathsSetOut[pathsIn [i]] then errorLine("Path is both input and output: "..pathsIn [i]) end
if pathsOut[i] ~= "-" and pathsSetIn [pathsOut[i]] then errorLine("Path is both input and output: "..pathsOut[i]) end
if pathsIn[i] ~= "-" and pathsSetOut[pathsIn[i]] then
errorLine("Path is both input and output: " .. pathsIn[i])
end
if pathsOut[i] ~= "-" and pathsSetIn[pathsOut[i]] then
errorLine("Path is both input and output: " .. pathsOut[i])
end
end
-- Process files.
-- :SavedInfo
local processingInfo = {
date = os.date("%Y-%m-%d %H:%M:%S", startTime),
files = {},
date = os.date("%Y-%m-%d %H:%M:%S", startTime),
files = {},
}
local byteCount = 0
local lineCount = 0
local byteCount = 0
local lineCount = 0
local lineCountCode = 0
local tokenCount = 0
local tokenCount = 0
for i, pathIn in ipairs(pathsIn) do
local startClockForPath = os.clock()
printfNoise("Processing '%s'...", pathIn)
local startClockForPath = os.clock()
printfNoise("Processing '%s'...", pathIn)
local pathOut = pathsOut[i]
local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "")..".meta.lua"
local pathOut = pathsOut[i]
local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "") .. ".meta.lua"
if not outputMeta or pathOut == "-" then
pathMeta = nil
end
if not outputMeta or pathOut == "-" then
pathMeta = nil
end
local info, err = pp.processFile{
pathIn = pathIn,
pathMeta = pathMeta,
pathOut = pathOut,
local info, err = pp.processFile({
pathIn = pathIn,
pathMeta = pathMeta,
pathOut = pathOut,
debug = isDebug,
addLineNumbers = addLineNumbers,
debug = isDebug,
addLineNumbers = addLineNumbers,
backtickStrings = allowBacktickStrings,
jitSyntax = allowJitSyntax,
canOutputNil = canOutputNil,
fastStrings = fastStrings,
validate = validate,
strictMacroArguments = strictMacroArguments,
backtickStrings = allowBacktickStrings,
jitSyntax = allowJitSyntax,
canOutputNil = canOutputNil,
fastStrings = fastStrings,
validate = validate,
strictMacroArguments = strictMacroArguments,
macroPrefix = macroPrefix,
macroSuffix = macroSuffix,
macroPrefix = macroPrefix,
macroSuffix = macroSuffix,
release = releaseMode,
logLevel = maxLogLevel,
release = releaseMode,
logLevel = maxLogLevel,
onInsert = (hasMessageHandler("insert") or nil) and function(name)
local lua = sendMessage("insert", pathIn, name)
onInsert = (hasMessageHandler("insert") or nil) and function(name)
local lua = sendMessage("insert", pathIn, name)
-- onInsert() is expected to return a Lua code string and so is the message
-- handler. However, if the handler is a single catch-all function we allow
-- the message to not be handled and we fall back to the default behavior of
-- treating 'name' as a path to a file to be inserted. If we didn't allow this
-- then it would be required for the "insert" message to be handled. I think
-- it's better if the user can choose whether to handle a message or not!
--
if lua == nil and type(messageHandler) == "function" then
return assert(pp.readFile(name))
end
-- onInsert() is expected to return a Lua code string and so is the message
-- handler. However, if the handler is a single catch-all function we allow
-- the message to not be handled and we fall back to the default behavior of
-- treating 'name' as a path to a file to be inserted. If we didn't allow this
-- then it would be required for the "insert" message to be handled. I think
-- it's better if the user can choose whether to handle a message or not!
--
if lua == nil and type(messageHandler) == "function" then
return assert(pp.readFile(name))
end
return lua
end,
return lua
end,
onBeforeMeta = messageHandler and function(lua)
sendMessage("beforemeta", pathIn, lua)
end,
onBeforeMeta = messageHandler and function(lua)
sendMessage("beforemeta", pathIn, lua)
end,
onAfterMeta = messageHandler and function(lua)
local luaModified = sendMessage("aftermeta", pathIn, lua)
onAfterMeta = messageHandler
and function(lua)
local luaModified = sendMessage("aftermeta", pathIn, lua)
if type(luaModified) == "string" then
lua = luaModified
if type(luaModified) == "string" then
lua = luaModified
elseif luaModified ~= nil then
error(
F(
"%s: Message handler did not return a string for 'aftermeta'. (Got %s)",
messageHandlerPath,
type(luaModified)
)
)
end
elseif luaModified ~= nil then
error(F(
"%s: Message handler did not return a string for 'aftermeta'. (Got %s)",
messageHandlerPath, type(luaModified)
))
end
return lua
end,
return lua
end,
onDone = messageHandler and function(info)
sendMessage("filedone", pathIn, pathOut, info)
end,
onDone = messageHandler and function(info)
sendMessage("filedone", pathIn, pathOut, info)
end,
onError = function(err)
xpcall(function()
sendMessage("fileerror", pathIn, err)
end, function(err)
printfError("Additional error in 'fileerror' message handler...\n%s", pp.tryToFormatError(err))
end)
os.exit(1)
end,
})
assert(info, err) -- The onError() handler above should have been called and we should have exited already.
onError = function(err)
xpcall(function()
sendMessage("fileerror", pathIn, err)
end, function(err)
printfError("Additional error in 'fileerror' message handler...\n%s", pp.tryToFormatError(err))
end)
os.exit(1)
end,
}
assert(info, err) -- The onError() handler above should have been called and we should have exited already.
byteCount = byteCount + info.processedByteCount
lineCount = lineCount + info.lineCount
lineCountCode = lineCountCode + info.linesOfCode
tokenCount = tokenCount + info.tokenCount
byteCount = byteCount + info.processedByteCount
lineCount = lineCount + info.lineCount
lineCountCode = lineCountCode + info.linesOfCode
tokenCount = tokenCount + info.tokenCount
if processingInfoPath ~= "" then
-- :SavedInfo
table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains.
end
if processingInfoPath ~= "" then
-- :SavedInfo
table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains.
end
printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock()-startClockForPath)
printfNoise(("-"):rep(#header))
printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock() - startClockForPath)
printfNoise(("-"):rep(#header))
end
-- Finalize stuff.
if processingInfoPath ~= "" then
printfNoise("Saving processing info to '%s'.", processingInfoPath)
printfNoise("Saving processing info to '%s'.", processingInfoPath)
local luaParts = {"return"}
assert(pp.serialize(luaParts, processingInfo))
local lua = table.concat(luaParts)
local luaParts = { "return" }
assert(pp.serialize(luaParts, processingInfo))
local lua = table.concat(luaParts)
local file = assert(io.open(processingInfoPath, "wb"))
file:write(lua)
file:close()
local file = assert(io.open(processingInfoPath, "wb"))
file:write(lua)
file:close()
end
printfNoise(
"All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)",
os.clock()-startClock,
#pathsIn, (#pathsIn == 1) and "" or "s",
lineCountCode,
lineCount, (lineCount == 1) and "" or "s",
tokenCount, (tokenCount == 1) and "" or "s",
formatBytes(byteCount)
"All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)",
os.clock() - startClock,
#pathsIn,
(#pathsIn == 1) and "" or "s",
lineCountCode,
lineCount,
(lineCount == 1) and "" or "s",
tokenCount,
(tokenCount == 1) and "" or "s",
formatBytes(byteCount)
)
sendMessage("alldone") -- @Incomplete: Use pcall and format error message better?
--[[!===========================================================
Copyright © 2018-2022 Marcus 'ReFreezed' Thunström
@ -648,4 +624,3 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
==============================================================]]

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---@field indices table<any, any> field is a table of Entity keys to their indices in the entities list. Most Systems can ignore this.
---@field modified boolean indicator for if the System has been modified in the last update. If so, the onModify callback will be called on the System in the next update, if it has one. This is usually managed by tiny-ecs, so users should mostly ignore this, too.
--- @module tiny-ecs
-- @author Calvin Rose
-- @license MIT
@ -103,14 +102,13 @@ local filterJoin
-- A helper function to filters from string
local filterBuildString
local function filterJoinRaw(invert, joining_op, ...)
local _args = {...}
local _args = { ... }
return function(system, e)
local acc
local args = _args
if joining_op == 'or' then
if joining_op == "or" then
acc = false
for i = 1, #args do
local v = args[i]
@ -119,7 +117,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then
acc = acc or v(system, e)
else
error 'Filter token must be a string or a filter function.'
error("Filter token must be a string or a filter function.")
end
end
else
@ -131,7 +129,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then
acc = acc and v(system, e)
else
error 'Filter token must be a string or a filter function.'
error("Filter token must be a string or a filter function.")
end
end
end
@ -146,69 +144,68 @@ local function filterJoinRaw(invert, joining_op, ...)
end
do
function filterJoin(...)
local state, value = pcall(filterJoinRaw, ...)
if state then return value else return nil, value end
if state then
return value
else
return nil, value
end
end
local function buildPart(str)
local accum = {}
local subParts = {}
str = str:gsub('%b()', function(p)
str = str:gsub("%b()", function(p)
subParts[#subParts + 1] = buildPart(p:sub(2, -2))
return ('\255%d'):format(#subParts)
return ("\255%d"):format(#subParts)
end)
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do
if part:match('^\255%d+$') then
for invert, part, sep in str:gmatch("(%!?)([^%|%&%!]+)([%|%&]?)") do
if part:match("^\255%d+$") then
local partIndex = tonumber(part:match(part:sub(2)))
accum[#accum + 1] = ('%s(%s)')
:format(invert == '' and '' or 'not', subParts[partIndex])
accum[#accum + 1] = ("%s(%s)"):format(invert == "" and "" or "not", subParts[partIndex])
else
accum[#accum + 1] = ("(e[%s] %s nil)")
:format(make_safe(part), invert == '' and '~=' or '==')
accum[#accum + 1] = ("(e[%s] %s nil)"):format(make_safe(part), invert == "" and "~=" or "==")
end
if sep ~= '' then
accum[#accum + 1] = (sep == '|' and ' or ' or ' and ')
if sep ~= "" then
accum[#accum + 1] = (sep == "|" and " or " or " and ")
end
end
return table.concat(accum)
end
function filterBuildString(str)
local source = ("return function(_, e) return %s end")
:format(buildPart(str))
local source = ("return function(_, e) return %s end"):format(buildPart(str))
local loader, err = loadstring(source)
if err then
error(err)
end
return loader()
end
end
--- Makes a Filter that selects Entities with all specified Components and
-- Filters.
function tiny.requireAll(...)
return filterJoin(false, 'and', ...)
return filterJoin(false, "and", ...)
end
--- Makes a Filter that selects Entities with at least one of the specified
-- Components and Filters.
function tiny.requireAny(...)
return filterJoin(false, 'or', ...)
return filterJoin(false, "or", ...)
end
--- Makes a Filter that rejects Entities with all specified Components and
-- Filters, and selects all other Entities.
function tiny.rejectAll(...)
return filterJoin(true, 'and', ...)
return filterJoin(true, "and", ...)
end
--- Makes a Filter that rejects Entities with at least one of the specified
-- Components and Filters, and selects all other Entities.
function tiny.rejectAny(...)
return filterJoin(true, 'or', ...)
return filterJoin(true, "or", ...)
end
--- Makes a Filter from a string. Syntax of `pattern` is as follows.
@ -224,7 +221,11 @@ end
-- @param pattern
function tiny.filter(pattern)
local state, value = pcall(filterBuildString, pattern)
if state then return value else return nil, value end
if state then
return value
else
return nil, value
end
end
--- System functions.
@ -463,8 +464,7 @@ function tiny.world(...)
entities = {},
-- List of Systems
systems = {}
systems = {},
}, worldMetaTable)
tiny_add(ret, ...)
@ -673,7 +673,6 @@ end
-- Adds, removes, and changes Entities that have been marked.
function tiny_manageEntities(world)
local e2r = world.entitiesToRemove
local e2c = world.entitiesToChange
@ -793,7 +792,6 @@ end
-- Systems. If `filter` is not supplied, all Systems are updated. Put this
-- function in your main loop.
function tiny.update(world, dt, filter)
tiny_manageSystems(world)
tiny_manageEntities(world)
@ -809,8 +807,7 @@ function tiny.update(world, dt, filter)
onModify(system, dt)
end
local preWrap = system.preWrap
if preWrap and
((not filter) or filter(world, system)) then
if preWrap and ((not filter) or filter(world, system)) then
preWrap(system, dt)
end
end
@ -853,12 +850,10 @@ function tiny.update(world, dt, filter)
for i = 1, #systems do
local system = systems[i]
local postWrap = system.postWrap
if postWrap and system.active and
((not filter) or filter(world, system)) then
if postWrap and system.active and ((not filter) or filter(world, system)) then
postWrap(system, dt)
end
end
end
--- Removes all Entities from the World.
@ -924,11 +919,11 @@ worldMetaTable = {
clearSystems = tiny.clearSystems,
getEntityCount = tiny.getEntityCount,
getSystemCount = tiny.getSystemCount,
setSystemIndex = tiny.setSystemIndex
setSystemIndex = tiny.setSystemIndex,
},
__tostring = function()
return "<tiny-ecs_World>"
end
end,
}
_G.tiny = tiny

190
main.lua
View File

@ -9,25 +9,177 @@ require("generated/filter-types")
require("generated/assets")
require("generated/all-systems")
local scenarios = {
default = function()
-- TODO: Add default entities
end,
textTestScenario = function()
local width, height = love.graphics.getWidth(), love.graphics.getHeight()
local squareSize = 80
local tileSize = math.floor(squareSize * 1.2)
local marginSize = 10
CursorMask = 1
BallMask = 2
local function emptyTile(unhighlightSiblings, gridPosition)
local size = { x = squareSize, y = squareSize }
return {
unhighlightSiblings = unhighlightSiblings,
canBeCollidedBy = CursorMask,
highlightOnMouseOver = T.marker,
size = size,
gridPosition = gridPosition,
drawAsRectangle = { size = size },
}
end
local function replaceAt(grid, y, x, entity)
local current = grid[y][x]
grid[y][x] = entity
current.unhighlightSiblings[entity] = true
current.unhighlightSiblings[current] = nil
entity.unhighlightSiblings = current.unhighlightSiblings
-- entity.position = current.position
entity.gridPosition = current.gridPosition
entity.toLeft = current.toLeft
entity.toRight = current.toRight
entity.above = current.above
entity.below = current.below
current.toLeft.toRight = current.toRight
current.toRight.toLeft = current.toLeft
current.above.below = current.below
current.below.above = current.above
World:removeEntity(current)
World:addEntity(entity)
end
function PositionAtGridXy(x, y)
return {
x = 20 + ((x - 1) * tileSize),
y = 60 + ((y - 1) * tileSize),
}
end
Scenarios = {
---@return table xyGrid Where grid[y][x] is an Entity
emptyGrid = function()
local cursorSize = { x = 5, y = 5 }
World:addEntity({
position = { x = 0, y = 600 },
drawAsText = {
text = "Hello, world!",
style = TextStyle.Inverted,
size = cursorSize,
moveWithCursor = T.marker,
canCollideWith = bit.bor(CursorMask, BallMask),
position = { x = -999, y = -999 },
highlightsCollided = T.marker,
drawAsRectangle = {
size = cursorSize,
mode = "fill",
},
})
World:addEntity({
position = { x = 0, y = 0 },
drawAsSprite = Grass,
z = 0,
})
-- Temporary storages for connecting grid items
local previous
local unhighlightSiblings = {}
local yxGrid = {}
local xCount = 8
local yCount = 5
for y = 1, yCount do
yxGrid[y] = {}
for x = 1, xCount do
local current = World:addEntity(emptyTile(unhighlightSiblings, { x = x, y = y }))
yxGrid[y][x] = current
unhighlightSiblings[current] = true
current.toLeft = previous
if previous ~= nil then
if previous.toRight == nil then
previous.toRight = current
end
else
current.canReceiveButtons = T.marker
current.highlighted = T.marker
end
if yxGrid[y - 1] and yxGrid[y - 1][x] then
current.above = yxGrid[y - 1][x]
yxGrid[y - 1][x].below = current
end
if y == yCount then
-- Connect last row to first row
yxGrid[1][x].above = current
current.below = yxGrid[1][x]
end
if x == xCount then
-- Connect last entry to first Entry
current.toRight = yxGrid[y][1]
yxGrid[y][1].toLeft = current
end
previous = current
end
end
return yxGrid
end,
firstLevel = function()
local yxGrid = Scenarios.emptyGrid()
replaceAt(yxGrid, 4, 7, {
drawAsSprite = Flag,
z = 1,
effectsToApply = {
endOfRound = T.marker,
}
})
replaceAt(yxGrid, 2, 2, {
drawAsSprite = SmallSandTrap,
z = 1,
spriteAfterEffect = SmallSandTrapActivated,
effectsToApply = {
slowsBy = { x = 1, y = 1 },
movement = { x = 0, y = 0, },
},
})
replaceAt(yxGrid, 1, 1, {
pickedUpOnClick = T.marker,
drawAsSprite = GolferRight,
z = 1,
spriteAfterEffect = GolferRightActivated,
effectsToApply = {
movement = { x = 6, y = 0, },
},
})
replaceAt(yxGrid, 1, 7, {
pickedUpOnClick = T.marker,
drawAsSprite = GolferRight,
z = 1,
spriteAfterEffect = GolferRightActivated,
effectsToApply = {
movement = { x = 0, y = 3 },
},
})
World:addEntity({
ballEffects = {},
drawAsSprite = Ball,
gridPosition = { x = 1, y = 1 },
z = 2,
})
-- TODO: Make the start button start
World:addEntity({
drawAsSprite = StartButton,
z = 3,
position = {
x = width - 120,
y = height - 50,
},
velocity = { x = 240, y = -500 },
mass = 1,
decayAfterSeconds = 10,
})
end,
}
scenarios.textTestScenario()
Scenarios.firstLevel()
function love.load()
love.graphics.setBackgroundColor(1, 1, 1)
@ -35,5 +187,15 @@ function love.load()
end
function love.draw()
World:update(love.timer.getDelta())
local dt = love.timer.getDelta()
if love.keyboard.isDown("r") then
World:clearEntities()
Scenarios.firstLevel()
end
World:update(dt, function(_, system)
return not system.isDrawSystem
end)
World:update(dt, function(_, system)
return system.isDrawSystem
end)
end

View File

@ -1,11 +1,24 @@
collidingEntities = filteredSystem("collidingEntitites", {
velocity = T.XyPair,
position = T.XyPair,
size = T.XyPair,
canCollideWith = T.BitMask,
isSolid = Maybe(T.bool),
})
local function intersects(rect, rectOther)
local left = rect.position.x
local right = rect.position.x + rect.size.x
local top = rect.position.y
local bottom = rect.position.y + rect.size.y
local leftOther = rectOther.position.x
local rightOther = rectOther.position.x + rectOther.size.x
local topOther = rectOther.position.y
local bottomOther = rectOther.position.y + rectOther.size.y
return leftOther < right and left < rightOther and topOther < bottom and top < bottomOther
end
filteredSystem(
"collisionDetection",
{ position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) },
@ -18,19 +31,7 @@ filteredSystem(
and e.canBeCollidedBy
and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0
then
local colliderTop = collider.position.y
local colliderBottom = collider.position.y + collider.size.y
local entityTop = e.position.y
local entityBottom = entityTop + e.size.y
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
or (entityBottom > colliderTop and entityBottom < colliderBottom)
if
withinY
and collider.position.x < e.position.x + e.size.x
and collider.position.x + collider.size.x > e.position.x
then
if intersects(e, collider) then
system.world:addEntity({ collisionBetween = { e, collider } })
end
end

View File

@ -1,4 +1,23 @@
filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system)
Collisions = filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, _, system)
local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2]
if collider.highlightsCollided then
if collidedInto.unhighlightSiblings then
for sibling in pairs(collidedInto.unhighlightSiblings) do
sibling.highlighted = nil
sibling.canReceiveButtons = nil
system.world:addEntity(sibling)
end
end
if collidedInto.highlightOnMouseOver ~= nil then
collidedInto.highlighted = T.marker
collidedInto.canReceiveButtons = T.marker
system.world:addEntity(collidedInto)
end
if collidedInto.pickedUpOnClick ~= nil then -- and #HeldByCursor.entities == 0 and love.mouse.isDown(1) then
print("Click!")
collidedInto.moveWithCursor = T.marker
system.world:addEntity(collidedInto)
end
end
system.world:removeEntity(e)
end)

View File

@ -3,4 +3,4 @@ filteredSystem("decay", { decayAfterSeconds = T.number }, function(e, dt, system
if e.decayAfterSeconds <= 0 then
system.world:removeEntity(e)
end
end)
end)

View File

@ -1,23 +1,52 @@
local floor = math.floor
local gfx = love.graphics
---
---@generic T
---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil
---@return System | { entities: T[] }
local function drawSystem(name, shape, process)
local system = filteredSystem(name, shape, process, function(_, a, b)
if a.z ~= nil and b.z ~= nil then
return a.z < b.z
end
if a.z ~= nil then
return true
end
return false
end)
system.isDrawSystem = true
return system
end
filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
filteredSystem("mapGridPositionToRealPosition", { gridPosition = T.XyPair }, function(e, _, system)
e.position = PositionAtGridXy(e.gridPosition.x, e.gridPosition.y)
system.world:addEntity(e)
end)
filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e)
if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then
return
local spriteDrawSystem = drawSystem(
"drawSprites",
{ position = T.XyPair, drawAsSprite = T.Drawable, rotation = Maybe(T.number) },
function(e)
if not e.drawAsSprite then
return
end
local width, height = e.drawAsSprite:getDimensions()
gfx.draw(e.drawAsSprite, e.position.x, e.position.y)
end
e.drawAsSprite:draw(e.position.x, e.position.y)
end)
)
function spriteDrawSystem:preProcess()
gfx.setColor(1, 1, 1)
end
local margin = 8
filteredSystem(
drawSystem(
"drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } },
function(e)
local font = gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local font = e.font or gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local textHeight = font:getHeight()
local textWidth = font:getWidth(e.drawAsText.text)
@ -40,3 +69,9 @@ filteredSystem(
gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin)
end
)
drawSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.setColor(1, 1, 1, 0.5)
local mode = e.drawAsRectangle.mode or (e.highlighted and "fill" or "line")
gfx.rectangle(mode, e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
end)

View File

@ -1,3 +1,5 @@
local isDown = love.keyboard.isDown
---@type ButtonState
local buttonState = {}
@ -6,8 +8,105 @@ buttonInputSystem = filteredSystem("buttonInput", { canReceiveButtons = T.marker
system.world:addEntity(e)
end)
HeldByCursor = filteredSystem("HeldByCursor", { pickedUpOnClick = T.marker, moveWithCursor = T.marker })
function buttonInputSystem:preProcess()
if #self.entities == 0 then
return
end
end
end
function love.keypressed(key, _, _)
buttonState[key] = true
end
function ClearButtonState()
for key in pairs(buttonState) do
buttonState[key] = nil
end
end
---@type System
local cursorTracking
local mouseX, mouseY = -9999, -9999
local mouseInControl = false
function love.mousemoved(x, y)
mouseInControl = true
mouseX, mouseY = x, y
end
local keyDebounceSec = 0.1
local delay = 0
local menuSystem = filteredSystem(
"menu",
{ canReceiveButtons = T.marker, highlighted = T.marker },
function(e, dt, system)
if delay > 0 then
delay = delay - dt
return
end
local function tryShiftMenu(target, keys)
if target == nil then
return false
end
for _, key in ipairs(keys) do
if isDown(key) then
e.highlighted = nil
e.canReceiveButtons = nil
target.canReceiveButtons = T.marker
target.highlighted = T.marker
system.world:addEntity(e)
system.world:addEntity(target)
e = target
return true
end
end
return false
end
local pressed = tryShiftMenu(e.toRight, { "right", "d" })
pressed = tryShiftMenu(e.toLeft, { "left", "a" }) or pressed
pressed = tryShiftMenu(e.below, { "down", "s" }) or pressed
pressed = tryShiftMenu(e.above, { "up", "w" }) or pressed
if isDown("return") then
pressed = true
system.world:addEntity({
round = "start",
})
end
if pressed then
mouseInControl = false
mouseX, mouseY = -9999, -9999
for _, tracker in pairs(cursorTracking.entities) do
tracker.position.x = -9999
tracker.position.y = -9999
end
delay = keyDebounceSec
end
end
)
cursorTracking = filteredSystem("cursorTracking", { moveWithCursor = T.marker, position = T.XyPair }, function(e)
if mouseInControl then
e.position.x = mouseX
e.position.y = mouseY
end
end)
-- local allHighlighted = filteredSystem("allHighlighted", { highlighted = T.marker })
function cursorTracking:postProcess()
-- if mouseInControl and #Collisions.entities == 0 then
-- -- If the cursor is not colliding with anything, wipe all highlighted components
-- for _, highlighted in pairs(allHighlighted.entities) do
-- highlighted.highlighted = nil
-- self.world:addEntity(highlighted)
-- end
-- end
end

85
systems/rounds.lua Normal file
View File

@ -0,0 +1,85 @@
local gridElements = filteredSystem("gridElements", { gridPosition = T.XyPair, effectsToApply = T.AnyComponent })
local roundRunning = false
filteredSystem("timers", { timerSec = T.number, callback = T.SelfFunction }, function(e, dt, system)
e.timerSec = e.timerSec - dt
if e.timerSec < 0 then
e:callback()
system.world:removeEntity(e)
end
end)
local function sign(n)
return n > 0 and 1 or n < 0 and -1 or 0
end
local roundSystem = filteredSystem("rounds", { round = T.str })
local activeBallEffects = filteredSystem("activeBallEffects", { ballEffects = T.AnyComponent, gridPosition = T.XyPair }, function(e, dt, system)
local roundActive = false
for _, state in pairs(roundSystem.entities) do
if state.round == "start" then
roundActive = true
end
end
if not roundActive then
return
end
local gridPosition, effects = e.gridPosition, e.ballEffects
-- Search for new effects from the current tile
for _, gridElement in pairs(gridElements.entities) do
if
gridPosition.x == gridElement.gridPosition.x
and gridPosition.y == gridElement.gridPosition.y
then
-- More direct-mutation-y than I'd like,
-- but offers a simple way to overwrite existing effects.
-- We're "setting InRelations" :D
for key, value in pairs(gridElement.effectsToApply) do
effects[key] = value
end
if gridElement.spriteAfterEffect then
gridElement.drawAsSprite = gridElement.spriteAfterEffect
gridElement.spriteAfterEffect = nil
end
gridElement.effectsToApply = nil
system.world:addEntity(gridElement)
end
end
-- Apply any effects currently connected to this ball
if effects.movement ~= nil then
gridPosition.x = gridPosition.x + sign(effects.movement.x)
gridPosition.y = gridPosition.y + sign(effects.movement.y)
effects.movement.x = effects.movement.x - sign(effects.movement.x)
effects.movement.y = effects.movement.y - sign(effects.movement.y)
if effects.movement.x == 0 and effects.movement.y == 0 then
effects.movement = nil
end
end
-- TODO: Trigger the round end
-- for _, _ in pairs(effects) do
-- -- Return if there are any effects left
-- return
-- end
-- system.world:addEntity({ round = "end" })
end)
local stepTimeSec = 0.3
local stepTimer = stepTimeSec
function activeBallEffects:preProcess(dt)
stepTimer = stepTimer - dt
if stepTimer <= 0 then
stepTimer = stepTimeSec
return
end
return tiny.SKIP_PROCESS
end

View File

@ -13,4 +13,4 @@ filteredSystem("drag", { velocity = T.XyPair, drag = T.number }, function(e, dt,
local currentDrag = e.drag * dt
e.velocity.x = e.velocity.x - (e.velocity.x * currentDrag * dt)
e.velocity.y = e.velocity.y - (e.velocity.y * currentDrag * dt)
end)
end)

View File

@ -39,4 +39,4 @@ if tinyWarnWhenNonDataOnEntities then
return valType
end
end
end
end

View File

@ -1,13 +1,15 @@
---@generic T
---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil
---@param compare nil | fun(system: System, entityA: T, entityB: T): boolean
---@return System | { entities: T[] }
function filteredSystem(name, shape, process)
function filteredSystem(name, shape, process, compare)
assert(type(name) == "string")
assert(type(shape) == "table" or type(shape) == "function")
assert(process == nil or type(process) == "function")
local system = tiny.processingSystem()
local system = compare and tiny.sortedProcessingSystem() or tiny.processingSystem()
system.compare = compare
system.name = name
if type(shape) == "table" then
local keys = {}