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>
81 lines
2.8 KiB
C#
81 lines
2.8 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|