feat: implement procedural arena generation system and support infrastructure
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://debug/dust2_level_builder.gd" id="1"]
|
||||
|
||||
[node name="Dust2Level" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,5 @@
|
||||
[map]
|
||||
name="Dust 2"
|
||||
scene_path="res://scenes/maps/dust2/dust2_level.tscn"
|
||||
color1=Color(0.9, 0.8, 0.6, 1.0)
|
||||
color2=Color(0.6, 0.5, 0.3, 1.0)
|
||||
@@ -0,0 +1,380 @@
|
||||
[gd_scene load_steps=10 format=3 uid="uid://93f273e01255"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/maps/level_runtime.gd" id="1_rt"]
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_grey"]
|
||||
albedo_color = Color(0.3, 0.3, 0.35, 1)
|
||||
roughness = 0.7
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_orange"]
|
||||
albedo_color = Color(0.8, 0.4, 0.1, 1)
|
||||
roughness = 0.7
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_blue"]
|
||||
albedo_color = Color(0.1, 0.4, 0.8, 1)
|
||||
roughness = 0.7
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dark_pit"]
|
||||
albedo_color = Color(0.1, 0.1, 0.1, 1)
|
||||
roughness = 0.7
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_glass"]
|
||||
albedo_color = Color(0.2, 0.8, 0.9, 0.3)
|
||||
roughness = 0.1
|
||||
transparency = 1
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_xxxx"]
|
||||
sky_top_color = Color(0.385, 0.454, 0.55, 1)
|
||||
sky_horizon_color = Color(0.646, 0.656, 0.67, 1)
|
||||
|
||||
[sub_resource type="Sky" id="Sky_xxxx"]
|
||||
sky_material = SubResource("ProceduralSkyMaterial_xxxx")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_xxxx"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_xxxx")
|
||||
tonemap_mode = 2
|
||||
glow_enabled = true
|
||||
|
||||
[node name="MapRoot" type="CSGCombiner3D"]
|
||||
use_collision = true
|
||||
script = ExtResource("1_rt")
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_xxxx")
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 10, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45, 4, 0)
|
||||
|
||||
[node name="Spawn2" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45, 4, 0)
|
||||
|
||||
[node name="Spawn3" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45, 2, -45)
|
||||
|
||||
[node name="Spawn4" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45, 2, -45)
|
||||
|
||||
[node name="Spawn5" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -20, 2, -15)
|
||||
|
||||
[node name="Spawn6" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 20, 2, -15)
|
||||
|
||||
[node name="Spawn7" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -30, 8, 0)
|
||||
|
||||
[node name="Spawn8" type="Marker3D" parent="." groups=["spawn_points"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 30, 8, 0)
|
||||
|
||||
[node name="FloorBound" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -2, 0)
|
||||
size = Vector3(120, 4, 120)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="CeilingBound" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 30, 0)
|
||||
size = Vector3(120, 2, 120)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WallN" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 14, -60)
|
||||
size = Vector3(120, 32, 2)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WallS" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 14, 60)
|
||||
size = Vector3(120, 32, 2)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WallE" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 60, 14, 0)
|
||||
size = Vector3(2, 32, 120)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WallW" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -60, 14, 0)
|
||||
size = Vector3(2, 32, 120)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="CenterPlat" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(20, 2, 20)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="CenterHole" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(8, 4, 8)
|
||||
material = SubResource("StandardMaterial3D_dark_pit")
|
||||
operation = 2
|
||||
|
||||
[node name="LowerPit" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -4, 0)
|
||||
size = Vector3(30, 10, 30)
|
||||
material = SubResource("StandardMaterial3D_dark_pit")
|
||||
operation = 2
|
||||
|
||||
[node name="LowerPitFloor" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -8, 0)
|
||||
size = Vector3(30, 2, 30)
|
||||
material = SubResource("StandardMaterial3D_dark_pit")
|
||||
|
||||
[node name="PillarC1" type="CSGCylinder3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 2, -8)
|
||||
radius = 1
|
||||
height = 10
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="PillarC2" type="CSGCylinder3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, -8)
|
||||
radius = 1
|
||||
height = 10
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="PillarC3" type="CSGCylinder3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -8, 2, 8)
|
||||
radius = 1
|
||||
height = 10
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="PillarC4" type="CSGCylinder3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 8)
|
||||
radius = 1
|
||||
height = 10
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="BridgeN" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, -18)
|
||||
size = Vector3(6, 1, 16)
|
||||
material = SubResource("StandardMaterial3D_blue")
|
||||
|
||||
[node name="BridgeS" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 18)
|
||||
size = Vector3(6, 1, 16)
|
||||
material = SubResource("StandardMaterial3D_blue")
|
||||
|
||||
[node name="BridgeE" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 18, 6, 0)
|
||||
size = Vector3(16, 1, 6)
|
||||
material = SubResource("StandardMaterial3D_blue")
|
||||
|
||||
[node name="BridgeW" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -18, 6, 0)
|
||||
size = Vector3(16, 1, 6)
|
||||
material = SubResource("StandardMaterial3D_blue")
|
||||
|
||||
[node name="GrappleAnchor1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.7071067811865476, 0, -0.7071067811865476, 0, 1, 0, 0.7071067811865476, 0, 0.7071067811865476, -10, 20, -10)
|
||||
size = Vector3(2, 2, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="GrappleAnchor2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.7071067811865476, 0, -0.7071067811865476, 0, 1, 0, 0.7071067811865476, 0, 0.7071067811865476, 10, 20, 10)
|
||||
size = Vector3(2, 2, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="GrappleAnchor3" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.7071067811865476, 0, -0.7071067811865476, 0, 1, 0, 0.7071067811865476, 0, 0.7071067811865476, 10, 20, -10)
|
||||
size = Vector3(2, 2, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="GrappleAnchor4" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.7071067811865476, 0, -0.7071067811865476, 0, 1, 0, 0.7071067811865476, 0, 0.7071067811865476, -10, 20, 10)
|
||||
size = Vector3(2, 2, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="WalkwayN" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, -30)
|
||||
size = Vector3(70, 1, 10)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WalkwayS" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 30)
|
||||
size = Vector3(70, 1, 10)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WalkwayE" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 30, 6, 0)
|
||||
size = Vector3(10, 1, 70)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WalkwayW" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -30, 6, 0)
|
||||
size = Vector3(10, 1, 70)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="AbyssPit" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -4, 45)
|
||||
size = Vector3(120, 10, 30)
|
||||
material = SubResource("StandardMaterial3D_dark_pit")
|
||||
operation = 2
|
||||
|
||||
[node name="AbyssFloor" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -8, 45)
|
||||
size = Vector3(120, 2, 30)
|
||||
material = SubResource("StandardMaterial3D_dark_pit")
|
||||
|
||||
[node name="WallRun1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.984807753012208, 0, 0.17364817766693033, 0, 1, 0, -0.17364817766693033, 0, 0.984807753012208, -30, 8, 40)
|
||||
size = Vector3(24, 16, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="WallRun2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.984807753012208, 0, -0.17364817766693033, 0, 1, 0, 0.17364817766693033, 0, 0.984807753012208, 0, 10, 50)
|
||||
size = Vector3(24, 16, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="WallRun3" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.984807753012208, 0, 0.17364817766693033, 0, 1, 0, -0.17364817766693033, 0, 0.984807753012208, 30, 12, 40)
|
||||
size = Vector3(24, 16, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="TunnelWallBlock" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, -45)
|
||||
size = Vector3(120, 8, 30)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="TunnelCutMain" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, -45)
|
||||
size = Vector3(120, 5, 10)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
operation = 2
|
||||
|
||||
[node name="SlideBulkhead1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -20, 3.5, -45)
|
||||
size = Vector3(4, 4, 10)
|
||||
material = SubResource("StandardMaterial3D_blue")
|
||||
|
||||
[node name="SlideBulkhead2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 20, 3.5, -45)
|
||||
size = Vector3(4, 4, 10)
|
||||
material = SubResource("StandardMaterial3D_blue")
|
||||
|
||||
[node name="RampNW" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9366721892483976, 0.35020738125946743, 0, -0.35020738125946743, 0.9366721892483976, -30, 3, -20)
|
||||
size = Vector3(10, 16, 1)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampNE" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9366721892483976, 0.35020738125946743, 0, -0.35020738125946743, 0.9366721892483976, 30, 3, -20)
|
||||
size = Vector3(10, 16, 1)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampSW" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9366721892483976, -0.35020738125946743, 0, 0.35020738125946743, 0.9366721892483976, -30, 3, 20)
|
||||
size = Vector3(10, 16, 1)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampSE" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.9366721892483976, -0.35020738125946743, 0, 0.35020738125946743, 0.9366721892483976, 30, 3, 20)
|
||||
size = Vector3(10, 16, 1)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampEN" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.9366721892483976, 0.35020738125946743, 0, -0.35020738125946743, 0.9366721892483976, 0, 0, 0, 1, 20, 3, -30)
|
||||
size = Vector3(1, 16, 10)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampES" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.9366721892483976, 0.35020738125946743, 0, -0.35020738125946743, 0.9366721892483976, 0, 0, 0, 1, 20, 3, 30)
|
||||
size = Vector3(1, 16, 10)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampWN" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.9366721892483976, -0.35020738125946743, 0, 0.35020738125946743, 0.9366721892483976, 0, 0, 0, 1, -20, 3, -30)
|
||||
size = Vector3(1, 16, 10)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="RampWS" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(0.9366721892483976, -0.35020738125946743, 0, 0.35020738125946743, 0.9366721892483976, 0, 0, 0, 1, -20, 3, 30)
|
||||
size = Vector3(1, 16, 10)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="EastBase" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45, 4, 0)
|
||||
size = Vector3(20, 8, 30)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="EastBaseRoom" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45, 5, 0)
|
||||
size = Vector3(18, 6, 28)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
operation = 2
|
||||
|
||||
[node name="EastBaseWindow" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 35, 6, 0)
|
||||
size = Vector3(2, 4, 16)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
operation = 2
|
||||
|
||||
[node name="EastBaseRoof" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 45, 16, 0)
|
||||
size = Vector3(20, 1, 30)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WestBase" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45, 4, 0)
|
||||
size = Vector3(20, 8, 30)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="WestBaseRoom" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45, 5, 0)
|
||||
size = Vector3(18, 6, 28)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
operation = 2
|
||||
|
||||
[node name="WestBaseWindow" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -35, 6, 0)
|
||||
size = Vector3(2, 4, 16)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
operation = 2
|
||||
|
||||
[node name="WestBaseRoof" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -45, 16, 0)
|
||||
size = Vector3(20, 1, 30)
|
||||
material = SubResource("StandardMaterial3D_grey")
|
||||
|
||||
[node name="CoverNE1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 2, -15)
|
||||
size = Vector3(2, 4, 8)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverNE2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, 2, -12)
|
||||
size = Vector3(6, 4, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverNW1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 2, -15)
|
||||
size = Vector3(2, 4, 8)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverNW2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12, 2, -12)
|
||||
size = Vector3(6, 4, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverSE1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 15, 2, 15)
|
||||
size = Vector3(2, 4, 8)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverSE2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12, 2, 12)
|
||||
size = Vector3(6, 4, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverSW1" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -15, 2, 15)
|
||||
size = Vector3(2, 4, 8)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
[node name="CoverSW2" type="CSGBox3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12, 2, 12)
|
||||
size = Vector3(6, 4, 2)
|
||||
material = SubResource("StandardMaterial3D_orange")
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[map]
|
||||
name="FPS Blockout"
|
||||
scene_path="res://scenes/maps/fps_blockout/fps_blockout.tscn"
|
||||
color1=Color(0.2, 0.2, 0.2, 1.0)
|
||||
color2=Color(0.4, 0.45, 0.5, 1.0)
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env -S godot --headless --script
|
||||
extends SceneTree
|
||||
|
||||
func _init():
|
||||
print("Generating Map programmatically...")
|
||||
var root = CSGCombiner3D.new()
|
||||
root.name = "MapRoot"
|
||||
root.use_collision = true
|
||||
|
||||
# 1. Base Geometry
|
||||
var floor = CSGBox3D.new()
|
||||
floor.name = "Floor"
|
||||
floor.size = Vector3(36, 0.5, 36)
|
||||
floor.position = Vector3(0, -0.25, 0)
|
||||
root.add_child(floor)
|
||||
floor.owner = root
|
||||
|
||||
# Outer Shell Walls
|
||||
var top_wall = CSGBox3D.new()
|
||||
top_wall.name = "TopWall"
|
||||
top_wall.size = Vector3(36, 3, 0.5)
|
||||
top_wall.position = Vector3(0, 1.5, -17.75)
|
||||
root.add_child(top_wall)
|
||||
top_wall.owner = root
|
||||
|
||||
var bot_wall = CSGBox3D.new()
|
||||
bot_wall.name = "BottomWall"
|
||||
bot_wall.size = Vector3(36, 3, 0.5)
|
||||
bot_wall.position = Vector3(0, 1.5, 17.75)
|
||||
root.add_child(bot_wall)
|
||||
bot_wall.owner = root
|
||||
|
||||
var left_wall = CSGBox3D.new()
|
||||
left_wall.name = "LeftWall"
|
||||
left_wall.size = Vector3(0.5, 3, 35)
|
||||
left_wall.position = Vector3(-17.75, 1.5, 0)
|
||||
root.add_child(left_wall)
|
||||
left_wall.owner = root
|
||||
|
||||
var right_wall = CSGBox3D.new()
|
||||
right_wall.name = "RightWall"
|
||||
right_wall.size = Vector3(0.5, 3, 35)
|
||||
right_wall.position = Vector3(17.75, 1.5, 0)
|
||||
root.add_child(right_wall)
|
||||
right_wall.owner = root
|
||||
|
||||
# Inner blocks and subtractions...
|
||||
_create_room_block(root, "TopLeft", Vector3(-9, 1.5, -9), Vector3(1, 0, 0), Vector3(0, 0, 1))
|
||||
_create_room_block(root, "TopRight", Vector3(9, 1.5, -9), Vector3(-1, 0, 0), Vector3(0, 0, 1))
|
||||
_create_room_block(root, "BottomLeft", Vector3(-9, 1.5, 9), Vector3(1, 0, 0), Vector3(0, 0, -1))
|
||||
_create_room_block(root, "BottomRight", Vector3(9, 1.5, 9), Vector3(-1, 0, 0), Vector3(0, 0, -1))
|
||||
|
||||
var packed = PackedScene.new()
|
||||
var result = packed.pack(root)
|
||||
if result == OK:
|
||||
var err = ResourceSaver.save(packed, "res://scenes/maps/fps_blockout.tscn")
|
||||
if err == OK:
|
||||
print("Map saved successfully to res://scenes/maps/fps_blockout.tscn")
|
||||
else:
|
||||
print("Error saving map: ", err)
|
||||
else:
|
||||
print("Error packing scene: ", result)
|
||||
|
||||
quit()
|
||||
|
||||
func _create_room_block(root: CSGCombiner3D, block_name: String, pos: Vector3, door_horizontal_dir: Vector3, door_vertical_dir: Vector3):
|
||||
var block = CSGBox3D.new()
|
||||
block.name = block_name + "Block"
|
||||
block.size = Vector3(10, 3, 10)
|
||||
block.position = pos
|
||||
root.add_child(block)
|
||||
block.owner = root
|
||||
|
||||
var room = CSGBox3D.new()
|
||||
room.name = block_name + "Room"
|
||||
room.operation = CSGShape3D.OPERATION_SUBTRACTION
|
||||
room.size = Vector3(6, 3, 6)
|
||||
room.position = pos
|
||||
root.add_child(room)
|
||||
room.owner = root
|
||||
|
||||
var door_h = CSGBox3D.new()
|
||||
door_h.name = block_name + "DoorH"
|
||||
door_h.operation = CSGShape3D.OPERATION_SUBTRACTION
|
||||
# For horizontal doors (East/West), x is the long dimension going through the wall
|
||||
door_h.size = Vector3(3.0, 3.0, 2.5)
|
||||
door_h.position = pos + door_horizontal_dir * 3.75
|
||||
root.add_child(door_h)
|
||||
door_h.owner = root
|
||||
|
||||
var door_v = CSGBox3D.new()
|
||||
door_v.name = block_name + "DoorV"
|
||||
door_v.operation = CSGShape3D.OPERATION_SUBTRACTION
|
||||
# For vertical doors (North/South), z is the long dimension going through the wall
|
||||
door_v.size = Vector3(2.5, 3.0, 3.0)
|
||||
door_v.position = pos + door_vertical_dir * 3.75
|
||||
root.add_child(door_v)
|
||||
door_v.owner = root
|
||||
@@ -0,0 +1 @@
|
||||
uid://rwsrtgh08bhb
|
||||
@@ -0,0 +1,498 @@
|
||||
extends Node3D
|
||||
class_name LevelRuntime
|
||||
|
||||
var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _grapple_icon: TextureRect
|
||||
var _dash_icon: TextureRect
|
||||
var _grapple_label: Label
|
||||
var _dash_label: Label
|
||||
var _fps_label: Label
|
||||
var _player: CharacterBody3D
|
||||
var _debug_ui_panel: PanelContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Hide mouse
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
var spawner = MultiplayerSpawner.new()
|
||||
spawner.name = "PlayerSpawner"
|
||||
spawner.spawn_path = "." # Spawn directly as children of the level
|
||||
spawner.spawn_function = _spawn_player
|
||||
add_child(spawner)
|
||||
|
||||
var nm = get_node_or_null("/root/NetworkManager")
|
||||
if nm and nm.multiplayer.is_server():
|
||||
# Spawn existing players
|
||||
for pid in nm.player_stats.keys():
|
||||
spawner.spawn(pid)
|
||||
|
||||
# Spawn late joiners
|
||||
nm.player_connected.connect(func(pid):
|
||||
spawner.spawn(pid)
|
||||
)
|
||||
|
||||
# Despawn on disconnect
|
||||
nm.player_disconnected.connect(func(pid):
|
||||
var p = get_node_or_null(str(pid))
|
||||
if p:
|
||||
p.queue_free()
|
||||
)
|
||||
|
||||
|
||||
# ── Spawning ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func _get_dynamic_spawn_position() -> Vector3:
|
||||
var spawn_nodes = get_tree().get_nodes_in_group("spawn_points")
|
||||
if spawn_nodes.is_empty():
|
||||
return Vector3(0, 10, 0)
|
||||
|
||||
var all_players = []
|
||||
for p in get_tree().get_nodes_in_group("players"):
|
||||
if is_instance_valid(p) and p is Node3D:
|
||||
all_players.append(p)
|
||||
|
||||
if all_players.is_empty():
|
||||
var pos = spawn_nodes.pick_random().global_position
|
||||
pos += Vector3(randf_range(-1, 1), 0.5, randf_range(-1, 1))
|
||||
return pos
|
||||
|
||||
var best_spawn = spawn_nodes[0].global_position
|
||||
var max_min_dist = -1.0
|
||||
|
||||
for spawn_node in spawn_nodes:
|
||||
var pos = spawn_node.global_position
|
||||
var min_dist_to_player = 999999.0
|
||||
for p in all_players:
|
||||
var dist = pos.distance_to(p.global_position)
|
||||
if dist < min_dist_to_player:
|
||||
min_dist_to_player = dist
|
||||
|
||||
if min_dist_to_player > max_min_dist:
|
||||
max_min_dist = min_dist_to_player
|
||||
best_spawn = pos
|
||||
|
||||
# Add slight jitter to prevent exact stacking
|
||||
best_spawn += Vector3(randf_range(-1, 1), 0.5, randf_range(-1, 1))
|
||||
return best_spawn
|
||||
|
||||
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
var player := CharacterBody3D.new()
|
||||
player.name = str(pid)
|
||||
player.set_multiplayer_authority(pid)
|
||||
player.add_to_group("players")
|
||||
|
||||
# Dynamic Spawning
|
||||
player.position = _get_dynamic_spawn_position()
|
||||
|
||||
# Server Synchronizer (Host is the ground truth)
|
||||
var server_sync = MultiplayerSynchronizer.new()
|
||||
server_sync.name = "ServerSynchronizer"
|
||||
server_sync.set_multiplayer_authority(1) # Host always controls these
|
||||
var server_rep_config = SceneReplicationConfig.new()
|
||||
server_rep_config.add_property(":position")
|
||||
server_rep_config.add_property(":synced_movement_state")
|
||||
server_rep_config.add_property(":synced_movement_speed")
|
||||
server_rep_config.add_property(":synced_is_crouching")
|
||||
server_rep_config.add_property(":health")
|
||||
server_rep_config.add_property(":shield")
|
||||
server_rep_config.add_property(":is_dead")
|
||||
server_rep_config.add_property(":synced_grapple_point")
|
||||
server_rep_config.add_property(":synced_is_grapple_shooting")
|
||||
server_sync.replication_config = server_rep_config
|
||||
player.add_child(server_sync)
|
||||
|
||||
# Client Synchronizer (Client dictates their aim and loadout setup)
|
||||
var client_sync = MultiplayerSynchronizer.new()
|
||||
client_sync.name = "MultiplayerSynchronizer"
|
||||
client_sync.set_multiplayer_authority(pid)
|
||||
var client_rep_config = SceneReplicationConfig.new()
|
||||
client_rep_config.add_property(":rotation")
|
||||
client_rep_config.add_property("HeadPivot:rotation")
|
||||
client_rep_config.add_property(":synced_weapon_path")
|
||||
client_rep_config.add_property(":synced_loadout_p1")
|
||||
client_rep_config.add_property(":synced_loadout_p2")
|
||||
client_rep_config.add_property(":synced_loadout_sp")
|
||||
client_rep_config.add_property(":synced_loadout_melee")
|
||||
client_rep_config.add_property(":synced_loadout_ready")
|
||||
client_rep_config.add_property(":synced_is_targeting")
|
||||
client_rep_config.add_property(":synced_homing_target_pos")
|
||||
client_sync.replication_config = client_rep_config
|
||||
player.add_child(client_sync)
|
||||
|
||||
# Collision shape
|
||||
var col_shape := CollisionShape3D.new()
|
||||
col_shape.name = "CollisionShape3D"
|
||||
var capsule := CapsuleShape3D.new()
|
||||
capsule.radius = 0.4
|
||||
capsule.height = 1.8
|
||||
col_shape.shape = capsule
|
||||
player.add_child(col_shape)
|
||||
|
||||
# Apply movement controller script
|
||||
var mover_script = preload("res://movement/player_movement_controller.gd")
|
||||
if mover_script:
|
||||
player.set_script(mover_script)
|
||||
|
||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||
humanoid.name = "HumanoidModel"
|
||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||
player.add_child(humanoid)
|
||||
|
||||
# Movement State Machine
|
||||
var sm := Node.new()
|
||||
sm.name = "MovementStateMachine"
|
||||
player.add_child(sm)
|
||||
|
||||
var sm_script = preload("res://movement/movement_state_machine.gd")
|
||||
if sm_script:
|
||||
sm.set_script(sm_script)
|
||||
|
||||
# Add all movement states
|
||||
var state_scripts := {
|
||||
"ground": "res://movement/states/state_ground.gd",
|
||||
"air": "res://movement/states/state_air.gd",
|
||||
"wall_run": "res://movement/states/state_wall_run.gd",
|
||||
"wall_cling": "res://movement/states/state_wall_cling.gd",
|
||||
"slide": "res://movement/states/state_slide.gd",
|
||||
"dash": "res://movement/states/state_dash.gd",
|
||||
}
|
||||
for state_name in state_scripts:
|
||||
var st := Node.new()
|
||||
st.name = "state_" + state_name
|
||||
sm.add_child(st)
|
||||
var st_script = load(state_scripts[state_name])
|
||||
if st_script:
|
||||
st.set_script(st_script)
|
||||
|
||||
# FPS Camera Rig (HeadPivot → Camera3D)
|
||||
var head_pivot := Node3D.new()
|
||||
head_pivot.name = "HeadPivot"
|
||||
head_pivot.position = Vector3(0, 0.7, 0) # Eye height
|
||||
player.add_child(head_pivot)
|
||||
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "Camera3D"
|
||||
camera.current = (pid == multiplayer.get_unique_id())
|
||||
camera.fov = 90.0
|
||||
head_pivot.add_child(camera)
|
||||
|
||||
# ── Weapon Setup ────────────────────────────────────────────────────────────
|
||||
var wmanager_script = preload("res://weapons/weapon_manager.gd")
|
||||
if wmanager_script:
|
||||
var wman = wmanager_script.new()
|
||||
wman.name = "WeaponManager"
|
||||
wman.player = player
|
||||
wman.camera = camera
|
||||
camera.add_child(wman)
|
||||
|
||||
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
||||
if rig_script:
|
||||
head_pivot.set_script(rig_script)
|
||||
|
||||
# Store original capsule height
|
||||
sm.original_capsule_height = capsule.height
|
||||
|
||||
# ── Dependencies ────────────────────────────────────────────────────────
|
||||
sm.player = player
|
||||
sm.params = player.params
|
||||
head_pivot.params = player.params
|
||||
|
||||
# Authority configuration
|
||||
var is_local = (pid == multiplayer.get_unique_id())
|
||||
if is_local:
|
||||
_player = player
|
||||
player.set_physics_process(true)
|
||||
player.set_process(true)
|
||||
sm.set_physics_process(true)
|
||||
else:
|
||||
player.set_physics_process(false)
|
||||
player.set_process(true)
|
||||
player.set_process_input(false)
|
||||
sm.set_physics_process(false)
|
||||
sm.set_process(false)
|
||||
sm.set_process_input(false)
|
||||
head_pivot.set_process_input(false)
|
||||
head_pivot.set_process(false)
|
||||
head_pivot.set_physics_process(false)
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman:
|
||||
wman.set_process_input(false)
|
||||
wman.set_process(false)
|
||||
wman.set_physics_process(false)
|
||||
wman.visible = false
|
||||
var cv = wman.get("canvas_layer")
|
||||
if cv:
|
||||
cv.visible = false
|
||||
|
||||
return player
|
||||
|
||||
|
||||
# ── HUD ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_hud() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.name = "UI"
|
||||
add_child(canvas)
|
||||
|
||||
# ── Crosshair ─────────────────────────────────────────────────────────
|
||||
var crosshair := Control.new()
|
||||
crosshair.name = "Crosshair"
|
||||
crosshair.set_anchors_preset(Control.PRESET_CENTER)
|
||||
crosshair.custom_minimum_size = Vector2(20, 20)
|
||||
canvas.add_child(crosshair)
|
||||
|
||||
var ch_dot := ColorRect.new()
|
||||
ch_dot.name = "Dot"
|
||||
ch_dot.color = Color(1, 1, 1, 0.8)
|
||||
ch_dot.size = Vector2(4, 4)
|
||||
ch_dot.position = Vector2(-2, -2)
|
||||
crosshair.add_child(ch_dot)
|
||||
|
||||
# Crosshair lines
|
||||
for data in [
|
||||
{"pos": Vector2(-10, -1), "size": Vector2(6, 2)}, # Left
|
||||
{"pos": Vector2(4, -1), "size": Vector2(6, 2)}, # Right
|
||||
{"pos": Vector2(-1, -10), "size": Vector2(2, 6)}, # Top
|
||||
{"pos": Vector2(-1, 4), "size": Vector2(2, 6)}, # Bottom
|
||||
]:
|
||||
var line := ColorRect.new()
|
||||
line.color = Color(1, 1, 1, 0.6)
|
||||
line.position = data["pos"]
|
||||
line.size = data["size"]
|
||||
crosshair.add_child(line)
|
||||
|
||||
# ── Info panel background ─────────────────────────────────────────────
|
||||
_fps_label = Label.new()
|
||||
_fps_label.name = "FPSLabel"
|
||||
_fps_label.text = "FPS: 0"
|
||||
_fps_label.add_theme_font_size_override("font_size", 24)
|
||||
_fps_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
|
||||
_fps_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_fps_label.add_theme_constant_override("outline_size", 4)
|
||||
_fps_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_fps_label.position = Vector2(12, 12)
|
||||
canvas.add_child(_fps_label)
|
||||
|
||||
_debug_ui_panel = PanelContainer.new()
|
||||
_debug_ui_panel.name = "InfoPanel"
|
||||
_debug_ui_panel.offset_left = 12
|
||||
_debug_ui_panel.offset_top = 50
|
||||
_debug_ui_panel.offset_right = 400
|
||||
_debug_ui_panel.offset_bottom = 160
|
||||
var panel_style := StyleBoxFlat.new()
|
||||
panel_style.bg_color = Color(0, 0, 0, 0.55)
|
||||
panel_style.corner_radius_top_left = 8
|
||||
panel_style.corner_radius_top_right = 8
|
||||
panel_style.corner_radius_bottom_left = 8
|
||||
panel_style.corner_radius_bottom_right = 8
|
||||
panel_style.content_margin_left = 12
|
||||
panel_style.content_margin_top = 8
|
||||
panel_style.content_margin_right = 12
|
||||
panel_style.content_margin_bottom = 8
|
||||
_debug_ui_panel.add_theme_stylebox_override("panel", panel_style)
|
||||
canvas.add_child(_debug_ui_panel)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "InfoVBox"
|
||||
_debug_ui_panel.add_child(vbox)
|
||||
|
||||
_speed_label = Label.new()
|
||||
_speed_label.name = "SpeedLabel"
|
||||
_speed_label.text = "Speed: 0.0 m/s"
|
||||
_speed_label.add_theme_font_size_override("font_size", 18)
|
||||
_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
vbox.add_child(_speed_label)
|
||||
|
||||
_state_label = Label.new()
|
||||
_state_label.name = "StateLabel"
|
||||
_state_label.text = "State: ground"
|
||||
_state_label.add_theme_font_size_override("font_size", 16)
|
||||
_state_label.add_theme_color_override("font_color", Color(0.7, 0.85, 1.0))
|
||||
vbox.add_child(_state_label)
|
||||
|
||||
_chain_label = Label.new()
|
||||
_chain_label.name = "ChainLabel"
|
||||
_chain_label.text = "Chain: 0 (+0%)"
|
||||
_chain_label.add_theme_font_size_override("font_size", 16)
|
||||
_chain_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.3))
|
||||
vbox.add_child(_chain_label)
|
||||
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
wp_panel.name = "WeaponPanel"
|
||||
wp_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
wp_panel.offset_left = -250
|
||||
wp_panel.offset_top = -100
|
||||
wp_panel.offset_right = -20
|
||||
wp_panel.offset_bottom = -20
|
||||
var wp_style := StyleBoxFlat.new()
|
||||
wp_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
wp_style.corner_radius_top_left = 8
|
||||
wp_style.corner_radius_top_right = 8
|
||||
wp_style.corner_radius_bottom_left = 8
|
||||
wp_style.corner_radius_bottom_right = 8
|
||||
wp_style.content_margin_left = 16
|
||||
wp_style.content_margin_top = 12
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
_weapon_label.name = "WeaponLabel"
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
_weapon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_weapon_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
# ── Utilities Panel ───────────────────────────────────────────────────────
|
||||
var util_panel := PanelContainer.new()
|
||||
util_panel.name = "UtilPanel"
|
||||
util_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
util_panel.offset_left = -200
|
||||
util_panel.offset_top = -180
|
||||
util_panel.offset_right = -20
|
||||
util_panel.offset_bottom = -110
|
||||
var util_style := StyleBoxFlat.new()
|
||||
util_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
util_style.corner_radius_top_left = 8
|
||||
util_style.corner_radius_top_right = 8
|
||||
util_style.corner_radius_bottom_left = 8
|
||||
util_style.corner_radius_bottom_right = 8
|
||||
util_style.content_margin_left = 12
|
||||
util_style.content_margin_top = 8
|
||||
util_style.content_margin_right = 12
|
||||
util_style.content_margin_bottom = 8
|
||||
util_panel.add_theme_stylebox_override("panel", util_style)
|
||||
util_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(util_panel)
|
||||
|
||||
var util_hbox := HBoxContainer.new()
|
||||
util_hbox.name = "UtilHBox"
|
||||
util_hbox.add_theme_constant_override("separation", 20)
|
||||
util_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_panel.add_child(util_hbox)
|
||||
|
||||
var grapple_vbox := VBoxContainer.new()
|
||||
grapple_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(grapple_vbox)
|
||||
|
||||
_grapple_icon = TextureRect.new()
|
||||
if ResourceLoader.exists("res://assets/ui/grapple_icon.jpg"):
|
||||
_grapple_icon.texture = load("res://assets/ui/grapple_icon.jpg")
|
||||
_grapple_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_grapple_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_grapple_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
grapple_vbox.add_child(_grapple_icon)
|
||||
|
||||
_grapple_label = Label.new()
|
||||
_grapple_label.text = "Grapple"
|
||||
_grapple_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_grapple_label.add_theme_font_size_override("font_size", 12)
|
||||
grapple_vbox.add_child(_grapple_label)
|
||||
|
||||
var dash_vbox := VBoxContainer.new()
|
||||
dash_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(dash_vbox)
|
||||
|
||||
_dash_icon = TextureRect.new()
|
||||
if ResourceLoader.exists("res://assets/ui/dash_icon.jpg"):
|
||||
_dash_icon.texture = load("res://assets/ui/dash_icon.jpg")
|
||||
_dash_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_dash_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_dash_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
dash_vbox.add_child(_dash_icon)
|
||||
|
||||
_dash_label = Label.new()
|
||||
_dash_label.text = "Ready"
|
||||
_dash_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_dash_label.add_theme_font_size_override("font_size", 12)
|
||||
dash_vbox.add_child(_dash_label)
|
||||
|
||||
# ── HUD Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _player or not is_instance_valid(_player):
|
||||
return
|
||||
|
||||
if _debug_ui_panel:
|
||||
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
||||
|
||||
if _fps_label:
|
||||
_fps_label.visible = SettingsManager.show_fps
|
||||
if _fps_label.visible:
|
||||
_fps_label.text = "FPS: %d" % Engine.get_frames_per_second()
|
||||
|
||||
var vel: Vector3 = _player.velocity
|
||||
var hspeed := Vector2(vel.x, vel.z).length()
|
||||
var total_speed := vel.length()
|
||||
|
||||
if _speed_label:
|
||||
_speed_label.text = "Speed: %.1f m/s (total: %.1f)" % [hspeed, total_speed]
|
||||
|
||||
if _state_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_state_label.text = "State: %s" % sm.current_state
|
||||
|
||||
if _chain_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
# Update utility indicators
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
_grapple_icon.modulate = Color(0.2, 1.0, 0.4)
|
||||
_grapple_label.text = "Grappling"
|
||||
else:
|
||||
_grapple_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_grapple_label.text = "Ready"
|
||||
|
||||
if sm.has_method("get_dash_cooldown_remaining"):
|
||||
var dash_rem = sm.get_dash_cooldown_remaining()
|
||||
if dash_rem > 0.0:
|
||||
_dash_icon.modulate = Color(1.0, 0.3, 0.3)
|
||||
_dash_label.text = "%.1f" % dash_rem
|
||||
else:
|
||||
_dash_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_dash_label.text = "Ready"
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
var active_weapon = wman.weapons[wman.active_slot]
|
||||
var w_name = "Weapon"
|
||||
var cur_ammo = 0
|
||||
var max_ammo = 0
|
||||
|
||||
if "weapon_name" in active_weapon:
|
||||
w_name = active_weapon.weapon_name
|
||||
elif active_weapon is DoubleBarrelShotgun:
|
||||
w_name = "Double Barrel Shotgun"
|
||||
|
||||
if "current_ammo" in active_weapon:
|
||||
cur_ammo = active_weapon.current_ammo
|
||||
max_ammo = active_weapon.max_ammo
|
||||
elif "shells" in active_weapon:
|
||||
cur_ammo = active_weapon.shells
|
||||
max_ammo = 2
|
||||
|
||||
if "reloading" in active_weapon and active_weapon.reloading:
|
||||
_weapon_label.text = "%s\nReloading..." % w_name
|
||||
else:
|
||||
_weapon_label.text = "%s\n%d / %d" % [w_name, cur_ammo, max_ammo]
|
||||
else:
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
@@ -0,0 +1 @@
|
||||
uid://bv0ov3bo3p0uu
|
||||
@@ -0,0 +1,221 @@
|
||||
@tool
|
||||
extends Node3D
|
||||
class_name ArenaGenerator
|
||||
|
||||
@export var generate: bool = false:
|
||||
set(value):
|
||||
if value:
|
||||
if not is_inside_tree():
|
||||
call_deferred("_generate_tactical_arena")
|
||||
else:
|
||||
_generate_tactical_arena()
|
||||
generate = false
|
||||
|
||||
@export var clear: bool = false:
|
||||
set(value):
|
||||
if value:
|
||||
_clear_arena()
|
||||
clear = false
|
||||
|
||||
@export var sector_size = 24.0
|
||||
@export var step_height = 3.0
|
||||
@export var wall_height = 12.0
|
||||
|
||||
@export var floor_color = Color(0.2, 0.22, 0.25)
|
||||
@export var wall_color = Color(0.3, 0.4, 0.5)
|
||||
@export var obstacle_color = Color(0.6, 0.35, 0.15)
|
||||
@export var ramp_color = Color(0.7, 0.4, 0.2)
|
||||
|
||||
func _clear_arena() -> void:
|
||||
var children = get_children()
|
||||
for c in children:
|
||||
remove_child(c)
|
||||
c.free()
|
||||
|
||||
func _generate_tactical_arena() -> void:
|
||||
_clear_arena()
|
||||
|
||||
var cols = 3 # Left, Mid, Right
|
||||
var rows = 5 # Spawn A, Conn A, Mid, Conn B, Spawn B
|
||||
|
||||
# Generate elevations (symmetric for fairness)
|
||||
var elevations = []
|
||||
for x in range(cols):
|
||||
elevations.append([])
|
||||
for z in range(rows):
|
||||
elevations[x].append(0)
|
||||
|
||||
# Randomize half, mirror other half
|
||||
for x in range(cols):
|
||||
for z in range(3): # 0, 1, 2
|
||||
elevations[x][z] = randi_range(0, 2)
|
||||
|
||||
# Mirror to the other side (point symmetry)
|
||||
# Or mirror horizontally (Spawn A == Spawn B setup)
|
||||
# A standard mirror across mid:
|
||||
for x in range(cols):
|
||||
elevations[x][3] = elevations[cols - 1 - x][1]
|
||||
elevations[x][4] = elevations[cols - 1 - x][0]
|
||||
|
||||
# Build Sector Floors
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _step_h = float(step_height if step_height != null else 3.0)
|
||||
var _wall_h = float(wall_height if wall_height != null else 12.0)
|
||||
|
||||
var _f_col = floor_color if floor_color != null else Color(0.2, 0.22, 0.25)
|
||||
var _w_col = wall_color if wall_color != null else Color(0.3, 0.4, 0.5)
|
||||
|
||||
var hw = (cols * _sec_size) / 2.0
|
||||
var hl = (rows * _sec_size) / 2.0
|
||||
|
||||
# Global origin is center of grid
|
||||
for x in range(cols):
|
||||
for z in range(rows):
|
||||
var cx = -hw + x * _sec_size + _sec_size / 2.0
|
||||
var cz = -hl + z * _sec_size + _sec_size / 2.0
|
||||
var y = float(elevations[x][z]) * _step_h
|
||||
|
||||
# Make floors incredibly thick (40 units) so they act as solid pillars all the way down
|
||||
# This prevents players from walking underneath higher sectors and falling into the void
|
||||
_create_box("Sector_%d_%d" % [x, z], Vector3(cx, y - 20.0, cz), Vector3(_sec_size, 40.0, _sec_size), _f_col)
|
||||
|
||||
# Add cover in sector (except spawns z=0 and z=4)
|
||||
if z > 0 and z < 4:
|
||||
_generate_cover("Cover_%d_%d" % [x, z], Vector3(cx, y, cz))
|
||||
|
||||
# Ramps between Z adjacent (North/South)
|
||||
if z < rows - 1:
|
||||
var diff = elevations[x][z+1] - elevations[x][z]
|
||||
if diff != 0:
|
||||
_create_ramp_z("RampZ_%d_%d" % [x, z], cx, cz + _sec_size / 2.0, y, diff)
|
||||
|
||||
# Ramps/Walls between X adjacent (East/West)
|
||||
if x < cols - 1:
|
||||
var is_connector = randf() > 0.4 # 60% chance to be an open connector
|
||||
var diff = elevations[x+1][z] - elevations[x][z]
|
||||
|
||||
if is_connector:
|
||||
if diff != 0:
|
||||
_create_ramp_x("RampX_%d_%d" % [x, z], cx + _sec_size / 2.0, cz, y, diff)
|
||||
else:
|
||||
# Solid wall
|
||||
var max_y = max(elevations[x][z], elevations[x+1][z]) * _step_h
|
||||
_create_box("InnerWall_%d_%d" % [x, z], Vector3(cx + _sec_size / 2.0, max_y + _wall_h/2, cz), Vector3(1.0, _wall_h, _sec_size), _w_col)
|
||||
|
||||
# Outer Walls
|
||||
_create_box("Wall_North", Vector3(0, _wall_h * 0.5, -hl), Vector3(hw * 2, _wall_h, 1.0), _w_col)
|
||||
_create_box("Wall_South", Vector3(0, _wall_h * 0.5, hl), Vector3(hw * 2, _wall_h, 1.0), _w_col)
|
||||
_create_box("Wall_East", Vector3(hw, _wall_h * 0.5, 0), Vector3(1.0, _wall_h, hl * 2), _w_col)
|
||||
_create_box("Wall_West", Vector3(-hw, _wall_h * 0.5, 0), Vector3(1.0, _wall_h, hl * 2), _w_col)
|
||||
|
||||
|
||||
func _generate_cover(base_name: String, center: Vector3) -> void:
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _o_col = obstacle_color if obstacle_color != null else Color(0.6, 0.35, 0.15)
|
||||
# Generate 1-3 blocks of cover
|
||||
var count = randi_range(1, 3)
|
||||
for i in range(count):
|
||||
var ox = randf_range(-_sec_size*0.3, _sec_size*0.3)
|
||||
var oz = randf_range(-_sec_size*0.3, _sec_size*0.3)
|
||||
var sx = randf_range(2.0, 6.0)
|
||||
var sy = randf_range(1.0, 4.0)
|
||||
var sz = randf_range(2.0, 6.0)
|
||||
_create_box("%s_%d" % [base_name, i], center + Vector3(ox, sy * 0.5, oz), Vector3(sx, sy, sz), _o_col)
|
||||
|
||||
func _create_ramp_z(node_name: String, cx: float, cz: float, base_y: float, diff_levels: int) -> void:
|
||||
var _step_h = float(step_height if step_height != null else 3.0)
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _r_col = ramp_color if ramp_color != null else Color(0.7, 0.4, 0.2)
|
||||
# Ramp along Z axis (North/South)
|
||||
var h = abs(diff_levels) * _step_h
|
||||
var length = _sec_size * 0.4 # Ramp takes up 40% of sector depth
|
||||
var center_y = base_y + h / 2.0
|
||||
|
||||
var dir = sign(diff_levels)
|
||||
var rot_x = 0
|
||||
if dir > 0:
|
||||
rot_x = rad_to_deg(atan2(h, length))
|
||||
else:
|
||||
rot_x = -rad_to_deg(atan2(h, length))
|
||||
|
||||
var true_length = sqrt(length*length + h*h)
|
||||
_create_ramp(node_name, Vector3(cx, center_y, cz), Vector3(_sec_size * 0.5, 0.5, true_length), Vector3(rot_x, 0, 0), _r_col)
|
||||
|
||||
func _create_ramp_x(node_name: String, cx: float, cz: float, base_y: float, diff_levels: int) -> void:
|
||||
var _step_h = float(step_height if step_height != null else 3.0)
|
||||
var _sec_size = float(sector_size if sector_size != null else 24.0)
|
||||
var _r_col = ramp_color if ramp_color != null else Color(0.7, 0.4, 0.2)
|
||||
# Ramp along X axis (East/West)
|
||||
var h = abs(diff_levels) * _step_h
|
||||
var length = _sec_size * 0.4
|
||||
var center_y = base_y + h / 2.0
|
||||
|
||||
var dir = sign(diff_levels)
|
||||
var rot_z = 0
|
||||
if dir > 0:
|
||||
rot_z = -rad_to_deg(atan2(h, length))
|
||||
else:
|
||||
rot_z = rad_to_deg(atan2(h, length))
|
||||
|
||||
var true_length = sqrt(length*length + h*h)
|
||||
_create_ramp(node_name, Vector3(cx, center_y, cz), Vector3(true_length, 0.5, _sec_size * 0.5), Vector3(0, 0, rot_z), _r_col)
|
||||
|
||||
|
||||
func _create_box(node_name: String, pos: Vector3, size: Vector3, color: Color) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.name = node_name
|
||||
body.position = pos
|
||||
|
||||
var shape := CollisionShape3D.new()
|
||||
shape.name = "CollisionShape3D"
|
||||
shape.shape = BoxShape3D.new()
|
||||
shape.shape.size = size
|
||||
body.add_child(shape)
|
||||
|
||||
var mesh := MeshInstance3D.new()
|
||||
mesh.name = "MeshInstance3D"
|
||||
mesh.mesh = BoxMesh.new()
|
||||
mesh.mesh.size = size
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
mesh.mesh.surface_set_material(0, mat)
|
||||
body.add_child(mesh)
|
||||
|
||||
add_child(body)
|
||||
if Engine.is_editor_hint():
|
||||
var scene_root = get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
body.owner = scene_root
|
||||
shape.owner = scene_root
|
||||
mesh.owner = scene_root
|
||||
|
||||
func _create_ramp(node_name: String, pos: Vector3, size: Vector3, rot_deg: Vector3, color: Color) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.name = node_name
|
||||
body.position = pos
|
||||
body.rotation_degrees = rot_deg
|
||||
|
||||
var shape := CollisionShape3D.new()
|
||||
shape.name = "CollisionShape3D"
|
||||
shape.shape = BoxShape3D.new()
|
||||
shape.shape.size = size
|
||||
body.add_child(shape)
|
||||
|
||||
var mesh := MeshInstance3D.new()
|
||||
mesh.name = "MeshInstance3D"
|
||||
mesh.mesh = BoxMesh.new()
|
||||
mesh.mesh.size = size
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
mat.roughness = 0.8
|
||||
mesh.mesh.surface_set_material(0, mat)
|
||||
body.add_child(mesh)
|
||||
|
||||
add_child(body)
|
||||
if Engine.is_editor_hint():
|
||||
var scene_root = get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
body.owner = scene_root
|
||||
shape.owner = scene_root
|
||||
mesh.owner = scene_root
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxf17ncr1fxd1
|
||||
@@ -0,0 +1,5 @@
|
||||
[map]
|
||||
name="Procedural Arena"
|
||||
scene_path="res://scenes/maps/procedural_arena/procedural_arena.tscn"
|
||||
color1=Color(0.15, 0.4, 0.2, 1.0)
|
||||
color2=Color(0.2, 0.6, 0.3, 1.0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
extends Node3D
|
||||
|
||||
|
||||
var _speed_label: Label
|
||||
var _state_label: Label
|
||||
var _chain_label: Label
|
||||
var _weapon_label: Label
|
||||
var _grapple_icon: TextureRect
|
||||
var _dash_icon: TextureRect
|
||||
var _grapple_label: Label
|
||||
var _dash_label: Label
|
||||
var _fps_label: Label
|
||||
var _player: CharacterBody3D
|
||||
var _debug_ui_panel: PanelContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# Hide mouse
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
_build_hud()
|
||||
|
||||
# Multiplayer Spawning
|
||||
var spawner = MultiplayerSpawner.new()
|
||||
spawner.name = "PlayerSpawner"
|
||||
spawner.spawn_path = "." # Spawn directly as children of the level
|
||||
spawner.spawn_function = _spawn_player
|
||||
add_child(spawner)
|
||||
|
||||
var nm = get_node_or_null("/root/NetworkManager")
|
||||
if nm and nm.multiplayer.is_server():
|
||||
# Spawn existing players
|
||||
for pid in nm.player_stats.keys():
|
||||
spawner.spawn(pid)
|
||||
|
||||
# Spawn late joiners
|
||||
nm.player_connected.connect(func(pid):
|
||||
spawner.spawn(pid)
|
||||
)
|
||||
|
||||
# Despawn on disconnect
|
||||
nm.player_disconnected.connect(func(pid):
|
||||
var p = get_node_or_null(str(pid))
|
||||
if p:
|
||||
p.queue_free()
|
||||
)
|
||||
|
||||
|
||||
# ── Player ────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _spawn_player(pid: int) -> CharacterBody3D:
|
||||
var player := CharacterBody3D.new()
|
||||
player.name = str(pid)
|
||||
player.set_multiplayer_authority(pid)
|
||||
|
||||
# Alternate spawn sides (Team A / Team B style)
|
||||
var spawn_z = 40.0 if (pid % 2 == 0) else -40.0
|
||||
player.position = Vector3(randf_range(-6, 6), 10.0, spawn_z + randf_range(-4, 4))
|
||||
|
||||
# Server Synchronizer (Host is the ground truth)
|
||||
var server_sync = MultiplayerSynchronizer.new()
|
||||
server_sync.name = "ServerSynchronizer"
|
||||
server_sync.set_multiplayer_authority(1) # Host always controls these
|
||||
var server_rep_config = SceneReplicationConfig.new()
|
||||
server_rep_config.add_property(":position")
|
||||
server_rep_config.add_property(":synced_movement_state")
|
||||
server_rep_config.add_property(":synced_movement_speed")
|
||||
server_rep_config.add_property(":synced_is_crouching")
|
||||
server_rep_config.add_property(":health")
|
||||
server_rep_config.add_property(":shield")
|
||||
server_rep_config.add_property(":is_dead")
|
||||
server_rep_config.add_property(":synced_grapple_point")
|
||||
server_rep_config.add_property(":synced_is_grapple_shooting")
|
||||
server_sync.replication_config = server_rep_config
|
||||
player.add_child(server_sync)
|
||||
|
||||
# Client Synchronizer (Client dictates their aim and loadout setup)
|
||||
var client_sync = MultiplayerSynchronizer.new()
|
||||
client_sync.name = "MultiplayerSynchronizer"
|
||||
client_sync.set_multiplayer_authority(pid)
|
||||
var client_rep_config = SceneReplicationConfig.new()
|
||||
client_rep_config.add_property(":rotation")
|
||||
client_rep_config.add_property("HeadPivot:rotation")
|
||||
client_rep_config.add_property(":synced_weapon_path")
|
||||
client_rep_config.add_property(":synced_loadout_p1")
|
||||
client_rep_config.add_property(":synced_loadout_p2")
|
||||
client_rep_config.add_property(":synced_loadout_sp")
|
||||
client_rep_config.add_property(":synced_loadout_melee")
|
||||
client_rep_config.add_property(":synced_loadout_ready")
|
||||
client_rep_config.add_property(":synced_is_targeting")
|
||||
client_rep_config.add_property(":synced_homing_target_pos")
|
||||
client_sync.replication_config = client_rep_config
|
||||
player.add_child(client_sync)
|
||||
|
||||
# Collision shape
|
||||
var col_shape := CollisionShape3D.new()
|
||||
col_shape.name = "CollisionShape3D"
|
||||
var capsule := CapsuleShape3D.new()
|
||||
capsule.radius = 0.4
|
||||
capsule.height = 1.8
|
||||
col_shape.shape = capsule
|
||||
player.add_child(col_shape)
|
||||
|
||||
# Apply movement controller script
|
||||
var mover_script = preload("res://movement/player_movement_controller.gd")
|
||||
if mover_script:
|
||||
player.set_script(mover_script)
|
||||
|
||||
# Humanoid Model for Player (Shadows only locally, visible to others)
|
||||
var humanoid = load("res://characters/humanoid_model.gd").new()
|
||||
humanoid.name = "HumanoidModel"
|
||||
humanoid.color = Color(0.2, 0.4, 0.8) # Blueish for player
|
||||
humanoid.shadows_only = (pid == multiplayer.get_unique_id())
|
||||
humanoid.position = Vector3(0, -0.9, 0) # Offset from center to feet
|
||||
player.add_child(humanoid)
|
||||
|
||||
# Movement State Machine
|
||||
var sm := Node.new()
|
||||
sm.name = "MovementStateMachine"
|
||||
player.add_child(sm)
|
||||
|
||||
var sm_script = preload("res://movement/movement_state_machine.gd")
|
||||
if sm_script:
|
||||
sm.set_script(sm_script)
|
||||
|
||||
# Add all movement states
|
||||
var state_scripts := {
|
||||
"ground": "res://movement/states/state_ground.gd",
|
||||
"air": "res://movement/states/state_air.gd",
|
||||
"wall_run": "res://movement/states/state_wall_run.gd",
|
||||
"wall_cling": "res://movement/states/state_wall_cling.gd",
|
||||
"slide": "res://movement/states/state_slide.gd",
|
||||
"dash": "res://movement/states/state_dash.gd",
|
||||
}
|
||||
for state_name in state_scripts:
|
||||
var st := Node.new()
|
||||
st.name = "state_" + state_name
|
||||
sm.add_child(st)
|
||||
var st_script = load(state_scripts[state_name])
|
||||
if st_script:
|
||||
st.set_script(st_script)
|
||||
|
||||
# FPS Camera Rig (HeadPivot → Camera3D)
|
||||
var head_pivot := Node3D.new()
|
||||
head_pivot.name = "HeadPivot"
|
||||
head_pivot.position = Vector3(0, 0.7, 0) # Eye height
|
||||
player.add_child(head_pivot)
|
||||
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "Camera3D"
|
||||
camera.current = (pid == multiplayer.get_unique_id())
|
||||
camera.fov = 90.0
|
||||
head_pivot.add_child(camera)
|
||||
|
||||
# ── Weapon Setup ────────────────────────────────────────────────────────────
|
||||
var wmanager_script = preload("res://weapons/weapon_manager.gd")
|
||||
if wmanager_script:
|
||||
var wman = wmanager_script.new()
|
||||
wman.name = "WeaponManager"
|
||||
wman.player = player
|
||||
wman.camera = camera
|
||||
camera.add_child(wman)
|
||||
|
||||
var rig_script = preload("res://characters/player/fps_camera_rig.gd")
|
||||
if rig_script:
|
||||
head_pivot.set_script(rig_script)
|
||||
|
||||
# Store original capsule height
|
||||
sm.original_capsule_height = capsule.height
|
||||
|
||||
# ── Dependencies ────────────────────────────────────────────────────────
|
||||
sm.player = player
|
||||
sm.params = player.params
|
||||
head_pivot.params = player.params
|
||||
|
||||
# Authority configuration
|
||||
var is_local = (pid == multiplayer.get_unique_id())
|
||||
if is_local:
|
||||
_player = player
|
||||
player.set_physics_process(true)
|
||||
player.set_process(true)
|
||||
sm.set_physics_process(true)
|
||||
else:
|
||||
player.set_physics_process(false)
|
||||
player.set_process(true)
|
||||
player.set_process_input(false)
|
||||
sm.set_physics_process(false)
|
||||
sm.set_process(false)
|
||||
sm.set_process_input(false)
|
||||
head_pivot.set_process_input(false)
|
||||
head_pivot.set_process(false)
|
||||
head_pivot.set_physics_process(false)
|
||||
var wman = camera.get_node_or_null("WeaponManager")
|
||||
if wman:
|
||||
wman.set_process_input(false)
|
||||
wman.set_process(false)
|
||||
wman.set_physics_process(false)
|
||||
wman.visible = false
|
||||
var cv = wman.get("canvas_layer")
|
||||
if cv:
|
||||
cv.visible = false
|
||||
|
||||
return player
|
||||
|
||||
|
||||
# ── HUD ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
func _build_hud() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.name = "UI"
|
||||
add_child(canvas)
|
||||
|
||||
# ── Crosshair ─────────────────────────────────────────────────────────
|
||||
var crosshair := Control.new()
|
||||
crosshair.name = "Crosshair"
|
||||
crosshair.set_anchors_preset(Control.PRESET_CENTER)
|
||||
crosshair.custom_minimum_size = Vector2(20, 20)
|
||||
canvas.add_child(crosshair)
|
||||
|
||||
var ch_dot := ColorRect.new()
|
||||
ch_dot.name = "Dot"
|
||||
ch_dot.color = Color(1, 1, 1, 0.8)
|
||||
ch_dot.size = Vector2(4, 4)
|
||||
ch_dot.position = Vector2(-2, -2)
|
||||
crosshair.add_child(ch_dot)
|
||||
|
||||
# Crosshair lines
|
||||
for data in [
|
||||
{"pos": Vector2(-10, -1), "size": Vector2(6, 2)}, # Left
|
||||
{"pos": Vector2(4, -1), "size": Vector2(6, 2)}, # Right
|
||||
{"pos": Vector2(-1, -10), "size": Vector2(2, 6)}, # Top
|
||||
{"pos": Vector2(-1, 4), "size": Vector2(2, 6)}, # Bottom
|
||||
]:
|
||||
var line := ColorRect.new()
|
||||
line.color = Color(1, 1, 1, 0.6)
|
||||
line.position = data["pos"]
|
||||
line.size = data["size"]
|
||||
crosshair.add_child(line)
|
||||
|
||||
# ── Info panel background ─────────────────────────────────────────────
|
||||
_fps_label = Label.new()
|
||||
_fps_label.name = "FPSLabel"
|
||||
_fps_label.text = "FPS: 0"
|
||||
_fps_label.add_theme_font_size_override("font_size", 24)
|
||||
_fps_label.add_theme_color_override("font_color", Color(0.9, 0.9, 0.2))
|
||||
_fps_label.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||
_fps_label.add_theme_constant_override("outline_size", 4)
|
||||
_fps_label.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||
_fps_label.position = Vector2(12, 12)
|
||||
canvas.add_child(_fps_label)
|
||||
|
||||
_debug_ui_panel = PanelContainer.new()
|
||||
_debug_ui_panel.name = "InfoPanel"
|
||||
_debug_ui_panel.offset_left = 12
|
||||
_debug_ui_panel.offset_top = 50
|
||||
_debug_ui_panel.offset_right = 400
|
||||
_debug_ui_panel.offset_bottom = 160
|
||||
var panel_style := StyleBoxFlat.new()
|
||||
panel_style.bg_color = Color(0, 0, 0, 0.55)
|
||||
panel_style.corner_radius_top_left = 8
|
||||
panel_style.corner_radius_top_right = 8
|
||||
panel_style.corner_radius_bottom_left = 8
|
||||
panel_style.corner_radius_bottom_right = 8
|
||||
panel_style.content_margin_left = 12
|
||||
panel_style.content_margin_top = 8
|
||||
panel_style.content_margin_right = 12
|
||||
panel_style.content_margin_bottom = 8
|
||||
_debug_ui_panel.add_theme_stylebox_override("panel", panel_style)
|
||||
canvas.add_child(_debug_ui_panel)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "InfoVBox"
|
||||
_debug_ui_panel.add_child(vbox)
|
||||
|
||||
_speed_label = Label.new()
|
||||
_speed_label.name = "SpeedLabel"
|
||||
_speed_label.text = "Speed: 0.0 m/s"
|
||||
_speed_label.add_theme_font_size_override("font_size", 18)
|
||||
_speed_label.add_theme_color_override("font_color", Color(0.3, 1.0, 0.5))
|
||||
vbox.add_child(_speed_label)
|
||||
|
||||
_state_label = Label.new()
|
||||
_state_label.name = "StateLabel"
|
||||
_state_label.text = "State: ground"
|
||||
_state_label.add_theme_font_size_override("font_size", 16)
|
||||
_state_label.add_theme_color_override("font_color", Color(0.7, 0.85, 1.0))
|
||||
vbox.add_child(_state_label)
|
||||
|
||||
_chain_label = Label.new()
|
||||
_chain_label.name = "ChainLabel"
|
||||
_chain_label.text = "Chain: 0 (+0%)"
|
||||
_chain_label.add_theme_font_size_override("font_size", 16)
|
||||
_chain_label.add_theme_color_override("font_color", Color(1.0, 0.8, 0.3))
|
||||
vbox.add_child(_chain_label)
|
||||
|
||||
|
||||
# ── Weapon & Ammo Panel ───────────────────────────────────────────────
|
||||
var wp_panel := PanelContainer.new()
|
||||
wp_panel.name = "WeaponPanel"
|
||||
wp_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
wp_panel.offset_left = -250
|
||||
wp_panel.offset_top = -100
|
||||
wp_panel.offset_right = -20
|
||||
wp_panel.offset_bottom = -20
|
||||
var wp_style := StyleBoxFlat.new()
|
||||
wp_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
wp_style.corner_radius_top_left = 8
|
||||
wp_style.corner_radius_top_right = 8
|
||||
wp_style.corner_radius_bottom_left = 8
|
||||
wp_style.corner_radius_bottom_right = 8
|
||||
wp_style.content_margin_left = 16
|
||||
wp_style.content_margin_top = 12
|
||||
wp_style.content_margin_right = 16
|
||||
wp_style.content_margin_bottom = 12
|
||||
wp_panel.add_theme_stylebox_override("panel", wp_style)
|
||||
wp_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(wp_panel)
|
||||
|
||||
_weapon_label = Label.new()
|
||||
_weapon_label.name = "WeaponLabel"
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
_weapon_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
_weapon_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM
|
||||
_weapon_label.add_theme_font_size_override("font_size", 24)
|
||||
wp_panel.add_child(_weapon_label)
|
||||
|
||||
# ── Utilities Panel ───────────────────────────────────────────────────────
|
||||
var util_panel := PanelContainer.new()
|
||||
util_panel.name = "UtilPanel"
|
||||
util_panel.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
util_panel.offset_left = -200
|
||||
util_panel.offset_top = -180
|
||||
util_panel.offset_right = -20
|
||||
util_panel.offset_bottom = -110
|
||||
var util_style := StyleBoxFlat.new()
|
||||
util_style.bg_color = Color(0, 0, 0, 0.6)
|
||||
util_style.corner_radius_top_left = 8
|
||||
util_style.corner_radius_top_right = 8
|
||||
util_style.corner_radius_bottom_left = 8
|
||||
util_style.corner_radius_bottom_right = 8
|
||||
util_style.content_margin_left = 12
|
||||
util_style.content_margin_top = 8
|
||||
util_style.content_margin_right = 12
|
||||
util_style.content_margin_bottom = 8
|
||||
util_panel.add_theme_stylebox_override("panel", util_style)
|
||||
util_panel.grow_horizontal = Control.GROW_DIRECTION_BEGIN
|
||||
canvas.add_child(util_panel)
|
||||
|
||||
var util_hbox := HBoxContainer.new()
|
||||
util_hbox.name = "UtilHBox"
|
||||
util_hbox.add_theme_constant_override("separation", 20)
|
||||
util_hbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_panel.add_child(util_hbox)
|
||||
|
||||
var grapple_vbox := VBoxContainer.new()
|
||||
grapple_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(grapple_vbox)
|
||||
|
||||
_grapple_icon = TextureRect.new()
|
||||
_grapple_icon.texture = load("res://assets/ui/grapple_icon.jpg")
|
||||
_grapple_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_grapple_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_grapple_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
grapple_vbox.add_child(_grapple_icon)
|
||||
|
||||
_grapple_label = Label.new()
|
||||
_grapple_label.text = "Grapple"
|
||||
_grapple_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_grapple_label.add_theme_font_size_override("font_size", 12)
|
||||
grapple_vbox.add_child(_grapple_label)
|
||||
|
||||
var dash_vbox := VBoxContainer.new()
|
||||
dash_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
util_hbox.add_child(dash_vbox)
|
||||
|
||||
_dash_icon = TextureRect.new()
|
||||
_dash_icon.texture = load("res://assets/ui/dash_icon.jpg")
|
||||
_dash_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
_dash_icon.custom_minimum_size = Vector2(32, 32)
|
||||
_dash_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
dash_vbox.add_child(_dash_icon)
|
||||
|
||||
_dash_label = Label.new()
|
||||
_dash_label.text = "Ready"
|
||||
_dash_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_dash_label.add_theme_font_size_override("font_size", 12)
|
||||
dash_vbox.add_child(_dash_label)
|
||||
|
||||
# ── Utility: Key Name ─────────────────────────────────────────────────────────
|
||||
|
||||
func _get_key_name(action: String) -> String:
|
||||
if not InputMap.has_action(action):
|
||||
return "?"
|
||||
var events = InputMap.action_get_events(action)
|
||||
for e in events:
|
||||
if e is InputEventKey:
|
||||
var code = e.physical_keycode if e.physical_keycode != 0 else e.keycode
|
||||
return OS.get_keycode_string(code)
|
||||
elif e is InputEventMouseButton:
|
||||
if e.button_index == MOUSE_BUTTON_LEFT: return "LClick"
|
||||
elif e.button_index == MOUSE_BUTTON_RIGHT: return "RClick"
|
||||
elif e.button_index == MOUSE_BUTTON_MIDDLE: return "MClick"
|
||||
return "?"
|
||||
|
||||
# ── HUD Update ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _player or not is_instance_valid(_player):
|
||||
return
|
||||
|
||||
if _debug_ui_panel:
|
||||
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
||||
|
||||
if _fps_label:
|
||||
_fps_label.visible = SettingsManager.show_fps
|
||||
if _fps_label.visible:
|
||||
_fps_label.text = "FPS: %d" % Engine.get_frames_per_second()
|
||||
|
||||
var vel: Vector3 = _player.velocity
|
||||
var hspeed := Vector2(vel.x, vel.z).length()
|
||||
var total_speed := vel.length()
|
||||
|
||||
if _speed_label:
|
||||
_speed_label.text = "Speed: %.1f m/s (total: %.1f)" % [hspeed, total_speed]
|
||||
|
||||
if _state_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_state_label.text = "State: %s" % sm.current_state
|
||||
|
||||
if _chain_label:
|
||||
var sm = _player.get_node_or_null("MovementStateMachine")
|
||||
if sm:
|
||||
_chain_label.text = "Chain: %d (+%d%%)" % [sm.chain_count, int(sm.current_chain_bonus * 100)]
|
||||
|
||||
# Update utility indicators
|
||||
if sm.current_state == "grapple" or sm.is_grapple_shooting:
|
||||
_grapple_icon.modulate = Color(0.2, 1.0, 0.4)
|
||||
_grapple_label.text = "Grappling"
|
||||
else:
|
||||
_grapple_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_grapple_label.text = "Ready"
|
||||
|
||||
var dash_rem = sm.get_dash_cooldown_remaining()
|
||||
if dash_rem > 0.0:
|
||||
_dash_icon.modulate = Color(1.0, 0.3, 0.3)
|
||||
_dash_label.text = "%.1f" % dash_rem
|
||||
else:
|
||||
_dash_icon.modulate = Color(1.0, 1.0, 1.0)
|
||||
_dash_label.text = "Ready"
|
||||
|
||||
if _weapon_label:
|
||||
var wman = _player.get_node_or_null("HeadPivot/Camera3D/WeaponManager")
|
||||
if wman and wman.weapons.has(wman.active_slot):
|
||||
var active_weapon = wman.weapons[wman.active_slot]
|
||||
var w_name = "Weapon"
|
||||
var cur_ammo = 0
|
||||
var max_ammo = 0
|
||||
|
||||
if "weapon_name" in active_weapon:
|
||||
w_name = active_weapon.weapon_name
|
||||
elif active_weapon is DoubleBarrelShotgun:
|
||||
w_name = "Double Barrel Shotgun"
|
||||
|
||||
if "current_ammo" in active_weapon:
|
||||
cur_ammo = active_weapon.current_ammo
|
||||
max_ammo = active_weapon.max_ammo
|
||||
elif "shells" in active_weapon:
|
||||
cur_ammo = active_weapon.shells
|
||||
max_ammo = 2
|
||||
|
||||
if "reloading" in active_weapon and active_weapon.reloading:
|
||||
_weapon_label.text = "%s\nReloading..." % w_name
|
||||
else:
|
||||
_weapon_label.text = "%s\n%d / %d" % [w_name, cur_ammo, max_ammo]
|
||||
else:
|
||||
_weapon_label.text = "Unarmed\n0 / 0"
|
||||
@@ -0,0 +1 @@
|
||||
uid://1oenqnfde4yw
|
||||
@@ -0,0 +1,5 @@
|
||||
[map]
|
||||
name="Test Level"
|
||||
scene_path="res://scenes/maps/test_level/test_level.tscn"
|
||||
color1=Color(0.2, 0.2, 0.25, 1.0)
|
||||
color2=Color(0.3, 0.5, 0.6, 1.0)
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://c5k51gbegvxss"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c7ltcn37gfd71" path="res://debug/test_level_builder.gd" id="1"]
|
||||
|
||||
[node name="TestLevel" type="Node3D" unique_id=130984349]
|
||||
script = ExtResource("1")
|
||||
Reference in New Issue
Block a user