diff --git a/src/equipment/balls/physics_ball/game_ball.gd b/src/equipment/balls/physics_ball/game_ball.gd index c01552b..e639c8f 100644 --- a/src/equipment/balls/physics_ball/game_ball.gd +++ b/src/equipment/balls/physics_ball/game_ball.gd @@ -12,6 +12,7 @@ enum Type { } const TERRAIN_DAMPING_EPSILON := 1e-6 +const MAGNUS_EPSILON := 1e-3 const IRON_DAMPING := 9999.0 ## Angular damping while in air @@ -21,6 +22,16 @@ const IRON_DAMPING := 9999.0 ## Angular damping for iron balls @export var iron_damping := 9999.0 +#@export var fluid_density := 1.225 +#@export var lift_coefficient := 0.05 +#@export var radius := 0.05 +## Coefficient of angular velocity influence on linear velocity +## This is approximately 1/2 * rho * C_L * pi * r^2 +## where `rho` is the fluid density of the medium, or 1.225 for air at sea level, +## and `C_L` is the lift coefficient which for our purposes is 0.05, +## and `r` is the radius of the ball, which is 5cm. +@export var magnus_coefficient := 0.00024 + ## Causes the ball to act more like a brick @export var iron_ball := false: set(value): @@ -59,15 +70,22 @@ func _total_terrain_angular_damping() -> float: ) +func _magnus_force() -> Vector3: + return magnus_coefficient * angular_velocity.cross(linear_velocity) + + func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: if not _awake: # Triggered on first frame after waking _awake = true _position_on_last_wake = global_position _last_contact_normal = Vector3.UP + # TODO something's fucky here... I think this gets called once after the ball sleeps var damping := air_damping if state.get_contact_count(): + constant_force = Vector3.ZERO + # We want the contact normal which minimizes the angle to the up vector var min_dot := -1.0 for i: int in range(state.get_contact_count()): @@ -85,6 +103,13 @@ func _integrate_forces(state: PhysicsDirectBodyState3D) -> void: angular_damp = damping +func _physics_process(_delta: float) -> void: + # Simulate magnus effect + var magnus := _magnus_force() + if magnus.length_squared() > MAGNUS_EPSILON: + apply_central_force(magnus) + + func enter_zone(zone: BallZone) -> void: _zones.push_back(zone) diff --git a/src/equipment/balls/physics_ball/physics_ball.tscn b/src/equipment/balls/physics_ball/physics_ball.tscn index 2f25e3e..69b0c25 100644 --- a/src/equipment/balls/physics_ball/physics_ball.tscn +++ b/src/equipment/balls/physics_ball/physics_ball.tscn @@ -34,8 +34,13 @@ script/source = "extends Control const COLOR_X := Color(1, 0, 0) const COLOR_Y := Color(0, 1, 0) const COLOR_Z := Color(0, 0, 1) +const COLOR_MAGNUS := Color.CYAN +const MAGNUS_SCALE := 3 const WIDTH := 4 +@export var draw_reoriented_basis := true +@export var draw_magnus_effect := true + @onready var physics_ball: GameBall = $\"..\" @@ -45,9 +50,9 @@ func _process(_delta: float) -> void: func _draw() -> void: - if physics_ball._last_contact_normal != null: - var camera := get_viewport().get_camera_3d() - var start := camera.unproject_position(physics_ball.global_position) + var camera := get_viewport().get_camera_3d() + var start := camera.unproject_position(physics_ball.global_position) + if draw_reoriented_basis and physics_ball._last_contact_normal != null: var basis := physics_ball.get_reoriented_basis() var end_x := camera.unproject_position(physics_ball.global_position + basis.x) var end_y := camera.unproject_position(physics_ball.global_position + basis.y) @@ -55,6 +60,10 @@ func _draw() -> void: draw_line(start, end_x, COLOR_X, WIDTH) draw_line(start, end_y, COLOR_Y, WIDTH) draw_line(start, end_z, COLOR_Z, WIDTH) + + if draw_magnus_effect: + var end := camera.unproject_position(physics_ball.global_position + physics_ball._magnus_force() * MAGNUS_SCALE) + draw_line(start, end, COLOR_MAGNUS, WIDTH) " [node name="PhysicsBall" type="RigidBody3D"] @@ -86,5 +95,6 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 script = SubResource("GDScript_p4v7o") +draw_reoriented_basis = false [connection signal="sleeping_state_changed" from="." to="." method="_on_sleeping_state_changed"] diff --git a/src/player/shot_setup/shot_setup.gd b/src/player/shot_setup/shot_setup.gd index daf9fa4..55b1abc 100644 --- a/src/player/shot_setup/shot_setup.gd +++ b/src/player/shot_setup/shot_setup.gd @@ -35,6 +35,12 @@ const BIG_POWER_THRESHOLD := 0.7 ## Amount of life lost when landing in water const WATER_DAMAGE := 10.0 +## Angle of influence that shot curve has, in radians +const CURVE_INFLUENCE := PI / 8 + +## Maximum absolute curve for the "nice shot" animation to play +const NICE_THRESHOLD := 0.2 + ## In Driving Range mode, the ball can be retrieved in the shot phase. @export var driving_range := false @@ -108,7 +114,7 @@ var shot_curve: float: set(value): hud.curve_bar.value = value get: - return hud.power_bar.value + return hud.curve_bar.value var game_ball: GameBall: get: @@ -231,13 +237,22 @@ func take_shot() -> void: print_debug("WHACK!\nPower: ", shot_power, "\nCurve: ", shot_curve) + # Momentary impulse applied to the ball var impulse := get_shot_impulse(shot_power) print_debug("Shot impulse: ", impulse, "; ", impulse.length(), " N*s") + var curve := shot_ref.global_basis.x.normalized() * shot_curve + + # Position where the ball is hit (imparts spin) + var offset := curve * 0.001 + print_debug("Shot offset: ", offset, "; ", offset.length(), " m") + # Rotate impulse + impulse = impulse.rotated(Vector3.UP, -shot_curve * CURVE_INFLUENCE) + if game_ball: game_ball.iron_ball = club == Club.Type.IRON game_ball.freeze = false - game_ball.apply_central_impulse(impulse) + game_ball.apply_impulse(impulse, offset) # Use a ball if a limited type is selected if player.get_balls(ball_type) > 0: @@ -407,7 +422,11 @@ func _on_phase_change(new_phase: Phase) -> void: hud.power_bar.hide() hud.curve_bar.hide() - hud.play_nice_animation() + if perfect_aim: + shot_curve = 0.0 + + if abs(shot_curve) <= NICE_THRESHOLD: + hud.play_nice_animation() if not driving_range: shot_animation.play("shoot")