diff --git a/Game/scenes/HUD.tscn b/Game/scenes/HUD.tscn index c38b96ca..bad3f146 100644 --- a/Game/scenes/HUD.tscn +++ b/Game/scenes/HUD.tscn @@ -71,15 +71,12 @@ theme_override_constants/separation = 16 [node name="Team0Name" type="Label" parent="Control/ScoreboardPanel/ScoreRow"] layout_mode = 2 size_flags_vertical = 4 -theme_override_colors/font_color = Color(0.25, 0.55, 1, 1) theme_override_font_sizes/font_size = 14 horizontal_alignment = 1 -text = "BLUE" [node name="Team0Score" type="Label" parent="Control/ScoreboardPanel/ScoreRow"] layout_mode = 2 size_flags_vertical = 4 -theme_override_colors/font_color = Color(0.25, 0.55, 1, 1) theme_override_font_sizes/font_size = 32 horizontal_alignment = 1 text = "0" @@ -93,7 +90,6 @@ horizontal_alignment = 1 [node name="Team1Score" type="Label" parent="Control/ScoreboardPanel/ScoreRow"] layout_mode = 2 size_flags_vertical = 4 -theme_override_colors/font_color = Color(1, 0.5, 0.15, 1) theme_override_font_sizes/font_size = 32 horizontal_alignment = 1 text = "0" @@ -101,10 +97,8 @@ text = "0" [node name="Team1Name" type="Label" parent="Control/ScoreboardPanel/ScoreRow"] layout_mode = 2 size_flags_vertical = 4 -theme_override_colors/font_color = Color(1, 0.5, 0.15, 1) theme_override_font_sizes/font_size = 14 horizontal_alignment = 1 -text = "ORANGE" [node name="KickoffLabel" type="Label" parent="Control"] visible = false diff --git a/Game/scripts/HUDController.gd b/Game/scripts/HUDController.gd index 35baf803..c7e08ea0 100644 --- a/Game/scripts/HUDController.gd +++ b/Game/scripts/HUDController.gd @@ -24,10 +24,6 @@ class_name HUDController @onready var thrust_bar: HudGauge = get_node_or_null("Control/Instruments/Cluster/ThrustBar") @onready var camera_mode_label = get_node_or_null("Control/Instruments/Cluster/CameraModeLabel") -# Team identity, mirroring TEAM_COLORS in ship.gd -const TEAM_NAMES := {0: "Blue", 1: "Orange"} -const TEAM_COLORS := {0: Color(0.25, 0.55, 1.0), 1: Color(1.0, 0.5, 0.15)} - var ship: Node var _last_score := {0: 0, 1: 0} @@ -66,6 +62,20 @@ func _initialize_hud(): if timer_label and is_instance_valid(timer_label): timer_label.visible = has_timer + # Team identity (name + color) is fixed regardless of whether this + # mode tracks score, so set it here rather than baking it into the + # scene where it can't track TeamColors. + if team0_name_label and is_instance_valid(team0_name_label): + team0_name_label.text = TeamColors.TEAM_NAMES[0] + team0_name_label.add_theme_color_override("font_color", TeamColors.TEAM_COLORS[0]) + if team0_score_label and is_instance_valid(team0_score_label): + team0_score_label.add_theme_color_override("font_color", TeamColors.TEAM_COLORS[0]) + if team1_score_label and is_instance_valid(team1_score_label): + team1_score_label.add_theme_color_override("font_color", TeamColors.TEAM_COLORS[1]) + if team1_name_label and is_instance_valid(team1_name_label): + team1_name_label.text = TeamColors.TEAM_NAMES[1] + team1_name_label.add_theme_color_override("font_color", TeamColors.TEAM_COLORS[1]) + # 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") @@ -177,9 +187,9 @@ func _on_match_ended(winning_team: int, score: Dictionary): result_label.remove_theme_color_override("font_color") _set_result_panel_accent(Color(1, 1, 1, 0.2)) else: - result_label.text = "%s team wins!" % TEAM_NAMES[winning_team] - result_label.add_theme_color_override("font_color", TEAM_COLORS[winning_team]) - _set_result_panel_accent(TEAM_COLORS[winning_team]) + result_label.text = "%s team wins!" % TeamColors.TEAM_NAMES[winning_team] + result_label.add_theme_color_override("font_color", TeamColors.TEAM_COLORS[winning_team]) + _set_result_panel_accent(TeamColors.TEAM_COLORS[winning_team]) final_score_label.text = "%d - %d" % [score.get(0, 0), score.get(1, 0)] result_overlay.visible = true _animate_result_panel_in() diff --git a/Game/scripts/arena_boundary.gd b/Game/scripts/arena_boundary.gd index de7446d4..10f2a104 100644 --- a/Game/scripts/arena_boundary.gd +++ b/Game/scripts/arena_boundary.gd @@ -91,8 +91,8 @@ const DECK_SHADER_PATH := "res://shaders/arena_deck.gdshader" @export var field_tint := Color(0.45, 0.65, 1.0) @export var field_intensity := 0.09 -@export var team0_tint := Color(0.15, 0.45, 1.0) -@export var team1_tint := Color(1.0, 0.35, 0.25) +@export var team0_tint := TeamColors.TEAM_COLORS[0] +@export var team1_tint := TeamColors.TEAM_COLORS[1] # The single merged surface shell and the material whose camera-side fade # _process() drives. Both stay null in headless runs, which never render. diff --git a/Game/scripts/goal.gd b/Game/scripts/goal.gd index 65312b42..c966270e 100644 --- a/Game/scripts/goal.gd +++ b/Game/scripts/goal.gd @@ -10,9 +10,6 @@ extends Area3D signal goal_scored(team: int) -# Frame tints, indexed by team. -const TEAM_COLORS := [Color(0.2, 0.55, 1.0), Color(1.0, 0.4, 0.28)] - # The pocket is sunk into the end wall, so it can be at most as deep as that # wall is thick or it pokes out the back of the arena. ArenaBoundary cuts the # matching aperture in the hull (see its GOAL_APERTURE_* constants). @@ -52,7 +49,7 @@ func _on_body_entered(body): func _build_visuals() -> void: var mouth := ($CollisionShape3D.shape as BoxShape3D).size var half := Vector2(mouth.x, mouth.y) / 2.0 - var tint: Color = TEAM_COLORS[team % TEAM_COLORS.size()] + var tint: Color = TeamColors.TEAM_COLORS[team % TeamColors.TEAM_COLORS.size()] # One ArrayMesh, four surfaces (pocket, net, bezel, rim). They stay separate # surfaces rather than sharing materials because they aren't visually diff --git a/Game/scripts/ship.gd b/Game/scripts/ship.gd index f6b858a2..7cbc0cd1 100644 --- a/Game/scripts/ship.gd +++ b/Game/scripts/ship.gd @@ -25,13 +25,6 @@ extends RigidBody3D @export var ceiling_pull_strength = 11.5 # Ceiling grav-plating strength; nets above gravity so a ship can hold a ceiling @export var ceiling_pull_range = 3.0 # Metres from the ceiling where pull begins -# Accent colours per team, applied to the nose and tail fin meshes so the -# two sides are tellable apart at a glance. -const TEAM_COLORS := { - 0: Color(0.25, 0.55, 1.0), - 1: Color(1.0, 0.5, 0.15), -} - # Non-tinted hull meshes, runtime-merged into one ArrayMesh by # _build_merged_hull() (Nose/TailFin stay separate MeshInstance3Ds since # _apply_team_color() retints them per-team and must keep addressing them by @@ -73,7 +66,7 @@ static var _team_materials: Dictionary = {} # team:int -> StandardMaterial3D static func _get_team_material(team: int) -> StandardMaterial3D: if _team_materials.has(team): return _team_materials[team] - var color: Color = TEAM_COLORS.get(team, TEAM_COLORS[0]) + var color: Color = TeamColors.TEAM_COLORS.get(team, TeamColors.TEAM_COLORS[0]) var accent := StandardMaterial3D.new() accent.albedo_color = color accent.metallic = 0.3 diff --git a/Game/scripts/team_colors.gd b/Game/scripts/team_colors.gd new file mode 100644 index 00000000..a8cba5e7 --- /dev/null +++ b/Game/scripts/team_colors.gd @@ -0,0 +1,10 @@ +class_name TeamColors + +# Single source of truth for team identity. Ship nose/tailfin, HUD result +# panel, goal rim/net, and the arena end-zone floor shader all read from +# here — do not restate these values locally (see TODO.md history). +const TEAM_NAMES := {0: "Purple", 1: "Green"} +const TEAM_COLORS := { + 0: Color(0.48, 0.18, 0.88), + 1: Color(0.24, 0.86, 0.42), +} diff --git a/Game/scripts/team_colors.gd.uid b/Game/scripts/team_colors.gd.uid new file mode 100644 index 00000000..312bea60 --- /dev/null +++ b/Game/scripts/team_colors.gd.uid @@ -0,0 +1 @@ +uid://c6r8q1dpd8klq diff --git a/Game/shaders/arena_deck.gdshader b/Game/shaders/arena_deck.gdshader index 20470907..49fa6ef1 100644 --- a/Game/shaders/arena_deck.gdshader +++ b/Game/shaders/arena_deck.gdshader @@ -21,8 +21,12 @@ uniform vec3 panel_color : source_color = vec3(0.104, 0.116, 0.146); uniform float hull_fill : hint_range(0.0, 2.0) = 0.55; uniform float panel_variation : hint_range(0.0, 1.0) = 0.4; uniform vec3 line_color : source_color = vec3(0.62, 0.78, 1.0); -uniform vec3 team0_color : source_color = vec3(0.15, 0.45, 1.0); -uniform vec3 team1_color : source_color = vec3(1.0, 0.35, 0.25); +// Must track TeamColors.TEAM_COLORS in scripts/team_colors.gd — shaders can't +// import GDScript consts, and arena_boundary.gd always overrides these at +// runtime, but keep the literal defaults in sync for anyone inspecting the +// shader/material directly. +uniform vec3 team0_color : source_color = vec3(0.48, 0.18, 0.88); +uniform vec3 team1_color : source_color = vec3(0.24, 0.86, 0.42); uniform vec3 seam_color : source_color = vec3(0.35, 0.75, 1.0); // Play-volume dimensions, from ArenaBoundary's constants. diff --git a/TODO.md b/TODO.md index 40158eaa..480a2219 100644 --- a/TODO.md +++ b/TODO.md @@ -15,7 +15,7 @@ The training pipeline is built — see `TRAINING.md` (self-play PPO via the vend Bugs found in an adversarial review. None are gameplay- or physics-affecting, so all are safe to land against the current `Game/bots/` checkpoints. - [x] `VideoSettings.apply_to_environment()` (`scripts/video_settings.gd`) compounds on every arena load: `env.glow_intensity *= glow_scale` mutates an `Environment` that is a `[sub_resource]` of the arena scene, and Godot shares sub-resources across instantiations of a cached `PackedScene`. Glow at 50% becomes 25% then 12.5% across repeat entries. Fix by duplicating the Environment in `Arena._ready()` (`scripts/arena.gd`). Regression test: set glow to 50%, enter/leave Free Play three times, confirm it's still 50%. -- [ ] Collapse the four disagreeing team palettes into one source of truth — `ship.gd`, `HUDController.gd` and `goal.gd` each declare `TEAM_COLORS`, `arena_boundary.gd` exports `team0_tint`/`team1_tint`, and `arena_deck.gdshader` defaults to a fifth pair. Three of them disagree, so nose, goal rim, end zone and scoreboard are all different blues. +- [x] Collapse the four disagreeing team palettes into one source of truth — `ship.gd`, `HUDController.gd` and `goal.gd` each declare `TEAM_COLORS`, `arena_boundary.gd` exports `team0_tint`/`team1_tint`, and `arena_deck.gdshader` defaults to a fifth pair. Three of them disagree, so nose, goal rim, end zone and scoreboard are all different blues. - [ ] Goal scoring volume (3.5 x 1.5, `objects/goal.tscn`) is smaller than the drawn mouth (3.7 x 1.65, `ArenaBoundary.GOAL_APERTURE_*`) — a ball crossing the visible edge doesn't score. Derive the aperture constants from the goal's collision shape, the way `goal.gd:53` already measures its own visuals. - [ ] `match_mode.gd`: full time can fire mid-kickoff-countdown, and the stalled coroutine resumes into the dying scene (can re-emit `kickoff_countdown` / unfreeze bodies for a frame). Guard `_run_kickoff_countdown` with a match-over flag. - [ ] `match_mode.gd` emits `timer_updated` every frame for a value that changes once a second; the HUD re-formats and re-shapes the label each time. Emit only on change, matching `ship.gd`'s threshold-gated telemetry discipline.