gfolf2/src/player/shot_setup/shot_setup.gd

384 lines
10 KiB
GDScript

class_name ShotSetup extends Node3D
enum Phase {
AIM,
POWER_ADJUST,
CURVE_ADJUST,
DOWNSWING,
SHOT,
FINISHED,
}
const PITCH_MIN := deg_to_rad(-60.0)
const PITCH_MAX := deg_to_rad(-5.0)
const ZOOM_LENGTH := 0.1
const ZOOM_ELEVATION_RATIO := 0.3166
const ZOOM_MIN := 1.0
const ZOOM_MAX := 12.0
const ARROW_ACCELERATION := 8.0
const PLAYER_ACCELERATION := 2.0
const FREE_CAM_RETURN_TIME := 0.618
const BALL_RETURN_TIME := 0.618
const CAMERA_SNAP_TIME := 0.3
## In Driving Range mode, the ball can be retrieved in the shot phase.
@export var driving_range := false
@export_category("Shot Parameters")
@export var base_power := 2.5
@export var base_curve := 0.0
var base_speed: float = ProjectSettings.get_setting("game/config/controls/camera/free_camera_speed")
var x_sensitivity: float = ProjectSettings.get_setting(
"game/config/controls/camera/x_axis_sensitivity"
)
var x_acceleration: float = ProjectSettings.get_setting(
"game/config/controls/camera/x_axis_acceleration"
)
var y_sensitivity: float = ProjectSettings.get_setting(
"game/config/controls/camera/y_axis_sensitivity"
)
var y_acceleration: float = ProjectSettings.get_setting(
"game/config/controls/camera/y_axis_acceleration"
)
var invert_pitch: bool = ProjectSettings.get_setting("game/config/controls/camera/invert_pitch")
var control_disabled := false
var phase: Phase:
set(value):
if value != phase:
_on_phase_change(value)
phase = value
var club: T.Club:
set(value):
if value != club:
_on_club_change(value)
club = value
var shot_ref: Node3D
var _free_camera: FreeCamera
var _returning_free_camera := false
@onready var direction: Node3D = %Direction
@onready var elevation: Node3D = %Elevation
@onready var pitch: Node3D = %Pitch
@onready var zoom: Node3D = %Zoom
@onready var camera: Camera3D = %Camera
@onready var player_pivot: Node3D = %PlayerPivot
# TODO: genericize for selectable characters
@onready var character: CharacterController = $PlayerPivot/GfolfGirl
@onready var shot_animation: AnimationPlayer = %ShotAnimation
@onready var arrow: Node3D = %Arrow
@onready var arrow_animation: AnimationPlayer = %ArrowAnimation
@onready var power_bar: ProgressBar = %PowerBar
@onready var power_animation: AnimationPlayer = %PowerAnimation
@onready var curve_bar: ProgressBar = %CurveBar
@onready var curve_animation: AnimationPlayer = %CurveAnimation
@onready var ball_point: Node3D = %BallPoint
@onready var physics_ball: RigidBody3D = %PhysicsBall
@onready var drive_ref: RayCast3D = %DriveRef
@onready var drive_arrow: Node3D = %DriveArrow
@onready var putt_ref: RayCast3D = %PuttRef
@onready var putt_arrow: Node3D = %PuttArrow
@onready var club_selector: ClubSelector = %ClubSelector
@onready var nice_animation: AnimationPlayer = %NiceAnimation
@onready var hud_state_machine: AnimationTree = %HUDStateMachine
@onready var hud_state: AnimationNodeStateMachinePlayback = hud_state_machine["parameters/playback"]
@onready var ball_impulse_debug: Node3D = %BallImpulseDebug
@onready var camera_distance := camera.position.z:
set = _set_camera_distance
@onready var phys_ball_scene := preload("res://src/player/physics_ball/physics_ball.tscn")
@onready var _target_rotation := Vector2(pitch.rotation.x, direction.rotation.y)
func _ready() -> void:
_on_phase_change(phase)
_on_club_change(club)
func _set_camera_distance(value: float) -> void:
var tween := get_tree().create_tween()
tween.tween_property(zoom, "position:z", value, ZOOM_LENGTH).set_trans(Tween.TRANS_SINE)
tween.set_parallel()
(
tween
. tween_property(elevation, "position:y", value * ZOOM_ELEVATION_RATIO, ZOOM_LENGTH)
. set_trans(Tween.TRANS_SINE)
)
camera_distance = value
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
elif event is InputEventMouseMotion:
if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
camera_motion((event as InputEventMouseMotion).relative)
func camera_motion(motion: Vector2) -> void:
if not control_disabled and phase == Phase.AIM:
# Can only control camera while aiming
_target_rotation.y = _target_rotation.y - deg_to_rad(motion.x * x_sensitivity)
_target_rotation.x = clampf(
_target_rotation.x - deg_to_rad(motion.y * y_sensitivity) * (-1 if invert_pitch else 1),
PITCH_MIN,
PITCH_MAX
)
func finish_downswing() -> void:
phase = Phase.SHOT
func take_shot() -> void:
print("WHACK!")
print("Power: ", power_bar.value)
print("Curve: ", curve_bar.value)
var impulse := shot_ref.global_basis.z * base_power * power_bar.value
print("Shot impulse: ", impulse)
ball_impulse_debug.transform = (
Transform3D.IDENTITY.scaled(Vector3.ONE * impulse.length()).looking_at(impulse)
)
physics_ball.freeze = false
physics_ball.apply_central_impulse(impulse)
func insert_free_cam() -> void:
arrow_animation.play("hide")
hud_state.travel("hidden")
_free_camera = FreeCamera.create(camera)
add_sibling(_free_camera)
control_disabled = true
camera.current = false
func return_free_cam() -> void:
# TODO alter shot aim based on free camera selection
arrow_animation.play("show")
hud_state.travel("visible")
_free_camera.queue_free()
_free_camera = null
control_disabled = false
_returning_free_camera = false
camera.current = true
func return_ball() -> void:
var tween := get_tree().create_tween()
physics_ball.freeze = true
(
tween
. tween_property(
physics_ball,
"global_transform",
ball_point.global_transform,
BALL_RETURN_TIME,
)
. set_trans(Tween.TRANS_SINE)
)
func travel_to_ball() -> void:
physics_ball.freeze = true
global_position = physics_ball.global_position
physics_ball.global_transform = ball_point.global_transform
func start_shot_track() -> void:
#tracking_camera.make_current()
#tracking_camera.active = true
if phase == Phase.SHOT:
var orbital_cam := OrbitalCamera.create(physics_ball)
orbital_cam.rotation.y = randf_range(0.0, TAU)
add_sibling(orbital_cam)
func end_shot_track() -> void:
camera.make_current()
#tracking_camera.active = false
## Called immediately before `club` is mutated.
func _on_club_change(new_club: T.Club) -> void:
club_selector.value = new_club
match new_club:
T.Club.DRIVER:
shot_ref = drive_ref
drive_arrow.show()
putt_arrow.hide()
T.Club.PUTTER:
shot_ref = putt_ref
putt_arrow.show()
drive_arrow.hide()
## Called immediately before `phase` is mutated.
func _on_phase_change(new_phase: Phase) -> void:
match new_phase:
Phase.AIM:
hud_state.travel("visible")
if not arrow.visible:
arrow_animation.play("show")
camera.make_current()
power_bar.hide()
curve_bar.hide()
character.reset()
Phase.POWER_ADJUST:
curve_bar.hide()
power_bar.show()
power_animation.stop() # Reset if needed
Phase.CURVE_ADJUST:
curve_bar.show()
curve_animation.play("fill")
Phase.DOWNSWING:
power_bar.hide()
curve_bar.hide()
character.downswing()
shot_animation.play("swing_delay") # calls `take_shot`
Phase.SHOT:
power_bar.hide()
curve_bar.hide()
nice_animation.play("display")
if not driving_range:
shot_animation.play("shoot")
arrow_animation.play("hide")
hud_state.travel("hidden")
take_shot()
Phase.FINISHED:
power_bar.hide()
curve_bar.hide()
end_shot_track()
travel_to_ball()
func _process(delta: float) -> void:
## Visual updates
# Rotation
direction.rotation.y = lerp_angle(
direction.rotation.y, _target_rotation.y, delta * x_acceleration
)
pitch.rotation.x = lerp_angle(pitch.rotation.x, _target_rotation.x, delta * y_acceleration)
# Arrow lags behind camera control
arrow.rotation.y = lerp_angle(arrow.rotation.y, _target_rotation.y, delta * ARROW_ACCELERATION)
# Player lags further behind
player_pivot.rotation.y = lerp_angle(
player_pivot.rotation.y, _target_rotation.y, delta * PLAYER_ACCELERATION
)
## Input Handling
if control_disabled:
if Input.is_action_just_pressed("camera_cancel"):
if is_instance_valid(_free_camera) and not _returning_free_camera:
_returning_free_camera = true
var tween := get_tree().create_tween()
(
tween
. tween_property(
_free_camera,
"global_transform",
camera.global_transform,
FREE_CAM_RETURN_TIME
)
. set_trans(Tween.TRANS_SINE)
)
tween.tween_callback(return_free_cam)
return
match phase:
Phase.AIM:
# Camera zoom
if Input.is_action_just_pressed("shot_zoom_in"):
camera_distance = max(camera_distance - 1.0, ZOOM_MIN)
if Input.is_action_just_pressed("shot_zoom_out"):
camera_distance = min(camera_distance + 1.0, ZOOM_MAX)
# Club select
if Input.is_action_just_pressed("select_driver"):
club = T.Club.DRIVER
if Input.is_action_just_pressed("select_iron"):
club = T.Club.IRON
if Input.is_action_just_pressed("select_wedge"):
club = T.Club.WEDGE
if Input.is_action_just_pressed("select_gold"):
club = T.Club.GOLD
if Input.is_action_just_pressed("select_putter"):
club = T.Club.PUTTER
# Switch to free cam
if (
Input.is_action_just_pressed("camera_back")
or Input.is_action_just_pressed("camera_forward")
or Input.is_action_just_pressed("camera_left")
or Input.is_action_just_pressed("camera_right")
):
insert_free_cam()
# Advance to next phase
if Input.is_action_just_pressed("shot_accept"):
phase = Phase.POWER_ADJUST
Phase.POWER_ADJUST:
if Input.is_action_just_pressed("shot_accept"):
# TODO set power gauge parameters if needed
character.start_upswing()
power_animation.play("fill")
if Input.is_action_just_pressed("ui_cancel"):
power_animation.stop()
phase = Phase.AIM
if Input.is_action_just_released("shot_accept") and power_bar.value > 0:
power_animation.pause()
phase = Phase.CURVE_ADJUST
Phase.CURVE_ADJUST:
if Input.is_action_just_pressed("ui_cancel"):
curve_animation.stop()
phase = Phase.POWER_ADJUST
if Input.is_action_just_pressed("shot_accept"):
curve_animation.pause()
phase = Phase.DOWNSWING
Phase.SHOT:
# REMOVEME
if Input.is_action_just_pressed("camera_forward"):
insert_free_cam()
if driving_range and Input.is_action_just_pressed("shot_accept"):
phase = Phase.AIM
return_ball()
Phase.FINISHED:
# TODO: hand over to next player where appropriate
phase = Phase.AIM
func _on_physics_ball_sleeping_state_changed() -> void:
if physics_ball.sleeping and phase == Phase.SHOT:
phase = Phase.FINISHED