# Pathfinding System

This document describes the grid-based pathfinding used for the player and all NPCs, including the collision avoidance and movement quality improvements.

---

## Table of Contents

1. [Grid Representation](#1-grid-representation)
2. [Building the Walkable Grid](#2-building-the-walkable-grid)
3. [A\* Path Search](#3-a-path-search)
4. [Path Smoothing](#4-path-smoothing)
5. [Approaching Unreachable Destinations](#5-approaching-unreachable-destinations)
6. [Dynamic Obstacles](#6-dynamic-obstacles)
7. [Path Following](#7-path-following)
8. [Character Collision Resolution](#8-character-collision-resolution)
9. [Dynamic Replanning](#9-dynamic-replanning)
10. [Key Constants Reference](#10-key-constants-reference)

---

## 1. Grid Representation

The world is divided into a uniform 2D grid in the XZ plane (Y is ignored during pathfinding; all characters walk on a flat floor at `floorY`).

Each cell is either **walkable** (`1`) or **blocked** (`0`). The grid is stored as a flat `std::vector<unsigned char>` indexed by `z * gridWidth + x`.

**Parameters** (all configurable in the JSON config file):

| Parameter | Default | Description |
|---|---|---|
| `cellSize` | 0.4 m | Width and depth of one cell |
| `agentRadius` | 0.45 m | Half-width of a character — used to erode free space |
| `objectPadding` | 0.25 m | Extra clearance added around obstacle polygons |
| `boundaryPadding` | 0.0 m | Inward erosion from the edges of navigation areas |
| `floorY` | 0.0 | Y coordinate placed on every path waypoint |

**Grid bounds** are computed from the union of all navigation area polygons plus a padding margin of `cellSize * 2 + agentRadius + objectPadding` on every side.

**Cell coordinate conversion:**

```
cell.x = floor((worldX - minX) / cellSize)
cell.z = floor((worldZ - minZ) / cellSize)

cellCenter.x = minX + (cell.x + 0.5) * cellSize
cellCenter.z = minZ + (cell.z + 0.5) * cellSize
```

---

## 2. Building the Walkable Grid

The grid can be loaded in two ways.

### 2a. Pre-computed grid (`.txt` file)

A plain-text file with a small header followed by rows of `1`/`0` characters:

```
cellSize 0.4
agentRadius 0.45
floorY 0.0
...
minX -5.0
minZ -5.0
gridWidth 50
gridDepth 50
11111111...
10000001...
```

This format is generated by `PathFinder::saveGrid()` after building from polygons and can be loaded much faster than recomputing from geometry.

### 2b. Polygon-based config (`.json` file)

The JSON file lists **navigation areas** (convex or concave walkable regions) and **obstacle polygons** (impassable zones within those regions):

```json
{
  "cellSize": 0.4,
  "areas": [
    { "name": "main_room", "available": true, "polygon": [[x,z], ...] }
  ],
  "obstacles": [
    { "name": "table", "polygon": [[x,z], ...] }
  ]
}
```

**Build steps:**

1. **Mark available areas walkable** — every cell whose center lies inside any `available` navigation area polygon gets `walkable = 1`. If `boundaryPadding > 0`, cells too close to the outer edge of the area are left blocked.
2. **Mark obstacle polygons blocked** — cells whose center lies inside an obstacle polygon, or within `agentRadius + objectPadding` of its edges, are set to `0`.

Navigation areas can be toggled at runtime via `PathFinder::setAreaAvailable()`, which rebuilds the entire grid. This is used to open or close doors, gated areas, etc.

---

## 3. A\* Path Search

`PathFinder::findPath(start, end)` runs a standard A\* on the walkable grid.

**Neighbor connectivity:** 8-directional (cardinal + diagonal). Diagonal moves are blocked if either of the two adjacent cardinal cells is unwalkable (no corner-cutting).

**Step costs:** `1.0` for cardinal, `√2 ≈ 1.414` for diagonal.

**Heuristic:** Euclidean distance in cell units to the end cell.

**Start/end snapping:** If the exact cell for `start` or `end` is not walkable, `findNearestWalkableCell` expands a square ring outward (up to radius 8 m) to find the nearest walkable cell. This makes clicking slightly outside the nav mesh still produce a valid path.

**Path reconstruction:** After A\* completes, the cell chain is walked via `cameFrom[]` from `end` back to `start`, reversed, then smoothed (see §4).

**First-waypoint trimming:** If the first waypoint is within `cellSize × 0.75` of `start`, it is dropped (the character is already close enough).

**Last-waypoint precision:** If the requested `end` maps to the same cell as the snapped end cell, the last waypoint is replaced with the exact `end` world position rather than the cell centre.

---

## 4. Path Smoothing

Raw A\* paths follow the grid diagonals and produce staircase-shaped routes. A **string-pulling** (line-of-sight) pass compresses them:

```
anchor = path[0]
result = [anchor]
while anchor is not the last cell:
    find the furthest cell 'next' from anchor with unobstructed line of sight
    result.append(next)
    anchor = next
```

Line-of-sight is checked by stepping along the segment in increments of `cellSize / 2` and verifying that each sampled cell is walkable. The result is a minimal set of waypoints connected by straight, obstacle-free segments.

---

## 5. Approaching Unreachable Destinations

When a player clicks on a point in a disconnected region (e.g., across a thin wall), the original `findPath` returns an empty path and the character does not move. This is surprising — a click on a solid wall sensibly moves the character to the nearest reachable point, but a click into an inaccessible room does nothing.

**`findPathToNearest`** fixes this with a three-step cascade:

1. Try `findPath` with dynamic obstacles (stationary characters are avoided).
2. If empty, retry `findPath` without dynamic obstacles (an NPC blocking a doorway is ignored).
3. If still empty (destination genuinely unreachable), run **nearest-reachable A\***.

**Nearest-reachable A\***, implemented in `findNearestReachableImpl`:

- Runs the identical A\* loop against the static walkable grid.
- While processing cells, tracks `bestIndex` — the already-visited cell with the smallest Euclidean distance (in cell units) to the end cell.
- If A\* exhausts all reachable space without finding `end`, it reconstructs and returns a path to `bestIndex`.
- If `bestIndex` is still the start cell (character is completely isolated), an empty path is returned and the character stays put.

The net effect: clicking anywhere in the world always moves the character as close as possible to the target, matching the behaviour of clicking on a solid wall.

`findPathToNearest` replaces the direct `findPath` call in `Location::setupNavigation`'s path planner lambda, so it applies equally to the player and all NPCs.

---

## 6. Dynamic Obstacles

When a path is planned, other characters can temporarily mark cells as blocked to make the character walk around them rather than through them.

**How it works:**

In `Location::setupNavigation`, every character is given a `PathPlanner` closure. Before calling `findPath`, the closure builds a list of `PathFinder::DynamicObstacle` entries (position + radius) representing nearby characters. `findPath` copies the static walkable grid, stamps zeros in circles around each obstacle, then runs A\* on the modified copy. The static grid is never mutated.

**Which characters become obstacles:**

A character is added as a dynamic obstacle only when **all** of these are true:

- It is not the character currently planning the path (`self`).
- It is alive and enabled.
- **It is not moving** — a moving character is transparent to pathfinding, so it does not block narrow corridors that it is actively passing through.
- Its position lies within `kDynamicObstacleInfluenceDist = 6 m` of the direct line segment from `start` to `end` (distant characters do not affect the search).

**Obstacle radius:** `character.collisionRadius × 0.6`. Using 60 % of the physical collision radius makes path planning less conservative; physical separation at full radius is still enforced by collision resolution (§8).

**Fallback when dynamic obstacles block the only path:**

If step 1 of `findPathToNearest` (with dynamic obstacles) returns empty, step 2 retries without any dynamic obstacles. This handles the common case of an NPC standing in a doorway: the player paths through the NPC's position, and the nudge logic (§8) pushes the NPC aside as the player passes.

---

## 7. Path Following

`Character::setTarget(destination, onArrived)` sets a new walk target. It calls the path planner to generate a waypoint list. The result is stored in `pathWaypoints`; the final destination is also stored in `walkTarget` and `requestedWalkTarget`.

Each frame in `Character::update`:

1. **Active target** — if `pathWaypoints` is non-empty, the character moves toward `pathWaypoints[currentWaypointIndex]`; otherwise it moves toward `walkTarget`.
2. **Movement** — the character advances along the XZ direction at `walkSpeed` m/s and rotates smoothly toward the movement direction at `rotationSpeed` rad/s.
3. **Waypoint advance** — when the character is within `WALK_THRESHOLD = 0.05 m` of the current waypoint, it advances to the next one. When the last waypoint is reached, `pathWaypoints` is cleared and the optional `onArrived` callback is fired.
4. **State machine** — the animation state switches between `STAND` and `WALK` based on whether the character is moving.

`Character::isMoving()` returns `true` if `pathWaypoints` is non-empty or the distance to `walkTarget` exceeds `WALK_THRESHOLD`. This is used by dynamic obstacle filtering and collision nudging.

**Stopping in place:** `Character::stopInPlace()` sets `walkTarget` and `requestedWalkTarget` to the current position and clears `pathWaypoints`. It is called when an external force (collision resolution) displaces a stationary player so that the player does not walk back to their previous target position.

---

## 8. Character Collision Resolution

Pathfinding alone does not prevent two characters from occupying the same space — it only steers paths around stationary characters. Physical separation is handled separately each frame by `Location::resolveCharacterCollisions`.

**Algorithm** (3 iterations per frame):

For every pair `(A, B)` of living, enabled characters:

1. Compute the overlap: `penetration = (collisionRadius_A + collisionRadius_B) - distance(A, B)`.
2. If `penetration > 0`, compute a push direction (A-to-B normal) and a push magnitude of `penetration / 2` per character.
3. Compute candidate new positions `newA` and `newB`.
4. Validate against the navigation grid (`PathFinder::isWalkable`). If a pushed position is unwalkable, only the other character is moved.
5. **Player stays put:** if the player was not moving (`!isMoving()`) before the push, `stopInPlace()` is called after the push so the player does not walk back to the old target.
6. **NPC yielding:** if one character was moving and the other was standing, `nudgeCharacterAside` is called on the standing character.

**`nudgeCharacterAside(standing, awayFrom)`:**

Gives the standing NPC a short walk target so it steps out of the way:

1. Compute the direction from `awayFrom` to the NPC's current position.
2. Try four candidate targets at distance `1.2 m` in directions: straight away, +90°, −90°, 180°.
3. Use the first candidate that is walkable (per `PathFinder::isWalkable`).
4. Call `standing->setTarget(candidate)` — the NPC takes a small step aside, then stands at the new spot.
5. The player is never nudged; combat NPCs can be nudged, but their attack AI immediately overrides the yield target on the next tick.

---

## 9. Dynamic Replanning

When characters move they can displace each other or enter each other's planned paths. `Location::updateDynamicReplans` handles this:

**Every frame:**

1. Measure how much each character moved since the last frame. Characters that moved more than `kMovedEps = 0.05 m` are collected as **movers**.
2. For each mover, find other characters that are currently walking. If the mover's position is within `kReplanTriggerDist = 1.8 m` of the segment `[walker.position → walker.nextWaypoint]`, trigger a replan for the walker via `forceReplan()`.
3. A per-character cooldown of `kReplanCooldownMs = 500 ms` prevents the same character from replanning more often than twice per second.

**`Character::forceReplan()`** re-runs the path planner from the character's current position to its stored `requestedWalkTarget`, updating `pathWaypoints` in place. If the replanned path is empty, the character stops at its current position.

The relatively generous trigger distance (1.8 m vs the old 1.1 m) and cooldown (500 ms vs 300 ms) prevent micro-jitter: small position corrections from collision resolution no longer spam replanning events.

---

## 10. Key Constants Reference

| Constant | Location | Value | Description |
|---|---|---|---|
| `cellSize` | `PathFinder` config | 0.4 m | Grid cell size |
| `agentRadius` | `PathFinder` config | 0.45 m | Character half-width for grid erosion |
| `objectPadding` | `PathFinder` config | 0.25 m | Extra clearance around obstacles |
| `WALK_THRESHOLD` | `Character.h` | 0.05 m | Distance below which a waypoint is considered reached |
| `TARGET_REPLAN_THRESHOLD` | `Character.h` | 0.25 m | Deduplication threshold in `setTarget` |
| `kDynamicObstacleInfluenceDist` | `Location.cpp` | 6.0 m | Max distance from path for a character to become an obstacle |
| `kDynamicObstacleRadiusFraction` | `Location.cpp` | 0.6× | Fraction of collision radius used for dynamic obstacle footprint |
| `kNudgeDist` | `Location.cpp` | 1.2 m | Distance an NPC steps aside when yielding |
| `kReplanTriggerDist` | `Location.cpp` | 1.8 m | Mover must be this close to a walker's path to trigger replan |
| `kReplanCooldownMs` | `Location.cpp` | 500 ms | Minimum interval between replans for any one character |
| `NPC_TALK_DISTANCE` | `Location.cpp` | 1.35 m | Distance at which walking-to-NPC interaction fires |
| `kIterations` (collision) | `Location.cpp` | 3 | Push-apart iterations per frame |
