Patch up some template holes.

More consistent collision detection, LiveForNFrames system, drawSystem split, more correct LOVE types, inspect.lus library for table printing, and a few improved tiny types.
This commit is contained in:
Sage Vaillancourt 2025-03-19 14:07:19 -04:00
parent 3316f3fc3b
commit 7c69889098
10 changed files with 531 additions and 98 deletions

View File

@ -9,9 +9,8 @@ local SOME_TABLE = {}
---@alias BitMask number
---@alias ButtonState { receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean }
---@alias Collision { collisionBetween: Entity[] }
---@alias CrankState { crankChange: number, changeInLastHalfSecond: number }
---@alias Entity table
---@alias InRelations Entity[]
---@alias FontData love.FontData
---@alias XyPair { x: number, y: number }
T = {
@ -22,10 +21,6 @@ T = {
marker = SOME_TABLE,
---@type fun(self)
SelfFunction = function() end,
---@type pd_image
pd_image = SOME_TABLE,
---@type pd_font
pd_font = SOME_TABLE,
---@type AnyComponent
AnyComponent = SOME_TABLE,
@ -39,14 +34,11 @@ T = {
---@type Collision
Collision = SOME_TABLE,
---@type CrankState
CrankState = SOME_TABLE,
---@type Entity
Entity = SOME_TABLE,
---@type InRelations
InRelations = SOME_TABLE,
---@type FontData
FontData = SOME_TABLE,
---@type XyPair
XyPair = SOME_TABLE,

View File

@ -59,9 +59,8 @@ local SOME_TABLE = {}
XyPair = "{ x: number, y: number }",
Collision = "{ collisionBetween: Entity[] }",
BitMask = "number",
InRelations = "Entity[]",
FontData = "love.FontData",
ButtonState = "{ receivedInputThisFrame: boolean, aJustPressed: boolean, bJustPressed: boolean, upJustPressed: boolean, downJustPressed: boolean, leftJustPressed: boolean, rightJustPressed: boolean }",
CrankState = "{ crankChange: number, changeInLastHalfSecond: number }",
}))
T = {
bool = true,
@ -70,11 +69,7 @@ T = {
str = "",
marker = SOME_TABLE,
---@type fun(self)
SelfFunction = function() end,
---@type pd_image
pd_image = SOME_TABLE,
---@type pd_font
pd_font = SOME_TABLE,!!(dumpTypeObjects())
SelfFunction = function() end,!!(dumpTypeObjects())
}
---@generic T

367
lib/inspect.lua Normal file
View File

@ -0,0 +1,367 @@
local _tl_compat
if (tonumber((_VERSION or ""):match("[%d.]*$")) or 0) < 5.3 then
local p, m = pcall(require, "compat53.module")
if p then
_tl_compat = m
end
end
local math = _tl_compat and _tl_compat.math or math
local string = _tl_compat and _tl_compat.string or string
local table = _tl_compat and _tl_compat.table or table
local inspect = { Options = {} }
inspect._VERSION = "inspect.lua 3.1.0"
inspect._URL = "http://github.com/kikito/inspect.lua"
inspect._DESCRIPTION = "human-readable representations of tables"
inspect._LICENSE = [[
MIT LICENSE
Copyright (c) 2022 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
inspect.KEY = setmetatable({}, {
__tostring = function()
return "inspect.KEY"
end,
})
inspect.METATABLE = setmetatable({}, {
__tostring = function()
return "inspect.METATABLE"
end,
})
local tostring = tostring
local rep = string.rep
local match = string.match
local char = string.char
local gsub = string.gsub
local fmt = string.format
local _rawget
if rawget then
_rawget = rawget
else
_rawget = function(t, k)
return t[k]
end
end
local function rawpairs(t)
return next, t, nil
end
local function smartQuote(str)
if match(str, '"') and not match(str, "'") then
return "'" .. str .. "'"
end
return '"' .. gsub(str, '"', '\\"') .. '"'
end
local shortControlCharEscapes = {
["\a"] = "\\a",
["\b"] = "\\b",
["\f"] = "\\f",
["\n"] = "\\n",
["\r"] = "\\r",
["\t"] = "\\t",
["\v"] = "\\v",
["\127"] = "\\127",
}
local longControlCharEscapes = { ["\127"] = "\127" }
for i = 0, 31 do
local ch = char(i)
if not shortControlCharEscapes[ch] then
shortControlCharEscapes[ch] = "\\" .. i
longControlCharEscapes[ch] = fmt("\\%03d", i)
end
end
local function escape(str)
return (gsub(gsub(gsub(str, "\\", "\\\\"), "(%c)%f[0-9]", longControlCharEscapes), "%c", shortControlCharEscapes))
end
local luaKeywords = {
["and"] = true,
["break"] = true,
["do"] = true,
["else"] = true,
["elseif"] = true,
["end"] = true,
["false"] = true,
["for"] = true,
["function"] = true,
["goto"] = true,
["if"] = true,
["in"] = true,
["local"] = true,
["nil"] = true,
["not"] = true,
["or"] = true,
["repeat"] = true,
["return"] = true,
["then"] = true,
["true"] = true,
["until"] = true,
["while"] = true,
}
local function isIdentifier(str)
return type(str) == "string" and not not str:match("^[_%a][_%a%d]*$") and not luaKeywords[str]
end
local flr = math.floor
local function isSequenceKey(k, sequenceLength)
return type(k) == "number" and flr(k) == k and 1 <= k and k <= sequenceLength
end
local defaultTypeOrders = {
["number"] = 1,
["boolean"] = 2,
["string"] = 3,
["table"] = 4,
["function"] = 5,
["userdata"] = 6,
["thread"] = 7,
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
if ta == tb and (ta == "string" or ta == "number") then
return a < b
end
local dta = defaultTypeOrders[ta] or 100
local dtb = defaultTypeOrders[tb] or 100
return dta == dtb and ta < tb or dta < dtb
end
local function getKeys(t)
local seqLen = 1
while _rawget(t, seqLen) ~= nil do
seqLen = seqLen + 1
end
seqLen = seqLen - 1
local keys, keysLen = {}, 0
for k in rawpairs(t) do
if not isSequenceKey(k, seqLen) then
keysLen = keysLen + 1
keys[keysLen] = k
end
end
table.sort(keys, sortKeys)
return keys, keysLen, seqLen
end
local function countCycles(x, cycles)
if type(x) == "table" then
if cycles[x] then
cycles[x] = cycles[x] + 1
else
cycles[x] = 1
for k, v in rawpairs(x) do
countCycles(k, cycles)
countCycles(v, cycles)
end
countCycles(getmetatable(x), cycles)
end
end
end
local function makePath(path, a, b)
local newPath = {}
local len = #path
for i = 1, len do
newPath[i] = path[i]
end
newPath[len + 1] = a
newPath[len + 2] = b
return newPath
end
local function processRecursive(process, item, path, visited)
if item == nil then
return nil
end
if visited[item] then
return visited[item]
end
local processed = process(item, path)
if type(processed) == "table" then
local processedCopy = {}
visited[item] = processedCopy
local processedKey
for k, v in rawpairs(processed) do
processedKey = processRecursive(process, k, makePath(path, k, inspect.KEY), visited)
if processedKey ~= nil then
processedCopy[processedKey] = processRecursive(process, v, makePath(path, processedKey), visited)
end
end
local mt = processRecursive(process, getmetatable(processed), makePath(path, inspect.METATABLE), visited)
if type(mt) ~= "table" then
mt = nil
end
setmetatable(processedCopy, mt)
processed = processedCopy
end
return processed
end
local function puts(buf, str)
buf.n = buf.n + 1
buf[buf.n] = str
end
local Inspector = {}
local Inspector_mt = { __index = Inspector }
local function tabify(inspector)
puts(inspector.buf, inspector.newline .. rep(inspector.indent, inspector.level))
end
function Inspector:getId(v)
local id = self.ids[v]
local ids = self.ids
if not id then
local tv = type(v)
id = (ids[tv] or 0) + 1
ids[v], ids[tv] = id, id
end
return tostring(id)
end
function Inspector:putValue(v)
local buf = self.buf
local tv = type(v)
if tv == "string" then
puts(buf, smartQuote(escape(v)))
elseif tv == "number" or tv == "boolean" or tv == "nil" or tv == "cdata" or tv == "ctype" then
puts(buf, tostring(v))
elseif tv == "table" and not self.ids[v] then
local t = v
if t == inspect.KEY or t == inspect.METATABLE then
puts(buf, tostring(t))
elseif self.level >= self.depth then
puts(buf, "{...}")
else
if self.cycles[t] > 1 then
puts(buf, fmt("<%d>", self:getId(t)))
end
local keys, keysLen, seqLen = getKeys(t)
puts(buf, "{")
self.level = self.level + 1
for i = 1, seqLen + keysLen do
if i > 1 then
puts(buf, ",")
end
if i <= seqLen then
puts(buf, " ")
self:putValue(t[i])
else
local k = keys[i - seqLen]
tabify(self)
if isIdentifier(k) then
puts(buf, k)
else
puts(buf, "[")
self:putValue(k)
puts(buf, "]")
end
puts(buf, " = ")
self:putValue(t[k])
end
end
local mt = getmetatable(t)
if type(mt) == "table" then
if seqLen + keysLen > 0 then
puts(buf, ",")
end
tabify(self)
puts(buf, "<metatable> = ")
self:putValue(mt)
end
self.level = self.level - 1
if keysLen > 0 or type(mt) == "table" then
tabify(self)
elseif seqLen > 0 then
puts(buf, " ")
end
puts(buf, "}")
end
else
puts(buf, fmt("<%s %d>", tv, self:getId(v)))
end
end
function inspect.inspect(root, options)
options = options or {}
local depth = options.depth or math.huge
local newline = options.newline or "\n"
local indent = options.indent or " "
local process = options.process
if process then
root = processRecursive(process, root, {}, {})
end
local cycles = {}
countCycles(root, cycles)
local inspector = setmetatable({
buf = { n = 0 },
ids = {},
cycles = cycles,
depth = depth,
level = 0,
newline = newline,
indent = indent,
}, Inspector_mt)
inspector:putValue(root)
return table.concat(inspector.buf)
end
setmetatable(inspect, {
__call = function(_, root, options)
return inspect.inspect(root, options)
end,
})
_G.Inspect = inspect
return inspect

View File

@ -19,8 +19,6 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
---@class World
---@class System
---@field world World field points to the World that the System belongs to. Useful for adding and removing Entities from the world dynamically via the System.
---@field active boolean flag for whether or not the System is updated automatically. Inactive Systems should be updated manually or not at all via system:update(dt). Defaults to true.
@ -29,12 +27,14 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
---@field index number is the System's index in the World. Lower indexed Systems are processed before higher indices. The index is a read only field; to set the index, use tiny.setSystemIndex(world, system).
---@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 preProcess nil | fun(self, dt: number): nil | table
---@field postProcess nil | fun(self, dt: number)
---@field isDrawSystem nil | boolean
--- @module tiny-ecs
-- @author Calvin Rose
-- @license MIT
-- @copyright 2016
--- @author Calvin Rose
--- @license MIT
--- @copyright 2016
local tiny = {}
-- Local versions of standard lua functions
@ -103,14 +103,13 @@ local filterJoin
-- A helper function to filters from string
local filterBuildString
local function filterJoinRaw(invert, joining_op, ...)
local _args = {...}
local _args = { ... }
return function(system, e)
local acc
local args = _args
if joining_op == 'or' then
if joining_op == "or" then
acc = false
for i = 1, #args do
local v = args[i]
@ -119,7 +118,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then
acc = acc or v(system, e)
else
error 'Filter token must be a string or a filter function.'
error("Filter token must be a string or a filter function.")
end
end
else
@ -131,7 +130,7 @@ local function filterJoinRaw(invert, joining_op, ...)
elseif type(v) == "function" then
acc = acc and v(system, e)
else
error 'Filter token must be a string or a filter function.'
error("Filter token must be a string or a filter function.")
end
end
end
@ -146,69 +145,68 @@ local function filterJoinRaw(invert, joining_op, ...)
end
do
function filterJoin(...)
local state, value = pcall(filterJoinRaw, ...)
if state then return value else return nil, value end
if state then
return value
else
return nil, value
end
end
local function buildPart(str)
local accum = {}
local subParts = {}
str = str:gsub('%b()', function(p)
str = str:gsub("%b()", function(p)
subParts[#subParts + 1] = buildPart(p:sub(2, -2))
return ('\255%d'):format(#subParts)
return ("\255%d"):format(#subParts)
end)
for invert, part, sep in str:gmatch('(%!?)([^%|%&%!]+)([%|%&]?)') do
if part:match('^\255%d+$') then
for invert, part, sep in str:gmatch("(%!?)([^%|%&%!]+)([%|%&]?)") do
if part:match("^\255%d+$") then
local partIndex = tonumber(part:match(part:sub(2)))
accum[#accum + 1] = ('%s(%s)')
:format(invert == '' and '' or 'not', subParts[partIndex])
accum[#accum + 1] = ("%s(%s)"):format(invert == "" and "" or "not", subParts[partIndex])
else
accum[#accum + 1] = ("(e[%s] %s nil)")
:format(make_safe(part), invert == '' and '~=' or '==')
accum[#accum + 1] = ("(e[%s] %s nil)"):format(make_safe(part), invert == "" and "~=" or "==")
end
if sep ~= '' then
accum[#accum + 1] = (sep == '|' and ' or ' or ' and ')
if sep ~= "" then
accum[#accum + 1] = (sep == "|" and " or " or " and ")
end
end
return table.concat(accum)
end
function filterBuildString(str)
local source = ("return function(_, e) return %s end")
:format(buildPart(str))
local source = ("return function(_, e) return %s end"):format(buildPart(str))
local loader, err = loadstring(source)
if err then
error(err)
end
return loader()
end
end
--- Makes a Filter that selects Entities with all specified Components and
-- Filters.
function tiny.requireAll(...)
return filterJoin(false, 'and', ...)
return filterJoin(false, "and", ...)
end
--- Makes a Filter that selects Entities with at least one of the specified
-- Components and Filters.
function tiny.requireAny(...)
return filterJoin(false, 'or', ...)
return filterJoin(false, "or", ...)
end
--- Makes a Filter that rejects Entities with all specified Components and
-- Filters, and selects all other Entities.
function tiny.rejectAll(...)
return filterJoin(true, 'and', ...)
return filterJoin(true, "and", ...)
end
--- Makes a Filter that rejects Entities with at least one of the specified
-- Components and Filters, and selects all other Entities.
function tiny.rejectAny(...)
return filterJoin(true, 'or', ...)
return filterJoin(true, "or", ...)
end
--- Makes a Filter from a string. Syntax of `pattern` is as follows.
@ -224,7 +222,11 @@ end
-- @param pattern
function tiny.filter(pattern)
local state, value = pcall(filterBuildString, pattern)
if state then return value else return nil, value end
if state then
return value
else
return nil, value
end
end
--- System functions.
@ -463,8 +465,7 @@ function tiny.world(...)
entities = {},
-- List of Systems
systems = {}
systems = {},
}, worldMetaTable)
tiny_add(ret, ...)
@ -673,7 +674,6 @@ end
-- Adds, removes, and changes Entities that have been marked.
function tiny_manageEntities(world)
local e2r = world.entitiesToRemove
local e2c = world.entitiesToChange
@ -793,7 +793,6 @@ end
-- Systems. If `filter` is not supplied, all Systems are updated. Put this
-- function in your main loop.
function tiny.update(world, dt, filter)
tiny_manageSystems(world)
tiny_manageEntities(world)
@ -809,8 +808,7 @@ function tiny.update(world, dt, filter)
onModify(system, dt)
end
local preWrap = system.preWrap
if preWrap and
((not filter) or filter(world, system)) then
if preWrap and ((not filter) or filter(world, system)) then
preWrap(system, dt)
end
end
@ -853,12 +851,10 @@ function tiny.update(world, dt, filter)
for i = 1, #systems do
local system = systems[i]
local postWrap = system.postWrap
if postWrap and system.active and
((not filter) or filter(world, system)) then
if postWrap and system.active and ((not filter) or filter(world, system)) then
postWrap(system, dt)
end
end
end
--- Removes all Entities from the World.
@ -924,11 +920,11 @@ worldMetaTable = {
clearSystems = tiny.clearSystems,
getEntityCount = tiny.getEntityCount,
getSystemCount = tiny.getSystemCount,
setSystemIndex = tiny.setSystemIndex
setSystemIndex = tiny.setSystemIndex,
},
__tostring = function()
return "<tiny-ecs_World>"
end
end,
}
_G.tiny = tiny

View File

@ -27,13 +27,40 @@ local scenarios = {
end,
}
scenarios.textTestScenario()
local currentScenario = scenarios.textTestScenario
local freeze = false
local delta
function love.load()
currentScenario()
World:setSystemIndex(LiveForNFrames, 1)
love.graphics.setBackgroundColor(1, 1, 1)
love.graphics.setFont(EtBt7001Z0xa(32))
end
function love.draw()
World:update(love.timer.getDelta())
function love.update(dt)
delta = dt
if freeze then
return
end
if love.keyboard.isDown("r") then
World:clearEntities()
currentScenario()
freeze = false
end
if love.keyboard.isDown("f") then
freeze = not freeze
end
World:update(delta, function(_, system)
return not system.isDrawSystem
end)
end
function love.draw()
World:update(delta, function(_, system)
return system.isDrawSystem
end)
end

View File

@ -1,36 +1,37 @@
collidingEntities = filteredSystem("collidingEntitites", {
velocity = T.XyPair,
position = T.XyPair,
size = T.XyPair,
canCollideWith = T.BitMask,
isSolid = Maybe(T.bool),
})
local function intersects(rect, rectOther)
local left = rect.position.x
local right = rect.position.x + rect.size.x
local top = rect.position.y
local bottom = rect.position.y + rect.size.y
local leftOther = rectOther.position.x
local rightOther = rectOther.position.x + rectOther.size.x
local topOther = rectOther.position.y
local bottomOther = rectOther.position.y + rectOther.size.y
return leftOther < right and left < rightOther and topOther < bottom and top < bottomOther
end
filteredSystem(
"collisionDetection",
{ position = T.XyPair, size = T.XyPair, canBeCollidedBy = T.BitMask, isSolid = Maybe(T.bool) },
-- Here, the entity, e, refers to some entity that a moving object may be colliding *into*
-- Here, the entity, e, refers to some entity that a moving object may be colliding *into*
function(e, _, system)
for _, collider in pairs(collidingEntities.entities) do
if
(e ~= collider)
(e ~= collider)
and collider.canCollideWith
and e.canBeCollidedBy
and bit.band(collider.canCollideWith, e.canBeCollidedBy) ~= 0
then
local colliderTop = collider.position.y
local colliderBottom = collider.position.y + collider.size.y
local entityTop = e.position.y
local entityBottom = entityTop + e.size.y
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
or (entityBottom > colliderTop and entityBottom < colliderBottom)
if
withinY
and collider.position.x < e.position.x + e.size.x
and collider.position.x + collider.size.x > e.position.x
then
if intersects(e, collider) then
system.world:addEntity({ collisionBetween = { e, collider } })
end
end

View File

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

View File

@ -1,27 +1,49 @@
local gfx = love.graphics
filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
end)
---@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("drawSprites", { position = T.XyPair, drawAsSprite = T.pd_image }, function(e)
if e.position.y < Camera.pan.y - 240 or e.position.y > Camera.pan.y + 480 then
return
local spriteDrawSystem = drawSystem(
"drawSprites",
{ position = T.XyPair, drawAsSprite = T.Drawable, rotation = Maybe(T.number) },
function(e)
if not e.drawAsSprite then
return
end
gfx.draw(e.drawAsSprite, e.position.x, e.position.y)
end
e.drawAsSprite:draw(e.position.x, e.position.y)
end)
)
function spriteDrawSystem:preProcess()
gfx.setColor(1, 1, 1)
end
local margin = 8
filteredSystem(
drawSystem(
"drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.pd_font) } },
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str), font = Maybe(T.FontData) } },
function(e)
local font = gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local font = e.drawAsText.font or gfx.getFont() -- e.drawAsText.font or AshevilleSans14Bold
local textHeight = font:getHeight()
local textWidth = font:getWidth(e.drawAsText.text)
local bgLeftEdge = e.position.x - margin - textWidth / 2
local bgLeftEdge = e.position.x - margin -- - (textWidth / 2)
local bgTopEdge = e.position.y - 2
local bgWidth, bgHeight = textWidth + (margin * 2), textHeight + 2
@ -34,9 +56,15 @@ filteredSystem(
gfx.rectangle("fill", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
gfx.setColor(0, 0, 0)
gfx.drawRect("line", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
gfx.rectangle("line", bgLeftEdge, bgTopEdge, bgWidth, bgHeight)
end
gfx.print(e.drawAsText.text, bgLeftEdge + margin, bgTopEdge + margin)
end
)
drawSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, _, _)
gfx.setColor(1, 1, 1, 0.5)
local mode = e.drawAsRectangle.mode or "line" -- (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,13 +1,15 @@
---@generic T
---@param shape T | fun()
---@param process fun(entity: T, dt: number, system: System) | nil
---@param compare nil | fun(system: System, entityA: T, entityB: T): boolean
---@return System | { entities: T[] }
function filteredSystem(name, shape, process)
function filteredSystem(name, shape, process, compare)
assert(type(name) == "string")
assert(type(shape) == "table" or type(shape) == "function")
assert(process == nil or type(process) == "function")
local system = tiny.processingSystem()
local system = compare and tiny.sortedProcessingSystem() or tiny.processingSystem()
system.compare = compare
system.name = name
if type(shape) == "table" then
local keys = {}

View File

@ -1,12 +1,18 @@
---@meta
---@meta tiny-ecs
---@class World
World = {}
function World:add(...) end
---@generic T : Entity
---@param entity T
---@return T
function World:addEntity(entity) end
---@generic T : System
---@param system T
---@return T
function World:addSystem(system) end
function World:remove(...) end
@ -15,17 +21,28 @@ function World:removeEntity(entity) end
function World:removeSystem(system) end
--- Manages Entities and Systems marked for deletion or addition. Call this
--- before modifying Systems and Entities outside of a call to `tiny.update`.
--- Do not call this within a call to `tiny.update`.
function World:refresh() end
---@param dt number
function World:update(dt) end
---@param systemFilter nil | fun(world: World, system: System): boolean
function World:update(dt, systemFilter) end
--- Removes all Entities from the World.
function World:clearEntities() end
--- Removes all Systems from the World.
function World:clearSystems() end
--- Gets number of Entities in the World.
function World:getEntityCount() end
--- Gets number of Systems in World.
function World:getSystemCount() end
function World:setSystemIndex() end
--- Sets the index of a System in the World, and returns the old index. Changes
--- the order in which they Systems processed, because lower indexed Systems are
--- processed first. Returns the old system.index.
function World:setSystemIndex(world, system, index) end