extends SceneTree
## Does SLOPE stop the player, and if so at which layer?
##
## large_coord_physics_gate settled that coordinate magnitude does NOT: on FLAT
## ground at the DC Erosion spawn the shipping capsule travelled 99.8% of the
## expected distance. So "gets stuck and cannot travel" on the big maps has to
## come from something else, and the erosion map's own comments measure an RMS
## slope of 0.50 at 60 km wavelength with a documented "everything is a cliff"
## complaint. Slope is the candidate.
##
## TWO LAYERS, MEASURED SEPARATELY, because they fail for different reasons and
## a single number would hide which one is biting:
##
## A. THE ENGINE. A bare CharacterBody3D with the shipping capsule and the
## shipping floor settings (floor_max_angle 1.047 rad = 60 deg,
## floor_snap_length 0.2, floor_constant_speed true) driven uphill at a
## constant velocity. This measures move_and_slide and the floor
## classification, nothing else.
##
## B. THE CONTROLLER. player_procedural's OWN _update_movement, called on a
## real instance of the shipping scene -- not a transcription of it. It sets
## max_speed = run_speed * (1 - slope_component)
## and then damps velocity by pow(E, -WALK_ACCEL*dt/max_speed), so as
## max_speed falls toward zero the damping goes to zero with it and the
## steady-state speed collapses. That is arithmetic, no physics involved,
## and it is testable without a floor at all.
##
## Run:
## "$HYPER_TPS_GODOT" --headless --script res://scenes/test/slope_travel_gate.gd
## load(), NOT preload(). preload resolves at PARSE time -- before the project's
## autoloads are registered -- so player_procedural.gd failed to compile with
## "Identifier not found: GameManager" and instantiate() handed back a bare
## scriptless CharacterBody3D. The gate then reported a table of nothing.
var _player_scene: PackedScene = null
const CAP_RADIUS := 0.3 # player_procedural.tscn:14
const CAP_HEIGHT := 1.58 # player_procedural.tscn:15
const CAP_Y := 0.79 # player_procedural.tscn:48
const FLOOR_MAX_ANGLE := 1.047
const FLOOR_SNAP := 0.2
const DRIVE_SPEED := 8.0 # run_speed, player_procedural.gd:95
const SETTLE_FRAMES := 60
const DRIVE_FRAMES := 120
## The DC Erosion spawn altitude, because that is where the report came from.
const SPAWN_ALTITUDE := 9700.0
var _angles: Array = [0.0, 10.0, 20.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0, 60.0, 65.0]
## The SAME triangulated slope at two altitudes. This is the control the first
## run lacked: it separates SLOPE from ALTITUDE, which is the open question.
##
## A rotated BoxShape3D was tried as a "no internal edges" control and had to be
## dropped -- it stalls at frame 14 on FLAT ground at 9700 m and travels 10.9%,
## so it is broken by the very variable it was meant to hold still. (That box
## behaviour is real and unexplained; large_coord_physics_gate sees it too. It is
## not the shipping ground, which is a ConcavePolygonShape3D.)
## The altitude ladder. The boundary is NOT a cliff -- measured 2026-08-31:
## 0..6000 clean at every angle
## 7000 one failing cell (20 deg, 21.2%, stall at frame 28)
## 8000 clean
## 9700 stalls at every shallow angle
## 15000 chaotic, 55 deg down to 9.6%
## Isolated failures appear around 7 km and are pervasive by 9.7 km, which is why
## map_dc_erosion now centres its band on zero (max |y| 5300).
var _grounds: Array = [0.0, 5000.0, 9700.0, 15000.0]
## THE PREDICTION THIS TESTED, AND IT WAS FALSIFIED. Travel is clean at
## 0/500/2000/5000 m and breaks at 9700 m. float32 spacing at y is y * 2^-23 --
## 0.00049 m at 5000, 0.00098 m at 9700 -- and CharacterBody3D's safe_margin
## DEFAULTS TO 0.001 m, so the break lands exactly where the representable step
## reaches the margin the solver depenetrates with. That predicted a one-line
## fix: raise the margin above the step.
##
## Measured at 9700 m, margin 0.001 -> 0.010:
##
## 5 deg 87.3% -> 25.3% WORSE
## 10 deg 50.2% -> 50.0% unchanged
## 15 deg 18.9% -> 107.0% fixed
## 20 deg 12.9% -> 76.9% better, still stalls at frame 99
## 25 deg 18.1% -> 90.0% fixed
## 30 deg 68.6% -> 85.8% fixed
##
## So the margin is A factor -- it restores the mid slopes and takes on_floor
## from 70-98% to a flat 100%, i.e. it does fix the floor-contact flapping --
## but it makes the SHALLOWEST slopes worse and is not a monotone knob. The
## ulp-versus-margin story is not the whole mechanism and raising safe_margin is
## not the fix.
##
## The measured remedy is the one that needs no engine change: keep the playable
## surface BELOW ~5 km of altitude. Everything at 0/500/2000/5000 m is clean at
## every angle and every margin.
var _margins: Array = [0.001, 0.01]
var _midx: int = 0
var _gidx: int = 0
var _min_step: float = 1e30
var _stall_frame: int = -1
var _warm: int = 0
var _idx: int = 0
var _phase: int = 0
var _frame: int = 0
var _body: CharacterBody3D = null
var _ground: StaticBody3D = null
var _start: Vector3 = Vector3.ZERO
var _floor_frames: int = 0
var _rows: Array = []
var _tick: float = 1.0 / 60.0
func _initialize() -> void:
_tick = 1.0 / float(Engine.physics_ticks_per_second)
# ---------------------------------------------------------------------------
# LAYER B — the controller's own arithmetic, on a real instance of the scene.
# ---------------------------------------------------------------------------
func _layer_b() -> void:
print("")
print("LAYER B -- player_procedural._update_movement, called on the shipping scene")
print("(no physics, no floor: this is the controller's speed ceiling alone)")
print("")
print("slope ground normal max_speed steady speed of flat")
print("--------------------------------------------------------------------")
var flat_speed := 0.0
for a in _angles:
var rad: float = deg_to_rad(a)
var n := Vector3(-sin(rad), cos(rad), 0.0).normalized()
if _player_scene == null:
_player_scene = load("res://scenes/player/player_procedural.tscn")
var p: Node = _player_scene.instantiate()
if p.get_script() == null:
print(" ABORT: the player scene has no script attached -- the gate is")
print(" measuring a bare CharacterBody3D, not the controller.")
p.queue_free()
return
# DISABLED before it enters the tree, so _ready still wires the @onready
# refs but _physics_process never runs and cannot fight the direct calls
# below. The scene is the artifact under test; a hand-built stand-in
# would be a transcription and would not be.
p.process_mode = Node.PROCESS_MODE_DISABLED
root.add_child(p)
p.gravity_dir = Vector3.DOWN
p.ground_normal = n
p.surface_friction = 1.0
p.duck_amount = 0.0
p.sprinting = false
p.walking = false
p.velocity = Vector3.ZERO
# --ski-hold: what HOLDING jump does, as opposed to what a steep slope
# does on its own. Both take traction away; only this one works on flat.
for arg in OS.get_cmdline_user_args():
if arg == "--ski-hold":
p.ski_holding = true
# Face uphill: -z of camera_head must be +X, which is where the slope rises.
# --across: input points ACROSS the slope instead of straight up it, which
# is the traverse where holding and moving compete for the same budget.
var face: float = -PI * 0.5
for arg2 in OS.get_cmdline_user_args():
if arg2 == "--across":
face = 0.0
p.camera_head.global_transform = Transform3D(Basis(Vector3.UP, face), Vector3.ZERO)
p.input_sync.input_direction = Vector2(0.0, -1.0)
var k := 0
while k < 600:
p._update_movement(_tick)
k += 1
var speed: float = p.velocity.length()
if a == 0.0:
flat_speed = speed
var frac := 0.0
if flat_speed > 0.0:
frac = 100.0 * speed / flat_speed
print("%5.0f deg (%+.3f, %.3f, 0) %8.3f %8.3f m/s %5.1f%%" % [
a, n.x, n.y, p.max_speed, speed, frac])
p.queue_free()
print("")
# ---------------------------------------------------------------------------
# LAYER A — the engine. Shipping capsule and floor settings, constant velocity.
# ---------------------------------------------------------------------------
func _build() -> void:
var a: float = _angles[_idx]
var rad: float = deg_to_rad(a)
var s: float = tan(rad)
_ground = StaticBody3D.new()
var cs := CollisionShape3D.new()
var f := PackedVector3Array()
var i := 0
while i < 60:
var j := 0
while j < 20:
var x0 := float(i) * 2.0 - 20.0
var z0 := float(j) * 2.0 - 20.0
var x1 := x0 + 2.0
var z1 := z0 + 2.0
var y0 := x0 * s
var y1 := x1 * s
# CW so the face normal points UP. [[feedback_godot-cw-winding]]
f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z1)); f.push_back(Vector3(x0, y0, z1))
f.push_back(Vector3(x0, y0, z0)); f.push_back(Vector3(x1, y1, z0)); f.push_back(Vector3(x1, y1, z1))
j += 1
i += 1
var tri := ConcavePolygonShape3D.new()
tri.set_faces(f)
cs.shape = tri
_ground.add_child(cs)
_ground.position = Vector3(0.0, _alt(), 0.0)
root.add_child(_ground)
_build_body()
func _alt() -> float:
return _grounds[_gidx]
func _build_body() -> void:
_body = CharacterBody3D.new()
_body.floor_max_angle = FLOOR_MAX_ANGLE
_body.floor_snap_length = FLOOR_SNAP
_body.floor_constant_speed = true
_body.safe_margin = _margins[_midx]
var bcs := CollisionShape3D.new()
var cap := CapsuleShape3D.new()
cap.radius = CAP_RADIUS
cap.height = CAP_HEIGHT
bcs.shape = cap
bcs.position = Vector3(0, CAP_Y, 0)
_body.add_child(bcs)
root.add_child(_body)
_body.position = Vector3(0.0, _alt() + 1.0, 0.0)
_phase = 0
_frame = 0
_floor_frames = 0
_min_step = 1e30
_stall_frame = -1
func _physics_process(_d: float) -> bool:
_warm += 1
if _warm < 3:
return false
# Nodes added in _initialize are not inside the tree yet, so _ready has not
# run and every @onready ref is null. Layer B needs a live tree; so does the
# broadphase Layer A queries.
if _warm == 3:
_layer_b()
# Layer B is the controller's arithmetic and needs no physics, no floor and
# no skeleton — so it is the one measurement here that is fast and that
# still works. Let it be run on its own.
for a in OS.get_cmdline_user_args():
if a == "--layer-b-only":
return true
_build()
return false
if _body == null:
return true
if _phase == 0:
_body.velocity = Vector3(0.0, -4.0, 0.0)
_body.move_and_slide()
_frame += 1
if _frame >= SETTLE_FRAMES:
_phase = 1
_frame = 0
_start = _body.global_position
return false
# Drive uphill (+X) at a constant speed, with a little downward bias so the
# body stays seated -- the same shape as the controller's own gravity.
var before: float = _body.global_position.x
_body.velocity = Vector3(DRIVE_SPEED, -1.0, 0.0)
_body.move_and_slide()
var step: float = _body.global_position.x - before
if step < _min_step:
_min_step = step
if step < 0.001 and _stall_frame < 0:
_stall_frame = _frame
if _body.is_on_floor():
_floor_frames += 1
_frame += 1
if _frame < DRIVE_FRAMES:
return false
var moved: Vector3 = _body.global_position - _start
var horiz: float = absf(moved.x)
var expected: float = DRIVE_SPEED * float(DRIVE_FRAMES) * _tick
_rows.push_back({
"g": "%5.0f m %.3f" % [_grounds[_gidx], _margins[_midx]], "a": _angles[_idx], "horiz": horiz, "rise": moved.y,
"pct": 100.0 * horiz / expected,
"floor": 100.0 * float(_floor_frames) / float(DRIVE_FRAMES),
"minstep": _min_step, "stall": _stall_frame,
})
_body.queue_free()
_ground.queue_free()
_body = null
_ground = null
_idx += 1
if _idx >= _angles.size():
_idx = 0
_gidx += 1
if _gidx >= _grounds.size():
_gidx = 0
_midx += 1
if _midx >= _margins.size():
_report()
return true
_build()
return false
func _report() -> void:
print("LAYER A -- bare CharacterBody3D, constant %.1f m/s uphill, at %.0f m altitude" % [DRIVE_SPEED, SPAWN_ALTITUDE])
print("(floor_max_angle is %.1f deg -- above that the surface is a WALL, not a floor)" % rad_to_deg(FLOOR_MAX_ANGLE))
print("")
print("alt margin slope horizontal of expected rise on_floor min step first stall")
print("---------------------------------------------------------------------------------------------")
for r in _rows:
var st := "none"
if r["stall"] >= 0:
st = "frame %d" % r["stall"]
print("%-14s %5.0f deg %8.3f m %6.1f%% %+7.3f m %5.1f%% %+8.4f %s" % [
r["g"], r["a"], r["horiz"], r["pct"], r["rise"], r["floor"],
r["minstep"], st])
print("")