Merge branch 'worktree-agent-a220f513d68e27a82'

This commit is contained in:
Foxyz committed 2026-10-02 20:14:11 +03:00
commit 537e72c207
17 files changed
+6863 -65

No files matched your search

+11 -1
View File
@@ -40,7 +40,7 @@ TERMINOLOGY (owner-corrected — use precisely):
`body_spike` "BODY: …", `collapse_spike` "COLLAPSE: …", `arm_spike` "ARM: overall PASS",
`sixdof_spike` "6DOF: overall PASS" (run FIRST with `weld_spike` whenever the addon binary changes),
`truebody_spike` "TRUEBODY: overall PASS" (no physics; plain `--headless`), `menu_spike` "MENU: overall PASS"
(in-VR menu input injection, no XR). `ragdoll_spike` "RAGDOLL: overall PASS" (`-- only=a,b` runs the phases whose names contain a or b), `crag_spike` "CRAG: overall PASS" (layout geometry only). Only run the spikes your change touches — the full set takes a while. All nine PASS as of this handoff.
(in-VR menu input injection, no XR). `ragdoll_spike` "RAGDOLL: overall PASS" (`-- only=a,b` runs the phases whose names contain a or b), `crag_spike` "CRAG: overall PASS" (layout geometry of the SAVED crag scene; see Maps below). Only run the spikes your change touches — the full set takes a while. All nine PASS as of this handoff.
**Fresh checkout / new worktree: run `godot-mono --headless --import` FIRST** (it may segfault at
exit — harmless) — without `.godot/extension_list.cfg` the Box3D extension silently doesn't load,
Godot falls back to its default physics and weld/grab/arm/6dof spikes report CHECK. Headless asset import:
@@ -111,6 +111,16 @@ TERMINOLOGY (owner-corrected — use precisely):
- **Playground.cs** — the body sandbox around the table (east climbing/mantling, west physics toys, south
crawl/duck/door); dynamic pieces are "prop"s reset by the menu's Reset props. `tools/playground_shots.tscn` renders
overview PNGs (`godot-mono --path . tools/playground_shots.tscn -- <dir>`).
- **Maps are EDITOR-BUILT scenes** (`MapScene.cs`): `scenes/maps/playground.tscn` and `scenes/maps/crag.tscn` (roots =
the `[Tool]` scripts Playground / Crag, instanced by main.tscn). The game NEVER regenerates them — it loads what is
saved, so hand edits in the editor stick. Regenerate from code with the map's inspector button "Rebuild (replaces
all children)" (open the map's own scene, then save), or headless for agents:
`godot-mono --headless --path . tools/rebuild_maps.tscn [-- only=playground|crag]` → "MAPS: rebuilt …" / "MAPS: done"
(rebuild REPLACES all children, discarding hand edits). After changing Playground.cs/Crag.cs builders, rebuild,
then run `crag_spike` (it checks the SAVED crag: route = the Marker3D children of its "Route" node, metadata
kind/label; route bodies in group `crag_route`, rock in `crag_mass`). Joint node paths are saved relative
(`../Name`); groups must be added persistent (`AddToGroup(g, true)`) or they aren't saved. `tools/map_dump.tscn`
dumps a layout (type/transform/size/groups) for before/after comparisons.
- **AnchorMoveMode.cs** is now SPECTATOR-only (`AnchorMoveMode.Enabled`, menu → Player → Spectator), where the
ragdoll is cleared; leaving rebuilds it at the current pose.
- **LEGACY (not in main.tscn since step 2; kept for their spikes): PhysicsHands / PhysicsTorso / PhysicsArms / BodyRig.**
+4 -6
View File
@@ -10,8 +10,8 @@
[ext_resource type="Script" path="res://scripts/Calibration.cs" id="12_calibration"]
[ext_resource type="Script" path="res://scripts/TrueBody.cs" id="13_truebody"]
[ext_resource type="Script" path="res://scripts/PlayerRig.cs" id="14_playerrig"]
[ext_resource type="Script" path="res://scripts/Playground.cs" id="15_playground"]
[ext_resource type="Script" path="res://scripts/Crag.cs" id="16_crag"]
[ext_resource type="PackedScene" path="res://scenes/maps/playground.tscn" id="15_playground"]
[ext_resource type="PackedScene" path="res://scenes/maps/crag.tscn" id="16_crag"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
sky_horizon_color = Color(0.66, 0.72, 0.81, 1)
@@ -109,11 +109,9 @@ script = ExtResource("3_locomotion")
[node name="AnchorMoveMode" type="Node" parent="."]
script = ExtResource("5_anchormove")
[node name="Playground" type="Node3D" parent="."]
script = ExtResource("15_playground")
[node name="Playground" parent="." instance=ExtResource("15_playground")]
[node name="Crag" type="Node3D" parent="."]
script = ExtResource("16_crag")
[node name="Crag" parent="." instance=ExtResource("16_crag")]
[node name="PropShop" type="Node3D" parent="."]
script = ExtResource("6_propshop")
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+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);
}
}
+5 -3
View File
@@ -1,7 +1,8 @@
using Godot;
using System.Collections.Generic;
// Regression for the crag (no player): the route is climbable on paper — consecutive route points
// Regression for the crag (no player), checked on the SAVED scenes/maps/crag.tscn (route = its
// Route markers; rebuild with tools/rebuild_maps.tscn): the route is climbable on paper — consecutive route points
// within human reach, every point in free space, route pieces not buried in each other or the
// rock — the summit is high enough, and it's in plain view from spawn. Prints "CRAG: overall PASS".
public partial class CragSpike : Node3D
@@ -23,8 +24,9 @@ public partial class CragSpike : Node3D
floor.AddToGroup(PropGrabber.NoGrabGroup);
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(80, 0.1f, 80) }, Position = new Vector3(0, -0.05f, 0) });
AddChild(floor);
AddChild(new Playground());
_crag = new Crag();
// The SAVED scenes (as the game loads them, hand edits included), not a fresh rebuild.
AddChild(GD.Load<PackedScene>("res://scenes/maps/playground.tscn").Instantiate());
_crag = GD.Load<PackedScene>("res://scenes/maps/crag.tscn").Instantiate<Crag>();
AddChild(_crag);
}
+80
View File
@@ -0,0 +1,80 @@
using Godot;
using System.Collections.Generic;
// Shared plumbing for the editor-built maps (Playground, Crag). Each map is the ROOT of its own scene
// (scenes/maps/*.tscn) with a [Tool] script whose Rebuild() regenerates every child from code and
// gives it to the scene (Owner = the map root) so it is saved into the .tscn like hand-made nodes.
// The game never regenerates: it loads what was saved, so edits made in the editor stick (until the
// next Rebuild, which replaces ALL children). Rebuild from the inspector button with the map scene
// open, or headless for agents: godot-mono --headless --path . tools/rebuild_maps.tscn
public static class MapScene
{
public const string PropGroup = "prop";
// The inspector button only works with the map scene itself open (nodes owned by an instanced
// sub-scene's root aren't saved into the outer scene).
public static bool CanRebuildInEditor(Node map)
{
if (map.GetTree()?.EditedSceneRoot == map)
return true;
GD.PushWarning($"{map.Name}: open its own scene (scenes/maps/) to rebuild it — it's instanced here.");
return false;
}
public static void Clear(Node map)
{
foreach (Node child in map.GetChildren())
{
map.RemoveChild(child);
child.Free();
}
}
// Every generated node belongs to the map scene; children of instanced sub-scenes (e.g. a
// teleport marker) keep their own owner.
public static void Adopt(Node map)
{
foreach (Node child in map.GetChildren())
Adopt(map, child);
}
private static void Adopt(Node root, Node node)
{
if (node.Owner == null)
node.Owner = root;
if (node.SceneFilePath != "")
return;
foreach (Node child in node.GetChildren())
Adopt(root, child);
}
public static void MarkUnsaved()
{
#if TOOLS
if (Engine.IsEditorHint())
EditorInterface.Singleton.MarkSceneAsUnsaved();
#endif
}
// Runtime: the dynamic props (group "prop") and where they start, for Reset().
public static void CollectProps(Node map, List<(RigidBody3D Body, Transform3D Start)> props)
{
props.Clear();
foreach (Node child in map.GetChildren())
if (child is RigidBody3D body && body.IsInGroup(PropGroup))
props.Add((body, body.GlobalTransform));
}
public static void Reset(List<(RigidBody3D Body, Transform3D Start)> props)
{
foreach (var (body, start) in props)
{
if (!GodotObject.IsInstanceValid(body))
continue;
body.GlobalTransform = start;
body.LinearVelocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
body.ResetPhysicsInterpolation();
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://d0oo5qx6v57bs
+42 -26
View File
@@ -1,7 +1,9 @@
using Godot;
using System.Collections.Generic;
// The body-physics sandbox (Nimso-sandbox style), built in code around the spawn/table area:
// The body-physics sandbox (Nimso-sandbox style) around the spawn/table area. EDITOR-BUILT: this is the
// root of scenes/maps/playground.tscn and Rebuild() generates the pieces below into that scene
// (MapScene: inspector button, or tools/rebuild_maps.tscn headless); the game loads the saved scene.
// EAST — mantling ledges (0.3 … 1.5 m), a 1 m platform with stairs, a walkable 18° ramp and a
// too-steep 40° one, a climbing wall with holds onto a 2.4 m top, a pull-up bar, monkey bars.
// WEST — physics toys: a plank bridge on two blocks (walk on it / knock it off), loose planks,
@@ -10,8 +12,12 @@ using System.Collections.Generic;
// SOUTH — a crawl tunnel (0.6 m clearance), a duck-under bar (1.2 m) and a doorway.
// Static pieces are world (layer 1) and grabbable for climbing (PropGrabber grabs any static body
// not in "no_grab"); dynamic pieces are "prop"s (grab/push) and come back with Reset().
[Tool]
public partial class Playground : Node3D
{
[ExportToolButton("Rebuild (replaces all children)")]
public Callable RebuildButton => Callable.From(RebuildInEditor);
private readonly List<(RigidBody3D Body, Transform3D Start)> _dynamic = new();
private const uint WorldLayer = 1, WorldMask = 0b11101, PropMask = 0b11101;
@@ -21,22 +27,28 @@ public partial class Playground : Node3D
public override void _Ready()
{
if (!Engine.IsEditorHint())
MapScene.CollectProps(this, _dynamic);
}
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);
BuildEast();
BuildWest();
BuildSouth();
}
public void Reset()
{
foreach (var (body, start) in _dynamic)
{
if (!IsInstanceValid(body))
continue;
body.GlobalTransform = start;
body.LinearVelocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
body.ResetPhysicsInterpolation();
}
MapScene.Adopt(this);
}
// ---------------------------------------------------------------- builders
@@ -46,7 +58,7 @@ public partial class Playground : Node3D
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(basis ?? Basis.Identity, center) };
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
body.AddChild(Mesh(new BoxMesh { Size = size }, color));
AddChild(body);
AddChild(body, true);
return body;
}
@@ -59,7 +71,7 @@ public partial class Playground : 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;
}
@@ -75,11 +87,10 @@ public partial class Playground : Node3D
MaxContactsReported = 4,
PhysicsMaterialOverride = new PhysicsMaterial { Friction = friction, 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;
}
@@ -97,13 +108,14 @@ public partial class Playground : Node3D
{
AddChild(new Label3D
{
Name = "Sign",
Text = text,
FontSize = 96,
PixelSize = 0.002f,
OutlineSize = 12,
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
Position = at,
});
}, true);
}
// ---------------------------------------------------------------- zones
@@ -183,13 +195,16 @@ public partial class Playground : Node3D
// Seesaw: a plank hinged on a pivot block.
Block("SeesawPivot", new Vector3(0.3f, 0.45f, 0.5f), new Vector3(-8.5f, 0.225f, -1.5f), Metal);
RigidBody3D seesaw = BoxProp("Seesaw", new Vector3(3.2f, 0.06f, 0.45f), 12f, new Vector3(-8.5f, 0.48f, -1.5f), DarkWood);
// Joint paths resolve relative to the joint (a sibling here) and are saved that way; set
// before AddChild so the joint never exists without its bodies.
HingeJoint3D pivot = new()
{
Name = "SeesawHinge",
Transform = new Transform3D(new Basis(Vector3.Right, Vector3.Up, Vector3.Back), new Vector3(-8.5f, 0.48f, -1.5f)),
NodeA = GetNode<Node>("SeesawPivot").GetPath(), // absolute: joint paths resolve relative to the joint
NodeB = seesaw.GetPath(),
NodeA = $"../{GetNode<Node>("SeesawPivot").Name}",
NodeB = $"../{seesaw.Name}",
};
AddChild(pivot);
AddChild(pivot, true);
Sign("Seesaw", new Vector3(-8.5f, 1.3f, -1.5f));
// Swing board: a 10 kg board hinged at its top edge from a 2.4 m beam — grab it and hang.
@@ -199,11 +214,12 @@ public partial class Playground : Node3D
RigidBody3D board = BoxProp("SwingBoard", new Vector3(1.2f, 1.2f, 0.05f), 10f, new Vector3(-6f, 1.75f, 1.5f), Wood);
HingeJoint3D swing = new()
{
Name = "SwingHinge",
Transform = new Transform3D(new Basis(Vector3.Back, Vector3.Up, Vector3.Left), new Vector3(-6f, 2.35f, 1.5f)),
NodeA = beam.GetPath(),
NodeB = board.GetPath(),
NodeA = $"../{beam.Name}",
NodeB = $"../{board.Name}",
};
AddChild(swing);
AddChild(swing, true);
Sign("Hanging board", new Vector3(-6f, 2.9f, 1.5f));
// Crate pyramid (4 kg each), a heavy 25 kg box, barrels.
+2 -1
View File
@@ -1193,7 +1193,8 @@ public partial class RagdollSpike : Node3D
private void SetupPlayground()
{
Rest();
_playground = new Playground { Position = new Vector3(0f, 0f, 40f) }; // far from the player
_playground = GD.Load<PackedScene>("res://scenes/maps/playground.tscn").Instantiate<Playground>(); // the saved scene
_playground.Position = new Vector3(0f, 0f, 40f); // far from the player
_stage.AddChild(_playground);
}
+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")