Restructure into core/ and content/official/
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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
|
||||
@@ -0,0 +1 @@
|
||||
uid://sueo33rxr07e
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,45 @@
|
||||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://fp0gcwcyfd7g"
|
||||
path="res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://core/dart/dart.glb"
|
||||
dest_files=["res://.godot/imported/dart.glb-ca8e61ccf0bb4431c3c990bf6c68da95.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
@@ -0,0 +1,21 @@
|
||||
[gd_scene format=3 uid="uid://fshohiewm7et"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://sueo33rxr07e" path="res://core/dart/dart.gd" id="1"]
|
||||
[ext_resource type="PackedScene" uid="uid://fp0gcwcyfd7g" path="res://core/dart/dart.glb" id="2"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_1"]
|
||||
radius = 0.008
|
||||
height = 0.15
|
||||
|
||||
[node name="Dart" type="RigidBody3D" unique_id=1124867376]
|
||||
collision_layer = 2
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="MeshInstance3D" parent="." unique_id=936158903 instance=ExtResource("2")]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=2015440784]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.07, 0)
|
||||
shape = SubResource("CapsuleShape3D_1")
|
||||
|
||||
[node name="Tip" type="Marker3D" parent="." unique_id=2014607155]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.14, 0)
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b3n3th3102f2g"
|
||||
path.s3tc="res://.godot/imported/dart_body.png-b1ba77c6cead7a089d94545960115fac.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "9c4718c8d2830abe8cc9971ef621b46a"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://core/dart/dart_body.png"
|
||||
dest_files=["res://.godot/imported/dart_body.png-b1ba77c6cead7a089d94545960115fac.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c85g6b4hhn83g"
|
||||
path.s3tc="res://.godot/imported/dart_wings.png-45f87827353eb4dc5f134b80f5e80ea7.s3tc.ctex"
|
||||
metadata={
|
||||
"imported_formats": ["s3tc_bptc"],
|
||||
"vram_texture": true
|
||||
}
|
||||
generator_parameters={
|
||||
"md5": "52ee9a89e7c6ed5f537103a41115551e"
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://core/dart/dart_wings.png"
|
||||
dest_files=["res://.godot/imported/dart_wings.png-45f87827353eb4dc5f134b80f5e80ea7.s3tc.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=0
|
||||
Reference in New Issue
Block a user