Files
VR-Mess/scripts/Crag.cs
T
FoxyzandClaude Opus 5.5 df7a2c5a89 Maps are editor-built scenes: [Tool] Playground/Crag, headless rebuild for agents
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>
2026-10-02 20:10:10 +03:00

422 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}