Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
653 changes: 14 additions & 639 deletions addons/openfairway/physics/BallPhysics.cs

Large diffs are not rendered by default.

550 changes: 550 additions & 0 deletions addons/openfairway/physics/BounceCalculator.cs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions addons/openfairway/physics/BounceCalculator.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://ceyra2rahbv4l
1 change: 1 addition & 0 deletions addons/openfairway/physics/RolloutProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public sealed class RolloutProfile

// --- Friction blending ---
public float FrictionBlendSpeed { get; init; } = 15.0f;
public float TangentVelocityThreshold { get; init; } = 0.05f;

// --- Metadata ---
public string Name { get; init; } = "Default";
Expand Down
80 changes: 15 additions & 65 deletions game/GolfBall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -320,43 +320,23 @@ private bool CheckOutOfBounds()

private bool TryRecoverToGround()
{
var world = GetWorld3D();
if (world == null)
var probe = TerrainProbe.Raycast(GetWorld3D(), GlobalPosition, GROUND_RAYCAST_UP, GROUND_RAYCAST_DOWN, GetRaycastExclude());
if (probe is not { } hit)
return false;

Vector3 rayStart = GlobalPosition + Vector3.Up * GROUND_RAYCAST_UP;
Vector3 rayEnd = GlobalPosition + Vector3.Down * GROUND_RAYCAST_DOWN;

var query = PhysicsRayQueryParameters3D.Create(rayStart, rayEnd);
query.CollideWithAreas = false;
query.CollideWithBodies = true;
query.Exclude = GetRaycastExclude();

var hit = world.DirectSpaceState.IntersectRay(query);
if (hit.Count == 0)
return false;

Vector3 hitPosition = (Vector3)hit["position"];
Vector3 hitNormal = ((Vector3)hit["normal"]).Normalized();
Node hitCollider = hit.ContainsKey("collider") && hit["collider"].Obj is Node collider
? collider
: null;
if (hitNormal.LengthSquared() < 0.000001f)
hitNormal = Vector3.Up;

GlobalPosition = hitPosition + hitNormal * (BallPhysics.RADIUS + GROUND_SNAP_OFFSET);
FloorNormal = hitNormal;
Velocity = RemoveVelocityAlongNormal(Velocity, hitNormal, removeBothDirections: false);
GlobalPosition = hit.Position + hit.Normal * (BallPhysics.RADIUS + GROUND_SNAP_OFFSET);
FloorNormal = hit.Normal;
Velocity = RemoveVelocityAlongNormal(Velocity, hit.Normal, removeBothDirections: false);
OnGround = true;
UpdateLieSurfaceFromContact(hitCollider, hitPosition);
UpdateLieSurfaceFromContact(hit.Collider, hit.Position);

if (State == PhysicsEnums.BallState.Flight)
{
State = PhysicsEnums.BallState.Rollout;
EmitSignal(SignalName.BallLanded);
}

PhysicsLogger.Verbose($"Recovered ball-to-ground at {GlobalPosition} (normal: {hitNormal})");
PhysicsLogger.Verbose($"Recovered ball-to-ground at {GlobalPosition} (normal: {hit.Normal})");
return true;
}

Expand All @@ -366,30 +346,13 @@ private bool TryProbeGround(out Vector3 groundNormal, out Node groundCollider, o
groundCollider = null;
groundPoint = GlobalPosition;

var world = GetWorld3D();
if (world == null)
var probe = TerrainProbe.Raycast(GetWorld3D(), GlobalPosition, 0.05f, BallPhysics.RADIUS + GROUND_PROBE_DISTANCE, GetRaycastExclude());
if (probe is not { } hit)
return false;

Vector3 rayStart = GlobalPosition + Vector3.Up * 0.05f;
Vector3 rayEnd = GlobalPosition + Vector3.Down * (BallPhysics.RADIUS + GROUND_PROBE_DISTANCE);

var query = PhysicsRayQueryParameters3D.Create(rayStart, rayEnd);
query.CollideWithAreas = false;
query.CollideWithBodies = true;
query.Exclude = GetRaycastExclude();

var hit = world.DirectSpaceState.IntersectRay(query);
if (hit.Count == 0)
return false;

groundPoint = (Vector3)hit["position"];
groundNormal = ((Vector3)hit["normal"]).Normalized();
groundCollider = hit.ContainsKey("collider") && hit["collider"].Obj is Node collider
? collider
: null;
if (groundNormal.LengthSquared() < 0.000001f)
groundNormal = Vector3.Up;

groundPoint = hit.Position;
groundNormal = hit.Normal;
groundCollider = hit.Collider;
return true;
}

Expand Down Expand Up @@ -536,24 +499,11 @@ public void SnapToGround()
}

// Fallback: physics raycast (requires collision shapes to be ready).
var world = GetWorld3D();
if (world == null)
return;

Vector3 rayStart = GlobalPosition + Vector3.Up * 50.0f;
Vector3 rayEnd = GlobalPosition + Vector3.Down * 50.0f;

var query = PhysicsRayQueryParameters3D.Create(rayStart, rayEnd);
query.CollideWithAreas = false;
query.CollideWithBodies = true;
query.Exclude = GetRaycastExclude();

var hit = world.DirectSpaceState.IntersectRay(query);
if (hit.Count == 0)
var probe = TerrainProbe.Raycast(GetWorld3D(), GlobalPosition, 50.0f, 50.0f, GetRaycastExclude());
if (probe is not { } hit)
return;

Vector3 hitPosition = (Vector3)hit["position"];
GlobalPosition = new Vector3(GlobalPosition.X, hitPosition.Y + TEE_HEIGHT, GlobalPosition.Z);
GlobalPosition = new Vector3(GlobalPosition.X, hit.Position.Y + TEE_HEIGHT, GlobalPosition.Z);
}

/// <summary>
Expand Down
49 changes: 49 additions & 0 deletions game/TerrainProbe.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Godot;
using Godot.Collections;

/// <summary>
/// Reusable terrain raycast utility.
/// Encapsulates the common pattern of casting a vertical ray through the physics world
/// and parsing the hit dictionary into a structured result.
/// </summary>
public static class TerrainProbe
{
public readonly record struct TerrainHit(Vector3 Position, Vector3 Normal, Node Collider);

/// <summary>
/// Cast a vertical ray from (origin + Up * upOffset) to (origin + Down * downOffset).
/// Returns null if no hit or world is unavailable.
/// </summary>
public static TerrainHit? Raycast(
World3D world,
Vector3 origin,
float upOffset,
float downOffset,
Array<Rid> exclude)
{
if (world == null)
return null;

Vector3 rayStart = origin + Vector3.Up * upOffset;
Vector3 rayEnd = origin + Vector3.Down * downOffset;

var query = PhysicsRayQueryParameters3D.Create(rayStart, rayEnd);
query.CollideWithAreas = false;
query.CollideWithBodies = true;
query.Exclude = exclude;

var hit = world.DirectSpaceState.IntersectRay(query);
if (hit.Count == 0)
return null;

Vector3 position = (Vector3)hit["position"];
Vector3 normal = ((Vector3)hit["normal"]).Normalized();
Node collider = hit.ContainsKey("collider") && hit["collider"].Obj is Node node
? node
: null;
if (normal.LengthSquared() < 0.000001f)
normal = Vector3.Up;

return new TerrainHit(position, normal, collider);
}
}
1 change: 1 addition & 0 deletions game/TerrainProbe.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cjqe75xo6wcys
63 changes: 63 additions & 0 deletions game/hole/HoleAudioManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Godot;

/// <summary>
/// Manages hole scene audio: non-attenuated 3D audio configuration, driver hit, and ball landing sounds.
/// </summary>
public sealed class HoleAudioManager
{
private readonly AudioStreamPlayer3D _driverHit;
private readonly AudioStreamPlayer3D _ambientBirds;
private readonly AudioStreamPlayer3D _ballLanding;

public HoleAudioManager(
AudioStreamPlayer3D driverHit,
AudioStreamPlayer3D ambientBirds,
AudioStreamPlayer3D ballLanding)
{
_driverHit = driverHit;
_ambientBirds = ambientBirds;
_ballLanding = ballLanding;
}

public void ConfigureAll(bool startAmbient)
{
ConfigureNonAttenuated(_ambientBirds, ensurePlaying: startAmbient);
ConfigureNonAttenuated(_driverHit, ensurePlaying: false);
ConfigureNonAttenuated(_ballLanding, ensurePlaying: false);
}

public void PlayDriverHit()
{
if (_driverHit == null)
return;

if (_driverHit.Playing)
_driverHit.Stop();

_driverHit.Play();
}

public void PlayBallLanding(Vector3 ballPosition)
{
if (_ballLanding == null)
return;

_ballLanding.GlobalPosition = ballPosition;
if (_ballLanding.Playing)
_ballLanding.Stop();

_ballLanding.Play();
}

private static void ConfigureNonAttenuated(AudioStreamPlayer3D player, bool ensurePlaying)
{
if (player == null)
return;

player.AttenuationModel = AudioStreamPlayer3D.AttenuationModelEnum.Disabled;
player.DopplerTracking = AudioStreamPlayer3D.DopplerTrackingEnum.Disabled;

if (ensurePlaying && !player.Playing)
player.Play();
}
}
1 change: 1 addition & 0 deletions game/hole/HoleAudioManager.cs.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://cjtq3iowwfu1g
49 changes: 6 additions & 43 deletions game/hole/HoleSceneControllerBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public abstract partial class HoleSceneControllerBase : Node3D
private AudioStreamPlayer3D _audioDriverHit;
private AudioStreamPlayer3D _audioBackgroundBirds;
private AudioStreamPlayer3D _audioGolfBallLanding;
private HoleAudioManager _audioManager;
private TcpServer _tcpServer;
private ShotRecordingService _shotRecordingService;
private GameSettings _gameSettings;
Expand Down Expand Up @@ -289,7 +290,8 @@ private void InitializeCoreStage()
_audioDriverHit = GetNodeOrNull<AudioStreamPlayer3D>(DriverHitAudioPath);
_audioBackgroundBirds = GetNodeOrNull<AudioStreamPlayer3D>(AmbientAudioPath);
_audioGolfBallLanding = GetNodeOrNull<AudioStreamPlayer3D>(BallLandingAudioPath);
ConfigureConsistentAudioLevels(startAmbientAudio: false);
_audioManager = new HoleAudioManager(_audioDriverHit, _audioBackgroundBirds, _audioGolfBallLanding);
_audioManager.ConfigureAll(startAmbient: false);
_progressStore = GetNodeOrNull<GameProgressStore>("/root/GameProgressStore");
_sceneId = GetSceneId();
ResolveCourseCard();
Expand Down Expand Up @@ -378,7 +380,7 @@ private void BeginBackgroundStartupStage()

_startupStage = StartupStage.Background;
bool shouldPlayAmbientAudio = ShouldPlayAmbientAudioOnStartup();
ConfigureNonAttenuated3DAudio(_audioBackgroundBirds, ensurePlaying: shouldPlayAmbientAudio);
_audioManager.ConfigureAll(startAmbient: shouldPlayAmbientAudio);
if (!shouldPlayAmbientAudio && _audioBackgroundBirds != null && _audioBackgroundBirds.Playing)
_audioBackgroundBirds.Stop();

Expand Down Expand Up @@ -590,7 +592,7 @@ private async void OnGolfBallRest()

private void OnGolfBallLanded()
{
PlayGolfBallLandingAudio();
_audioManager.PlayBallLanding(_ball.GlobalPosition);
}

private void OnGameplayUiHitShot(Dictionary data)
Expand All @@ -617,16 +619,7 @@ private void OnTestHitRequested()
LaunchShot(data, useTcpTracker: false, logPayload: false);
}

private void PlayDriverHitAudio()
{
if (_audioDriverHit == null)
return;

if (_audioDriverHit.Playing)
_audioDriverHit.Stop();

_audioDriverHit.Play();
}

private void LaunchShot(Dictionary data, bool useTcpTracker, bool logPayload)
{
Expand All @@ -644,7 +637,7 @@ private void LaunchShot(Dictionary data, bool useTcpTracker, bool logPayload)

_displaySession.SetRawPayload(data);
UpdateBallDisplay();
PlayDriverHitAudio();
_audioManager.PlayDriverHit();
IncrementStrokeCount();
_shotRecordingService?.RecordShot(data, ResolveShotRecordingClubTag());

Expand All @@ -657,36 +650,6 @@ private void LaunchShot(Dictionary data, bool useTcpTracker, bool logPayload)
_ = _shotCameraController.EnableFollowDeferredAsync();
}

private void ConfigureConsistentAudioLevels(bool startAmbientAudio)
{
ConfigureNonAttenuated3DAudio(_audioBackgroundBirds, ensurePlaying: startAmbientAudio);
ConfigureNonAttenuated3DAudio(_audioDriverHit, ensurePlaying: false);
ConfigureNonAttenuated3DAudio(_audioGolfBallLanding, ensurePlaying: false);
}

private void ConfigureNonAttenuated3DAudio(AudioStreamPlayer3D player, bool ensurePlaying)
{
if (player == null)
return;

player.AttenuationModel = AudioStreamPlayer3D.AttenuationModelEnum.Disabled;
player.DopplerTracking = AudioStreamPlayer3D.DopplerTrackingEnum.Disabled;

if (ensurePlaying && !player.Playing)
player.Play();
}

private void PlayGolfBallLandingAudio()
{
if (_audioGolfBallLanding == null)
return;

_audioGolfBallLanding.GlobalPosition = _ball.GlobalPosition;
if (_audioGolfBallLanding.Playing)
_audioGolfBallLanding.Stop();

_audioGolfBallLanding.Play();
}

private void OnCameraFollowChanged(Variant value)
{
Expand Down
16 changes: 3 additions & 13 deletions ui/CourseHud.cs
Original file line number Diff line number Diff line change
Expand Up @@ -620,21 +620,11 @@ private void DisconnectRangeControlSignals()
_rangeClubOption.ItemSelected -= OnRangeClubSelected;
}

private void OnRangeTargetSliderChanged(double value)
{
if (_isSyncingRangeControls)
return;
private void OnRangeTargetSliderChanged(double value) => SyncRangeTargetValue(value);

_rangeTargetYards = Mathf.Clamp(Mathf.RoundToInt((float)value), _rangeTargetMinYards, _rangeTargetMaxYards);
_isSyncingRangeControls = true;
if (_rangeTargetStepper != null)
_rangeTargetStepper.Value = _rangeTargetYards;
if (_rangeTargetSlider != null)
_rangeTargetSlider.Value = _rangeTargetYards;
_isSyncingRangeControls = false;
}
private void OnRangeTargetStepperChanged(double value) => SyncRangeTargetValue(value);

private void OnRangeTargetStepperChanged(double value)
private void SyncRangeTargetValue(double value)
{
if (_isSyncingRangeControls)
return;
Expand Down
Loading
Loading