Source: physics/PhysicsSystem.js

// src/engine/physics/PhysicsSystem.js
import System from '../ecs/System.js'
import { EPSILON } from './PhysicsConstants.js' // GRAVITY constants might not be used for basic overworld movement

/**
 * @file PhysicsSystem.js
 * @description Handles 2D planar movement, tile-based collision, and basic AABB entity collision
 * for an Octopath Traveler style overworld.
 * Assumes X = horizontal screen, Y = vertical screen. Z is for layering/depth, not primary collision.
 */

// --- Component Structure Definitions (JSDoc Typedefs) ---
/**
 * @typedef {object} PositionComponent
 * @property {number} x - World x-coordinate.
 * @property {number} y - World y-coordinate.
 * @property {number} [z=0] - World z-coordinate (for layering or simple height).
 */
/**
 * @typedef {object} VelocityComponent
 * @property {number} vx - Velocity on the x-axis (pixels/second).
 * @property {number} vy - Velocity on the y-axis.
 * @property {number} [vz=0] - Velocity on the z-axis (if used for depth changes).
 */
/**
 * @typedef {object} PhysicsBodyComponent
 * @property {'dynamic' | 'static'} entityType - 'dynamic' entities move, 'static' are immovable.
 * @property {boolean} [useGravity=false] - Typically false for Octopath-style overworld player/NPCs.
 * Could be true for specific physics objects if needed.
 * @property {number} [gravityScale=1.0]
 * @property {boolean} [isOnGround=true] - For this style, usually true unless specific state like "being knocked back".
 */
/**
 * @typedef {object} ColliderComponent
 * @property {'aabb'} shape
 * @property {number} width - Width of the AABB (along X).
 * @property {number} height - Height of the AABB (along Y).
 * @property {number} [depth=1] - Depth of the AABB (along Z, can be minimal for 2D plane collision).
 * @property {number} [offsetX=0]
 * @property {number} [offsetY=0]
 * @property {number} [offsetZ=0]
 * @property {boolean} [isTrigger=false]
 * @property {number} [collisionLayer=1]
 * @property {number} [collisionMask=1]
 * @property {boolean} [collidesWithTiles=true]
 */

class PhysicsSystem extends System {
  static requiredComponents = ['Position', 'Velocity', 'PhysicsBody', 'Collider']

  constructor() {
    super()
    // console.log("PhysicsSystem: Constructed for 2D Overworld style.");
  }

  initialize(engine) {
    super.initialize(engine)
    // console.log("PhysicsSystem: Initialized.");
  }

  _getAABB(position, collider) {
    // For 2D planar collision (XY), we primarily care about X and Y for AABB overlap.
    // Z can be used for layering or simple height checks if needed.
    // Assuming position x,y,z is the CENTER of the entity for now
    const halfWidth = collider.width / 2
    const halfHeight = collider.height / 2
    // const halfDepth = (collider.depth || 1) / 2; // Use a default depth if not specified

    const centerX = position.x + (collider.offsetX || 0)
    const centerY = position.y + (collider.offsetY || 0)
    // const centerZ = position.z + (collider.offsetZ || 0);

    return {
      minX: centerX - halfWidth,
      maxX: centerX + halfWidth,
      minY: centerY - halfHeight,
      maxY: centerY + halfHeight,
      // minZ: centerZ - halfDepth, maxZ: centerZ + halfDepth, // Keep Z for potential 3D AABB later
    }
  }

  _checkAABBOverlap(aabb1, aabb2) {
    // Simplified for 2D XY plane
    return (
      aabb1.minX < aabb2.maxX &&
      aabb1.maxX > aabb2.minX &&
      aabb1.minY < aabb2.maxY &&
      aabb1.maxY > aabb2.minY
      // && aabb1.minZ < aabb2.maxZ && aabb1.maxZ > aabb2.minZ // For 3D
    )
  }

  _checkEntityTileCollision(entityId, position, collider, scene) {
    const colX = position.x + (collider.offsetX || 0)
    const colY = position.y + (collider.offsetY || 0)
    // For Octopath-style, often a single point or a small set of points is sufficient
    // depending on how tight you want the tile collision. Using AABB corners for now.
    const points = [
      { x: colX - collider.width / 2 + EPSILON, y: colY - collider.height / 2 + EPSILON }, // Top-left
      { x: colX + collider.width / 2 - EPSILON, y: colY - collider.height / 2 + EPSILON }, // Top-right
      { x: colX - collider.width / 2 + EPSILON, y: colY + collider.height / 2 - EPSILON }, // Bottom-left
      { x: colX + collider.width / 2 - EPSILON, y: colY + collider.height / 2 - EPSILON }, // Bottom-right
    ]

    for (const point of points) {
      if (scene.isTileSolidAtWorldXY(point.x, point.y)) {
        return true
      }
    }
    return false
  }

  update(deltaTime, entities, engineRef) {
    if (!this.entityManager || !this.engine) return

    const activeScene = this.engine.sceneManager
      ? this.engine.sceneManager.getActiveSceneInstance()
      : null
    const canCheckTileCollisions =
      activeScene &&
      typeof activeScene.isTileSolidAtWorldXY === 'function' &&
      activeScene.tileWidth > 0 &&
      activeScene.tileHeight > 0

    for (const entityId of entities) {
      const position = this.entityManager.getComponent(entityId, 'Position')
      const velocity = this.entityManager.getComponent(entityId, 'Velocity')
      const physicsBody = this.entityManager.getComponent(entityId, 'PhysicsBody')
      const collider = this.entityManager.getComponent(entityId, 'Collider')

      // Acceleration component might not be used for simple top-down movement
      // let acceleration = this.entityManager.getComponent(entityId, 'Acceleration');
      // if (!acceleration) acceleration = { ax: 0, ay: 0, az: 0 };

      if (physicsBody.entityType === 'static') {
        velocity.vx = 0
        velocity.vy = 0
        velocity.vz = 0
        continue
      }

      // For Octopath-style, gravity is typically not applied to character movement on the map.
      // If physicsBody.useGravity were true, you'd apply it here.
      // velocity.vy += (physicsBody.useGravity ? GRAVITY_Y * (physicsBody.gravityScale || 1.0) : 0) * deltaTime;
      // velocity.vx += ... (similar for other gravity axes if any)

      // Store current position
      const prevX = position.x
      const prevY = position.y
      // const prevZ = position.z;

      // Update position based on velocity (this is the potential new position)
      position.x += velocity.vx * deltaTime
      position.y += velocity.vy * deltaTime
      // position.z += velocity.vz * deltaTime; // If Z movement is allowed

      // --- Collision Detection & Response ---
      let collisionOccurred = false

      // 1. Tile Collision (for X and Y plane)
      if (canCheckTileCollisions && collider.collidesWithTiles) {
        // Check collision at new proposed position (position.x, position.y)
        if (this._checkEntityTileCollision(entityId, position, collider, activeScene)) {
          // console.log(`[PhysicsSystem] Tile Collision for Entity ${entityId} at P(${position.x.toFixed(1)}, ${position.y.toFixed(1)})`);
          collisionOccurred = true
          // Simple resolution: revert to previous position for now.
          // More advanced: resolve per axis.
          position.x = prevX
          position.y = prevY
          // Optionally zero out velocity components if desired upon hitting a tile wall.
          // For Octopath, usually just stops. Velocity is set by input next frame.
          // velocity.vx = 0;
          // velocity.vy = 0;
        }
      }

      // 2. Entity-to-Entity Collision (AABB on XY plane)
      // We use the (potentially tile-collision-resolved) position for aabbA
      const aabbA = this._getAABB(position, collider)
      let entityIdA
      for (const entityIdB of entities) {
        // Could optimize by only checking against other relevant entities
        if (entityIdA === entityIdB) continue

        const positionB = this.entityManager.getComponent(entityIdB, 'Position')
        const colliderB = this.entityManager.getComponent(entityIdB, 'Collider')
        const physicsBodyB = this.entityManager.getComponent(entityIdB, 'PhysicsBody')

        if (!positionB || !colliderB || !physicsBodyB) continue

        const canACollideB = (collider.collisionLayer & colliderB.collisionMask) !== 0
        const canBCollideA = (colliderB.collisionLayer & collider.collisionMask) !== 0

        if (!(canACollideB && canBCollideA)) continue

        const aabbB = this._getAABB(positionB, colliderB)

        if (this._checkAABBOverlap(aabbA, aabbB)) {
          if (collider.isTrigger || colliderB.isTrigger) {
            if (this.engine.events) {
              this.engine.events.emit('triggerEnter', { entityA: entityIdA, entityB: entityIdB })
            }
          } else {
            // Solid collision
            collisionOccurred = true
            // console.log(`%c[PhysicsSystem] SOLID AABB Collision: ${entityIdA} vs ${entityIdB}`, "color: red");
            // Simple resolution: revert entityA to previous position.
            // This is a very basic response and can cause issues with multiple collisions.
            // A proper response would involve penetration depth and directional resolution.
            position.x = prevX
            position.y = prevY
            // Optionally modify velocities (e.g., stop)
            // velocity.vx = 0;
            // velocity.vy = 0;
            // If entityB is dynamic, it might also need to be moved/stopped.
            break // Stop checking other entities for entityA if a solid collision occurred
          }
        }
      } // End entity-entity collision loop

      // If a collision forced a position revert, re-evaluate velocity for next frame.
      // For simple stop, velocity would be re-evaluated by input next frame.
      // If after all checks, position is same as prevPosition, and velocity was non-zero, then zero it.
      if (position.x === prevX && velocity.vx !== 0) velocity.vx = 0
      if (position.y === prevY && velocity.vy !== 0) velocity.vy = 0
    } // End main entity loop
  }
}

export default PhysicsSystem