diff --git a/Game/scenes/arena_01.tscn b/Game/scenes/arena_01.tscn index bd48e15b..28570ef7 100644 --- a/Game/scenes/arena_01.tscn +++ b/Game/scenes/arena_01.tscn @@ -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) diff --git a/Game/scenes/arena_02.tscn b/Game/scenes/arena_02.tscn index b5e604da..481693e4 100644 --- a/Game/scenes/arena_02.tscn +++ b/Game/scenes/arena_02.tscn @@ -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) diff --git a/Game/scenes/arena_03.tscn b/Game/scenes/arena_03.tscn index 72a8bc22..56b858b6 100644 --- a/Game/scenes/arena_03.tscn +++ b/Game/scenes/arena_03.tscn @@ -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="."] diff --git a/Game/scenes/arena_base.tscn b/Game/scenes/arena_base.tscn new file mode 100644 index 00000000..2aaedba0 --- /dev/null +++ b/Game/scenes/arena_base.tscn @@ -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) diff --git a/Game/scripts/arena.gd b/Game/scripts/arena.gd index f9d27e42..47124714 100644 --- a/Game/scripts/arena.gd +++ b/Game/scripts/arena.gd @@ -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: diff --git a/Game/scripts/game_mode.gd b/Game/scripts/game_mode.gd index 9b11f4db..88ca765b 100644 --- a/Game/scripts/game_mode.gd +++ b/Game/scripts/game_mode.gd @@ -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) diff --git a/Game/scripts/hud_attitude_indicator.gd b/Game/scripts/hud_attitude_indicator.gd index 0368ee1a..6aea1420 100644 --- a/Game/scripts/hud_attitude_indicator.gd +++ b/Game/scripts/hud_attitude_indicator.gd @@ -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 diff --git a/Game/scripts/hud_gauge.gd b/Game/scripts/hud_gauge.gd index 150399e0..4aba4fc9 100644 --- a/Game/scripts/hud_gauge.gd +++ b/Game/scripts/hud_gauge.gd @@ -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() diff --git a/Game/scripts/hud_heading_tape.gd b/Game/scripts/hud_heading_tape.gd index 9f68e36b..df5a811c 100644 --- a/Game/scripts/hud_heading_tape.gd +++ b/Game/scripts/hud_heading_tape.gd @@ -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() diff --git a/Game/scripts/hud_instrument.gd b/Game/scripts/hud_instrument.gd new file mode 100644 index 00000000..4b2c022f --- /dev/null +++ b/Game/scripts/hud_instrument.gd @@ -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 diff --git a/Game/scripts/hud_instrument.gd.uid b/Game/scripts/hud_instrument.gd.uid new file mode 100644 index 00000000..b0a8438d --- /dev/null +++ b/Game/scripts/hud_instrument.gd.uid @@ -0,0 +1 @@ +uid://bfjncr0nnakun diff --git a/Game/scripts/main_menu.gd b/Game/scripts/main_menu.gd index 6e70e9f2..39ae2677 100644 --- a/Game/scripts/main_menu.gd +++ b/Game/scripts/main_menu.gd @@ -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}, diff --git a/Game/scripts/match_mode.gd b/Game/scripts/match_mode.gd index 3e401ac9..8df4c8c8 100644 --- a/Game/scripts/match_mode.gd +++ b/Game/scripts/match_mode.gd @@ -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) diff --git a/Game/scripts/scene_paths.gd b/Game/scripts/scene_paths.gd new file mode 100644 index 00000000..4a0f8959 --- /dev/null +++ b/Game/scripts/scene_paths.gd @@ -0,0 +1,3 @@ +class_name ScenePaths + +const MAIN_MENU := "res://scenes/main_menu.tscn" diff --git a/Game/scripts/scene_paths.gd.uid b/Game/scripts/scene_paths.gd.uid new file mode 100644 index 00000000..e0e553e5 --- /dev/null +++ b/Game/scripts/scene_paths.gd.uid @@ -0,0 +1 @@ +uid://5msygpcnvgfd diff --git a/Game/scripts/settings_menu.gd b/Game/scripts/settings_menu.gd index 948c4d4d..849f3852 100644 --- a/Game/scripts/settings_menu.gd +++ b/Game/scripts/settings_menu.gd @@ -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: diff --git a/Game/scripts/spectate_mode.gd b/Game/scripts/spectate_mode.gd index 6f6fdd5c..4c8ff5b3 100644 --- a/Game/scripts/spectate_mode.gd +++ b/Game/scripts/spectate_mode.gd @@ -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() diff --git a/TODO.md b/TODO.md index a387b6fc..5f3660d1 100644 --- a/TODO.md +++ b/TODO.md @@ -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