docs: community mapmaking documentation (7-file SDK guide)
- docs/mapmaking/00-index.md — SDK overview with workflow diagram - docs/mapmaking/01-getting-started.md — template setup & first map - docs/mapmaking/02-building-geometry.md — CSG guide & prefab reference - docs/mapmaking/03-lighting-and-env.md — LightmapGI baking guide - docs/mapmaking/04-validation.md — validator CLI & CI/CD usage - docs/mapmaking/05-packaging-and-shipping.md — .pck pipeline - docs/mapmaking/06-faq-and-troubleshooting.md — 30+ common issues - README.md — add mapmaking SDK link to features list
This commit is contained in:
@@ -5,6 +5,7 @@ A competitive, round-based tactical FPS built in Godot 4.
|
|||||||
- 128-tick authoritative dedicated servers
|
- 128-tick authoritative dedicated servers
|
||||||
- GDExtension (C++) simulation core
|
- GDExtension (C++) simulation core
|
||||||
- Community-hostable, self-hosted server browser
|
- Community-hostable, self-hosted server browser
|
||||||
- Map-making SDK for community maps
|
- [Map-making SDK](docs/mapmaking/00-index.md) for community maps
|
||||||
|
|
||||||
See [PLANS/PROJECT_PLAN.md](PLANS/PROJECT_PLAN.md) for the full build plan.
|
See [PLANS/PROJECT_PLAN.md](PLANS/PROJECT_PLAN.md) for the full build plan.
|
||||||
|
See [Mapmaking SDK docs](docs/mapmaking/00-index.md) to build and ship custom maps.
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Tactical Shooter — Mapmaking SDK
|
||||||
|
|
||||||
|
Everything you need to build, validate, package, and ship competitive maps for
|
||||||
|
Tactical Shooter. This SDK is designed for the community — no C++, no GDextension
|
||||||
|
tinkering required. You only need Godot 4 (any 4.x) and a text editor.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy the template project
|
||||||
|
cp -r client/map_template/ maps/my_first_map/
|
||||||
|
|
||||||
|
# Rename the demo scene
|
||||||
|
mv maps/my_first_map/template_map.tscn maps/my_first_map/de_my_map.tscn
|
||||||
|
|
||||||
|
# Open in Godot
|
||||||
|
godot maps/my_first_map/project.godot
|
||||||
|
```
|
||||||
|
|
||||||
|
Then read the [Getting Started guide](01-getting-started.md).
|
||||||
|
|
||||||
|
## What You Can Build
|
||||||
|
|
||||||
|
- **Competitive defuse maps** — A-site, B-site, CT/T spawns, buy zones
|
||||||
|
- **Custom game modes** — The template is gameplay-agnostic; your map logic
|
||||||
|
is in Godot groups, not hardcoded
|
||||||
|
- **Community servers** — Ship maps as `.pck` files via the registry server
|
||||||
|
|
||||||
|
## SDK Components
|
||||||
|
|
||||||
|
| Component | Location | Purpose |
|
||||||
|
|-----------|----------|---------|
|
||||||
|
| [Template project](01-getting-started.md) | `client/map_template/` | Standalone Godot project with CSG prefabs and demo scene |
|
||||||
|
| [CSG Prefabs](02-building-geometry.md) | `client/map_template/assets/prefabs/` | 6 drop-in node types (spawns, sites, zones) |
|
||||||
|
| [Editor Validator](04-validation.md) | `client/map_template/template_map.gd` | Auto-runs when scene opened in editor |
|
||||||
|
| [Headless Validator](04-validation.md) | `client/tools/validate_map.gd` | 4-module CLI validator for CI/CD |
|
||||||
|
| [Packaging](05-packaging-and-shipping.md) | `scripts/map_packaging/pack_map.gd` | Exports `.tscn` → `.pck` addon pack |
|
||||||
|
| [Registry Server](05-packaging-and-shipping.md) | `scripts/map_packaging/map_registry_server.py` | Serves `.pck` files over HTTP |
|
||||||
|
| [Client Downloader](05-packaging-and-shipping.md) | `client/scripts/map_downloader.gd` | Autoload that downloads + loads `.pck` at runtime |
|
||||||
|
|
||||||
|
## Map Authoring Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
|
│ 1. Build │ ──→ │ 2. Validate │ ──→ │ 3. Package │
|
||||||
|
│ - CSG geometry │ │ - Editor check │ │ - pack_map.gd │
|
||||||
|
│ - Prefab nodes │ │ - CLI validator │ │ - → .pck file │
|
||||||
|
│ - Lighting │ │ - Fix warnings │ │ │
|
||||||
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
|
│ 6. Load & Play │ ←── │ 5. Download │ ←── │ 4. Upload │
|
||||||
|
│ - Auto-download │ │ - Registry HTTP │ │ - map_registry │
|
||||||
|
│ - changelevel │ │ - Cache to disk │ │ - .pck + .json │
|
||||||
|
└──────────────────┘ └──────────────────┘ └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Targets
|
||||||
|
|
||||||
|
Maps built with the SDK must meet these budgets:
|
||||||
|
|
||||||
|
| Metric | Target | Enforced by |
|
||||||
|
|------------------|-------------|--------------------------------|
|
||||||
|
| Triangle count | ≤ 50,000 | `validate_polycount.gd` |
|
||||||
|
| Texture size | ≤ 1024×1024 | `validate_textures.gd` |
|
||||||
|
| Dynamic lights | ≤ 4 | `validate_lights.gd` |
|
||||||
|
| LightmapGI | ✓ Baked | `validate_lights.gd` |
|
||||||
|
| ReflectionProbe | ≥ 1 | `validate_lights.gd` (warning) |
|
||||||
|
| FPS target | 60+ | Project rendering settings |
|
||||||
|
|
||||||
|
## File Structure Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
tactical-shooter/
|
||||||
|
├── client/
|
||||||
|
│ ├── project.godot # Main game project
|
||||||
|
│ ├── map_template/ # ← YOU ARE HERE (template project)
|
||||||
|
│ │ ├── project.godot # Standalone Godot project
|
||||||
|
│ │ ├── template_map.tscn # Demo scene (rename me)
|
||||||
|
│ │ ├── template_map.gd # Editor validation script
|
||||||
|
│ │ └── assets/prefabs/ # CSG prefab nodes
|
||||||
|
│ ├── tools/
|
||||||
|
│ │ ├── validate_map.gd # CLI validator entry point
|
||||||
|
│ │ └── validate_map/ # 4 validator modules
|
||||||
|
│ │ ├── validate_scene.gd
|
||||||
|
│ │ ├── validate_polycount.gd
|
||||||
|
│ │ ├── validate_textures.gd
|
||||||
|
│ │ └── validate_lights.gd
|
||||||
|
│ └── scripts/
|
||||||
|
│ └── map_downloader.gd # Client autoload for .pck loading
|
||||||
|
├── scripts/
|
||||||
|
│ └── map_packaging/
|
||||||
|
│ ├── pack_map.gd # Godot editor → .pck exporter
|
||||||
|
│ └── map_registry_server.py # Python HTTP registry server
|
||||||
|
└── docs/mapmaking/
|
||||||
|
├── 00-index.md # This document
|
||||||
|
├── 01-getting-started.md # Template project setup
|
||||||
|
├── 02-building-geometry.md # CSG & prefab guide
|
||||||
|
├── 03-lighting-and-env.md # Lighting & baking
|
||||||
|
├── 04-validation.md # Validator usage
|
||||||
|
├── 05-packaging-and-shipping.md# Publishing pipeline
|
||||||
|
└── 06-faq-and-troubleshooting.md
|
||||||
|
```
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Getting Started — Your First Map
|
||||||
|
|
||||||
|
This guide walks you through setting up the map template project and creating
|
||||||
|
a basic playable map.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Godot 4.0 or later (any 4.x stable)
|
||||||
|
- The Tactical Shooter repository cloned
|
||||||
|
- Familiarity with Godot's editor (scene tree, inspector, 3D viewport)
|
||||||
|
|
||||||
|
## 1. Copy the Template
|
||||||
|
|
||||||
|
The template project lives at `client/map_template/`. Copy it somewhere outside
|
||||||
|
the main project (or keep it adjacent — it's a standalone Godot project):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the tactical-shooter repository root
|
||||||
|
cp -r client/map_template/ maps/my_first_map/
|
||||||
|
cd maps/my_first_map/
|
||||||
|
mv template_map.tscn de_dust2.tscn
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Open in Godot
|
||||||
|
|
||||||
|
Launch Godot and click **Import** (not Open). Navigate to your copied directory
|
||||||
|
and select `project.godot`. The map template project will open.
|
||||||
|
|
||||||
|
> **Why "Import"?** Because template is a standalone project with its own
|
||||||
|
> `project.godot`. You're importing a new project into the Godot project list,
|
||||||
|
> not opening a scene within an existing project.
|
||||||
|
|
||||||
|
## 3. Explore the Demo Scene
|
||||||
|
|
||||||
|
Open `de_dust2.tscn` (the renamed `template_map.tscn`). You'll see:
|
||||||
|
|
||||||
|
- **Floor**: A green CSGBox3D pad (16×20 units)
|
||||||
|
- **Walls**: Orange CSGBox3D walls forming a rough box
|
||||||
|
- **Spawn pads**: Green (CT) and red (T) Marker3D spawn points
|
||||||
|
- **Bomb sites**: Semi-transparent orange floor panels
|
||||||
|
- **Buy zones**: Semi-transparent yellow trigger areas
|
||||||
|
- **Lighting**: DirectionalLight3D (sun), OmniLight3D fill, LightmapGI
|
||||||
|
- **Environment**: WorldEnvironment with sky
|
||||||
|
|
||||||
|
The scene also contains a `template_map.gd` editor tool script. When you open
|
||||||
|
this scene, the **Output** panel (bottom dock) shows auto-validation results
|
||||||
|
for the map structure.
|
||||||
|
|
||||||
|
## 4. Build Your First Room
|
||||||
|
|
||||||
|
1. **Select the floor** — Click `Floor` in the scene tree
|
||||||
|
2. **Scale it** — With the scale tool (R), drag the red/green/blue handles
|
||||||
|
to resize the floor to roughly 12×16 units
|
||||||
|
3. **Add walls** — Select `Walls` → right-click → **Instantiate Child Scene** →
|
||||||
|
select `assets/prefabs/map_bounds.tscn`. Size them to enclose the floor
|
||||||
|
4. **Add a ceiling** — Duplicate (Ctrl+D) the floor, move it up 4 units,
|
||||||
|
set **Operation** to Union (0) so it acts as a solid ceiling
|
||||||
|
|
||||||
|
> **Tip:** Use **View → Perspective** and rotate around to check your geometry
|
||||||
|
> from all angles. Walls should overlap slightly to prevent light leaks.
|
||||||
|
|
||||||
|
## 5. Place Spawn Points
|
||||||
|
|
||||||
|
Spawn points tell the game where players appear at round start.
|
||||||
|
|
||||||
|
**CT Spawns:**
|
||||||
|
- Drag `assets/prefabs/ct_spawn.tscn` from the FileSystem dock into the scene
|
||||||
|
- Position it on the floor at one end of the map
|
||||||
|
- The green pad's +Z arrow shows the direction players face on spawn
|
||||||
|
- Duplicate for 5 players (competitive standard)
|
||||||
|
|
||||||
|
**T Spawns:**
|
||||||
|
- Same process with `assets/prefabs/t_spawn.tscn` (red pads)
|
||||||
|
- Place at the opposite end of the map
|
||||||
|
|
||||||
|
## 6. Define Bomb Sites
|
||||||
|
|
||||||
|
A competitive defuse map needs two bomb sites, A and B.
|
||||||
|
|
||||||
|
1. Drag `assets/prefabs/bomb_site.tscn` into the scene
|
||||||
|
2. Rename it to `BombsiteA`
|
||||||
|
3. Resize the CollisionShape3D to cover the intended site area
|
||||||
|
4. In the **Node** dock → **Groups** tab, ensure it has:
|
||||||
|
- `bomb_site` (required — game logic finds sites by this group)
|
||||||
|
- `bombsite_a` (informational — helps identify which site)
|
||||||
|
5. Repeat for BombsiteB with group `bombsite_b`
|
||||||
|
|
||||||
|
## 7. Add Buy Zones
|
||||||
|
|
||||||
|
Buy zones mark areas where players can purchase equipment.
|
||||||
|
|
||||||
|
1. Drag `assets/prefabs/buy_zone.tscn` into the scene
|
||||||
|
2. Place one at the CT spawn area (group `buy_zone` stays)
|
||||||
|
3. Resize the yellow CollisionShape3D to cover the spawn room
|
||||||
|
4. Duplicate for T spawn — add subgroup `ct_buy` or `t_buy` on each
|
||||||
|
|
||||||
|
## 8. Check the Editor Validator
|
||||||
|
|
||||||
|
Open the **Output** (bottom dock). The `template_map.gd` script runs
|
||||||
|
automatically. You should see something 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)
|
||||||
|
+ CSG Floor geometry (1 nodes)
|
||||||
|
+ CSG Wall geometry (4 walls)
|
||||||
|
+ LightmapGI configured ✓
|
||||||
|
+ LightmapGI: NOT YET BAKED
|
||||||
|
+ ReflectionProbe present
|
||||||
|
+ WorldEnvironment present
|
||||||
|
+ DirectionalLight3D (sun) present
|
||||||
|
```
|
||||||
|
|
||||||
|
The "NOT YET BAKED" warning is expected — you'll bake lighting after geometry
|
||||||
|
is finalised (see [Lighting guide](03-lighting-and-env.md)).
|
||||||
|
|
||||||
|
## 9. Save and Package
|
||||||
|
|
||||||
|
When your map is ready for testing:
|
||||||
|
|
||||||
|
1. Save the scene (`Ctrl+S`)
|
||||||
|
2. Read the [Validation guide](04-validation.md) for headless CI checks
|
||||||
|
3. Read the [Packaging guide](05-packaging-and-shipping.md) to ship it
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Building Geometry with CSG](02-building-geometry.md) — detailed prefab reference
|
||||||
|
- [Lighting & Baking](03-lighting-and-env.md) — how to light your map properly
|
||||||
|
- [Validator Reference](04-validation.md) — CLI validator exit codes and CI
|
||||||
|
- [Packaging & Shipping](05-packaging-and-shipping.md) — publishing as .pck
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
# Building Geometry with CSG
|
||||||
|
|
||||||
|
Tactical Shooter maps are built with Godot's **CSG** (Constructive Solid
|
||||||
|
Geometry) system — the same tool you'd use in a 3D modeller, but inside the
|
||||||
|
Godot editor. CSG nodes let you cut, join, and extrude geometry in real-time
|
||||||
|
with instant visual feedback.
|
||||||
|
|
||||||
|
## CSG Basics
|
||||||
|
|
||||||
|
### CSG Node Types
|
||||||
|
|
||||||
|
| Node | Use | Shape |
|
||||||
|
|------|-----|-------|
|
||||||
|
| `CSGBox3D` | Walls, floors, ceilings, pillars | Rectangular prism |
|
||||||
|
| `CSGSphere3D` | Domes, curved edges | Sphere |
|
||||||
|
| `CSGCylinder3D` | Columns, pipes | Cylinder |
|
||||||
|
| `CSGCombiner3D` | Group container, non-collidable parent | — |
|
||||||
|
| `CSGPolygon3D` | Extruded 2D path | Custom shape |
|
||||||
|
|
||||||
|
### CSG Operations
|
||||||
|
|
||||||
|
Each CSG node has an **Operation** property that determines how it combines
|
||||||
|
with sibling CSG nodes in the same parent:
|
||||||
|
|
||||||
|
| Value | Operation | Effect |
|
||||||
|
|-------|-----------|--------|
|
||||||
|
| 0 | **Union** | Adds to existing geometry (default) |
|
||||||
|
| 1 | **Subtraction** | Cuts a hole through parent |
|
||||||
|
| 2 | **Intersection** | Keeps only overlapping volume |
|
||||||
|
|
||||||
|
**Example — a window cutout:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Wall (CSGBox3D, Union)
|
||||||
|
└── WindowCutout (CSGBox3D, Subtraction)
|
||||||
|
```
|
||||||
|
|
||||||
|
This subtracts the window shape from the wall. Move/resize the subtraction
|
||||||
|
child to reposition the window.
|
||||||
|
|
||||||
|
### Key Properties
|
||||||
|
|
||||||
|
- **`use_collision`** (bool) — Must be `true` on all floor/wall/ceiling CSG
|
||||||
|
nodes so players and bullets collide with them
|
||||||
|
- **`use_shadows`** (bool) — Enable shadow-casting on geometry
|
||||||
|
- **`material`** — Assign placeholder materials during construction; final
|
||||||
|
materials come from the main game project library
|
||||||
|
- **`snap_mode`** — Controls vertex snapping (useful for aligning geometry)
|
||||||
|
|
||||||
|
## Building a Room
|
||||||
|
|
||||||
|
A basic room needs 3 CSG layers:
|
||||||
|
|
||||||
|
```
|
||||||
|
Room (CSGCombiner3D)
|
||||||
|
├── Floor (CSGBox3D, Union, use_collision=true)
|
||||||
|
├── Walls (CSGCombiner3D)
|
||||||
|
│ ├── Wall_North (CSGBox3D, Union)
|
||||||
|
│ ├── Wall_South (CSGBox3D, Union)
|
||||||
|
│ ├── Wall_East (CSGBox3D, Union)
|
||||||
|
│ └── Wall_West (CSGBox3D, Union)
|
||||||
|
└── Ceiling (CSGBox3D, Union, use_collision=true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSG Building Tips
|
||||||
|
|
||||||
|
1. **Overlap slightly** — Walls should extend past the floor edge by ~0.1 units
|
||||||
|
to prevent light leaks (thin gaps let light bleed through)
|
||||||
|
2. **Keep CSG simple** — Use as few CSG nodes as possible. A wall panel is one
|
||||||
|
CSGBox3D, not 4 extruded edges
|
||||||
|
3. **Convert to Mesh when done** — Once geometry is final, select all CSG
|
||||||
|
nodes → right-click → **Convert CSG to Mesh**. This bakes them into static
|
||||||
|
MeshInstance3D nodes that perform much better
|
||||||
|
4. **Avoid curved CSG for floor/walls** — CSGSphere3D and CSGCylinder3D are
|
||||||
|
expensive. Use them sparingly
|
||||||
|
|
||||||
|
## Prefab Reference
|
||||||
|
|
||||||
|
The template provides 6 drag-and-drop prefab nodes. Each is an `assets/prefabs/*.tscn`
|
||||||
|
file that you instantiate into your scene.
|
||||||
|
|
||||||
|
### ct_spawn.tscn
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Type | `Marker3D` (green pad + arrow) |
|
||||||
|
| Group | `ct_spawn` |
|
||||||
|
| Purpose | CT team spawn position |
|
||||||
|
| Quantity | 5 (one per player) |
|
||||||
|
|
||||||
|
The +Z arrow (blue axis) shows the direction players face when they spawn.
|
||||||
|
Position the pad at floor level — make it flush with the floor surface.
|
||||||
|
|
||||||
|
**Customisation:** Duplicate instances rather than editing the prefab. Adjust
|
||||||
|
only position and rotation. The green pad is cosmetic.
|
||||||
|
|
||||||
|
### t_spawn.tscn
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Type | `Marker3D` (red pad + arrow) |
|
||||||
|
| Group | `t_spawn` |
|
||||||
|
| Purpose | Terrorist team spawn position |
|
||||||
|
| Quantity | 5 |
|
||||||
|
|
||||||
|
Same usage as `ct_spawn` but red. Place at the opposite end of the map.
|
||||||
|
|
||||||
|
### buy_zone.tscn
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Type | `Area3D` with `CollisionShape3D` |
|
||||||
|
| Group | `buy_zone` |
|
||||||
|
| Purpose | Purchase-eligible zone |
|
||||||
|
| Quantity | 1–2 |
|
||||||
|
|
||||||
|
The game monitors `body_entered` / `body_exited` on these areas to show/hide
|
||||||
|
the buy menu. Resize the child CollisionShape3D to match the room shape.
|
||||||
|
|
||||||
|
**Additional groups (optional):**
|
||||||
|
- `ct_buy` — Only CT team can buy in this zone
|
||||||
|
- `t_buy` — Only T team can buy in this zone
|
||||||
|
|
||||||
|
If omitted, any player inside a `buy_zone` can buy (suitable for shared mid areas).
|
||||||
|
|
||||||
|
### bomb_site.tscn
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Type | `Area3D` with `CollisionShape3D` |
|
||||||
|
| Group | `bomb_site` |
|
||||||
|
| Purpose | Bomb plant zone |
|
||||||
|
| Quantity | 2 (A and B) |
|
||||||
|
|
||||||
|
Rename instances to `BombsiteA` and `BombsiteB`. Resize the CollisionShape3D
|
||||||
|
to cover the plantable area. Visual: semi-transparent orange floor panel.
|
||||||
|
|
||||||
|
**Additional groups (strongly recommended):**
|
||||||
|
- `bombsite_a` — Identifies this as site A
|
||||||
|
- `bombsite_b` — Identifies this as site B
|
||||||
|
|
||||||
|
### cubemap_origin.tscn
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Type | `Node3D` (blue sphere marker) |
|
||||||
|
| Group | `cubemap_origin` |
|
||||||
|
| Purpose | Marks ReflectionProbe capture position |
|
||||||
|
| Quantity | 1 |
|
||||||
|
|
||||||
|
Place at eye height ≈ 1.6 units above the floor in the most visually prominent
|
||||||
|
area (usually mid-map or the central choke point). The ReflectionProbe is a
|
||||||
|
separate node that should be placed at this marker's position after geometry
|
||||||
|
is finalised.
|
||||||
|
|
||||||
|
### map_bounds.tscn
|
||||||
|
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Type | `Area3D` with `CollisionShape3D` |
|
||||||
|
| Group | `map_bounds` |
|
||||||
|
| Purpose | Out-of-bounds kill wall |
|
||||||
|
| Quantity | 4–8 (enclose the perimeter) |
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
- Place one instance per perimeter face (north, south, east, west)
|
||||||
|
- Resize each CollisionShape3D to form a closed box around the playable area
|
||||||
|
- Make the floor-bound wall tall enough that players can't jump over it
|
||||||
|
- The game detects `body_exited` from these areas and teleports/kills
|
||||||
|
out-of-bounds players
|
||||||
|
|
||||||
|
> **Performance tip:** `map_bounds` nodes should be as thin as practical —
|
||||||
|
> these are just detection triggers, not visual geometry. A thickness of 0.5
|
||||||
|
> units is sufficient.
|
||||||
|
|
||||||
|
## Advanced Geometry Techniques
|
||||||
|
|
||||||
|
### Corridors and Hallways
|
||||||
|
|
||||||
|
Build a corridor as a CSGCombiner3D with union floor + walls + ceiling and
|
||||||
|
subtraction nodes for doors/windows:
|
||||||
|
|
||||||
|
```
|
||||||
|
Corridor (CSGCombiner3D)
|
||||||
|
├── Floor (CSGBox3D, Union)
|
||||||
|
├── LeftWall (CSGBox3D, Union)
|
||||||
|
│ └── Door1 (CSGBox3D, Subtraction)
|
||||||
|
├── RightWall (CSGBox3D, Union)
|
||||||
|
│ └── Window1 (CSGBox3D, Subtraction)
|
||||||
|
└── Ceiling (CSGBox3D, Union)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ramps and Stairs
|
||||||
|
|
||||||
|
Use multiple CSGBox3D nodes at increasing heights. Stack them like steps and
|
||||||
|
wrap in a CSGCombiner3D for a clean hierarchy:
|
||||||
|
|
||||||
|
```
|
||||||
|
Stairs (CSGCombiner3D)
|
||||||
|
├── Step1 (CSGBox3D, Union, pos=(0, 0, 0))
|
||||||
|
├── Step2 (CSGBox3D, Union, pos=(0, 0.5, 1))
|
||||||
|
├── Step3 (CSGBox3D, Union, pos=(0, 1.0, 2))
|
||||||
|
└── Step4 (CSGBox3D, Union, pos=(0, 1.5, 3))
|
||||||
|
```
|
||||||
|
|
||||||
|
For ramps, rotate a CSGBox3D or use CSGPolygon3D with an extruded triangle shape.
|
||||||
|
|
||||||
|
### Map Scale Reference
|
||||||
|
|
||||||
|
| Element | Size (units) | Notes |
|
||||||
|
|---------|-------------|-------|
|
||||||
|
| Player height | 1.8 | Eye level ≈ 1.6 |
|
||||||
|
| Player width | 0.5 | Collision capsule radius |
|
||||||
|
| Doorway | 1.5 × 2.5 | Wide enough for 2 players |
|
||||||
|
| Corridor | 3–4 wide | Comfortable movement |
|
||||||
|
| Bombsite | 8×8 minimum | Playable site area |
|
||||||
|
| Spawn room | 6×8 | 5 players need space |
|
||||||
|
| Mid area | 6–8 wide | Main chokepoint |
|
||||||
|
|
||||||
|
## From CSG to MeshInstance3D (Baking)
|
||||||
|
|
||||||
|
When your geometry is **final** (no more changes expected):
|
||||||
|
|
||||||
|
1. Select all CSG nodes in the scene tree
|
||||||
|
2. Right-click → **Convert CSG to Mesh**
|
||||||
|
3. This replaces CSG nodes with static `MeshInstance3D` nodes
|
||||||
|
4. The mesh instances are much more performant
|
||||||
|
|
||||||
|
**Don't convert until you're done** — CSG is editable, MeshInstance3D is not.
|
||||||
|
Keep working in CSG mode; convert only for final optimisation before packaging.
|
||||||
|
|
||||||
|
## Performance Budget
|
||||||
|
|
||||||
|
See the [Validator guide](04-validation.md) for detailed numbers, but the key
|
||||||
|
geometry rules are:
|
||||||
|
|
||||||
|
- **≤ 50,000 triangles** total across all meshes
|
||||||
|
- **≤ 5,000 triangles** per individual mesh (recommended; enforced as warning)
|
||||||
|
- Use CSG for blockout only — convert to mesh before shipping
|
||||||
|
- Avoid CSGSphere3D in visible areas; use hand-modelled meshes
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Lighting & Environment Setup](03-lighting-and-env.md) — light your map
|
||||||
|
- [Map Validation](04-validation.md) — run the validator on your scene
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# Lighting & Environment Setup
|
||||||
|
|
||||||
|
Proper lighting is the difference between a map that looks professional and one
|
||||||
|
that looks like a grey box. Tactical Shooter uses Godot's **LightmapGI** system
|
||||||
|
for baked global illumination: light bounces are pre-calculated and stored in
|
||||||
|
lightmaps, giving you realistic lighting at zero runtime cost.
|
||||||
|
|
||||||
|
## Lighting Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
DirectionalLight3D (sun) — main directional light, shadows
|
||||||
|
└─ OmniLight3D (fill) — ambient fill lights, no shadow
|
||||||
|
└─ LightmapGI — bakes all static lighting into textures
|
||||||
|
└─ baked lightmaps (zero runtime cost)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. WorldEnvironment
|
||||||
|
|
||||||
|
Every map needs a `WorldEnvironment` node. This controls:
|
||||||
|
|
||||||
|
- **Sky** — Background appearance (skybox or solid colour)
|
||||||
|
- **Ambient light** — Base light level for unlit surfaces
|
||||||
|
- **Tonemapping** — Colour grading and exposure
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
1. The template already includes a `WorldEnvironment` node
|
||||||
|
2. If creating from scratch, add one via the scene tree → + button → `WorldEnvironment`
|
||||||
|
3. In the Inspector, assign an `Environment` resource (New Environment)
|
||||||
|
4. Configure:
|
||||||
|
- **Background:** Sky (for outdoor) or Clear Color (for underground maps)
|
||||||
|
- **Ambient Light:** Sky (mimics sky colour) or Color (custom ambient)
|
||||||
|
- **Tonemap:** Reinhard or Filmic (prevents blown-out highlights)
|
||||||
|
|
||||||
|
> The validator warns if WorldEnvironment is missing — add it early.
|
||||||
|
|
||||||
|
## 2. DirectionalLight3D (Sun)
|
||||||
|
|
||||||
|
The main sun light. One per map.
|
||||||
|
|
||||||
|
**Recommended settings:**
|
||||||
|
| Property | Value | Reason |
|
||||||
|
|----------|-------|--------|
|
||||||
|
| `light_bake_mode` | Static (2) | Bakes into lightmap |
|
||||||
|
| `shadow_enabled` | true | Required for competitive visibility |
|
||||||
|
| `shadow_bias` | 0.1 | Prevents shadow acne on CSG |
|
||||||
|
| `angular_distance` | 0.01 | Sharp sunlight (competitive visual clarity) |
|
||||||
|
| `range` | 50–100 | Large enough to cover the map |
|
||||||
|
|
||||||
|
**Positioning:**
|
||||||
|
- Rotate the sun to create interesting shadows through windows/doorways
|
||||||
|
- Avoid straight-down lighting (makes everything look flat)
|
||||||
|
- A 45° angle works well for most maps
|
||||||
|
|
||||||
|
## 3. Fill Lights (OmniLight3D)
|
||||||
|
|
||||||
|
Fill lights illuminate areas the sun doesn't reach (interiors, tunnels).
|
||||||
|
|
||||||
|
**Rules:**
|
||||||
|
- **Maximum 4 dynamic (non-baked) lights** — enforced by the validator
|
||||||
|
- Fill lights should use `light_bake_mode = Static (2)` so they bake into
|
||||||
|
the lightmap and don't count toward the dynamic budget
|
||||||
|
- Use low intensity (0.3–0.5) for ambient fill
|
||||||
|
|
||||||
|
**Performance tip:** Every omni/spot light with `shadow_enabled = true` is
|
||||||
|
expensive. The validator warns on dynamic lights with shadows. Baked lights
|
||||||
|
with shadows disabled are free — use them.
|
||||||
|
|
||||||
|
## 4. LightmapGI — Baked Global Illumination
|
||||||
|
|
||||||
|
LightmapGI is the backbone of Tactical Shooter's lighting. It pre-computes
|
||||||
|
light bouncing and stores the result as textures (lightmaps).
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
| Property | Recommended | Notes |
|
||||||
|
|----------|-------------|-------|
|
||||||
|
| `quality` | 2 (High) | Higher = better GI, longer bake |
|
||||||
|
| `bounces` | 3 | Minimum 2 for realistic indirect lighting |
|
||||||
|
| `texel_scale` | 1.0–2.0 | Lower = higher res, longer bake |
|
||||||
|
| `max_texture_size` | 1024 | Controls lightmap atlas size |
|
||||||
|
| `use_denoiser` | true | Removes noise from baked result |
|
||||||
|
| `environment_mode` | 0 (Baked Environment) | Uses WorldEnvironment for sky lighting |
|
||||||
|
|
||||||
|
### Baking
|
||||||
|
|
||||||
|
To bake: select the `LightmapGI` node → click **Bake Lightmap** in the top
|
||||||
|
toolbar (or `Ctrl+Shift+C`).
|
||||||
|
|
||||||
|
**Baking checklist:**
|
||||||
|
- [ ] All geometry is finalised (moving geometry after baking invalidates it)
|
||||||
|
- [ ] All lights have correct bake mode (Static for baked, Dynamic for real-time)
|
||||||
|
- [ ] CSG has `use_collision = true` (non-collidable CSG may not bake properly)
|
||||||
|
- [ ] WorldEnvironment is configured with a sky or ambient colour
|
||||||
|
- [ ] Save the scene before baking
|
||||||
|
|
||||||
|
**Baking time:** Varies by map complexity. A small map (10×14 units) bakes in
|
||||||
|
30–60 seconds. A full competitive map (20×20) may take 5–10 minutes.
|
||||||
|
|
||||||
|
### After Baking
|
||||||
|
|
||||||
|
- The LightmapGI node's `light_data` property will show a `LightmapGIData` resource
|
||||||
|
- The validator checks for `light_data != null` to confirm the map is baked
|
||||||
|
- Baked lightmaps are stored in the `.tscn` file as embedded resources
|
||||||
|
- You can rebake at any time — it overwrites the previous lightmaps
|
||||||
|
|
||||||
|
**Warning:** Opening an unbaked map on a server will default to unlit rendering.
|
||||||
|
Always bake before packaging.
|
||||||
|
|
||||||
|
## 5. ReflectionProbe
|
||||||
|
|
||||||
|
ReflectionProbes add specular reflections to shiny surfaces (metal, glass, water).
|
||||||
|
|
||||||
|
- Place one `ReflectionProbe` near the `cubemap_origin` marker
|
||||||
|
- Set `max_distance` to cover the visible area (10–30 units)
|
||||||
|
- Enable `box_projection` for interior spaces (corrects reflection distortion)
|
||||||
|
- Set `intensity` to 0.5–0.8 (realistic reflection strength)
|
||||||
|
|
||||||
|
**Tip:** You need reflection probes even if your map has no mirrors — they make
|
||||||
|
metal and plastic materials look correct.
|
||||||
|
|
||||||
|
## 6. Lighting Performance Budget
|
||||||
|
|
||||||
|
| Metric | Limit | Enforced |
|
||||||
|
|--------|-------|----------|
|
||||||
|
| Dynamic (non-baked) lights | ≤ 4 | Validator (error) |
|
||||||
|
| Total lights (baked + dynamic) | Unlimited | — |
|
||||||
|
| LightmapGI bounces | ≥ 2 | Validator (warning) |
|
||||||
|
| ReflectionProbes | ≥ 1 | Validator (warning) |
|
||||||
|
| Lightmap max texture | ≤ 2048 | Project settings |
|
||||||
|
|
||||||
|
If you exceed the dynamic light budget, set more lights to
|
||||||
|
`light_bake_mode = Static (2)` — baked lights cost nothing at runtime.
|
||||||
|
|
||||||
|
## 7. Quick Start Checklist
|
||||||
|
|
||||||
|
- [ ] WorldEnvironment with Environment resource assigned
|
||||||
|
- [ ] DirectionalLight3D (sun) with shadows
|
||||||
|
- [ ] 1–3 fill OmniLight3D with Static bake mode
|
||||||
|
- [ ] LightmapGI with quality=2, bounces=3, texel_scale=1.0
|
||||||
|
- [ ] ReflectionProbe at cubemap origin
|
||||||
|
- [ ] All geometry CSG has `use_collision = true`
|
||||||
|
- [ ] Save scene → Bake LightmapGI (Ctrl+Shift+C)
|
||||||
|
- [ ] Verify `light_data` is populated (not null)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Problem | Likely Cause | Fix |
|
||||||
|
|---------|-------------|-----|
|
||||||
|
| Light leaks at corners | CSG gap < 0.01 units | Overlap CSG edges by 0.1 |
|
||||||
|
| Baked lighting too dark | bounces too low | Set bounces ≥ 2 |
|
||||||
|
| Lightmap seams visible | texel_scale too high | Lower texel_scale to 1.0 |
|
||||||
|
| Shadow acne (striped shadows) | shadow_bias too low | Increase shadow_bias to 0.1 |
|
||||||
|
| ReflectionProbe visible as sphere | probe_mode wrong | Set to Box or disable visibility |
|
||||||
|
| Bake fails with error | Scene not saved | Save `.tscn` before baking |
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [Map Validation](04-validation.md) — run all checks on your scene
|
||||||
|
- [Packaging & Shipping](05-packaging-and-shipping.md) — publish your map
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
# Packaging & Shipping
|
||||||
|
|
||||||
|
Once your map is built, validated, and baked, it's time to publish it to the
|
||||||
|
world. Tactical Shooter uses the **PCK** (Godot Resource Pack) format — the map
|
||||||
|
is packaged as a `.pck` file, uploaded to a registry server, and clients
|
||||||
|
download it automatically when they join a server running that map.
|
||||||
|
|
||||||
|
## Pipeline Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐
|
||||||
|
│ pack_map │ → │ .pck + .json │ → │ Map Registry │ → │ Client │
|
||||||
|
│ .gd │ │ files │ → │ Server │ → │ Downloader │
|
||||||
|
└──────────┘ └──────────────┘ └──────────────┘ └────────────┘
|
||||||
|
(editor (artifacts) (HTTP serving) (autoload)
|
||||||
|
tool)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 1: Package the Map
|
||||||
|
|
||||||
|
The `pack_map.gd` script (at `scripts/map_packaging/pack_map.gd`) is a Godot
|
||||||
|
editor tool that exports a `.tscn` scene as a standalone `.pck` resource pack.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
From the **map template project** (not the main game project):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd maps/my_first_map/
|
||||||
|
|
||||||
|
godot --headless --script /path/to/scripts/map_packaging/pack_map.gd \
|
||||||
|
--map=res://de_my_map.tscn
|
||||||
|
```
|
||||||
|
|
||||||
|
> Replace `/path/to/` with the actual path to the tactical-shooter repository
|
||||||
|
> root, or copy `pack_map.gd` into your map project's `scripts/` directory.
|
||||||
|
|
||||||
|
### What It Does
|
||||||
|
|
||||||
|
1. Loads the specified `.tscn` scene
|
||||||
|
2. Recursively collects all dependencies (materials, textures, meshes, etc.)
|
||||||
|
3. Exports them to `user://packed_maps/de_my_map.pck`
|
||||||
|
4. Writes a metadata file `user://packed_maps/de_my_map.json`
|
||||||
|
|
||||||
|
### Output Files
|
||||||
|
|
||||||
|
```
|
||||||
|
user://packed_maps/
|
||||||
|
├── de_my_map.pck # Binary resource pack (~1–10 MB for a typical map)
|
||||||
|
└── de_my_map.json # JSON metadata (machine-readable)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Metadata example** (`de_my_map.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"map_name": "de_my_map",
|
||||||
|
"source_scene": "res://de_my_map.tscn",
|
||||||
|
"godot_version": {
|
||||||
|
"major": 4,
|
||||||
|
"minor": 2,
|
||||||
|
"string": "4.2"
|
||||||
|
},
|
||||||
|
"packed_at": "2026-06-25T14:30:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- The map scene must use **local resources** only (relative `res://` paths)
|
||||||
|
- External dependencies (game shared assets like weapon models, player
|
||||||
|
models) should stay in the base game — the `.pck` is **additive** content
|
||||||
|
- The `.pck` contains only the map's unique assets, not the entire game
|
||||||
|
|
||||||
|
### From the Editor
|
||||||
|
|
||||||
|
You can also run `pack_map.gd` from the Godot editor:
|
||||||
|
|
||||||
|
1. Open your map project in Godot
|
||||||
|
2. Open your map scene (`de_my_map.tscn`)
|
||||||
|
3. **Project → Tools → Pack Current Map** (if configured)
|
||||||
|
4. The script detects the active scene and exports it
|
||||||
|
|
||||||
|
## Step 2: Set Up the Map Registry Server
|
||||||
|
|
||||||
|
The registry server is a lightweight Python HTTP server
|
||||||
|
(`scripts/map_packaging/map_registry_server.py`) that:
|
||||||
|
- Serves `.pck` files for download
|
||||||
|
- Provides a JSON `/maps` endpoint listing available maps
|
||||||
|
- Auto-scans the maps directory every 5 seconds
|
||||||
|
- Computes and serves SHA-256 checksums for integrity verification
|
||||||
|
|
||||||
|
### Starting the Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/tactical-shooter
|
||||||
|
|
||||||
|
# Default: port 8090, maps from ./packed_maps/
|
||||||
|
python3 scripts/map_packaging/map_registry_server.py
|
||||||
|
|
||||||
|
# Custom configuration
|
||||||
|
python3 scripts/map_packaging/map_registry_server.py \
|
||||||
|
--port 8080 \
|
||||||
|
--maps-dir /data/tactical-shooter-maps \
|
||||||
|
--verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment variables:**
|
||||||
|
| Variable | Overrides | Default |
|
||||||
|
|----------|-----------|---------|
|
||||||
|
| `MAP_REGISTRY_PORT` | `--port` / `-p` | 8090 |
|
||||||
|
| `MAP_REGISTRY_MAPS` | `--maps-dir` / `-d` | `./packed_maps/` |
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `GET /` | — | Server info and endpoint documentation |
|
||||||
|
| `GET /maps` | — | JSON list of all available maps with metadata |
|
||||||
|
| `GET /maps/<name>.pck` | — | Download map `.pck` binary |
|
||||||
|
| `GET /maps/<name>.json` | — | Download per-map metadata |
|
||||||
|
| `OPTIONS /maps` | — | CORS preflight |
|
||||||
|
|
||||||
|
**Example responses:**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /maps
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"maps": [
|
||||||
|
{
|
||||||
|
"name": "de_dust2",
|
||||||
|
"size": 5432100,
|
||||||
|
"version": 1,
|
||||||
|
"description": "Classic competitive map",
|
||||||
|
"checksum_sha256": "a1b2c3d4...",
|
||||||
|
"packed_at": "2026-06-25T14:30:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"server_name": "Tactical Shooter Map Registry",
|
||||||
|
"server_version": "1.0.0",
|
||||||
|
"map_count": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /maps/de_dust2.pck
|
||||||
|
```
|
||||||
|
→ Returns the `.pck` binary file as an octet-stream download.
|
||||||
|
|
||||||
|
### Deployment Options
|
||||||
|
|
||||||
|
- **Local dev:** Run on the same machine as the game (default `127.0.0.1:8090`)
|
||||||
|
- **Server host:** Run on a public server alongside the master server
|
||||||
|
- **Docker:** Wrap in a Docker container for easy deployment
|
||||||
|
- **Systemd service:** Create a service file for production hosting
|
||||||
|
|
||||||
|
**Systemd service example:**
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=Tactical Shooter Map Registry
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=tactical
|
||||||
|
WorkingDirectory=/opt/tactical-shooter
|
||||||
|
ExecStart=/usr/bin/python3 scripts/map_packaging/map_registry_server.py \
|
||||||
|
--port 8090 --maps-dir /data/tactical-maps
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 3: Upload Your Map
|
||||||
|
|
||||||
|
1. Copy the `.pck` and `.json` files to the registry server's maps directory:
|
||||||
|
```bash
|
||||||
|
cp user://packed_maps/de_my_map.pck /data/tactical-maps/
|
||||||
|
cp user://packed_maps/de_my_map.json /data/tactical-maps/
|
||||||
|
```
|
||||||
|
2. The server auto-detects new files within 5 seconds
|
||||||
|
3. Verify via `curl http://your-server:8090/maps`
|
||||||
|
|
||||||
|
## Step 4: Client Downloads
|
||||||
|
|
||||||
|
The client uses `MapDownloader` (autoload at `client/scripts/map_downloader.gd`)
|
||||||
|
to fetch and load maps at runtime.
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
1. Player joins a server that specifies a map name
|
||||||
|
2. The client checks its local cache (`user://maps/manifest.json`)
|
||||||
|
3. If the map is not cached, `MapDownloader.fetch_map_list()` polls
|
||||||
|
the registry server to verify the map exists
|
||||||
|
4. `MapDownloader.download_map(name)` downloads the `.pck` to `user://maps/`
|
||||||
|
5. `ProjectSettings.load_resource_pack()` loads it into the game
|
||||||
|
6. The server changes to the new level
|
||||||
|
|
||||||
|
### Map Caching
|
||||||
|
|
||||||
|
```
|
||||||
|
user://maps/
|
||||||
|
├── de_dust2.pck # Cached map binary
|
||||||
|
├── de_inferno.pck # Another cached map
|
||||||
|
└── manifest.json # Cache index
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cache behaviour:**
|
||||||
|
- Maps are cached to disk after download (survives restarts)
|
||||||
|
- The manifest tracks version numbers for staleness checks
|
||||||
|
- `MapDownloader.remove_map(name)` clears a specific map
|
||||||
|
- `MapDownloader.clear_cache()` removes all cached maps
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
The `MapDownloader` autoload can be configured in-game:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Set the registry server URL (override default http://127.0.0.1:8090)
|
||||||
|
MapDownloader.registry_url = "http://maps.myserver.com:8090"
|
||||||
|
```
|
||||||
|
|
||||||
|
The URL can also be set via the `MAP_REGISTRY_URL` environment variable,
|
||||||
|
which is read in `_ready()`.
|
||||||
|
|
||||||
|
### Signals
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Register for map lifecycle events
|
||||||
|
MapDownloader.map_list_loaded.connect(_on_map_list)
|
||||||
|
MapDownloader.map_download_progress.connect(_on_download_progress)
|
||||||
|
MapDownloader.map_download_complete.connect(_on_download_done)
|
||||||
|
MapDownloader.map_loaded.connect(_on_map_loaded)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Pipeline Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Package from the template project
|
||||||
|
cd maps/my_first_map
|
||||||
|
godot --headless --script ../../scripts/map_packaging/pack_map.gd \
|
||||||
|
--map=res://de_my_map.tscn
|
||||||
|
|
||||||
|
# 2. Start registry server
|
||||||
|
cd ../..
|
||||||
|
python3 scripts/map_packaging/map_registry_server.py --port 8090 &
|
||||||
|
|
||||||
|
# 3. Deploy map
|
||||||
|
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.pck \
|
||||||
|
scripts/map_packaging/packed_maps/
|
||||||
|
cp ~/.local/share/godot/app_userdata/MapTemplate/packed_maps/de_my_map.json \
|
||||||
|
scripts/map_packaging/packed_maps/
|
||||||
|
|
||||||
|
# 4. Verify
|
||||||
|
curl -s http://localhost:8090/maps | python3 -m json.tool
|
||||||
|
|
||||||
|
# 5. In-game: connect to server and play the map
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- [FAQ & Troubleshooting](06-faq-and-troubleshooting.md) — common issues
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
# FAQ & Troubleshooting
|
||||||
|
|
||||||
|
Common questions, issues, and solutions encountered while building maps.
|
||||||
|
|
||||||
|
## Template Project
|
||||||
|
|
||||||
|
### Q: Can I use the template project without the main game repo?
|
||||||
|
|
||||||
|
Yes. The template project at `client/map_template/` is a **standalone Godot
|
||||||
|
project**. Copy it anywhere, open it in Godot, and start building. You only
|
||||||
|
need the main repo for packaging (`pack_map.gd`) and the registry server
|
||||||
|
(`map_registry_server.py`).
|
||||||
|
|
||||||
|
### Q: Why does Godot say "Import" instead of "Open"?
|
||||||
|
|
||||||
|
The template has its own `project.godot`, making it a separate project. Use
|
||||||
|
**Import** to add it to your Godot project list. You're not opening a scene
|
||||||
|
within an existing project.
|
||||||
|
|
||||||
|
### Q: How do I add the main game's assets to my map?
|
||||||
|
|
||||||
|
The template uses placeholder materials. For final maps:
|
||||||
|
|
||||||
|
1. Copy your map's `.tscn` into the main project under `client/maps/<name>/`
|
||||||
|
2. Open it from the main project's scenes tab
|
||||||
|
3. The main game's modular kit and PBR materials will be available
|
||||||
|
4. Assign materials to your CSG/MeshInstance3D nodes from the main project's
|
||||||
|
asset library
|
||||||
|
|
||||||
|
### Q: Can I use external models from Blender/Maya?
|
||||||
|
|
||||||
|
Yes. Export as `.glb` or `.obj` and import into Godot. Keep triangle counts
|
||||||
|
within budget (≤ 50K total, ≤ 5K per mesh recommended). Place imported meshes
|
||||||
|
as children of the map scene root.
|
||||||
|
|
||||||
|
## Geometry & CSG
|
||||||
|
|
||||||
|
### Q: My map has light leaks at the corners
|
||||||
|
|
||||||
|
Thin gaps between CSG blocks let light bleed through. Fix by overlapping CSG
|
||||||
|
edges by **0.1 units** instead of butting them flush.
|
||||||
|
|
||||||
|
```
|
||||||
|
Bad: Wall ends at x=10.0, floor ends at x=10.0 → gap
|
||||||
|
Good: Wall extends to x=10.1, floor ends at x=10.0 → overlap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q: Players can walk through walls
|
||||||
|
|
||||||
|
Make sure `use_collision = true` is set on all CSG geometry nodes. CSG nodes
|
||||||
|
don't have collision by default — you must enable it.
|
||||||
|
|
||||||
|
### Q: My map renders as a grey box / no materials
|
||||||
|
|
||||||
|
Placeholder materials are normal during construction. For the final look, you
|
||||||
|
need to:
|
||||||
|
1. Copy the map to the main project (see above)
|
||||||
|
2. Assign materials from the game's asset library
|
||||||
|
3. Re-bake LightmapGI after materials are assigned
|
||||||
|
|
||||||
|
### Q: CSG performance is bad in the editor
|
||||||
|
|
||||||
|
This is normal. CSG is re-calculated every time you move a node. For testing,
|
||||||
|
convert stable geometry to MeshInstance3D:
|
||||||
|
- Right-click CSG parent → **Convert CSG to Mesh**
|
||||||
|
- Keep a backup copy of the CSG hierarchy in another scene
|
||||||
|
|
||||||
|
### Q: How do I make a skybox?
|
||||||
|
|
||||||
|
Add a `WorldEnvironment` node and assign an Environment resource:
|
||||||
|
1. Set **Background** mode to **Sky**
|
||||||
|
2. Create a new **Sky** resource
|
||||||
|
3. Assign a **PanoramaSky** or **ProceduralSky** material
|
||||||
|
4. The sky appears behind your geometry
|
||||||
|
|
||||||
|
### Q: My map has no collision after CSG → Mesh conversion
|
||||||
|
|
||||||
|
When you convert CSG to MeshInstance3D, collision is not preserved. Add a
|
||||||
|
`CollisionShape3D` as a child of each MeshInstance3D, or use Godot's
|
||||||
|
**Mesh → Create Trimesh Collision** (right-click the MeshInstance3D).
|
||||||
|
|
||||||
|
## Lighting
|
||||||
|
|
||||||
|
### Q: My lightmap bake takes forever
|
||||||
|
|
||||||
|
Baking time depends on:
|
||||||
|
- **texel_scale** — lower = higher resolution = longer bake (try 2.0 for testing)
|
||||||
|
- **bounces** — fewer bounces = faster bake (use 1 for quick tests, 3 for final)
|
||||||
|
- **Map size** — larger maps bake longer
|
||||||
|
- **Light count** — more lights = more calculations
|
||||||
|
|
||||||
|
For quick iteration, use Low quality (0) during development and High quality (2)
|
||||||
|
for the final bake.
|
||||||
|
|
||||||
|
### Q: Baked lighting is too dark
|
||||||
|
|
||||||
|
- Increase `bounces` to 3 (more light bounce = brighter interiors)
|
||||||
|
- Add a low-intensity ambient fill (OmniLight3D with Static bake mode)
|
||||||
|
- Lower the `texel_scale` for more lightmap resolution
|
||||||
|
- Check that the DirectionalLight3D intensity is sufficient
|
||||||
|
|
||||||
|
### Q: Lightmap has visible seams
|
||||||
|
|
||||||
|
Seams appear where lightmap texels don't align. Fixes:
|
||||||
|
- Lower `texel_scale` to 1.0 (higher resolution lightmaps)
|
||||||
|
- Ensure CSG edges overlap (no gaps)
|
||||||
|
- Convert CSG to MeshInstance3D before final bake
|
||||||
|
|
||||||
|
### Q: Shadows look blocky or jagged
|
||||||
|
|
||||||
|
This is Godot's shadow mapping at default resolution. For competitive clarity:
|
||||||
|
- Keep `shadow_bias` at 0.1 (higher = softer shadows, fewer artefacts)
|
||||||
|
- Directional shadow `max_distance` at 20–30 (shadows beyond this use lower
|
||||||
|
resolution)
|
||||||
|
|
||||||
|
### Q: I see a sphere artifact on my ReflectionProbe
|
||||||
|
|
||||||
|
The ReflectionProbe has a `probe_mode` property — if set to **Reflection Probe**
|
||||||
|
(sphere mode), it renders as a visible sphere in reflections. Change to **Box
|
||||||
|
Probe** for interior spaces, or disable reflection visibility via the `visible`
|
||||||
|
property on the probe's MeshInstance3D child (Godot 4.x).
|
||||||
|
|
||||||
|
### Q: Can I have a day/night cycle?
|
||||||
|
|
||||||
|
No. LightmapGI bakes lighting statically. Dynamic lighting is limited to
|
||||||
|
≤ 4 real-time lights. Day/night cycles are outside the current scope.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
### Q: The validator says "No MeshInstance3D nodes found"
|
||||||
|
|
||||||
|
Your map is using CSG nodes that haven't been converted to meshes. The
|
||||||
|
validator counts mesh triangles — CSG nodes are scanned as MeshInstance3D
|
||||||
|
after conversion. Either:
|
||||||
|
1. Convert CSG → Mesh for the area you want validated (right-click → Convert CSG to Mesh)
|
||||||
|
2. This warning is expected during early construction stages
|
||||||
|
|
||||||
|
### Q: The validator fails with "Scene cannot be instantiated"
|
||||||
|
|
||||||
|
The scene depends on resources that don't exist in the template project. This
|
||||||
|
usually happens when the map references game assets from the main project.
|
||||||
|
Either:
|
||||||
|
1. Run the validator from the **main game project** (`--path client` with the
|
||||||
|
main `client/project.godot`), or
|
||||||
|
2. Strip external dependencies — the map should be self-contained
|
||||||
|
|
||||||
|
### Q: The validator reports texture errors but my textures are 1024×1024
|
||||||
|
|
||||||
|
Check for textures with different aspect ratios. A 2048×512 texture fails
|
||||||
|
because the larger dimension (2048) exceeds 1024. Resize or tile.
|
||||||
|
|
||||||
|
### Q: Can I skip certain validator checks?
|
||||||
|
|
||||||
|
Not directly. Run the validator, fix errors, then fix warnings. If a check
|
||||||
|
produces a false positive, the `@tool` scripts are in
|
||||||
|
`client/tools/validate_map/` — you can edit the constants (e.g.
|
||||||
|
`MAX_TOTAL_TRIANGLES`) for custom pipelines.
|
||||||
|
|
||||||
|
## Packaging
|
||||||
|
|
||||||
|
### Q: The .pck file is very large
|
||||||
|
|
||||||
|
The `.pck` includes all dependencies of the scene. Large files mean:
|
||||||
|
- High-resolution textures (use ≤ 1024×1024 per the budget)
|
||||||
|
- High-poly meshes (stay under 50K triangles)
|
||||||
|
- Embedded audio or other large assets
|
||||||
|
|
||||||
|
Run the validator first to check budgets.
|
||||||
|
|
||||||
|
### Q: I get "Failed to load pack" in-game
|
||||||
|
|
||||||
|
Possible causes:
|
||||||
|
- `.pck` file is corrupted (check SHA-256 checksum)
|
||||||
|
- The map scene or a dependency path changed between packaging and loading
|
||||||
|
- The `.pck` was built with a different Godot version than the game
|
||||||
|
- The resource path in the `.pck` conflicts with a path in the base game
|
||||||
|
|
||||||
|
**Solution:** Re-package the map from the same Godot version the game uses.
|
||||||
|
Verify with `curl` that the download matches the server's checksum.
|
||||||
|
|
||||||
|
### Q: Can I update a map without changing the client version?
|
||||||
|
|
||||||
|
Yes. The `.pck` is additive content. Update the `.pck` on the registry server
|
||||||
|
and increment the `version` field in the `.json` metadata. The client will
|
||||||
|
re-download when it notices the version mismatch.
|
||||||
|
|
||||||
|
### Q: Can I host the registry server on a different port?
|
||||||
|
|
||||||
|
Yes. Use `--port <port>` or the `MAP_REGISTRY_PORT` environment variable.
|
||||||
|
The client's `MapDownloader` has a `registry_url` property you can set.
|
||||||
|
|
||||||
|
### Q: Can I load more than one .pck at a time?
|
||||||
|
|
||||||
|
Yes. `ProjectSettings.load_resource_pack()` is additive. Multiple `.pck` files
|
||||||
|
can be loaded simultaneously — each adds its resources to the global namespace.
|
||||||
|
|
||||||
|
### Q: The registry server returns 404 for my map
|
||||||
|
|
||||||
|
- Check that the `.pck` file is in the maps directory (default `./packed_maps/`)
|
||||||
|
- Check the file extension is `.pck` (lowercase)
|
||||||
|
- The server auto-scans every 5 seconds — wait or send SIGHUP to force a scan
|
||||||
|
- Verify by listing: `curl http://localhost:8090/maps`
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
### Q: Can I make a hostage rescue map?
|
||||||
|
|
||||||
|
The SDK is gameplay-agnostic. The groups `ct_spawn`, `t_spawn`, `bomb_site`,
|
||||||
|
and `buy_zone` are the default set. You can add your own groups (e.g.,
|
||||||
|
`hostage`, `hostage_zone`) — the game logic discovers them at runtime.
|
||||||
|
Check the game's plugin API documentation for custom group support.
|
||||||
|
|
||||||
|
### Q: Can I use this SDK for a different game?
|
||||||
|
|
||||||
|
The SDK is designed for Tactical Shooter's Godot project structure. The
|
||||||
|
packaging and validation scripts are reusable for any Godot map-authoring
|
||||||
|
workflow, but the group conventions and gameplay nodes are game-specific.
|
||||||
|
|
||||||
|
### Q: The editor validator doesn't run
|
||||||
|
|
||||||
|
The `template_map.gd` script uses `@tool` and checks `Engine.is_editor_hint()`.
|
||||||
|
Make sure:
|
||||||
|
1. The script is attached to the root node of the scene
|
||||||
|
2. You're editing the scene in the Godot editor (not running the game)
|
||||||
|
3. The Output panel is visible (Window → Output)
|
||||||
|
|
||||||
|
### Q: How do I report a bug in the SDK?
|
||||||
|
|
||||||
|
Open an issue on the project's Gitea repository. Include:
|
||||||
|
- SDK version (commit hash)
|
||||||
|
- Godot version
|
||||||
|
- Steps to reproduce
|
||||||
|
- Log output (editor or CLI validator)
|
||||||
|
|
||||||
|
### Q: Can I contribute validator modules?
|
||||||
|
|
||||||
|
Yes. See [Validation — Adding a Custom Validator Module](04-validation.md#adding-a-custom-validator-module).
|
||||||
|
Submit a pull request with your module and updated documentation.
|
||||||
Reference in New Issue
Block a user