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( private val collisionResolver: CollisionResolver, private val subgridDimension: Vec2 = Vec2(50f, 50f) ) : PhysicsTick { override fun runTick(world: World) { 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, potentialCollisions: Collection) { 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, 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 { 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.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) } }