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:
1 parent
3e300c7f04
commit
df7a2c5a89
17 files changed
+6863
-65
No files matched your search
+89
-26
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user