Belt rocks and their reticle are drawn on the floater layer over the field by DrawAhead

DrawAhead.Chunk/ChunkHeight (ruling asteroids-on-floater-layer); ZoneRenderer and the
chunk reticle both read it; ChunkWorldPosition and the renderer's global are gone.
Fixes the reticle sitting off the rock by |field|. Test: AChunkIsDrawnOnTheFloaterLayerOverTheField.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-17 13:48:34 +02:00
commit 71fa9411c6
4 changed files with 40 additions and 13 deletions

View file

@ -1174,15 +1174,6 @@ public class ActionGameManager : MonoBehaviour
}
}
// A chunk's pose is planar (Zone.ChunkPose), read at the drawn time so the reticle sits on the drawn rock; the renderer
// draws it at the asteroid layer's height.
private float3 ChunkWorldPosition(ChunkId chunk)
{
var zone = CurrentEntity.Zone;
var pose = zone.ChunkPose(chunk.Field, chunk.Index, DrawAhead.Time(zone, Clock.Lead));
return float3(pose.x, Settings.PlanetSettings.AsteroidVerticalOffset, pose.y);
}
// The one way a run ends, won or lost: the run is cleared and the main menu returns.
private void EndRun()
{
@ -1465,7 +1456,7 @@ public class ActionGameManager : MonoBehaviour
ViewDot.Target = ZoneRenderer.EntityInstances[CurrentEntity].LookAtPoint.position;
var target = CurrentEntity.Target.Value;
if (target.Chunk is ChunkId chunk)
TargetIndicator.Target = ChunkWorldPosition(chunk).ToUnity();
TargetIndicator.Target = DrawAhead.Chunk(CurrentEntity.Zone, Settings.GameplaySettings, chunk, lead).ToUnity();
else if (target.Entity != null)
TargetIndicator.Target = DrawAhead.Position(target.Entity, lead).ToUnity();
var distance = length(ViewDot.Target.ToCultMath() - ownDrawn);

View file

@ -29,6 +29,16 @@ public static class DrawAhead
// The drawn height of a floater (loot, mines): the field at the drawn time plus the floater layer.
public static float FloaterHeight(Zone zone, GameplaySettings settings, float2 planar, float lead) => Height(zone, planar, lead) + settings.FloaterLayerHeight;
// A belt rock's drawn position (ruling asteroids-on-floater-layer): its planar pose at the drawn time, on the floater layer over the field.
// Open question asteroid-clearance: the rock centre sits at the layer. ChunkHeight is the one line that changes if its top should (pose.w is the radius).
public static float ChunkHeight(Zone zone, GameplaySettings settings, float4 pose, float lead) => FloaterHeight(zone, settings, pose.xy, lead);
public static float3 Chunk(Zone zone, GameplaySettings settings, ChunkId chunk, float lead)
{
var pose = zone.ChunkPose(chunk.Field, chunk.Index, Time(zone, lead));
return float3(pose.x, ChunkHeight(zone, settings, pose, lead), pose.y);
}
// A ship in a wormhole animation has its pose scripted by the step, not integrated from Velocity and TurnRate,
// so it is drawn where the step put it: extrapolating a scripted pose draws a streak that resets every step.
// A child is drawn where its parent is drawn, as the step copies the parent's pose onto it.

View file

@ -533,7 +533,6 @@ public class ZoneRenderer : MonoBehaviour
// SlimeRenderer.SpawnPositions[i] = _suns[i].Body.transform.position.Flatland();
// }
Shader.SetGlobalFloat("_AsteroidVerticalOffset", ActionGameManager.Instance.Settings.PlanetSettings.AsteroidVerticalOffset);
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(MainCamera);
bool isVisible(Bounds bounds) => GeometryUtility.TestPlanesAABB(planes, bounds);
@ -559,7 +558,7 @@ public class ZoneRenderer : MonoBehaviour
{
var tx = t + i * count;
var transform = transforms[tx];
matrices[i][t] = Matrix4x4.TRS(new Vector3(transform.x,0,transform.y),
matrices[i][t] = Matrix4x4.TRS(new Vector3(transform.x,DrawAhead.ChunkHeight(Zone,ItemManager.GameplaySettings,transform,lead),transform.y),
Quaternion.Euler(
cos(transform.z + (float)i / meshes.Length) * 100,
sin(transform.z + (float)i / meshes.Length) * 100,

View file

@ -29,6 +29,7 @@ public sealed class DrawnPoseTests : IDisposable
private const float StationDistance = 3f, StationPhase = .4f;
private const float ShipLayerHeight = 2.5f;
private const float OrbitalLayerHeight = 7.5f;
private const float FloaterLayerHeight = -1.25f;
// The sun's rail is parentless and off-centre, the shape ZoneGenerator gives a cluster root (FixedPosition = cluster centre).
private static readonly float2 SunCentre = new float2(700f, -300f);
@ -88,7 +89,7 @@ public sealed class DrawnPoseTests : IDisposable
DefaultEntitySettings = new EntitySettings(),
Tiers = new[] { new RarityTier { Name = "Common", Quality = .5f, Rarity = 0, Color = new float3(1, 1, 1) } },
QualityPriceModifier = new ExponentialLerp(),
ShipLayerHeight = ShipLayerHeight, OrbitalLayerHeight = OrbitalLayerHeight
ShipLayerHeight = ShipLayerHeight, OrbitalLayerHeight = OrbitalLayerHeight, FloaterLayerHeight = FloaterLayerHeight
}, _ => { });
CultRecordKey Orbit(CultRecordKey parent, float distance, float phase, float2 fixedPosition = default)
@ -347,6 +348,32 @@ public sealed class DrawnPoseTests : IDisposable
}
}
// The reticle (DrawAhead.Chunk) and the renderer's per-rock instance (DrawAhead.ChunkHeight on EvaluateBelt's drawn pose)
// are one height: the field at the rock's planar position at the drawn time, plus the floater layer. Ruling asteroids-on-floater-layer.
[Fact]
public void AChunkIsDrawnOnTheFloaterLayerOverTheField()
{
var w = Build();
Step(w, 1f / 60f, 301);
const float lead = .01f;
var time = w.Zone.ExactTime + lead;
var drawn = new float4[w.Asteroids.Length];
w.Zone.EvaluateBelt(w.Belt, drawn, time);
var settings = w.Items.GameplaySettings;
var deep = 0;
for (var i = 0; i < w.Asteroids.Length; i++)
{
var reticle = DrawAhead.Chunk(w.Zone, settings, new ChunkId(w.Belt, i), lead);
var field = FieldAt(w, drawn[i].xy, time);
AssertNear(drawn[i].xy, reticle.xz, 1e-3f, $"chunk {i} reticle xz");
Assert.Equal(field + FloaterLayerHeight, reticle.y, 3);
Assert.Equal(reticle.y, DrawAhead.ChunkHeight(w.Zone, settings, drawn[i], lead), 4);
if (abs(field) > 1f) deep++;
}
Assert.True(deep > 0, "some rock sits where the field is more than a unit from zero, so a dropped field fails");
}
// The term the shader takes sits on its node's rail at the drawn time: the lowering copies DrawAhead.Orbit of the term's
// node and decides nothing, so a drawn term and the drawn planet share one pose at a non-zero lead.
[Fact]