mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
refactor(*): DRY up arena scenes, game modes, and HUD instruments
Arenas inherit from a new arena_base.tscn instead of restating ~40 shared lines each; only sky/ambient/glow/tint/decoration vary, exposed via new Arena exports since nested Environment properties aren't overridable through scene inheritance. Bot construction and score-keeping move onto GameMode, shared by match and spectate modes while preserving their differing GameSettings-override behavior and the HUD's score-row duck-typing. HUD instruments share a HudInstrument base for the smoothing-weight calc and angle-lerp helper. Also dedupes MAIN_MENU_SCENE_PATH into ScenePaths and documents why DIFFICULTIES tiers share one checkpoint.
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
[gd_scene load_steps=8 format=3]
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/arena.gd" id="1_iywne"]
|
||||
[ext_resource type="PackedScene" path="res://objects/arena_boundary.tscn" id="2_bndry"]
|
||||
[ext_resource type="PackedScene" uid="uid://cofdcxo5170rs" path="res://objects/goal.tscn" id="6_p57ef"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/arena_base.tscn" id="1_base"]
|
||||
|
||||
[sub_resource type="Shader" id="Shader_stars"]
|
||||
code = "shader_type sky;
|
||||
@@ -27,74 +25,5 @@ void sky() {
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_stars"]
|
||||
shader = SubResource("Shader_stars")
|
||||
|
||||
[sub_resource type="Sky" id="Sky_space"]
|
||||
[node name="Arena" instance=ExtResource("1_base")]
|
||||
sky_material = SubResource("ShaderMaterial_stars")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_space"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_space")
|
||||
ambient_light_source = 2
|
||||
ambient_light_color = Color(0.55, 0.6, 0.75, 1)
|
||||
ambient_light_energy = 0.4
|
||||
glow_enabled = true
|
||||
glow_levels/1 = 0.0
|
||||
glow_levels/2 = 0.4
|
||||
glow_levels/3 = 0.8
|
||||
glow_levels/4 = 0.6
|
||||
glow_levels/5 = 0.2
|
||||
glow_intensity = 0.9
|
||||
glow_strength = 1.2
|
||||
glow_bloom = 0.05
|
||||
glow_blend_mode = 1
|
||||
glow_hdr_threshold = 1.0
|
||||
glow_hdr_scale = 2.0
|
||||
adjustment_enabled = true
|
||||
adjustment_brightness = 1.0
|
||||
adjustment_contrast = 1.05
|
||||
adjustment_saturation = 1.08
|
||||
|
||||
[node name="Arena" type="Node3D"]
|
||||
script = ExtResource("1_iywne")
|
||||
|
||||
[node name="Boundary" parent="." instance=ExtResource("2_bndry")]
|
||||
field_tint = Color(0.5, 0.7, 1, 1)
|
||||
field_intensity = 0.09
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.866025, -0.383022, 0.321394, 0, 0.642788, 0.766044, -0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="FillLight" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(-0.866025, -0.383022, -0.321394, 0, 0.642788, 0.766044, 0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
light_color = Color(0.55, 0.65, 0.85, 1)
|
||||
light_energy = 0.4
|
||||
shadow_enabled = false
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_space")
|
||||
|
||||
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(26, 14, 40)
|
||||
box_projection = true
|
||||
update_mode = 0
|
||||
|
||||
[node name="GoalTeam0" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 18)
|
||||
|
||||
[node name="GoalTeam1" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -18)
|
||||
team = 1
|
||||
|
||||
[node name="BallSpawn" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
|
||||
[node name="SpawnsTeam0" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 13.5)
|
||||
|
||||
[node name="SpawnsTeam1" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -13.5)
|
||||
|
||||
+13
-68
@@ -1,8 +1,6 @@
|
||||
[gd_scene load_steps=16 format=3]
|
||||
[gd_scene load_steps=13 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/arena.gd" id="1_iywne"]
|
||||
[ext_resource type="PackedScene" path="res://objects/arena_boundary.tscn" id="2_bndry"]
|
||||
[ext_resource type="PackedScene" uid="uid://cofdcxo5170rs" path="res://objects/goal.tscn" id="6_p57ef"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/arena_base.tscn" id="1_base"]
|
||||
[ext_resource type="Texture2D" path="res://assets/textures/sky_nebula.png" id="7_nebula"]
|
||||
[ext_resource type="PackedScene" path="res://assets/models/nebula_station.glb" id="8_station"]
|
||||
[ext_resource type="PackedScene" path="res://assets/models/nebula_debris.glb" id="9_debris"]
|
||||
@@ -13,32 +11,6 @@
|
||||
[sub_resource type="PanoramaSkyMaterial" id="PanoramaSkyMaterial_nebula"]
|
||||
panorama = ExtResource("7_nebula")
|
||||
|
||||
[sub_resource type="Sky" id="Sky_nebula"]
|
||||
sky_material = SubResource("PanoramaSkyMaterial_nebula")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_nebula"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_nebula")
|
||||
ambient_light_source = 2
|
||||
ambient_light_color = Color(0.65, 0.35, 0.75, 1)
|
||||
ambient_light_energy = 0.45
|
||||
glow_enabled = true
|
||||
glow_levels/1 = 0.0
|
||||
glow_levels/2 = 0.4
|
||||
glow_levels/3 = 0.8
|
||||
glow_levels/4 = 0.6
|
||||
glow_levels/5 = 0.2
|
||||
glow_intensity = 0.8
|
||||
glow_strength = 1.0
|
||||
glow_bloom = 0.03
|
||||
glow_blend_mode = 1
|
||||
glow_hdr_threshold = 1.1
|
||||
glow_hdr_scale = 2.0
|
||||
adjustment_enabled = true
|
||||
adjustment_brightness = 1.0
|
||||
adjustment_contrast = 1.05
|
||||
adjustment_saturation = 1.08
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_dust"]
|
||||
shader = ExtResource("12_dust_shader")
|
||||
shader_parameter/albedo_tex = ExtResource("10_dust")
|
||||
@@ -75,52 +47,25 @@ scale_min = 0.5
|
||||
scale_max = 1.6
|
||||
color_ramp = SubResource("GradientTexture1D_dust")
|
||||
|
||||
[node name="Arena" type="Node3D"]
|
||||
script = ExtResource("1_iywne")
|
||||
[node name="Arena" instance=ExtResource("1_base")]
|
||||
sky_material = SubResource("PanoramaSkyMaterial_nebula")
|
||||
ambient_light_color = Color(0.65, 0.35, 0.75, 1)
|
||||
ambient_light_energy = 0.45
|
||||
glow_intensity = 0.8
|
||||
glow_strength = 1.0
|
||||
glow_bloom = 0.03
|
||||
glow_hdr_threshold = 1.1
|
||||
|
||||
[node name="Boundary" parent="." instance=ExtResource("2_bndry")]
|
||||
[node name="Boundary" parent="." index="0"]
|
||||
field_tint = Color(0.78, 0.55, 1, 1)
|
||||
field_intensity = 0.1
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.866025, -0.383022, 0.321394, 0, 0.642788, 0.766044, -0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
[node name="DirectionalLight3D" parent="." index="1"]
|
||||
light_color = Color(0.85, 0.75, 1, 1)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="FillLight" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(-0.866025, -0.383022, -0.321394, 0, 0.642788, 0.766044, 0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
[node name="FillLight" parent="." index="2"]
|
||||
light_color = Color(0.55, 0.35, 0.65, 1)
|
||||
light_energy = 0.45
|
||||
shadow_enabled = false
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_nebula")
|
||||
|
||||
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(26, 14, 40)
|
||||
box_projection = true
|
||||
update_mode = 0
|
||||
|
||||
[node name="GoalTeam0" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 18)
|
||||
|
||||
[node name="GoalTeam1" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -18)
|
||||
team = 1
|
||||
|
||||
[node name="BallSpawn" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
|
||||
[node name="SpawnsTeam0" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 13.5)
|
||||
|
||||
[node name="SpawnsTeam1" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -13.5)
|
||||
|
||||
[node name="NebulaDust" type="GPUParticles3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
|
||||
|
||||
@@ -1,84 +1,24 @@
|
||||
[gd_scene load_steps=6 format=3]
|
||||
[gd_scene load_steps=4 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/arena.gd" id="1_iywne"]
|
||||
[ext_resource type="PackedScene" path="res://objects/arena_boundary.tscn" id="2_bndry"]
|
||||
[ext_resource type="PackedScene" uid="uid://cofdcxo5170rs" path="res://objects/goal.tscn" id="6_p57ef"]
|
||||
[ext_resource type="PackedScene" path="res://scenes/arena_base.tscn" id="1_base"]
|
||||
[ext_resource type="Shader" path="res://shaders/sky_asteroid_field.gdshader" id="7_asteroid"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_asteroid"]
|
||||
shader = ExtResource("7_asteroid")
|
||||
|
||||
[sub_resource type="Sky" id="Sky_asteroid"]
|
||||
[node name="Arena" instance=ExtResource("1_base")]
|
||||
sky_material = SubResource("ShaderMaterial_asteroid")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_asteroid"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_asteroid")
|
||||
ambient_light_source = 2
|
||||
ambient_light_color = Color(0.6, 0.45, 0.3, 1)
|
||||
ambient_light_energy = 0.3
|
||||
glow_enabled = true
|
||||
glow_levels/1 = 0.0
|
||||
glow_levels/2 = 0.4
|
||||
glow_levels/3 = 0.8
|
||||
glow_levels/4 = 0.6
|
||||
glow_levels/5 = 0.2
|
||||
glow_intensity = 0.9
|
||||
glow_strength = 1.2
|
||||
glow_bloom = 0.05
|
||||
glow_blend_mode = 1
|
||||
glow_hdr_threshold = 1.0
|
||||
glow_hdr_scale = 2.0
|
||||
adjustment_enabled = true
|
||||
adjustment_brightness = 1.0
|
||||
adjustment_contrast = 1.05
|
||||
adjustment_saturation = 1.08
|
||||
|
||||
[node name="Arena" type="Node3D"]
|
||||
script = ExtResource("1_iywne")
|
||||
|
||||
[node name="Boundary" parent="." instance=ExtResource("2_bndry")]
|
||||
[node name="Boundary" parent="." index="0"]
|
||||
field_tint = Color(1, 0.72, 0.4, 1)
|
||||
field_intensity = 0.09
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.866025, -0.383022, 0.321394, 0, 0.642788, 0.766044, -0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
[node name="DirectionalLight3D" parent="." index="1"]
|
||||
light_color = Color(1, 0.85, 0.65, 1)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="FillLight" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(-0.866025, -0.383022, -0.321394, 0, 0.642788, 0.766044, 0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
[node name="FillLight" parent="." index="2"]
|
||||
light_color = Color(0.55, 0.4, 0.3, 1)
|
||||
light_energy = 0.4
|
||||
shadow_enabled = false
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_asteroid")
|
||||
|
||||
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(26, 14, 40)
|
||||
box_projection = true
|
||||
update_mode = 0
|
||||
|
||||
[node name="GoalTeam0" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 18)
|
||||
|
||||
[node name="GoalTeam1" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -18)
|
||||
team = 1
|
||||
|
||||
[node name="BallSpawn" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
|
||||
[node name="SpawnsTeam0" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 13.5)
|
||||
|
||||
[node name="SpawnsTeam1" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -13.5)
|
||||
|
||||
[node name="Decoration" type="Node3D" parent="."]
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
[gd_scene load_steps=6 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://scripts/arena.gd" id="1_iywne"]
|
||||
[ext_resource type="PackedScene" path="res://objects/arena_boundary.tscn" id="2_bndry"]
|
||||
[ext_resource type="PackedScene" uid="uid://cofdcxo5170rs" path="res://objects/goal.tscn" id="6_p57ef"]
|
||||
|
||||
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_base"]
|
||||
|
||||
[sub_resource type="Sky" id="Sky_base"]
|
||||
sky_material = SubResource("ProceduralSkyMaterial_base")
|
||||
|
||||
[sub_resource type="Environment" id="Environment_base"]
|
||||
background_mode = 2
|
||||
sky = SubResource("Sky_base")
|
||||
ambient_light_source = 2
|
||||
ambient_light_color = Color(0.55, 0.6, 0.75, 1)
|
||||
ambient_light_energy = 0.4
|
||||
glow_enabled = true
|
||||
glow_levels/1 = 0.0
|
||||
glow_levels/2 = 0.4
|
||||
glow_levels/3 = 0.8
|
||||
glow_levels/4 = 0.6
|
||||
glow_levels/5 = 0.2
|
||||
glow_intensity = 0.9
|
||||
glow_strength = 1.2
|
||||
glow_bloom = 0.05
|
||||
glow_blend_mode = 1
|
||||
glow_hdr_threshold = 1.0
|
||||
glow_hdr_scale = 2.0
|
||||
adjustment_enabled = true
|
||||
adjustment_brightness = 1.0
|
||||
adjustment_contrast = 1.05
|
||||
adjustment_saturation = 1.08
|
||||
|
||||
[node name="Arena" type="Node3D"]
|
||||
script = ExtResource("1_iywne")
|
||||
|
||||
[node name="Boundary" parent="." instance=ExtResource("2_bndry")]
|
||||
field_tint = Color(0.5, 0.7, 1, 1)
|
||||
field_intensity = 0.09
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.866025, -0.383022, 0.321394, 0, 0.642788, 0.766044, -0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
shadow_enabled = true
|
||||
|
||||
[node name="FillLight" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(-0.866025, -0.383022, -0.321394, 0, 0.642788, 0.766044, 0.5, -0.663414, 0.55667, 0, 11, 0)
|
||||
light_color = Color(0.55, 0.65, 0.85, 1)
|
||||
light_energy = 0.4
|
||||
shadow_enabled = false
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
environment = SubResource("Environment_base")
|
||||
|
||||
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(26, 14, 40)
|
||||
box_projection = true
|
||||
update_mode = 0
|
||||
|
||||
[node name="GoalTeam0" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 18)
|
||||
|
||||
[node name="GoalTeam1" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -18)
|
||||
team = 1
|
||||
|
||||
[node name="BallSpawn" type="Marker3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
|
||||
[node name="SpawnsTeam0" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 13.5)
|
||||
|
||||
[node name="SpawnsTeam1" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -13.5)
|
||||
+25
-1
@@ -5,12 +5,36 @@ extends Node3D
|
||||
# spawn markers. An arena holds no rules and no state — game modes query it
|
||||
# for spawn transforms and goals, then spawn ships/ball themselves.
|
||||
|
||||
# Per-arena Environment tuning. These live as exports (rather than baked
|
||||
# into each arena's Environment sub-resource) because Godot's scene
|
||||
# inheritance can only override whole top-level node properties, not
|
||||
# properties nested inside a sub-resource — so each arena_0N.tscn overrides
|
||||
# these on the Arena root instead, and _ready() applies them here.
|
||||
@export var sky_material: Material
|
||||
@export var ambient_light_color := Color(0.55, 0.6, 0.75, 1)
|
||||
@export var ambient_light_energy := 0.4
|
||||
@export var glow_intensity := 0.9
|
||||
@export var glow_strength := 1.2
|
||||
@export var glow_bloom := 0.05
|
||||
@export var glow_hdr_threshold := 1.0
|
||||
|
||||
|
||||
func _ready():
|
||||
add_to_group("arena")
|
||||
var world_env := get_node_or_null("WorldEnvironment") as WorldEnvironment
|
||||
if world_env and world_env.environment:
|
||||
VideoSettings.apply_to_environment(world_env.environment)
|
||||
var env := world_env.environment
|
||||
if sky_material:
|
||||
if not env.sky:
|
||||
env.sky = Sky.new()
|
||||
env.sky.sky_material = sky_material
|
||||
env.ambient_light_color = ambient_light_color
|
||||
env.ambient_light_energy = ambient_light_energy
|
||||
env.glow_intensity = glow_intensity
|
||||
env.glow_strength = glow_strength
|
||||
env.glow_bloom = glow_bloom
|
||||
env.glow_hdr_threshold = glow_hdr_threshold
|
||||
VideoSettings.apply_to_environment(env)
|
||||
|
||||
|
||||
func get_ball_spawn() -> Transform3D:
|
||||
|
||||
@@ -11,13 +11,18 @@ extends Node3D
|
||||
@export var ball_scene: PackedScene = preload("res://objects/ball.tscn")
|
||||
|
||||
const CAMERA_RIG_SCENE = preload("res://scenes/ship_camera_rig.tscn")
|
||||
const MAIN_MENU_SCENE_PATH = "res://scenes/main_menu.tscn"
|
||||
|
||||
var arena: Arena
|
||||
var ball: RigidBody3D
|
||||
var ships: Array[Ship] = []
|
||||
var _ship_spawn_transforms := {}
|
||||
|
||||
# Shared by modes that keep score (Match, Spectate); Free Play never
|
||||
# references this or emits a score_changed signal, and HUDController relies
|
||||
# on has_signal("score_changed") to decide whether to show a score row — so
|
||||
# that signal stays declared per-subclass, not here.
|
||||
var score := {0: 0, 1: 0}
|
||||
|
||||
|
||||
func _ready():
|
||||
# Group lets the HUD discover the game mode for timer/score signals
|
||||
@@ -97,6 +102,28 @@ func spawn_camera_rig(target: Ship) -> ShipCameraRig:
|
||||
return rig
|
||||
|
||||
|
||||
# Given an already-resolved (path, reaction_ticks, action_noise) — callers
|
||||
# apply their own GameSettings-override logic first, which differs between
|
||||
# modes (Match lets GameSettings override all three fields, Spectate only
|
||||
# the path) — builds a trained-policy controller, or an inert placeholder if
|
||||
# the path is empty or missing.
|
||||
func _build_opponent(model_path: String, reaction_ticks: int, action_noise: float, label: String = "GameMode") -> ShipController:
|
||||
if not model_path.is_empty() and FileAccess.file_exists(model_path):
|
||||
var bot := AIShipController.new()
|
||||
bot.model_path = model_path
|
||||
bot.reaction_ticks = reaction_ticks
|
||||
bot.action_noise = action_noise
|
||||
return bot
|
||||
if not model_path.is_empty():
|
||||
push_warning("%s: bot model not found at %s, spawning inert opponent" % [label, model_path])
|
||||
return ShipController.new() # inert placeholder
|
||||
|
||||
|
||||
func _record_goal(scoring_team: int) -> void:
|
||||
score[scoring_team] += 1
|
||||
print("Goal for team %d! Score: %d - %d" % [scoring_team, score[0], score[1]])
|
||||
|
||||
|
||||
# Tiny per-reset randomization, well below anything a player would notice as
|
||||
# "not a real kickoff" — just enough that two ships running the identical
|
||||
# deterministic AI policy (action_noise = 0) don't start every kickoff from a
|
||||
@@ -141,4 +168,4 @@ func _reset_body(body: RigidBody3D, to: Transform3D) -> void:
|
||||
|
||||
func _unhandled_input(event):
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
get_tree().change_scene_to_file(MAIN_MENU_SCENE_PATH)
|
||||
get_tree().change_scene_to_file(ScenePaths.MAIN_MENU)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class_name HudAttitudeIndicator
|
||||
extends Control
|
||||
extends HudInstrument
|
||||
|
||||
# Aircraft-style attitude indicator (artificial horizon): the sky/ground disc
|
||||
# rolls and the pitch ladder scrolls behind a fixed ship symbol, so pitch and
|
||||
@@ -14,8 +14,6 @@ const RING_COLOR := Color(0.5, 0.85, 1.0, 0.7)
|
||||
|
||||
# Instrument scale: degrees of pitch visible from centre to rim.
|
||||
const PITCH_RANGE := 45.0
|
||||
# Display smoothing rate (per second); high = snappy, low = floaty.
|
||||
const SMOOTHING := 12.0
|
||||
|
||||
var _target_pitch := 0.0
|
||||
var _target_roll := 0.0
|
||||
@@ -29,16 +27,12 @@ func set_attitude(pitch_deg: float, roll_deg: float) -> void:
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var t := 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
|
||||
var t := _smoothing_weight(delta)
|
||||
_pitch = lerpf(_pitch, _target_pitch, t)
|
||||
_roll = lerp_angle_deg(_roll, _target_roll, t)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
static func lerp_angle_deg(from: float, to: float, weight: float) -> float:
|
||||
return rad_to_deg(lerp_angle(deg_to_rad(from), deg_to_rad(to), weight))
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var center := size / 2.0
|
||||
var radius := minf(size.x, size.y) / 2.0 - 10.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class_name HudGauge
|
||||
extends Control
|
||||
extends HudInstrument
|
||||
|
||||
# Generic bar gauge (vertical or horizontal) with a short label and numeric
|
||||
# readout — used for speed, altitude, and thrust. A dumb display fed by
|
||||
@@ -9,8 +9,6 @@ const BG_COLOR := Color(0.05, 0.07, 0.11, 0.55)
|
||||
const FRAME_COLOR := Color(0.5, 0.85, 1.0, 0.7)
|
||||
const TEXT_COLOR := Color(0.85, 0.92, 1.0, 0.9)
|
||||
|
||||
const SMOOTHING := 12.0
|
||||
|
||||
@export var label := "SPD"
|
||||
@export var max_value := 100.0
|
||||
@export var horizontal := false
|
||||
@@ -27,7 +25,7 @@ func set_value(value: float) -> void:
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var t := 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
|
||||
var t := _smoothing_weight(delta)
|
||||
_value = lerpf(_value, _target, t)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class_name HudHeadingTape
|
||||
extends Control
|
||||
extends HudInstrument
|
||||
|
||||
# Aircraft-style heading tape: a strip of compass ticks slides under a fixed
|
||||
# centre marker, with cardinal letters so the player always knows which way
|
||||
@@ -11,7 +11,6 @@ const MARKER_COLOR := Color(1.0, 0.8, 0.2)
|
||||
|
||||
# Degrees of heading visible across the full tape width.
|
||||
const VISIBLE_RANGE := 90.0
|
||||
const SMOOTHING := 12.0
|
||||
|
||||
const CARDINALS := {0: "N", 45: "NE", 90: "E", 135: "SE", 180: "S", 225: "SW", 270: "W", 315: "NW"}
|
||||
|
||||
@@ -28,8 +27,8 @@ func set_heading(heading_deg: float) -> void:
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
var t := 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
|
||||
_heading = fmod(HudAttitudeIndicator.lerp_angle_deg(_heading, _target_heading, t) + 360.0, 360.0)
|
||||
var t := _smoothing_weight(delta)
|
||||
_heading = fmod(lerp_angle_deg(_heading, _target_heading, t) + 360.0, 360.0)
|
||||
queue_redraw()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
class_name HudInstrument
|
||||
extends Control
|
||||
|
||||
# Shared base for the HUD's procedural instruments (heading tape, attitude
|
||||
# indicator, gauges): the exponential-smoothing weight calc and the
|
||||
# angle-aware lerp helper they all use. Each instrument still owns its own
|
||||
# _process (smoothing 1-2 distinct fields) and _draw (entirely bespoke).
|
||||
|
||||
const SMOOTHING := 12.0
|
||||
|
||||
|
||||
static func lerp_angle_deg(from: float, to: float, weight: float) -> float:
|
||||
return rad_to_deg(lerp_angle(deg_to_rad(from), deg_to_rad(to), weight))
|
||||
|
||||
|
||||
func _smoothing_weight(delta: float) -> float:
|
||||
return 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
|
||||
@@ -0,0 +1 @@
|
||||
uid://bfjncr0nnakun
|
||||
@@ -11,6 +11,10 @@ extends Control
|
||||
|
||||
const BOTS_DIR := "res://bots"
|
||||
|
||||
# All three tiers deliberately point at the one trained checkpoint
|
||||
# (easy.json) and differ only by reaction_ticks/action_noise handicap, not by
|
||||
# model skill. Superseded once real medium/hard checkpoints are promoted into
|
||||
# res://bots/promoted/ — see the "AI opponent" section of TODO.md.
|
||||
const DIFFICULTIES := [
|
||||
{"name": "Easy", "model": "res://bots/promoted/easy.json", "reaction_ticks": 24, "action_noise": 0.35},
|
||||
{"name": "Medium", "model": "res://bots/promoted/easy.json", "reaction_ticks": 14, "action_noise": 0.15},
|
||||
|
||||
@@ -27,7 +27,6 @@ const KICKOFF_COUNTDOWN_SECONDS := 3
|
||||
@export_range(1, 60) var bot_reaction_ticks: int = 8
|
||||
@export_range(0.0, 1.0) var bot_action_noise: float = 0.0
|
||||
|
||||
var score := {0: 0, 1: 0}
|
||||
var match_timer: Timer
|
||||
var _in_overtime := false
|
||||
|
||||
@@ -56,15 +55,9 @@ func _make_opponent_controller() -> ShipController:
|
||||
# The main menu's dropdown selection (GameSettings autoload) wins over the
|
||||
# scene's export, which stays as the fallback for direct-scene runs.
|
||||
var path := bot_model_path if GameSettings.selected_bot_path.is_empty() else GameSettings.selected_bot_path
|
||||
if not path.is_empty() and FileAccess.file_exists(path):
|
||||
var bot := AIShipController.new()
|
||||
bot.model_path = path
|
||||
bot.reaction_ticks = bot_reaction_ticks if GameSettings.selected_bot_reaction_ticks < 0 else GameSettings.selected_bot_reaction_ticks
|
||||
bot.action_noise = bot_action_noise if GameSettings.selected_bot_action_noise < 0.0 else GameSettings.selected_bot_action_noise
|
||||
return bot
|
||||
if not path.is_empty():
|
||||
push_warning("MatchMode: bot model not found at %s, spawning inert opponent" % path)
|
||||
return ShipController.new() # inert placeholder
|
||||
var reaction_ticks := bot_reaction_ticks if GameSettings.selected_bot_reaction_ticks < 0 else GameSettings.selected_bot_reaction_ticks
|
||||
var action_noise := bot_action_noise if GameSettings.selected_bot_action_noise < 0.0 else GameSettings.selected_bot_action_noise
|
||||
return _build_opponent(path, reaction_ticks, action_noise, "MatchMode")
|
||||
|
||||
|
||||
func _process(_delta):
|
||||
@@ -75,9 +68,8 @@ func _process(_delta):
|
||||
|
||||
func _on_goal_scored(conceding_team: int) -> void:
|
||||
var scoring_team := 1 - conceding_team
|
||||
score[scoring_team] += 1
|
||||
_record_goal(scoring_team)
|
||||
score_changed.emit(score.duplicate())
|
||||
print("Goal for team %d! Score: %d - %d" % [scoring_team, score[0], score[1]])
|
||||
if _in_overtime:
|
||||
# Golden goal: the first score after a draw ends the match outright.
|
||||
_end_match(scoring_team)
|
||||
@@ -136,4 +128,4 @@ func _end_match(winning_team: int) -> void:
|
||||
await get_tree().create_timer(RESULT_SCREEN_SECONDS).timeout
|
||||
# Unpause before leaving, or the menu arrives paused and unresponsive.
|
||||
get_tree().paused = false
|
||||
get_tree().change_scene_to_file(MAIN_MENU_SCENE_PATH)
|
||||
get_tree().change_scene_to_file(ScenePaths.MAIN_MENU)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
class_name ScenePaths
|
||||
|
||||
const MAIN_MENU := "res://scenes/main_menu.tscn"
|
||||
@@ -0,0 +1 @@
|
||||
uid://5msygpcnvgfd
|
||||
@@ -6,8 +6,6 @@ extends Control
|
||||
# apply the next time an arena loads (see arena.gd), since they scale each
|
||||
# arena's own tuned Environment values rather than something viewport-wide.
|
||||
|
||||
const MAIN_MENU_SCENE_PATH := "res://scenes/main_menu.tscn"
|
||||
|
||||
const AA_OPTIONS := [
|
||||
{"name": "Off", "mode": VideoSettings.AAMode.OFF},
|
||||
{"name": "FXAA", "mode": VideoSettings.AAMode.FXAA},
|
||||
@@ -62,7 +60,7 @@ func _on_brightness_slider_value_changed(value: float) -> void:
|
||||
|
||||
func _on_back_pressed() -> void:
|
||||
VideoSettings.save()
|
||||
get_tree().change_scene_to_file(MAIN_MENU_SCENE_PATH)
|
||||
get_tree().change_scene_to_file(ScenePaths.MAIN_MENU)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
|
||||
@@ -16,9 +16,6 @@ signal score_changed(score: Dictionary)
|
||||
@export_range(1, 60) var bot_b_reaction_ticks: int = 8
|
||||
@export_range(0.0, 1.0) var bot_b_action_noise: float = 0.0
|
||||
|
||||
var score := {0: 0, 1: 0}
|
||||
|
||||
|
||||
func _get_arena_scene_path() -> String:
|
||||
return ArenaRegistry.random_path()
|
||||
|
||||
@@ -29,28 +26,15 @@ func _start() -> void:
|
||||
# scene's exports, which stay as the fallback for direct-scene runs.
|
||||
var path_a := bot_a_model_path if GameSettings.spectate_bot_a_path.is_empty() else GameSettings.spectate_bot_a_path
|
||||
var path_b := bot_b_model_path if GameSettings.spectate_bot_b_path.is_empty() else GameSettings.spectate_bot_b_path
|
||||
var ship_a := spawn_ship(0, 0, _make_bot(path_a, bot_a_reaction_ticks, bot_a_action_noise))
|
||||
spawn_ship(1, 0, _make_bot(path_b, bot_b_reaction_ticks, bot_b_action_noise))
|
||||
var ship_a := spawn_ship(0, 0, _build_opponent(path_a, bot_a_reaction_ticks, bot_a_action_noise, "SpectateMode"))
|
||||
spawn_ship(1, 0, _build_opponent(path_b, bot_b_reaction_ticks, bot_b_action_noise, "SpectateMode"))
|
||||
spawn_camera_rig(ship_a)
|
||||
|
||||
|
||||
func _make_bot(model_path: String, reaction_ticks: int, action_noise: float) -> ShipController:
|
||||
if not model_path.is_empty() and FileAccess.file_exists(model_path):
|
||||
var bot := AIShipController.new()
|
||||
bot.model_path = model_path
|
||||
bot.reaction_ticks = reaction_ticks
|
||||
bot.action_noise = action_noise
|
||||
return bot
|
||||
if not model_path.is_empty():
|
||||
push_warning("SpectateMode: bot model not found at %s, spawning inert ship" % model_path)
|
||||
return ShipController.new() # inert placeholder
|
||||
|
||||
|
||||
func _on_goal_scored(conceding_team: int) -> void:
|
||||
var scoring_team := 1 - conceding_team
|
||||
score[scoring_team] += 1
|
||||
_record_goal(scoring_team)
|
||||
score_changed.emit(score.duplicate())
|
||||
print("Goal for team %d! Score: %d - %d" % [scoring_team, score[0], score[1]])
|
||||
reset_ball()
|
||||
reset_ships()
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ Bugs found in an adversarial review. None are gameplay- or physics-affecting, so
|
||||
|
||||
## DRY / structure
|
||||
|
||||
- [ ] `arena_base.tscn` + inherited themes. `arena_01/02/03.tscn` each restate ~40 identical lines (both lights, the reflection probe, goal transforms, ball/ship spawn markers); only sky, ambient, three glow numbers, tint and decoration differ. The `_elevated` variants already prove the inherited-scene pattern works here — `arena_01_elevated.tscn` is 20 lines to `arena_01.tscn`'s 100. Unblocks cheap new arenas.
|
||||
- [ ] Hoist bot construction into `GameMode` — `match_mode._make_opponent_controller` and `spectate_mode._make_bot` are the same function (same existence check, same three fields, same warning + inert fallback).
|
||||
- [ ] Hoist score-keeping into `GameMode` — both modes declare `score := {0:0, 1:0}`, a `score_changed` signal, and the identical increment/emit/print block.
|
||||
- [ ] `HudInstrument extends Control` base for the three HUD widgets: each repeats `const SMOOTHING := 12.0` and the same `1.0 - exp(-SMOOTHING * delta)` lerp/redraw `_process`, and `hud_heading_tape.gd` reaches across to a static angle helper parked on `HudAttitudeIndicator`.
|
||||
- [ ] `MAIN_MENU_SCENE_PATH` is declared in both `game_mode.gd` and `settings_menu.gd`.
|
||||
- [ ] Comment `main_menu.gd`'s `DIFFICULTIES` to say the tiers deliberately share `easy.json` and differ only by handicap (superseded once real `medium`/`hard` tiers land — see the AI section above).
|
||||
- [x] `arena_base.tscn` + inherited themes. `arena_01/02/03.tscn` each restate ~40 identical lines (both lights, the reflection probe, goal transforms, ball/ship spawn markers); only sky, ambient, three glow numbers, tint and decoration differ. The `_elevated` variants already prove the inherited-scene pattern works here — `arena_01_elevated.tscn` is 20 lines to `arena_01.tscn`'s 100. Unblocks cheap new arenas.
|
||||
- [x] Hoist bot construction into `GameMode` — `match_mode._make_opponent_controller` and `spectate_mode._make_bot` are the same function (same existence check, same three fields, same warning + inert fallback).
|
||||
- [x] Hoist score-keeping into `GameMode` — both modes declare `score := {0:0, 1:0}`, a `score_changed` signal, and the identical increment/emit/print block.
|
||||
- [x] `HudInstrument extends Control` base for the three HUD widgets: each repeats `const SMOOTHING := 12.0` and the same `1.0 - exp(-SMOOTHING * delta)` lerp/redraw `_process`, and `hud_heading_tape.gd` reaches across to a static angle helper parked on `HudAttitudeIndicator`.
|
||||
- [x] `MAIN_MENU_SCENE_PATH` is declared in both `game_mode.gd` and `settings_menu.gd`.
|
||||
- [x] Comment `main_menu.gd`'s `DIFFICULTIES` to say the tiers deliberately share `easy.json` and differ only by handicap (superseded once real `medium`/`hard` tiers land — see the AI section above).
|
||||
|
||||
## Performance
|
||||
|
||||
|
||||
Reference in New Issue
Block a user