From 7fea0b920f19dc33cf5f43270a24ec32e9742d23 Mon Sep 17 00:00:00 2001 From: Jesse Hernandez Date: Fri, 13 Mar 2026 00:17:10 -0700 Subject: [PATCH 1/5] feat: target yardage, added club selection. --- courses/RangeCourse.cs | 47 ++++ game/ShotRecordingService.cs | 25 +- game/hole/HoleSceneControllerBase.cs | 63 ++++- ui/CourseHud.cs | 218 +++++++++++++++++- ui/GameplayUI.cs | 35 +++ ui/MarkerHUD.cs | 20 ++ ui/SettingsPanel.cs | 62 +++++ ui/course_hud.tscn | 80 +++++++ ui/settings_panel.tscn | 43 ++++ utils/RangeClubCatalog.cs | 41 ++++ utils/RangeClubCatalog.cs.uid | 1 + utils/Settings/AppSettings.cs | 6 +- .../Settings/AppSettingsPersistenceService.cs | 2 + 13 files changed, 637 insertions(+), 6 deletions(-) create mode 100644 utils/RangeClubCatalog.cs create mode 100644 utils/RangeClubCatalog.cs.uid diff --git a/courses/RangeCourse.cs b/courses/RangeCourse.cs index 42be4bd..60bb7e3 100644 --- a/courses/RangeCourse.cs +++ b/courses/RangeCourse.cs @@ -7,6 +7,7 @@ public partial class RangeCourse : HoleSceneControllerBase { private const float YardsToMeters = 1.0f / ShotSetup.YARDS_PER_METER; + private Vector3 _teePoint = GolfBall.START_POSITION; [ExportGroup("Range Surface")] [Export] public NodePath SurfaceGridPath { get; set; } = new NodePath("SurfaceGrid"); @@ -42,6 +43,31 @@ protected override bool ShouldShowCourseMeta() return false; } + protected override bool ShouldShowTargetElevation() + { + return false; + } + + protected override bool ShouldShowRangeHudControls() + { + return true; + } + + protected override int GetRangeTargetMinYards() + { + return 5; + } + + protected override int GetRangeTargetMaxYards() + { + return 350; + } + + protected override int GetDefaultRangeTargetYards() + { + return 100; + } + protected override bool ShouldShowTracerHistorySetting() { return true; @@ -72,8 +98,29 @@ protected override bool ShouldClearTracersOnBallReset() return false; } + protected override Vector3? ResolveDistanceReferencePoint() + { + int selectedYards = GameplayUi != null ? GameplayUi.GetRangeTargetYardage() : GetDefaultRangeTargetYards(); + selectedYards = Mathf.Clamp(selectedYards, GetRangeTargetMinYards(), GetRangeTargetMaxYards()); + float meters = selectedYards * YardsToMeters; + + return _teePoint + new Vector3(meters, 0.0f, 0.0f); + } + + protected override string ResolveShotRecordingClubTag() + { + if (GameplayUi == null) + return RangeClubCatalog.ToFileTag(AppSettings.DefaultRangeDefaultClub); + + return GameplayUi.GetRangeSelectedClubFileTag(); + } + protected override void OnHoleReadyAfterInit() { + GolfBall ball = GetNodeOrNull(BallNodePath); + if (ball != null) + _teePoint = ball.GlobalPosition; + ExtendFairwaySurface(); } diff --git a/game/ShotRecordingService.cs b/game/ShotRecordingService.cs index af865c9..d8fcd7a 100644 --- a/game/ShotRecordingService.cs +++ b/game/ShotRecordingService.cs @@ -1,5 +1,6 @@ using System.IO; using System.Text.Json; +using System.Text; using Godot; using Godot.Collections; @@ -39,7 +40,7 @@ public override void _ExitTree() _recordingEnabledSetting.SettingChanged -= OnRecordingEnabledChanged; } - public void RecordShot(Dictionary ballData) + public void RecordShot(Dictionary ballData, string clubTag = "") { if (!_isRecording || ballData == null) return; @@ -50,7 +51,11 @@ public void RecordShot(Dictionary ballData) _shotCounter++; var shotJson = BuildShotJson(ballData); - string filePath = Path.Combine(_currentSessionPath, $"shot_{_shotCounter}.json"); + string safeClubTag = SanitizeClubTag(clubTag); + string fileName = string.IsNullOrWhiteSpace(safeClubTag) + ? $"shot_{_shotCounter}.json" + : $"shot_{safeClubTag}_{_shotCounter}.json"; + string filePath = Path.Combine(_currentSessionPath, fileName); try { @@ -63,6 +68,22 @@ public void RecordShot(Dictionary ballData) } } + private static string SanitizeClubTag(string clubTag) + { + if (string.IsNullOrWhiteSpace(clubTag)) + return string.Empty; + + string lower = clubTag.Trim().ToLowerInvariant(); + var builder = new StringBuilder(lower.Length); + foreach (char c in lower) + { + if (char.IsLetterOrDigit(c) || c == '_') + builder.Append(c); + } + + return builder.ToString(); + } + private void OnRecordingEnabledChanged(Variant value) { bool enabled = (bool)value; diff --git a/game/hole/HoleSceneControllerBase.cs b/game/hole/HoleSceneControllerBase.cs index f7cc6ff..5eeea35 100644 --- a/game/hole/HoleSceneControllerBase.cs +++ b/game/hole/HoleSceneControllerBase.cs @@ -89,6 +89,7 @@ private enum StartupStage } private bool IsGoalCountdownRunning => _goalCompletionFlow != null && _goalCompletionFlow.IsRunning; + protected GameplayUI GameplayUi => _gameplayUi; protected virtual void OnHoleReadyAfterInit() { @@ -159,6 +160,36 @@ protected virtual bool ShouldShowCourseMeta() return true; } + protected virtual bool ShouldShowTargetElevation() + { + return true; + } + + protected virtual bool ShouldShowRangeHudControls() + { + return false; + } + + protected virtual int GetRangeTargetMinYards() + { + return 5; + } + + protected virtual int GetRangeTargetMaxYards() + { + return 350; + } + + protected virtual int GetDefaultRangeTargetYards() + { + return 100; + } + + protected virtual string ResolveShotRecordingClubTag() + { + return string.Empty; + } + protected virtual bool ShouldShowTracerHistorySetting() { return false; @@ -293,6 +324,7 @@ private void InitializeCoreStage() _gameSettings.CameraFollowMode.SettingChanged += OnCameraFollowChanged; _gameSettings.SurfaceType.SettingChanged += OnSurfaceChanged; ConfigureTracerBehavior(); + ConfigureRangeHudBehavior(); _ball.ResolveLieSurface = ResolveLieSurfaceAtContact; _ball.DescribeLieSurfaceResolution = () => _lieSurfaceResolver.DescribeLastResolution(); _cameraOrbitDistanceSetting = _appSettings?.CameraOrbitDistance; @@ -611,7 +643,7 @@ private void LaunchShot(Dictionary data, bool useTcpTracker, bool logPayload) UpdateBallDisplay(); PlayDriverHitAudio(); IncrementStrokeCount(); - _shotRecordingService?.RecordShot(data); + _shotRecordingService?.RecordShot(data, ResolveShotRecordingClubTag()); if (useTcpTracker) _shotTracker.OnTcpClientHitBall(data); @@ -735,6 +767,32 @@ private void ConfigureTracerBehavior() _gameplayUi?.SetTracerHistorySettingVisible(ShouldShowTracerHistorySetting()); } + private void ConfigureRangeHudBehavior() + { + if (_gameplayUi == null) + return; + + bool showRangeHudControls = ShouldShowRangeHudControls(); + _gameplayUi.SetRangeHudControlsVisible(showRangeHudControls); + _gameplayUi.SetRangeDefaultClubSettingVisible(showRangeHudControls); + _gameplayUi.SetTargetElevationVisible(ShouldShowTargetElevation()); + _gameplayUi.SetMarkerElevationVisible(ShouldShowTargetElevation()); + + if (!showRangeHudControls) + return; + + string defaultClub = _appSettings != null + ? _appSettings.RangeDefaultClub.Value.ToString() + : AppSettings.DefaultRangeDefaultClub; + + _gameplayUi.ConfigureRangeHudControls( + GetRangeTargetMinYards(), + GetRangeTargetMaxYards(), + GetDefaultRangeTargetYards(), + defaultClub + ); + } + private float GetCameraOrbitDistanceSetting() { if (_appSettings == null) @@ -1269,7 +1327,8 @@ private void MaybeRefreshTargetHud(double delta, bool force = false) private void RefreshTargetHud() { UpdateTargetYardageDisplay(); - UpdateTargetElevationDisplay(); + if (ShouldShowTargetElevation()) + UpdateTargetElevationDisplay(); } private void UpdateTargetYardageDisplay() diff --git a/ui/CourseHud.cs b/ui/CourseHud.cs index 5ae5130..9bba317 100644 --- a/ui/CourseHud.cs +++ b/ui/CourseHud.cs @@ -14,6 +14,9 @@ public partial class CourseHud : Control private const int DefaultHoleNumber = 1; private const int DefaultPar = 3; private const int DefaultYardage = 203; + private const int DefaultRangeTargetYards = 100; + private const int DefaultRangeTargetMinYards = 5; + private const int DefaultRangeTargetMaxYards = 350; private string _selectedShotPath = TestShots.DefaultShot; private GridCanvas _gridCanvas; @@ -29,7 +32,12 @@ public partial class CourseHud : Control private Control _courseMetaBar; private Control _courseMetaSpacer; private Label _playerNameLabel; + private Control _rangeControlsBar; + private HSlider _rangeTargetSlider; + private SpinBox _rangeTargetStepper; + private OptionButton _rangeClubOption; private Label _shotLabel; + private Label _targetLabel; private Label _targetYardageLabel; private Label _targetElevationLabel; private Label _roundEndScoreOverlay; @@ -40,6 +48,12 @@ public partial class CourseHud : Control private Setting _shotInjectorSetting; private Setting _testShotsEnabledSetting; private Setting _playerNameSetting; + private Setting _rangeDefaultClubSetting; + private bool _isRangeHudControlsVisible; + private bool _isSyncingRangeControls; + private int _rangeTargetMinYards = DefaultRangeTargetMinYards; + private int _rangeTargetMaxYards = DefaultRangeTargetMaxYards; + private int _rangeTargetYards = DefaultRangeTargetYards; private DataPanel _panelDistance; private DataPanel _panelCarry; @@ -63,11 +77,14 @@ public override void _Ready() _shotInjectorSetting = globalSettings.GameSettings.ShotInjectorEnabled; _testShotsEnabledSetting = globalSettings.AppSettings?.TestShotsEnabled; _playerNameSetting = globalSettings.AppSettings?.PlayerName; + _rangeDefaultClubSetting = globalSettings.AppSettings?.RangeDefaultClub; _shotInjectorSetting.SettingChanged += OnShotInjectorSettingChanged; if (_testShotsEnabledSetting != null) _testShotsEnabledSetting.SettingChanged += OnTestShotsEnabledSettingChanged; if (_playerNameSetting != null) _playerNameSetting.SettingChanged += OnPlayerNameSettingChanged; + if (_rangeDefaultClubSetting != null) + _rangeDefaultClubSetting.SettingChanged += OnRangeDefaultClubSettingChanged; _shotInjector = GetNode("ShotInjector"); _shotInjector.Inject += OnShotInjectorInject; @@ -84,7 +101,12 @@ public override void _Ready() _courseMetaBar = GetNodeOrNull("OverlayLayer/CourseHeaderCard/InfoBlock/CourseMetaBar"); _courseMetaSpacer = GetNodeOrNull("OverlayLayer/CourseHeaderCard/InfoBlock/CourseMetaBar/MetaHBox/MetaSpacer"); _playerNameLabel = GetNode