# Physics & Actors

## Physics lifecycle callbacks

```lua
function onCollide(actorA, actorB, normalX, normalY)
  -- Called once when two solid actors BEGIN touching
  -- normalX, normalY point from actorB toward actorA
end

function onSeparate(actorA, actorB)
  -- Called once when two solid actors STOP touching
end
```

---

## `castle.createActor(table)`

Creates a physics actor and returns an actor table.

```lua
local ball = castle.createActor({
  body = { x = 0, y = -3, width = 0.5, height = 0.5 },
  tags = { "ball" },
  solid = {},
  moving = {},
  falling = {},
})
```

**Body fields** (`body = { ... }`):
- `x`, `y` — spawn position (default 0)
- `width`, `height` — size in world units (default 1)
- `angle` — rotation in radians (default 0)
- `fixtures` — optional array of collision shapes (see below)

**Collision shapes (`fixtures` array):**

```lua
-- Circle fixture
castle.createActor({
  body = { x=0, y=0, fixtures = {
    { shapeType="circle", radius=0.5 }
  }},
  solid = {},
})

-- Custom convex polygon (flat list of x,y pairs, up to 8 vertices)
castle.createActor({
  body = { x=0, y=0, fixtures = {
    { shapeType="polygon", points={-1,-1, 1,-1, 1,1, -1,1} }
  }},
  solid = {},
})
```

**Physics keys:**

| Key | Effect | Sub-properties |
|-----|--------|----------------|
| `solid = {}` | Collides with other solid actors | _(none)_ |
| `moving = {}` | Dynamic rigid body (affected by forces) | `vx`, `vy`, `density` |
| `falling = {}` | Gravity pulls the actor down | `gravity` (default 1.0) |
| `bouncy = {}` | Bounces on collision | `bounciness` (0–1) |
| `friction = {}` | Surface friction | `friction` (0–1) |

**Static vs dynamic:**
- `solid = {}` alone → **static body** (immovable — use for walls and floors)
- `solid = {}, moving = {}, falling = {}` → **dynamic body** (falls, collides)

---

## `castle.destroyActor(actorOrId)`

Removes an actor from the scene.

---

## Actor queries

```lua
castle.numActorsWithTag("ball")          -- count
castle.actorsWithTag("ball")             -- array of actor tables
castle.closestActorWithTag("ball")       -- nearest actor or nil
```

---

## Actor methods

```lua
-- Lifecycle
actor:destroy()
actor:isAlive()

-- Position and rotation
actor:setPosition(x, y)
actor:getX()                           -- nil if destroyed
actor:getY()
actor:getRotation()                    -- degrees
actor:setRotation(degrees)

-- Velocity (requires moving = {})
actor:getVelocity()                    -- returns vx, vy
actor:setVelocity(vx, vy)             -- nil component = preserve existing

-- Tags
actor:hasTag("tag")
actor:addTag("tag")
actor:removeTag("tag")
actor:getTags()

-- Collision (current-frame check)
actor:isColliding()
actor:isColliding("tag")
actor:getCollidingActors()
actor:getCollidingActors("tag")

-- Camera and z-order
actor:followWithCamera()
actor:moveToFront()
actor:moveToBack()

-- Geometry helpers
actor:distanceTo(other)               -- Euclidean distance
actor:angleTo(other)                  -- angle in degrees toward other
actor:speed()                         -- velocity magnitude
actor:angleOfMotion()                 -- direction of velocity in degrees
```

---

## Rendering actors

Actors have no built-in visual appearance. Read their positions in `onDraw()` and draw shapes there.

```lua
function onDraw()
  castle.draw.setColor(1, 0.3, 0.1)
  for _, ball in ipairs(castle.actorsWithTag("ball")) do
    local x, y = ball:getX(), ball:getY()
    if x then  -- nil if actor was just destroyed
      castle.draw.circle("fill", x, y, 0.25)
    end
  end
end
```

**Drawing a rotated actor:**
```lua
local x, y = actor:getX(), actor:getY()
castle.draw.push()
  castle.draw.translate(x, y)
  castle.draw.rotate(math.rad(actor:getRotation()))
  castle.draw.rectangle("fill", -0.5, -0.25, 1, 0.5)
castle.draw.pop()
```

---

## Physics queries (`castle.physics.*`)

### `castle.physics.raycast(x1, y1, x2, y2 [, tagFilter])`
Returns the first physics body hit along the ray, or `nil`.

```lua
local hit = castle.physics.raycast(0, 0, 3, 3)
if hit then
  -- hit.actor, hit.x, hit.y, hit.normalX, hit.normalY, hit.fraction
end
```

### `castle.physics.raycastAll(x1, y1, x2, y2 [, tagFilter])`
Returns all hits along the ray (sorted by fraction).

### `castle.physics.queryPoint(x, y [, tagFilter])`
Returns all actors whose physics body contains the point.

### `castle.physics.queryCircle(x, y, radius [, tagFilter])`
Returns all actors whose physics body overlaps the circle.

### `castle.physics.queryBox(x, y, width, height [, tagFilter])`
Returns all actors whose physics body overlaps the box.

---

## Joints

```lua
local joint = castle.physics.newRevoluteJoint(actorA, actorB, {
  motorSpeed = 90,
  maxMotorTorque = 100,
})

local joint = castle.physics.newDistanceJoint(actorA, actorB, {
  length = 0.2,
})

joint:destroy()
```

Available joint types: `newRevoluteJoint`, `newDistanceJoint`, `newPrismaticJoint`, `newWeldJoint`, `newRopeJoint`.

---

## Minimal example

**Physics ball that falls and bounces:**
```lua
local floor = castle.createActor({
  body = { x=0, y=6.5, width=10, height=0.5 },
  solid = {}, tags = { "floor" },
})

function onUpdate(dt)
  for _, touch in ipairs(castle.getTouches()) do
    if touch.pressed then
      castle.createActor({
        body = { x=touch.x, y=touch.y, width=0.5, height=0.5 },
        solid = {}, moving = {}, falling = {},
        bouncy = { bounciness=0.6 }, tags = { "ball" },
      })
    end
  end
end

function onDraw()
  castle.draw.setColor(0.4, 0.4, 0.4)
  local fx, fy = floor:getX(), floor:getY()
  if fx then castle.draw.rectangle("fill", fx-5, fy-0.25, 10, 0.5) end
  castle.draw.setColor(1, 0.3, 0.1)
  for _, ball in ipairs(castle.actorsWithTag("ball")) do
    local bx, by = ball:getX(), ball:getY()
    if bx then castle.draw.circle("fill", bx, by, 0.25) end
  end
end
```
