summaryrefslogtreecommitdiff
path: root/composeApp/src/commonMain/kotlin/coffee/liz/ecs/physics/CollisionTick.kt
blob: d7d4ddaa690a68db7e50bfc2136892cb0ae6d83f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package coffee.liz.ecs.physics

import coffee.liz.ecs.Entity
import coffee.liz.ecs.Rect
import coffee.liz.ecs.Vec2
import coffee.liz.ecs.World
import kotlin.math.min

internal class CollisionTick<Outside>(
    private val collisionResolver: CollisionResolver<Outside>,
    private val subgridDimension: Vec2 = Vec2(50f, 50f)
) : PhysicsTick<Outside> {
    override fun runTick(world: World<Outside>) {
        val subgridEntities = world.collidingBounds().split(subgridDimension).map { subgrid ->
            subgrid.map { cell ->
                world.query(Colliding::class, Position::class).filter { entity ->
                    entity.positionalHitBoxes().any { cell.overlaps(it) }
                }
            }
        }

        for (y in subgridEntities.indices) {
            for (x in subgridEntities[y].indices) {
                resolveCollisions(world, subgridEntities[y][x])
            }
        }
    }

    private fun resolveCollisions(world: World<Outside>, potentialCollisions: Collection<Entity>) {
        potentialCollisions.forEach { from ->
            potentialCollisions.filter { to ->
                val fromColliding = from.get(Colliding::class)
                val toColliding = to.get(Colliding::class)

                val canCollide = fromColliding.group.collideInto.contains(toColliding.group.collisionGroup)
                if (!canCollide) {
                    return@filter false
                }

                val fromHitBoxes = from.positionalHitBoxes()
                val toHitBoxes = to.positionalHitBoxes()

                fromHitBoxes.any { a -> toHitBoxes.any { b -> a.overlaps(b) } }
            }.forEach { collision -> propagateCollision(world, from, collision)}
        }
    }

    private fun propagateCollision(world: World<Outside>, from: Entity, to: Entity) {
        collisionResolver.resolveCollision(world, from, to)

        val fromVelocity = from.takeIf { it.has(Velocity::class) }?.get(Velocity::class) ?: return

        val toVelocity = to.takeIf { it.has(Velocity::class) }?.get(Velocity::class)
            ?: Velocity(Vec2(0f, 0f))
        val toColliding = to.get(Colliding::class)

        if (toColliding.propagateMovement) {
            to.add(Velocity(toVelocity.velocity.plus(fromVelocity.velocity)))
        }
    }
}

private fun Entity.positionalHitBoxes(): Collection<Rect> {
    val pos = get(Position::class)
    val hitboxes = get(Colliding::class).hitboxes
    return hitboxes.map { Rect(pos.position.plus(it.topLeft), it.dimensions) }
}

fun World<*>.collidingBounds(): Rect {
    return query(Position::class, Colliding::class).map { entity ->
        val topLeft = entity.get(Position::class).position
        val dimensions = entity.get(Colliding::class).hitboxes.merge().dimensions
        Rect(topLeft, dimensions)
    }.merge()
}

fun Collection<Rect>.merge(): Rect {
    return reduce { a, b ->
        val newTopLeft = Vec2(
            min(a.topLeft.x, b.topLeft.x),
            min(a.topLeft.y, b.topLeft.y)
        )
        val dimensions = Vec2(
            maxOf(a.topLeft.x + a.dimensions.x, b.topLeft.x + b.dimensions.x) - newTopLeft.x,
            maxOf(a.topLeft.y + a.dimensions.y, b.topLeft.y + b.dimensions.y) - newTopLeft.y
        )
        Rect(newTopLeft, dimensions)
    }
}