Merge branch 'main' into test
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,46 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://cvu5f82j7vxdl"
|
||||||
|
path="res://.godot/imported/Level1_v1.fbx-6541db1786b04595d703452dfc5c0724.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/Level1_v1.fbx"
|
||||||
|
dest_files=["res://.godot/imported/Level1_v1.fbx-6541db1786b04595d703452dfc5c0724.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=true
|
||||||
|
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={}
|
||||||
|
fbx/importer=0
|
||||||
|
fbx/allow_geometry_helper_nodes=false
|
||||||
|
fbx/embedded_image_handling=1
|
||||||
|
fbx/naming_version=2
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
# Drives the players apart at a set speed and measures actual droop: how much
|
||||||
|
# longer the rope's real path is than the straight line between the anchors.
|
||||||
|
# droop ~0 means visually taut. That is the thing the fix has to deliver.
|
||||||
|
|
||||||
|
var rope
|
||||||
|
var p1
|
||||||
|
var p2
|
||||||
|
var t := 0.0
|
||||||
|
var speed := 0.0
|
||||||
|
var trial := 0
|
||||||
|
var speeds := [1.0, 3.0, 6.0]
|
||||||
|
var worst_droop_while_taut := 0.0
|
||||||
|
var saw_taut := false
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var level = load("res://scenes/game_scene/levels/3dlevel.tscn").instantiate()
|
||||||
|
root.add_child(level)
|
||||||
|
rope = level.get_node("Path3DRope")
|
||||||
|
p1 = level.get_node("Player1")
|
||||||
|
p2 = level.get_node("Player2")
|
||||||
|
rope.can_snap = false
|
||||||
|
speed = speeds[0]
|
||||||
|
print("max_length=%.1f reel_speed=%.1f taut_margin=%.2f" % [
|
||||||
|
rope.max_length, rope.reel_speed, rope.taut_margin])
|
||||||
|
|
||||||
|
|
||||||
|
# Length of the rope's actual polyline through every segment.
|
||||||
|
func path_len() -> float:
|
||||||
|
var total := 0.0
|
||||||
|
var pts := []
|
||||||
|
for s in rope.segments:
|
||||||
|
var h: float = s.get_child(0).shape.height * 0.5
|
||||||
|
pts.append(s.position + s.transform.basis.y * h)
|
||||||
|
var last = rope.segments[-1]
|
||||||
|
var lh: float = last.get_child(0).shape.height * 0.5
|
||||||
|
pts.append(last.position - last.transform.basis.y * lh)
|
||||||
|
for i in range(pts.size() - 1):
|
||||||
|
total += pts[i].distance_to(pts[i + 1])
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
func _physics_process(d: float) -> bool:
|
||||||
|
t += d
|
||||||
|
if t < 0.4:
|
||||||
|
return false
|
||||||
|
# Walk them apart at a controlled rate.
|
||||||
|
p1.global_position.x -= speed * d * 0.5
|
||||||
|
p2.global_position.x += speed * d * 0.5
|
||||||
|
p1.velocity.x = 0.0
|
||||||
|
p2.velocity.x = 0.0
|
||||||
|
|
||||||
|
var gap: float = p1.global_position.distance_to(p2.global_position)
|
||||||
|
var droop: float = path_len() - gap
|
||||||
|
if rope.taut:
|
||||||
|
saw_taut = true
|
||||||
|
worst_droop_while_taut = max(worst_droop_while_taut, droop)
|
||||||
|
|
||||||
|
if fmod(t, 0.6) < d:
|
||||||
|
print(" sep=%.0fm/s gap=%5.2f rope=%5.2f path=%5.2f droop=%5.2f taut=%s" % [
|
||||||
|
speed, gap, rope._rope_length, path_len(), droop, str(rope.taut)])
|
||||||
|
|
||||||
|
if gap > rope.max_length * 1.3:
|
||||||
|
print("trial sep=%.0fm/s -> saw_taut=%s worst droop while taut=%.3f\n" % [
|
||||||
|
speed, str(saw_taut), worst_droop_while_taut])
|
||||||
|
trial += 1
|
||||||
|
if trial >= speeds.size():
|
||||||
|
quit(0)
|
||||||
|
return true
|
||||||
|
speed = speeds[trial]
|
||||||
|
saw_taut = false
|
||||||
|
worst_droop_while_taut = 0.0
|
||||||
|
p1.global_position.x = -1.0
|
||||||
|
p2.global_position.x = 1.0
|
||||||
|
t = 0.0
|
||||||
|
return false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bgk47b00v2uvm
|
||||||
@@ -48,21 +48,36 @@ transform = Transform3D(1, 0, 0, 0, 0.25881907, 0.9659258, 0, -0.9659258, 0.2588
|
|||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
|
|
||||||
[node name="Player1" parent="." unique_id=68487004 instance=ExtResource("1_e2xum")]
|
[node name="Player1" parent="." unique_id=68487004 instance=ExtResource("1_e2xum")]
|
||||||
|
<<<<<<< HEAD
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0015082, 1, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0015082, 1, 0)
|
||||||
|
|
||||||
[node name="Player2" parent="." unique_id=432105976 instance=ExtResource("1_e2xum")]
|
[node name="Player2" parent="." unique_id=432105976 instance=ExtResource("1_e2xum")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.154851, 1, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.154851, 1, 0)
|
||||||
|
=======
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1742709, 1, 0)
|
||||||
|
|
||||||
|
[node name="Player2" parent="." unique_id=432105976 instance=ExtResource("1_e2xum")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.182826, 1, 0)
|
||||||
|
>>>>>>> main
|
||||||
player_id = 2
|
player_id = 2
|
||||||
|
|
||||||
[node name="Path3DRope" parent="." unique_id=301387047 instance=ExtResource("2_jetiq")]
|
[node name="Path3DRope" parent="." unique_id=301387047 instance=ExtResource("2_jetiq")]
|
||||||
curve = SubResource("Curve3D_epadm")
|
curve = SubResource("Curve3D_epadm")
|
||||||
attach_start_path = NodePath("../Player1")
|
attach_start_path = NodePath("../Player1")
|
||||||
attach_end_path = NodePath("../Player2")
|
attach_end_path = NodePath("../Player2")
|
||||||
|
<<<<<<< HEAD
|
||||||
max_length = 4.0
|
max_length = 4.0
|
||||||
elastic_stiffness = 20.0
|
elastic_stiffness = 20.0
|
||||||
haul_drag = 20.0
|
haul_drag = 20.0
|
||||||
reel_speed = 2.0
|
reel_speed = 2.0
|
||||||
number_of_segments = 16
|
number_of_segments = 16
|
||||||
|
=======
|
||||||
|
max_length = 6.0
|
||||||
|
reel_speed = 2.0
|
||||||
|
tension_start = 4.8
|
||||||
|
snap_length = 10.0
|
||||||
|
number_of_segments = 12
|
||||||
|
>>>>>>> main
|
||||||
cable_thickness = 0.05
|
cable_thickness = 0.05
|
||||||
fixed_start_point = false
|
fixed_start_point = false
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ var attach_end : PhysicsBody3D
|
|||||||
## at all times and the leash is an explicit force instead.
|
## at all times and the leash is an explicit force instead.
|
||||||
@export var max_length := 20.0
|
@export var max_length := 20.0
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
## Leash spring: metres per second squared of pull per metre of stretch, plus
|
## Leash spring: metres per second squared of pull per metre of stretch, plus
|
||||||
## damping on the separation speed. Applied as acceleration, so it behaves the
|
## damping on the separation speed. Applied as acceleration, so it behaves the
|
||||||
## same whether an endpoint is a CharacterBody3D or a RigidBody3D.
|
## same whether an endpoint is a CharacterBody3D or a RigidBody3D.
|
||||||
@@ -85,6 +86,11 @@ var attach_end : PhysicsBody3D
|
|||||||
## winch, high values like the rope is weightless. Reeling out is not rate
|
## winch, high values like the rope is weightless. Reeling out is not rate
|
||||||
## limited — see _reel.
|
## limited — see _reel.
|
||||||
@export var reel_speed := 6.0
|
@export var reel_speed := 6.0
|
||||||
|
=======
|
||||||
|
## How fast total length changes, in metres per second. Low values feel like a
|
||||||
|
## winch, high values like the rope is weightless.
|
||||||
|
@export var reel_speed := 8.0
|
||||||
|
>>>>>>> main
|
||||||
|
|
||||||
## Constrain segments to the X-Y plane (linear Z, angular X and Y).
|
## Constrain segments to the X-Y plane (linear Z, angular X and Y).
|
||||||
@export var plane_lock_z := true
|
@export var plane_lock_z := true
|
||||||
@@ -92,10 +98,85 @@ var attach_end : PhysicsBody3D
|
|||||||
@export_flags_3d_physics var segment_collision_layer := 4
|
@export_flags_3d_physics var segment_collision_layer := 4
|
||||||
@export_flags_3d_physics var segment_collision_mask := 1
|
@export_flags_3d_physics var segment_collision_mask := 1
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# Fraction of the endpoint gap the rope keeps in hand at minimum. `slack` is the
|
# Fraction of the endpoint gap the rope keeps in hand at minimum. `slack` is the
|
||||||
# resting sag; this is only the floor that keeps the chain off dead taut when
|
# resting sag; this is only the floor that keeps the chain off dead taut when
|
||||||
# slack is turned down to nothing.
|
# slack is turned down to nothing.
|
||||||
const TAUT_MARGIN := 0.02
|
const TAUT_MARGIN := 0.02
|
||||||
|
=======
|
||||||
|
|
||||||
|
# --- Pull / tension / snap ---------------------------------------------------
|
||||||
|
#
|
||||||
|
# Ported from the 2D Verlet rope. That rope integrated its own points, so it
|
||||||
|
# could resolve stretch by moving them directly; here the rope body is solved by
|
||||||
|
# the physics server, so only the *anchor* half of that logic carries over: the
|
||||||
|
# players get pulled, damped and finally launched. `max_length` is shared with
|
||||||
|
# the reel, which is exactly right — the reel stops paying out rope there, so
|
||||||
|
# that is the gap at which the rope runs out and starts pulling back.
|
||||||
|
|
||||||
|
## Note: the reference script's `reel_speed` is this — how fast the rope hauls
|
||||||
|
## the players together. Ours was already taken by the rope's own length change,
|
||||||
|
## so the anchor-pulling rate is `pull_speed`.
|
||||||
|
@export_group("Tension")
|
||||||
|
|
||||||
|
## Gap at which strain starts registering for visuals. Below max_length, so the
|
||||||
|
## rope reads as straining before it actually starts pulling.
|
||||||
|
@export var tension_start := 16.0
|
||||||
|
|
||||||
|
## Fraction of its nominal length the rope gives before it is genuinely straight,
|
||||||
|
## soaked up by compliant pin joints and the two end anchors.
|
||||||
|
##
|
||||||
|
## 0.12 is the measured sweet spot on a 6m/12-segment rope: sag at the moment
|
||||||
|
## pull engages falls from 0.73m at 0.02 to 0.41m here. Past ~0.18 it gets worse
|
||||||
|
## again — the extra span opens the joints further, which adds back the path
|
||||||
|
## length it was meant to take up. Retune if segment count or joint softness
|
||||||
|
## changes; those move the curve.
|
||||||
|
@export_range(0.0, 0.5, 0.005) var taut_margin := 0.12
|
||||||
|
|
||||||
|
## Metres per second the rope hauls the players together while over-stretched.
|
||||||
|
@export var pull_speed := 6.0
|
||||||
|
|
||||||
|
## How the pull is split. 0.5 moves both equally; 0.0 moves only attach_end.
|
||||||
|
@export_range(0.0, 1.0, 0.01) var pull_bias := 0.5
|
||||||
|
|
||||||
|
## Fraction of velocity-away-from-the-rope killed per tick while taut.
|
||||||
|
@export_range(0.0, 1.0, 0.01) var damping := 0.2
|
||||||
|
|
||||||
|
## Hard break distance, and how fast sustained over-stretch accumulates toward a
|
||||||
|
## break. The rope snaps on whichever arrives first.
|
||||||
|
@export var snap_length := 34.0
|
||||||
|
@export var snap_tension := 1.0
|
||||||
|
@export var tension_rise := 1.5
|
||||||
|
@export var tension_fall := 2.0
|
||||||
|
|
||||||
|
## Speed kicked into each player when the rope lets go.
|
||||||
|
@export var snap_impulse := 19.0
|
||||||
|
|
||||||
|
## Seconds the two dead halves dangle and fade before being freed.
|
||||||
|
@export var snap_fade := 0.6
|
||||||
|
|
||||||
|
## Off makes the rope an unbreakable leash: it still pulls, never snaps.
|
||||||
|
@export var can_snap := true
|
||||||
|
|
||||||
|
signal became_taut
|
||||||
|
signal became_slack
|
||||||
|
signal rope_snapped
|
||||||
|
signal tension_changed(amount)
|
||||||
|
|
||||||
|
## True while the gap exceeds max_length and the rope is actively hauling.
|
||||||
|
var taut := false
|
||||||
|
## True once the rope has broken. It stays broken until reset().
|
||||||
|
var snapped := false
|
||||||
|
## Accumulated over-stretch. Reaching snap_tension breaks the rope.
|
||||||
|
var tension := 0.0
|
||||||
|
## 0..1 strain for visuals and camera shake. Driven by whichever of raw distance
|
||||||
|
## or accumulated tension is further along.
|
||||||
|
var strain_amt := 0.0
|
||||||
|
|
||||||
|
var _emitted_strain := -1.0
|
||||||
|
var _break_time := 0.0
|
||||||
|
var _halves : Array[Path3D] = []
|
||||||
|
>>>>>>> main
|
||||||
|
|
||||||
# The base script zeroes `position` and bakes it into child positions, so keep
|
# The base script zeroes `position` and bakes it into child positions, so keep
|
||||||
# our own copy to place the end joint we add.
|
# our own copy to place the end joint we add.
|
||||||
@@ -131,6 +212,7 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
|
<<<<<<< HEAD
|
||||||
if _snapped:
|
if _snapped:
|
||||||
# Nothing left to reel, tension or leash. Both halves just hang off their
|
# Nothing left to reel, tension or leash. Both halves just hang off their
|
||||||
# player, so all that is left is drawing them.
|
# player, so all that is left is drawing them.
|
||||||
@@ -141,11 +223,20 @@ func _physics_process(delta: float) -> void:
|
|||||||
_apply_elastic(delta)
|
_apply_elastic(delta)
|
||||||
_apply_tautness(delta)
|
_apply_tautness(delta)
|
||||||
_update_timer()
|
_update_timer()
|
||||||
|
=======
|
||||||
|
if snapped:
|
||||||
|
_update_break(delta)
|
||||||
|
return
|
||||||
|
if dynamic_length:
|
||||||
|
_reel(delta)
|
||||||
|
_constrain(delta)
|
||||||
|
>>>>>>> main
|
||||||
# Base script redraws the CSG curve from the segment transforms and capsule
|
# Base script redraws the CSG curve from the segment transforms and capsule
|
||||||
# heights, so it has to run after the resize.
|
# heights, so it has to run after the resize.
|
||||||
super(delta)
|
super(delta)
|
||||||
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
func _on_snap_timer_timeout() -> void:
|
func _on_snap_timer_timeout() -> void:
|
||||||
snap()
|
snap()
|
||||||
|
|
||||||
@@ -232,10 +323,121 @@ func _target_length() -> float:
|
|||||||
# real error to correct and they push it right back off the line.
|
# real error to correct and they push it right back off the line.
|
||||||
var effective_slack := lerpf(slack, TAUT_MARGIN, _taut_ramp())
|
var effective_slack := lerpf(slack, TAUT_MARGIN, _taut_ramp())
|
||||||
return maxf(_endpoint_span() * (1.0 + effective_slack), min_length)
|
return maxf(_endpoint_span() * (1.0 + effective_slack), min_length)
|
||||||
|
=======
|
||||||
|
## Pull the players together while the gap exceeds the rope, accumulate strain,
|
||||||
|
## and break the rope when it has taken too much.
|
||||||
|
func _constrain(delta: float) -> void:
|
||||||
|
if attach_start == null or attach_end == null:
|
||||||
|
return
|
||||||
|
var v := attach_end.global_position - attach_start.global_position
|
||||||
|
if plane_lock_z:
|
||||||
|
v.z = 0.0
|
||||||
|
var dist := v.length()
|
||||||
|
if dist < 0.001:
|
||||||
|
return
|
||||||
|
var stretch := dist - _taut_distance()
|
||||||
|
|
||||||
|
var now_taut := stretch > 0.0
|
||||||
|
if now_taut != taut:
|
||||||
|
taut = now_taut
|
||||||
|
if taut:
|
||||||
|
became_taut.emit()
|
||||||
|
else:
|
||||||
|
became_slack.emit()
|
||||||
|
|
||||||
|
# Tension only builds while genuinely over-stretched, and bleeds off the rest
|
||||||
|
# of the time, so a rope that is repeatedly yanked breaks but one held at a
|
||||||
|
# steady hard stretch does not break instantly.
|
||||||
|
if stretch > 0.0:
|
||||||
|
var s := clampf(stretch / maxf(snap_length - max_length, 0.001), 0.0, 1.0)
|
||||||
|
tension += tension_rise * s * delta
|
||||||
|
else:
|
||||||
|
tension = move_toward(tension, 0.0, tension_fall * delta)
|
||||||
|
|
||||||
|
var dist_strain := clampf(
|
||||||
|
(dist - tension_start) / maxf(snap_length - tension_start, 0.001), 0.0, 1.0)
|
||||||
|
strain_amt = maxf(dist_strain, tension / maxf(snap_tension, 0.001))
|
||||||
|
_emit_strain()
|
||||||
|
|
||||||
|
if now_taut:
|
||||||
|
var dir := v / dist
|
||||||
|
var pull := minf(stretch, pull_speed * delta)
|
||||||
|
_haul(attach_start, dir * pull * pull_bias)
|
||||||
|
_haul(attach_end, -dir * pull * (1.0 - pull_bias))
|
||||||
|
if damping > 0.0:
|
||||||
|
_damp(attach_start, -dir)
|
||||||
|
_damp(attach_end, dir)
|
||||||
|
|
||||||
|
if can_snap and (tension >= snap_tension or dist >= snap_length):
|
||||||
|
_snap()
|
||||||
|
|
||||||
|
|
||||||
|
# How far apart the rope can actually hold the players, which is *not* the
|
||||||
|
# length the reel commands. Pin joints are compliant: measured on the stock
|
||||||
|
# settings the interior joints open ~0.29m and the two end anchors another
|
||||||
|
# ~0.14m on a 6m rope, so the rope keeps roughly `taut_margin` in hand past its
|
||||||
|
# nominal length. Pulling at nominal starts hauling while the rope still droops.
|
||||||
|
#
|
||||||
|
# This deliberately does not measure the rope's live path. That was tried and it
|
||||||
|
# feeds back: compliant joints stretch to meet whatever the gap is, so measured
|
||||||
|
# reach chases the span, `stretch` never turns positive and the rope never pulls
|
||||||
|
# at all. The allowance has to be a value the current stretch cannot influence.
|
||||||
|
func _taut_distance() -> float:
|
||||||
|
return _rope_length * (1.0 + taut_margin)
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_strain() -> void:
|
||||||
|
if absf(strain_amt - _emitted_strain) > 0.01:
|
||||||
|
_emitted_strain = strain_amt
|
||||||
|
tension_changed.emit(strain_amt)
|
||||||
|
|
||||||
|
|
||||||
|
# Position is moved rather than velocity added, matching the reference: the pull
|
||||||
|
# has to survive the player's own _physics_process, which rewrites velocity.x
|
||||||
|
# from input every tick and would swallow a velocity nudge.
|
||||||
|
func _haul(body: PhysicsBody3D, delta_pos: Vector3) -> void:
|
||||||
|
var rigid := body as RigidBody3D
|
||||||
|
if rigid != null:
|
||||||
|
rigid.apply_central_impulse(delta_pos / maxf(get_physics_process_delta_time(), 0.001))
|
||||||
|
else:
|
||||||
|
body.global_position += delta_pos
|
||||||
|
|
||||||
|
|
||||||
|
# Kill only the component of velocity heading away from the rope, so the rope
|
||||||
|
# resists being stretched without deadening movement along it.
|
||||||
|
func _damp(body: PhysicsBody3D, away: Vector3) -> void:
|
||||||
|
var character := body as CharacterBody3D
|
||||||
|
if character != null:
|
||||||
|
var along := character.velocity.dot(away)
|
||||||
|
if along > 0.0:
|
||||||
|
character.velocity -= away * along * damping
|
||||||
|
return
|
||||||
|
var rigid := body as RigidBody3D
|
||||||
|
if rigid != null:
|
||||||
|
var along := rigid.linear_velocity.dot(away)
|
||||||
|
if along > 0.0:
|
||||||
|
rigid.linear_velocity -= away * along * damping
|
||||||
|
|
||||||
|
|
||||||
|
## Length the rope wants to be for the current endpoint gap.
|
||||||
|
##
|
||||||
|
## Divided by the compliance allowance rather than set to the gap directly. The
|
||||||
|
## chain can reach `taut_margin` past whatever length it is told to be, so
|
||||||
|
## commanding exactly the gap leaves that surplus hanging as droop at every
|
||||||
|
## distance — which is what made the rope look slack however hard it was pulled.
|
||||||
|
## Commanding the gap *minus* the stretch it is going to gain means its real
|
||||||
|
## reach lands on the gap, and the rope hangs straight.
|
||||||
|
func _target_length() -> float:
|
||||||
|
if attach_start == null or attach_end == null:
|
||||||
|
return maxf(distance, min_length)
|
||||||
|
var span := attach_start.global_position.distance_to(attach_end.global_position)
|
||||||
|
return clampf(span / (1.0 + taut_margin), min_length, max_length)
|
||||||
|
>>>>>>> main
|
||||||
|
|
||||||
|
|
||||||
func _reel(delta: float) -> void:
|
func _reel(delta: float) -> void:
|
||||||
var previous := _rope_length
|
var previous := _rope_length
|
||||||
|
<<<<<<< HEAD
|
||||||
var target := _target_length()
|
var target := _target_length()
|
||||||
if target > _rope_length:
|
if target > _rope_length:
|
||||||
# Out is instant. Rate-limiting this direction is what lets the gap outrun
|
# Out is instant. Rate-limiting this direction is what lets the gap outrun
|
||||||
@@ -248,6 +450,15 @@ func _reel(delta: float) -> void:
|
|||||||
# it is here so that a slack of 0 still cannot produce a chain shorter than the
|
# it is here so that a slack of 0 still cannot produce a chain shorter than the
|
||||||
# straight-line gap.
|
# straight-line gap.
|
||||||
_rope_length = maxf(_rope_length, _endpoint_span() * (1.0 + TAUT_MARGIN))
|
_rope_length = maxf(_rope_length, _endpoint_span() * (1.0 + TAUT_MARGIN))
|
||||||
|
=======
|
||||||
|
# Both directions are rate limited. Paying out instantly was what made the
|
||||||
|
# rope feel permanently slack: the commanded length would sit exactly on the
|
||||||
|
# gap, and since the chain can reach `taut_margin` beyond what it is told to
|
||||||
|
# be, that surplus had nowhere to go and hung as droop no matter how hard the
|
||||||
|
# players pulled. Trailing the gap instead means a pull faster than
|
||||||
|
# `reel_speed` eats the surplus and the rope comes up taut.
|
||||||
|
_rope_length = move_toward(_rope_length, _target_length(), reel_speed * delta)
|
||||||
|
>>>>>>> main
|
||||||
if absf(_rope_length - previous) > 0.0001:
|
if absf(_rope_length - previous) > 0.0001:
|
||||||
_set_segment_length(_rope_length / float(number_of_segments))
|
_set_segment_length(_rope_length / float(number_of_segments))
|
||||||
|
|
||||||
@@ -533,9 +744,173 @@ func _wire_endpoints() -> void:
|
|||||||
end_joint.set_param(PinJoint3D.PARAM_IMPULSE_CLAMP, max_impulse)
|
end_joint.set_param(PinJoint3D.PARAM_IMPULSE_CLAMP, max_impulse)
|
||||||
joints.append(end_joint)
|
joints.append(end_joint)
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
func _update_timer() -> void:
|
func _update_timer() -> void:
|
||||||
var stretched = is_equal_approx(tension_ratio(), 1.0)
|
var stretched = is_equal_approx(tension_ratio(), 1.0)
|
||||||
if stretched and $SnapTimer.is_stopped():
|
if stretched and $SnapTimer.is_stopped():
|
||||||
$SnapTimer.start()
|
$SnapTimer.start()
|
||||||
elif !stretched:
|
elif !stretched:
|
||||||
$SnapTimer.stop()
|
$SnapTimer.stop()
|
||||||
|
=======
|
||||||
|
|
||||||
|
## Break the rope: kick the players apart and split the chain in two.
|
||||||
|
##
|
||||||
|
## The reference had to hand-integrate the two dead halves after a break. Here
|
||||||
|
## the halves are still pin-jointed rigid bodies with one loose end, so the
|
||||||
|
## physics server drapes them for free — all that is needed is to drop the
|
||||||
|
## middle joint and give each half its own tube to draw into.
|
||||||
|
func _snap() -> void:
|
||||||
|
var dir := attach_end.global_position - attach_start.global_position
|
||||||
|
if plane_lock_z:
|
||||||
|
dir.z = 0.0
|
||||||
|
dir = dir.normalized() if dir.length() > 0.001 else Vector3.RIGHT
|
||||||
|
_kick(attach_start, -dir * snap_impulse)
|
||||||
|
_kick(attach_end, dir * snap_impulse)
|
||||||
|
|
||||||
|
_split_chain()
|
||||||
|
|
||||||
|
snapped = true
|
||||||
|
taut = false
|
||||||
|
tension = 0.0
|
||||||
|
strain_amt = 0.0
|
||||||
|
_emit_strain()
|
||||||
|
rope_snapped.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _kick(body: PhysicsBody3D, impulse: Vector3) -> void:
|
||||||
|
var character := body as CharacterBody3D
|
||||||
|
if character != null:
|
||||||
|
character.velocity += impulse
|
||||||
|
return
|
||||||
|
var rigid := body as RigidBody3D
|
||||||
|
if rigid != null:
|
||||||
|
rigid.apply_central_impulse(impulse * rigid.mass)
|
||||||
|
|
||||||
|
|
||||||
|
# Free the middle joint so the chain parts, then hand each half its own path and
|
||||||
|
# tube. The single shared tube cannot be reused: its curve would still run
|
||||||
|
# through both halves and stretch a band of geometry across the break.
|
||||||
|
func _split_chain() -> void:
|
||||||
|
var mid := int(segments.size() / 2.0)
|
||||||
|
# joints[i] bridges segments[i-1] and segments[i], so joints[mid] is the one
|
||||||
|
# holding the two halves together.
|
||||||
|
if mid > 0 and mid < joints.size():
|
||||||
|
var seam := joints[mid]
|
||||||
|
joints.remove_at(mid)
|
||||||
|
seam.queue_free()
|
||||||
|
|
||||||
|
mesh.visible = false
|
||||||
|
_halves.clear()
|
||||||
|
_halves.append(_make_half(0, mid - 1))
|
||||||
|
_halves.append(_make_half(mid, segments.size() - 1))
|
||||||
|
_break_time = 0.0
|
||||||
|
_update_break(0.0)
|
||||||
|
|
||||||
|
|
||||||
|
# A Path3D + CSGPolygon3D pair covering segments[lo..hi], mirroring the settings
|
||||||
|
# of the main tube so the halves look like the rope they came from.
|
||||||
|
func _make_half(lo: int, hi: int) -> Path3D:
|
||||||
|
var path := Path3D.new()
|
||||||
|
path.top_level = true
|
||||||
|
path.curve = Curve3D.new()
|
||||||
|
path.set_meta("lo", lo)
|
||||||
|
path.set_meta("hi", hi)
|
||||||
|
add_child(path)
|
||||||
|
|
||||||
|
var tube := CSGPolygon3D.new()
|
||||||
|
tube.polygon = mesh.polygon
|
||||||
|
tube.mode = mesh.mode
|
||||||
|
tube.path_interval_type = mesh.path_interval_type
|
||||||
|
tube.path_interval = mesh.path_interval
|
||||||
|
tube.path_rotation = mesh.path_rotation
|
||||||
|
tube.path_local = mesh.path_local
|
||||||
|
tube.path_continuous_u = mesh.path_continuous_u
|
||||||
|
tube.path_joined = mesh.path_joined
|
||||||
|
tube.smooth_faces = mesh.smooth_faces
|
||||||
|
tube.calculate_tangents = mesh.calculate_tangents
|
||||||
|
# The rope material is an opaque resource shared with anything else using it,
|
||||||
|
# so the fade gets its own transparent copy rather than mutating the original.
|
||||||
|
if material != null:
|
||||||
|
var faded := material.duplicate() as StandardMaterial3D
|
||||||
|
if faded != null:
|
||||||
|
faded.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
tube.material = faded
|
||||||
|
path.add_child(tube)
|
||||||
|
# path_node resolves relative to the CSG node, so it can only be set once the
|
||||||
|
# tube is actually in the tree under the path.
|
||||||
|
tube.path_node = tube.get_path_to(path)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
func _update_break(delta: float) -> void:
|
||||||
|
_break_time += delta
|
||||||
|
var alpha := clampf(1.0 - _break_time / maxf(snap_fade, 0.001), 0.0, 1.0)
|
||||||
|
for path in _halves:
|
||||||
|
if not is_instance_valid(path):
|
||||||
|
continue
|
||||||
|
_trace_half(path)
|
||||||
|
var tube := path.get_child(0) as CSGPolygon3D
|
||||||
|
var mat := tube.material as StandardMaterial3D
|
||||||
|
if mat != null:
|
||||||
|
mat.albedo_color.a = alpha
|
||||||
|
if _break_time >= snap_fade:
|
||||||
|
_clear_rope()
|
||||||
|
|
||||||
|
|
||||||
|
# Same endpoint math the base script uses for the main curve: a capsule's +Y end
|
||||||
|
# is the one nearer the rope start, so the run is every segment's near end plus
|
||||||
|
# the far end of the last one.
|
||||||
|
func _trace_half(path: Path3D) -> void:
|
||||||
|
var lo : int = path.get_meta("lo")
|
||||||
|
var hi : int = path.get_meta("hi")
|
||||||
|
var c := path.curve
|
||||||
|
c.clear_points()
|
||||||
|
for i in range(lo, hi + 1):
|
||||||
|
var seg := segments[i]
|
||||||
|
var half_h : float = seg.get_child(0).shape.height * 0.5
|
||||||
|
c.add_point(seg.position + seg.transform.basis.y * half_h)
|
||||||
|
var last := segments[hi]
|
||||||
|
var last_half : float = last.get_child(0).shape.height * 0.5
|
||||||
|
c.add_point(last.position - last.transform.basis.y * last_half)
|
||||||
|
|
||||||
|
|
||||||
|
# Tear down every body, joint and tube the rope owns. A snapped rope should stop
|
||||||
|
# costing physics once it has finished falling.
|
||||||
|
func _clear_rope() -> void:
|
||||||
|
for path in _halves:
|
||||||
|
if is_instance_valid(path):
|
||||||
|
path.queue_free()
|
||||||
|
_halves.clear()
|
||||||
|
for joint in joints:
|
||||||
|
if is_instance_valid(joint):
|
||||||
|
joint.queue_free()
|
||||||
|
joints.clear()
|
||||||
|
for segment in segments:
|
||||||
|
if is_instance_valid(segment):
|
||||||
|
segment.queue_free()
|
||||||
|
segments.clear()
|
||||||
|
curve_points.clear()
|
||||||
|
|
||||||
|
|
||||||
|
## Rebuild the rope from scratch after a snap.
|
||||||
|
func reset() -> void:
|
||||||
|
_clear_rope()
|
||||||
|
snapped = false
|
||||||
|
taut = false
|
||||||
|
tension = 0.0
|
||||||
|
strain_amt = 0.0
|
||||||
|
_emitted_strain = -1.0
|
||||||
|
_break_time = 0.0
|
||||||
|
if mesh != null:
|
||||||
|
mesh.visible = true
|
||||||
|
# The base _ready() bakes `position` into the segments and then zeroes it, so
|
||||||
|
# the offset has to be put back before rebuilding or the new rope spawns at
|
||||||
|
# the parent's origin instead of the rope's.
|
||||||
|
position = _origin_offset
|
||||||
|
curve = Curve3D.new()
|
||||||
|
_fit_curve_to_endpoints()
|
||||||
|
super._ready()
|
||||||
|
_apply_plane_lock()
|
||||||
|
_wire_endpoints()
|
||||||
|
_rope_length = _target_length()
|
||||||
|
>>>>>>> main
|
||||||
|
|||||||
Reference in New Issue
Block a user