diff --git a/rope_diag.gd b/rope_diag.gd deleted file mode 100644 index 16c57d0..0000000 --- a/rope_diag.gd +++ /dev/null @@ -1,78 +0,0 @@ -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 diff --git a/rope_diag.gd.uid b/rope_diag.gd.uid deleted file mode 100644 index f2c6c0b..0000000 --- a/rope_diag.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://bgk47b00v2uvm diff --git a/scenes/game_scene/levels/3dlevel.tscn b/scenes/game_scene/levels/3dlevel.tscn index b10e2d9..8b7b5fc 100644 --- a/scenes/game_scene/levels/3dlevel.tscn +++ b/scenes/game_scene/levels/3dlevel.tscn @@ -48,36 +48,21 @@ transform = Transform3D(1, 0, 0, 0, 0.25881907, 0.9659258, 0, -0.9659258, 0.2588 shadow_enabled = true [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) [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.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 [node name="Path3DRope" parent="." unique_id=301387047 instance=ExtResource("2_jetiq")] curve = SubResource("Curve3D_epadm") attach_start_path = NodePath("../Player1") attach_end_path = NodePath("../Player2") -<<<<<<< HEAD max_length = 4.0 elastic_stiffness = 20.0 haul_drag = 20.0 reel_speed = 2.0 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 fixed_start_point = false diff --git a/scripts/rope_tether.gd b/scripts/rope_tether.gd index 11b6d60..7f39a49 100644 --- a/scripts/rope_tether.gd +++ b/scripts/rope_tether.gd @@ -42,7 +42,6 @@ var attach_end : PhysicsBody3D ## at all times and the leash is an explicit force instead. @export var max_length := 20.0 -<<<<<<< HEAD ## 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 ## same whether an endpoint is a CharacterBody3D or a RigidBody3D. @@ -86,11 +85,6 @@ var attach_end : PhysicsBody3D ## winch, high values like the rope is weightless. Reeling out is not rate ## limited — see _reel. @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). @export var plane_lock_z := true @@ -98,85 +92,10 @@ var attach_end : PhysicsBody3D @export_flags_3d_physics var segment_collision_layer := 4 @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 # resting sag; this is only the floor that keeps the chain off dead taut when # slack is turned down to nothing. 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 # our own copy to place the end joint we add. @@ -212,7 +131,6 @@ func _ready() -> void: func _physics_process(delta: float) -> void: -<<<<<<< HEAD if _snapped: # Nothing left to reel, tension or leash. Both halves just hang off their # player, so all that is left is drawing them. @@ -223,20 +141,11 @@ func _physics_process(delta: float) -> void: _apply_elastic(delta) _apply_tautness(delta) _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 # heights, so it has to run after the resize. super(delta) -<<<<<<< HEAD func _on_snap_timer_timeout() -> void: snap() @@ -323,121 +232,10 @@ func _target_length() -> float: # real error to correct and they push it right back off the line. var effective_slack := lerpf(slack, TAUT_MARGIN, _taut_ramp()) 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: var previous := _rope_length -<<<<<<< HEAD var target := _target_length() if target > _rope_length: # Out is instant. Rate-limiting this direction is what lets the gap outrun @@ -450,15 +248,6 @@ func _reel(delta: float) -> void: # it is here so that a slack of 0 still cannot produce a chain shorter than the # straight-line gap. _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: _set_segment_length(_rope_length / float(number_of_segments)) @@ -744,173 +533,9 @@ func _wire_endpoints() -> void: end_joint.set_param(PinJoint3D.PARAM_IMPULSE_CLAMP, max_impulse) joints.append(end_joint) -<<<<<<< HEAD func _update_timer() -> void: var stretched = is_equal_approx(tension_ratio(), 1.0) if stretched and $SnapTimer.is_stopped(): $SnapTimer.start() elif !stretched: $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