Drop available upgrades after ingredient stack.

***Temporarily*** use timers to defer ingredient spawn
This should be more component-y for sure.
This commit is contained in:
Sage Vaillancourt 2025-03-05 18:11:58 -05:00
parent f570a4a966
commit 94b391801d
12 changed files with 195 additions and 63 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 <const> = tiny
local gfx <const> = 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
)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -48,7 +48,7 @@ T = {
---@type RoundStateAction
RoundStateAction = "start",
---@type CanSpawn
CanSpawn = {}
CanSpawn = {},
}
---@generic T

View File

@ -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
)

View File

@ -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)
end)

View File

@ -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
end

28
src/utils.lua Normal file
View File

@ -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