mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 00:14:00 +00:00
feat(*): Add bot selection, score HUD, and winner reveal to matches, replace HUD text with aircraft-style flight instruments, and fix camera judder
This commit is contained in:
@@ -112,6 +112,7 @@ roll_right={
|
||||
|
||||
[physics]
|
||||
|
||||
common/physics_interpolation=true
|
||||
3d/physics_engine="Jolt Physics"
|
||||
|
||||
[autoload]
|
||||
@@ -121,3 +122,11 @@ roll_right={
|
||||
|
||||
|
||||
|
||||
|
||||
GameSettings="*res://scripts/game_settings.gd"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+133
-33
@@ -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" 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"]
|
||||
font_size = 32
|
||||
|
||||
[node name="HUD" type="CanvasLayer"]
|
||||
process_mode = 3
|
||||
script = ExtResource("1_hud_controller")
|
||||
|
||||
[node name="Control" type="Control" parent="."]
|
||||
@@ -15,6 +19,7 @@ anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="TimerLabel" type="Label" parent="Control"]
|
||||
layout_mode = 1
|
||||
@@ -28,7 +33,37 @@ grow_horizontal = 2
|
||||
label_settings = SubResource("LabelSettings_hud")
|
||||
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
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
@@ -36,48 +71,113 @@ anchor_bottom = 1.0
|
||||
grow_horizontal = 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
|
||||
anchors_preset = 2
|
||||
anchor_top = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 10.0
|
||||
offset_top = -200.0
|
||||
offset_right = 300.0
|
||||
offset_bottom = -10.0
|
||||
offset_left = 16.0
|
||||
offset_top = -248.0
|
||||
offset_right = 294.0
|
||||
offset_bottom = -16.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
|
||||
text = "Speed: 0.0 m/s"
|
||||
label_settings = SubResource("LabelSettings_hud")
|
||||
theme_override_constants/separation = 8
|
||||
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
|
||||
text = "Altitude: 0.0 m"
|
||||
label_settings = SubResource("LabelSettings_hud")
|
||||
mouse_filter = 2
|
||||
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
|
||||
text = "Angular Vel: 0.0 rad/s"
|
||||
label_settings = SubResource("LabelSettings_hud")
|
||||
mouse_filter = 2
|
||||
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
|
||||
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"
|
||||
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")
|
||||
|
||||
@@ -20,8 +20,9 @@ grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
|
||||
custom_minimum_size = Vector2(420, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 24
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="CenterContainer/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
@@ -29,20 +30,100 @@ theme_override_font_sizes/font_size = 48
|
||||
text = "Cosmic Clash"
|
||||
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"]
|
||||
custom_minimum_size = Vector2(330, 60)
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
layout_mode = 2
|
||||
text = "Free Play"
|
||||
|
||||
[node name="MatchButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(330, 60)
|
||||
[node name="FreePlayHint" type="Label" parent="CenterContainer/VBoxContainer"]
|
||||
modulate = Color(1, 1, 1, 0.55)
|
||||
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"
|
||||
|
||||
[node name="SpectateButton" type="Button" parent="CenterContainer/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(330, 60)
|
||||
[node name="MatchHint" type="Label" parent="CenterContainer/VBoxContainer"]
|
||||
modulate = Color(1, 1, 1, 0.55)
|
||||
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/MatchButton" to="." method="_on_match_pressed"]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
[node name="Match" type="Node3D"]
|
||||
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")]
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
[node name="Spectate" type="Node3D"]
|
||||
script = ExtResource("1_s")
|
||||
bot_a_model_path = "res://bots/run02.json"
|
||||
bot_b_model_path = "res://bots/run01.json"
|
||||
bot_a_model_path = "res://bots/run05.json"
|
||||
bot_b_model_path = "res://bots/run05.json"
|
||||
|
||||
[node name="Arena" parent="." instance=ExtResource("2_s")]
|
||||
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
# HUD Controller - handles all display logic and label updates
|
||||
# Receives ship data via signals and updates UI accordingly
|
||||
# Simplified version focusing on labels only
|
||||
# HUD Controller - discovers the ship/camera/game mode via groups and routes
|
||||
# their signals to the display widgets. All flight data is rendered by
|
||||
# aircraft-style instruments (attitude indicator, heading tape, bar gauges) —
|
||||
# this node is only the wiring hub; the instruments are dumb displays.
|
||||
extends CanvasLayer
|
||||
class_name HUDController
|
||||
|
||||
# UI Label References
|
||||
@onready var timer_label = get_node("Control/TimerLabel")
|
||||
@onready var speed_label = get_node("Control/InfoPanel/SpeedLabel")
|
||||
@onready var altitude_label = get_node("Control/InfoPanel/AltitudeLabel")
|
||||
@onready var angular_vel_label = get_node("Control/InfoPanel/AngularVelLabel")
|
||||
@onready var camera_mode_label = get_node("Control/InfoPanel/CameraModeLabel")
|
||||
@onready var attitude_label = get_node("Control/InfoPanel/AttitudeLabel")
|
||||
@onready var heading_label = get_node("Control/InfoPanel/HeadingLabel")
|
||||
@onready var thrust_label = get_node("Control/InfoPanel/ThrustLabel")
|
||||
@onready var score_row = get_node_or_null("Control/ScoreRow")
|
||||
@onready var team0_score_label = get_node_or_null("Control/ScoreRow/Team0Score")
|
||||
@onready var team1_score_label = get_node_or_null("Control/ScoreRow/Team1Score")
|
||||
@onready var result_overlay = get_node_or_null("Control/ResultOverlay")
|
||||
@onready var result_label = get_node_or_null("Control/ResultOverlay/Center/VBox/ResultLabel")
|
||||
@onready var final_score_label = get_node_or_null("Control/ResultOverlay/Center/VBox/FinalScoreLabel")
|
||||
|
||||
@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
|
||||
|
||||
@@ -29,6 +39,11 @@ func _initialize_hud():
|
||||
return
|
||||
|
||||
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()
|
||||
|
||||
# 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):
|
||||
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():
|
||||
# Connect ship signals to label update methods
|
||||
# Route ship telemetry to the flight instruments
|
||||
if ship.has_signal("speed_changed"):
|
||||
ship.speed_changed.connect(_on_ship_speed_changed)
|
||||
print("HUDController: Connected to speed_changed signal")
|
||||
if ship.has_signal("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"):
|
||||
ship.attitude_changed.connect(_on_ship_attitude_changed)
|
||||
if ship.has_signal("heading_changed"):
|
||||
@@ -62,38 +85,33 @@ func _connect_ship_signals():
|
||||
if ship.has_signal("thrust_changed"):
|
||||
ship.thrust_changed.connect(_on_ship_thrust_changed)
|
||||
|
||||
# Signal handlers for ship data - these update the legacy text labels
|
||||
# All handlers include null checks for robustness
|
||||
# Instrument signal handlers - all include null checks for robustness
|
||||
func _on_ship_speed_changed(speed: float):
|
||||
if speed_label and is_instance_valid(speed_label):
|
||||
speed_label.text = "Speed: %.1f m/s" % speed
|
||||
if speed_gauge and is_instance_valid(speed_gauge):
|
||||
speed_gauge.set_value(speed)
|
||||
|
||||
func _on_ship_altitude_changed(altitude: float):
|
||||
if altitude_label and is_instance_valid(altitude_label):
|
||||
altitude_label.text = "Altitude: %.1f m" % altitude
|
||||
if altitude_gauge and is_instance_valid(altitude_gauge):
|
||||
altitude_gauge.set_value(altitude)
|
||||
|
||||
func _on_ship_angular_velocity_changed(angular_speed: float):
|
||||
if angular_vel_label and is_instance_valid(angular_vel_label):
|
||||
angular_vel_label.text = "Angular Vel: %.2f rad/s" % angular_speed
|
||||
func _on_ship_attitude_changed(pitch: float, roll: float, _yaw: float):
|
||||
if attitude_indicator and is_instance_valid(attitude_indicator):
|
||||
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):
|
||||
if camera_mode_label and is_instance_valid(camera_mode_label):
|
||||
var camera_mode = "Ball Cam" if is_ball_cam else "Ship Cam"
|
||||
camera_mode_label.text = "Camera: %s" % camera_mode
|
||||
|
||||
func _on_ship_attitude_changed(pitch: float, roll: float, _yaw: float):
|
||||
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)
|
||||
# Timer signal handler from game manager
|
||||
func _on_timer_updated(minutes: int, seconds: int):
|
||||
if timer_label and is_instance_valid(timer_label):
|
||||
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):
|
||||
if timer_label and is_instance_valid(timer_label):
|
||||
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
|
||||
|
||||
@@ -101,6 +101,9 @@ func _reset_body(body: RigidBody3D, to: Transform3D) -> void:
|
||||
body.set_deferred("global_transform", to)
|
||||
body.set_deferred("linear_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):
|
||||
|
||||
@@ -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 = ""
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv4wiy0oor065
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkdy0yxxveg7s
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dtk57s0hvb4sf
|
||||
@@ -1,7 +1,57 @@
|
||||
extends Control
|
||||
|
||||
# Main menu: one handler per game mode. Adding a mode later (e.g. Vs AI)
|
||||
# is a new button + a one-line handler pointing at its scene.
|
||||
# Main menu: one handler per game mode. Bot dropdowns are populated from
|
||||
# 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:
|
||||
@@ -9,8 +59,11 @@ func _on_free_play_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")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@@ -7,6 +7,11 @@ extends GameMode
|
||||
|
||||
signal timer_updated(minutes: int, seconds: int)
|
||||
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
|
||||
|
||||
@@ -36,14 +41,17 @@ func _start() -> void:
|
||||
|
||||
|
||||
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()
|
||||
bot.model_path = bot_model_path
|
||||
bot.model_path = path
|
||||
bot.reaction_ticks = bot_reaction_ticks
|
||||
bot.action_noise = bot_action_noise
|
||||
return bot
|
||||
if not bot_model_path.is_empty():
|
||||
push_warning("MatchMode: bot model not found at %s, spawning inert opponent" % bot_model_path)
|
||||
if not path.is_empty():
|
||||
push_warning("MatchMode: bot model not found at %s, spawning inert opponent" % path)
|
||||
return ShipController.new() # inert placeholder
|
||||
|
||||
|
||||
@@ -64,4 +72,13 @@ func _on_goal_scored(conceding_team: int) -> void:
|
||||
|
||||
func _on_match_timer_timeout() -> void:
|
||||
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)
|
||||
|
||||
@@ -230,8 +230,12 @@ func _emit_telemetry_data():
|
||||
|
||||
# Attitude telemetry (pitch, roll, yaw from ship orientation)
|
||||
# Physics: Euler angles from rotation matrix
|
||||
# Pitch = rotation around X-axis, Roll = rotation around Z-axis
|
||||
var ship_rotation = global_transform.basis.get_euler(EULER_ORDER_XYZ)
|
||||
# Aviation convention (yaw → pitch → roll, EULER_ORDER_YXZ): pitch stays in
|
||||
# ±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 roll_deg = rad_to_deg(ship_rotation.z)
|
||||
var yaw_deg = rad_to_deg(ship_rotation.y)
|
||||
|
||||
@@ -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)
|
||||
if flat.length() > 0.25:
|
||||
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
|
||||
camera_target_pos.y = maxf(camera_target_pos.y, min_camera_height)
|
||||
|
||||
@@ -21,8 +21,12 @@ var score := {0: 0, 1: 0}
|
||||
|
||||
func _start() -> void:
|
||||
spawn_ball()
|
||||
var ship_a := spawn_ship(0, 0, _make_bot(bot_a_model_path, bot_a_reaction_ticks, bot_a_action_noise))
|
||||
spawn_ship(1, 0, _make_bot(bot_b_model_path, bot_b_reaction_ticks, bot_b_action_noise))
|
||||
# The main menu's dropdown selections (GameSettings autoload) win over the
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user