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("res://scenes/teleport_marker.tscn").Instantiate(); 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, }); } }