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>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 20:10:10 +03:00
1 parent 3e300c7f04
commit df7a2c5a89
17 files changed
+6863 -65

No files matched your search

+89 -26
View File
@@ -12,11 +12,20 @@ using System.Collections.Generic;
// 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.
// 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();
@@ -37,25 +46,76 @@ public partial class Crag : Node3D
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();
}
public void Reset()
// Route points and piece bounds from the (saved) nodes.
public void ReadLayout()
{
foreach (var (body, start) in _dynamic)
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 (!IsInstanceValid(body))
if (child is not CollisionObject3D body)
continue;
body.GlobalTransform = start;
body.LinearVelocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
body.ResetPhysicsInterpolation();
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()
@@ -241,8 +301,9 @@ public partial class Crag : Node3D
// 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);
AddChild(marker, true);
}
private void BuildSummitProps()
@@ -258,45 +319,47 @@ public partial class Crag : Node3D
// ---------------------------------------------------------------- builders
private void AddStep(Step kind, Vector3 point, string name) => Route.Add((kind, point, name));
// 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);
MassPieces.Add((b, BoxAabb(size, b.Transform)));
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);
RoutePieces.Add((b, BoxAabb(size, b.Transform)));
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);
// 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)));
b.AddToGroup(RouteGroup, true);
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.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);
AddChild(body, true);
return body;
}
@@ -309,7 +372,7 @@ public partial class Crag : Node3D
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);
AddChild(body, true);
return body;
}
@@ -325,11 +388,10 @@ public partial class Crag : Node3D
MaxContactsReported = 4,
PhysicsMaterialOverride = new PhysicsMaterial { Friction = 0.8f, Bounce = 0.05f },
};
body.AddToGroup("prop");
body.AddToGroup(MapScene.PropGroup, true);
body.AddChild(new CollisionShape3D { Shape = shape });
body.AddChild(Mesh(mesh, color));
AddChild(body);
_dynamic.Add((body, body.GlobalTransform));
AddChild(body, true);
return body;
}
@@ -347,12 +409,13 @@ public partial class Crag : Node3D
{
AddChild(new Label3D
{
Name = "Sign",
Text = text,
FontSize = 96,
PixelSize = 0.002f * scale,
OutlineSize = 12,
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
Position = at,
});
}, true);
}
}