Get most of the way toward basic new-round behavior

Committing while in a mostly-working state so I can get some refactoring done.
This commit is contained in:
Sage Vaillancourt 2025-03-06 19:15:31 -05:00
parent 2e87bc8836
commit 590121f7a6
19 changed files with 208 additions and 65 deletions

View File

@ -782,10 +782,11 @@ function tiny.update(world, dt, filter)
for i = 1, #systems do for i = 1, #systems do
local system = systems[i] local system = systems[i]
if system.active and ((not filter) or filter(world, system)) then if system.active and ((not filter) or filter(world, system)) then
-- TODO: Track how long each system runs for during any given frame.
-- Update Systems that have an update method (most Systems) -- Update Systems that have an update method (most Systems)
local update = system.update local update = system.update
if update then if update then
--local currentMs = playdate.getCurrentTimeMilliseconds()
local interval = system.interval local interval = system.interval
if interval then if interval then
local bufferedTime = (system.bufferedTime or 0) + dt local bufferedTime = (system.bufferedTime or 0) + dt
@ -797,11 +798,14 @@ function tiny.update(world, dt, filter)
else else
update(system, dt) update(system, dt)
end end
--local endTimeMs = playdate.getCurrentTimeMilliseconds()
--print(tostring(endTimeMs - currentMs) .. "ms taken to update " .. system.name)
end end
system.modified = false system.modified = false
end end
end end
--print("")
-- Iterate through Systems IN ORDER AGAIN -- Iterate through Systems IN ORDER AGAIN
for i = 1, #systems do for i = 1, #systems do

View File

@ -3,13 +3,16 @@ Cart = {}
local sizeX, sizeY = CartSprite:getSize() local sizeX, sizeY = CartSprite:getSize()
local size = { x = sizeX, y = sizeY * 0.75 } local size = { x = sizeX, y = sizeY * 0.75 }
cartSystem = filteredSystem("cart", { isCart = T.marker })
function Cart.reset(o) function Cart.reset(o)
o.isCart = T.marker
o.position = { o.position = {
x = 20, x = 20,
y = 50, y = 50,
} }
o.velocity = { o.velocity = {
x = 200 + (100 * math.random()), x = 300 + (100 * math.random()),
y = 175 * (math.random() - 1), y = 175 * (math.random() - 1),
} }
o.size = size o.size = size
@ -33,9 +36,11 @@ function Cart.reset(o)
y = self.position.y, y = self.position.y,
}, },
}) })
-- Focus on the center, where the cart stopped
world:addEntity({ world:addEntity({
position = { x = self.position.x, y = self.position.y }, position = { x = self.position.x, y = self.position.y },
focusPriority = 1, focusPriority = 1,
removeAtRoundStart = true,
}) })
end end

View File

@ -1,4 +1,4 @@
effectSystem = filteredSystem({ canReceive }) effectSystem = filteredSystem("effects", { canReceive })
Effects = {} Effects = {}

View File

@ -4,7 +4,7 @@ Ingredients = {}
local ingredientCache = {} local ingredientCache = {}
local _ingredientCacheIndex = 1 local _ingredientCacheIndex = 1
local maxCache = 100 local maxCache = 80
for i = 1, maxCache do for i = 1, maxCache do
ingredientCache[i] = {} ingredientCache[i] = {}
end end
@ -19,6 +19,16 @@ function Ingredients.cacheSize()
return maxCache return maxCache
end end
function Ingredients.clearCache(world)
for _, o in ipairs(ingredientCache) do
for key in pairs(o) do
o[key] = nil
end
world:addEntity(o)
end
world:addEntity(Ingredients.getFirst())
end
function Ingredients.nextInCache() function Ingredients.nextInCache()
local index = _ingredientCacheIndex local index = _ingredientCacheIndex
_ingredientCacheIndex = _ingredientCacheIndex + 1 _ingredientCacheIndex = _ingredientCacheIndex + 1

View File

@ -18,8 +18,8 @@ import("systems/filter-types.lua")
import("systems/gravity.lua") import("systems/gravity.lua")
import("systems/move-toward.lua") import("systems/move-toward.lua")
import("systems/velocity.lua") import("systems/velocity.lua")
import("systems/rounds.lua")
import("systems/spawner.lua") import("systems/spawner.lua")
import("systems/rounds.lua")
import("systems/camera-pan.lua") import("systems/camera-pan.lua")
import("systems/collision-resolution.lua") import("systems/collision-resolution.lua")
import("systems/collision-detection.lua") import("systems/collision-detection.lua")
@ -74,12 +74,18 @@ world:addEntity(Ingredients.getFirst())
-- TODO: Re-enable when cart stops -- TODO: Re-enable when cart stops
playdate.setAutoLockDisabled(true) playdate.setAutoLockDisabled(true)
local startMsOffset = -playdate.getCurrentTimeMilliseconds()
function playdate.update() function playdate.update()
local deltaSeconds = playdate.getElapsedTime() local deltaSeconds = playdate.getElapsedTime()
playdate.resetElapsedTime() playdate.resetElapsedTime()
playdate.timer.updateTimers()
gfx.clear(gfx.kColorWhite) gfx.clear(gfx.kColorWhite)
playdate.drawFPS(5, 5) playdate.drawFPS(5, 5)
local fps = playdate.getFPS()
if fps > 0 and fps < 20 then
local currentTime = playdate.getCurrentTimeMilliseconds() + startMsOffset
print("At " .. (currentTime / 1000) .. "s, FPS below 20: " .. fps)
end
floor.position.x = Camera.pan.x - 600 floor.position.x = Camera.pan.x - 600
@ -87,9 +93,4 @@ function playdate.update()
gfx.setDrawOffset(0, 0) gfx.setDrawOffset(0, 0)
Score:draw() Score:draw()
if playdate.buttonJustPressed(playdate.kButtonA) then
Cart.reset(cart)
init()
end
end end

View File

@ -9,9 +9,9 @@ Camera = {
}, },
} }
expireBelowScreenSystem = filteredSystem({ position = T.XyPair, expireBelowScreenBy = T.number }) expireBelowScreenSystem = filteredSystem("expireBelowScreen", { position = T.XyPair, expireBelowScreenBy = T.number })
cameraPanSystem = filteredSystem({ focusPriority = T.number, position = T.XyPair }, function(e, dt) cameraPanSystem = filteredSystem("cameraPan", { focusPriority = T.number, position = T.XyPair }, function(e, dt)
if e.focusPriority >= focusPriority.priority then if e.focusPriority >= focusPriority.priority then
focusPriority.position = e.position focusPriority.position = e.position
end end

View File

@ -1,16 +1,17 @@
collidingEntities = filteredSystem({ 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),
}) })
collisionDetection = filteredSystem( collisionDetection = filteredSystem("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) },
-- 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) function(e, _, system)
for _, collider in pairs(collidingEntities.entities) do for _, collider in pairs(collidingEntities.entities) do
if (e ~= collider) and collider.canCollideWith and ((collider.canCollideWith & e.canBeCollidedBy) ~= 0) then if (e ~= collider) and collider.canCollideWith and e.canBeCollidedBy and ((collider.canCollideWith & e.canBeCollidedBy) ~= 0) then
local colliderTop = collider.position.y local colliderTop = collider.position.y
local colliderBottom = collider.position.y + collider.size.y local colliderBottom = collider.position.y + collider.size.y
local entityTop = e.position.y local entityTop = e.position.y
@ -33,3 +34,8 @@ collisionDetection = filteredSystem(
end end
end end
) )
function collisionDetection:preProcess()
-- print("collidingEntities count: " .. #collidingEntities.entities)
-- print("collidedEntities count: " .. #self.entities)
end

View File

@ -1,4 +1,4 @@
collisionResolution = filteredSystem({ collisionBetween = T.Collision }, function(e, dt, system) collisionResolution = filteredSystem("collisionResolution", { collisionBetween = T.Collision }, function(e, dt, system)
local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2] local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2]
local colliderTop = collidedInto.position.y local colliderTop = collidedInto.position.y
@ -22,6 +22,7 @@ collisionResolution = filteredSystem({ collisionBetween = T.Collision }, functio
if collider.focusOnCollide then if collider.focusOnCollide then
system.world:addEntity({ system.world:addEntity({
removeAtRoundStart = true,
focusPriority = collider.focusOnCollide, focusPriority = collider.focusOnCollide,
position = { position = {
x = collider.position.x, x = collider.position.x,

View File

@ -1,17 +1,17 @@
local gfx <const> = playdate.graphics local gfx <const> = playdate.graphics
drawRectanglesSystem = filteredSystem({ position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, dt) drawRectanglesSystem = filteredSystem("drawRectangles", { position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, dt)
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
end) end)
drawSpriteSystem = filteredSystem({ position = T.XyPair, drawAsSprite = T.PdImage }, function(e, dt, system) drawSpriteSystem = filteredSystem("drawSprites", { position = T.XyPair, drawAsSprite = T.PdImage }, function(e, dt, system)
e.drawAsSprite:draw(e.position.x, e.position.y) e.drawAsSprite:draw(e.position.x, e.position.y)
end) end)
local textHeight = AshevilleSans14Bold:getHeight() local textHeight = AshevilleSans14Bold:getHeight()
local xMargin = 4 local xMargin = 4
drawTextSystem = filteredSystem( drawTextSystem = filteredSystem("drawText",
{ position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str) } }, { position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str) } },
function(e, dt) function(e, dt)
local textWidth = AshevilleSans14Bold:getTextWidth(e.drawAsText.text) local textWidth = AshevilleSans14Bold:getTextWidth(e.drawAsText.text)

View File

@ -55,6 +55,8 @@ T = {
InRelations = {}, InRelations = {},
---@type InputState ---@type InputState
InputState = {}, InputState = {},
---@type Entity
Entity = {},
} }
---@generic T ---@generic T

View File

@ -1,4 +1,4 @@
local G = -300 local G = -300
fallSystem = filteredSystem({ velocity = T.XyPair, mass = T.number }, function(e, dt) fallSystem = filteredSystem("fall", { velocity = T.XyPair, mass = T.number }, function(e, dt)
e.velocity.y = e.velocity.y - (G * dt * e.mass) - (0.5 * dt * dt) e.velocity.y = e.velocity.y - (G * dt * e.mass) - (0.5 * dt * dt)
end) end)

View File

@ -3,7 +3,7 @@
local buttonJustPressed = playdate.buttonJustPressed local buttonJustPressed = playdate.buttonJustPressed
local inputState = {} local inputState = {}
inputSystem = filteredSystem({ canReceiveInput = T.marker }, function(e, _, system) inputSystem = filteredSystem("input", { canReceiveInput = T.marker }, function(e, _, system)
e.inputState = inputState e.inputState = inputState
system.world:addEntity(e) system.world:addEntity(e)
end) end)

View File

@ -1,8 +1,13 @@
menuController = filteredSystem({ menuItems = T.InRelations, inputState = T.InputState }, function(e, _, _) ---@alias MenuItem { onSelect: fun(), highlighted: boolean, navigateDown: MenuItem | nil, navigateUp: MenuItem | nil }
---@type MenuItem[]
local MenuItems = {}
menuController = filteredSystem("menuController", { menuItems = MenuItems, inputState = T.InputState }, function(e, _, system)
for _, menuItem in pairs(e.menuItems) do for _, menuItem in pairs(e.menuItems) do
if menuItem.highlighted then if menuItem.highlighted then
if e.inputState.aJustPressed then if e.inputState.aJustPressed then
menuItem.onSelect() menuItem.onSelect(system.world)
end end
if e.inputState.downJustPressed and menuItem.navigateDown then if e.inputState.downJustPressed and menuItem.navigateDown then
menuItem.highlighted = false menuItem.highlighted = false

View File

@ -15,7 +15,7 @@ local function normalizeVector(xy1, xy2)
return x / distance, y / distance, distance return x / distance, y / distance, distance
end end
moveTowardSystem = filteredSystem({ moveToward = MoveToward, position = T.XyPair }, function(e, dt, system) moveTowardSystem = filteredSystem("moveToward", { moveToward = MoveToward, position = T.XyPair }, function(e, dt, system)
local xNorm, yNorm, distance = normalizeVector(e.position, e.moveToward.target) local xNorm, yNorm, distance = normalizeVector(e.position, e.moveToward.target)
if distance > e.moveToward.range then if distance > e.moveToward.range then
return return

View File

@ -1,10 +1,66 @@
collectedEntities = filteredSystem({ collected = T.PdImage }) collectedEntities = filteredSystem("collectedEntities", { collected = T.PdImage })
local onCollidingRemove = { "mass", "velocity", "canCollideWith" } local onCollidingRemove = { "mass", "velocity", "canCollideWith" }
roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Maybe(T.XyPair) }, function(e, _, system) local Drop = { i = T.number, delay = T.number, startAt = T.XyPair }
if e.roundAction == "end" then
playdate.setAutoLockDisabled(false) disableCollisionWhenRoundEnds = filteredSystem("disableCollisionWhenRoundEnds", { disableCollisionWhenRoundEnds = T.marker })
collectableDropSystem = filteredSystem("collectableDrop", { drop = Drop }, function(e, dt, system)
e.drop.delay = e.drop.delay - dt
if e.drop.delay > 0 then
return
end
local collX, collY = e.drop.sprite:getSize()
system.world:addEntity({
drawAsSprite = e.drop.sprite,
size = { x = collX, y = collY / 2 },
mass = 0.5,
velocity = { x = 0, y = 0 },
position = { x = e.drop.startAt.x - (collX / 2), y = e.drop.startAt.y },
canCollideWith = 2,
canBeCollidedBy = 2,
isSolid = true,
stopMovingOnCollision = true,
onCollidingRemove = onCollidingRemove,
focusOnCollide = e.drop.i,
expireBelowScreenBy = 5,
removeAtRoundStart = true,
})
system.world:removeEntity(e)
end)
removeAtRoundStart = filteredSystem("removeAtRoundStart", { removeAtRoundStart = T.bool })
filteredSystem("afterDelayAdd", { afterDelayAdd = { entity = T.Entity, delay = T.number } }, function(e, dt, system)
e.afterDelayAdd.delay = e.afterDelayAdd.delay - dt
if e.afterDelayAdd.delay > 0 then
return
end
system.world:addEntity(e.afterDelayAdd.entity)
system.world:removeEntity(e)
end)
roundSystem = filteredSystem("round", { roundAction = T.RoundStateAction, position = Maybe(T.XyPair) }, function(e, _, system)
if e.roundAction == "start" then
for _, cart in pairs(cartSystem.entities) do
Cart.reset(cart)
system.world:addEntity(cart)
end
for _, remove in pairs(removeAtRoundStart.entities) do
system.world:removeEntity(remove)
end
system.world:addSystem(spawnerSystem)
Ingredients.clearCache(system.world)
elseif e.roundAction == "end" then
system.world:removeSystem(spawnerSystem)
for _, toExpire in pairs(disableCollisionWhenRoundEnds.entities) do
toExpire.canCollideWith = nil
toExpire.canBeCollidedBy = nil
--system.world:addEntity(toExpire)
system.world:removeEntity(toExpire)
end
-- playdate.setAutoLockDisabled(false)
local y = e.position.y - 240 local y = e.position.y - 240
local rectWidth = 150 local rectWidth = 150
@ -20,31 +76,27 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb
canBeCollidedBy = 2, canBeCollidedBy = 2,
isSolid = true, isSolid = true,
stopMovingOnCollision = true, stopMovingOnCollision = true,
removeAtRoundStart = true,
}) })
-- TODO: Big ol' numbers displaying how many ingredients were collected? -- TODO: Big ol' numbers displaying how many ingredients were collected?
-- TODO: Could layer ingredients in rows of three? Maybe just when it's higher? -- TODO: Could layer ingredients in rows of three? Maybe just when it's higher?
local delayPerDrop = 150 local delayPerDrop = 0.100
local delay = 0 local delay = 0
for i, collectable in ipairs(collectedEntities.entities) do for i, collectable in ipairs(collectedEntities.entities) do
local collX, collY = collectable.collected:getSize() local _, collY = collectable.collected:getSize()
y = y - collY - 15 y = y - collY - 15
playdate.timer.new(delay, function(ee, ccollX, ccollY, yy, ii, ssystem, ccollectable) system.world:addEntity({
ssystem.world:addEntity({ drop = {
drawAsSprite = ccollectable.collected, sprite = collectable.collected,
size = { x = ccollX, y = ccollY / 2 }, i = i,
mass = 0.5, delay = delay,
velocity = { x = 0, y = 0 }, startAt = {
position = { x = ee.position.x - (ccollX / 2), y = yy }, x = e.position.x,
canCollideWith = 2, y = y
canBeCollidedBy = 2, }
isSolid = true, },
stopMovingOnCollision = true, })
onCollidingRemove = onCollidingRemove,
focusOnCollide = ii,
expireBelowScreenBy = 5,
})
end, e, collX, collY, y, i, system, collectable)
delay = delay + delayPerDrop delay = delay + delayPerDrop
system.world:removeEntity(collectable) system.world:removeEntity(collectable)
end end
@ -61,9 +113,11 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb
canReceiveInput = T.marker, canReceiveInput = T.marker,
} }
local upgradeBelow local upgradeBelow
local i = #collectedEntities.entities
for _, upgrade in ipairs(availableUpgrades) do for _, upgrade in ipairs(availableUpgrades) do
i = i + 1
local collX, collY = 75, 21 local collX, collY = 75, 21
y = y - collY - 15 y = y - collY - 15 - 15
local upgradeEntity = { local upgradeEntity = {
onSelect = upgrade.apply, onSelect = upgrade.apply,
drawAsText = { drawAsText = {
@ -82,6 +136,7 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb
focusOnCollide = i, focusOnCollide = i,
navigateDown = upgradeBelow, navigateDown = upgradeBelow,
highlighted = true, highlighted = true,
removeAtRoundStart = true,
} }
if upgradeBelow then if upgradeBelow then
upgradeBelow.navigateUp = upgradeEntity upgradeBelow.navigateUp = upgradeEntity
@ -90,12 +145,15 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb
end end
upgradeBelow = upgradeEntity upgradeBelow = upgradeEntity
menuEntity.menuItems[#menuEntity.menuItems + 1] = upgradeEntity menuEntity.menuItems[#menuEntity.menuItems + 1] = upgradeEntity
playdate.timer.new(delay, function(_system, _upgradeEntity)
_system.world:addEntity(_upgradeEntity)
end, system, upgradeEntity)
delay = delay + delayPerDrop delay = delay + delayPerDrop
system.world:addEntity({
afterDelayAdd = {
delay = delay,
entity = upgradeEntity
},
})
system.world:addEntity(menuEntity) system.world:addEntity(menuEntity)
end end
system.world:removeEntity(e)
end end
system.world:removeEntity(e)
end) end)

View File

@ -3,7 +3,7 @@ local odds = 0
---@type { canSpawn: CanSpawn } ---@type { canSpawn: CanSpawn }
local selectedSpawner local selectedSpawner
spawnerSystem = filteredSystem({ canSpawn = T.CanSpawn, odds = T.number }, function(spawner, _, _) spawnerSystem = filteredSystem("spawner", { canSpawn = T.CanSpawn, odds = T.number }, function(spawner, _, _)
if odds <= 0 then if odds <= 0 then
return return
end end
@ -35,7 +35,7 @@ function spawnerSystem:preProcess()
return tiny.SKIP_PROCESS return tiny.SKIP_PROCESS
end end
local spawnEveryX = 26 local spawnEveryX = 30
-- Currently spawns AT MOST one new ingredient per frame, which is probably not enough at high speeds! -- Currently spawns AT MOST one new ingredient per frame, which is probably not enough at high speeds!
function spawnerSystem:postProcess() function spawnerSystem:postProcess()
@ -55,8 +55,6 @@ function spawnerSystem:postProcess()
self.world:addEntity(newlySpawned) self.world:addEntity(newlySpawned)
end end
local expireWhenOffScreenBy = { x = 2000, y = 480 }
---@param world World ---@param world World
function addAllSpawners(world) function addAllSpawners(world)
function addCollectableSpawner(name, spawnerOdds, score, sprite, canBounce, yRange) function addCollectableSpawner(name, spawnerOdds, score, sprite, canBounce, yRange)
@ -78,6 +76,7 @@ function addAllSpawners(world)
drawAsSprite = sprite, drawAsSprite = sprite,
collectable = sprite, collectable = sprite,
expireWhenOffScreenBy = expireWhenOffScreenBy, expireWhenOffScreenBy = expireWhenOffScreenBy,
disableCollisionWhenRoundEnds = T.marker,
canBounce = canBounce, canBounce = canBounce,
}, },
}, },
@ -85,7 +84,7 @@ function addAllSpawners(world)
end end
addCollectableSpawner("Lettuce", 0.7, 1, LettuceSprite, { addCollectableSpawner("Lettuce", 0.7, 1, LettuceSprite, {
flat = { x = 22, y = 190 }, flat = { x = 12, y = 220 },
mult = { x = 1, y = -0.5 }, mult = { x = 1, y = -0.5 },
}) })
@ -113,20 +112,25 @@ function getAvailableSpawnerUpgrades()
end end
if spawner.canSpawn.entity.score then if spawner.canSpawn.entity.score then
local name = "Double " .. spawner.name .. " value"
upgrades[#upgrades + 1] = { upgrades[#upgrades + 1] = {
name = "Double " .. spawner.name .. " value", name = name,
apply = function() apply = function(world)
print("Applying " .. name)
spawner.canSpawn.entity.score = spawner.canSpawn.entity.score * 2 spawner.canSpawn.entity.score = spawner.canSpawn.entity.score * 2
world:addEntity({ roundAction = "start" })
end, end,
} }
end end
assert(spawner.odds, "Expected all spawners to have an `odds` field!") assert(spawner.odds, "Expected all spawners to have an `odds` field!")
local name = "Double " .. spawner.name .. " frequency"
upgrades[#upgrades + 1] = { upgrades[#upgrades + 1] = {
name = "Double " .. spawner.name .. " frequency", name = name,
apply = function() apply = function(world)
print("Applying " .. name)
spawner.odds = spawner.odds * 2 spawner.odds = spawner.odds * 2
-- addEntity({ roundAction = "NEXT_ROUND" }) world:addEntity({ roundAction = "start" })
end, end,
} }

View File

@ -1,6 +1,6 @@
local sqrt = math.sqrt local sqrt = math.sqrt
velocitySystem = filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt, system) velocitySystem = filteredSystem("velocity", { position = T.XyPair, velocity = T.XyPair }, function(e, dt, system)
if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 2 then if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 2 then
e.velocity = nil e.velocity = nil
if e.spawnEntitiesWhenStopped then if e.spawnEntitiesWhenStopped then

View File

@ -13,8 +13,9 @@ local isSimulator = playdate.isSimulator
---@param shape T ---@param shape T
---@param process fun(entity: T, dt: number, system: System) ---@param process fun(entity: T, dt: number, system: System)
---@return System | { entities: T[] } ---@return System | { entities: T[] }
function filteredSystem(shape, process) function filteredSystem(name, shape, process)
local system = tiny.processingSystem() local system = tiny.processingSystem()
system.name = name
local keys = {} local keys = {}
for key, value in pairs(shape) do for key, value in pairs(shape) do
if type(value) ~= "table" or value.maybe == nil then if type(value) ~= "table" or value.maybe == nil then

View File

@ -4,7 +4,7 @@ Utils = {}
---@generic T ---@generic T
---@param fromArr T[] ---@param fromArr T[]
---@param n number ---@param n number
---@generic T[] ---@return T[]
function Utils.getNDifferentValues(fromArr, n) function Utils.getNDifferentValues(fromArr, n)
assert(n >= 0, "n must be a non-negative integer") assert(n >= 0, "n must be a non-negative integer")
if n > #fromArr then if n > #fromArr then
@ -26,3 +26,49 @@ function Utils.getNDifferentValues(fromArr, n)
end end
return randoms return randoms
end end
--- Track the number of instances of a given element, instead of needing multiple copies.
---@class CountSet
---@field private data table<table, number>
---@field private elementCount number
CountSet = {}
function CountSet.new()
return setmetatable({ data = {}, elementCount = 0 }, { __index = CountSet })
end
function CountSet:add(element)
local existing = self.data[element]
if existing then
self.data[element] = existing + 1
else
self.data[element] = 1
end
self.elementCount = self.elementCount + 1
end
function CountSet:balancedRandomPop()
if self.elementCount == 0 then
return
end
local toPop = math.random(self.elementCount)
for element, count in pairs(self.data) do
toPop = toPop - count
if toPop <= 0 then
local newCount = count - 1
if newCount == 0 then
self.data[element] = nil
else
self.data[element] = newCount
end
self.elementCount = self.elementCount - 1
return element
end
end
end
function CountSet:iterRandom()
return function()
return self:balancedRandomPop()
end
end