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

+62
View File
@@ -0,0 +1,62 @@
using Godot;
using System.Collections.Generic;
using System.Linq;
// Dumps the map layout (type, transform, shape/mesh size, groups) sorted, for before/after comparison.
// godot-mono --headless --path . tools/map_dump.tscn -- out=<file>
public partial class MapDump : Node3D
{
private int _frame;
public override void _Ready()
{
foreach (string path in RebuildMaps.Maps)
AddChild(GD.Load<PackedScene>(path).Instantiate());
}
public override void _Process(double delta)
{
if (++_frame < 3)
return;
string outPath = "/tmp/claude-1000/maps/dump.txt";
foreach (string a in OS.GetCmdlineUserArgs())
if (a.StartsWith("out="))
outPath = a[4..];
var lines = new List<string>();
foreach (Node map in GetChildren())
Walk(map, map.GetType().Name, lines);
lines.Sort(System.StringComparer.Ordinal);
System.IO.File.WriteAllLines(outPath, lines);
GD.Print($"MAPDUMP: {lines.Count} nodes → {outPath}");
GetTree().Quit();
}
private static string R(Vector3 v) => $"({v.X:F3},{v.Y:F3},{v.Z:F3})";
private void Walk(Node n, string map, List<string> lines)
{
foreach (Node ch in n.GetChildren())
{
string info = ch.GetClass();
if (ch is Node3D n3)
{
Transform3D t = n3.GlobalTransform;
info += $" o{R(t.Origin)} x{R(t.Basis.X)} y{R(t.Basis.Y)}";
}
if (ch is CollisionShape3D cs && cs.Shape is BoxShape3D bs) info += $" box{R(bs.Size)}";
if (ch is CollisionShape3D cc && cc.Shape is CylinderShape3D cy) info += $" cyl{cy.Radius:F3}/{cy.Height:F3}";
if (ch is CollisionShape3D cp && cp.Shape is SphereShape3D sp) info += $" sph{sp.Radius:F3}";
if (ch is MeshInstance3D mi && mi.GetSurfaceOverrideMaterial(0) is StandardMaterial3D m) info += $" col{m.AlbedoColor.ToHtml()} sh{(int)mi.CastShadow}";
if (ch is Label3D l) info += $" text[{l.Text}] px{l.PixelSize:F4}";
if (ch is CollisionObject3D co) info += $" L{co.CollisionLayer}/{co.CollisionMask}";
if (ch is RigidBody3D rb) info += $" m{rb.Mass:F2} f{rb.PhysicsMaterialOverride?.Friction:F2}";
var groups = ch.GetGroups().Select(g => g.ToString()).Where(g => !g.StartsWith("_")).OrderBy(g => g);
info += " g[" + string.Join(",", groups) + "]";
if (ch is Joint3D j) info += $" joint {j.GetNodeOrNull(j.NodeA)?.GetClass()}@{R((j.GetNodeOrNull(j.NodeA) as Node3D)?.GlobalPosition ?? Vector3.Zero)}-{j.GetNodeOrNull(j.NodeB)?.GetClass()}@{R((j.GetNodeOrNull(j.NodeB) as Node3D)?.GlobalPosition ?? Vector3.Zero)}";
if (ch.GetType().Name == "TeleportMarker" || ch.SceneFilePath != "") info += $" scene[{ch.SceneFilePath}] label[{ch.Get("MarkerLabel")}]";
lines.Add($"{map}: {info}");
if (ch.SceneFilePath == "")
Walk(ch, map, lines);
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://bt87gcewn6ihe
+2 -2
View File
@@ -31,8 +31,8 @@ public partial class PlaygroundShots : Node3D
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(80, 0.1f, 80) }, Position = new Vector3(0, -0.05f, 0) });
floor.AddChild(new MeshInstance3D { Mesh = new PlaneMesh { Size = new Vector2(80, 80) } });
AddChild(floor);
AddChild(new Playground());
AddChild(new Crag());
AddChild(GD.Load<PackedScene>("res://scenes/maps/playground.tscn").Instantiate()); // the saved maps
AddChild(GD.Load<PackedScene>("res://scenes/maps/crag.tscn").Instantiate());
_cam = new Camera3D { Current = true };
AddChild(_cam);
SetView();
+68
View File
@@ -0,0 +1,68 @@
using Godot;
// Headless map rebuild for agents (same as pressing each map's "Rebuild" button in the editor and
// saving): loads each map scene, regenerates its children from code and saves it back.
// godot-mono --headless --path . tools/rebuild_maps.tscn [-- only=playground|crag]
// Prints "MAPS: rebuilt <path> (<n> nodes)" per map and "MAPS: done". Overwrites hand edits in those
// scenes — that's what Rebuild means.
public partial class RebuildMaps : Node
{
public static readonly string[] Maps = { "res://scenes/maps/playground.tscn", "res://scenes/maps/crag.tscn" };
public override void _Ready()
{
string only = null;
foreach (string a in OS.GetCmdlineUserArgs())
if (a.StartsWith("only="))
only = a[5..];
bool ok = true;
foreach (string path in Maps)
{
if (only != null && !path.Contains(only))
continue;
ok &= RebuildOne(path);
}
GD.Print(ok ? "MAPS: done" : "MAPS: FAILED");
GetTree().Quit(ok ? 0 : 1);
}
private bool RebuildOne(string path)
{
Node map = ResourceLoader.Load<PackedScene>(path, null, ResourceLoader.CacheMode.Ignore).Instantiate();
AddChild(map); // in the tree, so joint paths and globals resolve like in the editor
switch (map)
{
case Playground p:
p.Rebuild();
break;
case Crag c:
c.Rebuild();
break;
default:
GD.PushError($"MAPS: {path} root is not a map ({map.GetType().Name})");
return false;
}
RemoveChild(map);
PackedScene packed = new();
Error err = packed.Pack(map);
if (err == Error.Ok)
err = ResourceSaver.Save(packed, path);
int count = CountOwned(map, map);
map.Free();
if (err != Error.Ok)
{
GD.PushError($"MAPS: saving {path} failed: {err}");
return false;
}
GD.Print($"MAPS: rebuilt {path} ({count} nodes)");
return true;
}
private static int CountOwned(Node root, Node n)
{
int c = 0;
foreach (Node child in n.GetChildren())
c += (child.Owner == root ? 1 : 0) + CountOwned(root, child);
return c;
}
}
+1
View File
@@ -0,0 +1 @@
uid://cd3m3hyr27lpy
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tools/MapDump.cs" id="1"]
[node name="MapDump" type="Node3D"]
script = ExtResource("1")
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://tools/RebuildMaps.cs" id="1"]
[node name="RebuildMaps" type="Node"]
script = ExtResource("1")