From 590121f7a65f68c2d2de8cfede7e3487fa5a0ed6 Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Thu, 6 Mar 2025 19:15:31 -0500 Subject: [PATCH] Get most of the way toward basic new-round behavior Committing while in a mostly-working state so I can get some refactoring done. --- lib/tiny.lua | 6 +- src/cart.lua | 7 +- src/effects.lua | 2 +- src/ingredients/ingredients.lua | 12 ++- src/main.lua | 15 ++-- src/systems/camera-pan.lua | 4 +- src/systems/collision-detection.lua | 12 ++- src/systems/collision-resolution.lua | 3 +- src/systems/draw.lua | 6 +- src/systems/filter-types.lua | 2 + src/systems/gravity.lua | 2 +- src/systems/input.lua | 2 +- src/systems/menu.lua | 9 ++- src/systems/move-toward.lua | 2 +- src/systems/rounds.lua | 112 ++++++++++++++++++++------- src/systems/spawner.lua | 24 +++--- src/systems/velocity.lua | 2 +- src/tiny-tools.lua | 3 +- src/utils.lua | 48 +++++++++++- 19 files changed, 208 insertions(+), 65 deletions(-) diff --git a/lib/tiny.lua b/lib/tiny.lua index a8076ec..ce305a6 100644 --- a/lib/tiny.lua +++ b/lib/tiny.lua @@ -782,10 +782,11 @@ function tiny.update(world, dt, filter) for i = 1, #systems do local system = systems[i] 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) local update = system.update if update then + --local currentMs = playdate.getCurrentTimeMilliseconds() local interval = system.interval if interval then local bufferedTime = (system.bufferedTime or 0) + dt @@ -797,11 +798,14 @@ function tiny.update(world, dt, filter) else update(system, dt) end + --local endTimeMs = playdate.getCurrentTimeMilliseconds() + --print(tostring(endTimeMs - currentMs) .. "ms taken to update " .. system.name) end system.modified = false end end + --print("") -- Iterate through Systems IN ORDER AGAIN for i = 1, #systems do diff --git a/src/cart.lua b/src/cart.lua index 056c441..9e53a6e 100644 --- a/src/cart.lua +++ b/src/cart.lua @@ -3,13 +3,16 @@ Cart = {} local sizeX, sizeY = CartSprite:getSize() local size = { x = sizeX, y = sizeY * 0.75 } +cartSystem = filteredSystem("cart", { isCart = T.marker }) + function Cart.reset(o) + o.isCart = T.marker o.position = { x = 20, y = 50, } o.velocity = { - x = 200 + (100 * math.random()), + x = 300 + (100 * math.random()), y = 175 * (math.random() - 1), } o.size = size @@ -33,9 +36,11 @@ function Cart.reset(o) y = self.position.y, }, }) + -- Focus on the center, where the cart stopped world:addEntity({ position = { x = self.position.x, y = self.position.y }, focusPriority = 1, + removeAtRoundStart = true, }) end diff --git a/src/effects.lua b/src/effects.lua index 8abcde3..448bfb2 100644 --- a/src/effects.lua +++ b/src/effects.lua @@ -1,4 +1,4 @@ -effectSystem = filteredSystem({ canReceive }) +effectSystem = filteredSystem("effects", { canReceive }) Effects = {} diff --git a/src/ingredients/ingredients.lua b/src/ingredients/ingredients.lua index c0f6476..d6301d0 100644 --- a/src/ingredients/ingredients.lua +++ b/src/ingredients/ingredients.lua @@ -4,7 +4,7 @@ Ingredients = {} local ingredientCache = {} local _ingredientCacheIndex = 1 -local maxCache = 100 +local maxCache = 80 for i = 1, maxCache do ingredientCache[i] = {} end @@ -19,6 +19,16 @@ function Ingredients.cacheSize() return maxCache 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() local index = _ingredientCacheIndex _ingredientCacheIndex = _ingredientCacheIndex + 1 diff --git a/src/main.lua b/src/main.lua index 3b31cf4..85a27dd 100644 --- a/src/main.lua +++ b/src/main.lua @@ -18,8 +18,8 @@ import("systems/filter-types.lua") import("systems/gravity.lua") import("systems/move-toward.lua") import("systems/velocity.lua") -import("systems/rounds.lua") import("systems/spawner.lua") +import("systems/rounds.lua") import("systems/camera-pan.lua") import("systems/collision-resolution.lua") import("systems/collision-detection.lua") @@ -74,12 +74,18 @@ world:addEntity(Ingredients.getFirst()) -- TODO: Re-enable when cart stops playdate.setAutoLockDisabled(true) +local startMsOffset = -playdate.getCurrentTimeMilliseconds() + function playdate.update() local deltaSeconds = playdate.getElapsedTime() playdate.resetElapsedTime() - playdate.timer.updateTimers() gfx.clear(gfx.kColorWhite) 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 @@ -87,9 +93,4 @@ function playdate.update() gfx.setDrawOffset(0, 0) Score:draw() - - if playdate.buttonJustPressed(playdate.kButtonA) then - Cart.reset(cart) - init() - end end diff --git a/src/systems/camera-pan.lua b/src/systems/camera-pan.lua index cb5e601..6f11c89 100644 --- a/src/systems/camera-pan.lua +++ b/src/systems/camera-pan.lua @@ -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 focusPriority.position = e.position end diff --git a/src/systems/collision-detection.lua b/src/systems/collision-detection.lua index acf1b42..8c499e6 100644 --- a/src/systems/collision-detection.lua +++ b/src/systems/collision-detection.lua @@ -1,16 +1,17 @@ -collidingEntities = filteredSystem({ +collidingEntities = filteredSystem("collidingEntitites", { + velocity = T.XyPair, position = T.XyPair, size = T.XyPair, canCollideWith = T.bitMask, isSolid = Maybe(T.bool), }) -collisionDetection = filteredSystem( +collisionDetection = 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* function(e, _, system) 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 colliderBottom = collider.position.y + collider.size.y local entityTop = e.position.y @@ -33,3 +34,8 @@ collisionDetection = filteredSystem( end end ) + +function collisionDetection:preProcess() + -- print("collidingEntities count: " .. #collidingEntities.entities) + -- print("collidedEntities count: " .. #self.entities) +end diff --git a/src/systems/collision-resolution.lua b/src/systems/collision-resolution.lua index 34b256b..02e94af 100644 --- a/src/systems/collision-resolution.lua +++ b/src/systems/collision-resolution.lua @@ -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 colliderTop = collidedInto.position.y @@ -22,6 +22,7 @@ collisionResolution = filteredSystem({ collisionBetween = T.Collision }, functio if collider.focusOnCollide then system.world:addEntity({ + removeAtRoundStart = true, focusPriority = collider.focusOnCollide, position = { x = collider.position.x, diff --git a/src/systems/draw.lua b/src/systems/draw.lua index 89c6020..8261be5 100644 --- a/src/systems/draw.lua +++ b/src/systems/draw.lua @@ -1,17 +1,17 @@ local gfx = 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) 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) end) local textHeight = AshevilleSans14Bold:getHeight() local xMargin = 4 -drawTextSystem = filteredSystem( +drawTextSystem = filteredSystem("drawText", { position = T.XyPair, drawAsText = { text = T.str, style = Maybe(T.str) } }, function(e, dt) local textWidth = AshevilleSans14Bold:getTextWidth(e.drawAsText.text) diff --git a/src/systems/filter-types.lua b/src/systems/filter-types.lua index 2667283..95b0869 100644 --- a/src/systems/filter-types.lua +++ b/src/systems/filter-types.lua @@ -55,6 +55,8 @@ T = { InRelations = {}, ---@type InputState InputState = {}, + ---@type Entity + Entity = {}, } ---@generic T diff --git a/src/systems/gravity.lua b/src/systems/gravity.lua index fbde43f..791989f 100644 --- a/src/systems/gravity.lua +++ b/src/systems/gravity.lua @@ -1,4 +1,4 @@ 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) end) diff --git a/src/systems/input.lua b/src/systems/input.lua index a226204..c68373b 100644 --- a/src/systems/input.lua +++ b/src/systems/input.lua @@ -3,7 +3,7 @@ local buttonJustPressed = playdate.buttonJustPressed local inputState = {} -inputSystem = filteredSystem({ canReceiveInput = T.marker }, function(e, _, system) +inputSystem = filteredSystem("input", { canReceiveInput = T.marker }, function(e, _, system) e.inputState = inputState system.world:addEntity(e) end) diff --git a/src/systems/menu.lua b/src/systems/menu.lua index d226fc1..f39228d 100644 --- a/src/systems/menu.lua +++ b/src/systems/menu.lua @@ -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 if menuItem.highlighted then if e.inputState.aJustPressed then - menuItem.onSelect() + menuItem.onSelect(system.world) end if e.inputState.downJustPressed and menuItem.navigateDown then menuItem.highlighted = false diff --git a/src/systems/move-toward.lua b/src/systems/move-toward.lua index 3777c09..6b58daf 100644 --- a/src/systems/move-toward.lua +++ b/src/systems/move-toward.lua @@ -15,7 +15,7 @@ local function normalizeVector(xy1, xy2) return x / distance, y / distance, distance 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) if distance > e.moveToward.range then return diff --git a/src/systems/rounds.lua b/src/systems/rounds.lua index 3a5ec37..b7c4137 100644 --- a/src/systems/rounds.lua +++ b/src/systems/rounds.lua @@ -1,10 +1,66 @@ -collectedEntities = filteredSystem({ collected = T.PdImage }) +collectedEntities = filteredSystem("collectedEntities", { collected = T.PdImage }) local onCollidingRemove = { "mass", "velocity", "canCollideWith" } -roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Maybe(T.XyPair) }, function(e, _, system) - if e.roundAction == "end" then - playdate.setAutoLockDisabled(false) +local Drop = { i = T.number, delay = T.number, startAt = T.XyPair } + +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 rectWidth = 150 @@ -20,31 +76,27 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb canBeCollidedBy = 2, isSolid = true, stopMovingOnCollision = true, + removeAtRoundStart = true, }) -- TODO: Big ol' numbers displaying how many ingredients were collected? -- TODO: Could layer ingredients in rows of three? Maybe just when it's higher? - local delayPerDrop = 150 + local delayPerDrop = 0.100 local delay = 0 for i, collectable in ipairs(collectedEntities.entities) do - local collX, collY = collectable.collected:getSize() + local _, collY = collectable.collected:getSize() y = y - collY - 15 - playdate.timer.new(delay, function(ee, ccollX, ccollY, yy, ii, ssystem, ccollectable) - ssystem.world:addEntity({ - drawAsSprite = ccollectable.collected, - size = { x = ccollX, y = ccollY / 2 }, - mass = 0.5, - velocity = { x = 0, y = 0 }, - position = { x = ee.position.x - (ccollX / 2), y = yy }, - canCollideWith = 2, - canBeCollidedBy = 2, - isSolid = true, - stopMovingOnCollision = true, - onCollidingRemove = onCollidingRemove, - focusOnCollide = ii, - expireBelowScreenBy = 5, - }) - end, e, collX, collY, y, i, system, collectable) + system.world:addEntity({ + drop = { + sprite = collectable.collected, + i = i, + delay = delay, + startAt = { + x = e.position.x, + y = y + } + }, + }) delay = delay + delayPerDrop system.world:removeEntity(collectable) end @@ -61,9 +113,11 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb canReceiveInput = T.marker, } local upgradeBelow + local i = #collectedEntities.entities for _, upgrade in ipairs(availableUpgrades) do + i = i + 1 local collX, collY = 75, 21 - y = y - collY - 15 + y = y - collY - 15 - 15 local upgradeEntity = { onSelect = upgrade.apply, drawAsText = { @@ -82,6 +136,7 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb focusOnCollide = i, navigateDown = upgradeBelow, highlighted = true, + removeAtRoundStart = true, } if upgradeBelow then upgradeBelow.navigateUp = upgradeEntity @@ -90,12 +145,15 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb end upgradeBelow = upgradeEntity menuEntity.menuItems[#menuEntity.menuItems + 1] = upgradeEntity - playdate.timer.new(delay, function(_system, _upgradeEntity) - _system.world:addEntity(_upgradeEntity) - end, system, upgradeEntity) delay = delay + delayPerDrop + system.world:addEntity({ + afterDelayAdd = { + delay = delay, + entity = upgradeEntity + }, + }) system.world:addEntity(menuEntity) end - system.world:removeEntity(e) end + system.world:removeEntity(e) end) diff --git a/src/systems/spawner.lua b/src/systems/spawner.lua index ed01416..7db32e3 100644 --- a/src/systems/spawner.lua +++ b/src/systems/spawner.lua @@ -3,7 +3,7 @@ local odds = 0 ---@type { canSpawn: CanSpawn } 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 return end @@ -35,7 +35,7 @@ function spawnerSystem:preProcess() return tiny.SKIP_PROCESS end -local spawnEveryX = 26 +local spawnEveryX = 30 -- Currently spawns AT MOST one new ingredient per frame, which is probably not enough at high speeds! function spawnerSystem:postProcess() @@ -55,8 +55,6 @@ function spawnerSystem:postProcess() self.world:addEntity(newlySpawned) end -local expireWhenOffScreenBy = { x = 2000, y = 480 } - ---@param world World function addAllSpawners(world) function addCollectableSpawner(name, spawnerOdds, score, sprite, canBounce, yRange) @@ -78,6 +76,7 @@ function addAllSpawners(world) drawAsSprite = sprite, collectable = sprite, expireWhenOffScreenBy = expireWhenOffScreenBy, + disableCollisionWhenRoundEnds = T.marker, canBounce = canBounce, }, }, @@ -85,7 +84,7 @@ function addAllSpawners(world) end addCollectableSpawner("Lettuce", 0.7, 1, LettuceSprite, { - flat = { x = 22, y = 190 }, + flat = { x = 12, y = 220 }, mult = { x = 1, y = -0.5 }, }) @@ -113,20 +112,25 @@ function getAvailableSpawnerUpgrades() end if spawner.canSpawn.entity.score then + local name = "Double " .. spawner.name .. " value" upgrades[#upgrades + 1] = { - name = "Double " .. spawner.name .. " value", - apply = function() + name = name, + apply = function(world) + print("Applying " .. name) spawner.canSpawn.entity.score = spawner.canSpawn.entity.score * 2 + world:addEntity({ roundAction = "start" }) end, } end assert(spawner.odds, "Expected all spawners to have an `odds` field!") + local name = "Double " .. spawner.name .. " frequency" upgrades[#upgrades + 1] = { - name = "Double " .. spawner.name .. " frequency", - apply = function() + name = name, + apply = function(world) + print("Applying " .. name) spawner.odds = spawner.odds * 2 - -- addEntity({ roundAction = "NEXT_ROUND" }) + world:addEntity({ roundAction = "start" }) end, } diff --git a/src/systems/velocity.lua b/src/systems/velocity.lua index 7de745d..f3e6be5 100644 --- a/src/systems/velocity.lua +++ b/src/systems/velocity.lua @@ -1,6 +1,6 @@ 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 e.velocity = nil if e.spawnEntitiesWhenStopped then diff --git a/src/tiny-tools.lua b/src/tiny-tools.lua index a7cf0ab..b3f6e68 100644 --- a/src/tiny-tools.lua +++ b/src/tiny-tools.lua @@ -13,8 +13,9 @@ local isSimulator = playdate.isSimulator ---@param shape T ---@param process fun(entity: T, dt: number, system: System) ---@return System | { entities: T[] } -function filteredSystem(shape, process) +function filteredSystem(name, shape, process) local system = tiny.processingSystem() + system.name = name local keys = {} for key, value in pairs(shape) do if type(value) ~= "table" or value.maybe == nil then diff --git a/src/utils.lua b/src/utils.lua index 6281b04..7f55fcf 100644 --- a/src/utils.lua +++ b/src/utils.lua @@ -4,7 +4,7 @@ Utils = {} ---@generic T ---@param fromArr T[] ---@param n number ----@generic T[] +---@return T[] function Utils.getNDifferentValues(fromArr, n) assert(n >= 0, "n must be a non-negative integer") if n > #fromArr then @@ -26,3 +26,49 @@ function Utils.getNDifferentValues(fromArr, n) end return randoms end + +--- Track the number of instances of a given element, instead of needing multiple copies. +---@class CountSet +---@field private data table +---@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