Aetheria/Assets/Scripts/UI/FieldTester.cs
Meta Vens a345bd3dcb Loot 3 fix1: the loot-3 findings' fixtures, and the held-pose curve has one owner
PickupTests: TheBeamTakesFromWhicheverBayCanStore is a theory over which bay is full.
CapabilityPresentationSmoke: tracked ship's hull hit is drawn; a beam above the held point stays on the ground plane; a grab at the beam keeps its aim; the other entity's grab reaches its own bound beam and not this one.
FloatingItemsDrawnSmoke: a held item's label comes up; the test scene's stand-in pose equals the zone's held pose.
Zone.HeldPull is the one smoothstep of the held pose; FieldDriver's tendril and FieldTester's stand-in call it (FieldTester.PulledPosition, PullProgress), deleting the two copies.
All commits build (Unity compile and both smokes green).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:25:49 +02:00

211 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using UnityEngine;
using CultMath;
using CultMath.UnityBridge;
using static CultMath.math;
using Random = UnityEngine.Random;
// Drives the field ONLY by publishing capability events into a CapabilityEvents source that
// CapabilityPresentationBinder (a component alongside FieldDriver) feeds to it. This scene stands in
// for the capability owner the simulation does not have yet (docs/headless-playground-cut.md fork L:
// "Simulation emits capability events; presentations subscribe and decide nothing"). FieldTester
// itself decides nothing about how those events render -- that stays FieldDriver's job as a presenter.
public class FieldTester : MonoBehaviour
{
public int PickupCount;
public ItemPickup[] PickupPrefabs;
public float PickupTravelDistance;
public float PickupTravelTimeMin;
public float PickupTravelTimeMax;
public float PickupSizeMin;
public float PickupSizeMax;
public float ScaleExponent;
public float PickupSpawnDistanceMin;
public float PickupSpawnDistanceMax;
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;
private float _throttleDecay = 2;
private bool _directionalPush;
// Damped thrust state: previously lived on TestField.Push/FrontTwist/RearTwist and was written
// there directly every frame. Now that FieldTester only ever publishes a ThrustEvent, the damped
// state itself has to live here instead -- FieldDriver's own Push/FrontTwist/RearTwist fields are
// set from the event by FieldDriver.Thrust() and read only for the shader and the properties panel.
private float2 _push;
private float _frontTwist;
private float _rearTwist;
private CapabilityEvents _capabilityEvents;
// Cycles through every DamageType on each absorbed hit so the operator can see how a presenter
// that varies by type would behave; the field shield presentation itself ignores it today.
private static readonly DamageType[] DamageTypeCycle = (DamageType[])Enum.GetValues(typeof(DamageType));
private int _nextDamageTypeIndex;
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();
_input.Player.Enable();
_capabilityEvents = new CapabilityEvents();
var binder = TestField.GetComponent<CapabilityPresentationBinder>();
if (binder == null)
Debug.LogError("FieldTester expects a CapabilityPresentationBinder on TestField's GameObject; no capability events will reach the field.");
else
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.
var clickableCollider = TestField.GetComponent<ClickableCollider>();
if (clickableCollider != null)
{
clickableCollider.OnClick += (_, _, ray, hit) =>
{
var damageType = DamageTypeCycle[_nextDamageTypeIndex];
_nextDamageTypeIndex = (_nextDamageTypeIndex + 1) % DamageTypeCycle.Length;
_capabilityEvents.PublishAbsorb(new AbsorbEvent(hit.point.ToCultMath(), ray.direction.ToCultMath(), TestField.TestMagnitude, damageType, AbsorbLayer.Shield));
};
}
Properties.AddField("Time Scale", () => Time.timeScale, f => Time.timeScale = f, 0, 2);
Properties.AddField("FOV", () => Camera.fieldOfView, f => Camera.fieldOfView = f, 15, 45);
Properties.AddButton("Melee", () => _capabilityEvents.PublishMelee(
new MeleeEvent(float3(0, 0, 1), TestField.MeleeRange, TestField.MeleeAngle, TestField.MeleeDuration)));
Properties.AddField("Force Thrust", () => _forceThrust, b => _forceThrust = b);
Properties.AddField("Directional Push", () => _directionalPush, b => _directionalPush = b);
Properties.AddField("Throttle Decay", () => _throttleDecay, f => _throttleDecay = f);
Properties.Inspect(TestField, true, true);
Properties.AddProperty("Current Hits", () => TestField.HitCount.ToString());
Properties.AddProperty("Push X", () => $"{(int)(TestField.Push.x * 100)}%");
Properties.AddProperty("Push Y", () => $"{(int)(TestField.Push.y * 100)}%");
Properties.AddProperty("Front Twist", () => $"{(int)(TestField.FrontTwist * 100)}%");
Properties.AddProperty("Rear Twist", () => $"{(int)(TestField.RearTwist * 100)}%");
for (int i = 0; i < PickupCount; i++)
{
var l = (float)i / PickupCount;
var pickup = Instantiate(PickupPrefabs[(int)(l * PickupPrefabs.Length)], transform);
var i1 = i;
var click = pickup.GetComponent<ClickableCollider>();
click.OnClick += (collider, data, ray, hit) =>
{
if (_grabbing) return;
click.Clear();
_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);
pickup.enabled = false;
var pickupTransform = pickup.transform;
var time = Random.value;
var circle = Random.insideUnitCircle.normalized * Random.Range(PickupSpawnDistanceMin,PickupSpawnDistanceMax);
pickupTransform.position = new Vector3(circle.x,circle.y, (time-.5f)*PickupTravelDistance);
_pickups.Add((l, time, pickupTransform));
}
}
// 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;
pickup.position = PulledPosition(_grabStart, TestField.transform.position.ToCultMath(), _grabElapsed,
GrabExtendTime, GrabEnvelopTime, GrabPullTime).ToUnity();
if (_pullPublished && PullProgress(_grabElapsed, GrabExtendTime, GrabEnvelopTime, GrabPullTime) >= 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;
}
}
// How far along the pull a grab is `elapsed` seconds in: 0 through Extend and Envelop, then 0..1 over the pull.
public static float PullProgress(float elapsed, float extend, float envelop, float pull) =>
saturate((elapsed - extend - envelop) / max(pull, 1e-4f));
// Where the stand-in sim holds its pickup: still through Extend and Envelop, then along Zone's held-pose curve.
public static float3 PulledPosition(float3 start, float3 holder, float elapsed, float extend, float envelop, float pull) =>
lerp(start, holder, Zone.HeldPull(PullProgress(elapsed, extend, envelop, pull)));
private void Update()
{
var move = _forceThrust ? float2(0,1) : _input.Player.Move.ReadValue<Vector2>().ToCultMath();
var turn = _input.Player.Turn.ReadValue<float>();
float2 targetPush;
float targetFrontTwist;
float targetRearTwist;
if (_directionalPush)
{
targetPush = move;
targetFrontTwist = turn;
targetRearTwist = turn;
}
else
{
targetPush = float2(0, move.y);
targetFrontTwist = clamp(turn + move.x, -1, 1) * (1 + min(move.y, 0));
targetRearTwist = clamp(turn - move.x, -1, 1) * (1 + min(-move.y, 0));
}
_push = damp(_push, targetPush, _throttleDecay, Time.deltaTime);
_frontTwist = damp(_frontTwist, targetFrontTwist, _throttleDecay, Time.deltaTime);
_rearTwist = damp(_rearTwist, targetRearTwist, _throttleDecay, Time.deltaTime);
_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 || (_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);
_pickups[i] = (l, time, t);
}
}
}