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

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

@ -175,14 +175,14 @@ 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:
@ -217,7 +217,7 @@ local function formatBytes(n)
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
@ -246,9 +246,13 @@ local loadLuaFile = (
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
@ -270,59 +274,46 @@ 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
elseif not (processOptions and arg:find"^%-.") then
local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn local paths = (hasOutputPaths and #pathsOut < #pathsIn) and pathsOut or pathsIn
table.insert(paths, arg) table.insert(paths, arg)
if arg == "-" and (not hasOutputPaths or paths == pathsOut) then if arg == "-" and (not hasOutputPaths or paths == pathsOut) then
silent = true silent = true
end end
elseif arg == "--" then elseif arg == "--" then
processOptions = false processOptions = false
elseif arg:find("^%-%-data=") or arg:find("^%-d=") then
elseif arg:find"^%-%-data=" or arg:find"^%-d=" then
customData = arg:gsub("^.-=", "") customData = arg:gsub("^.-=", "")
elseif arg == "--backtickstrings" then elseif arg == "--backtickstrings" then
allowBacktickStrings = true allowBacktickStrings = true
elseif arg == "--debug" then elseif arg == "--debug" then
isDebug = true isDebug = true
outputMeta = outputMeta or true outputMeta = outputMeta or true
elseif arg:find("^%-%-handler=") or arg:find("^%-h=") then
elseif arg:find"^%-%-handler=" or arg:find"^%-h=" then
messageHandlerPath = arg:gsub("^.-=", "") messageHandlerPath = arg:gsub("^.-=", "")
elseif arg == "--jitsyntax" then elseif arg == "--jitsyntax" then
allowJitSyntax = true allowJitSyntax = true
elseif arg == "--linenumbers" then elseif arg == "--linenumbers" then
addLineNumbers = true addLineNumbers = true
elseif arg == "--meta" then elseif arg == "--meta" then
outputMeta = true outputMeta = true
elseif arg:find"^%-%-meta=" then elseif arg:find("^%-%-meta=") then
outputMeta = arg:gsub("^.-=", "") outputMeta = arg:gsub("^.-=", "")
elseif arg == "--nonil" then elseif arg == "--nonil" then
canOutputNil = false canOutputNil = false
elseif arg == "--novalidate" then elseif arg == "--novalidate" then
validate = false validate = false
elseif arg:find("^%-%-outputextension=") then
elseif arg:find"^%-%-outputextension=" then
if hasOutputPaths then if hasOutputPaths then
errorLine("Cannot specify both --outputextension and --outputpaths") errorLine("Cannot specify both --outputextension and --outputpaths")
end end
hasOutputExtension = true hasOutputExtension = true
outputExtension = arg:gsub("^.-=", "") outputExtension = arg:gsub("^.-=", "")
elseif arg == "--outputpaths" or arg == "-o" then elseif arg == "--outputpaths" or arg == "-o" then
if hasOutputExtension then if hasOutputExtension then
errorLine("Cannot specify both --outputpaths and --outputextension") errorLine("Cannot specify both --outputpaths and --outputextension")
@ -330,38 +321,27 @@ for _, arg in ipairs(args) do
errorLine(arg .. " must appear before any input path.") errorLine(arg .. " must appear before any input path.")
end end
hasOutputPaths = true hasOutputPaths = true
elseif arg:find("^%-%-saveinfo=") or arg:find("^%-i=") then
elseif arg:find"^%-%-saveinfo=" or arg:find"^%-i=" then
processingInfoPath = arg:gsub("^.-=", "") processingInfoPath = arg:gsub("^.-=", "")
elseif arg == "--silent" then elseif arg == "--silent" then
silent = true silent = true
elseif arg == "--faststrings" then elseif arg == "--faststrings" then
fastStrings = true fastStrings = true
elseif arg == "--nogc" then elseif arg == "--nogc" then
collectgarbage("stop") collectgarbage("stop")
elseif arg:find("^%-%-macroprefix=") then
elseif arg:find"^%-%-macroprefix=" then
macroPrefix = arg:gsub("^.-=", "") macroPrefix = arg:gsub("^.-=", "")
elseif arg:find("^%-%-macrosuffix=") then
elseif arg:find"^%-%-macrosuffix=" then
macroSuffix = arg:gsub("^.-=", "") macroSuffix = arg:gsub("^.-=", "")
elseif arg == "--release" then elseif arg == "--release" then
releaseMode = true releaseMode = true
elseif arg:find("^%-%-loglevel=") then
elseif arg:find"^%-%-loglevel=" then
maxLogLevel = arg:gsub("^.-=", "") maxLogLevel = arg:gsub("^.-=", "")
elseif arg == "--version" then elseif arg == "--version" then
io.stdout:write(pp.VERSION) io.stdout:write(pp.VERSION)
os.exit() os.exit()
elseif arg == "--nostrictmacroarguments" then elseif arg == "--nostrictmacroarguments" then
strictMacroArguments = false strictMacroArguments = false
else else
errorLine("Unknown option '" .. arg:gsub("=.*", "") .. "'.") errorLine("Unknown option '" .. arg:gsub("=.*", "") .. "'.")
end end
@ -380,26 +360,19 @@ 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 elseif type(messageHandler) == "table" then
return messageHandler[message] ~= nil return messageHandler[message] ~= nil
else else
assert(false) assert(false)
end end
@ -408,18 +381,17 @@ end
local function sendMessage(message, ...) local function sendMessage(message, ...)
if not messageHandler then if not messageHandler then
return return
elseif type(messageHandler) == "function" then elseif type(messageHandler) == "function" then
local returnValues = pp.pack(messageHandler(message, ...)) local returnValues = pp.pack(messageHandler(message, ...))
return pp.unpack(returnValues, 1, returnValues.n) return pp.unpack(returnValues, 1, returnValues.n)
elseif type(messageHandler) == "table" then elseif type(messageHandler) == "table" then
local _messageHandler = messageHandler[message] local _messageHandler = messageHandler[message]
if not _messageHandler then return end if not _messageHandler then
return
end
local returnValues = pp.pack(_messageHandler(...)) local returnValues = pp.pack(_messageHandler(...))
return pp.unpack(returnValues, 1, returnValues.n) return pp.unpack(returnValues, 1, returnValues.n)
else else
assert(false) assert(false)
end end
@ -450,8 +422,6 @@ if messageHandlerPath ~= "" then
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?
@ -471,17 +441,23 @@ 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.
@ -507,7 +483,7 @@ for i, pathIn in ipairs(pathsIn) do
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,
@ -549,17 +525,20 @@ for i, pathIn in ipairs(pathsIn) do
sendMessage("beforemeta", pathIn, lua) sendMessage("beforemeta", pathIn, lua)
end, end,
onAfterMeta = messageHandler and function(lua) onAfterMeta = messageHandler
and function(lua)
local luaModified = sendMessage("aftermeta", pathIn, 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 elseif luaModified ~= nil then
error(F( error(
F(
"%s: Message handler did not return a string for 'aftermeta'. (Got %s)", "%s: Message handler did not return a string for 'aftermeta'. (Got %s)",
messageHandlerPath, type(luaModified) messageHandlerPath,
)) type(luaModified)
)
)
end end
return lua return lua
@ -577,7 +556,7 @@ for i, pathIn in ipairs(pathsIn) do
end) end)
os.exit(1) os.exit(1)
end, end,
} })
assert(info, err) -- The onError() handler above should have been called and we should have exited already. assert(info, err) -- The onError() handler above should have been called and we should have exited already.
byteCount = byteCount + info.processedByteCount byteCount = byteCount + info.processedByteCount
@ -586,18 +565,14 @@ for i, pathIn in ipairs(pathsIn) do
tokenCount = tokenCount + info.tokenCount tokenCount = tokenCount + info.tokenCount
if processingInfoPath ~= "" then if processingInfoPath ~= "" then
-- :SavedInfo -- :SavedInfo
table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains. table.insert(processingInfo.files, info) -- See 'ProcessInfo' in preprocess.lua for what more 'info' contains.
end end
printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock() - startClockForPath) printfNoise("Processing '%s' successful! (%.3fs)", pathIn, os.clock() - startClockForPath)
printfNoise(("-"):rep(#header)) 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)
@ -614,17 +589,18 @@ 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,
(#pathsIn == 1) and "" or "s",
lineCountCode, lineCountCode,
lineCount, (lineCount == 1) and "" or "s", lineCount,
tokenCount, (tokenCount == 1) and "" or "s", (lineCount == 1) and "" or "s",
tokenCount,
(tokenCount == 1) and "" or "s",
formatBytes(byteCount) 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

@ -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",
{ position = T.XyPair, drawAsSprite = T.Drawable, rotation = Maybe(T.number) },
function(e)
if not e.drawAsSprite then
return return
end end
e.drawAsSprite:draw(e.position.x, e.position.y) local width, height = e.drawAsSprite:getDimensions()
end) gfx.draw(e.drawAsSprite, 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

@ -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 = {}