The crag: a 26 m box-rock cliff with a graded climbing route
North of the playground, facing spawn: bouldering blocks, a ladder to an 8 m rest ledge, a jug wall, a 0.4 m traverse ledge, an overhang with monkey bars and a lip bar onto a 16 m rest ledge, a chimney to the 26 m summit with throwable props. A 'Crag' teleport marker at the base is the way down; teleports now land the floor on the marker's height. Floor widened to 80 m. Regression: scenes/crag_spike.tscn (reach, free space, overlaps, height, line of sight). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
84df772f38
commit
ca9a299fe8
9 files changed
+554
-8
No files matched your search
+358
@@ -0,0 +1,358 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// The crag: a 26 m cliff of stacked rock boxes north of the playground, one route up, getting
|
||||
// harder as it goes (built in code like Playground, same layers and grab rules):
|
||||
// 1. bouldering blocks (1.2 / 2.1 / 3.0 m) to mantle and hop between;
|
||||
// 2. a ladder up the first tier's face to REST LEDGE 1 (8 m, flag);
|
||||
// 3. a jug wall (two staggered columns of holds) up the second tier to 13.5 m;
|
||||
// 4. a narrow 0.4 m ledge traversed sideways along a handrail of holds, open air behind;
|
||||
// 5. an overhang: monkey bars under a roof, a jug on its lip and a lip bar to pull over onto
|
||||
// REST LEDGE 2 (16 m, flag);
|
||||
// 6. a 0.9 m chimney with holds on both walls up the third tier;
|
||||
// 7. the summit (26 m) with throwable props. Way down: aim at the "Crag" teleport marker at the
|
||||
// base (or jump — no fall damage).
|
||||
// The cliff faces south (+Z) toward spawn. Every route point is recorded in Route so crag_spike
|
||||
// can check reach; route bodies in RoutePieces so it can check nothing intersects them.
|
||||
public partial class Crag : Node3D
|
||||
{
|
||||
public enum Step { Stand, Hold }
|
||||
public readonly List<(Step Kind, Vector3 Point, string Name)> Route = new();
|
||||
public readonly List<(CollisionObject3D Body, Aabb Box)> RoutePieces = new();
|
||||
public readonly List<(CollisionObject3D Body, Aabb Box)> MassPieces = new();
|
||||
public const float SummitHeight = 26f;
|
||||
public static readonly Vector3 SummitPoint = new(2.25f, SummitHeight, -18f);
|
||||
|
||||
private readonly List<(RigidBody3D Body, Transform3D Start)> _dynamic = new();
|
||||
private const uint WorldLayer = 1, WorldMask = 0b11101, PropMask = 0b11101;
|
||||
private readonly RandomNumberGenerator _rng = new() { Seed = 26 };
|
||||
|
||||
private static readonly Color[] Rock =
|
||||
{
|
||||
new(0.47f, 0.43f, 0.39f), new(0.54f, 0.5f, 0.45f), new(0.41f, 0.39f, 0.37f), new(0.58f, 0.53f, 0.47f),
|
||||
};
|
||||
private static readonly Color Hold = new(0.9f, 0.45f, 0.15f), Metal = new(0.45f, 0.47f, 0.52f),
|
||||
Wood = new(0.72f, 0.55f, 0.33f), Flag = new(0.85f, 0.15f, 0.15f), Bark = new(0.35f, 0.25f, 0.17f),
|
||||
Leaves = new(0.25f, 0.45f, 0.22f), Pillar = new(0.7f, 0.7f, 0.68f);
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
BuildMass();
|
||||
BuildRoute();
|
||||
BuildBase();
|
||||
BuildSummitProps();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
foreach (var (body, start) in _dynamic)
|
||||
{
|
||||
if (!IsInstanceValid(body))
|
||||
continue;
|
||||
body.GlobalTransform = start;
|
||||
body.LinearVelocity = Vector3.Zero;
|
||||
body.AngularVelocity = Vector3.Zero;
|
||||
body.ResetPhysicsInterpolation();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the rock
|
||||
|
||||
private void BuildMass()
|
||||
{
|
||||
// Three tiers, each set back 1.5 m: A 0–8 m, B 8–16 m, C 16–26 m (C split by the chimney).
|
||||
Mass("TierA", new Vector3(12f, 8f, 7f), new Vector3(0f, 4f, -16.5f), Rock[0]);
|
||||
Mass("TierB", new Vector3(10f, 8f, 5.5f), new Vector3(0f, 12f, -17.25f), Rock[1]);
|
||||
Mass("TierCWest", new Vector3(3.55f, 10f, 4f), new Vector3(-2.225f, 21f, -18f), Rock[2]);
|
||||
Mass("TierCEast", new Vector3(3.55f, 10f, 4f), new Vector3(2.225f, 21f, -18f), Rock[2]);
|
||||
Mass("TierCBack", new Vector3(0.9f, 10f, 3f), new Vector3(0f, 21f, -18.5f), Rock[3]);
|
||||
Mass("Spire", new Vector3(1.3f, 2.6f, 1.3f), new Vector3(3.1f, 27.3f, -19.1f), Rock[1], new Basis(Vector3.Up, 0.3f));
|
||||
|
||||
// Shoulders and buttresses break the silhouette (sides and back only — the south faces
|
||||
// carry the route).
|
||||
Mass("ShoulderWestA", new Vector3(2.4f, 5.5f, 5f), new Vector3(-7f, 2.75f, -16.8f), Rock[2], new Basis(Vector3.Up, 0.12f));
|
||||
Mass("ShoulderEastA", new Vector3(2.8f, 6.8f, 5.5f), new Vector3(7.1f, 3.4f, -16.2f), Rock[3], new Basis(Vector3.Up, -0.15f));
|
||||
Mass("ShoulderEastB", new Vector3(1.8f, 5.5f, 4f), new Vector3(5.6f, 10.75f, -17.8f), Rock[0], new Basis(Vector3.Up, 0.1f));
|
||||
Mass("ShoulderWestB", new Vector3(1.6f, 3.6f, 3.5f), new Vector3(-5.5f, 9.8f, -18f), Rock[3], new Basis(Vector3.Up, -0.2f));
|
||||
Mass("ShoulderWestC", new Vector3(1.4f, 6f, 3f), new Vector3(-4.3f, 19f, -18.6f), Rock[0], new Basis(Vector3.Up, 0.18f));
|
||||
Mass("ShoulderEastC", new Vector3(1.5f, 7.5f, 3.2f), new Vector3(4.4f, 19.75f, -18.4f), Rock[3], new Basis(Vector3.Up, -0.12f));
|
||||
Mass("BackA", new Vector3(9f, 6f, 1.8f), new Vector3(0.5f, 3f, -20.6f), Rock[1], new Basis(Vector3.Up, 0.06f));
|
||||
Mass("BackB", new Vector3(6f, 5f, 1.4f), new Vector3(-0.8f, 13.5f, -20.4f), Rock[2], new Basis(Vector3.Up, -0.08f));
|
||||
|
||||
// Broken corners: big turned chunks knocking the tier edges out of square.
|
||||
(Vector3 At, Vector3 Size, Vector3 Turn)[] corners =
|
||||
{
|
||||
(new(6.3f, 5.6f, -13.4f), new(2.2f, 4.4f, 2.4f), new(0.08f, 0.35f, -0.1f)),
|
||||
(new(-6.6f, 5.5f, -13.6f), new(2f, 4.6f, 2.2f), new(-0.06f, -0.4f, 0.08f)),
|
||||
(new(6.4f, 12.4f, -15.4f), new(1.6f, 4f, 2f), new(0.1f, 0.3f, 0.12f)),
|
||||
(new(-5.5f, 13.6f, -15.2f), new(1.8f, 3.6f, 2f), new(-0.08f, -0.35f, -0.1f)),
|
||||
(new(4.3f, 23f, -16.4f), new(1.6f, 4.4f, 1.8f), new(0.05f, 0.4f, 0.1f)),
|
||||
(new(-4.4f, 22.2f, -16.6f), new(1.6f, 4.8f, 1.8f), new(-0.05f, -0.3f, -0.08f)),
|
||||
(new(8.2f, 2.4f, -17.8f), new(2.6f, 4.8f, 2f), new(0f, 0.2f, -0.35f)), // slab leaning on the east side
|
||||
};
|
||||
for (int i = 0; i < corners.Length; i++)
|
||||
Mass($"Corner{i}", corners[i].Size, corners[i].At, Rock[(i + 1) % Rock.Length], Basis.FromEuler(corners[i].Turn));
|
||||
|
||||
// Talus: fallen blocks around the foot (clear of the route's boulders and the marker).
|
||||
(Vector3 At, float Size)[] talus =
|
||||
{
|
||||
(new(9.5f, 0.9f, -13.5f), 1.8f), (new(11f, 0.6f, -16.2f), 1.2f), (new(-9.6f, 1.1f, -14.2f), 2.2f),
|
||||
(new(-10.5f, 0.6f, -19.5f), 1.2f), (new(6.5f, 0.7f, -11.2f), 1.4f), (new(-8.3f, 0.45f, -11.8f), 0.9f),
|
||||
(new(4f, 1.2f, -23f), 2.4f), (new(-5f, 0.8f, -23.4f), 1.6f),
|
||||
};
|
||||
for (int i = 0; i < talus.Length; i++)
|
||||
{
|
||||
float s = talus[i].Size;
|
||||
Vector3 size = new(s * _rng.RandfRange(0.9f, 1.4f), s, s * _rng.RandfRange(0.8f, 1.2f));
|
||||
Mass($"Talus{i}", size, talus[i].At, Rock[i % Rock.Length], Basis.FromEuler(new Vector3(_rng.RandfRange(-0.15f, 0.15f), _rng.RandfRange(-1f, 1f), _rng.RandfRange(-0.15f, 0.15f))));
|
||||
}
|
||||
|
||||
// Rubble on the walls away from the route: protruding, slightly turned blocks.
|
||||
Vector3[] spots =
|
||||
{
|
||||
new(3.5f, 1.2f, -12.8f), new(5f, 3.5f, -12.8f), new(1.6f, 5.6f, -12.8f), new(4.2f, 6.8f, -12.8f),
|
||||
new(-0.2f, 2.4f, -12.8f), new(0.4f, 10.2f, -14.3f), new(-1.2f, 11.6f, -14.3f), new(-4.5f, 11.0f, -14.3f),
|
||||
new(-6.1f, 4f, -14.2f), new(6.2f, 7.5f, -14.5f), new(-3.6f, 20.5f, -15.8f), new(3.2f, 22.8f, -15.8f),
|
||||
new(-2.4f, 24.6f, -15.8f), new(1.8f, 18.4f, -15.8f),
|
||||
};
|
||||
for (int i = 0; i < spots.Length; i++)
|
||||
{
|
||||
Vector3 size = new(_rng.RandfRange(0.8f, 1.8f), _rng.RandfRange(0.6f, 1.4f), 0.6f);
|
||||
Basis turn = Basis.FromEuler(new Vector3(_rng.RandfRange(-0.12f, 0.12f), _rng.RandfRange(-0.25f, 0.25f), _rng.RandfRange(-0.15f, 0.15f)));
|
||||
Mass($"Rubble{i}", size, spots[i], Rock[i % Rock.Length].Darkened(_rng.RandfRange(0f, 0.08f)), turn);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the route
|
||||
|
||||
private void BuildRoute()
|
||||
{
|
||||
// 1. Bouldering: three blocks rising 1.2 / 0.9 / 0.9 m with small gaps between.
|
||||
Vector3 start = new(-5.6f, 0f, -8.2f);
|
||||
AddStep(Step.Stand, start, "start");
|
||||
RouteBlock("Boulder1", new Vector3(1.4f, 1.2f, 1.4f), new Vector3(-5.6f, 0.6f, -9.6f), Rock[3]);
|
||||
AddStep(Step.Stand, new Vector3(-5.6f, 1.2f, -9.1f), "boulder 1 (mantle 1.2 m)");
|
||||
AddStep(Step.Stand, new Vector3(-5.0f, 1.2f, -10.1f), "boulder 1 far edge");
|
||||
RouteBlock("Boulder2", new Vector3(1.4f, 2.1f, 1.4f), new Vector3(-4.6f, 1.05f, -11.2f), Rock[0]);
|
||||
AddStep(Step.Stand, new Vector3(-4.8f, 2.1f, -10.7f), "boulder 2 (mantle 0.9 m)");
|
||||
AddStep(Step.Stand, new Vector3(-4.1f, 2.1f, -11.4f), "boulder 2 far edge");
|
||||
RouteBlock("Boulder3", new Vector3(1.5f, 3f, 1.6f), new Vector3(-2.7f, 1.5f, -12.2f), Rock[2]);
|
||||
AddStep(Step.Stand, new Vector3(-3.3f, 3f, -11.8f), "boulder 3 (hop 0.45 m gap, mantle 0.9 m)");
|
||||
AddStep(Step.Stand, new Vector3(-2.7f, 3f, -12.5f), "foot of the ladder");
|
||||
|
||||
// 2. Ladder up tier A's face (z = -13) to rest ledge 1; rails run past the lip to pull over.
|
||||
const float ladderX = -2.7f, ladderZ = -12.85f, half = 0.3f, rail = 0.03f, rung = 0.022f;
|
||||
foreach (float sx in new[] { -half, half })
|
||||
{
|
||||
RouteBar("LadderRail", new Vector3(ladderX + sx, 3f, ladderZ), new Vector3(ladderX + sx, 9f, ladderZ), rail, Metal);
|
||||
for (float y = 4f; y < 9f; y += 2f)
|
||||
RouteBlock("LadderStandoff", new Vector3(0.04f, 0.04f, 0.12f), new Vector3(ladderX + sx, y, -12.94f), Metal);
|
||||
}
|
||||
for (float y = 3.3f; y < 8.85f; y += 0.3f)
|
||||
{
|
||||
RouteBar("Rung", new Vector3(ladderX - half + rail, y, ladderZ), new Vector3(ladderX + half - rail, y, ladderZ), rung, Metal);
|
||||
AddStep(Step.Hold, new Vector3(ladderX, y + rung + 0.02f, ladderZ), $"rung {y:0.0} m");
|
||||
}
|
||||
|
||||
// Rest ledge 1: tier A's top strip in front of tier B (1.5 m deep, 12 m wide).
|
||||
AddStep(Step.Stand, new Vector3(ladderX, 8f, -13.4f), "REST LEDGE 1 (8 m)");
|
||||
FlagPole("8 m", new Vector3(-5.3f, 8f, -13.7f));
|
||||
AddStep(Step.Stand, new Vector3(-3f, 8f, -14.1f), "foot of the jug wall");
|
||||
|
||||
// 3. Jug wall on tier B's face (z = -14.5): two staggered columns, 0.45 m apart vertically.
|
||||
int n = 0;
|
||||
for (float y = 9f; y < 13.2f; y += 0.45f, n++)
|
||||
{
|
||||
float x = n % 2 == 0 ? -3.25f : -2.75f;
|
||||
Jug(new Vector3(x, y, -14.5f), Vector3.Back);
|
||||
}
|
||||
// 4. Traverse ledge at 13.5 m: 0.4 m deep; its lip is the last jug.
|
||||
RouteBlock("TraverseLedge", new Vector3(8.2f, 0.12f, 0.4f), new Vector3(0.5f, 13.44f, -14.3f), Rock[3]);
|
||||
AddStep(Step.Hold, new Vector3(-3f, 13.52f, -14.08f), "traverse ledge lip");
|
||||
AddStep(Step.Stand, new Vector3(-3f, 13.5f, -14.3f), "traverse ledge (0.4 m wide, 13.5 m)");
|
||||
for (float x = -2.4f; x <= 3.05f; x += 0.6f)
|
||||
Jug(new Vector3(x, 14.9f, -14.5f), Vector3.Back);
|
||||
AddStep(Step.Stand, new Vector3(3f, 13.5f, -14.3f), "under the overhang");
|
||||
|
||||
// 5. Overhang: a roof slab 1.5 m out from tier B, monkey bars under it, a jug on its front
|
||||
// face and a bar along its top edge to pull over onto rest ledge 2.
|
||||
RouteBlock("Overhang", new Vector3(3f, 0.6f, 1.5f), new Vector3(3f, 15.7f, -13.75f), Rock[0]);
|
||||
const float barY = 15.2f, barR = 0.025f;
|
||||
foreach (float z in new[] { -14.2f, -13.7f, -13.2f })
|
||||
{
|
||||
RouteBar("RoofBar", new Vector3(1.7f, barY, z), new Vector3(4.3f, barY, z), barR, Metal);
|
||||
foreach (float x in new[] { 1.8f, 4.2f })
|
||||
RouteBar("RoofHanger", new Vector3(x, barY + barR, z), new Vector3(x, 15.4f, z), 0.015f, Metal);
|
||||
AddStep(Step.Hold, new Vector3(3f, barY - barR - 0.02f, z), $"roof bar z {z:0.0}");
|
||||
}
|
||||
Jug(new Vector3(3f, 15.65f, -13f), Vector3.Back);
|
||||
RouteBar("LipBar", new Vector3(1.7f, 16.03f, -13.05f), new Vector3(4.3f, 16.03f, -13.05f), 0.03f, Metal);
|
||||
AddStep(Step.Hold, new Vector3(3f, 16.08f, -13.05f), "lip bar");
|
||||
AddStep(Step.Stand, new Vector3(3f, 16f, -13.6f), "REST LEDGE 2 (16 m)");
|
||||
FlagPole("16 m", new Vector3(-4.3f, 16f, -15.2f));
|
||||
AddStep(Step.Stand, new Vector3(0f, 16f, -15.6f), "mouth of the chimney");
|
||||
|
||||
// 6. Chimney: a 0.9 m slot between tier C's halves (x ±0.45, z -16 … -17), holds on both
|
||||
// walls staggered 0.5 m and the back wall in between, topping out at the summit.
|
||||
n = 0;
|
||||
for (float y = 16.7f; y < 25.9f; y += 0.25f, n++)
|
||||
{
|
||||
bool west = n % 2 == 0;
|
||||
Jug(new Vector3(west ? -0.45f : 0.45f, y, -16.5f), west ? Vector3.Right : Vector3.Left);
|
||||
}
|
||||
AddStep(Step.Hold, new Vector3(-0.47f, SummitHeight + 0.02f, -16.4f), "chimney exit lip");
|
||||
|
||||
// 7. Summit.
|
||||
AddStep(Step.Stand, new Vector3(-1f, SummitHeight, -16.6f), "SUMMIT (26 m)");
|
||||
Sign($"Summit {SummitHeight:0} m", new Vector3(-2.4f, SummitHeight + 1.6f, -18.5f), 2f);
|
||||
}
|
||||
|
||||
// A hold: a 0.3 m wide, 0.12 m proud box on the face at `at`, sticking out along `out`.
|
||||
private void Jug(Vector3 at, Vector3 outward)
|
||||
{
|
||||
Vector3 size = Mathf.Abs(outward.X) > 0.5f ? new Vector3(0.12f, 0.08f, 0.3f) : new Vector3(0.3f, 0.08f, 0.12f);
|
||||
Vector3 center = at + outward * 0.06f;
|
||||
RouteBlock("Jug", size, center, Hold, castShadow: false);
|
||||
AddStep(Step.Hold, center + Vector3.Up * 0.06f + outward * 0.02f, $"jug {at.Y:0.00} m");
|
||||
}
|
||||
|
||||
private void FlagPole(string text, Vector3 foot)
|
||||
{
|
||||
RouteBar("FlagPole", foot, foot + Vector3.Up * 2.4f, 0.03f, Metal);
|
||||
Block("Flag", new Vector3(0.02f, 0.45f, 0.7f), foot + new Vector3(0f, 2.15f, 0.38f), Flag, false);
|
||||
Sign(text, foot + Vector3.Up * 2.8f, 1.5f);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- base and summit
|
||||
|
||||
private void BuildBase()
|
||||
{
|
||||
// Trees and pillars at the foot: things of known size to read the height against.
|
||||
(Vector3 At, float Height)[] trees = { (new(-10f, 0f, -11.5f), 6f), (new(10.5f, 0f, -12f), 7f), (new(-12f, 0f, -17f), 5.5f), (new(12f, 0f, -18f), 6.5f) };
|
||||
foreach (var (at, h) in trees)
|
||||
{
|
||||
Bar("Trunk", at, at + Vector3.Up * h * 0.6f, 0.18f, Bark);
|
||||
Block("Canopy", new Vector3(2.2f, h * 0.5f, 2.2f), at + Vector3.Up * h * 0.75f, Leaves, true, new Basis(Vector3.Up, at.X * 0.3f), grab: false);
|
||||
}
|
||||
Bar("PillarWest", new Vector3(-8.5f, 0f, -10.5f), new Vector3(-8.5f, 9f, -10.5f), 0.35f, Pillar);
|
||||
Bar("PillarEast", new Vector3(8.5f, 0f, -10f), new Vector3(8.5f, 12f, -10f), 0.35f, Pillar);
|
||||
Sign("The Crag — 26 m", new Vector3(-5.6f, 2.6f, -7.6f), 1.5f);
|
||||
|
||||
// The way down: a teleport marker at the base (aim at it from the summit).
|
||||
Node3D marker = GD.Load<PackedScene>("res://scenes/teleport_marker.tscn").Instantiate<Node3D>();
|
||||
marker.Set("MarkerLabel", "Crag");
|
||||
marker.Position = new Vector3(1.5f, 0f, -9.5f);
|
||||
AddChild(marker);
|
||||
}
|
||||
|
||||
private void BuildSummitProps()
|
||||
{
|
||||
for (int i = 0; i < 3; i++)
|
||||
BoxProp($"SummitCrate{i}", Vector3.One * 0.4f, 4f, new Vector3(2f + i * 0.5f, SummitHeight + 0.2f, -17.2f), Wood.Darkened(0.05f * i));
|
||||
Prop("SummitBarrel", new CylinderShape3D { Radius = 0.28f, Height = 0.85f }, new CylinderMesh { TopRadius = 0.28f, BottomRadius = 0.28f, Height = 0.85f },
|
||||
12f, new Vector3(-2.8f, SummitHeight + 0.425f, -18.8f), Flag.Darkened(0.2f));
|
||||
Prop("SummitBall", new SphereShape3D { Radius = 0.15f }, new SphereMesh { Radius = 0.15f, Height = 0.3f }, 1.5f,
|
||||
new Vector3(-1.6f, SummitHeight + 0.15f, -18.4f), new Color(0.95f, 0.85f, 0.2f));
|
||||
BoxProp("SummitPlank", new Vector3(2f, 0.04f, 0.2f), 4f, new Vector3(2.4f, SummitHeight + 0.02f, -18.8f), Wood);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- builders
|
||||
|
||||
private void AddStep(Step kind, Vector3 point, string name) => Route.Add((kind, point, name));
|
||||
|
||||
private StaticBody3D Mass(string name, Vector3 size, Vector3 center, Color color, Basis? basis = null)
|
||||
{
|
||||
StaticBody3D b = Block(name, size, center, color, true, basis);
|
||||
MassPieces.Add((b, BoxAabb(size, b.Transform)));
|
||||
return b;
|
||||
}
|
||||
|
||||
private StaticBody3D RouteBlock(string name, Vector3 size, Vector3 center, Color color, bool castShadow = true)
|
||||
{
|
||||
StaticBody3D b = Block(name, size, center, color, castShadow);
|
||||
RoutePieces.Add((b, BoxAabb(size, b.Transform)));
|
||||
return b;
|
||||
}
|
||||
|
||||
private StaticBody3D RouteBar(string name, Vector3 from, Vector3 to, float radius, Color color)
|
||||
{
|
||||
StaticBody3D b = Bar(name, from, to, radius, color);
|
||||
// Bounds of the cylinder: radius across the axis only (the ends are flat).
|
||||
Vector3 d = (to - from).Normalized(), r = new Vector3(
|
||||
radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.X * d.X)), radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Y * d.Y)), radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Z * d.Z)));
|
||||
RoutePieces.Add((b, new Aabb(from - r, Vector3.Zero).Expand(from + r).Expand(to - r).Expand(to + r)));
|
||||
return b;
|
||||
}
|
||||
|
||||
private static Aabb BoxAabb(Vector3 size, Transform3D t) => t * new Aabb(-size * 0.5f, size);
|
||||
|
||||
private StaticBody3D Block(string name, Vector3 size, Vector3 center, Color color, bool castShadow, Basis? basis = null, bool grab = true)
|
||||
{
|
||||
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(basis ?? Basis.Identity, center) };
|
||||
if (!grab)
|
||||
body.AddToGroup(PropGrabber.NoGrabGroup);
|
||||
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
|
||||
MeshInstance3D mesh = Mesh(new BoxMesh { Size = size }, color);
|
||||
if (!castShadow)
|
||||
mesh.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
|
||||
body.AddChild(mesh);
|
||||
AddChild(body);
|
||||
return body;
|
||||
}
|
||||
|
||||
private StaticBody3D Bar(string name, Vector3 from, Vector3 to, float radius, Color color)
|
||||
{
|
||||
Vector3 axis = to - from;
|
||||
float len = axis.Length();
|
||||
Vector3 y = axis / len;
|
||||
Vector3 x = Mathf.Abs(y.Y) < 0.99f ? Vector3.Up.Cross(y).Normalized() : Vector3.Right;
|
||||
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(new Basis(x, y, x.Cross(y)), (from + to) * 0.5f) };
|
||||
body.AddChild(new CollisionShape3D { Shape = new CylinderShape3D { Radius = radius, Height = len } });
|
||||
body.AddChild(Mesh(new CylinderMesh { TopRadius = radius, BottomRadius = radius, Height = len, RadialSegments = 12 }, color));
|
||||
AddChild(body);
|
||||
return body;
|
||||
}
|
||||
|
||||
private RigidBody3D Prop(string name, Shape3D shape, Mesh mesh, float mass, Vector3 center, Color color)
|
||||
{
|
||||
RigidBody3D body = new()
|
||||
{
|
||||
Name = name,
|
||||
Mass = mass,
|
||||
Transform = new Transform3D(Basis.Identity, center),
|
||||
CollisionMask = PropMask,
|
||||
ContactMonitor = true,
|
||||
MaxContactsReported = 4,
|
||||
PhysicsMaterialOverride = new PhysicsMaterial { Friction = 0.8f, Bounce = 0.05f },
|
||||
};
|
||||
body.AddToGroup("prop");
|
||||
body.AddChild(new CollisionShape3D { Shape = shape });
|
||||
body.AddChild(Mesh(mesh, color));
|
||||
AddChild(body);
|
||||
_dynamic.Add((body, body.GlobalTransform));
|
||||
return body;
|
||||
}
|
||||
|
||||
private RigidBody3D BoxProp(string name, Vector3 size, float mass, Vector3 center, Color color) =>
|
||||
Prop(name, new BoxShape3D { Size = size }, new BoxMesh { Size = size }, mass, center, color);
|
||||
|
||||
private static MeshInstance3D Mesh(Mesh mesh, Color color)
|
||||
{
|
||||
MeshInstance3D m = new() { Mesh = mesh };
|
||||
m.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.9f });
|
||||
return m;
|
||||
}
|
||||
|
||||
private void Sign(string text, Vector3 at, float scale)
|
||||
{
|
||||
AddChild(new Label3D
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 96,
|
||||
PixelSize = 0.002f * scale,
|
||||
OutlineSize = 12,
|
||||
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
|
||||
Position = at,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqeckjtcuntlv
|
||||
@@ -0,0 +1,166 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// Regression for the crag (no player): 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<string> _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);
|
||||
AddChild(new Playground());
|
||||
_crag = new Crag();
|
||||
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"]})" : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bohh3so3ue5jk
|
||||
@@ -232,13 +232,16 @@ public partial class Locomotion : Node
|
||||
|
||||
private void MoveToSpawn() => PlacePlayer(_spawn.GlobalPosition, ForwardYaw(_spawn.GlobalTransform));
|
||||
|
||||
private void PlacePlayer(Vector3 targetXz, float yaw)
|
||||
// Lands the play-space floor on the target's height (head height above the floor kept), so a
|
||||
// marker at the foot of the crag brings you down from the summit instead of dropping you 26 m.
|
||||
private void PlacePlayer(Vector3 target, float yaw)
|
||||
{
|
||||
Transform3D h = _camera.GlobalTransform;
|
||||
Transform3D hLocal = _origin.GlobalTransform.AffineInverse() * h;
|
||||
float hmdYaw = h.Basis.GetEuler().Y;
|
||||
Basis basis = Basis.FromEuler(new Vector3(0, yaw - hmdYaw, 0)) * h.Basis;
|
||||
Transform3D desired = new Transform3D(basis, new Vector3(targetXz.X, h.Origin.Y, targetXz.Z));
|
||||
float headAboveFloor = h.Origin.Y - _origin.GlobalPosition.Y;
|
||||
Transform3D desired = new Transform3D(basis, new Vector3(target.X, target.Y + headAboveFloor, target.Z));
|
||||
_origin.GlobalTransform = desired * hLocal.AffineInverse();
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ public partial class WorldPanel : Node3D
|
||||
{
|
||||
_propShop.Respawn();
|
||||
GetNodeOrNull<Playground>("../../Playground")?.Reset();
|
||||
GetNodeOrNull<Crag>("../../Crag")?.Reset();
|
||||
_menu.Log("Props reset");
|
||||
};
|
||||
_menu.PingRequested += () =>
|
||||
|
||||
Reference in new issue
Block a user