using Godot; using System.Collections.Generic; // Regression for the crag (no player), checked on the SAVED scenes/maps/crag.tscn (route = its // Route markers; rebuild with tools/rebuild_maps.tscn): the route is climbable on paper — consecutive route points // within human reach, every point in free space, route pieces not buried in each other or the // rock — the summit is high enough, and it's in plain view from spawn. Prints "CRAG: overall PASS". public partial class CragSpike : Node3D { public const float MaxHoldToHold = 1.0f; // m between consecutive holds (two hands span the chimney) public const float MaxHoldGain = 0.7f; // m of height gained per hand move public const float MaxReachAboveFeet = 2.3f; // standing overhead grip height, 1.75 m person public const float MaxReachOut = 1.0f; public const float MaxMantle = 1.3f; // m a stand-to-stand step may rise (mantle / jump) public const float MaxGap = 1.0f; // m horizontal between stand points across a rise or a gap private Crag _crag; private int _frame; private readonly List _fails = new(); public override void _Ready() { StaticBody3D floor = new(); floor.AddToGroup(PropGrabber.NoGrabGroup); floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(80, 0.1f, 80) }, Position = new Vector3(0, -0.05f, 0) }); AddChild(floor); // The SAVED scenes (as the game loads them, hand edits included), not a fresh rebuild. AddChild(GD.Load("res://scenes/maps/playground.tscn").Instantiate()); _crag = GD.Load("res://scenes/maps/crag.tscn").Instantiate(); AddChild(_crag); } public override void _PhysicsProcess(double delta) { if (++_frame < 3) return; PhysicsDirectSpaceState3D space = GetWorld3D().DirectSpaceState; CheckReach(); CheckFreeSpace(space); CheckWalks(space); CheckOverlaps(); Check(Crag.SummitHeight >= 25f, $"summit {Crag.SummitHeight} m >= 25 m"); CheckView(space); foreach (string f in _fails) GD.Print("CRAG: FAIL " + f); GD.Print($"CRAG: {_crag.Route.Count} route points, {_crag.RoutePieces.Count} route pieces, {_crag.MassPieces.Count} rock pieces, {_crag.GetChildCount()} nodes"); GD.Print(_fails.Count == 0 ? "CRAG: overall PASS" : $"CRAG: overall FAIL ({_fails.Count})"); GetTree().Quit(); } private void Check(bool ok, string what) { if (!ok) _fails.Add(what); } private static float Flat(Vector3 a, Vector3 b) => new Vector2(a.X - b.X, a.Z - b.Z).Length(); private void CheckReach() { var r = _crag.Route; for (int i = 1; i < r.Count; i++) { var (ka, a, na) = r[i - 1]; var (kb, b, nb) = r[i]; string pair = $"'{na}' -> '{nb}'"; if (ka == Crag.Step.Hold && kb == Crag.Step.Hold) { Check(a.DistanceTo(b) <= MaxHoldToHold, $"{pair}: holds {a.DistanceTo(b):0.00} m apart"); Check(b.Y - a.Y <= MaxHoldGain, $"{pair}: hand move gains {b.Y - a.Y:0.00} m"); } else if (ka == Crag.Step.Stand && kb == Crag.Step.Hold) { Check(b.Y - a.Y <= MaxReachAboveFeet && b.Y - a.Y >= -0.3f, $"{pair}: hold {b.Y - a.Y:0.00} m above feet"); Check(Flat(a, b) <= MaxReachOut, $"{pair}: hold {Flat(a, b):0.00} m out"); } else if (ka == Crag.Step.Hold && kb == Crag.Step.Stand) { Check(b.Y <= a.Y + 0.3f, $"{pair}: stand {b.Y - a.Y:0.00} m above the last hold"); Check(Flat(a, b) <= MaxReachOut, $"{pair}: stand {Flat(a, b):0.00} m from the last hold"); } else { float rise = b.Y - a.Y; Check(rise <= MaxMantle, $"{pair}: rise {rise:0.00} m"); if (Mathf.Abs(rise) > 0.05f) Check(Flat(a, b) <= MaxGap, $"{pair}: {Flat(a, b):0.00} m across a {rise:0.00} m step"); } } } // Every route point is in the open; standing points have a body's worth of room above them // and ground right under them. private void CheckFreeSpace(PhysicsDirectSpaceState3D space) { foreach (var (kind, p, name) in _crag.Route) { var pq = new PhysicsPointQueryParameters3D { Position = p + (kind == Crag.Step.Stand ? Vector3.Up * 0.05f : Vector3.Zero) }; Check(space.IntersectPoint(pq, 1).Count == 0, $"'{name}' point is inside geometry"); if (kind != Crag.Step.Stand) continue; var box = new PhysicsShapeQueryParameters3D { Shape = new BoxShape3D { Size = new Vector3(0.3f, 1.2f, 0.3f) }, Transform = new Transform3D(Basis.Identity, p + Vector3.Up * 0.7f), }; Check(space.IntersectShape(box, 1).Count == 0, $"'{name}' has no room to stand"); var ray = PhysicsRayQueryParameters3D.Create(p + Vector3.Up * 0.05f, p + Vector3.Down * 0.1f); Check(space.IntersectRay(ray).Count > 0, $"'{name}' has nothing underfoot"); } } // Level stand-to-stand moves are walks: there is ground all along the way. private void CheckWalks(PhysicsDirectSpaceState3D space) { var r = _crag.Route; for (int i = 1; i < r.Count; i++) { if (r[i - 1].Kind != Crag.Step.Stand || r[i].Kind != Crag.Step.Stand || Mathf.Abs(r[i].Point.Y - r[i - 1].Point.Y) > 0.05f) continue; for (float t = 0.1f; t < 1f; t += 0.1f) { Vector3 p = r[i - 1].Point.Lerp(r[i].Point, t); var ray = PhysicsRayQueryParameters3D.Create(p + Vector3.Up * 0.05f, p + Vector3.Down * 0.1f); if (space.IntersectRay(ray).Count == 0) { _fails.Add($"walk '{r[i - 1].Name}' -> '{r[i].Name}': no ground at {p}"); break; } } } } // No route piece is buried in another piece or in the rock (touching is fine). private void CheckOverlaps() { var route = _crag.RoutePieces; for (int i = 0; i < route.Count; i++) { for (int j = i + 1; j < route.Count; j++) CheckPair(route[i], route[j]); foreach (var m in _crag.MassPieces) CheckPair(route[i], m); } } private void CheckPair((CollisionObject3D Body, Aabb Box) a, (CollisionObject3D Body, Aabb Box) b) { if (!a.Box.Intersects(b.Box)) return; Aabb i = a.Box.Intersection(b.Box); float depth = Mathf.Min(i.Size.X, Mathf.Min(i.Size.Y, i.Size.Z)); if (depth > 0.01f) _fails.Add($"{a.Body.Name} overlaps {b.Body.Name} by {depth:0.000} m"); } // From the spawn point's eye height, the summit's front edge, both flags and the foot of the route are in view. private void CheckView(PhysicsDirectSpaceState3D space) { Vector3 eye = new(0f, 1.7f, 2.5f); foreach (Vector3 target in new[] { new Vector3(Crag.SummitPoint.X, Crag.SummitHeight - 0.1f, -15.95f), new Vector3(-5.6f, 1f, -9.6f), new Vector3(-2.7f, 6f, -12.8f), new Vector3(-5.3f, 10.6f, -13.7f), new Vector3(-4.3f, 18.6f, -15.2f) }) { var hit = space.IntersectRay(PhysicsRayQueryParameters3D.Create(eye, target)); bool clear = hit.Count == 0 || ((Vector3)hit["position"]).DistanceTo(target) < 1f; Check(clear, $"no line of sight from spawn to {target}" + (hit.Count > 0 ? $" (blocked at {(Vector3)hit["position"]})" : "")); } } }