# Map Validation

The validator catches structural problems, performance budget violations, and
lighting issues before your map ships. Two validation modes are available:

1. **Editor validation** — runs automatically when you open a map scene
2. **Headless CLI validation** — runs from the terminal, suitable for CI/CD

## Editor Validation (template_map.gd)

Every map created from the template includes `template_map.gd`, a `@tool`
script that runs automatically when the scene opens in the Godot editor.

**How to use it:**
1. Open your map's `.tscn` file in the Godot editor
2. Look at the **Output** panel (bottom dock)
3. The script prints a validation report like:
   ```
   === Map Template: Validate Scene ===
     + CT Spawn points (5 found)
     + T Spawn points (5 found)
     + Buy zones (2 found)
     + Bomb sites (2 found)
     + Cubemap capture origins (1 found)
     + Map boundary walls (8 found)
     + LightmapGI configured ✓
     + LightmapGI: NOT YET BAKED
     + DirectionalLight3D (sun) present
   ```

**Checks performed:**
- Required gameplay groups exist (`ct_spawn`, `t_spawn`, `buy_zone`, etc.)
- CSG floor and wall geometry present
- LightmapGI configured and baked
- ReflectionProbe and WorldEnvironment present
- DirectionalLight3D (sun) present
- Playable area extents estimated

## Headless CLI Validator (validate_map.gd)

The headless validator is a 4-module CLI tool at `client/tools/validate_map.gd`.
Use it for automated checking before packaging or in CI/CD pipelines.

### Usage

```bash
# From the tactical-shooter repository root
godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
```

> The `--` separates Godot engine arguments from user arguments.
> The scene path must be a `res://`-prefixed Godot resource path.

### Exit Codes

| Code | Meaning | Description |
|------|---------|-------------|
| 0 | **PASS** | All checks passed, no warnings |
| 1 | **WARNINGS** | Passed with non-blocking suggestions |
| 2 | **ERRORS** | Failed — must-fix items present |

### CI/CD Integration

```bash
#!/bin/bash
# Gate map merges on validation

godot --path client --script tools/validate_map.gd -- res://maps/mymap.tscn
exit_code=$?

if [ $exit_code -eq 2 ]; then
    echo "❌ Map validation FAILED — fix errors before submitting"
    exit 1
elif [ $exit_code -eq 1 ]; then
    echo "⚠️  Map passed with warnings — review suggestions"
fi

echo "✅ Map validated successfully"
exit 0
```

### Example Output

```
═══════════════════════════════════════════
  MAP VALIDATOR
  Scene: res://maps/de_dust2.tscn
═══════════════════════════════════════════

── [validate_scene] ────────────────────────────
  ✓ Root node: World (Node3D)
  ✓ WorldEnvironment (WorldEnvironment)
  ✓ LightmapGI (LightmapGI)
  ✓ Group "ct_spawn" → CT_SpawnPos (5 node(s))
  ✓ Group "t_spawn" → T_SpawnPos (5 node(s))
  ✓ Group "buy_zone" → BuyZone_A (2 node(s))
  ✓ Group "bomb_site" → BombsiteA (2 node(s))
  ✓ Group "cubemap_origin" → CubemapOrigin (1 node(s))
  ✓ pass

── [validate_polycount] ────────────────────────
  Meshes scanned: 24
  Total triangles: 12,842
  Budget: 50,000
  ✓ Within budget (25.7% of 50000)
  Top meshes by triangle count:
     4200  Building_01  (MeshInstance3D)
     2100  Wall_Detail   (MeshInstance3D)
     1042  Floor_Tiles   (MeshInstance3D)
  ✓ pass

── [validate_textures] ─────────────────────────
  Textures inspected: 18
  ✓ All textures within 1024×1024 limit
  ✓ pass

── [validate_lights] ───────────────────────────
  Light breakdown:
    DirectionalLight3D: 1
    OmniLight3D:        2
    SpotLight3D:        0
    Dynamic (non-baked): 1
    Baked:              2
  ✓ Dynamic light count: 1 (limit: 4)
  ✓ LightmapGI baked (light_data present)
    Quality: 2 (0=Low, 1=Med, 2=High, 3=Ultra)
    Bounces: 3
    Texel Scale: 1.0
    Max Texture Size: 1024
  ✓ WorldEnvironment configured
  ReflectionProbes: 1
  ✓ pass

═══════════════════════════════════════════
  SUMMARY
═══════════════════════════════════════════
  [PASS] validate_scene
  [PASS] validate_polycount
  [PASS] validate_textures
  [PASS] validate_lights

  Errors:   0
  Warnings: 0
  Result:   PASS
```

## Validator Modules

### 1. Scene Structure (`validate_scene.gd`)

**What it checks:**
- Scene root is a `Node3D` type
- `WorldEnvironment` node present and named correctly
- `LightmapGI` node present and named correctly
- All 5 required groups assigned (`ct_spawn`, `t_spawn`, `buy_zone`, `bomb_site`, `cubemap_origin`)
- Node names follow PascalCase convention (no spaces)
- No duplicate required groups on different nodes

**Typical errors:**
```
✖ Missing required group: "ct_spawn"
✖ Missing WorldEnvironment — map needs a WorldEnvironment node
✖ Node name contains spaces: "My Room Section"
```

### 2. Polygon Count (`validate_polycount.gd`)

**What it checks:**
- Total triangle count across all `MeshInstance3D` nodes ≤ **50,000**
- Per-mesh triangle breakdown printed (helps identify heavy assets)
- Warns on individual meshes > **5,000** triangles
- Warns if no `MeshInstance3D` nodes found at all

**Typical errors:**
```
✖ Map exceeds 50,000 triangle budget: 68,420 total (36.8% over budget)
```

### 3. Texture Sizes (`validate_textures.gd`)

**What it checks:**
- All material textures ≤ **1024×1024**
- Checks `StandardMaterial3D`, `ORMMaterial3D`, and `ShaderMaterial` texture params
- Warns on textures > **512×512** (suggested for secondary surfaces)
- LightmapGI textures checked separately (baked textures can be larger)

**Typical errors:**
```
✖ Texture exceeds 1024×1024: floor_tiles.png (albedo) — 2048×
```

### 4. Lights & Lightmap (`validate_lights.gd`)

**What it checks:**
- Dynamic (non-baked) OmniLight3D + SpotLight3D ≤ **4**
- DirectionalLight3D counts toward dynamic budget if not baked
- LightmapGI present and baked (`light_data != null`)
- Lightmap quality, bounces, texel scale, max texture size
- WorldEnvironment assigned with Environment resource
- ReflectionProbe presence and `max_distance` coverage
- Warns on dynamic lights with shadow enabled (performance cost)

**Typical errors:**
```
✖ Too many dynamic lights: 6 (max 4)
✖ No LightmapGI node found
✖ No WorldEnvironment node found
```

## Passing the Validator

The goal is exit code 0: **PASS**, no errors, no warnings.

| Code | Action |
|------|--------|
| 0 (PASS) | Your map is ready to package |
| 1 (WARNINGS) | Fix suggestions before shipping (non-blocking) |
| 2 (ERRORS) | Fix all errors — map must pass before packaging |

> **CI gate rule:** Treat exit code 2 as a hard block. Exit code 1 is advisory
> but should be reviewed. Only exit code 0 means "ship it."

## Adding a Custom Validator Module

Advanced users can extend the validator:

1. Create `client/tools/validate_map/validate_<name>.gd` extending `RefCounted`
2. Implement `func validate(scene_root: Node, scene_path: String) -> Dictionary`
3. Return `{"pass": bool, "errors": [String], "warnings": [String]}`
4. Add a `_run_module("<name>", instance, scene_path)` call in
   `validate_map.gd:_ready()`

## Next Steps

- [Packaging & Shipping](05-packaging-and-shipping.md) — ship your validated map
