/// PhysicsEngine - Sports-optimized physics simulation /// NO PLATFORM-SPECIFIC IMPORTS ALLOWED IN THIS MODULE import Foundation // MARK: - Physics Types /// A physics body in the simulation public struct PhysicsBody: Sendable { public var id: UUID public var position: SIMD3 public var velocity: SIMD3 public var acceleration: SIMD3 public var mass: Float public var restitution: Float // Bounciness (0-1) public var friction: Float public var isStatic: Bool public init(id: UUID = UUID(), position: SIMD3 = .zero, velocity: SIMD3 = .zero, mass: Float = 1.0, restitution: Float = 0.5, friction: Float = 0.5, isStatic: Bool = false) { self.id = id self.position = position self.velocity = velocity self.acceleration = .zero self.mass = mass self.restitution = restitution self.friction = friction self.isStatic = isStatic } } /// Collision shapes public enum CollisionShape: Sendable { case sphere(radius: Float) case box(halfExtents: SIMD3) case capsule(radius: Float, height: Float) case mesh(vertices: [SIMD3], indices: [UInt32]) } /// Collision data public struct Collision: Sendable { public var bodyA: UUID public var bodyB: UUID public var contactPoint: SIMD3 public var normal: SIMD3 public var penetrationDepth: Float public init(bodyA: UUID, bodyB: UUID, contactPoint: SIMD3, normal: SIMD3, penetrationDepth: Float) { self.bodyA = bodyA self.bodyB = bodyB self.contactPoint = contactPoint self.normal = normal self.penetrationDepth = penetrationDepth } } // MARK: - Physics World /// Main physics simulation world - optimized for sports game scenarios public actor PhysicsWorld { private var bodies: [UUID: PhysicsBody] = [:] private var shapes: [UUID: CollisionShape] = [:] private var gravity: SIMD3 = SIMD3(0, -9.81, 0) private var collisions: [Collision] = [] // Sports-specific optimizations private var ballBodies: Set = [] // Track ball/puck for special handling private var playerBodies: Set = [] // Track player bodies public init() {} // MARK: - Lifecycle public func initialize() { print(" ✓ Physics engine initialized (gravity: \(gravity))") } public func shutdown() { bodies.removeAll() shapes.removeAll() collisions.removeAll() } // MARK: - Body Management public func addBody(_ body: PhysicsBody, shape: CollisionShape, isBall: Bool = false, isPlayer: Bool = false) { bodies[body.id] = body shapes[body.id] = shape if isBall { ballBodies.insert(body.id) } if isPlayer { playerBodies.insert(body.id) } } public func removeBody(_ id: UUID) { bodies.removeValue(forKey: id) shapes.removeValue(forKey: id) ballBodies.remove(id) playerBodies.remove(id) } public func getBody(_ id: UUID) -> PhysicsBody? { return bodies[id] } public func updateBody(_ id: UUID, transform: (inout PhysicsBody) -> Void) { guard var body = bodies[id] else { return } transform(&body) bodies[id] = body } // MARK: - Simulation /// Step the physics simulation forward by deltaTime public func step(deltaTime: Float) { // Apply forces applyForces(deltaTime: deltaTime) // Integrate velocities integrateVelocities(deltaTime: deltaTime) // Detect collisions detectCollisions() // Resolve collisions resolveCollisions() // Integrate positions integratePositions(deltaTime: deltaTime) // Apply sports-specific constraints (e.g., keep ball in bounds) applySportsConstraints() } private func applyForces(deltaTime: Float) { for (id, var body) in bodies { guard !body.isStatic else { continue } // Apply gravity body.acceleration = gravity // Apply custom forces (wind, drag, etc.) // For sports games: apply spin to balls, player momentum, etc. if ballBodies.contains(id) { // Apply air drag to ball let dragCoefficient: Float = 0.1 let drag = -body.velocity * dragCoefficient body.acceleration += drag / body.mass } bodies[id] = body } } private func integrateVelocities(deltaTime: Float) { for (id, var body) in bodies { guard !body.isStatic else { continue } // Semi-implicit Euler integration body.velocity += body.acceleration * deltaTime bodies[id] = body } } private func integratePositions(deltaTime: Float) { for (id, var body) in bodies { guard !body.isStatic else { continue } body.position += body.velocity * deltaTime bodies[id] = body } } private func detectCollisions() { collisions.removeAll() let bodyArray = Array(bodies.values) // Broad phase - simple N^2 check (would use spatial partitioning in production) for i in 0.. Collision? { guard let shapeA = shapes[bodyA.id], let shapeB = shapes[bodyB.id] else { return nil } // Simplified collision detection (sphere-sphere for now) switch (shapeA, shapeB) { case (.sphere(let radiusA), .sphere(let radiusB)): return checkSphereSphere(bodyA: bodyA, radiusA: radiusA, bodyB: bodyB, radiusB: radiusB) default: return nil // Other collision shapes not implemented yet } } private func checkSphereSphere(bodyA: PhysicsBody, radiusA: Float, bodyB: PhysicsBody, radiusB: Float) -> Collision? { let delta = bodyB.position - bodyA.position let distanceSquared = simd_length_squared(delta) let radiusSum = radiusA + radiusB if distanceSquared < radiusSum * radiusSum { let distance = sqrt(distanceSquared) let normal = distance > 0.001 ? delta / distance : SIMD3(0, 1, 0) let penetration = radiusSum - distance let contactPoint = bodyA.position + normal * radiusA return Collision( bodyA: bodyA.id, bodyB: bodyB.id, contactPoint: contactPoint, normal: normal, penetrationDepth: penetration ) } return nil } private func resolveCollisions() { for collision in collisions { guard var bodyA = bodies[collision.bodyA], var bodyB = bodies[collision.bodyB] else { continue } // Position correction (separate bodies) let correctionPercent: Float = 0.8 let correction = collision.normal * collision.penetrationDepth * correctionPercent if !bodyA.isStatic && !bodyB.isStatic { let totalMass = bodyA.mass + bodyB.mass bodyA.position -= correction * (bodyB.mass / totalMass) bodyB.position += correction * (bodyA.mass / totalMass) } else if !bodyA.isStatic { bodyA.position -= correction } else if !bodyB.isStatic { bodyB.position += correction } // Velocity resolution (impulse-based) let relativeVelocity = bodyB.velocity - bodyA.velocity let velocityAlongNormal = simd_dot(relativeVelocity, collision.normal) // Don't resolve if bodies are separating if velocityAlongNormal > 0 { continue } // Calculate restitution (bounciness) let restitution = min(bodyA.restitution, bodyB.restitution) // Calculate impulse scalar let j = -(1 + restitution) * velocityAlongNormal let impulseScalar = bodyA.isStatic ? j / bodyB.mass : bodyB.isStatic ? j / bodyA.mass : j / (1/bodyA.mass + 1/bodyB.mass) // Apply impulse let impulse = collision.normal * impulseScalar if !bodyA.isStatic { bodyA.velocity -= impulse / bodyA.mass } if !bodyB.isStatic { bodyB.velocity += impulse / bodyB.mass } bodies[collision.bodyA] = bodyA bodies[collision.bodyB] = bodyB } } private func applySportsConstraints() { // Keep ball in play bounds, apply field friction, etc. // This would contain sports-specific rules } // MARK: - Public API public func setGravity(_ gravity: SIMD3) { self.gravity = gravity } public func getGravity() -> SIMD3 { return gravity } public func getAllBodies() -> [PhysicsBody] { return Array(bodies.values) } public func getCollisions() -> [Collision] { return collisions } }