193 lines
6.0 KiB
GDScript
193 lines
6.0 KiB
GDScript
class_name Dart
|
|
extends RigidBody3D
|
|
|
|
## A single dart. Dragged from the tray, thrown at the board.
|
|
##
|
|
## Lifecycle: FROZEN -> DRAGGING -> IN_FLIGHT -> LANDED
|
|
## The scene root (main.gd) routes mouse input here; the dart does not
|
|
## hunt for the camera or raycast for itself.
|
|
|
|
signal landed(dart: Dart, hit_point: Vector3)
|
|
|
|
enum State { IDLE, DRAGGING, IN_FLIGHT, LANDED }
|
|
|
|
# --- Tuning -------------------------------------------------------------
|
|
|
|
@export_group("Drag")
|
|
## How hard the dart chases the cursor while held.
|
|
@export var drag_follow_speed: float = 20.0
|
|
## Clamp so a fast flick doesn't launch it across the room mid-drag.
|
|
@export var max_drag_speed: float = 42.0
|
|
## Tilt of the drag plane relative to the camera, in degrees.
|
|
@export_range(-180.0, 180.0) var drag_plane_angle: float = 15.0
|
|
@export var drag_linear_damp: float = 5.0
|
|
@export var drag_angular_damp: float = 10.0
|
|
|
|
@export_group("Throw")
|
|
@export var forward_impulse: float = 8.0
|
|
|
|
## Scale the raw flick velocity down. Raw mouse speed is usually too fast.
|
|
@export var throw_force_multiplier: float = 0.5
|
|
@export var max_throw_speed: float = 42.0
|
|
## Higher = more visible arc. Real gravity looks too floaty at dart scale.
|
|
@export var flight_gravity_scale: float = 2.0
|
|
|
|
@export_group("Flight")
|
|
## How fast the nose swings to face the direction of travel.
|
|
@export var nose_align_speed: float = 1.0
|
|
## Which local axis is the dart's tip. Check this against the model.
|
|
@export var forward_axis: Vector3 = Vector3.FORWARD
|
|
## Below this speed the dart is considered landed.
|
|
@export var landed_speed_threshold: float = 0.5
|
|
|
|
# --- State --------------------------------------------------------------
|
|
|
|
var state: State = State.IDLE
|
|
|
|
var _drag_plane: Plane
|
|
var _drag_offset: Vector3 = Vector3.ZERO
|
|
var _target_pos: Vector3 = Vector3.ZERO
|
|
|
|
## Sampled positions, used to compute throw velocity on release.
|
|
const HISTORY_LENGTH := 6
|
|
var _pos_history: Array[Vector3] = []
|
|
var _time_history: Array[float] = []
|
|
|
|
|
|
func _ready() -> void:
|
|
freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
|
freeze = true
|
|
add_to_group(&"dart")
|
|
|
|
|
|
# --- Public API (called by main.gd) --------------------------------------
|
|
|
|
func begin_drag(camera: Camera3D, hit_point: Vector3) -> void:
|
|
state = State.DRAGGING
|
|
freeze = false
|
|
|
|
gravity_scale = 0.0 # don't sag while held
|
|
linear_damp = drag_linear_damp
|
|
angular_damp = drag_angular_damp
|
|
|
|
# The plane the dart slides along while dragged. Tilted off the camera's
|
|
# view axis so pulling back also pulls the dart down, which feels natural.
|
|
var base_normal := camera.global_transform.basis.z
|
|
var tilted := base_normal.rotated(
|
|
camera.global_transform.basis.x,
|
|
deg_to_rad(drag_plane_angle)
|
|
)
|
|
_drag_plane = Plane(tilted.normalized(), hit_point)
|
|
_drag_offset = global_position - hit_point
|
|
|
|
# Seed the target to the current position. Without this the dart drops
|
|
# to the plane origin until the mouse first moves.
|
|
_target_pos = global_position
|
|
|
|
_pos_history.clear()
|
|
_time_history.clear()
|
|
|
|
|
|
func update_drag(camera: Camera3D, mouse_pos: Vector2) -> void:
|
|
if state != State.DRAGGING:
|
|
return
|
|
|
|
var ray_origin := camera.project_ray_origin(mouse_pos)
|
|
var ray_dir := camera.project_ray_normal(mouse_pos)
|
|
var hit = _drag_plane.intersects_ray(ray_origin, ray_dir)
|
|
if hit != null:
|
|
_target_pos = hit + _drag_offset
|
|
|
|
func release_drag() -> void:
|
|
if state != State.DRAGGING:
|
|
return
|
|
state = State.IN_FLIGHT
|
|
gravity_scale = flight_gravity_scale
|
|
linear_damp = 0.0
|
|
angular_damp = 0.0
|
|
|
|
# The flick gives aim (up/down/left/right). It CANNOT give forward speed —
|
|
# the drag plane faces the camera, so motion toward the board is ~zero.
|
|
var flick := _calculate_throw_velocity() * throw_force_multiplier
|
|
|
|
# The forward component has to come from somewhere. Use the camera's
|
|
# forward, so "throw" always means "toward what you're looking at".
|
|
var cam := get_viewport().get_camera_3d()
|
|
var forward := -cam.global_transform.basis.z * forward_impulse
|
|
|
|
var throw := flick + forward
|
|
if throw.length() > max_throw_speed:
|
|
throw = throw.normalized() * max_throw_speed
|
|
linear_velocity = throw
|
|
|
|
var raw := _calculate_throw_velocity()
|
|
print("flick: %s forward: %s final: %s (%.1f m/s)" % [flick, forward, throw, throw.length()])
|
|
|
|
|
|
# --- Physics ------------------------------------------------------------
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
match state:
|
|
State.DRAGGING:
|
|
_process_drag()
|
|
State.IN_FLIGHT:
|
|
_process_flight(delta)
|
|
|
|
|
|
func _process_drag() -> void:
|
|
# Chase the target with velocity rather than teleporting, so the dart
|
|
# still collides with things and carries momentum into the throw.
|
|
var to_target := _target_pos - global_position
|
|
var desired := to_target * drag_follow_speed
|
|
if desired.length() > max_drag_speed:
|
|
desired = desired.normalized() * max_drag_speed
|
|
|
|
linear_velocity = desired
|
|
angular_velocity = angular_velocity.lerp(Vector3.ZERO, 0.2)
|
|
|
|
_pos_history.append(global_position)
|
|
_time_history.append(Time.get_ticks_msec() / 1000.0)
|
|
if _pos_history.size() > HISTORY_LENGTH:
|
|
_pos_history.pop_front()
|
|
_time_history.pop_front()
|
|
|
|
|
|
func _process_flight(delta: float) -> void:
|
|
if linear_velocity.length() > landed_speed_threshold:
|
|
_align_to_velocity(delta)
|
|
else:
|
|
_on_landed()
|
|
|
|
|
|
func _align_to_velocity(delta: float) -> void:
|
|
var vel_dir := linear_velocity.normalized()
|
|
var current_forward := global_transform.basis * forward_axis
|
|
var axis := current_forward.cross(vel_dir)
|
|
|
|
if axis.length() < 0.001:
|
|
return # already aligned, or exactly opposed
|
|
|
|
axis = axis.normalized()
|
|
var angle := current_forward.angle_to(vel_dir)
|
|
var step := minf(angle, nose_align_speed * delta)
|
|
|
|
global_transform.basis = global_transform.basis.rotated(axis, step)
|
|
global_transform.basis = global_transform.basis.orthonormalized()
|
|
|
|
|
|
func _on_landed() -> void:
|
|
state = State.LANDED
|
|
angular_velocity = Vector3.ZERO
|
|
landed.emit(self, global_position)
|
|
|
|
|
|
func _calculate_throw_velocity() -> Vector3:
|
|
if _pos_history.size() < 2:
|
|
return linear_velocity
|
|
|
|
var dt: float = _time_history[-1] - _time_history[0]
|
|
if dt <= 0.0:
|
|
return linear_velocity
|
|
|
|
return (_pos_history[-1] - _pos_history[0]) / dt
|