mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-17 07:12:02 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cb5902eb6 | |||
| 240e362f2f | |||
| 48369c50dc | |||
| b8f5f4938d |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
|
||||
|
||||
|
||||
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
Reference in New Issue
Block a user