From 0b79dc68fbd2ab15e6a2b3bb7cad05d332236e63 Mon Sep 17 00:00:00 2001
From: Sage Vaillancourt <sage@sagev.space>
Date: Tue, 4 Mar 2025 00:24:47 -0500
Subject: [PATCH] Much much much more generic collision detection.

Now uses a small collidingEntities system essentially as just a query for the collisionDetection system.
Makes a simple stack of collected ingredients at the end of a round.
Added some hacks to the velocity system that I don't like!
But it works somewhat splendidly!
---
 src/cart.lua                         | 50 +++++++++++++++++++++++++++-
 src/ingredients/cheese.lua           |  2 ++
 src/ingredients/ingredients.lua      |  2 +-
 src/ingredients/lettuce.lua          |  2 ++
 src/ingredients/mushroom.lua         |  2 ++
 src/ingredients/tomato.lua           |  2 ++
 src/main.lua                         | 36 ++++++++++----------
 src/systems/camera-pan.lua           | 20 +++++++++++
 src/systems/collision-detection.lua  | 47 +++++++++++++++-----------
 src/systems/collision-resolution.lua | 47 ++++++++++++++------------
 src/systems/draw.lua                 |  2 +-
 src/systems/filter-types.lua         |  4 +++
 src/systems/gravity.lua              |  4 +--
 src/systems/velocity.lua             | 13 ++++++--
 src/tiny-tools.lua                   |  5 ++-
 15 files changed, 171 insertions(+), 67 deletions(-)
 create mode 100644 src/systems/camera-pan.lua

diff --git a/src/cart.lua b/src/cart.lua
index 6dbc90a..c62be6f 100644
--- a/src/cart.lua
+++ b/src/cart.lua
@@ -1,3 +1,5 @@
+import("plate.lua")
+
 Cart = {}
 
 local sizeX, sizeY = CartSprite:getSize()
@@ -17,8 +19,54 @@ function Cart.reset(o)
         flat = { x = 0, y = 0 },
         mult = { x = 1, y = 1 },
     }
-    o.mass = T.marker
+    o.canCollideWith = 1
+    o.mass = 1
     o.drawAsSprite = CartSprite
+    o.focusPriority = 0
+
+    ---@type pd_image[]
+    o.collectables = {}
+
+    ---@param world World
+    function o:spawnEntitiesWhenStopped(world)
+        local y = self.position.y - 240
+        local rectWidth = 150
+        local plateSize = { x = rectWidth, y = 10 }
+        self.velocity = { x = 300, y = 0 }
+        world:addEntity(self)
+        world:addEntity({
+            position = { x = self.position.x, y = self.position.y },
+            focusPriority = 1,
+        })
+        world:addEntity({
+            drawAsRectangle = { size = plateSize },
+            size = plateSize,
+            mass = 0.5,
+            velocity = { x = 0, y = 0 },
+            position = { x = self.position.x - (rectWidth / 2), y = y },
+            canCollideWith = 2,
+            canBeCollidedBy = 2,
+            isSolid = true,
+            stopMovingOnCollision = true,
+        })
+
+        for _, collectable in ipairs(self.collectables) do
+            local collX, collY = collectable:getSize()
+            y = y - collY - 5
+            world:addEntity({
+                drawAsSprite = collectable,
+                size = { x = collX, y = collY / 2 },
+                mass = 0.5,
+                velocity = { x = 0, y = 0 },
+                position = { x = self.position.x - (collX / 2), y = y },
+                canCollideWith = 2,
+                canBeCollidedBy = 2,
+                isSolid = true,
+                stopMovingOnCollision = true,
+            })
+        end
+    end
+
     return o
 end
 
diff --git a/src/ingredients/cheese.lua b/src/ingredients/cheese.lua
index 17ed97d..370ba49 100644
--- a/src/ingredients/cheese.lua
+++ b/src/ingredients/cheese.lua
@@ -11,10 +11,12 @@ local canBounce = {
 
 function Cheese.initialize(o, x, y)
     o.score = 5
+    o.canBeCollidedBy = 1
     o.expireAfterCollision = true
     o.size = size
     o.position = { x = x, y = y }
     o.drawAsSprite = CheeseSprite
+    o.collectable = o.drawAsSprite
     o.expireWhenOffScreenBy = expireWhenOffScreenBy
     o.canBounce = canBounce
     newestBooster = o
diff --git a/src/ingredients/ingredients.lua b/src/ingredients/ingredients.lua
index aad9298..542f598 100644
--- a/src/ingredients/ingredients.lua
+++ b/src/ingredients/ingredients.lua
@@ -42,7 +42,7 @@ function Ingredients.getNext(x, y)
         return Lettuce.initialize(o, x, y)
     elseif odds > 0.2 then
         return Tomato.initialize(o, x, y)
-    elseif odds > 0.1 then
+    elseif odds > 0.05 then
         return Mushroom.initialize(o, x, 218 + math.random(1, 5))
     else
         return Cheese.initialize(o, x, y)
diff --git a/src/ingredients/lettuce.lua b/src/ingredients/lettuce.lua
index f28f284..521d2bf 100644
--- a/src/ingredients/lettuce.lua
+++ b/src/ingredients/lettuce.lua
@@ -11,10 +11,12 @@ local canBounce = {
 
 function Lettuce.initialize(o, x, y)
     o.score = 1
+    o.canBeCollidedBy = 1
     o.expireAfterCollision = true
     o.size = size
     o.position = { x = x, y = y }
     o.drawAsSprite = LettuceSprite
+    o.collectable = o.drawAsSprite
     o.expireWhenOffScreenBy = expireWhenOffScreenBy
     o.canBounce = canBounce
     newestBooster = o
diff --git a/src/ingredients/mushroom.lua b/src/ingredients/mushroom.lua
index c251e1f..e2b24cf 100644
--- a/src/ingredients/mushroom.lua
+++ b/src/ingredients/mushroom.lua
@@ -11,10 +11,12 @@ local canBounce = {
 
 function Mushroom.initialize(o, x, y)
     o.score = 5
+    o.canBeCollidedBy = 1
     o.expireAfterCollision = true
     o.size = size
     o.position = { x = x, y = y }
     o.drawAsSprite = MushroomSprite
+    o.collectable = o.drawAsSprite
     o.expireWhenOffScreenBy = expireWhenOffScreenBy
     o.canBounce = canBounce
     newestBooster = o
diff --git a/src/ingredients/tomato.lua b/src/ingredients/tomato.lua
index f4b65c9..209d31f 100644
--- a/src/ingredients/tomato.lua
+++ b/src/ingredients/tomato.lua
@@ -6,10 +6,12 @@ local expireWhenOffScreenBy = { x = 2000, y = 480 }
 
 function Tomato.initialize(o, x, y)
     o.score = 15
+    o.canBeCollidedBy = 1
     o.expireAfterCollision = true
     o.size = size
     o.position = { x = x, y = y }
     o.drawAsSprite = TomatoSprite
+    o.collectable = o.drawAsSprite
     o.expireWhenOffScreenBy = expireWhenOffScreenBy
     newestBooster = o
     return newestBooster
diff --git a/src/main.lua b/src/main.lua
index 32ff6a8..4db775e 100644
--- a/src/main.lua
+++ b/src/main.lua
@@ -1,18 +1,17 @@
--- stylua: ignore start
-import 'CoreLibs/animation.lua'
-import 'CoreLibs/animator.lua'
-import 'CoreLibs/easing.lua'
-import 'CoreLibs/graphics.lua'
-import 'CoreLibs/object.lua'
-import 'CoreLibs/timer.lua'
-import 'CoreLibs/ui.lua'
-import 'CoreLibs/utilities/where.lua'
--- stylua: ignore end
+import("CoreLibs/animation.lua")
+import("CoreLibs/animator.lua")
+import("CoreLibs/easing.lua")
+import("CoreLibs/graphics.lua")
+import("CoreLibs/object.lua")
+import("CoreLibs/timer.lua")
+import("CoreLibs/ui.lua")
+import("CoreLibs/utilities/where.lua")
 
 import("../lib/tiny.lua")
 import("tiny-tools.lua")
 import("assets.lua")
 import("systems/filter-types.lua")
+import("systems/camera-pan.lua")
 import("systems/collision-detection.lua")
 import("systems/collision-resolution.lua")
 import("systems/draw.lua")
@@ -31,6 +30,7 @@ local floorSize = { x = 10000, y = 10 }
 floor = {
     position = { x = 0, y = 235 },
     size = floorSize,
+    canBeCollidedBy = 1 | 2,
     canBounce = {
         flat = { x = 0, y = 0 },
         mult = { x = 0.9, y = -0.5 },
@@ -64,17 +64,17 @@ end
 world = tiny.world(
     fallSystem,
     velocitySystem,
+    collidingEntities,
     collisionResolution,
     collisionDetection,
-
+    cameraPanSystem,
     drawRectanglesSystem,
     drawSpriteSystem,
-
-    cart,
-    floor
+    floor,
+    cart
 )
 
-local ingredientsEveryX = 50
+local ingredientsEveryX = 90
 
 local function init()
     for i = 1, Ingredients.cacheSize() do
@@ -84,6 +84,9 @@ end
 
 init()
 
+-- TODO: Re-enable when cart stops
+playdate.setAutoLockDisabled(true)
+
 function playdate.update()
     local deltaSeconds = playdate.getElapsedTime()
     playdate.resetElapsedTime()
@@ -97,12 +100,11 @@ function playdate.update()
     local panX = Camera.pan.x
     local rightEdge = panX + 400
     local offset = 600
-    while newestX < (rightEdge + 100) do
+    while newestX < rightEdge + 100 do
         newestX = newestX + ingredientsEveryX
         world:addEntity(Ingredients.getNext(newestX, math.random(-500, 150)))
     end
     floor.position.x = Camera.pan.x - offset
-    gfx.setDrawOffset(-Camera.pan.x, -Camera.pan.y)
 
     world:update(deltaSeconds)
 
diff --git a/src/systems/camera-pan.lua b/src/systems/camera-pan.lua
new file mode 100644
index 0000000..4bfbb02
--- /dev/null
+++ b/src/systems/camera-pan.lua
@@ -0,0 +1,20 @@
+local gfx <const> = playdate.graphics
+
+local focusPriority = {}
+
+cameraPanSystem = filteredSystem({ focusPriority = T.number, position = T.XyPair }, function(e, dt)
+    if e.focusPriority >= focusPriority.priority then
+        focusPriority.position = e.position
+    end
+end)
+
+function cameraPanSystem:preProcess()
+    focusPriority.priority = 0
+    focusPriority.position = { x = 0, y = 0 }
+end
+
+function cameraPanSystem:postProcess()
+    local panX = math.max(0, focusPriority.position.x - 200)
+    local panY = math.min(0, focusPriority.position.y - 120)
+    gfx.setDrawOffset(-panX, -panY)
+end
diff --git a/src/systems/collision-detection.lua b/src/systems/collision-detection.lua
index 8e853da..acf1b42 100644
--- a/src/systems/collision-detection.lua
+++ b/src/systems/collision-detection.lua
@@ -1,27 +1,34 @@
+collidingEntities = filteredSystem({
+    position = T.XyPair,
+    size = T.XyPair,
+    canCollideWith = T.bitMask,
+    isSolid = Maybe(T.bool),
+})
+
 collisionDetection = filteredSystem(
-    { position = T.XyPair, size = T.XyPair, isSolid = Maybe(T.bool) },
-    -- Here, the entity, e, refers to some entity that the cart global(!) may be colliding with.
+    { 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)
-        local collider = cart
-        if not collider.velocity then
-            return
-        end
-        local colliderTop = collider.position.y
-        local colliderBottom = collider.position.y + collider.size.y
-        local entityTop = e.position.y
-        local entityBottom = entityTop + e.size.y
+        for _, collider in pairs(collidingEntities.entities) do
+            if (e ~= collider) and collider.canCollideWith 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
+                local entityBottom = entityTop + e.size.y
 
-        local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
-            or (entityBottom > colliderTop and entityBottom < colliderBottom)
+                local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
+                    or (entityBottom > colliderTop and entityBottom < colliderBottom)
 
-        if not withinY then
-            return
-        end
-
-        if collider.position.x < e.position.x + e.size.x and collider.position.x + collider.size.x > e.position.x then
-            system.world:addEntity({ collisionBetween = { e, collider } })
-            if e.expireAfterCollision then
-                system.world:removeEntity(e)
+                if
+                    withinY
+                    and collider.position.x < e.position.x + e.size.x
+                    and collider.position.x + collider.size.x > e.position.x
+                then
+                    system.world:addEntity({ collisionBetween = { e, collider } })
+                    if e.expireAfterCollision then
+                        system.world:removeEntity(e)
+                    end
+                end
             end
         end
     end
diff --git a/src/systems/collision-resolution.lua b/src/systems/collision-resolution.lua
index e41a550..2536654 100644
--- a/src/systems/collision-resolution.lua
+++ b/src/systems/collision-resolution.lua
@@ -1,37 +1,42 @@
 collisionResolution = filteredSystem({ collisionBetween = T.Collision }, function(e, _, system)
-    local collider, probablyCart = e.collisionBetween[1], e.collisionBetween[2]
-    local colliderTop = collider.position.y
+    local collidedInto, collider = e.collisionBetween[1], e.collisionBetween[2]
+    local colliderTop = collidedInto.position.y
 
-    if collider.isSolid then
+    if collidedInto.isSolid then
         -- Assumes impact from the top
-        probablyCart.position.y = colliderTop - probablyCart.size.y
+        collider.position.y = colliderTop - collider.size.y
     end
 
-    if collider.canBounce and probablyCart.canBeBounced then
-        probablyCart.velocity.x = probablyCart.velocity.x
-            + collider.canBounce.flat.x
-            + probablyCart.canBeBounced.flat.x
+    if collidedInto.canBounce and collider.canBeBounced then
+        collider.velocity.x = collider.velocity.x + collidedInto.canBounce.flat.x + collider.canBeBounced.flat.x
 
-        probablyCart.velocity.x = probablyCart.velocity.x
-            * collider.canBounce.mult.x
-            * probablyCart.canBeBounced.mult.x
+        collider.velocity.x = collider.velocity.x * collidedInto.canBounce.mult.x * collider.canBeBounced.mult.x
 
         -- abs() makes sure we always push upward
-        probablyCart.velocity.y = math.abs(probablyCart.velocity.y)
-            + collider.canBounce.flat.y
-            + probablyCart.canBeBounced.flat.y
+        collider.velocity.y = math.abs(collider.velocity.y)
+            + collidedInto.canBounce.flat.y
+            + collider.canBeBounced.flat.y
 
-        probablyCart.velocity.y = probablyCart.velocity.y
-            * collider.canBounce.mult.y
-            * probablyCart.canBeBounced.mult.y
+        collider.velocity.y = collider.velocity.y * collidedInto.canBounce.mult.y * collider.canBeBounced.mult.y
     end
 
-    if collider.score then
-        Score:add(collider.score)
+    if collider.stopMovingOnCollision then
+        print("stopMovingOnCollision!")
+        collider.velocity = nil
+        collider.canCollideWith = nil
+        system.world:addEntity(collider)
     end
 
-    if collider.expireAfterCollision then
-        system.world:removeEntity(collider)
+    if collidedInto.score then
+        Score:add(collidedInto.score)
+    end
+
+    if collidedInto.expireAfterCollision then
+        system.world:removeEntity(collidedInto)
+    end
+
+    if collidedInto.collectable and collider.collectables then
+        collider.collectables[#collider.collectables + 1] = collidedInto.collectable
     end
 
     system.world:removeEntity(e)
diff --git a/src/systems/draw.lua b/src/systems/draw.lua
index 229dc2d..30d8b03 100644
--- a/src/systems/draw.lua
+++ b/src/systems/draw.lua
@@ -6,4 +6,4 @@ end)
 
 drawSpriteSystem = filteredSystem({ position = T.XyPair, drawAsSprite = T.PdImage }, function(e, dt, system)
     e.drawAsSprite:draw(e.position.x, e.position.y)
-end)
\ No newline at end of file
+end)
diff --git a/src/systems/filter-types.lua b/src/systems/filter-types.lua
index f8c3297..14004ad 100644
--- a/src/systems/filter-types.lua
+++ b/src/systems/filter-types.lua
@@ -10,10 +10,14 @@ local Entity = {}
 ---@type XyPair
 local XyPair = { x = 1, y = 1 }
 
+---@alias BitMask number
+
 T = {
     XyPair = XyPair,
     bool = true,
     number = 0,
+    ---@type BitMask
+    bitMask = 0,
     numberArray = { 1, 2, 3 },
     str = "",
     marker = {},
diff --git a/src/systems/gravity.lua b/src/systems/gravity.lua
index 362ea30..fbde43f 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.marker }, function(e, dt)
-    e.velocity.y = e.velocity.y - (G * dt) - (0.5 * dt * dt)
+fallSystem = filteredSystem({ 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/velocity.lua b/src/systems/velocity.lua
index 4236caa..c55e9f4 100644
--- a/src/systems/velocity.lua
+++ b/src/systems/velocity.lua
@@ -1,9 +1,16 @@
 local sqrt = math.sqrt
 
-velocitySystem = filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt)
-    if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 0.1 then
+velocitySystem = filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt, system)
+    if not e.velocity then
+        return
+    end
+    if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 1 then
         e.velocity = nil
-        world:addEntity(e)
+        if e.spawnEntitiesWhenStopped then
+            e:spawnEntitiesWhenStopped(system.world)
+            e.spawnEntitiesWhenStopped = nil
+        end
+        system.world:addEntity(e)
     else
         e.position.x = e.position.x + (e.velocity.x * dt)
         e.position.y = e.position.y + (e.velocity.y * dt)
diff --git a/src/tiny-tools.lua b/src/tiny-tools.lua
index df08bf9..ebcd3c4 100644
--- a/src/tiny-tools.lua
+++ b/src/tiny-tools.lua
@@ -12,7 +12,7 @@ local isSimulator = playdate.isSimulator
 ---@generic T
 ---@param shape T
 ---@param process fun(entity: T, dt: number, system: System)
----@return
+---@return System | { entities: T[] }
 function filteredSystem(shape, process)
     local system = tiny.processingSystem()
     local keys = {}
@@ -23,6 +23,9 @@ function filteredSystem(shape, process)
         end
     end
     system.filter = tiny.requireAll(table.unpack(keys))
+    if not process then
+        return system
+    end
     if isSimulator then
         -- local acceptableKeys = ""
         -- for _, key in ipairs(keys) do