gfolf2/src/player/shot_setup/shot_setup.gd

592 lines
16 KiB
GDScript3
Raw Normal View History

2024-10-20 20:27:08 -06:00
class_name ShotSetup extends Node3D
signal finished(source: ShotSetup)
enum Phase {
AIM,
POWER_ADJUST,
CURVE_ADJUST,
DOWNSWING,
SHOT,
FINISHED,
}
2024-10-20 20:27:08 -06:00
const PITCH_MIN := deg_to_rad(-60.0)
const PITCH_MAX := deg_to_rad(-5.0)
const ZOOM_LENGTH := 0.1
2024-11-03 16:50:44 -07:00
const ZOOM_ELEVATION_RATIO := 0.3166
2024-10-20 20:27:08 -06:00
const ZOOM_MIN := 1.0
const ZOOM_MAX := 12.0
const ARROW_ACCELERATION := 8.0
const PLAYER_ACCELERATION := 2.0
2024-11-19 21:52:04 -07:00
const FREE_CAM_GIMBAL_TWEEN_TIME := 0.2
2024-11-02 09:30:45 -06:00
const FREE_CAM_RETURN_TIME := 0.618
2024-11-02 19:05:49 -06:00
const BALL_RETURN_TIME := 0.618
2024-11-03 16:50:44 -07:00
const CAMERA_SNAP_TIME := 0.3
2024-11-17 12:35:28 -07:00
const WASTED_BALL_RETURN_DELAY := 3.5
2024-11-17 20:10:33 -07:00
## Shots above this threshold trigger a "big power" effect
2024-11-17 18:51:06 -07:00
const BIG_POWER_THRESHOLD := 0.7
2024-11-18 12:06:43 -07:00
## 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
2024-11-02 19:05:49 -06:00
## In Driving Range mode, the ball can be retrieved in the shot phase.
@export var driving_range := false
## Initially-selected club
@export var initial_club: Club.Type = Club.Type.DRIVER
## Initially-selected ball type
2024-11-20 19:22:11 -07:00
@export var initial_ball: GameBall.Type = GameBall.Type.BASIC
2024-11-02 19:05:49 -06:00
@export_category("Shot Parameters")
2024-11-09 16:26:15 -07:00
@export var base_power := 2.5
2024-11-02 19:05:49 -06:00
@export var base_curve := 0.0
2024-10-20 20:27:08 -06:00
2024-11-12 16:18:57 -07:00
@export_category("Debug")
2024-11-12 16:56:01 -07:00
## When enabled, ignore curve meter and hit a perfect shot every time.
@export var perfect_aim := false
2024-11-13 09:38:47 -07:00
## Keep projection visible
@export var keep_projection := false
2024-11-12 16:18:57 -07:00
var player: WorldPlayer
2024-10-20 20:27:08 -06:00
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")
2024-11-02 19:05:49 -06:00
var control_disabled := false
var phase: Phase = Phase.FINISHED:
set(value):
if value != phase:
_on_phase_change(value)
phase = value
2024-11-19 11:51:05 -07:00
var hud: ShotHUD
var club: Club.Type:
2024-11-05 15:03:40 -07:00
set(value):
if value != club:
_on_club_change(value)
club = value
2024-11-20 19:22:11 -07:00
var ball_type: GameBall.Type:
set(value):
if value != ball_type:
2024-11-20 19:22:11 -07:00
hud.ball_selector.value = value
ball_point.spawn_ball(value)
ball_type = value
2024-11-05 15:03:40 -07:00
var shot_ref: Node3D
2024-11-17 12:35:28 -07:00
var shot_power: float:
set(value):
hud.power_bar.value = value
get:
return hud.power_bar.value
var shot_curve: float:
set(value):
hud.curve_bar.value = value
get:
return hud.curve_bar.value
2024-11-17 12:35:28 -07:00
var game_ball: GameBall:
get:
return ball_point.ball
var _free_camera: FreeCamera
var _returning_free_camera := false
var _restart_queued := false
2024-10-20 20:27:08 -06:00
2024-11-11 11:39:12 -07:00
var _tracking_camera: OrbitalCamera
2024-10-20 20:27:08 -06:00
@onready var direction: Node3D = %Direction
2024-11-03 16:50:44 -07:00
@onready var elevation: Node3D = %Elevation
2024-10-20 20:27:08 -06:00
@onready var pitch: Node3D = %Pitch
2024-11-03 16:50:44 -07:00
@onready var zoom: Node3D = %Zoom
2024-10-20 20:27:08 -06:00
@onready var camera: Camera3D = %Camera
@onready var player_pivot: Node3D = %PlayerPivot
# TODO: genericize for selectable characters
@onready var character: CharacterController = $PlayerPivot/GfolfGirl
2024-11-03 16:50:44 -07:00
@onready var shot_animation: AnimationPlayer = %ShotAnimation
@onready var arrow: Node3D = %Arrow
2024-11-12 16:18:57 -07:00
@onready var arrow_pivot: Node3D = %ArrowPivot
@onready var arrow_animation: AnimationPlayer = %ArrowAnimation
2024-11-12 16:18:57 -07:00
@onready var shot_projection: ProjectileArc = %ShotProjection
@onready var ball_point: BallPoint = %BallPoint
2024-11-02 19:05:49 -06:00
@onready var drive_ref: RayCast3D = %DriveRef
2024-11-05 15:03:40 -07:00
@onready var drive_arrow: Node3D = %DriveArrow
2024-11-12 16:56:01 -07:00
@onready var wedge_ref: RayCast3D = %WedgeRef
@onready var wedge_arrow: Node3D = %WedgeArrow
@onready var iron_ref: RayCast3D = %IronRef
@onready var iron_arrow: Node3D = %IronArrow
2024-11-05 15:03:40 -07:00
@onready var putt_ref: RayCast3D = %PuttRef
@onready var putt_arrow: Node3D = %PuttArrow
@onready var downswing_timer: Timer = %DownswingTimer
2024-11-17 12:35:28 -07:00
@onready var ball_return_timer: Timer = %BallReturnTimer
@onready var camera_distance := zoom.position.z:
2024-10-20 20:27:08 -06:00
set = _set_camera_distance
2024-11-17 12:35:28 -07:00
@onready var world: World = get_tree().get_first_node_in_group(World.group)
2024-11-17 18:51:06 -07:00
@onready var game: Game = get_tree().get_first_node_in_group(Game.group)
2024-11-17 12:35:28 -07:00
2024-10-20 20:27:08 -06:00
@onready var _target_rotation := Vector2(pitch.rotation.x, direction.rotation.y)
static var scene := preload("res://src/player/shot_setup/shot_setup.tscn")
2024-10-20 20:27:08 -06:00
2024-11-17 12:35:28 -07:00
func _ready() -> void:
2024-11-19 11:51:05 -07:00
# Create & set up HUD
hud = ShotHUD.create(player)
world.ui.add_player_hud(hud)
ball_type = initial_ball
2024-11-19 11:51:05 -07:00
club = initial_club
2024-11-21 17:41:55 -07:00
character.set_color(player.color)
_on_phase_change(phase)
2024-11-17 12:35:28 -07:00
2024-10-20 20:27:08 -06:00
func _set_camera_distance(value: float) -> void:
var tween := get_tree().create_tween()
2024-11-03 16:50:44 -07:00
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)
2024-10-20 20:27:08 -06:00
)
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:
2024-10-20 20:27:08 -06:00
# 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
)
## Return this instance to the AIM phase the next time we process while FINISHED.
func queue_restart() -> void:
_restart_queued = true
func is_active() -> bool:
return phase != Phase.FINISHED
func finish_downswing() -> void:
phase = Phase.SHOT
2024-11-12 16:18:57 -07:00
func get_shot_impulse(meter_pct: float) -> Vector3:
return -shot_ref.global_basis.z * base_power * meter_pct
func take_shot() -> void:
2024-11-17 18:51:06 -07:00
# Impact screenshake & hitlag
if game:
if shot_power > BIG_POWER_THRESHOLD:
game.viewport.hit_lag_big()
var shake_intensity: float = (
10.0 * (shot_power - BIG_POWER_THRESHOLD) / (1.0 - BIG_POWER_THRESHOLD)
)
game.viewport.screen_shake(shake_intensity, 1.0)
else:
game.viewport.hit_lag_small()
2024-11-17 12:35:28 -07:00
print_debug("WHACK!\nPower: ", shot_power, "\nCurve: ", shot_curve)
# Momentary impulse applied to the ball
2024-11-17 12:35:28 -07:00
var impulse := get_shot_impulse(shot_power)
2024-11-12 16:56:01 -07:00
print_debug("Shot impulse: ", impulse, "; ", impulse.length(), " N*s")
2024-11-02 19:05:49 -06:00
2024-11-21 11:27:35 -07:00
# Curve the curve
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
2024-11-21 11:27:35 -07:00
impulse = impulse.rotated(Vector3.UP, -shot_curve * CURVE_INFLUENCE * shot_power)
if game_ball:
2024-11-21 00:21:11 -07:00
game_ball.iron_ball = club == Club.Type.IRON
game_ball.freeze = false
game_ball.apply_impulse(impulse, offset)
2024-11-12 16:18:57 -07:00
2024-11-20 19:22:11 -07:00
# Use a ball if a limited type is selected
if player.get_balls(ball_type) > 0:
player.mutate_balls(ball_type, -1)
2024-11-12 16:18:57 -07:00
## Make the shot projection widget visible, with animated transition
func _show_shot_projection() -> void:
if not game_ball:
return
2024-11-21 11:27:35 -07:00
shot_projection.putt_projection = club == Club.Type.PUTTER
2024-11-12 16:18:57 -07:00
shot_projection.initial_speed = 1
2024-11-12 16:56:01 -07:00
shot_projection.basis = shot_ref.basis.orthonormalized()
2024-11-19 21:52:04 -07:00
var shot_speed := get_shot_impulse(1.0).length() / game_ball.mass
2024-11-12 16:18:57 -07:00
var tween := get_tree().create_tween()
tween.tween_property(shot_projection, "initial_speed", shot_speed, CAMERA_SNAP_TIME).set_trans(
Tween.TRANS_QUAD
)
shot_projection.show()
2024-11-02 19:05:49 -06:00
func insert_free_cam() -> void:
arrow_animation.play("hide")
2024-11-12 16:18:57 -07:00
_show_shot_projection()
2024-11-17 12:35:28 -07:00
hud.hide_hud()
_free_camera = FreeCamera.create(camera)
add_sibling(_free_camera)
2024-11-19 21:52:04 -07:00
# Un-gimbal-lock ourselves: quickly tween Z rotation to 0
var tween := get_tree().create_tween()
tween.tween_property(_free_camera, "rotation:z", 0, FREE_CAM_GIMBAL_TWEEN_TIME)
control_disabled = true
2024-11-03 16:50:44 -07:00
camera.current = false
func return_free_cam() -> void:
# TODO alter shot aim based on free camera selection
arrow_animation.play("show")
2024-11-13 09:38:47 -07:00
if not keep_projection:
shot_projection.hide()
2024-11-17 12:35:28 -07:00
hud.show_hud()
_free_camera.queue_free()
_free_camera = null
control_disabled = false
_returning_free_camera = false
2024-11-03 16:50:44 -07:00
camera.current = true
2024-11-02 19:05:49 -06:00
func return_ball() -> void:
if not game_ball:
return
2024-11-19 21:52:04 -07:00
game_ball.freeze = true
2024-11-11 11:39:12 -07:00
var tween := get_tree().create_tween()
2024-11-02 19:05:49 -06:00
(
tween
. tween_property(
2024-11-19 21:52:04 -07:00
game_ball,
2024-11-02 19:05:49 -06:00
"global_transform",
ball_point.global_transform,
BALL_RETURN_TIME,
)
. set_trans(Tween.TRANS_SINE)
)
2024-11-11 11:39:12 -07:00
tween.tween_callback(end_shot_track)
2024-11-02 19:05:49 -06:00
func travel_to_ball() -> void:
if not game_ball:
return
2024-11-19 21:52:04 -07:00
game_ball.freeze = true
global_position = game_ball.global_position
# Re-orient to the ball's last contact normal if there is one.
# Normally this will just be Vector3.UP or something close to it.
direction.rotation.y = 0
_target_rotation.y = 0
global_basis = game_ball.get_reoriented_basis()
2024-11-19 21:52:04 -07:00
ball_point.snap()
2024-11-03 16:50:44 -07:00
func start_shot_track() -> void:
if not game_ball:
return
2024-11-05 15:03:40 -07:00
if phase == Phase.SHOT:
2024-11-19 21:52:04 -07:00
_tracking_camera = OrbitalCamera.create(game_ball)
2024-11-11 11:39:12 -07:00
_tracking_camera.rotation.y = randf_range(0.0, TAU)
add_sibling(_tracking_camera)
_tracking_camera.global_transform = ball_point.global_transform
2024-11-03 16:50:44 -07:00
func end_shot_track() -> void:
camera.make_current()
2024-11-11 11:39:12 -07:00
if is_instance_valid(_tracking_camera):
_tracking_camera.queue_free()
if phase == Phase.SHOT:
phase = Phase.FINISHED
2024-11-03 16:50:44 -07:00
2024-11-05 15:03:40 -07:00
## Called immediately before `club` is mutated.
func _on_club_change(new_club_type: Club.Type) -> void:
var new_club := player.get_club(new_club_type)
if not new_club:
# `new_club` will be null if player has no club in the given slot
# TODO play bonk
return
# Hide all arrows
# TODO animate?
drive_arrow.hide()
2024-11-12 16:56:01 -07:00
wedge_arrow.hide()
iron_arrow.hide()
putt_arrow.hide()
hud.club_selector.value = new_club_type
# TODO club change animation
character.hold_right(new_club.get_model())
match new_club_type:
Club.Type.DRIVER:
2024-11-05 15:03:40 -07:00
shot_ref = drive_ref
drive_arrow.show()
Club.Type.PUTTER:
2024-11-05 15:03:40 -07:00
shot_ref = putt_ref
putt_arrow.show()
Club.Type.WEDGE:
shot_ref = wedge_ref
2024-11-12 16:56:01 -07:00
wedge_arrow.show()
Club.Type.IRON:
2024-11-12 16:56:01 -07:00
shot_ref = iron_ref
iron_arrow.show()
Club.Type.SPECIAL:
# TODO figure this out
shot_ref = drive_ref
_:
print_debug("Not sure how to equip club type: ", new_club)
2024-11-05 15:03:40 -07:00
## Called immediately before `phase` is mutated.
2024-11-04 15:17:32 -07:00
func _on_phase_change(new_phase: Phase) -> void:
match new_phase:
Phase.AIM:
2024-11-17 12:35:28 -07:00
hud.show_hud()
2024-11-06 10:44:27 -07:00
if not arrow.visible:
arrow_animation.play("show")
2024-11-04 15:17:32 -07:00
camera.make_current()
2024-11-17 12:35:28 -07:00
hud.power_bar.hide()
hud.curve_bar.hide()
character.reset()
2024-11-04 15:17:32 -07:00
Phase.POWER_ADJUST:
2024-11-17 12:35:28 -07:00
hud.curve_bar.hide()
2024-11-04 15:17:32 -07:00
2024-11-17 12:35:28 -07:00
hud.power_bar.show()
hud.reset_power_bar() # Reset if needed
2024-11-04 15:17:32 -07:00
Phase.CURVE_ADJUST:
2024-11-17 12:35:28 -07:00
hud.curve_bar.show()
hud.start_curve_bar()
Phase.DOWNSWING:
hud.gauge_flourish()
character.downswing()
downswing_timer.start()
2024-11-04 15:17:32 -07:00
Phase.SHOT:
2024-11-17 12:35:28 -07:00
hud.power_bar.hide()
hud.curve_bar.hide()
2024-11-04 15:17:32 -07:00
if perfect_aim:
shot_curve = 0.0
if abs(shot_curve) <= NICE_THRESHOLD:
hud.play_nice_animation()
2024-11-04 15:17:32 -07:00
if not driving_range:
shot_animation.play("shoot")
arrow_animation.play("hide")
2024-11-17 12:35:28 -07:00
hud.hide_hud()
2024-11-04 15:17:32 -07:00
take_shot()
Phase.FINISHED:
2024-11-17 12:35:28 -07:00
hud.power_bar.hide()
hud.curve_bar.hide()
2024-11-21 17:41:55 -07:00
hud.hide_hud()
finished.emit(self)
2024-11-04 15:17:32 -07:00
func _on_game_ball_changed(ball: GameBall) -> void:
if ball:
ball.entered_water.connect(_on_ball_entered_water)
ball.sleeping_state_changed.connect(_on_ball_sleeping_state_changed)
func _process(delta: float) -> void:
# REMOVEME
if Input.is_action_just_pressed("ui_menu"):
print("Debugging...")
## Visual updates
2024-10-20 20:27:08 -06:00
# 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
2024-11-12 16:18:57 -07:00
arrow_pivot.rotation.y = lerp_angle(
arrow_pivot.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)
2024-11-05 15:03:40 -07:00
# Club select
if Input.is_action_just_pressed("select_driver"):
club = Club.Type.DRIVER
2024-11-05 15:03:40 -07:00
if Input.is_action_just_pressed("select_iron"):
club = Club.Type.IRON
2024-11-05 15:03:40 -07:00
if Input.is_action_just_pressed("select_wedge"):
club = Club.Type.WEDGE
2024-11-11 13:44:23 -07:00
if Input.is_action_just_pressed("select_special"):
club = Club.Type.SPECIAL
2024-11-05 15:03:40 -07:00
if Input.is_action_just_pressed("select_putter"):
club = Club.Type.PUTTER
2024-11-05 15:03:40 -07:00
2024-11-20 19:22:11 -07:00
# Ball select
if Input.is_action_just_pressed("ball_next"):
ball_type = player.next_ball(ball_type)
if Input.is_action_just_pressed("ball_previous"):
ball_type = player.prev_ball(ball_type)
# 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"):
2024-11-20 19:22:11 -07:00
if player.get_balls(ball_type) != 0:
# Check that player has enough of the selected ball (<0 means unlimited)
phase = Phase.POWER_ADJUST
# TODO play UI bonk if player doesn't have balls (lmao)
Phase.POWER_ADJUST:
if Input.is_action_just_pressed("shot_accept"):
# TODO set power gauge parameters if needed
character.start_upswing()
2024-11-17 12:35:28 -07:00
hud.start_power_bar()
2024-11-19 17:41:43 -07:00
if Input.is_action_just_pressed("shot_cancel"):
2024-11-17 12:35:28 -07:00
hud.reset_power_bar()
2024-11-06 10:44:27 -07:00
phase = Phase.AIM
2024-11-17 12:35:28 -07:00
if Input.is_action_just_released("shot_accept") and shot_power > 0:
hud.stop_power_bar()
phase = Phase.CURVE_ADJUST
Phase.CURVE_ADJUST:
2024-11-19 17:41:43 -07:00
if Input.is_action_just_pressed("shot_cancel"):
2024-11-17 12:35:28 -07:00
hud.reset_curve_bar()
2024-11-06 10:44:27 -07:00
phase = Phase.POWER_ADJUST
if Input.is_action_just_pressed("shot_accept"):
2024-11-17 12:35:28 -07:00
hud.stop_curve_bar()
phase = Phase.DOWNSWING
Phase.SHOT:
2024-11-02 19:05:49 -06:00
if driving_range and Input.is_action_just_pressed("shot_accept"):
phase = Phase.AIM
return_ball()
Phase.FINISHED:
if _restart_queued:
_restart_queued = false
phase = Phase.AIM
2024-11-02 19:05:49 -06:00
2024-11-19 21:52:04 -07:00
func _on_ball_sleeping_state_changed() -> void:
if game_ball.sleeping and phase == Phase.SHOT:
2024-11-19 22:25:45 -07:00
travel_to_ball()
2024-11-11 11:39:12 -07:00
end_shot_track()
func _on_ball_entered_water() -> void:
# Should only be possible during SHOT phase, but let's check just to be sure...
if phase == Phase.SHOT:
2024-11-19 21:52:04 -07:00
game_ball.freeze = true
2024-11-17 12:35:28 -07:00
hud.play_wasted_animation()
2024-11-18 12:06:43 -07:00
player.life -= WATER_DAMAGE
2024-11-17 12:35:28 -07:00
ball_return_timer.start(WASTED_BALL_RETURN_DELAY)
2024-11-12 16:18:57 -07:00
2024-11-17 12:35:28 -07:00
func _on_ball_return_timer_timeout() -> void:
return_ball()
2024-11-18 12:57:11 -07:00
func _on_hitbox_ball_collision(ball: GameBall) -> void:
# TODO play animation
player.life -= ball.base_damage
## Create a new instance for the given player.
static func create(_player: WorldPlayer) -> ShotSetup:
var instance: ShotSetup = ShotSetup.scene.instantiate()
instance.player = _player
return instance