Pickup: one beam region for selection and the pre-pull cancel; selection skips what no bay can store; test gaps and the phase rule pinned

beam-corner-claim-loop: InBeam is the one region predicate; Select claims from it and the pre-pull cancel holds the item only while it stays in it (an item at 74 along, 20 off is collected, not claimed and dropped every other tick).
full-hold-regrab-pins-and-blocks: Select skips an item no cargo bay can store whole, asked of the bay's own TryFindSpace (the test TryStore opens with), per ruling beam-skips-unstorable-items; a hold that fills mid-grab still cancels at the end.
pickup-test-gaps: smoothstep pose, spin-down while the gear is inactive, Remove freeing only the removed holder's items.
phase-ticks-not-rate-invariant: the claim is corrected in the tests; each phase fires on the first tick at or after its boundary, pinned at 1/60, 1/600 and an uneven dt cycle.

Builds; not yet run (the verify job follows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 07:43:42 +02:00
commit 4fba95e56f
2 changed files with 200 additions and 22 deletions

View file

@ -82,12 +82,13 @@ public class Pickup : Behavior, IAlwaysUpdatedBehavior
var envelop = (double) Evaluate(_data.EnvelopTime);
var pull = (double) Evaluate(_data.PullTime);
var range = Evaluate(_data.Range);
var radius = Evaluate(_data.BeamRadius);
var origin = Entity.Position.xz;
if (!_grabbing)
{
if (!active || _power < grabPower) return;
var item = Select(zone, origin, range, Evaluate(_data.BeamRadius));
var item = Select(zone, origin, range, radius);
if (item == null || !zone.TryHold(Entity, item.Id)) return;
_grabbing = true;
_target = item.Id;
@ -107,7 +108,7 @@ public class Pickup : Behavior, IAlwaysUpdatedBehavior
}
var pulling = _elapsed >= extend + envelop;
if (!active || _power < grabPower || (!pulling && length(held.Body.Position - origin) > range))
if (!active || _power < grabPower || (!pulling && !InBeam(held.Body.Position, origin, range, radius, out _)))
{
_grabbing = false;
zone.Free(_target);
@ -131,22 +132,31 @@ public class Pickup : Behavior, IAlwaysUpdatedBehavior
}
}
// The free item nearest the beam axis within Range along it and BeamRadius of it; ties go to the lowest id. A
// zero aim selects nothing.
// The one beam region: within Range along the aim axis (never behind) and BeamRadius off it. Selection claims
// from it and the grab holds on to its item only while the item stays in it. A zero aim has no region.
private bool InBeam(float2 position, float2 origin, float range, float radius, out float off)
{
off = 0;
var aim = Entity.Aim.xz;
if (lengthsq(aim) <= 0) return false;
var axis = normalize(aim);
var to = position - origin;
var along = dot(to, axis);
off = length(to - axis * along);
return along >= 0 && along <= range && off <= radius;
}
// The free item nearest the beam axis within the beam region that some cargo bay can store whole (the bay's own
// TryFindSpace, the test TryStore opens with); ties go to the lowest id. A zero aim selects nothing.
private FloatingItem Select(Zone zone, float2 origin, float range, float radius)
{
var aim = Entity.Aim.xz;
if (lengthsq(aim) <= 0) return null;
var axis = normalize(aim);
FloatingItem best = null;
var bestOff = float.MaxValue;
foreach (var floating in zone.FloatingItems)
{
if (floating.Holder != null) continue;
var to = floating.Body.Position - origin;
var along = dot(to, axis);
var off = length(to - axis * along);
if (along < 0 || along > range || off > radius) continue;
if (!InBeam(floating.Body.Position, origin, range, radius, out var off)) continue;
if (!Entity.CargoBays.Any(bay => bay.TryFindSpace(floating.Item))) continue;
if (off < bestOff || (off == bestOff && floating.Id.Value < best.Id.Value))
{
best = floating;