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:
Josh Creek
2026-07-20 07:12:09 +01:00
parent 48369c50dc
commit 240e362f2f
20 changed files with 653 additions and 91 deletions
+73 -37
View File
@@ -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