Phase 7: netfox + godot-jolt stack upgrade

Stack installed:
- netfox v1.35.3 (core + extras + noray + internals)
- godot-jolt v0.16.0-stable

Architecture:
- Server: ENet transport (works headless, no netfox deps)
- Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator)

New/modified:
- docs/migration-netfox-plan.md — migration architecture
- scripts/network/network_manager.gd — netfox-aware ENet fallback
- scripts/network/player.gd — clean base player
- client/characters/player_netfox.gd — rollback player w/ WeaponManager
- client/characters/input/player_net_input.gd — BaseNetInput subclass
- client/characters/character/fps_character_controller.gd — netfox input feed
- client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager
- client/scripts/round_replicator.gd — client-side round state bridge
- server/scripts/round_manager.gd — improved state machine
- server/scripts/plugin_api/plugin_manager.gd — refined plugin system
- config: enemy_tag, ally_tag for meatball targeting

Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
This commit is contained in:
2026-07-02 17:38:50 -04:00
parent e2dc429caa
commit e7299b17e9
3237 changed files with 523530 additions and 18 deletions
+181
View File
@@ -0,0 +1,181 @@
# Tactical Shooter — Map Template
A standalone Godot 4 project for building custom competitive maps.
## Quick Start
1. **Copy this directory**`cp -r map_template/ maps/my_custom_map/`
2. **Rename `template_map.tscn`** — e.g. `de_dust2.tscn`
3. **Open the new directory in Godot 4** — add the map template project
4. **Build geometry** — use CSG brushes or instance modular kit pieces
5. **Place gameplay prefabs** — spawn points, bomb sites, buy zones
6. **Bake lighting** — select LightmapGI → "Bake Lightmap"
7. **Package & ship** — use the PCK pipeline to publish
## Directory Structure
```
map_template/
├── project.godot # Standalone Godot project config
├── template_map.tscn # Demo map scene (copy & rename)
├── template_map.gd # Editor tool script (auto-validates map)
└── assets/
└── prefabs/
├── ct_spawn.tscn # Counter-Terrorist spawn point
├── t_spawn.tscn # Terrorist spawn point
├── buy_zone.tscn # Purchase zone area
├── bomb_site.tscn # Bomb plant site area
├── cubemap_origin.tscn # Reflection probe origin
└── map_bounds.tscn # Out-of-bounds kill wall
```
## Prefab Reference
### Node Type Conventions
All gameplay nodes are discovered by **Godot groups**, not by node name.
Add nodes to these groups for game logic to find them:
| Group | Node Type | Purpose |
|-------|-----------|---------|
| `ct_spawn` | Marker3D | CT team spawn position |
| `t_spawn` | Marker3D | Terrorist team spawn position |
| `buy_zone` | Area3D | Purchase-eligible zone |
| `bomb_site` | Area3D | Bomb plant zone |
| `cubemap_origin` | Node3D | Reflection cubemap capture point |
| `map_bounds` | Area3D | Out-of-bounds kill wall |
**Additional groups (informational, set by map author):**
- `bombsite_a` / `bombsite_b` — sub-identify which bomb site
- `ct_buy` / `t_buy` — team-specific buy zones (if separated)
### ct_spawn.tscn
- **Type:** Marker3D (green pad with +Z direction arrow)
- **Group:** `ct_spawn`
- **Usage:** Place on floor, one per player slot (usually 5). The +Z arrow
indicates the direction players face on spawn. Add child Node3Ds for
additional spawn positions.
- **Customisation:** Duplicate instances rather than editing the prefab.
Adjust position and rotation only — the pad is cosmetic.
### t_spawn.tscn
- **Type:** Marker3D (red pad with +Z direction arrow)
- **Group:** `t_spawn`
- **Same usage as ct_spawn** but for the Terrorist team.
### buy_zone.tscn
- **Type:** Area3D with CollisionShape3D
- **Group:** `buy_zone`
- **Usage:** Place to define an area where players can purchase items.
Resize the child CollisionShape3D to match the room. The Area3D
monitors body_entered/body_exited to activate/deactivate the buy menu.
- **Visual:** Semi-transparent yellow box as editor reference.
### bomb_site.tscn
- **Type:** Area3D with CollisionShape3D
- **Group:** `bomb_site`
- **Usage:** Place to define where the bomb can be planted.
Resize the CollisionShape3D to cover the site. Rename the instance
"BombsiteA" or "BombsiteB" and add the sub-group (bombsite_a / bombsite_b).
- **Visual:** Semi-transparent orange floor panel + CSG subtraction hint
for plantable ground area.
### cubemap_origin.tscn
- **Type:** Node3D with small blue marker
- **Group:** `cubemap_origin`
- **Usage:** Mark the ideal position for the ReflectionProbe cubemap
capture. Place at eye height (1.6 units above floor) in the most
visually prominent part of the map.
- **Note:** The ReflectionProbe itself is a separate node — place it at
this marker's position after geometry is finalised.
### map_bounds.tscn
- **Type:** Area3D with CollisionShape3D
- **Group:** `map_bounds`
- **Usage:** Invisible kill wall. Place one per perimeter side, resize
to form a closed box around the playable area. Detects body_exited
to teleport/kill out-of-bounds players.
- **Visual:** Semi-transparent red wall as editor reference.
## Building a Map — Checklist
### Essential
- [ ] Floor geometry (CSGBox3D or modular kit tiles)
- [ ] Wall geometry enclosing the playable area
- [ ] Ceiling or skybox boundary
- [ ] Collision on all CSG brushes (`use_collision = true`)
### Gameplay
- [ ] CT spawn points for each player (group `ct_spawn`)
- [ ] T spawn points for each player (group `t_spawn`)
- [ ] Two bomb sites, each with group `bomb_site`
- [ ] Buy zones at each spawn area (group `buy_zone`)
- [ ] Map bounds perimeter walls (group `map_bounds`)
### Lighting
- [ ] WorldEnvironment with sky/ambient
- [ ] DirectionalLight3D (sun) with shadows enabled
- [ ] Fill/ambient OmniLight3D(s)
- [ ] ReflectionProbe at cubemap origin
- [ ] LightmapGI configured with quality=2, bounces=3
- [ ] Lightmap baked (select LightmapGI → "Bake Lightmap")
### Validation
- [ ] Open template_map.tscn in Godot editor — the tool script auto-runs
- [ ] Check Output panel (bottom dock) for validation results
- [ ] All checks pass (green ✓)
- [ ] For headless validation, use the main project's validator from `client/`:
`godot --script scripts/validate_scene.gd -- --scene=res://maps/my_map.tscn`
## CSG Building Guide
Godot's CSG (Constructive Solid Geometry) nodes let you build map geometry
directly in the editor. Key shortcuts:
- **W** — Move tool
- **E** — Rotate tool
- **R** — Scale tool
- **Ctrl+D** — Duplicate selected node
- **Ctrl+Shift+C** — Bake LightmapGI
CSG node types used in this template:
- `CSGBox3D` — Basic wall/floor/ceiling blocks
- `CSGCombiner3D` — Grouped geometry (container, non-collidable parent)
- `CSGSphere3D` / `CSGCylinder3D` — Curved shapes
CSG operations:
| Operation | Effect |
|-----------|--------|
| Union (0) | Adds to existing CSG (default) |
| Subtraction (1) | Cuts hole through parent CSG |
| Intersection (2) | Keeps only overlapping volume |
**Performance rule:** Use as few CSG nodes as possible. For detailed maps,
bake CSG to a MeshInstance3D via Scene → "Convert CSG to Mesh" when
geometry is finalised.
## Integrating with the Main Project
When your map is complete:
1. **Copy your .tscn + assets** into the main project at `client/maps/<map_name>/`
2. **Pack as .pck:** `cd client && godot --headless --pack --export-pack maps/<map_name>.pck res://maps/<map_name>.tscn`
3. **Register with master server** via POST `/api/v1/maps/register`
4. **Test in-game:** connect to server and `changelevel <map_name>`
## Limitations
- **Standalone preview:** Direct CSG rendering in this template uses basic
placeholder materials. Final maps should use the main project's modular
kit assets and PBR materials.
- **Lightmap baking:** Godot 4.7 LightmapGI baking is editor-only. Open
this project in the editor to bake.
- **Non-Euclidean geometry:** Not supported by CSG. Use MeshInstance3D
for complex shapes.
@@ -0,0 +1,36 @@
[gd_scene load_steps=4 format=3]
[sub_resource type="StandardMaterial3D" id="SiteMat"]
albedo_color = Color(1.0, 0.65, 0.0, 0.3)
metallic = 0.0
roughness = 0.5
transparency = 0.7
flags_transparent = true
[sub_resource type="BoxShape3D" id="SiteShape"]
size = Vector3(6.0, 3.0, 6.0)
[node name="BombSite" type="Area3D"]
groups = ["bomb_site"]
; Bomb site — the bomb can be planted within this area.
; Resize the CollisionShape3D to cover the site's playable area.
; Name the node "BombsiteA" or "BombsiteB" for game logic identification.
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SiteShape")
[node name="Visualizer" type="CSGBox3D" parent="."]
size = Vector3(6.0, 0.04, 6.0)
material = SubResource("SiteMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.02, 0)
; Semi-transparent orange floor panel to visualise the site in the editor.
; Adjust to match the site's floor area.
[node name="PlaneZone" type="CSGBox3D" parent="BombSite"]
operation = 1
size = Vector3(5.5, 0.5, 5.5)
material = SubResource("SiteMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
; Invisible CSG subtraction hint — marks the plantable ground area.
@@ -0,0 +1,28 @@
[gd_scene load_steps=4 format=3]
[sub_resource type="StandardMaterial3D" id="ZoneMat"]
albedo_color = Color(0.8, 0.8, 0.2, 0.3)
metallic = 0.0
roughness = 0.5
transparency = 0.7
flags_transparent = true
[sub_resource type="CapsuleShape3D" id="ZoneShape"]
radius = 2.0
height = 4.0
[node name="BuyZone" type="Area3D"]
groups = ["buy_zone"]
; Buy zone — players inside this Area3D can purchase weapons/equipment.
; Resize the CollisionShape3D child to match the room.
; Monitor player bodies with area_entered / area_exited signals.
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("ZoneShape")
[node name="Visualizer" type="CSGBox3D" parent="."]
size = Vector3(4.0, 3.0, 4.0)
material = SubResource("ZoneMat")
use_collision = false
; Semi-transparent yellow box to visualise the zone in the editor.
; Disabled in-game — the game uses the Area3D collision, not this visualiser.
@@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3]
[sub_resource type="StandardMaterial3D" id="SpawnMat"]
albedo_color = Color(0.2, 0.4, 1.0, 1.0)
metallic = 0.0
roughness = 0.8
[node name="CTSpawn" type="Marker3D"]
groups = ["ct_spawn"]
; CT spawn point — players on Counter-Terrorist team spawn here.
; Place on the floor surface. The +Z arrow indicates spawn forward direction.
; Child Node3Ds can be added for additional spawn positions.
[node name="SpawnPad" type="CSGBox3D" parent="."]
size = Vector3(0.5, 0.03, 0.5)
material = SubResource("SpawnMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.015, 0)
@@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=3]
[sub_resource type="StandardMaterial3D" id="OriginMat"]
albedo_color = Color(0.5, 0.8, 1.0, 0.5)
metallic = 0.3
roughness = 0.4
transparency = 0.5
flags_transparent = true
[node name="CubemapOrigin" type="Node3D"]
groups = ["cubemap_origin"]
; Cubemap reflection origin — position for ReflectionProbe cubemap capture.
; The ReflectionProbe should be placed here after map lighting is finalised.
; Place at eye height (1.6 units above floor) in the most visually prominent
; area of the map for best reflection results.
[node name="Marker" type="CSGBox3D" parent="."]
material = SubResource("OriginMat")
size = Vector3(0.2, 0.2, 0.2)
use_collision = false
; Small translucent blue cube as visual reference.
@@ -0,0 +1,27 @@
[gd_scene load_steps=4 format=3]
[sub_resource type="StandardMaterial3D" id="BoundMat"]
albedo_color = Color(1.0, 0.0, 0.0, 0.2)
metallic = 0.0
roughness = 1.0
transparency = 0.8
flags_transparent = true
[sub_resource type="BoxShape3D" id="BoundShape"]
size = Vector3(2.0, 4.0, 200.0)
[node name="MapBound" type="Area3D"]
groups = ["map_bounds"]
; Map boundary wall — prevents players from exiting the playable area.
; Copy this prefab for each side of the map, rotating and resizing
; the CollisionShape3D to form a closed perimeter.
; MapBounds detect body_exited and teleport/kill out-of-bounds players.
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("BoundShape")
[node name="Visualizer" type="CSGBox3D" parent="."]
size = Vector3(2.0, 3.8, 200.0)
material = SubResource("BoundMat")
use_collision = false
; Semi-transparent red wall visualiser. Resize to match perimeter.
@@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3]
[sub_resource type="StandardMaterial3D" id="SpawnMat"]
albedo_color = Color(1.0, 0.3, 0.15, 1.0)
metallic = 0.0
roughness = 0.8
[node name="TSpawn" type="Marker3D"]
groups = ["t_spawn"]
; Terrorist spawn point — players on Terrorist team spawn here.
; Place on the floor surface. The +Z arrow indicates spawn forward direction.
; Child Node3Ds can be added for additional spawn positions.
[node name="SpawnPad" type="CSGBox3D" parent="."]
size = Vector3(0.5, 0.03, 0.5)
material = SubResource("SpawnMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.015, 0)
+202
View File
@@ -0,0 +1,202 @@
@tool
extends Node3D
# Validates template map structure in the editor output panel.
#
# Run this in the Godot editor:
# 1. Open template_map.tscn
# 2. Look at the Output panel (bottom dock) for validation results
#
# Checks:
# - Required gameplay node groups are present
# - Spawn points exist for both teams
# - Bomb sites are defined
# - Buy zones are present
# - LightmapGI is configured
#
# Groups used by map nodes:
# ct_spawn — Counter-Terrorist spawn positions
# t_spawn — Terrorist spawn positions
# buy_zone — Purchase zone areas
# bomb_site — Bomb plant site areas (+ bombsite_a / bombsite_b for ID)
# cubemap_origin — Reflection cubemap capture origin
# map_bounds — Out-of-bounds kill walls
#
# Game logic discovers these at runtime by scanning for nodes with
# the corresponding group, not by node name.
func _ready():
if not Engine.is_editor_hint():
return
print("")
print("=== Map Template: Validate Scene ===")
print("")
var checks = []
# --- Required node types by group ---
var groups_to_check = {
"ct_spawn": "CT Spawn points",
"t_spawn": "T Spawn points",
"buy_zone": "Buy zones",
"bomb_site": "Bomb sites",
"cubemap_origin": "Cubemap capture origins",
"map_bounds": "Map boundary walls",
}
for group in groups_to_check:
var nodes = get_tree().get_nodes_in_group(group)
var label = groups_to_check[group]
if nodes.size() > 0:
checks.append([str(" + ", label, " (", nodes.size(), " found)"), true])
else:
checks.append([str(" - ", label, " — NONE FOUND"), false])
# --- CSG floor check ---
var floor_nodes = _find_csg_floor()
checks.append([str(" + CSG Floor geometry (", floor_nodes.size(), " nodes)"), floor_nodes.size() > 0])
# --- Wall check ---
var wall_count = _find_csg_walls()
checks.append([str(" + CSG Wall geometry (", wall_count, " walls)"), wall_count >= 3])
# --- LightmapGI check ---
var lightmap = _find_lightmap_gi()
if lightmap:
checks.append([" + LightmapGI configured ✓", true])
if lightmap.light_data != null:
checks.append([" + LightmapGI data: BAKED ✓", true])
else:
checks.append([" - LightmapGI: NOT YET BAKED", false])
checks.append([str(" + LightmapGI.bounces = ", lightmap.bounces), lightmap.bounces >= 2])
checks.append([str(" + LightmapGI.texel_scale = ", lightmap.texel_scale), lightmap.texel_scale <= 2.0])
else:
checks.append([" - LightmapGI — NOT FOUND", false])
# --- ReflectionProbe check ---
var probe = _find_reflection_probe()
checks.append([" + ReflectionProbe present", probe != null])
# --- WorldEnvironment check ---
var env = _find_world_env()
checks.append([" + WorldEnvironment present", env != null])
# --- SunLight check ---
var sun = _find_dir_light()
checks.append([" + DirectionalLight3D (sun) present", sun != null])
# --- Map scale / playable area estimate ---
var extents = _estimate_floor_extents(floor_nodes)
if extents:
checks.append([str(" + Playable area: ~", extents[0], "x", extents[1], " units"), true])
# Print results
print(" ─── Validation Results ───")
var passed = 0
var failed = 0
for check in checks:
if check[1]:
passed += 1
else:
failed += 1
print(check[0])
print("")
print(" Passed: ", passed, " Failed: ", failed)
print("")
if failed > 0:
print(" NOTE: ", failed, " check(s) did not pass.")
print(" See warnings above for details — these are advisory,")
print(" the map will still function but may be missing critical nodes.")
else:
print(" All checks passed. Map template is ready!")
print("")
print(" === Validation complete ===")
print("")
func _find_nodes_by_group(group_name: String) -> Array:
return get_tree().get_nodes_in_group(group_name)
func _find_csg_floor() -> Array:
"""Return all CSG nodes with floor-like Y position and flat orientation."""
var results = []
for child in get_children():
if child is CSGBox3D or child is CSGCombiner3D:
if abs(child.transform.origin.y) < 0.5:
var s = child
if s is CSGBox3D and s.size.y < 0.3:
results.append(child)
elif s is CSGCombiner3D:
results.append(child)
return results
func _find_csg_walls() -> int:
"""Count CSG box nodes with vertical wall-like dimensions."""
var count = 0
for child in get_children():
if child is CSGBox3D:
var s: CSGBox3D = child
# Wall-like: one thin dimension, tall Y
var dims = [s.size.x, s.size.y, s.size.z]
dims.sort()
if dims[0] < 0.5 and dims[1] > 1.5 and dims[2] > 0.5:
if child.name.begins_with("Wall") or child.name.begins_with("Divider") or child.name.begins_with("Mid"):
count += 1
return count
func _find_lightmap_gi() -> LightmapGI:
for child in get_children():
if child is LightmapGI:
return child
return null
func _find_reflection_probe() -> ReflectionProbe:
for child in get_children():
if child is ReflectionProbe:
return child
return null
func _find_world_env() -> WorldEnvironment:
for child in get_children():
if child is WorldEnvironment:
return child
return null
func _find_dir_light() -> DirectionalLight3D:
for child in get_children():
if child is DirectionalLight3D:
return child
return null
func _estimate_floor_extents(floor_nodes: Array) -> Array:
"""Estimate playable area width and depth from flat CSGBox3D nodes."""
var min_x = INF
var max_x = -INF
var min_z = INF
var max_z = -INF
for node in floor_nodes:
if node is CSGBox3D:
var p = node.transform.origin
var s = node.size
# Approximate bounding box using position + half-extents
min_x = min(min_x, p.x - s.x * 0.5)
max_x = max(max_x, p.x + s.x * 0.5)
min_z = min(min_z, p.z - s.z * 0.5)
max_z = max(max_z, p.z + s.z * 0.5)
if min_x != INF and max_x != INF and min_z != INF and max_z != INF:
return [int(max_x - min_x), int(max_z - min_z)]
return null
+356
View File
@@ -0,0 +1,356 @@
[gd_scene load_steps=32 format=3]
; === Prefab references ===
[ext_resource type="PackedScene" path="res://assets/prefabs/ct_spawn.tscn" id="1"]
[ext_resource type="PackedScene" path="res://assets/prefabs/t_spawn.tscn" id="2"]
[ext_resource type="PackedScene" path="res://assets/prefabs/buy_zone.tscn" id="3"]
[ext_resource type="PackedScene" path="res://assets/prefabs/bomb_site.tscn" id="4"]
[ext_resource type="PackedScene" path="res://assets/prefabs/cubemap_origin.tscn" id="5"]
[ext_resource type="PackedScene" path="res://assets/prefabs/map_bounds.tscn" id="6"]
; === Script ===
[ext_resource type="Script" path="res://template_map.gd" id="7"]
; === Subresources — Materials ===
[sub_resource type="StandardMaterial3D" id="FloorMat"]
albedo_color = Color(0.3, 0.3, 0.32, 1.0)
metallic = 0.0
roughness = 0.9
[sub_resource type="StandardMaterial3D" id="WallMat"]
albedo_color = Color(0.55, 0.55, 0.58, 1.0)
metallic = 0.0
roughness = 0.85
[sub_resource type="StandardMaterial3D" id="TrimMat"]
albedo_color = Color(0.25, 0.25, 0.28, 1.0)
metallic = 0.3
roughness = 0.6
[sub_resource type="StandardMaterial3D" id="FloorTrimMat"]
albedo_color = Color(0.2, 0.2, 0.22, 1.0)
metallic = 0.1
roughness = 0.7
[sub_resource type="StandardMaterial3D" id="SiteAFloorMat"]
albedo_color = Color(0.45, 0.35, 0.25, 1.0)
metallic = 0.0
roughness = 0.9
[sub_resource type="StandardMaterial3D" id="SiteBFloorMat"]
albedo_color = Color(0.3, 0.35, 0.45, 1.0)
metallic = 0.0
roughness = 0.9
[sub_resource type="StandardMaterial3D" id="PillarMat"]
albedo_color = Color(0.4, 0.4, 0.42, 1.0)
metallic = 0.2
roughness = 0.7
; === Subresources — Environment ===
[sub_resource type="Environment" id="Env"]
background_mode = 0
tonemap_mode = 0
glow_enabled = false
ambient_light_color = Color(0.2, 0.22, 0.25, 1.0)
ambient_light_energy = 0.35
ambient_light_sky_contribution = 0.0
; ============================
; === SHOWROOM DEMO MAP ===
; ============================
; This template demonstrates a 3-lane layout with:
; - CSG floor, walls, pillars, and dividers
; - CT spawn (left) and T spawn (right) with buy zones
; - Bomb sites A (CT-side) and B (T-side)
; - Cubemap origin for reflections
; - Map bounds around the perimeter
;
; Copy this .tscn + assets/prefabs/ to start a new map.
; Replace CSG geometry with the modular kit pieces from the main project.
[node name="TemplateMap" type="Node3D"]
script = ExtResource("7")
; ============ FLOOR ============
; Main floor — CSG box covering the entire playable area
; Subdivided into functional zones with different floor materials
[node name="Floor" type="CSGBox3D" parent="."]
size = Vector3(20.0, 0.08, 16.0)
material = SubResource("FloorMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
; Bombsite A floor — warmer tones (CT side, middle-left)
[node name="SiteAFloor" type="CSGCombiner3D" parent="."]
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0, 0, -3.0)
[node name="Base" type="CSGBox3D" parent="SiteAFloor"]
operation = 0
size = Vector3(5.0, 0.09, 5.0)
material = SubResource("SiteAFloorMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.005, 0)
; Bombsite B floor — cooler tones (T side, middle-right)
[node name="SiteBFloor" type="CSGCombiner3D" parent="."]
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.0, 0, -3.0)
[node name="Base" type="CSGBox3D" parent="SiteBFloor"]
operation = 0
size = Vector3(5.0, 0.09, 5.0)
material = SubResource("SiteBFloorMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.005, 0)
; Floor trim — baseboard around perimeter
; North wall trim
[node name="FloorTrim_N" type="CSGBox3D" parent="."]
size = Vector3(20.0, 0.24, 0.08)
material = SubResource("FloorTrimMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.04, -8.04)
; South wall trim
[node name="FloorTrim_S" type="CSGBox3D" parent="."]
size = Vector3(20.0, 0.24, 0.08)
material = SubResource("FloorTrimMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.04, 8.04)
; West wall trim
[node name="FloorTrim_W" type="CSGBox3D" parent="."]
size = Vector3(0.08, 0.24, 16.0)
material = SubResource("FloorTrimMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -10.04, 0.04, 0)
; East wall trim
[node name="FloorTrim_E" type="CSGBox3D" parent="."]
size = Vector3(0.08, 0.24, 16.0)
material = SubResource("FloorTrimMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10.04, 0.04, 0)
; Center divider wall (floor-level trim)
[node name="DividerTrim" type="CSGBox3D" parent="."]
size = Vector3(0.08, 0.32, 8.0)
material = SubResource("TrimMat")
use_collision = false
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.04, 1.0)
; ============ WALLS ============
; --- North wall (Z = -8) ---
[node name="Wall_N" type="CSGBox3D" parent="."]
size = Vector3(20.0, 3.0, 0.16)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, -8.0)
; --- South wall (Z = 8) ---
[node name="Wall_S" type="CSGBox3D" parent="."]
size = Vector3(20.0, 3.0, 0.16)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.5, 8.0)
; --- West wall (X = -10) ---
[node name="Wall_W" type="CSGBox3D" parent="."]
size = Vector3(0.16, 3.0, 16.0)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -10.0, 1.5, 0)
; --- East wall (X = 10) ---
[node name="Wall_E" type="CSGBox3D" parent="."]
size = Vector3(0.16, 3.0, 16.0)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 10.0, 1.5, 0)
; ============ INTERIOR DIVIDERS ============
; CT-side cover wall — partial wall to create a corridor
[node name="Divider_CT" type="CSGBox3D" parent="."]
size = Vector3(0.16, 2.4, 4.0)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6.0, 1.2, -4.0)
; T-side cover wall
[node name="Divider_T" type="CSGBox3D" parent="."]
size = Vector3(0.16, 2.4, 4.0)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.0, 1.2, -4.0)
; Mid divider — lane separator running from CT area toward T area
[node name="MidWall" type="CSGBox3D" parent="."]
size = Vector3(0.16, 2.0, 6.0)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0, 1.0)
; Mid divider short — second offset wall for zigzag corridor
[node name="MidWallShort" type="CSGBox3D" parent="."]
size = Vector3(3.0, 2.0, 0.16)
material = SubResource("WallMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.0, 1.0, -3.0)
; ============ PILLARS ============
[node name="Pillar_1" type="CSGBox3D" parent="."]
size = Vector3(0.4, 2.8, 0.4)
material = SubResource("PillarMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.0, 1.4, 2.0)
[node name="Pillar_2" type="CSGBox3D" parent="."]
size = Vector3(0.4, 2.8, 0.4)
material = SubResource("PillarMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.0, 1.4, 2.0)
[node name="Pillar_3" type="CSGBox3D" parent="."]
size = Vector3(0.4, 2.8, 0.4)
material = SubResource("PillarMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -3.0, 1.4, -5.0)
[node name="Pillar_4" type="CSGBox3D" parent="."]
size = Vector3(0.4, 2.8, 0.4)
material = SubResource("PillarMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 3.0, 1.4, -5.0)
; ============ CEILING / SKYBOX BOUNDARY ============
; Ceiling panel (roof) — encloses the room for lightmap baking
[node name="Ceiling" type="CSGBox3D" parent="."]
size = Vector3(20.0, 0.08, 16.0)
material = SubResource("TrimMat")
use_collision = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.0, 0)
; ============ GAMEPLAY PREFABS ============
; --- Spawn points ---
; CT spawns (left side of map)
[node name="CTSpawn1" parent="." instance=ExtResource("1")]
; CT spawn 1 — main position
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8.0, 0.03, -5.0)
[node name="CTSpawn2" parent="." instance=ExtResource("1")]
; CT spawn 2 — staggered offset for multi-player spawn
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8.0, 0.03, -3.0)
[node name="CTSpawn3" parent="." instance=ExtResource("1")]
; CT spawn 3
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8.0, 0.03, -1.0)
; T spawns (right side of map)
[node name="TSpawn1" parent="." instance=ExtResource("2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8.0, 0.03, 5.0)
[node name="TSpawn2" parent="." instance=ExtResource("2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8.0, 0.03, 3.0)
[node name="TSpawn3" parent="." instance=ExtResource("2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8.0, 0.03, 1.0)
; --- Buy zones ---
[node name="CTBuyZone" parent="." instance=ExtResource("3")]
; Buy zone at CT spawn — players can buy in this area
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8.0, 1.5, -3.0)
; Resize the CollisionShape3D child to cover the CT spawn area
[node name="TBuyZone" parent="." instance=ExtResource("3")]
; Buy zone at T spawn
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8.0, 1.5, 3.0)
; --- Bomb sites ---
[node name="BombsiteA" parent="." instance=ExtResource("4")]
; Bomb site A — CT-side area (warm floor zone)
groups = ["bomb_site"]
; Additional group "bombsite_a" for game logic identification
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.0, 0.5, -3.0)
; Resize Child/CollisionShape3D to match the brown floor zone (5x5)
[node name="BombsiteB" parent="." instance=ExtResource("4")]
; Bomb site B — T-side area (cool floor zone)
groups = ["bomb_site", "bombsite_b"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.0, 0.5, -3.0)
; Resize Child/CollisionShape3D to match the blue floor zone (5x5)
; --- Cubemap origin ---
[node name="CubemapOrigin" parent="." instance=ExtResource("5")]
; Reflection probe capture position at eye height in the middle lane
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0)
; --- Map bounds ---
; Perimeter boundary walls — extend slightly beyond visible walls
[node name="Bound_N" parent="." instance=ExtResource("6")]
; North bounds
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, -10.0)
[node name="Bound_S" parent="." instance=ExtResource("6")]
; South bounds
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0, 10.0)
[node name="Bound_W" parent="." instance=ExtResource("6")]
; West bounds — rotate 90°
transform = Transform3D(0, 0, -1, 0, 1, 0, 1, 0, 0, -12.0, 2.0, 0)
[node name="Bound_E" parent="." instance=ExtResource("6")]
; East bounds — rotate 90°
transform = Transform3D(0, 0, 1, 0, 1, 0, -1, 0, 0, 12.0, 2.0, 0)
; ============ LIGHTING ============
; WorldEnvironment — sky and ambient light
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Env")
; Directional light — key light from high angle (slightly warm)
[node name="SunLight" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866, 0, -0.5, -0.354, 0.707, -0.612, 0.354, 0.707, 0.612, 0, 5, 0)
light_energy = 1.0
light_indirect_energy = 0.8
light_color = Color(1.0, 0.95, 0.9, 1.0)
shadow_enabled = true
light_bake_mode = 2
directional_shadow_max_distance = 30.0
directional_shadow_split_1 = 0.1
directional_shadow_split_2 = 0.3
directional_shadow_split_3 = 0.6
directional_shadow_blend_splits = true
; Fill light — warm interior ambient from the center
[node name="FillLight" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, -2.0)
light_energy = 0.3
light_indirect_energy = 0.4
light_color = Color(1.0, 0.85, 0.7, 1.0)
light_bake_mode = 2
omni_range = 8.0
omni_attenuation = 0.8
shadow_enabled = false
; Reflection probe — interior specular reflections, positioned at cubemap origin
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.6, 0)
box_projection = true
interior = true
extents = Vector3(10.0, 1.8, 8.0)
intensity = 1.0
max_distance = 15.0
; LightmapGI — baked global illumination
[node name="LightmapGI" type="LightmapGI" parent="."]
quality = 2
bounces = 3
bounce_indirect_energy = 1.0
texel_scale = 1.0
max_texture_size = 2048
use_denoiser = true
interior = true