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:
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user