Compare commits

..

4 Commits

427 changed files with 672 additions and 91 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -112,6 +112,7 @@ roll_right={
[physics] [physics]
common/physics_interpolation=true
3d/physics_engine="Jolt Physics" 3d/physics_engine="Jolt Physics"
[autoload] [autoload]
@@ -121,3 +122,11 @@ roll_right={
GameSettings="*res://scripts/game_settings.gd"
+133 -33
View File
@@ -1,11 +1,15 @@
[gd_scene load_steps=3 format=3 uid="uid://c8kak2l3m4n5"] [gd_scene load_steps=6 format=3 uid="uid://c8kak2l3m4n5"]
[ext_resource type="Script" uid="uid://du7y176h5aaq4" path="res://scripts/HUDController.gd" id="1_hud_controller"] [ext_resource type="Script" uid="uid://du7y176h5aaq4" path="res://scripts/HUDController.gd" id="1_hud_controller"]
[ext_resource type="Script" path="res://scripts/hud_attitude_indicator.gd" id="2_adi"]
[ext_resource type="Script" path="res://scripts/hud_heading_tape.gd" id="3_tape"]
[ext_resource type="Script" path="res://scripts/hud_gauge.gd" id="4_gauge"]
[sub_resource type="LabelSettings" id="LabelSettings_hud"] [sub_resource type="LabelSettings" id="LabelSettings_hud"]
font_size = 32 font_size = 32
[node name="HUD" type="CanvasLayer"] [node name="HUD" type="CanvasLayer"]
process_mode = 3
script = ExtResource("1_hud_controller") script = ExtResource("1_hud_controller")
[node name="Control" type="Control" parent="."] [node name="Control" type="Control" parent="."]
@@ -15,6 +19,7 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
mouse_filter = 2
[node name="TimerLabel" type="Label" parent="Control"] [node name="TimerLabel" type="Label" parent="Control"]
layout_mode = 1 layout_mode = 1
@@ -28,7 +33,37 @@ grow_horizontal = 2
label_settings = SubResource("LabelSettings_hud") label_settings = SubResource("LabelSettings_hud")
horizontal_alignment = 1 horizontal_alignment = 1
[node name="Instruments" type="Control" parent="Control"] [node name="ScoreRow" type="HBoxContainer" parent="Control"]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -90.5
offset_right = 90.5
offset_top = 64.0
offset_bottom = 110.0
grow_horizontal = 2
alignment = 1
[node name="Team0Score" type="Label" parent="Control/ScoreRow"]
layout_mode = 2
theme_override_colors/font_color = Color(0.25, 0.55, 1, 1)
theme_override_font_sizes/font_size = 32
text = "0"
[node name="ScoreDash" type="Label" parent="Control/ScoreRow"]
layout_mode = 2
theme_override_font_sizes/font_size = 32
text = " - "
[node name="Team1Score" type="Label" parent="Control/ScoreRow"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.5, 0.15, 1)
theme_override_font_sizes/font_size = 32
text = "0"
[node name="ResultOverlay" type="Control" parent="Control"]
visible = false
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -36,48 +71,113 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
[node name="InfoPanel" type="VBoxContainer" parent="Control"] [node name="Dim" type="ColorRect" parent="Control/ResultOverlay"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.5)
[node name="Center" type="CenterContainer" parent="Control/ResultOverlay"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBox" type="VBoxContainer" parent="Control/ResultOverlay/Center"]
layout_mode = 2
theme_override_constants/separation = 16
[node name="ResultLabel" type="Label" parent="Control/ResultOverlay/Center/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 56
text = "Draw"
horizontal_alignment = 1
[node name="FinalScoreLabel" type="Label" parent="Control/ResultOverlay/Center/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 36
text = "0 - 0"
horizontal_alignment = 1
[node name="Instruments" type="Control" parent="Control"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="HeadingTape" type="Control" parent="Control/Instruments"]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -180.0
offset_right = 180.0
offset_top = 116.0
offset_bottom = 156.0
grow_horizontal = 2
mouse_filter = 2
script = ExtResource("3_tape")
[node name="Cluster" type="VBoxContainer" parent="Control/Instruments"]
layout_mode = 1 layout_mode = 1
anchors_preset = 2 anchors_preset = 2
anchor_top = 1.0 anchor_top = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = 10.0 offset_left = 16.0
offset_top = -200.0 offset_top = -248.0
offset_right = 300.0 offset_right = 294.0
offset_bottom = -10.0 offset_bottom = -16.0
grow_vertical = 0 grow_vertical = 0
theme_override_constants/separation = 8
mouse_filter = 2
[node name="SpeedLabel" type="Label" parent="Control/InfoPanel"] [node name="Dials" type="HBoxContainer" parent="Control/Instruments/Cluster"]
layout_mode = 2 layout_mode = 2
text = "Speed: 0.0 m/s" theme_override_constants/separation = 8
label_settings = SubResource("LabelSettings_hud") mouse_filter = 2
[node name="AltitudeLabel" type="Label" parent="Control/InfoPanel"] [node name="SpeedGauge" type="Control" parent="Control/Instruments/Cluster/Dials"]
custom_minimum_size = Vector2(46, 170)
layout_mode = 2 layout_mode = 2
text = "Altitude: 0.0 m" mouse_filter = 2
label_settings = SubResource("LabelSettings_hud") script = ExtResource("4_gauge")
label = "SPD"
max_value = 35.0
[node name="AngularVelLabel" type="Label" parent="Control/InfoPanel"] [node name="AttitudeIndicator" type="Control" parent="Control/Instruments/Cluster/Dials"]
custom_minimum_size = Vector2(170, 170)
layout_mode = 2 layout_mode = 2
text = "Angular Vel: 0.0 rad/s" mouse_filter = 2
label_settings = SubResource("LabelSettings_hud") script = ExtResource("2_adi")
[node name="CameraModeLabel" type="Label" parent="Control/InfoPanel"] [node name="AltitudeGauge" type="Control" parent="Control/Instruments/Cluster/Dials"]
custom_minimum_size = Vector2(46, 170)
layout_mode = 2 layout_mode = 2
mouse_filter = 2
script = ExtResource("4_gauge")
label = "ALT"
max_value = 12.0
value_format = "%.1f"
[node name="ThrustBar" type="Control" parent="Control/Instruments/Cluster"]
custom_minimum_size = Vector2(278, 16)
layout_mode = 2
mouse_filter = 2
script = ExtResource("4_gauge")
label = "THR"
horizontal = true
fill_color = Color(1, 0.55, 0.15, 0.85)
[node name="CameraModeLabel" type="Label" parent="Control/Instruments/Cluster"]
modulate = Color(1, 1, 1, 0.6)
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Camera: Ball Cam" text = "Camera: Ball Cam"
label_settings = SubResource("LabelSettings_hud")
[node name="AttitudeLabel" type="Label" parent="Control/InfoPanel"]
layout_mode = 2
text = "Pitch: 0° Roll: 0°"
label_settings = SubResource("LabelSettings_hud")
[node name="HeadingLabel" type="Label" parent="Control/InfoPanel"]
layout_mode = 2
text = "Heading: 0°"
label_settings = SubResource("LabelSettings_hud")
[node name="ThrustLabel" type="Label" parent="Control/InfoPanel"]
layout_mode = 2
text = "Thrust: 0%"
label_settings = SubResource("LabelSettings_hud")
+88 -7
View File
@@ -20,8 +20,9 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"] [node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
custom_minimum_size = Vector2(420, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 24 theme_override_constants/separation = 10
[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"] [node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"]
layout_mode = 2 layout_mode = 2
@@ -29,20 +30,100 @@ theme_override_font_sizes/font_size = 48
text = "Cosmic Clash" text = "Cosmic Clash"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="SubtitleLabel" type="Label" parent="CenterContainer/VBoxContainer"]
modulate = Color(1, 1, 1, 0.55)
layout_mode = 2
theme_override_font_sizes/font_size = 16
text = "Physics-based soccer in space"
horizontal_alignment = 1
[node name="TitleSpacer" type="Control" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 14)
layout_mode = 2
[node name="FreePlayButton" type="Button" parent="CenterContainer/VBoxContainer"] [node name="FreePlayButton" type="Button" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(330, 60) custom_minimum_size = Vector2(0, 56)
layout_mode = 2 layout_mode = 2
text = "Free Play" text = "Free Play"
[node name="MatchButton" type="Button" parent="CenterContainer/VBoxContainer"] [node name="FreePlayHint" type="Label" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(330, 60) modulate = Color(1, 1, 1, 0.55)
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 13
text = "Solo practice — no timer, R resets the ball"
horizontal_alignment = 1
[node name="MatchSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
[node name="MatchHeader" type="Label" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "Match" text = "Match"
[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer"] [node name="MatchHint" type="Label" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(330, 60) modulate = Color(1, 1, 1, 0.55)
layout_mode = 2 layout_mode = 2
text = "Spectate (Bot vs Bot)" theme_override_font_sizes/font_size = 13
text = "A 2:30 match — you vs a trained bot"
[node name="MatchRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="OpponentLabel" type="Label" parent="CenterContainer/VBoxContainer/MatchRow"]
layout_mode = 2
text = "Opponent"
[node name="BotDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/MatchRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_horizontal = 3
[node name="MatchButton" type="Button" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 56)
layout_mode = 2
text = "Play Match"
[node name="SpectateSeparator" type="HSeparator" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
[node name="SpectateHeader" type="Label" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "Spectate"
[node name="SpectateHint" type="Label" parent="CenterContainer/VBoxContainer"]
modulate = Color(1, 1, 1, 0.55)
layout_mode = 2
theme_override_font_sizes/font_size = 13
text = "Watch two bots play each other"
[node name="SpectateRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="BotADropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/SpectateRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_horizontal = 3
[node name="VsLabel" type="Label" parent="CenterContainer/VBoxContainer/SpectateRow"]
layout_mode = 2
text = "vs"
[node name="BotBDropdown" type="OptionButton" parent="CenterContainer/VBoxContainer/SpectateRow"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_horizontal = 3
[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 56)
layout_mode = 2
text = "Watch Match"
[connection signal="pressed" from="CenterContainer/VBoxContainer/FreePlayButton" to="." method="_on_free_play_pressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/FreePlayButton" to="." method="_on_free_play_pressed"]
[connection signal="pressed" from="CenterContainer/VBoxContainer/MatchButton" to="." method="_on_match_pressed"] [connection signal="pressed" from="CenterContainer/VBoxContainer/MatchButton" to="." method="_on_match_pressed"]
+1 -1
View File
@@ -6,7 +6,7 @@
[node name="Match" type="Node3D"] [node name="Match" type="Node3D"]
script = ExtResource("1_m") script = ExtResource("1_m")
bot_model_path = "res://bots/run03.json" bot_model_path = "res://bots/run05.json"
[node name="Arena" parent="." instance=ExtResource("2_m")] [node name="Arena" parent="." instance=ExtResource("2_m")]
+2 -2
View File
@@ -6,8 +6,8 @@
[node name="Spectate" type="Node3D"] [node name="Spectate" type="Node3D"]
script = ExtResource("1_s") script = ExtResource("1_s")
bot_a_model_path = "res://bots/run02.json" bot_a_model_path = "res://bots/run05.json"
bot_b_model_path = "res://bots/run01.json" bot_b_model_path = "res://bots/run05.json"
[node name="Arena" parent="." instance=ExtResource("2_s")] [node name="Arena" parent="." instance=ExtResource("2_s")]
+73 -37
View File
@@ -1,18 +1,28 @@
# HUD Controller - handles all display logic and label updates # HUD Controller - discovers the ship/camera/game mode via groups and routes
# Receives ship data via signals and updates UI accordingly # their signals to the display widgets. All flight data is rendered by
# Simplified version focusing on labels only # aircraft-style instruments (attitude indicator, heading tape, bar gauges) —
# this node is only the wiring hub; the instruments are dumb displays.
extends CanvasLayer extends CanvasLayer
class_name HUDController class_name HUDController
# UI Label References
@onready var timer_label = get_node("Control/TimerLabel") @onready var timer_label = get_node("Control/TimerLabel")
@onready var speed_label = get_node("Control/InfoPanel/SpeedLabel") @onready var score_row = get_node_or_null("Control/ScoreRow")
@onready var altitude_label = get_node("Control/InfoPanel/AltitudeLabel") @onready var team0_score_label = get_node_or_null("Control/ScoreRow/Team0Score")
@onready var angular_vel_label = get_node("Control/InfoPanel/AngularVelLabel") @onready var team1_score_label = get_node_or_null("Control/ScoreRow/Team1Score")
@onready var camera_mode_label = get_node("Control/InfoPanel/CameraModeLabel") @onready var result_overlay = get_node_or_null("Control/ResultOverlay")
@onready var attitude_label = get_node("Control/InfoPanel/AttitudeLabel") @onready var result_label = get_node_or_null("Control/ResultOverlay/Center/VBox/ResultLabel")
@onready var heading_label = get_node("Control/InfoPanel/HeadingLabel") @onready var final_score_label = get_node_or_null("Control/ResultOverlay/Center/VBox/FinalScoreLabel")
@onready var thrust_label = get_node("Control/InfoPanel/ThrustLabel")
@onready var heading_tape: HudHeadingTape = get_node_or_null("Control/Instruments/HeadingTape")
@onready var attitude_indicator: HudAttitudeIndicator = get_node_or_null("Control/Instruments/Cluster/Dials/AttitudeIndicator")
@onready var speed_gauge: HudGauge = get_node_or_null("Control/Instruments/Cluster/Dials/SpeedGauge")
@onready var altitude_gauge: HudGauge = get_node_or_null("Control/Instruments/Cluster/Dials/AltitudeGauge")
@onready var thrust_bar: HudGauge = get_node_or_null("Control/Instruments/Cluster/ThrustBar")
@onready var camera_mode_label = get_node_or_null("Control/Instruments/Cluster/CameraModeLabel")
# Team identity, mirroring TEAM_COLORS in ship.gd
const TEAM_NAMES := {0: "Blue", 1: "Orange"}
const TEAM_COLORS := {0: Color(0.25, 0.55, 1.0), 1: Color(1.0, 0.5, 0.15)}
var ship: Node var ship: Node
@@ -29,6 +39,11 @@ func _initialize_hud():
return return
print("HUDController: Found ship: ", ship.name) print("HUDController: Found ship: ", ship.name)
# Gauge ranges derive from the ship and arena rather than restating numbers
if speed_gauge and "max_speed" in ship:
speed_gauge.max_value = ship.max_speed
if altitude_gauge:
altitude_gauge.max_value = ArenaBoundary.INNER_HEIGHT
_connect_ship_signals() _connect_ship_signals()
# Camera mode comes from the camera rig, not the ship # Camera mode comes from the camera rig, not the ship
@@ -46,15 +61,23 @@ func _initialize_hud():
if timer_label and is_instance_valid(timer_label): if timer_label and is_instance_valid(timer_label):
timer_label.visible = has_timer timer_label.visible = has_timer
# Score display follows the same pattern: modes without scoring
# (e.g. free play) just don't show it
var has_score = game_manager and game_manager.has_signal("score_changed")
if has_score:
game_manager.score_changed.connect(_on_score_changed)
if score_row and is_instance_valid(score_row):
score_row.visible = has_score
if game_manager and game_manager.has_signal("match_ended"):
game_manager.match_ended.connect(_on_match_ended)
func _connect_ship_signals(): func _connect_ship_signals():
# Connect ship signals to label update methods # Route ship telemetry to the flight instruments
if ship.has_signal("speed_changed"): if ship.has_signal("speed_changed"):
ship.speed_changed.connect(_on_ship_speed_changed) ship.speed_changed.connect(_on_ship_speed_changed)
print("HUDController: Connected to speed_changed signal")
if ship.has_signal("altitude_changed"): if ship.has_signal("altitude_changed"):
ship.altitude_changed.connect(_on_ship_altitude_changed) ship.altitude_changed.connect(_on_ship_altitude_changed)
if ship.has_signal("angular_velocity_changed"):
ship.angular_velocity_changed.connect(_on_ship_angular_velocity_changed)
if ship.has_signal("attitude_changed"): if ship.has_signal("attitude_changed"):
ship.attitude_changed.connect(_on_ship_attitude_changed) ship.attitude_changed.connect(_on_ship_attitude_changed)
if ship.has_signal("heading_changed"): if ship.has_signal("heading_changed"):
@@ -62,38 +85,33 @@ func _connect_ship_signals():
if ship.has_signal("thrust_changed"): if ship.has_signal("thrust_changed"):
ship.thrust_changed.connect(_on_ship_thrust_changed) ship.thrust_changed.connect(_on_ship_thrust_changed)
# Signal handlers for ship data - these update the legacy text labels # Instrument signal handlers - all include null checks for robustness
# All handlers include null checks for robustness
func _on_ship_speed_changed(speed: float): func _on_ship_speed_changed(speed: float):
if speed_label and is_instance_valid(speed_label): if speed_gauge and is_instance_valid(speed_gauge):
speed_label.text = "Speed: %.1f m/s" % speed speed_gauge.set_value(speed)
func _on_ship_altitude_changed(altitude: float): func _on_ship_altitude_changed(altitude: float):
if altitude_label and is_instance_valid(altitude_label): if altitude_gauge and is_instance_valid(altitude_gauge):
altitude_label.text = "Altitude: %.1f m" % altitude altitude_gauge.set_value(altitude)
func _on_ship_angular_velocity_changed(angular_speed: float): func _on_ship_attitude_changed(pitch: float, roll: float, _yaw: float):
if angular_vel_label and is_instance_valid(angular_vel_label): if attitude_indicator and is_instance_valid(attitude_indicator):
angular_vel_label.text = "Angular Vel: %.2f rad/s" % angular_speed attitude_indicator.set_attitude(pitch, roll)
func _on_ship_heading_changed(heading_degrees: float):
if heading_tape and is_instance_valid(heading_tape):
heading_tape.set_heading(heading_degrees)
func _on_ship_thrust_changed(thrust_percent: float):
if thrust_bar and is_instance_valid(thrust_bar):
thrust_bar.set_value(thrust_percent)
func _on_ship_camera_mode_changed(is_ball_cam: bool): func _on_ship_camera_mode_changed(is_ball_cam: bool):
if camera_mode_label and is_instance_valid(camera_mode_label): if camera_mode_label and is_instance_valid(camera_mode_label):
var camera_mode = "Ball Cam" if is_ball_cam else "Ship Cam" var camera_mode = "Ball Cam" if is_ball_cam else "Ship Cam"
camera_mode_label.text = "Camera: %s" % camera_mode camera_mode_label.text = "Camera: %s" % camera_mode
func _on_ship_attitude_changed(pitch: float, roll: float, _yaw: float): # Timer signal handler from game manager
if attitude_label and is_instance_valid(attitude_label):
attitude_label.text = "Pitch: %.0f° Roll: %.0f°" % [pitch, roll]
func _on_ship_heading_changed(heading_degrees: float):
if heading_label and is_instance_valid(heading_label):
heading_label.text = "Heading: %.0f°" % heading_degrees
func _on_ship_thrust_changed(thrust_percent: float):
if thrust_label and is_instance_valid(thrust_label):
thrust_label.text = "Thrust: %.0f%%" % thrust_percent
# Timer signal handler from game manager (restored original functionality)
func _on_timer_updated(minutes: int, seconds: int): func _on_timer_updated(minutes: int, seconds: int):
if timer_label and is_instance_valid(timer_label): if timer_label and is_instance_valid(timer_label):
timer_label.text = "%02d:%02d" % [minutes, seconds] timer_label.text = "%02d:%02d" % [minutes, seconds]
@@ -102,3 +120,21 @@ func _on_timer_updated(minutes: int, seconds: int):
func update_timer(time_text: String): func update_timer(time_text: String):
if timer_label and is_instance_valid(timer_label): if timer_label and is_instance_valid(timer_label):
timer_label.text = time_text timer_label.text = time_text
func _on_score_changed(score: Dictionary):
if team0_score_label and is_instance_valid(team0_score_label):
team0_score_label.text = str(score.get(0, 0))
if team1_score_label and is_instance_valid(team1_score_label):
team1_score_label.text = str(score.get(1, 0))
func _on_match_ended(winning_team: int, score: Dictionary):
if not result_overlay or not is_instance_valid(result_overlay):
return
if winning_team < 0:
result_label.text = "Draw"
result_label.remove_theme_color_override("font_color")
else:
result_label.text = "%s team wins!" % TEAM_NAMES[winning_team]
result_label.add_theme_color_override("font_color", TEAM_COLORS[winning_team])
final_score_label.text = "%d - %d" % [score.get(0, 0), score.get(1, 0)]
result_overlay.visible = true
+3
View File
@@ -101,6 +101,9 @@ func _reset_body(body: RigidBody3D, to: Transform3D) -> void:
body.set_deferred("global_transform", to) body.set_deferred("global_transform", to)
body.set_deferred("linear_velocity", Vector3.ZERO) body.set_deferred("linear_velocity", Vector3.ZERO)
body.set_deferred("angular_velocity", Vector3.ZERO) body.set_deferred("angular_velocity", Vector3.ZERO)
# A kickoff reset is a teleport: without this, physics interpolation
# smears the body across the arena for a frame
body.call_deferred("reset_physics_interpolation")
func _unhandled_input(event): func _unhandled_input(event):
+11
View File
@@ -0,0 +1,11 @@
extends Node
# Autoload: cross-scene settings handed from the main menu to game modes.
# Defaults keep direct-scene runs (F6), training, and headless runs unchanged.
# Opponent policy chosen in the main menu; empty = use the mode's own export.
var selected_bot_path: String = ""
# Spectate (bot vs bot) policies chosen in the main menu; empty = mode export.
var spectate_bot_a_path: String = ""
var spectate_bot_b_path: String = ""
+1
View File
@@ -0,0 +1 @@
uid://dv4wiy0oor065
+106
View File
@@ -0,0 +1,106 @@
class_name HudAttitudeIndicator
extends Control
# Aircraft-style attitude indicator (artificial horizon): the sky/ground disc
# rolls and the pitch ladder scrolls behind a fixed ship symbol, so pitch and
# roll are readable at a glance. Pure _draw code — no textures. A dumb display:
# HUDController pushes values in via set_attitude; it never reads the ship.
const SKY_COLOR := Color(0.10, 0.24, 0.42, 0.9)
const GROUND_COLOR := Color(0.22, 0.15, 0.10, 0.9)
const LINE_COLOR := Color(0.85, 0.92, 1.0, 0.9)
const SHIP_SYMBOL_COLOR := Color(1.0, 0.8, 0.2)
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
var _pitch := 0.0
var _roll := 0.0
func set_attitude(pitch_deg: float, roll_deg: float) -> void:
_target_pitch = pitch_deg
_target_roll = roll_deg
func _process(delta: float) -> void:
var t := 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
_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
var px_per_deg := radius / PITCH_RANGE
# Screen y grows downward, so nose-up (positive pitch) pushes the horizon
# down the instrument (+y) and reveals more sky.
var horizon_y := clampf(_pitch * px_per_deg, -radius, radius)
var roll_rad := deg_to_rad(_roll)
# Everything inside the dial is drawn in the horizon frame: rotated
# opposite to roll so the horizon stays level while the instrument banks.
draw_set_transform(center, -roll_rad, Vector2.ONE)
# Ground disc, then the sky as the circle segment above the horizon chord.
draw_circle(Vector2.ZERO, radius, GROUND_COLOR)
var sky := PackedVector2Array()
for i in 97:
# Start sampling at the bottom of the circle (excluded region) so the
# kept arc through the top is contiguous.
var ang := PI / 2.0 + float(i) * TAU / 96.0
var p := Vector2(cos(ang), sin(ang)) * radius
if p.y < horizon_y:
sky.append(p)
if sky.size() >= 3:
draw_colored_polygon(sky, SKY_COLOR)
# Horizon line, clipped to the dial.
if absf(horizon_y) < radius:
var half_chord := sqrt(radius * radius - horizon_y * horizon_y)
draw_line(Vector2(-half_chord, horizon_y), Vector2(half_chord, horizon_y), LINE_COLOR, 2.0)
# Pitch ladder: rungs every 10 degrees, labelled, scrolling with pitch.
var font := ThemeDB.fallback_font
for ladder_deg: int in [-30, -20, -10, 10, 20, 30]:
var y := horizon_y - ladder_deg * px_per_deg
if absf(y) > radius - 12.0:
continue
var half_w := 22.0 if ladder_deg % 20 == 0 else 14.0
draw_line(Vector2(-half_w, y), Vector2(half_w, y), LINE_COLOR, 1.0)
draw_string(font, Vector2(half_w + 4.0, y + 4.0), str(absi(ladder_deg)),
HORIZONTAL_ALIGNMENT_LEFT, -1, 10, LINE_COLOR)
# Roll pointer: triangle at the dial's top edge in the horizon frame, so
# it swings against the fixed tick marks as the ship banks.
draw_colored_polygon(PackedVector2Array([
Vector2(0, -radius + 2.0), Vector2(-5, -radius + 12.0), Vector2(5, -radius + 12.0),
]), SHIP_SYMBOL_COLOR)
# Back to the instrument frame for the fixed furniture.
draw_set_transform(center, 0.0, Vector2.ONE)
# Roll scale ticks at 0, +/-30, +/-60 degrees around the top of the dial.
for tick_deg: int in [-60, -30, 0, 30, 60]:
var ang := deg_to_rad(float(tick_deg)) - PI / 2.0
var dir := Vector2(cos(ang), sin(ang))
var tick_len := 8.0 if tick_deg == 0 else 5.0
draw_line(dir * radius, dir * (radius + tick_len), RING_COLOR, 2.0)
# Fixed ship symbol: wings + centre dot, what the ladder is read against.
draw_line(Vector2(-34, 0), Vector2(-10, 0), SHIP_SYMBOL_COLOR, 3.0)
draw_line(Vector2(10, 0), Vector2(34, 0), SHIP_SYMBOL_COLOR, 3.0)
draw_line(Vector2(-10, 0), Vector2(0, 5), SHIP_SYMBOL_COLOR, 3.0)
draw_line(Vector2(0, 5), Vector2(10, 0), SHIP_SYMBOL_COLOR, 3.0)
draw_arc(Vector2.ZERO, radius, 0.0, TAU, 96, RING_COLOR, 2.0, true)
@@ -0,0 +1 @@
uid://dhe6eyuuuqauf
+65
View File
@@ -0,0 +1,65 @@
class_name HudGauge
extends Control
# Generic bar gauge (vertical or horizontal) with a short label and numeric
# readout — used for speed, altitude, and thrust. A dumb display fed by
# HUDController via set_value.
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
@export var fill_color := Color(0.5, 0.85, 1.0, 0.85)
# Printf pattern for the numeric readout.
@export var value_format := "%.0f"
var _target := 0.0
var _value := 0.0
func set_value(value: float) -> void:
_target = value
func _process(delta: float) -> void:
var t := 1.0 - exp(-SMOOTHING * delta) # frame-rate independent
_value = lerpf(_value, _target, t)
queue_redraw()
func _draw() -> void:
var font := ThemeDB.fallback_font
var fraction := clampf(_value / max_value, 0.0, 1.0)
var reading := value_format % _value
if horizontal:
# [label] [======bar======] [value]
var hbar := Rect2(34, 2, size.x - 74, size.y - 4)
draw_string(font, Vector2(0, size.y - 4), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, TEXT_COLOR)
draw_rect(hbar, BG_COLOR)
draw_rect(Rect2(hbar.position, Vector2(hbar.size.x * fraction, hbar.size.y)), fill_color)
draw_rect(hbar, FRAME_COLOR, false, 1.0)
draw_string(font, Vector2(hbar.end.x + 6, size.y - 4), reading, HORIZONTAL_ALIGNMENT_LEFT, -1, 11, TEXT_COLOR)
return
# label / vertical bar filling upward / value
var bar := Rect2(size.x / 2.0 - 9, 16, 18, size.y - 34)
var label_size := font.get_string_size(label, HORIZONTAL_ALIGNMENT_CENTER, -1, 11)
draw_string(font, Vector2(size.x / 2.0 - label_size.x / 2.0, 12), label,
HORIZONTAL_ALIGNMENT_CENTER, -1, 11, TEXT_COLOR)
draw_rect(bar, BG_COLOR)
var fill_h := bar.size.y * fraction
draw_rect(Rect2(Vector2(bar.position.x, bar.end.y - fill_h), Vector2(bar.size.x, fill_h)), fill_color)
draw_rect(bar, FRAME_COLOR, false, 1.0)
# Quarter tick marks up the side of the bar.
for i in range(1, 4):
var y := bar.end.y - bar.size.y * i / 4.0
draw_line(Vector2(bar.position.x - 3, y), Vector2(bar.position.x, y), FRAME_COLOR, 1.0)
var reading_size := font.get_string_size(reading, HORIZONTAL_ALIGNMENT_CENTER, -1, 11)
draw_string(font, Vector2(size.x / 2.0 - reading_size.x / 2.0, size.y - 2), reading,
HORIZONTAL_ALIGNMENT_CENTER, -1, 11, TEXT_COLOR)
+1
View File
@@ -0,0 +1 @@
uid://dkdy0yxxveg7s
+64
View File
@@ -0,0 +1,64 @@
class_name HudHeadingTape
extends Control
# 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
# they're facing. A dumb display fed by HUDController via set_heading.
const BG_COLOR := Color(0.05, 0.07, 0.11, 0.55)
const TICK_COLOR := Color(0.85, 0.92, 1.0, 0.9)
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"}
var _target_heading := 0.0
var _heading := 0.0
func _ready() -> void:
clip_contents = true
func set_heading(heading_deg: float) -> void:
_target_heading = heading_deg
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)
queue_redraw()
func _draw() -> void:
var font := ThemeDB.fallback_font
var center_x := size.x / 2.0
var px_per_deg := size.x / VISIBLE_RANGE
draw_rect(Rect2(Vector2.ZERO, size), BG_COLOR)
# Ticks every 5 degrees across the visible window, majors every 15.
var first := ceili((_heading - VISIBLE_RANGE / 2.0) / 5.0) * 5
for d in range(first, first + int(VISIBLE_RANGE) + 5, 5):
var x := center_x + (float(d) - _heading) * px_per_deg
var shown := wrapi(d, 0, 360)
var major := shown % 15 == 0
draw_line(Vector2(x, 0), Vector2(x, 10.0 if major else 5.0), TICK_COLOR, 1.0)
if major:
var text: String = CARDINALS.get(shown, str(shown))
var text_size := font.get_string_size(text, HORIZONTAL_ALIGNMENT_CENTER, -1, 11)
draw_string(font, Vector2(x - text_size.x / 2.0, 22.0), text,
HORIZONTAL_ALIGNMENT_CENTER, -1, 11,
MARKER_COLOR if CARDINALS.has(shown) else TICK_COLOR)
# Fixed centre marker and numeric readout.
draw_colored_polygon(PackedVector2Array([
Vector2(center_x, 12.0), Vector2(center_x - 5, 2.0), Vector2(center_x + 5, 2.0),
]), MARKER_COLOR)
var reading := "%03d" % (roundi(_heading) % 360)
var reading_size := font.get_string_size(reading, HORIZONTAL_ALIGNMENT_CENTER, -1, 13)
draw_string(font, Vector2(center_x - reading_size.x / 2.0, size.y - 4.0), reading,
HORIZONTAL_ALIGNMENT_CENTER, -1, 13, MARKER_COLOR)
+1
View File
@@ -0,0 +1 @@
uid://dtk57s0hvb4sf
+55 -2
View File
@@ -1,7 +1,57 @@
extends Control extends Control
# Main menu: one handler per game mode. Adding a mode later (e.g. Vs AI) # Main menu: one handler per game mode. Bot dropdowns are populated from
# is a new button + a one-line handler pointing at its scene. # res://bots at runtime so newly trained bots appear automatically — never
# maintain a hardcoded list. Selections are pushed into the GameSettings
# autoload for the target mode to read, and restored when returning to the
# menu within a session.
const BOTS_DIR := "res://bots"
@onready var bot_dropdown: OptionButton = %BotDropdown
@onready var bot_a_dropdown: OptionButton = %BotADropdown
@onready var bot_b_dropdown: OptionButton = %BotBDropdown
func _ready() -> void:
var bots := _list_bots()
_populate_dropdown(bot_dropdown, bots, GameSettings.selected_bot_path)
_populate_dropdown(bot_a_dropdown, bots, GameSettings.spectate_bot_a_path)
_populate_dropdown(bot_b_dropdown, bots, GameSettings.spectate_bot_b_path)
$CenterContainer/VBoxContainer/FreePlayButton.grab_focus()
func _list_bots() -> Array[String]:
var files: Array[String] = []
var dir := DirAccess.open(BOTS_DIR)
if dir:
for f in dir.get_files():
if f.get_extension() == "json":
files.append(f)
files.sort()
return files
# Fills a dropdown with bot names (metadata = full model path). Reselects
# `preferred_path` if it's still on disk, else the newest (last) bot.
func _populate_dropdown(dropdown: OptionButton, bots: Array[String], preferred_path: String) -> void:
dropdown.clear()
for f in bots:
dropdown.add_item(f.get_basename())
dropdown.set_item_metadata(dropdown.item_count - 1, BOTS_DIR + "/" + f)
if dropdown.item_count == 0:
return
dropdown.select(dropdown.item_count - 1)
for i in dropdown.item_count:
if dropdown.get_item_metadata(i) == preferred_path:
dropdown.select(i)
break
func _selected_path(dropdown: OptionButton) -> String:
if dropdown.item_count > 0 and dropdown.selected >= 0:
return dropdown.get_item_metadata(dropdown.selected)
return ""
func _on_free_play_pressed() -> void: func _on_free_play_pressed() -> void:
@@ -9,8 +59,11 @@ func _on_free_play_pressed() -> void:
func _on_match_pressed() -> void: func _on_match_pressed() -> void:
GameSettings.selected_bot_path = _selected_path(bot_dropdown)
get_tree().change_scene_to_file("res://scenes/match.tscn") get_tree().change_scene_to_file("res://scenes/match.tscn")
func _on_spectate_pressed() -> void: func _on_spectate_pressed() -> void:
GameSettings.spectate_bot_a_path = _selected_path(bot_a_dropdown)
GameSettings.spectate_bot_b_path = _selected_path(bot_b_dropdown)
get_tree().change_scene_to_file("res://scenes/spectate.tscn") get_tree().change_scene_to_file("res://scenes/spectate.tscn")
+21 -4
View File
@@ -7,6 +7,11 @@ extends GameMode
signal timer_updated(minutes: int, seconds: int) signal timer_updated(minutes: int, seconds: int)
signal score_changed(score: Dictionary) signal score_changed(score: Dictionary)
# winning_team is -1 for a draw.
signal match_ended(winning_team: int, score: Dictionary)
# How long the result overlay stays up before returning to the menu.
const RESULT_SCREEN_SECONDS := 5.0
@export var match_length_seconds := 150.0 @export var match_length_seconds := 150.0
@@ -36,14 +41,17 @@ func _start() -> void:
func _make_opponent_controller() -> ShipController: func _make_opponent_controller() -> ShipController:
if not bot_model_path.is_empty() and FileAccess.file_exists(bot_model_path): # 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() var bot := AIShipController.new()
bot.model_path = bot_model_path bot.model_path = path
bot.reaction_ticks = bot_reaction_ticks bot.reaction_ticks = bot_reaction_ticks
bot.action_noise = bot_action_noise bot.action_noise = bot_action_noise
return bot return bot
if not bot_model_path.is_empty(): if not path.is_empty():
push_warning("MatchMode: bot model not found at %s, spawning inert opponent" % bot_model_path) push_warning("MatchMode: bot model not found at %s, spawning inert opponent" % path)
return ShipController.new() # inert placeholder return ShipController.new() # inert placeholder
@@ -64,4 +72,13 @@ func _on_goal_scored(conceding_team: int) -> void:
func _on_match_timer_timeout() -> void: func _on_match_timer_timeout() -> void:
print("Full time! Final score: %d - %d" % [score[0], score[1]]) print("Full time! Final score: %d - %d" % [score[0], score[1]])
var winning_team := -1
if score[0] != score[1]:
winning_team = 0 if score[0] > score[1] else 1
match_ended.emit(winning_team, score.duplicate())
# Freeze gameplay while the HUD (process_mode ALWAYS) shows the result.
get_tree().paused = true
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(MAIN_MENU_SCENE_PATH)
+6 -2
View File
@@ -230,8 +230,12 @@ func _emit_telemetry_data():
# Attitude telemetry (pitch, roll, yaw from ship orientation) # Attitude telemetry (pitch, roll, yaw from ship orientation)
# Physics: Euler angles from rotation matrix # Physics: Euler angles from rotation matrix
# Pitch = rotation around X-axis, Roll = rotation around Z-axis # Aviation convention (yaw → pitch → roll, EULER_ORDER_YXZ): pitch stays in
var ship_rotation = global_transform.basis.get_euler(EULER_ORDER_XYZ) # ±90° and yaw covers the full circle. XYZ order would instead constrain
# yaw to ±90°, so an upright ship facing "south" would be reported as
# pitch 180 + roll 180 — mathematically equivalent, but it reads as
# upside-down on the attitude indicator and breaks the heading readout.
var ship_rotation = global_transform.basis.get_euler(EULER_ORDER_YXZ)
var pitch_deg = rad_to_deg(ship_rotation.x) var pitch_deg = rad_to_deg(ship_rotation.x)
var roll_deg = rad_to_deg(ship_rotation.z) var roll_deg = rad_to_deg(ship_rotation.z)
var yaw_deg = rad_to_deg(ship_rotation.y) var yaw_deg = rad_to_deg(ship_rotation.y)
+6 -1
View File
@@ -70,7 +70,12 @@ func _update_ball_cam(delta, ball: Node3D):
var flat := Vector3(ship_pos.x - ball_pos.x, 0.0, ship_pos.z - ball_pos.z) var flat := Vector3(ship_pos.x - ball_pos.x, 0.0, ship_pos.z - ball_pos.z)
if flat.length() > 0.25: if flat.length() > 0.25:
var t := 1.0 - exp(-orbit_smoothing * delta) # frame-rate independent var t := 1.0 - exp(-orbit_smoothing * delta) # frame-rate independent
_orbit_dir = _orbit_dir.slerp(flat.normalized(), t).normalized() # Both directions are horizontal, so swing about the exact UP axis.
# (Vector3.slerp derives its axis from the cross product, which
# degenerates when the directions are nearly parallel — the common
# case here — and spams normalization errors every frame.)
var swing := _orbit_dir.signed_angle_to(flat.normalized(), Vector3.UP)
_orbit_dir = _orbit_dir.rotated(Vector3.UP, swing * t).normalized()
var camera_target_pos := ship_pos + _orbit_dir * camera_distance + Vector3.UP * camera_height var camera_target_pos := ship_pos + _orbit_dir * camera_distance + Vector3.UP * camera_height
camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height) camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height)
+6 -2
View File
@@ -21,8 +21,12 @@ var score := {0: 0, 1: 0}
func _start() -> void: func _start() -> void:
spawn_ball() spawn_ball()
var ship_a := spawn_ship(0, 0, _make_bot(bot_a_model_path, bot_a_reaction_ticks, bot_a_action_noise)) # The main menu's dropdown selections (GameSettings autoload) win over the
spawn_ship(1, 0, _make_bot(bot_b_model_path, bot_b_reaction_ticks, bot_b_action_noise)) # 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))
spawn_camera_rig(ship_a) spawn_camera_rig(ship_a)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More