Merge branch 'worktree-agent-a25f37ee433ac3e9d'

This commit is contained in:
Foxyz committed 2026-10-02 17:25:18 +03:00
commit 6ff6130762
9 files changed
+554 -8

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://scripts/CragSpike.cs" id="1_spike"]
[node name="CragSpike" type="Node3D"]
script = ExtResource("1_spike")
+7 -3
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=21 format=3]
[gd_scene load_steps=22 format=3]
[ext_resource type="Script" path="res://scripts/Main.cs" id="1_main"]
[ext_resource type="PackedScene" path="res://scenes/teleport_marker.tscn" id="2_marker"]
@@ -11,6 +11,7 @@
[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"]
[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_sky"]
sky_horizon_color = Color(0.66, 0.72, 0.81, 1)
@@ -28,10 +29,10 @@ albedo_color = Color(0.35, 0.35, 0.38, 1)
[sub_resource type="PlaneMesh" id="PlaneMesh_floor"]
material = SubResource("Material_floor")
size = Vector2(40, 40)
size = Vector2(80, 80)
[sub_resource type="BoxShape3D" id="BoxShape_floor"]
size = Vector3(40, 0.1, 40)
size = Vector3(80, 0.1, 80)
[sub_resource type="SphereMesh" id="SphereMesh_hand"]
radius = 0.04
@@ -111,6 +112,9 @@ script = ExtResource("5_anchormove")
[node name="Playground" type="Node3D" parent="."]
script = ExtResource("15_playground")
[node name="Crag" type="Node3D" parent="."]
script = ExtResource("16_crag")
[node name="PropShop" type="Node3D" parent="."]
script = ExtResource("6_propshop")
+358
View File
@@ -0,0 +1,358 @@
using Godot;
using System.Collections.Generic;
// The crag: a 26 m cliff of stacked rock boxes north of the playground, one route up, getting
// harder as it goes (built in code like Playground, same layers and grab rules):
// 1. bouldering blocks (1.2 / 2.1 / 3.0 m) to mantle and hop between;
// 2. a ladder up the first tier's face to REST LEDGE 1 (8 m, flag);
// 3. a jug wall (two staggered columns of holds) up the second tier to 13.5 m;
// 4. a narrow 0.4 m ledge traversed sideways along a handrail of holds, open air behind;
// 5. an overhang: monkey bars under a roof, a jug on its lip and a lip bar to pull over onto
// REST LEDGE 2 (16 m, flag);
// 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.
public partial class Crag : Node3D
{
public enum Step { Stand, Hold }
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();
public const float SummitHeight = 26f;
public static readonly Vector3 SummitPoint = new(2.25f, SummitHeight, -18f);
private readonly List<(RigidBody3D Body, Transform3D Start)> _dynamic = new();
private const uint WorldLayer = 1, WorldMask = 0b11101, PropMask = 0b11101;
private readonly RandomNumberGenerator _rng = new() { Seed = 26 };
private static readonly Color[] Rock =
{
new(0.47f, 0.43f, 0.39f), new(0.54f, 0.5f, 0.45f), new(0.41f, 0.39f, 0.37f), new(0.58f, 0.53f, 0.47f),
};
private static readonly Color Hold = new(0.9f, 0.45f, 0.15f), Metal = new(0.45f, 0.47f, 0.52f),
Wood = new(0.72f, 0.55f, 0.33f), Flag = new(0.85f, 0.15f, 0.15f), Bark = new(0.35f, 0.25f, 0.17f),
Leaves = new(0.25f, 0.45f, 0.22f), Pillar = new(0.7f, 0.7f, 0.68f);
public override void _Ready()
{
BuildMass();
BuildRoute();
BuildBase();
BuildSummitProps();
}
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();
}
}
// ---------------------------------------------------------------- the rock
private void BuildMass()
{
// Three tiers, each set back 1.5 m: A 0–8 m, B 8–16 m, C 16–26 m (C split by the chimney).
Mass("TierA", new Vector3(12f, 8f, 7f), new Vector3(0f, 4f, -16.5f), Rock[0]);
Mass("TierB", new Vector3(10f, 8f, 5.5f), new Vector3(0f, 12f, -17.25f), Rock[1]);
Mass("TierCWest", new Vector3(3.55f, 10f, 4f), new Vector3(-2.225f, 21f, -18f), Rock[2]);
Mass("TierCEast", new Vector3(3.55f, 10f, 4f), new Vector3(2.225f, 21f, -18f), Rock[2]);
Mass("TierCBack", new Vector3(0.9f, 10f, 3f), new Vector3(0f, 21f, -18.5f), Rock[3]);
Mass("Spire", new Vector3(1.3f, 2.6f, 1.3f), new Vector3(3.1f, 27.3f, -19.1f), Rock[1], new Basis(Vector3.Up, 0.3f));
// Shoulders and buttresses break the silhouette (sides and back only — the south faces
// carry the route).
Mass("ShoulderWestA", new Vector3(2.4f, 5.5f, 5f), new Vector3(-7f, 2.75f, -16.8f), Rock[2], new Basis(Vector3.Up, 0.12f));
Mass("ShoulderEastA", new Vector3(2.8f, 6.8f, 5.5f), new Vector3(7.1f, 3.4f, -16.2f), Rock[3], new Basis(Vector3.Up, -0.15f));
Mass("ShoulderEastB", new Vector3(1.8f, 5.5f, 4f), new Vector3(5.6f, 10.75f, -17.8f), Rock[0], new Basis(Vector3.Up, 0.1f));
Mass("ShoulderWestB", new Vector3(1.6f, 3.6f, 3.5f), new Vector3(-5.5f, 9.8f, -18f), Rock[3], new Basis(Vector3.Up, -0.2f));
Mass("ShoulderWestC", new Vector3(1.4f, 6f, 3f), new Vector3(-4.3f, 19f, -18.6f), Rock[0], new Basis(Vector3.Up, 0.18f));
Mass("ShoulderEastC", new Vector3(1.5f, 7.5f, 3.2f), new Vector3(4.4f, 19.75f, -18.4f), Rock[3], new Basis(Vector3.Up, -0.12f));
Mass("BackA", new Vector3(9f, 6f, 1.8f), new Vector3(0.5f, 3f, -20.6f), Rock[1], new Basis(Vector3.Up, 0.06f));
Mass("BackB", new Vector3(6f, 5f, 1.4f), new Vector3(-0.8f, 13.5f, -20.4f), Rock[2], new Basis(Vector3.Up, -0.08f));
// Broken corners: big turned chunks knocking the tier edges out of square.
(Vector3 At, Vector3 Size, Vector3 Turn)[] corners =
{
(new(6.3f, 5.6f, -13.4f), new(2.2f, 4.4f, 2.4f), new(0.08f, 0.35f, -0.1f)),
(new(-6.6f, 5.5f, -13.6f), new(2f, 4.6f, 2.2f), new(-0.06f, -0.4f, 0.08f)),
(new(6.4f, 12.4f, -15.4f), new(1.6f, 4f, 2f), new(0.1f, 0.3f, 0.12f)),
(new(-5.5f, 13.6f, -15.2f), new(1.8f, 3.6f, 2f), new(-0.08f, -0.35f, -0.1f)),
(new(4.3f, 23f, -16.4f), new(1.6f, 4.4f, 1.8f), new(0.05f, 0.4f, 0.1f)),
(new(-4.4f, 22.2f, -16.6f), new(1.6f, 4.8f, 1.8f), new(-0.05f, -0.3f, -0.08f)),
(new(8.2f, 2.4f, -17.8f), new(2.6f, 4.8f, 2f), new(0f, 0.2f, -0.35f)), // slab leaning on the east side
};
for (int i = 0; i < corners.Length; i++)
Mass($"Corner{i}", corners[i].Size, corners[i].At, Rock[(i + 1) % Rock.Length], Basis.FromEuler(corners[i].Turn));
// Talus: fallen blocks around the foot (clear of the route's boulders and the marker).
(Vector3 At, float Size)[] talus =
{
(new(9.5f, 0.9f, -13.5f), 1.8f), (new(11f, 0.6f, -16.2f), 1.2f), (new(-9.6f, 1.1f, -14.2f), 2.2f),
(new(-10.5f, 0.6f, -19.5f), 1.2f), (new(6.5f, 0.7f, -11.2f), 1.4f), (new(-8.3f, 0.45f, -11.8f), 0.9f),
(new(4f, 1.2f, -23f), 2.4f), (new(-5f, 0.8f, -23.4f), 1.6f),
};
for (int i = 0; i < talus.Length; i++)
{
float s = talus[i].Size;
Vector3 size = new(s * _rng.RandfRange(0.9f, 1.4f), s, s * _rng.RandfRange(0.8f, 1.2f));
Mass($"Talus{i}", size, talus[i].At, Rock[i % Rock.Length], Basis.FromEuler(new Vector3(_rng.RandfRange(-0.15f, 0.15f), _rng.RandfRange(-1f, 1f), _rng.RandfRange(-0.15f, 0.15f))));
}
// Rubble on the walls away from the route: protruding, slightly turned blocks.
Vector3[] spots =
{
new(3.5f, 1.2f, -12.8f), new(5f, 3.5f, -12.8f), new(1.6f, 5.6f, -12.8f), new(4.2f, 6.8f, -12.8f),
new(-0.2f, 2.4f, -12.8f), new(0.4f, 10.2f, -14.3f), new(-1.2f, 11.6f, -14.3f), new(-4.5f, 11.0f, -14.3f),
new(-6.1f, 4f, -14.2f), new(6.2f, 7.5f, -14.5f), new(-3.6f, 20.5f, -15.8f), new(3.2f, 22.8f, -15.8f),
new(-2.4f, 24.6f, -15.8f), new(1.8f, 18.4f, -15.8f),
};
for (int i = 0; i < spots.Length; i++)
{
Vector3 size = new(_rng.RandfRange(0.8f, 1.8f), _rng.RandfRange(0.6f, 1.4f), 0.6f);
Basis turn = Basis.FromEuler(new Vector3(_rng.RandfRange(-0.12f, 0.12f), _rng.RandfRange(-0.25f, 0.25f), _rng.RandfRange(-0.15f, 0.15f)));
Mass($"Rubble{i}", size, spots[i], Rock[i % Rock.Length].Darkened(_rng.RandfRange(0f, 0.08f)), turn);
}
}
// ---------------------------------------------------------------- the route
private void BuildRoute()
{
// 1. Bouldering: three blocks rising 1.2 / 0.9 / 0.9 m with small gaps between.
Vector3 start = new(-5.6f, 0f, -8.2f);
AddStep(Step.Stand, start, "start");
RouteBlock("Boulder1", new Vector3(1.4f, 1.2f, 1.4f), new Vector3(-5.6f, 0.6f, -9.6f), Rock[3]);
AddStep(Step.Stand, new Vector3(-5.6f, 1.2f, -9.1f), "boulder 1 (mantle 1.2 m)");
AddStep(Step.Stand, new Vector3(-5.0f, 1.2f, -10.1f), "boulder 1 far edge");
RouteBlock("Boulder2", new Vector3(1.4f, 2.1f, 1.4f), new Vector3(-4.6f, 1.05f, -11.2f), Rock[0]);
AddStep(Step.Stand, new Vector3(-4.8f, 2.1f, -10.7f), "boulder 2 (mantle 0.9 m)");
AddStep(Step.Stand, new Vector3(-4.1f, 2.1f, -11.4f), "boulder 2 far edge");
RouteBlock("Boulder3", new Vector3(1.5f, 3f, 1.6f), new Vector3(-2.7f, 1.5f, -12.2f), Rock[2]);
AddStep(Step.Stand, new Vector3(-3.3f, 3f, -11.8f), "boulder 3 (hop 0.45 m gap, mantle 0.9 m)");
AddStep(Step.Stand, new Vector3(-2.7f, 3f, -12.5f), "foot of the ladder");
// 2. Ladder up tier A's face (z = -13) to rest ledge 1; rails run past the lip to pull over.
const float ladderX = -2.7f, ladderZ = -12.85f, half = 0.3f, rail = 0.03f, rung = 0.022f;
foreach (float sx in new[] { -half, half })
{
RouteBar("LadderRail", new Vector3(ladderX + sx, 3f, ladderZ), new Vector3(ladderX + sx, 9f, ladderZ), rail, Metal);
for (float y = 4f; y < 9f; y += 2f)
RouteBlock("LadderStandoff", new Vector3(0.04f, 0.04f, 0.12f), new Vector3(ladderX + sx, y, -12.94f), Metal);
}
for (float y = 3.3f; y < 8.85f; y += 0.3f)
{
RouteBar("Rung", new Vector3(ladderX - half + rail, y, ladderZ), new Vector3(ladderX + half - rail, y, ladderZ), rung, Metal);
AddStep(Step.Hold, new Vector3(ladderX, y + rung + 0.02f, ladderZ), $"rung {y:0.0} m");
}
// Rest ledge 1: tier A's top strip in front of tier B (1.5 m deep, 12 m wide).
AddStep(Step.Stand, new Vector3(ladderX, 8f, -13.4f), "REST LEDGE 1 (8 m)");
FlagPole("8 m", new Vector3(-5.3f, 8f, -13.7f));
AddStep(Step.Stand, new Vector3(-3f, 8f, -14.1f), "foot of the jug wall");
// 3. Jug wall on tier B's face (z = -14.5): two staggered columns, 0.45 m apart vertically.
int n = 0;
for (float y = 9f; y < 13.2f; y += 0.45f, n++)
{
float x = n % 2 == 0 ? -3.25f : -2.75f;
Jug(new Vector3(x, y, -14.5f), Vector3.Back);
}
// 4. Traverse ledge at 13.5 m: 0.4 m deep; its lip is the last jug.
RouteBlock("TraverseLedge", new Vector3(8.2f, 0.12f, 0.4f), new Vector3(0.5f, 13.44f, -14.3f), Rock[3]);
AddStep(Step.Hold, new Vector3(-3f, 13.52f, -14.08f), "traverse ledge lip");
AddStep(Step.Stand, new Vector3(-3f, 13.5f, -14.3f), "traverse ledge (0.4 m wide, 13.5 m)");
for (float x = -2.4f; x <= 3.05f; x += 0.6f)
Jug(new Vector3(x, 14.9f, -14.5f), Vector3.Back);
AddStep(Step.Stand, new Vector3(3f, 13.5f, -14.3f), "under the overhang");
// 5. Overhang: a roof slab 1.5 m out from tier B, monkey bars under it, a jug on its front
// face and a bar along its top edge to pull over onto rest ledge 2.
RouteBlock("Overhang", new Vector3(3f, 0.6f, 1.5f), new Vector3(3f, 15.7f, -13.75f), Rock[0]);
const float barY = 15.2f, barR = 0.025f;
foreach (float z in new[] { -14.2f, -13.7f, -13.2f })
{
RouteBar("RoofBar", new Vector3(1.7f, barY, z), new Vector3(4.3f, barY, z), barR, Metal);
foreach (float x in new[] { 1.8f, 4.2f })
RouteBar("RoofHanger", new Vector3(x, barY + barR, z), new Vector3(x, 15.4f, z), 0.015f, Metal);
AddStep(Step.Hold, new Vector3(3f, barY - barR - 0.02f, z), $"roof bar z {z:0.0}");
}
Jug(new Vector3(3f, 15.65f, -13f), Vector3.Back);
RouteBar("LipBar", new Vector3(1.7f, 16.03f, -13.05f), new Vector3(4.3f, 16.03f, -13.05f), 0.03f, Metal);
AddStep(Step.Hold, new Vector3(3f, 16.08f, -13.05f), "lip bar");
AddStep(Step.Stand, new Vector3(3f, 16f, -13.6f), "REST LEDGE 2 (16 m)");
FlagPole("16 m", new Vector3(-4.3f, 16f, -15.2f));
AddStep(Step.Stand, new Vector3(0f, 16f, -15.6f), "mouth of the chimney");
// 6. Chimney: a 0.9 m slot between tier C's halves (x ±0.45, z -16 … -17), holds on both
// walls staggered 0.5 m and the back wall in between, topping out at the summit.
n = 0;
for (float y = 16.7f; y < 25.9f; y += 0.25f, n++)
{
bool west = n % 2 == 0;
Jug(new Vector3(west ? -0.45f : 0.45f, y, -16.5f), west ? Vector3.Right : Vector3.Left);
}
AddStep(Step.Hold, new Vector3(-0.47f, SummitHeight + 0.02f, -16.4f), "chimney exit lip");
// 7. Summit.
AddStep(Step.Stand, new Vector3(-1f, SummitHeight, -16.6f), "SUMMIT (26 m)");
Sign($"Summit {SummitHeight:0} m", new Vector3(-2.4f, SummitHeight + 1.6f, -18.5f), 2f);
}
// A hold: a 0.3 m wide, 0.12 m proud box on the face at `at`, sticking out along `out`.
private void Jug(Vector3 at, Vector3 outward)
{
Vector3 size = Mathf.Abs(outward.X) > 0.5f ? new Vector3(0.12f, 0.08f, 0.3f) : new Vector3(0.3f, 0.08f, 0.12f);
Vector3 center = at + outward * 0.06f;
RouteBlock("Jug", size, center, Hold, castShadow: false);
AddStep(Step.Hold, center + Vector3.Up * 0.06f + outward * 0.02f, $"jug {at.Y:0.00} m");
}
private void FlagPole(string text, Vector3 foot)
{
RouteBar("FlagPole", foot, foot + Vector3.Up * 2.4f, 0.03f, Metal);
Block("Flag", new Vector3(0.02f, 0.45f, 0.7f), foot + new Vector3(0f, 2.15f, 0.38f), Flag, false);
Sign(text, foot + Vector3.Up * 2.8f, 1.5f);
}
// ---------------------------------------------------------------- base and summit
private void BuildBase()
{
// Trees and pillars at the foot: things of known size to read the height against.
(Vector3 At, float Height)[] trees = { (new(-10f, 0f, -11.5f), 6f), (new(10.5f, 0f, -12f), 7f), (new(-12f, 0f, -17f), 5.5f), (new(12f, 0f, -18f), 6.5f) };
foreach (var (at, h) in trees)
{
Bar("Trunk", at, at + Vector3.Up * h * 0.6f, 0.18f, Bark);
Block("Canopy", new Vector3(2.2f, h * 0.5f, 2.2f), at + Vector3.Up * h * 0.75f, Leaves, true, new Basis(Vector3.Up, at.X * 0.3f), grab: false);
}
Bar("PillarWest", new Vector3(-8.5f, 0f, -10.5f), new Vector3(-8.5f, 9f, -10.5f), 0.35f, Pillar);
Bar("PillarEast", new Vector3(8.5f, 0f, -10f), new Vector3(8.5f, 12f, -10f), 0.35f, Pillar);
Sign("The Crag — 26 m", new Vector3(-5.6f, 2.6f, -7.6f), 1.5f);
// 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.Position = new Vector3(1.5f, 0f, -9.5f);
AddChild(marker);
}
private void BuildSummitProps()
{
for (int i = 0; i < 3; i++)
BoxProp($"SummitCrate{i}", Vector3.One * 0.4f, 4f, new Vector3(2f + i * 0.5f, SummitHeight + 0.2f, -17.2f), Wood.Darkened(0.05f * i));
Prop("SummitBarrel", new CylinderShape3D { Radius = 0.28f, Height = 0.85f }, new CylinderMesh { TopRadius = 0.28f, BottomRadius = 0.28f, Height = 0.85f },
12f, new Vector3(-2.8f, SummitHeight + 0.425f, -18.8f), Flag.Darkened(0.2f));
Prop("SummitBall", new SphereShape3D { Radius = 0.15f }, new SphereMesh { Radius = 0.15f, Height = 0.3f }, 1.5f,
new Vector3(-1.6f, SummitHeight + 0.15f, -18.4f), new Color(0.95f, 0.85f, 0.2f));
BoxProp("SummitPlank", new Vector3(2f, 0.04f, 0.2f), 4f, new Vector3(2.4f, SummitHeight + 0.02f, -18.8f), Wood);
}
// ---------------------------------------------------------------- builders
private void AddStep(Step kind, Vector3 point, string name) => Route.Add((kind, point, name));
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)));
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)));
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)));
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.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);
return body;
}
private StaticBody3D Bar(string name, Vector3 from, Vector3 to, float radius, Color color)
{
Vector3 axis = to - from;
float len = axis.Length();
Vector3 y = axis / len;
Vector3 x = Mathf.Abs(y.Y) < 0.99f ? Vector3.Up.Cross(y).Normalized() : Vector3.Right;
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);
return body;
}
private RigidBody3D Prop(string name, Shape3D shape, Mesh mesh, float mass, Vector3 center, Color color)
{
RigidBody3D body = new()
{
Name = name,
Mass = mass,
Transform = new Transform3D(Basis.Identity, center),
CollisionMask = PropMask,
ContactMonitor = true,
MaxContactsReported = 4,
PhysicsMaterialOverride = new PhysicsMaterial { Friction = 0.8f, Bounce = 0.05f },
};
body.AddToGroup("prop");
body.AddChild(new CollisionShape3D { Shape = shape });
body.AddChild(Mesh(mesh, color));
AddChild(body);
_dynamic.Add((body, body.GlobalTransform));
return body;
}
private RigidBody3D BoxProp(string name, Vector3 size, float mass, Vector3 center, Color color) =>
Prop(name, new BoxShape3D { Size = size }, new BoxMesh { Size = size }, mass, center, color);
private static MeshInstance3D Mesh(Mesh mesh, Color color)
{
MeshInstance3D m = new() { Mesh = mesh };
m.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.9f });
return m;
}
private void Sign(string text, Vector3 at, float scale)
{
AddChild(new Label3D
{
Text = text,
FontSize = 96,
PixelSize = 0.002f * scale,
OutlineSize = 12,
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
Position = at,
});
}
}
+1
View File
@@ -0,0 +1 @@
uid://cqeckjtcuntlv
+166
View File
@@ -0,0 +1,166 @@
using Godot;
using System.Collections.Generic;
// Regression for the crag (no player): 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
{
public const float MaxHoldToHold = 1.0f; // m between consecutive holds (two hands span the chimney)
public const float MaxHoldGain = 0.7f; // m of height gained per hand move
public const float MaxReachAboveFeet = 2.3f; // standing overhead grip height, 1.75 m person
public const float MaxReachOut = 1.0f;
public const float MaxMantle = 1.3f; // m a stand-to-stand step may rise (mantle / jump)
public const float MaxGap = 1.0f; // m horizontal between stand points across a rise or a gap
private Crag _crag;
private int _frame;
private readonly List<string> _fails = new();
public override void _Ready()
{
StaticBody3D floor = new();
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();
AddChild(_crag);
}
public override void _PhysicsProcess(double delta)
{
if (++_frame < 3)
return;
PhysicsDirectSpaceState3D space = GetWorld3D().DirectSpaceState;
CheckReach();
CheckFreeSpace(space);
CheckWalks(space);
CheckOverlaps();
Check(Crag.SummitHeight >= 25f, $"summit {Crag.SummitHeight} m >= 25 m");
CheckView(space);
foreach (string f in _fails)
GD.Print("CRAG: FAIL " + f);
GD.Print($"CRAG: {_crag.Route.Count} route points, {_crag.RoutePieces.Count} route pieces, {_crag.MassPieces.Count} rock pieces, {_crag.GetChildCount()} nodes");
GD.Print(_fails.Count == 0 ? "CRAG: overall PASS" : $"CRAG: overall FAIL ({_fails.Count})");
GetTree().Quit();
}
private void Check(bool ok, string what)
{
if (!ok)
_fails.Add(what);
}
private static float Flat(Vector3 a, Vector3 b) => new Vector2(a.X - b.X, a.Z - b.Z).Length();
private void CheckReach()
{
var r = _crag.Route;
for (int i = 1; i < r.Count; i++)
{
var (ka, a, na) = r[i - 1];
var (kb, b, nb) = r[i];
string pair = $"'{na}' -> '{nb}'";
if (ka == Crag.Step.Hold && kb == Crag.Step.Hold)
{
Check(a.DistanceTo(b) <= MaxHoldToHold, $"{pair}: holds {a.DistanceTo(b):0.00} m apart");
Check(b.Y - a.Y <= MaxHoldGain, $"{pair}: hand move gains {b.Y - a.Y:0.00} m");
}
else if (ka == Crag.Step.Stand && kb == Crag.Step.Hold)
{
Check(b.Y - a.Y <= MaxReachAboveFeet && b.Y - a.Y >= -0.3f, $"{pair}: hold {b.Y - a.Y:0.00} m above feet");
Check(Flat(a, b) <= MaxReachOut, $"{pair}: hold {Flat(a, b):0.00} m out");
}
else if (ka == Crag.Step.Hold && kb == Crag.Step.Stand)
{
Check(b.Y <= a.Y + 0.3f, $"{pair}: stand {b.Y - a.Y:0.00} m above the last hold");
Check(Flat(a, b) <= MaxReachOut, $"{pair}: stand {Flat(a, b):0.00} m from the last hold");
}
else
{
float rise = b.Y - a.Y;
Check(rise <= MaxMantle, $"{pair}: rise {rise:0.00} m");
if (Mathf.Abs(rise) > 0.05f)
Check(Flat(a, b) <= MaxGap, $"{pair}: {Flat(a, b):0.00} m across a {rise:0.00} m step");
}
}
}
// Every route point is in the open; standing points have a body's worth of room above them
// and ground right under them.
private void CheckFreeSpace(PhysicsDirectSpaceState3D space)
{
foreach (var (kind, p, name) in _crag.Route)
{
var pq = new PhysicsPointQueryParameters3D { Position = p + (kind == Crag.Step.Stand ? Vector3.Up * 0.05f : Vector3.Zero) };
Check(space.IntersectPoint(pq, 1).Count == 0, $"'{name}' point is inside geometry");
if (kind != Crag.Step.Stand)
continue;
var box = new PhysicsShapeQueryParameters3D
{
Shape = new BoxShape3D { Size = new Vector3(0.3f, 1.2f, 0.3f) },
Transform = new Transform3D(Basis.Identity, p + Vector3.Up * 0.7f),
};
Check(space.IntersectShape(box, 1).Count == 0, $"'{name}' has no room to stand");
var ray = PhysicsRayQueryParameters3D.Create(p + Vector3.Up * 0.05f, p + Vector3.Down * 0.1f);
Check(space.IntersectRay(ray).Count > 0, $"'{name}' has nothing underfoot");
}
}
// Level stand-to-stand moves are walks: there is ground all along the way.
private void CheckWalks(PhysicsDirectSpaceState3D space)
{
var r = _crag.Route;
for (int i = 1; i < r.Count; i++)
{
if (r[i - 1].Kind != Crag.Step.Stand || r[i].Kind != Crag.Step.Stand || Mathf.Abs(r[i].Point.Y - r[i - 1].Point.Y) > 0.05f)
continue;
for (float t = 0.1f; t < 1f; t += 0.1f)
{
Vector3 p = r[i - 1].Point.Lerp(r[i].Point, t);
var ray = PhysicsRayQueryParameters3D.Create(p + Vector3.Up * 0.05f, p + Vector3.Down * 0.1f);
if (space.IntersectRay(ray).Count == 0)
{
_fails.Add($"walk '{r[i - 1].Name}' -> '{r[i].Name}': no ground at {p}");
break;
}
}
}
}
// No route piece is buried in another piece or in the rock (touching is fine).
private void CheckOverlaps()
{
var route = _crag.RoutePieces;
for (int i = 0; i < route.Count; i++)
{
for (int j = i + 1; j < route.Count; j++)
CheckPair(route[i], route[j]);
foreach (var m in _crag.MassPieces)
CheckPair(route[i], m);
}
}
private void CheckPair((CollisionObject3D Body, Aabb Box) a, (CollisionObject3D Body, Aabb Box) b)
{
if (!a.Box.Intersects(b.Box))
return;
Aabb i = a.Box.Intersection(b.Box);
float depth = Mathf.Min(i.Size.X, Mathf.Min(i.Size.Y, i.Size.Z));
if (depth > 0.01f)
_fails.Add($"{a.Body.Name} overlaps {b.Body.Name} by {depth:0.000} m");
}
// From the spawn point's eye height, the summit's front edge, both flags and the foot of the route are in view.
private void CheckView(PhysicsDirectSpaceState3D space)
{
Vector3 eye = new(0f, 1.7f, 2.5f);
foreach (Vector3 target in new[] { new Vector3(Crag.SummitPoint.X, Crag.SummitHeight - 0.1f, -15.95f), new Vector3(-5.6f, 1f, -9.6f), new Vector3(-2.7f, 6f, -12.8f), new Vector3(-5.3f, 10.6f, -13.7f), new Vector3(-4.3f, 18.6f, -15.2f) })
{
var hit = space.IntersectRay(PhysicsRayQueryParameters3D.Create(eye, target));
bool clear = hit.Count == 0 || ((Vector3)hit["position"]).DistanceTo(target) < 1f;
Check(clear, $"no line of sight from spawn to {target}" + (hit.Count > 0 ? $" (blocked at {(Vector3)hit["position"]})" : ""));
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://bohh3so3ue5jk
+5 -2
View File
@@ -232,13 +232,16 @@ public partial class Locomotion : Node
private void MoveToSpawn() => PlacePlayer(_spawn.GlobalPosition, ForwardYaw(_spawn.GlobalTransform));
private void PlacePlayer(Vector3 targetXz, float yaw)
// Lands the play-space floor on the target's height (head height above the floor kept), so a
// marker at the foot of the crag brings you down from the summit instead of dropping you 26 m.
private void PlacePlayer(Vector3 target, float yaw)
{
Transform3D h = _camera.GlobalTransform;
Transform3D hLocal = _origin.GlobalTransform.AffineInverse() * h;
float hmdYaw = h.Basis.GetEuler().Y;
Basis basis = Basis.FromEuler(new Vector3(0, yaw - hmdYaw, 0)) * h.Basis;
Transform3D desired = new Transform3D(basis, new Vector3(targetXz.X, h.Origin.Y, targetXz.Z));
float headAboveFloor = h.Origin.Y - _origin.GlobalPosition.Y;
Transform3D desired = new Transform3D(basis, new Vector3(target.X, target.Y + headAboveFloor, target.Z));
_origin.GlobalTransform = desired * hLocal.AffineInverse();
}
+1
View File
@@ -62,6 +62,7 @@ public partial class WorldPanel : Node3D
{
_propShop.Respawn();
GetNodeOrNull<Playground>("../../Playground")?.Reset();
GetNodeOrNull<Crag>("../../Crag")?.Reset();
_menu.Log("Props reset");
};
_menu.PingRequested += () =>
+9 -3
View File
@@ -14,19 +14,25 @@ public partial class PlaygroundShots : Node3D
(new Vector3(3f, 4f, 3f), new Vector3(9f, 1f, -4f)),
(new Vector3(-2f, 3.5f, 4f), new Vector3(-7f, 1f, -1f)),
(new Vector3(0f, 2.5f, 11f), new Vector3(1f, 0.6f, 7f)),
// The crag: from spawn eye height, from the side, the route up close, looking down from the summit.
(new Vector3(0f, 1.7f, 2.5f), new Vector3(-1f, 9f, -14f)),
(new Vector3(22f, 14f, 4f), new Vector3(0f, 12f, -16f)),
(new Vector3(-1f, 12f, -6f), new Vector3(-1f, 12f, -15f)),
(new Vector3(-0.8f, 27.7f, -16.4f), new Vector3(1.5f, 0f, -6f)),
};
public override void _Ready()
{
foreach (string a in OS.GetCmdlineUserArgs())
_dir = a;
AddChild(new DirectionalLight3D { RotationDegrees = new Vector3(-50f, -30f, 0f), ShadowEnabled = true });
AddChild(new DirectionalLight3D { DirectionalShadowMaxDistance = 60f, RotationDegrees = new Vector3(-50f, -30f, 0f), ShadowEnabled = true });
AddChild(new WorldEnvironment { Environment = new Environment { BackgroundMode = Environment.BGMode.Color, BackgroundColor = new Color(0.55f, 0.65f, 0.8f), AmbientLightSource = Environment.AmbientSource.Color, AmbientLightColor = new Color(0.5f, 0.5f, 0.55f) } });
StaticBody3D floor = new();
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(40, 0.1f, 40) }, Position = new Vector3(0, -0.05f, 0) });
floor.AddChild(new MeshInstance3D { Mesh = new PlaneMesh { Size = new Vector2(40, 40) } });
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());
_cam = new Camera3D { Current = true };
AddChild(_cam);
SetView();