diff --git a/lib/tiny.lua b/lib/tiny.lua index 9abd47c..a8076ec 100644 --- a/lib/tiny.lua +++ b/lib/tiny.lua @@ -310,6 +310,7 @@ end -- Use an empty table as a key for identifying Systems. Any table that contains -- this key is considered a System rather than an Entity. local systemTableKey = { "SYSTEM_TABLE_KEY" } +tiny.SKIP_PROCESS = { "SKIP_PROCESS_KEY" } -- Checks if a table is a System. local function isSystem(table) @@ -322,12 +323,12 @@ local function processingSystemUpdate(system, dt) local process = system.process local postProcess = system.postProcess - local shouldSkipSystemProcess = false + local shouldSkipSystemProcess if preProcess then shouldSkipSystemProcess = preProcess(system, dt) end - if process and not shouldSkipSystemProcess then + if process and shouldSkipSystemProcess ~= tiny.SKIP_PROCESS then if system.nocache then local entities = system.world.entities local filter = system.filter @@ -347,7 +348,7 @@ local function processingSystemUpdate(system, dt) end end - if postProcess and not shouldSkipSystemProcess then + if postProcess and shouldSkipSystemProcess ~= tiny.SKIP_PROCESS then postProcess(system, dt) end end @@ -474,6 +475,8 @@ end --- Adds an Entity to the world. -- Also call this on Entities that have changed Components such that they -- match different Filters. Returns the Entity. +-- TODO: Track entity age when debugging? +-- TODO: Track debugName field when debugging? function tiny.addEntity(world, entity) local e2c = world.entitiesToChange e2c[#e2c + 1] = entity diff --git a/src/cart.lua b/src/cart.lua index 879cd23..056c441 100644 --- a/src/cart.lua +++ b/src/cart.lua @@ -9,8 +9,8 @@ function Cart.reset(o) y = 50, } o.velocity = { - x = 10 + (150 * math.random()), - y = 150 * (math.random() - 1), + x = 200 + (100 * math.random()), + y = 175 * (math.random() - 1), } o.size = size o.canBeBounced = { @@ -37,7 +37,6 @@ function Cart.reset(o) position = { x = self.position.x, y = self.position.y }, focusPriority = 1, }) - end return o diff --git a/src/effects.lua b/src/effects.lua index 74204be..8abcde3 100644 --- a/src/effects.lua +++ b/src/effects.lua @@ -13,16 +13,10 @@ function Effects.makeFloatier(entity) end --- Cause the given ingredient to spawn more frequently -function Effects.moreOfIngredient(ingredient, spawner) - -end +function Effects.moreOfIngredient(ingredient, spawner) end --- Each of the given ingredient will have a *mult score from now on. -function Effects.multiplyIngredientValue(ingredient, mult, spawner) - -end +function Effects.multiplyIngredientValue(ingredient, mult, spawner) end --- Each ingredient *multiplies* score in some way, in addition to adding. -function Effects.multiplicativeIngredientValue(ingredient, addedMult, spawner) - -end +function Effects.multiplicativeIngredientValue(ingredient, addedMult, spawner) end diff --git a/src/main.lua b/src/main.lua index ee8a8a4..02b6c1c 100644 --- a/src/main.lua +++ b/src/main.lua @@ -16,11 +16,13 @@ import("systems/collision-detection.lua") import("systems/collision-resolution.lua") import("systems/draw.lua") import("systems/gravity.lua") +import("systems/move-toward.lua") import("systems/rounds.lua") import("systems/spawner.lua") import("systems/velocity.lua") import("ingredients/ingredients.lua") import("cart.lua") +import("utils.lua") local tiny = tiny local gfx = playdate.graphics @@ -58,9 +60,11 @@ end world = tiny.world( fallSystem, + moveTowardSystem, velocitySystem, roundSystem, spawnerSystem, + expireBelowScreenSystem, collectedEntities, collidingEntities, collisionResolution, @@ -68,6 +72,7 @@ world = tiny.world( cameraPanSystem, drawRectanglesSystem, drawSpriteSystem, + drawTextSystem, floor, cart ) diff --git a/src/systems/camera-pan.lua b/src/systems/camera-pan.lua index 1bf2ec3..cb5e601 100644 --- a/src/systems/camera-pan.lua +++ b/src/systems/camera-pan.lua @@ -9,6 +9,8 @@ Camera = { }, } +expireBelowScreenSystem = filteredSystem({ position = T.XyPair, expireBelowScreenBy = T.number }) + cameraPanSystem = filteredSystem({ focusPriority = T.number, position = T.XyPair }, function(e, dt) if e.focusPriority >= focusPriority.priority then focusPriority.position = e.position @@ -24,4 +26,11 @@ function cameraPanSystem:postProcess() Camera.pan.x = math.max(0, focusPriority.position.x - 200) Camera.pan.y = math.min(0, focusPriority.position.y - 120) gfx.setDrawOffset(-Camera.pan.x, -Camera.pan.y) + + for _, entity in pairs(expireBelowScreenSystem.entities) do + if entity.position.y - (Camera.pan.y + 240) > entity.expireBelowScreenBy then + print("Entity expired - was too far below screen!") + self.world:removeEntity(entity) + end + end end diff --git a/src/systems/collision-resolution.lua b/src/systems/collision-resolution.lua index e4e75b1..34b256b 100644 --- a/src/systems/collision-resolution.lua +++ b/src/systems/collision-resolution.lua @@ -26,7 +26,7 @@ collisionResolution = filteredSystem({ collisionBetween = T.Collision }, functio position = { x = collider.position.x, y = collider.position.y, - } + }, }) end @@ -55,25 +55,5 @@ collisionResolution = filteredSystem({ collisionBetween = T.Collision }, functio system.world:addEntity({ collected = collidedInto.collectable }) end - -- if collidedInto.sparkOnCollision and collider.sparkOnCollision and (math.abs(collider.velocity.x) + math.abs(collider.velocity.y)) > 2 then - -- local size = { x = 1, y = 1 } - -- local spark = { - -- position = { - -- x = collider.position.x, - -- y = collider.position.y + collider.size.y - 2, - -- }, - -- velocity = { - -- x = -collider.velocity.x, - -- y = 150, - -- }, - -- size = size, - -- canCollideWith = 1, - -- mass = 1, - -- drawAsSprite = Spark, - -- expireAfterCollision = true, - -- } - -- system.world:addEntity(spark) - -- end - system.world:removeEntity(e) end) diff --git a/src/systems/draw.lua b/src/systems/draw.lua index 30d8b03..e683fdf 100644 --- a/src/systems/draw.lua +++ b/src/systems/draw.lua @@ -4,6 +4,10 @@ drawRectanglesSystem = filteredSystem({ position = T.XyPair, drawAsRectangle = { gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y) end) +drawTextSystem = filteredSystem({ position = T.XyPair, drawAsText = { text = T.str } }, function(e, dt) + gfx.drawTextAligned(e.drawAsText.text, e.position.x, e.position.y, gfx.kAlignCenter) +end) + drawSpriteSystem = filteredSystem({ position = T.XyPair, drawAsSprite = T.PdImage }, function(e, dt, system) e.drawAsSprite:draw(e.position.x, e.position.y) end) diff --git a/src/systems/filter-types.lua b/src/systems/filter-types.lua index 5a088ad..cadb85d 100644 --- a/src/systems/filter-types.lua +++ b/src/systems/filter-types.lua @@ -48,7 +48,7 @@ T = { ---@type RoundStateAction RoundStateAction = "start", ---@type CanSpawn - CanSpawn = {} + CanSpawn = {}, } ---@generic T diff --git a/src/systems/move-toward.lua b/src/systems/move-toward.lua new file mode 100644 index 0000000..bb0ece1 --- /dev/null +++ b/src/systems/move-toward.lua @@ -0,0 +1,41 @@ +local MoveToward = { + target = T.XyPair, + range = T.number, + speed = T.number, +} + +local sqrt, abs = math.sqrt, math.abs + +---@param xy1 XyPair +---@param xy2 XyPair +local function normalizeVector(xy1, xy2) + local x = xy1.x - xy2.x + local y = xy1.y - xy2.y + local distance = sqrt((x * x) + (y * y)) + return x / distance, y / distance, distance +end + +moveTowardSystem = filteredSystem( + { 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 + end + + -- TODO May be incorrect when signs are mismatched between vel and diff + local xVel = xNorm * e.moveToward.speed * dt + if abs(e.position.x - e.moveToward.target.x) < abs(xVel) then + e.position.x = e.moveToward.target.x + else + e.position.x = e.position.x + xVel + end + + local yVel = yNorm * e.moveToward.speed * dt + if abs(e.position.y - e.moveToward.target.y) < abs(yVel) then + e.position.y = e.moveToward.target.y + else + e.position.y = e.position.y + yVel + end + end +) diff --git a/src/systems/rounds.lua b/src/systems/rounds.lua index 4d31c88..344f1f3 100644 --- a/src/systems/rounds.lua +++ b/src/systems/rounds.lua @@ -1,8 +1,11 @@ collectedEntities = filteredSystem({ 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 y = e.position.y - 240 local rectWidth = 150 local plateSize = { x = rectWidth, y = 10 } @@ -19,25 +22,57 @@ roundSystem = filteredSystem({ roundAction = T.RoundStateAction, position = Mayb stopMovingOnCollision = true, }) + local delayPerDrop = 150 + local delay = 0 for i, collectable in ipairs(collectedEntities.entities) do local collX, collY = collectable.collected:getSize() - local onCollidingRemove = {"mass", "velocity", "canCollideWith"} y = y - collY - 15 - system.world:addEntity({ - drawAsSprite = collectable.collected, - size = { x = collX, y = collY / 2 }, - mass = 0.5, - velocity = { x = 0, y = 0 }, - position = { x = e.position.x - (collX / 2), y = y }, - canCollideWith = 2, - canBeCollidedBy = 2, - isSolid = true, - stopMovingOnCollision = true, - onCollidingRemove = onCollidingRemove, - focusOnCollide = i, - }) + 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) + delay = delay + delayPerDrop system.world:removeEntity(collectable) end + + local availableUpgrades = Utils.getNDifferentValues(getAvailableSpawnerUpgrades(), 3) + + y = y - 50 + for _, upgrade in ipairs(availableUpgrades) do + printTable(upgrade) + local collX, collY = 75, 30 + y = y - collY - 15 + playdate.timer.new(delay, function(ee, ccollX, ccollY, yy, ii, ssystem, ccollectable) + ssystem.world:addEntity({ + drawAsText = { + text = upgrade.name, + }, + 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, + }) + end, e, collX, collY, y, i, system, collectable) + delay = delay + delayPerDrop + end system.world:removeEntity(e) end -end) \ No newline at end of file +end) diff --git a/src/systems/spawner.lua b/src/systems/spawner.lua index 3fd94fa..3ff3efb 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, _, system) +spawnerSystem = filteredSystem({ canSpawn = T.CanSpawn, odds = T.number }, function(spawner, _, _) if odds <= 0 then return end @@ -15,22 +15,28 @@ spawnerSystem = filteredSystem({ canSpawn = T.CanSpawn, odds = T.number }, funct end end) ---- process() and postProcess() are skipped when preProcess() returns true +--- May cause process() and postProcess() to be skipped function spawnerSystem:preProcess() local newestX = Ingredients.newestIngredient().position.x local panX = Camera.pan.x local rightEdge = panX + 400 + local totalOdds = 0 + for _, spawner in pairs(spawnerSystem.entities) do + totalOdds = totalOdds + spawner.odds + end if newestX < rightEdge + 100 then - odds = math.random() + odds = math.random() * totalOdds selectedSpawner = nil - return false -- A new ingredient needs spawning! + return -- A new ingredient needs spawning! end -- We do not need a new ingredient at this time - return true + return tiny.SKIP_PROCESS end +local spawnEveryX = 26 + -- Currently spawns AT MOST one new ingredient per frame, which is probably not enough at high speeds! function spawnerSystem:postProcess() local newestX = Ingredients.newestIngredient().position.x @@ -44,7 +50,7 @@ function spawnerSystem:postProcess() -- TODO: May not need to include lower spawners when we reach higher altitudes. local panY = math.floor(Camera.pan.y or 0) local yRange = selectedSpawner.canSpawn.yRange or { top = panY - 240, bottom = panY + 220 } - newlySpawned.position = { x = newestX + 60, y = math.random(yRange.top, yRange.bottom) } + newlySpawned.position = { x = newestX + spawnEveryX, y = math.random(yRange.top, yRange.bottom) } self.world:addEntity(newlySpawned) end @@ -58,6 +64,8 @@ function addAllSpawners(world) local size = { x = sizeX, y = sizeY / 2 } world:addEntity({ + -- NOTE: This name should NOT be used to identify the spawner. + -- It should only be used to supply visual information when searching for available upgrades. name = name, odds = spawnerOdds, canSpawn = { @@ -83,7 +91,7 @@ function addAllSpawners(world) addCollectableSpawner("Tomato", 0.1, 15, TomatoSprite) - addCollectableSpawner("Mushroom", 0.7, 5, MushroomSprite, { + addCollectableSpawner("Mushroom", 0.02, 5, MushroomSprite, { flat = { x = 0, y = 290 }, mult = { x = 1, y = -1 }, }, { top = 219, bottom = 223 }) @@ -94,15 +102,41 @@ function addAllSpawners(world) }) end +---@alias Upgrade { name: string, apply = fun() } + +---@return Upgrade[] function getAvailableSpawnerUpgrades() local upgrades = {} - for _, spawner in pairs(spawnSystem.entities) do + for _, spawner in pairs(spawnerSystem.entities) do if spawner.hasUpgradeSpeed then upgrades[#upgrades + 1] = { hasUpgradeSpeed = spawner.hasUpgradeSpeed } end - if spawner.hasUpgradeValue then - upgrades[#upgrades + 1] = { hasUpgradeValue = spawner.hasUpgradeValue } + + if spawner.canSpawn.entity.score then + upgrades[#upgrades + 1] = { + name = "Double " .. spawner.name .. " value", + apply = function() + spawner.canSpawn.entity.score = spawner.canSpawn.entity.score * 2 + end + } end + + assert(spawner.odds, "Expected all spawners to have an `odds` field!") + upgrades[#upgrades + 1] = { + name = "Double " .. spawner.name .. " frequency", + apply = function() + spawner.odds = spawner.odds * 2 + end + } + + -- if not spawner.canSpawn.entity.velocity then + -- upgrades[#upgrades + 1] = { + -- name = spawner.name .. " Movement", + -- upgrade = function() + -- spawner.canSpawn.entity.velocity = { x = -10, y = 0 } + -- end, + -- } + -- end end return upgrades -end \ No newline at end of file +end diff --git a/src/utils.lua b/src/utils.lua new file mode 100644 index 0000000..189eaef --- /dev/null +++ b/src/utils.lua @@ -0,0 +1,28 @@ +Utils = {} + +--- Returns up to `n` random values from the given array. Will return fewer if `n > #fromArr` +---@generic T +---@param fromArr T[] +---@param n number +---@generic T[] +function Utils.getNDifferentValues(fromArr, n) + assert(n >= 0, "n must be a non-negative integer") + if n > #fromArr then + n = #fromArr + end + local found = 0 + local indexes = {} + while found < n do + local randomIndex = math.random(#fromArr) + if not indexes[randomIndex] then + found = found + 1 + indexes[randomIndex] = true + end + end + + local randoms = {} + for i in pairs(indexes) do + randoms[#randoms + 1] = fromArr[i] + end + return randoms +end \ No newline at end of file