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" std = "lua54+love"
stds.project = { stds.project = {
globals = {"tiny"}, 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/draw")
require("../systems/gravity") require("../systems/gravity")
require("../systems/input") require("../systems/input")
require("../systems/rounds")
require("../systems/velocity") require("../systems/velocity")

View File

@ -1,11 +1,47 @@
-- GENERATED FILE - DO NOT EDIT -- GENERATED FILE - DO NOT EDIT
-- Instead, edit the source file directly: assets.lua2p. -- 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 -- luacheck: ignore
---@type FontData ---@type fun(fontSize: number | nil): love.Font
EtBt7001Z0xa = function(fontSize) EtBt7001Z0xa = function(fontSize)
return love.graphics.newFont("assets/fonts/EtBt7001Z0xa.ttf", fontSize) return love.graphics.newFont("assets/fonts/EtBt7001Z0xa.ttf", fontSize)
end end

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
_=[[ _ = [[
exec lua "$0" "$@" exec lua "$0" "$@"
]]and nil ]] and nil
--============================================================== --==============================================================
--= --=
--= LuaPreprocess command line program --= LuaPreprocess command line program
@ -175,35 +175,35 @@ Handler messages:
]] ]]
--============================================================== --==============================================================
local startTime = os.time()
local startTime = os.time()
local startClock = os.clock() local startClock = os.clock()
local args = arg 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"))) local pp = dofile((args[0]:gsub("[^/\\]+$", "preprocess.lua")))
-- From args: -- From args:
local addLineNumbers = false local addLineNumbers = false
local allowBacktickStrings = false local allowBacktickStrings = false
local allowJitSyntax = false local allowJitSyntax = false
local canOutputNil = true local canOutputNil = true
local customData = nil local customData = nil
local fastStrings = false local fastStrings = false
local hasOutputExtension = false local hasOutputExtension = false
local hasOutputPaths = false local hasOutputPaths = false
local isDebug = false local isDebug = false
local outputExtension = "lua" local outputExtension = "lua"
local outputMeta = false -- flag|path local outputMeta = false -- flag|path
local processingInfoPath = "" local processingInfoPath = ""
local silent = false local silent = false
local validate = true local validate = true
local macroPrefix = "" local macroPrefix = ""
local macroSuffix = "" local macroSuffix = ""
local releaseMode = false local releaseMode = false
local maxLogLevel = "trace" local maxLogLevel = "trace"
local strictMacroArguments = true local strictMacroArguments = true
--============================================================== --==============================================================
@ -212,46 +212,50 @@ local strictMacroArguments = true
local F = string.format local F = string.format
local function formatBytes(n) local function formatBytes(n)
if n >= 1024*1024*1024 then if n >= 1024 * 1024 * 1024 then
return F("%.2f GiB", n/(1024*1024*1024)) return F("%.2f GiB", n / (1024 * 1024 * 1024))
elseif n >= 1024*1024 then elseif n >= 1024 * 1024 then
return F("%.2f MiB", n/(1024*1024)) return F("%.2f MiB", n / (1024 * 1024))
elseif n >= 1024 then elseif n >= 1024 then
return F("%.2f KiB", n/(1024)) return F("%.2f KiB", n / 1024)
elseif n == 1 then elseif n == 1 then
return F("1 byte", n) return F("1 byte", n)
else else
return F("%d bytes", n) return F("%d bytes", n)
end end
end end
local function printfNoise(s, ...) local function printfNoise(s, ...)
print(s:format(...)) print(s:format(...))
end end
local function printError(s) local function printError(s)
io.stderr:write(s, "\n") io.stderr:write(s, "\n")
end end
local function printfError(s, ...) local function printfError(s, ...)
io.stderr:write(s:format(...), "\n") io.stderr:write(s:format(...), "\n")
end end
local function errorLine(err) local function errorLine(err)
printError(pp.tryToFormatError(err)) printError(pp.tryToFormatError(err))
os.exit(1) os.exit(1)
end end
local loadLuaFile = ( local loadLuaFile = (
(_VERSION >= "Lua 5.2" or jit) and function(path, env) (_VERSION >= "Lua 5.2" or jit) and function(path, env)
return loadfile(path, "bt", env) return loadfile(path, "bt", env)
end end
or function(path, env) or function(path, env)
local chunk, err = loadfile(path) local chunk, err = loadfile(path)
if not chunk then return nil, err end if not chunk then
return nil, err
end
if env then setfenv(chunk, env) end if env then
setfenv(chunk, env)
end
return chunk return chunk
end end
) )
--============================================================== --==============================================================
@ -264,367 +268,339 @@ io.stderr:setvbuf("no")
math.randomseed(os.time()) -- In case math.random() is used anywhere. math.randomseed(os.time()) -- In case math.random() is used anywhere.
math.random() -- Must kickstart... math.random() -- Must kickstart...
local processOptions = true local processOptions = true
local messageHandlerPath = "" local messageHandlerPath = ""
local pathsIn = {} local pathsIn = {}
local pathsOut = {} local pathsOut = {}
for _, arg in ipairs(args) do for _, arg in ipairs(args) do
if processOptions and (arg:find"^%-%-?help$" or arg == "/?" or arg:find"^/[Hh][Ee][Ll][Pp]$") then if processOptions and (arg:find("^%-%-?help$") or arg == "/?" or arg:find("^/[Hh][Ee][Ll][Pp]$")) then
print("LuaPreprocess v"..pp.VERSION) print("LuaPreprocess v" .. pp.VERSION)
print((help:gsub("\t", " "))) print((help:gsub("\t", " ")))
os.exit() 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 if arg == "-" and (not hasOutputPaths or paths == pathsOut) then
local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn silent = true
table.insert(paths, arg) end
elseif arg == "--" then
if arg == "-" and (not hasOutputPaths or paths == pathsOut) then processOptions = false
silent = true elseif arg:find("^%-%-data=") or arg:find("^%-d=") then
end customData = arg:gsub("^.-=", "")
elseif arg == "--backtickstrings" then
elseif arg == "--" then allowBacktickStrings = true
processOptions = false elseif arg == "--debug" then
isDebug = true
elseif arg:find"^%-%-data=" or arg:find"^%-d=" then outputMeta = outputMeta or true
customData = arg:gsub("^.-=", "") elseif arg:find("^%-%-handler=") or arg:find("^%-h=") then
messageHandlerPath = arg:gsub("^.-=", "")
elseif arg == "--backtickstrings" then elseif arg == "--jitsyntax" then
allowBacktickStrings = true allowJitSyntax = true
elseif arg == "--linenumbers" then
elseif arg == "--debug" then addLineNumbers = true
isDebug = true elseif arg == "--meta" then
outputMeta = outputMeta or true outputMeta = true
elseif arg:find("^%-%-meta=") then
elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then outputMeta = arg:gsub("^.-=", "")
messageHandlerPath = arg:gsub("^.-=", "") elseif arg == "--nonil" then
canOutputNil = false
elseif arg == "--jitsyntax" then elseif arg == "--novalidate" then
allowJitSyntax = true validate = false
elseif arg:find("^%-%-outputextension=") then
elseif arg == "--linenumbers" then if hasOutputPaths then
addLineNumbers = true errorLine("Cannot specify both --outputextension and --outputpaths")
end
elseif arg == "--meta" then hasOutputExtension = true
outputMeta = true outputExtension = arg:gsub("^.-=", "")
elseif arg:find"^%-%-meta=" then elseif arg == "--outputpaths" or arg == "-o" then
outputMeta = arg:gsub("^.-=", "") if hasOutputExtension then
errorLine("Cannot specify both --outputpaths and --outputextension")
elseif arg == "--nonil" then elseif pathsIn[1] then
canOutputNil = false errorLine(arg .. " must appear before any input path.")
end
elseif arg == "--novalidate" then hasOutputPaths = true
validate = false elseif arg:find("^%-%-saveinfo=") or arg:find("^%-i=") then
processingInfoPath = arg:gsub("^.-=", "")
elseif arg:find"^%-%-outputextension=" then elseif arg == "--silent" then
if hasOutputPaths then silent = true
errorLine("Cannot specify both --outputextension and --outputpaths") elseif arg == "--faststrings" then
end fastStrings = true
hasOutputExtension = true elseif arg == "--nogc" then
outputExtension = arg:gsub("^.-=", "") collectgarbage("stop")
elseif arg:find("^%-%-macroprefix=") then
elseif arg == "--outputpaths" or arg == "-o" then macroPrefix = arg:gsub("^.-=", "")
if hasOutputExtension then elseif arg:find("^%-%-macrosuffix=") then
errorLine("Cannot specify both --outputpaths and --outputextension") macroSuffix = arg:gsub("^.-=", "")
elseif pathsIn[1] then elseif arg == "--release" then
errorLine(arg.." must appear before any input path.") releaseMode = true
end elseif arg:find("^%-%-loglevel=") then
hasOutputPaths = true maxLogLevel = arg:gsub("^.-=", "")
elseif arg == "--version" then
elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then io.stdout:write(pp.VERSION)
processingInfoPath = arg:gsub("^.-=", "") os.exit()
elseif arg == "--nostrictmacroarguments" then
elseif arg == "--silent" then strictMacroArguments = false
silent = true else
errorLine("Unknown option '" .. arg:gsub("=.*", "") .. "'.")
elseif arg == "--faststrings" then end
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 end
if silent then if silent then
printfNoise = function()end printfNoise = function() end
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(("="):rep(#header))
printfNoise("%s", header) printfNoise("%s", header)
printfNoise(("="):rep(#header)) printfNoise(("="):rep(#header))
if hasOutputPaths and #pathsOut < #pathsIn then if hasOutputPaths and #pathsOut < #pathsIn then
errorLine("Missing output path for "..pathsIn[#pathsIn]) errorLine("Missing output path for " .. pathsIn[#pathsIn])
end end
-- Prepare metaEnvironment. -- Prepare metaEnvironment.
pp.metaEnvironment.dataFromCommandLine = customData -- May be nil. pp.metaEnvironment.dataFromCommandLine = customData -- May be nil.
-- Load message handler. -- Load message handler.
local messageHandler = nil local messageHandler = nil
local function hasMessageHandler(message) local function hasMessageHandler(message)
if not messageHandler then if not messageHandler then
return false return false
elseif type(messageHandler) == "function" then
elseif type(messageHandler) == "function" then return true
return true elseif type(messageHandler) == "table" then
return messageHandler[message] ~= nil
elseif type(messageHandler) == "table" then else
return messageHandler[message] ~= nil assert(false)
end
else
assert(false)
end
end end
local function sendMessage(message, ...) local function sendMessage(message, ...)
if not messageHandler then if not messageHandler then
return 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(...))
local returnValues = pp.pack(messageHandler(message, ...)) return pp.unpack(returnValues, 1, returnValues.n)
return pp.unpack(returnValues, 1, returnValues.n) else
assert(false)
elseif type(messageHandler) == "table" then end
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
end end
if messageHandlerPath ~= "" then if messageHandlerPath ~= "" then
-- Make the message handler and the metaprogram share the same environment. -- Make the message handler and the metaprogram share the same environment.
-- This way the message handler can easily define globals that the metaprogram uses. -- This way the message handler can easily define globals that the metaprogram uses.
local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment) local mainChunk, err = loadLuaFile(messageHandlerPath, pp.metaEnvironment)
if not mainChunk then if not mainChunk then
errorLine("Could not load message handler...\n"..pp.tryToFormatError(err)) errorLine("Could not load message handler...\n" .. pp.tryToFormatError(err))
end end
messageHandler = mainChunk() messageHandler = mainChunk()
if type(messageHandler) == "function" then if type(messageHandler) == "function" then
-- void -- void
elseif type(messageHandler) == "table" then elseif type(messageHandler) == "table" then
for message, _messageHandler in pairs(messageHandler) do for message, _messageHandler in pairs(messageHandler) do
if type(message) ~= "string" then if type(message) ~= "string" then
errorLine(messageHandlerPath..": Table of handlers must only contain messages as keys.") errorLine(messageHandlerPath .. ": Table of handlers must only contain messages as keys.")
elseif type(_messageHandler) ~= "function" then elseif type(_messageHandler) ~= "function" then
errorLine(messageHandlerPath..": Table of handlers must only contain functions as values.") errorLine(messageHandlerPath .. ": Table of handlers must only contain functions as values.")
end end
end end
else else
errorLine(messageHandlerPath..": File did not return a table or a function.") errorLine(messageHandlerPath .. ": File did not return a table or a function.")
end end
end end
-- Init stuff. -- Init stuff.
sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better? sendMessage("init", pathsIn, (hasOutputPaths and pathsOut or nil)) -- @Incomplete: Use pcall and format error message better?
if not hasOutputPaths then if not hasOutputPaths then
for i, pathIn in ipairs(pathsIn) do for i, pathIn in ipairs(pathsIn) do
pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "").."."..outputExtension pathsOut[i] = (pathIn == "-") and "-" or pathIn:gsub("%.%w+$", "") .. "." .. outputExtension
end end
end end
if not pathsIn[1] then if not pathsIn[1] then
errorLine("No path(s) specified.") errorLine("No path(s) specified.")
elseif #pathsIn ~= #pathsOut then 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 end
local pathsSetIn = {} local pathsSetIn = {}
local pathsSetOut = {} local pathsSetOut = {}
for i = 1, #pathsIn do for i = 1, #pathsIn do
if pathsSetIn [pathsIn [i]] then errorLine("Duplicate input path: " ..pathsIn [i]) end if pathsSetIn[pathsIn[i]] then
if pathsSetOut[pathsOut[i]] then errorLine("Duplicate output path: "..pathsOut[i]) end errorLine("Duplicate input path: " .. pathsIn[i])
end
if pathsSetOut[pathsOut[i]] then
errorLine("Duplicate output path: " .. pathsOut[i])
end
pathsSetIn [pathsIn [i]] = true pathsSetIn[pathsIn[i]] = true
pathsSetOut[pathsOut[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 pathsIn[i] ~= "-" and pathsSetOut[pathsIn[i]] then
if pathsOut[i] ~= "-" and pathsSetIn [pathsOut[i]] then errorLine("Path is both input and output: "..pathsOut[i]) end 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 end
-- Process files. -- Process files.
-- :SavedInfo -- :SavedInfo
local processingInfo = { local processingInfo = {
date = os.date("%Y-%m-%d %H:%M:%S", startTime), date = os.date("%Y-%m-%d %H:%M:%S", startTime),
files = {}, files = {},
} }
local byteCount = 0 local byteCount = 0
local lineCount = 0 local lineCount = 0
local lineCountCode = 0 local lineCountCode = 0
local tokenCount = 0 local tokenCount = 0
for i, pathIn in ipairs(pathsIn) do for i, pathIn in ipairs(pathsIn) do
local startClockForPath = os.clock() local startClockForPath = os.clock()
printfNoise("Processing '%s'...", pathIn) printfNoise("Processing '%s'...", pathIn)
local pathOut = pathsOut[i] local pathOut = pathsOut[i]
local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "")..".meta.lua" local pathMeta = (type(outputMeta) == "string") and outputMeta or pathOut:gsub("%.%w+$", "") .. ".meta.lua"
if not outputMeta or pathOut == "-" then if not outputMeta or pathOut == "-" then
pathMeta = nil pathMeta = nil
end end
local info, err = pp.processFile{ local info, err = pp.processFile({
pathIn = pathIn, pathIn = pathIn,
pathMeta = pathMeta, pathMeta = pathMeta,
pathOut = pathOut, pathOut = pathOut,
debug = isDebug, debug = isDebug,
addLineNumbers = addLineNumbers, addLineNumbers = addLineNumbers,
backtickStrings = allowBacktickStrings, backtickStrings = allowBacktickStrings,
jitSyntax = allowJitSyntax, jitSyntax = allowJitSyntax,
canOutputNil = canOutputNil, canOutputNil = canOutputNil,
fastStrings = fastStrings, fastStrings = fastStrings,
validate = validate, validate = validate,
strictMacroArguments = strictMacroArguments, strictMacroArguments = strictMacroArguments,
macroPrefix = macroPrefix, macroPrefix = macroPrefix,
macroSuffix = macroSuffix, macroSuffix = macroSuffix,
release = releaseMode, release = releaseMode,
logLevel = maxLogLevel, logLevel = maxLogLevel,
onInsert = (hasMessageHandler("insert") or nil) and function(name) onInsert = (hasMessageHandler("insert") or nil) and function(name)
local lua = sendMessage("insert", pathIn, name) local lua = sendMessage("insert", pathIn, name)
-- onInsert() is expected to return a Lua code string and so is the message -- 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 -- 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 -- 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 -- 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 -- 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! -- it's better if the user can choose whether to handle a message or not!
-- --
if lua == nil and type(messageHandler) == "function" then if lua == nil and type(messageHandler) == "function" then
return assert(pp.readFile(name)) return assert(pp.readFile(name))
end end
return lua return lua
end, end,
onBeforeMeta = messageHandler and function(lua) onBeforeMeta = messageHandler and function(lua)
sendMessage("beforemeta", pathIn, lua) sendMessage("beforemeta", pathIn, lua)
end, end,
onAfterMeta = messageHandler and function(lua) onAfterMeta = messageHandler
local luaModified = sendMessage("aftermeta", pathIn, lua) and function(lua)
local luaModified = sendMessage("aftermeta", pathIn, lua)
if type(luaModified) == "string" then if type(luaModified) == "string" then
lua = luaModified 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 return lua
error(F( end,
"%s: Message handler did not return a string for 'aftermeta'. (Got %s)",
messageHandlerPath, type(luaModified)
))
end
return lua onDone = messageHandler and function(info)
end, sendMessage("filedone", pathIn, pathOut, info)
end,
onDone = messageHandler and function(info) onError = function(err)
sendMessage("filedone", pathIn, pathOut, info) xpcall(function()
end, 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) byteCount = byteCount + info.processedByteCount
xpcall(function() lineCount = lineCount + info.lineCount
sendMessage("fileerror", pathIn, err) lineCountCode = lineCountCode + info.linesOfCode
end, function(err) tokenCount = tokenCount + info.tokenCount
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 if processingInfoPath ~= "" then
lineCount = lineCount + info.lineCount -- :SavedInfo
lineCountCode = lineCountCode + info.linesOfCode table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains.
tokenCount = tokenCount + info.tokenCount end
if processingInfoPath ~= "" then printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock() - startClockForPath)
printfNoise(("-"):rep(#header))
-- :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))
end end
-- Finalize stuff. -- Finalize stuff.
if processingInfoPath ~= "" then if processingInfoPath ~= "" then
printfNoise("Saving processing info to '%s'.", processingInfoPath) printfNoise("Saving processing info to '%s'.", processingInfoPath)
local luaParts = {"return"} local luaParts = { "return" }
assert(pp.serialize(luaParts, processingInfo)) assert(pp.serialize(luaParts, processingInfo))
local lua = table.concat(luaParts) local lua = table.concat(luaParts)
local file = assert(io.open(processingInfoPath, "wb")) local file = assert(io.open(processingInfoPath, "wb"))
file:write(lua) file:write(lua)
file:close() file:close()
end end
printfNoise( printfNoise(
"All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)", "All done! (%.3fs, %.0f file%s, %.0f LOC, %.0f line%s, %.0f token%s, %s)",
os.clock()-startClock, os.clock() - startClock,
#pathsIn, (#pathsIn == 1) and "" or "s", #pathsIn,
lineCountCode, (#pathsIn == 1) and "" or "s",
lineCount, (lineCount == 1) and "" or "s", lineCountCode,
tokenCount, (tokenCount == 1) and "" or "s", lineCount,
formatBytes(byteCount) (lineCount == 1) and "" or "s",
tokenCount,
(tokenCount == 1) and "" or "s",
formatBytes(byteCount)
) )
sendMessage("alldone") -- @Incomplete: Use pcall and format error message better? sendMessage("alldone") -- @Incomplete: Use pcall and format error message better?
--[[!=========================================================== --[[!===========================================================
Copyright © 2018-2022 Marcus 'ReFreezed' Thunström 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. 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 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. ---@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 --- @module tiny-ecs
-- @author Calvin Rose -- @author Calvin Rose
-- @license MIT -- @license MIT
@ -103,14 +102,13 @@ local filterJoin
-- A helper function to filters from string -- A helper function to filters from string
local filterBuildString local filterBuildString
local function filterJoinRaw(invert, joining_op, ...) local function filterJoinRaw(invert, joining_op, ...)
local _args = {...} local _args = { ... }
return function(system, e) return function(system, e)
local acc local acc
local args = _args local args = _args
if joining_op == 'or' then if joining_op == "or" then
acc = false acc = false
for i = 1, #args do for i = 1, #args do
local v = args[i] local v = args[i]
@ -119,7 +117,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then elseif type(v) == "function" then
acc = acc or v(system, e) acc = acc or v(system, e)
else 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 end
else else
@ -131,7 +129,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then elseif type(v) == "function" then
acc = acc and v(system, e) acc = acc and v(system, e)
else 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 end
end end
@ -146,69 +144,68 @@ local function filterJoinRaw(invert, joining_op, ...)
end end
do do
function filterJoin(...) function filterJoin(...)
local state, value = pcall(filterJoinRaw, ...) 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 end
local function buildPart(str) local function buildPart(str)
local accum = {} local accum = {}
local subParts = {} local subParts = {}
str = str:gsub('%b()', function(p) str = str:gsub("%b()", function(p)
subParts[#subParts + 1] = buildPart(p:sub(2, -2)) subParts[#subParts + 1] = buildPart(p:sub(2, -2))
return ('\255%d'):format(#subParts) return ("\255%d"):format(#subParts)
end) end)
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do for invert, part, sep in str:gmatch("(%!?)([^%|%&%!]+)([%|%&]?)") do
if part:match('^\255%d+$') then if part:match("^\255%d+$") then
local partIndex = tonumber(part:match(part:sub(2))) local partIndex = tonumber(part:match(part:sub(2)))
accum[#accum + 1] = ('%s(%s)') accum[#accum + 1] = ("%s(%s)"):format(invert == "" and "" or "not", subParts[partIndex])
:format(invert == '' and '' or 'not', subParts[partIndex])
else else
accum[#accum + 1] = ("(e[%s] %s nil)") accum[#accum + 1] = ("(e[%s] %s nil)"):format(make_safe(part), invert == "" and "~=" or "==")
:format(make_safe(part), invert == '' and '~=' or '==')
end end
if sep ~= '' then if sep ~= "" then
accum[#accum + 1] = (sep == '|' and ' or ' or ' and ') accum[#accum + 1] = (sep == "|" and " or " or " and ")
end end
end end
return table.concat(accum) return table.concat(accum)
end end
function filterBuildString(str) function filterBuildString(str)
local source = ("return function(_, e) return %s end") local source = ("return function(_, e) return %s end"):format(buildPart(str))
:format(buildPart(str))
local loader, err = loadstring(source) local loader, err = loadstring(source)
if err then if err then
error(err) error(err)
end end
return loader() return loader()
end end
end end
--- Makes a Filter that selects Entities with all specified Components and --- Makes a Filter that selects Entities with all specified Components and
-- Filters. -- Filters.
function tiny.requireAll(...) function tiny.requireAll(...)
return filterJoin(false, 'and', ...) return filterJoin(false, "and", ...)
end end
--- Makes a Filter that selects Entities with at least one of the specified --- Makes a Filter that selects Entities with at least one of the specified
-- Components and Filters. -- Components and Filters.
function tiny.requireAny(...) function tiny.requireAny(...)
return filterJoin(false, 'or', ...) return filterJoin(false, "or", ...)
end end
--- Makes a Filter that rejects Entities with all specified Components and --- Makes a Filter that rejects Entities with all specified Components and
-- Filters, and selects all other Entities. -- Filters, and selects all other Entities.
function tiny.rejectAll(...) function tiny.rejectAll(...)
return filterJoin(true, 'and', ...) return filterJoin(true, "and", ...)
end end
--- Makes a Filter that rejects Entities with at least one of the specified --- Makes a Filter that rejects Entities with at least one of the specified
-- Components and Filters, and selects all other Entities. -- Components and Filters, and selects all other Entities.
function tiny.rejectAny(...) function tiny.rejectAny(...)
return filterJoin(true, 'or', ...) return filterJoin(true, "or", ...)
end end
--- Makes a Filter from a string. Syntax of `pattern` is as follows. --- Makes a Filter from a string. Syntax of `pattern` is as follows.
@ -224,7 +221,11 @@ end
-- @param pattern -- @param pattern
function tiny.filter(pattern) function tiny.filter(pattern)
local state, value = pcall(filterBuildString, 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 end
--- System functions. --- System functions.
@ -463,8 +464,7 @@ function tiny.world(...)
entities = {}, entities = {},
-- List of Systems -- List of Systems
systems = {} systems = {},
}, worldMetaTable) }, worldMetaTable)
tiny_add(ret, ...) tiny_add(ret, ...)
@ -673,7 +673,6 @@ end
-- Adds, removes, and changes Entities that have been marked. -- Adds, removes, and changes Entities that have been marked.
function tiny_manageEntities(world) function tiny_manageEntities(world)
local e2r = world.entitiesToRemove local e2r = world.entitiesToRemove
local e2c = world.entitiesToChange local e2c = world.entitiesToChange
@ -793,7 +792,6 @@ end
-- Systems. If `filter` is not supplied, all Systems are updated. Put this -- Systems. If `filter` is not supplied, all Systems are updated. Put this
-- function in your main loop. -- function in your main loop.
function tiny.update(world, dt, filter) function tiny.update(world, dt, filter)
tiny_manageSystems(world) tiny_manageSystems(world)
tiny_manageEntities(world) tiny_manageEntities(world)
@ -809,8 +807,7 @@ function tiny.update(world, dt, filter)
onModify(system, dt) onModify(system, dt)
end end
local preWrap = system.preWrap local preWrap = system.preWrap
if preWrap and if preWrap and ((not filter) or filter(world, system)) then
((not filter) or filter(world, system)) then
preWrap(system, dt) preWrap(system, dt)
end end
end end
@ -853,12 +850,10 @@ function tiny.update(world, dt, filter)
for i = 1, #systems do for i = 1, #systems do
local system = systems[i] local system = systems[i]
local postWrap = system.postWrap local postWrap = system.postWrap
if postWrap and system.active and if postWrap and system.active and ((not filter) or filter(world, system)) then
((not filter) or filter(world, system)) then
postWrap(system, dt) postWrap(system, dt)
end end
end end
end end
--- Removes all Entities from the World. --- Removes all Entities from the World.
@ -924,11 +919,11 @@ worldMetaTable = {
clearSystems = tiny.clearSystems, clearSystems = tiny.clearSystems,
getEntityCount = tiny.getEntityCount, getEntityCount = tiny.getEntityCount,
getSystemCount = tiny.getSystemCount, getSystemCount = tiny.getSystemCount,
setSystemIndex = tiny.setSystemIndex setSystemIndex = tiny.setSystemIndex,
}, },
__tostring = function() __tostring = function()
return "<tiny-ecs_World>" return "<tiny-ecs_World>"
end end,
} }
_G.tiny = tiny _G.tiny = tiny

190
main.lua
View File

@ -9,25 +9,177 @@ require("generated/filter-types")
require("generated/assets") require("generated/assets")
require("generated/all-systems") require("generated/all-systems")
local scenarios = { local width, height = love.graphics.getWidth(), love.graphics.getHeight()
default = function()
-- TODO: Add default entities local squareSize = 80
end, local tileSize = math.floor(squareSize * 1.2)
textTestScenario = function() 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({ World:addEntity({
position = { x = 0, y = 600 }, size = cursorSize,
drawAsText = { moveWithCursor = T.marker,
text = "Hello, world!", canCollideWith = bit.bor(CursorMask, BallMask),
style = TextStyle.Inverted, 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, end,
} }
scenarios.textTestScenario() Scenarios.firstLevel()
function love.load() function love.load()
love.graphics.setBackgroundColor(1, 1, 1) love.graphics.setBackgroundColor(1, 1, 1)
@ -35,5 +187,15 @@ function love.load()
end end
function love.draw() 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 end

View File

@ -1,11 +1,24 @@
collidingEntities = filteredSystem("collidingEntitites", { collidingEntities = filteredSystem("collidingEntitites", {
velocity = T.XyPair,
position = T.XyPair, position = T.XyPair,
size = T.XyPair, size = T.XyPair,
canCollideWith = T.BitMask, canCollideWith = T.BitMask,
isSolid = Maybe(T.bool), 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( filteredSystem(
"collisionDetection", "collisionDetection",
{ position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) }, { position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) },
@ -18,19 +31,7 @@ filteredSystem(
and e.canBeCollidedBy and e.canBeCollidedBy
and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0 and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0
then then
local colliderTop = collider.position.y if intersects(e, collider) then
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
system.world:addEntity({ collisionBetween = { e, collider } }) system.world:addEntity({ collisionBetween = { e, collider } })
end end
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] 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) system.world:removeEntity(e)
end) end)

View File

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

View File

@ -1,23 +1,52 @@
local floor = math.floor
local gfx = love.graphics 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, _, _) filteredSystem("mapGridPositionToRealPosition", { gridPosition = T.XyPair }, function(e, _, system)
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) e.position = PositionAtGridXy(e.gridPosition.x, e.gridPosition.y)
system.world:addEntity(e)
end) end)
filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e) local spriteDrawSystem = drawSystem(
if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then "drawSprites",
return { 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 end
e.drawAsSprite:draw(e.position.x, e.position.y) )
end)
function spriteDrawSystem:preProcess()
gfx.setColor(1, 1, 1)
end
local margin = 8 local margin = 8
filteredSystem( drawSystem(
"drawText", "drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } }, { position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } },
function(e) 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 textHeight = font:getHeight()
local textWidth = font:getWidth(e.drawAsText.text) local textWidth = font:getWidth(e.drawAsText.text)
@ -40,3 +69,9 @@ filteredSystem(
gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin) gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin)
end 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 ---@type ButtonState
local buttonState = {} local buttonState = {}
@ -6,8 +8,105 @@ buttonInputSystem = filteredSystem("buttonInput", { canReceiveButtons = T.marker
system.world:addEntity(e) system.world:addEntity(e)
end) end)
HeldByCursor = filteredSystem("HeldByCursor", { pickedUpOnClick = T.marker, moveWithCursor = T.marker })
function buttonInputSystem:preProcess() function buttonInputSystem:preProcess()
if #self.entities == 0 then if #self.entities == 0 then
return return
end 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 local currentDrag = e.drag * dt
e.velocity.x = e.velocity.x - (e.velocity.x * currentDrag * dt) e.velocity.x = e.velocity.x - (e.velocity.x * currentDrag * dt)
e.velocity.y = e.velocity.y - (e.velocity.y * 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 return valType
end end
end end
end end

View File

@ -1,13 +1,15 @@
---@generic T ---@generic T
---@param shape T | fun() ---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil ---@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[] } ---@return System | { entities: T[] }
function filteredSystem(name, shape, process) function filteredSystem(name, shape, process, compare)
assert(type(name) == "string") assert(type(name) == "string")
assert(type(shape) == "table" or type(shape) == "function") assert(type(shape) == "table" or type(shape) == "function")
assert(process == nil or type(process) == "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 system.name = name
if type(shape) == "table" then if type(shape) == "table" then
local keys = {} local keys = {}