Loot 3 group A: the grab is presented, not simulated

IGrabPresenter.Grab(GrabEvent) only; the binder's Transform and velocity resolvers go. TractorBeam
presents a running grab (aims at Started's TargetPosition, emits at Power, dark otherwise). FieldDriver
draws its tendril from GrabEvents alone with a real-time phase clock over each event's PhaseDuration and
writes no transform and destroys nothing; its grab times move to FieldTester, now the scene's stand-in sim
that owns its pickup's timeline and pose. ItemPickup keeps its label lit while held. ImpactPresenterSmoke
becomes CapabilityPresentationSmoke with a grab section; its two untracked-ship expectations follow
DrawsAt since contact-hud. Builds (Unity 6000.3.24f1 batchmode: smoke ok, scene opens, no missing script).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 18:43:15 +02:00
commit 48b42992ee
9 changed files with 232 additions and 166 deletions

View file

@ -4818,6 +4818,3 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
ParticleSystem: {fileID: 5087162364086924483}
Radius: 25
Traction: 25
Distance: 75

View file

@ -811,6 +811,9 @@ MonoBehaviour:
Camera: {fileID: 1153356615}
TestField: {fileID: 909026488}
Properties: {fileID: 401779935}
GrabExtendTime: 1.5
GrabEnvelopTime: 0.75
GrabPullTime: 1.5
--- !u!4 &699508827
Transform:
m_ObjectHideFlags: 0
@ -1116,9 +1119,6 @@ MonoBehaviour:
TendrilBaseDamping: 2
TendrilTipRadiusAnimationExponent: 2
TendrilFadeExponent: 2
GrabExtendTime: 1.5
GrabEnvelopTime: 0.75
GrabPullTime: 1.5
RefractionAxis: 1
--- !u!114 &909026489
MonoBehaviour:

View file

@ -5,24 +5,27 @@ using System.Reflection;
using GameCult.Caching;
using UnityEditor;
using UnityEngine;
using CultMath;
using static CultMath.math;
// Batch proof of ImpactPresenter's wiring, without a scene: a zone ship is bound to an EntityInstance through the game's
// own SetEntity, and impacts are published on the ship's Capabilities exactly as FireControl does. A hull absorb spawns
// Batch proof of the capability presenters' wiring, without a scene: a zone ship is bound to an EntityInstance through the
// game's own SetEntity, and impacts and grabs are published on the ship's Capabilities exactly as the simulation does. A hull absorb spawns
// one hull effect under the instance and, for the player's ship at ScreenShake 1, raises one camera impulse; ScreenShake 0
// or another ship raises none; a shield absorb spawns no hull effect. The shield ripple itself (ShieldManager.ShowHit
// needs a shield material and a rig) is looked at in the editor, not here.
// needs a shield material and a rig) is looked at in the editor, not here. Grabs: a Tractor Beam prefab under the instance
// is bound by the same SetEntity and presents a grab from events alone (emission at Power and the aim at the held point
// from Started to Completed or Cancelled, nothing otherwise, nothing for another entity's grab).
//
// Unity -batchmode -nographics -projectPath <project> -executeMethod ImpactPresenterSmoke.Run -logFile <log>
// Unity -batchmode -nographics -projectPath <project> -executeMethod CapabilityPresentationSmoke.Run -logFile <log>
//
// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
public static class ImpactPresenterSmoke
public static class CapabilityPresentationSmoke
{
private const BindingFlags Any = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
public static void Run()
{
var work = Path.Combine(Path.GetTempPath(), $"impact-presenter-smoke-{Guid.NewGuid():N}");
var work = Path.Combine(Path.GetTempPath(), $"capability-presentation-smoke-{Guid.NewGuid():N}");
GameObject[] objects = null;
try
{
@ -86,6 +89,12 @@ public static class ImpactPresenterSmoke
instance.ThrusterHardpoints = new ThrusterHardpoint[0];
instance.WeaponHardpoints = new WeaponHardpoint[0];
instance.ArticulationPoints = new ArticulationPoint[0];
// The beam is a child before SetEntity on both ship paths, so the binder finds it as the game does.
var beamPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Content/Prefabs/Tractor Beam.prefab")
?? throw new InvalidOperationException("No Tractor Beam prefab.");
var beam = ((GameObject)PrefabUtility.InstantiatePrefab(beamPrefab, instanceObject.transform)).GetComponent<TractorBeam>();
beam.Power = .8f;
beam.Direction = Vector3.forward;
instance.SetEntity(null, ship);
int Effects() => instance.GetComponentsInChildren<Prototype>(true).Count(p => p != null && !p.isOriginalPrototype);
@ -111,13 +120,47 @@ public static class ImpactPresenterSmoke
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f;
manager.CurrentEntity = other;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("hull hit on another ship", 3, 1);
// An untracked ship is not drawn at (ActionGameManager.DrawsAt, since contact-hud): no hull effect, no impulse.
Expect("hull hit on an untracked ship", 2, 1);
manager.CurrentEntity = ship;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Shield));
Expect("player's shield hit", 3, 2);
Expect("player's shield hit", 2, 2);
Debug.Log("IMPACT_PRESENTER_SMOKE ok effects=3 impulses=2");
// Grabs: the beam presents the simulation's events and nothing else.
float Emission() => beam.ParticleSystem.emission.rateOverTimeMultiplier;
void ExpectBeam(string what, float emission, bool towardHeld)
{
beam.Update();
var forward = beam.transform.forward;
var aimed = towardHeld ? forward.x > .999f : forward.z > .999f;
if (abs(Emission() - emission) > 1e-4f || !aimed)
throw new InvalidOperationException($"{what}: emission {Emission()} (want {emission}), forward {forward} (want {(towardHeld ? "+x" : "+z")}).");
}
GrabEvent Grab(GrabPhase phase, float3 at) => new GrabEvent(new FloatingBodyId(7), phase, at, 1f);
ExpectBeam("idle beam", 0f, false);
ship.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
ExpectBeam("grab started", .8f, true);
foreach (var phase in new[] { GrabPhase.Extend, GrabPhase.Envelop, GrabPhase.Pull })
{
ship.Capabilities.PublishGrab(Grab(phase, float3(20f, 0f, 0f)));
ExpectBeam($"grab {phase}", .8f, true);
}
ship.Capabilities.PublishGrab(Grab(GrabPhase.Completed, float3(20f, 0f, 0f)));
ExpectBeam("grab completed", 0f, false);
// A grab cancelled after its item vanished carries no position: the beam goes dark and never turns toward it.
ship.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
ExpectBeam("second grab started", .8f, true);
ship.Capabilities.PublishGrab(Grab(GrabPhase.Cancelled, float3(0f, 0f, 0f)));
ExpectBeam("grab cancelled", 0f, false);
// The beam belongs to the ship's entity: another entity's grab is no business of its own.
other.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
ExpectBeam("another entity's grab", 0f, false);
Debug.Log("CAPABILITY_PRESENTATION_SMOKE ok effects=2 impulses=2 grabs=ok");
return new[] { feedbackObject, effectObject, managerObject, instanceObject };
}

View file

@ -8,27 +8,18 @@ using UnityEngine;
// playground-cut.md fork L: "presentation is chosen per item, independently of capability... one
// presentation... can visualize any subset of an item's capabilities"). This binder has no knowledge
// of FieldDriver or any other concrete presentation, and no knowledge of what publishes events --
// today the FieldShieldTest scene calls Bind() directly; a real capability owner will do the same
// later without this binder changing.
// EntityInstance binds every entity's events, and the FieldShieldTest scene binds its stand-in sim's the same way.
public class CapabilityPresentationBinder : MonoBehaviour
{
private readonly List<IDisposable> _subscriptions = new List<IDisposable>();
private Func<FloatingBodyId, Transform> _grabTargetResolver;
private Func<FloatingBodyId, Vector3> _grabInitialVelocityResolver;
// Subscribes every presenter interface implemented by components on this object or its children to
// the matching stream of `events`. `grabTargetResolver`/`grabInitialVelocityResolver` resolve a
// GrabEvent's target id to the live Transform/velocity a grab presenter needs; the owner
// of `events` supplies them (the test scene today; the real pickup capability's caller later).
public void Bind(CapabilityEvents events, Func<FloatingBodyId, Transform> grabTargetResolver = null,
Func<FloatingBodyId, Vector3> grabInitialVelocityResolver = null)
// the matching stream of `events`.
public void Bind(CapabilityEvents events)
{
Unbind();
if (events == null) return;
_grabTargetResolver = grabTargetResolver;
_grabInitialVelocityResolver = grabInitialVelocityResolver;
foreach (var presenter in GetComponentsInChildren<IAbsorbPresenter>(true))
{
var p = presenter;
@ -38,12 +29,7 @@ public class CapabilityPresentationBinder : MonoBehaviour
foreach (var presenter in GetComponentsInChildren<IGrabPresenter>(true))
{
var p = presenter;
_subscriptions.Add(events.Grab.Subscribe(e =>
{
var target = _grabTargetResolver?.Invoke(e.Target);
var velocity = _grabInitialVelocityResolver?.Invoke(e.Target) ?? Vector3.zero;
p.Grab(e, target, velocity);
}));
_subscriptions.Add(events.Grab.Subscribe(e => p.Grab(e)));
}
foreach (var presenter in GetComponentsInChildren<IMeleePresenter>(true))
@ -63,8 +49,6 @@ public class CapabilityPresentationBinder : MonoBehaviour
{
foreach (var s in _subscriptions) s.Dispose();
_subscriptions.Clear();
_grabTargetResolver = null;
_grabInitialVelocityResolver = null;
}
private void OnDisable() => Unbind();

View file

@ -10,13 +10,10 @@ public interface IAbsorbPresenter
void Absorb(AbsorbEvent e);
}
// Grab needs a live Transform to animate, which the engine-free GrabEvent cannot carry (its
// Target is a plain identity, not a Unity reference). The binder resolves the id through a
// delegate supplied by the event source's owner and hands the presenter the result, along with the
// initial velocity the target was already moving with (also owner-supplied, for the same reason).
// Presenters read the event and the simulated pose, never a Transform the sim does not own.
public interface IGrabPresenter
{
void Grab(GrabEvent e, Transform target, Vector3 initialVelocity);
void Grab(GrabEvent e);
}
public interface IMeleePresenter

View file

@ -63,11 +63,7 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
// public float GrabScaleAnimationExponent = .5f;
[Inspectable]
public float GrabExtendTime;
[Inspectable]
public float GrabEnvelopTime;
[Inspectable]
public float GrabPullTime;
public float TendrilTipScale = 1;
[Inspectable]
public RectTransform.Axis RefractionAxis = RectTransform.Axis.Horizontal;
@ -81,29 +77,17 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
private float _meleeTime;
private bool _meleeActive;
private MeshCollider _collider;
private Transform _grabObject;
private Vector3 _grabObjectStartPos;
private Vector3 _grabObjectStartTendrilBasePos;
private Vector3 _grabObjectEndPos;
private Vector3 _grabObjectVelocity;
private float _grabObjectScale;
private bool _grabbing;
private GrabPhase _grabPhase;
private float _grabTime;
private TendrilPhase _grabPhase;
private float _phaseDuration;
private Vector3 _grabStart;
private Vector3 _grabStartTendrilBase;
private Vector3 _tendrilBasePos;
private Vector3 _tendrilBendTarget;
private Vector3 _tendrilTargetPos;
private float _fadePoint;
// Named distinctly from the global GrabPhase (CapabilityEvents.cs): this is FieldDriver's own
// internal tendril-animation timing, still owned here until a real pickup capability exists to
// publish per-phase GrabEvent transitions (see Grab(GrabEvent, Transform, Vector3) below).
private enum TendrilPhase
{
Extend,
Envelop,
Pull
}
private struct FieldHit
{
public float3 Position;
@ -181,32 +165,41 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
_meleeTime = 0;
}
public bool CanGrab => _grabObject == null;
public void GrabObject(Transform t, Vector3 v)
{
_grabPhase = TendrilPhase.Extend;
_grabObject = t;
_grabObjectStartPos = t.position;
_grabObjectVelocity = v;
_grabObjectScale = t.localScale.x;
_tendrilBasePos = _grabObjectStartTendrilBasePos = _collider.ClosestPoint(_grabObjectStartPos);
_fadePoint = .999f;
}
// IAbsorbPresenter: delegates straight to the existing hit-buffer mechanism. e.DamageType is not
// read -- the field shield has no per-damage-type visual today; it rides along in the event for
// presentations that do vary by type (the mask itself is absorb-capability data, out of scope here).
public void Absorb(AbsorbEvent e) => AddHit(e.Position, e.Direction, e.Magnitude);
// IGrabPresenter: only the Started phase does anything today. FieldDriver still times the
// extend/envelop/pull animation itself (TendrilPhase above, driven by GrabExtendTime/
// GrabEnvelopTime/GrabPullTime) because no capability yet publishes those phase transitions;
// once one does, this is where FieldDriver would react to Envelop/Pull/Completed/Cancelled too.
public void Grab(GrabEvent e, Transform target, Vector3 initialVelocity)
// IGrabPresenter: the simulation owns the grab; this draws the tendril from the events alone. Started records
// where the item is held and where the tendril leaves the field; Extend, Envelop and Pull set the phase and
// restart the real-time phase clock over the event's duration (it clamps at 1 until the next event, so a
// slowed simulation stretches the wait, not the animation); Completed and Cancelled end it.
public void Grab(GrabEvent e)
{
if (e.Phase == GrabPhase.Started && target != null)
GrabObject(target, initialVelocity);
switch (e.Phase)
{
case GrabPhase.Started:
_grabbing = true;
_grabPhase = GrabPhase.Started;
_grabStart = e.TargetPosition.ToUnity();
_tendrilBasePos = _grabStartTendrilBase = _collider.ClosestPoint(_grabStart);
_tendrilTargetPos = _grabStart;
_fadePoint = .999f;
break;
case GrabPhase.Extend:
case GrabPhase.Envelop:
case GrabPhase.Pull:
if (!_grabbing) break;
_grabPhase = e.Phase;
_grabTime = 0;
_phaseDuration = e.PhaseDuration;
break;
case GrabPhase.Completed:
case GrabPhase.Cancelled:
_grabbing = false;
_field.SetFloat("_TendrilInfluence", 0);
break;
}
}
// IMeleePresenter: the field shield's swing shape comes from its own authored curves
@ -277,74 +270,47 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
}
if(_grabObject != null)
if (_grabbing && _grabPhase != GrabPhase.Started)
{
_grabTime += Time.deltaTime / _grabPhase switch
_grabTime = min(1, _grabTime + Time.deltaTime / max(_phaseDuration, 1e-4f));
_tendrilBasePos = damp(_tendrilBasePos.ToCultMath(), _tendrilTargetPos.ToCultMath(), TendrilBaseDamping, Time.deltaTime).ToUnity();
switch (_grabPhase)
{
TendrilPhase.Extend => GrabExtendTime,
TendrilPhase.Envelop => GrabEnvelopTime,
TendrilPhase.Pull => GrabPullTime,
_ => throw new ArgumentOutOfRangeException()
};
if (_grabTime > 1)
{
_grabTime = 0;
if (_grabPhase == TendrilPhase.Envelop)
{
_grabObjectEndPos = _grabObject.position;
}
if (_grabPhase == TendrilPhase.Pull)
{
Destroy(_grabObject.gameObject);
_grabObject = null;
_field.SetFloat("_TendrilInfluence", 0);
}
else _grabPhase++;
}
if (_grabObject != null)
{
_tendrilBasePos = damp(_tendrilBasePos.ToCultMath(), _grabObject.position.ToCultMath(), TendrilBaseDamping, Time.deltaTime).ToUnity();
switch (_grabPhase)
{
case TendrilPhase.Extend:
_tendrilBendTarget = _grabObjectStartPos;
_grabObject.position += _grabObjectVelocity * Time.deltaTime;
_tendrilTargetPos = lerp(
lerp(_grabObjectStartTendrilBasePos.ToCultMath(), _grabObjectStartPos.ToCultMath(), _grabTime),
lerp(_grabObjectStartPos.ToCultMath(), _grabObject.position.ToCultMath(), _grabTime),
pow(_grabTime, TendrilExtensionExponent)).ToUnity();
_field.SetFloat("_TendrilInfluence", pow(_grabTime, TendrilExtendBaseAnimationExponent));
_field.SetFloat("_TendrilSize", lerp(TendrilBaseRadius/2,TendrilBaseRadius,pow(_grabTime, TendrilExtendBaseAnimationExponent)));
_field.SetFloat("_TendrilRadius", _grabObjectScale * pow(_grabTime, TendrilTipRadiusAnimationExponent) * TendrilTipRadius);
break;
case TendrilPhase.Envelop:
_tendrilBendTarget = _grabObjectStartPos;
_tendrilTargetPos = _grabObject.position += _grabObjectVelocity * Time.deltaTime * (1-_grabTime);
_field.SetFloat("_TendrilInfluence", 1);
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
_field.SetFloat("_TendrilRadius", _grabObjectScale * TendrilTipRadius);
break;
case TendrilPhase.Pull:
_tendrilTargetPos = _grabObject.position = lerp(_grabObjectEndPos.ToCultMath(), transform.position.ToCultMath(), _grabTime*_grabTime).ToUnity();
_tendrilBendTarget = lerp(_grabObjectStartPos.ToCultMath(), _grabObjectEndPos.ToCultMath(), _grabTime*_grabTime).ToUnity();
if (_fadePoint > .99 && transform.InverseTransformPoint(_grabObject.position).sqrMagnitude < 1)
_fadePoint = _grabTime;
_field.SetFloat("_TendrilInfluence", pow(smoothstep(1, _fadePoint, _grabTime), TendrilFadeExponent));
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
_field.SetFloat("_TendrilRadius", _grabObjectScale * TendrilTipRadius);
break;
default:
throw new ArgumentOutOfRangeException();
}
var tendrilRadii = EnvelopeRadiiOrFallback();
_field.SetVector("_TendrilBase", Vector3.Scale(transform.InverseTransformPoint(_tendrilBasePos).normalized, tendrilRadii));
_field.SetVector("_TendrilBend", Vector3.Scale(transform.InverseTransformPoint(_tendrilBendTarget), tendrilRadii));
_field.SetVector("_TendrilTarget", Vector3.Scale(transform.InverseTransformPoint(_tendrilTargetPos), tendrilRadii));
case GrabPhase.Extend:
_tendrilBendTarget = _grabStart;
_tendrilTargetPos = lerp(
lerp(_grabStartTendrilBase.ToCultMath(), _grabStart.ToCultMath(), _grabTime),
_grabStart.ToCultMath(),
pow(_grabTime, TendrilExtensionExponent)).ToUnity();
_field.SetFloat("_TendrilInfluence", pow(_grabTime, TendrilExtendBaseAnimationExponent));
_field.SetFloat("_TendrilSize", lerp(TendrilBaseRadius/2,TendrilBaseRadius,pow(_grabTime, TendrilExtendBaseAnimationExponent)));
_field.SetFloat("_TendrilRadius", TendrilTipScale * pow(_grabTime, TendrilTipRadiusAnimationExponent) * TendrilTipRadius);
break;
case GrabPhase.Envelop:
_tendrilBendTarget = _grabStart;
_tendrilTargetPos = _grabStart;
_field.SetFloat("_TendrilInfluence", 1);
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
_field.SetFloat("_TendrilRadius", TendrilTipScale * TendrilTipRadius);
break;
case GrabPhase.Pull:
// The same curve as Zone's held pose (Zone.cs): the tip rides the item the simulation pulls in.
_tendrilTargetPos = lerp(_grabStart.ToCultMath(), transform.position.ToCultMath(), smoothstep(0, 1, _grabTime)).ToUnity();
_tendrilBendTarget = _grabStart;
if (_fadePoint > .99 && transform.InverseTransformPoint(_tendrilTargetPos).sqrMagnitude < 1)
_fadePoint = _grabTime;
_field.SetFloat("_TendrilInfluence", pow(smoothstep(1, _fadePoint, _grabTime), TendrilFadeExponent));
_field.SetFloat("_TendrilSize", TendrilBaseRadius);
_field.SetFloat("_TendrilRadius", TendrilTipScale * TendrilTipRadius);
break;
}
var tendrilRadii = EnvelopeRadiiOrFallback();
_field.SetVector("_TendrilBase", Vector3.Scale(transform.InverseTransformPoint(_tendrilBasePos).normalized, tendrilRadii));
_field.SetVector("_TendrilBend", Vector3.Scale(transform.InverseTransformPoint(_tendrilBendTarget), tendrilRadii));
_field.SetVector("_TendrilTarget", Vector3.Scale(transform.InverseTransformPoint(_tendrilTargetPos), tendrilRadii));
}
}
private float almostIdentity( float x )
{
return x*x*(2.0f-x);
@ -358,7 +324,7 @@ public class FieldDriver : MonoBehaviour, IAbsorbPresenter, IGrabPresenter, IMel
private const int GIZMO_STEPS = 16;
private void OnDrawGizmosSelected()
{
if (_grabObject != null)
if (_grabbing)
{
Vector3 previous = _tendrilBasePos;
for (int i = 1; i <= GIZMO_STEPS; i++)

View file

@ -58,6 +58,7 @@ public class ItemPickup : MonoBehaviour
var viewAngle = acos(dot(toThis, ViewDirection)) * Mathf.Rad2Deg;
if (length(diff) < LabelDisplayMaxDistance && viewAngle < LabelDisplayAngle)
_displayTime = Time.time;
if (_floating?.Holder != null) _displayTime = Time.time;
var targetAlpha = Time.time - _displayTime < LabelPersistDuration ? 1 : 0;
var c = ScanLabel.color;
c.a = c.a + sign(targetAlpha - c.a) * (Time.deltaTime / LabelFadeDuration);

View file

@ -1,22 +1,51 @@
using System;
using UnityEngine;
using CultMath.UnityBridge;
public class TractorBeam : MonoBehaviour
// Presents the ship's grab: while the simulation holds an item, the beam looks along the ground-plane vector to the
// point the item was held from and emits at Power. Zone pulls the held item along the segment from the ship to that
// point (docs/aetheria-release-map.md L11), which is why GrabEvent.TargetPosition on Started is the exact aim and no
// pose lookup is needed. Idle, the beam turns with Direction and emits nothing. The simulation decides every grab fact.
public class TractorBeam : MonoBehaviour, IGrabPresenter
{
public ParticleSystem ParticleSystem;
public float Radius;
public float Traction;
public float Distance;
public float Power { get; set; }
public Vector3 Direction { get; set; }
private bool _grabbing;
private Vector3 _heldPoint;
private Vector3 _heldDirection;
// Cancelled can carry no position (a vanished item), so only Started is read for one.
public void Grab(GrabEvent e)
{
switch (e.Phase)
{
case GrabPhase.Started:
_grabbing = true;
_heldPoint = e.TargetPosition.ToUnity();
_heldDirection = Direction;
break;
case GrabPhase.Completed:
case GrabPhase.Cancelled:
_grabbing = false;
break;
}
}
public void Update()
{
transform.rotation = Quaternion.LookRotation(Direction);
// The beam draws nothing until it presents a sim grab (loot-3): no particles, and no pull; the simulation
// decides every floating item's motion.
var look = Direction;
if (_grabbing)
{
var toHeld = _heldPoint - transform.position;
toHeld.y = 0;
if (toHeld.sqrMagnitude > 1e-6f) _heldDirection = toHeld;
look = _heldDirection;
}
transform.rotation = Quaternion.LookRotation(look);
var emission = ParticleSystem.emission;
emission.rateOverTimeMultiplier = 0;
emission.rateOverTimeMultiplier = _grabbing ? Power : 0;
}
}
}

View file

@ -26,6 +26,10 @@ public class FieldTester : MonoBehaviour
public Camera Camera;
public FieldDriver TestField;
public PropertiesPanel Properties;
// The scene's slow viewing timings for a grab, published as the events' phase durations.
public float GrabExtendTime;
public float GrabEnvelopTime;
public float GrabPullTime;
private AetheriaInput _input;
private bool _forceThrust;
@ -49,6 +53,14 @@ public class FieldTester : MonoBehaviour
private List<(float lerp, float time, Transform transform)> _pickups = new List<(float lerp, float time, Transform transform)>();
// This scene stands in for the simulation, so it owns its pickup's grab timeline and pose: one grab at a time.
private bool _grabbing;
private int _grabIndex;
private float _grabElapsed;
private float3 _grabStart;
private bool _envelopPublished;
private bool _pullPublished;
private void Start()
{
_input = new AetheriaInput();
@ -59,10 +71,7 @@ public class FieldTester : MonoBehaviour
if (binder == null)
Debug.LogError("FieldTester expects a CapabilityPresentationBinder on TestField's GameObject; no capability events will reach the field.");
else
binder.Bind(_capabilityEvents,
grabTargetResolver: id => (int) id.Value >= 0 && (int) id.Value < _pickups.Count ? _pickups[(int) id.Value].transform : null,
grabInitialVelocityResolver: id => Vector3.forward *
(PickupTravelDistance / lerp(PickupTravelTimeMin, PickupTravelTimeMax, _pickups[(int) id.Value].lerp)));
binder.Bind(_capabilityEvents);
// Impact absorption (fork L): what used to be FieldDriver's own click handler is now this
// scene's stand-in for whatever will eventually detect a hit and publish AbsorbEvent.
@ -99,12 +108,16 @@ public class FieldTester : MonoBehaviour
var click = pickup.GetComponent<ClickableCollider>();
click.OnClick += (collider, data, ray, hit) =>
{
if (!TestField.CanGrab) return;
if (_grabbing) return;
click.Clear();
var p = _pickups[i1];
_capabilityEvents.PublishGrab(new GrabEvent(new FloatingBodyId((uint) i1), GrabPhase.Started, p.transform.position.ToCultMath(), 0));
p.transform = null;
_pickups[i1] = p;
_grabbing = true;
_grabIndex = i1;
_grabElapsed = 0;
_envelopPublished = _pullPublished = false;
_grabStart = _pickups[i1].transform.position.ToCultMath();
var id = new FloatingBodyId((uint) i1);
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Started, _grabStart, 0));
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Extend, _grabStart, GrabExtendTime));
};
pickup.ScanLabelContainer.gameObject.SetActive(false);
@ -117,6 +130,40 @@ public class FieldTester : MonoBehaviour
}
}
// Publishes Envelop and Pull once each at their cumulative boundaries, holds the pickup still until the pull and
// then moves it along the same smoothstep as Zone's held pose, and ends with Completed and the pickup's removal.
private void AdvanceGrab()
{
_grabElapsed += Time.deltaTime;
var id = new FloatingBodyId((uint) _grabIndex);
var pullStart = GrabExtendTime + GrabEnvelopTime;
if (!_envelopPublished && _grabElapsed >= GrabExtendTime)
{
_envelopPublished = true;
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Envelop, _grabStart, GrabEnvelopTime));
}
if (!_pullPublished && _grabElapsed >= pullStart)
{
_pullPublished = true;
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Pull, _grabStart, GrabPullTime));
}
var pickup = _pickups[_grabIndex].transform;
if (_pullPublished)
{
var progress = saturate((_grabElapsed - pullStart) / max(GrabPullTime, 1e-4f));
pickup.position = lerp(_grabStart, TestField.transform.position.ToCultMath(), smoothstep(0, 1, progress)).ToUnity();
if (progress >= 1)
{
_capabilityEvents.PublishGrab(new GrabEvent(id, GrabPhase.Completed, pickup.position.ToCultMath(), 0));
Destroy(pickup.gameObject);
var p = _pickups[_grabIndex];
p.transform = null;
_pickups[_grabIndex] = p;
_grabbing = false;
}
}
}
private void Update()
{
var move = _forceThrust ? float2(0,1) : _input.Player.Move.ReadValue<Vector2>().ToCultMath();
@ -144,10 +191,12 @@ public class FieldTester : MonoBehaviour
_capabilityEvents.PublishThrust(new ThrustEvent(_push, min(length(_push), 1), float2(_frontTwist, _rearTwist)));
if (_grabbing) AdvanceGrab();
for (var i = 0; i < _pickups.Count; i++)
{
var (l, time, t) = _pickups[i];
if (t == null) continue;
if (t == null || (_grabbing && i == _grabIndex)) continue;
time = frac(time + Time.deltaTime / lerp(PickupTravelTimeMin, PickupTravelTimeMax, l));
t.localScale = Vector3.one * lerp(PickupSizeMin, PickupSizeMax, l) * Zone.PowerPulse(time - .5f, ScaleExponent);
t.position = new Vector3(t.position.x, t.position.y, (time - .5f) * PickupTravelDistance);