scenes/maps/playground.tscn and crag.tscn are generated by the map scripts' Rebuild (inspector button, or godot-mono --headless --path . tools/rebuild_maps.tscn) and loaded as saved by the game, so hand edits stick. The crag route is saved as Marker3D nodes and crag_spike checks the saved scene. Layout verified identical to the code-built one (tools/map_dump.tscn). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
422 lines
22 KiB
C#
422 lines
22 KiB
C#
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. EDITOR-BUILT: this is the root of scenes/maps/crag.tscn
|
||
// and Rebuild() generates everything into that scene (MapScene: inspector button, or
|
||
// tools/rebuild_maps.tscn headless); the game loads the saved scene. The route is saved too: every
|
||
// route point is a Marker3D under "Route" (metadata kind/label, in climbing order), route bodies are in
|
||
// group "crag_route" and rock in "crag_mass" — Route/RoutePieces/MassPieces are read back from the
|
||
// saved nodes on _Ready, so crag_spike checks the scene as saved (and as hand-edited).
|
||
[Tool]
|
||
public partial class Crag : Node3D
|
||
{
|
||
[ExportToolButton("Rebuild (replaces all children)")]
|
||
public Callable RebuildButton => Callable.From(RebuildInEditor);
|
||
|
||
public enum Step { Stand, Hold }
|
||
public const string RouteNode = "Route", RouteGroup = "crag_route", MassGroup = "crag_mass";
|
||
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()
|
||
{
|
||
if (Engine.IsEditorHint())
|
||
return;
|
||
MapScene.CollectProps(this, _dynamic);
|
||
ReadLayout();
|
||
}
|
||
|
||
public void Reset() => MapScene.Reset(_dynamic);
|
||
|
||
private void RebuildInEditor()
|
||
{
|
||
if (!MapScene.CanRebuildInEditor(this))
|
||
return;
|
||
Rebuild();
|
||
MapScene.MarkUnsaved();
|
||
}
|
||
|
||
// Replaces every child with the generated layout, owned by this map (saved with its scene).
|
||
public void Rebuild()
|
||
{
|
||
MapScene.Clear(this);
|
||
_rng.Seed = 26; // same rubble/talus every rebuild
|
||
_route = new Node3D { Name = RouteNode };
|
||
AddChild(_route);
|
||
BuildMass();
|
||
BuildRoute();
|
||
BuildBase();
|
||
BuildSummitProps();
|
||
MapScene.Adopt(this);
|
||
ReadLayout();
|
||
}
|
||
|
||
// Route points and piece bounds from the (saved) nodes.
|
||
public void ReadLayout()
|
||
{
|
||
Route.Clear();
|
||
RoutePieces.Clear();
|
||
MassPieces.Clear();
|
||
if (GetNodeOrNull<Node>(RouteNode) is Node route)
|
||
foreach (Node child in route.GetChildren())
|
||
if (child is Marker3D m)
|
||
Route.Add(((Step)(int)m.GetMeta("kind"), m.GlobalPosition, (string)m.GetMeta("label")));
|
||
foreach (Node child in GetChildren())
|
||
{
|
||
if (child is not CollisionObject3D body)
|
||
continue;
|
||
if (body.IsInGroup(RouteGroup))
|
||
RoutePieces.Add((body, Bounds(body)));
|
||
else if (body.IsInGroup(MassGroup))
|
||
MassPieces.Add((body, Bounds(body)));
|
||
}
|
||
}
|
||
|
||
// World bounds of a box or a cylinder piece (a cylinder's ends are flat: radius across its axis only).
|
||
private static Aabb Bounds(CollisionObject3D body)
|
||
{
|
||
CollisionShape3D cs = body.GetChildOrNull<CollisionShape3D>(0);
|
||
Transform3D t = body.GlobalTransform * cs.Transform;
|
||
if (cs.Shape is CylinderShape3D cyl)
|
||
{
|
||
Vector3 from = t * new Vector3(0f, -cyl.Height * 0.5f, 0f), to = t * new Vector3(0f, cyl.Height * 0.5f, 0f);
|
||
Vector3 d = (to - from).Normalized(), r = new(
|
||
cyl.Radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.X * d.X)), cyl.Radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Y * d.Y)), cyl.Radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Z * d.Z)));
|
||
return new Aabb(from - r, Vector3.Zero).Expand(from + r).Expand(to - r).Expand(to + r);
|
||
}
|
||
Vector3 size = ((BoxShape3D)cs.Shape).Size;
|
||
return t * new Aabb(-size * 0.5f, size);
|
||
}
|
||
|
||
private Node3D _route;
|
||
|
||
// ---------------------------------------------------------------- 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.Name = "CragMarker";
|
||
marker.Position = new Vector3(1.5f, 0f, -9.5f);
|
||
AddChild(marker, true);
|
||
}
|
||
|
||
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
|
||
|
||
// A route point: an editor-visible Marker3D under "Route", in climbing order.
|
||
private void AddStep(Step kind, Vector3 point, string name)
|
||
{
|
||
Marker3D m = new() { Name = kind == Step.Hold ? "Hold" : "Stand", Position = point };
|
||
m.SetMeta("kind", (int)kind);
|
||
m.SetMeta("label", name);
|
||
_route.AddChild(m, true);
|
||
}
|
||
|
||
private StaticBody3D Mass(string name, Vector3 size, Vector3 center, Color color, Basis? basis = null)
|
||
{
|
||
StaticBody3D b = Block(name, size, center, color, true, basis);
|
||
b.AddToGroup(MassGroup, true);
|
||
return b;
|
||
}
|
||
|
||
private StaticBody3D RouteBlock(string name, Vector3 size, Vector3 center, Color color, bool castShadow = true)
|
||
{
|
||
StaticBody3D b = Block(name, size, center, color, castShadow);
|
||
b.AddToGroup(RouteGroup, true);
|
||
return b;
|
||
}
|
||
|
||
private StaticBody3D RouteBar(string name, Vector3 from, Vector3 to, float radius, Color color)
|
||
{
|
||
StaticBody3D b = Bar(name, from, to, radius, color);
|
||
b.AddToGroup(RouteGroup, true);
|
||
return b;
|
||
}
|
||
|
||
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, true);
|
||
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, true);
|
||
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, true);
|
||
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(MapScene.PropGroup, true);
|
||
body.AddChild(new CollisionShape3D { Shape = shape });
|
||
body.AddChild(Mesh(mesh, color));
|
||
AddChild(body, true);
|
||
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
|
||
{
|
||
Name = "Sign",
|
||
Text = text,
|
||
FontSize = 96,
|
||
PixelSize = 0.002f * scale,
|
||
OutlineSize = 12,
|
||
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
|
||
Position = at,
|
||
}, true);
|
||
}
|
||
}
|