Unity compile is verified before push; not yet built when committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
171 lines
7.2 KiB
C#
171 lines
7.2 KiB
C#
using System;
|
|
using System.Linq;
|
|
using CultMath;
|
|
using MessagePack;
|
|
using Newtonsoft.Json;
|
|
using static CultMath.math;
|
|
using float2 = CultMath.float2;
|
|
|
|
// The tractor beam: the capability that lets a ship collect floating items. It spins its beam up in sim time toward
|
|
// the pilot's command, and once the beam is up claims the free item nearest its axis and runs one timed grab
|
|
// (extend, envelop, pull) that ends in Zone.TryPickUp, the one commit of a pickup. Its stats are flat
|
|
// (Min == Max), as the other behaviours that carry their own timings.
|
|
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
|
|
public class PickupData : BehaviorData
|
|
{
|
|
// How far along the beam's axis an item can be claimed, and how far off the axis.
|
|
[Inspectable, JsonProperty("range"), Key(1), RuntimeInspectable]
|
|
public PerformanceStat Range = new PerformanceStat();
|
|
|
|
[Inspectable, JsonProperty("beamRadius"), Key(2), RuntimeInspectable]
|
|
public PerformanceStat BeamRadius = new PerformanceStat();
|
|
|
|
// The spin-up (0..1) needed to claim an item, and to keep a grab going.
|
|
[Inspectable, JsonProperty("grabPower"), Key(3), RuntimeInspectable]
|
|
public PerformanceStat GrabPower = new PerformanceStat();
|
|
|
|
// Seconds of each phase of a grab.
|
|
[Inspectable, JsonProperty("extendTime"), Key(4), RuntimeInspectable]
|
|
public PerformanceStat ExtendTime = new PerformanceStat();
|
|
|
|
[Inspectable, JsonProperty("envelopTime"), Key(5), RuntimeInspectable]
|
|
public PerformanceStat EnvelopTime = new PerformanceStat();
|
|
|
|
[Inspectable, JsonProperty("pullTime"), Key(6), RuntimeInspectable]
|
|
public PerformanceStat PullTime = new PerformanceStat();
|
|
|
|
// Spin-up and spin-down, in power per sim second.
|
|
[Inspectable, JsonProperty("spinRate"), Key(7), RuntimeInspectable]
|
|
public PerformanceStat SpinRate = new PerformanceStat();
|
|
|
|
public override Behavior CreateInstance(EquippedItem item)
|
|
{
|
|
return new Pickup(this, item);
|
|
}
|
|
|
|
// A beam is a piece of gear the pilot holds on; no consumable carries one.
|
|
public override Behavior CreateInstance(ConsumableItemEffect item) =>
|
|
throw new NotSupportedException("pickup: gear only");
|
|
}
|
|
|
|
// Owns this ship's beam power and its one grab. Power is written only here; Zone owns the held item's pose.
|
|
public class Pickup : Behavior, IAlwaysUpdatedBehavior
|
|
{
|
|
private readonly PickupData _data;
|
|
// Power and the grab clock are kept in double so a fixed tick count lands on the same side of a boundary
|
|
// whatever dt the zone steps by.
|
|
private double _power;
|
|
private bool _grabbing;
|
|
private FloatingBodyId _target;
|
|
private double _elapsed;
|
|
// How many of Envelop and Pull have been published so far (0, 1 or 2).
|
|
private int _published;
|
|
|
|
public Pickup(PickupData data, EquippedItem item) : base(data, item)
|
|
{
|
|
_data = data;
|
|
}
|
|
|
|
// The beam's spun-up state in [0, 1]: what the pilot's command has driven it to.
|
|
public float Power => (float) _power;
|
|
|
|
public void Update(float dt)
|
|
{
|
|
var active = Item.Active.Value;
|
|
var command = active ? Math.Clamp(Entity.TractorCommand, 0f, 1f) : 0f;
|
|
var step = (double) Evaluate(_data.SpinRate) * dt;
|
|
_power = Math.Clamp(_power + Math.Clamp(command - _power, -step, step), 0.0, 1.0);
|
|
|
|
var zone = Entity.Zone;
|
|
var grabPower = Evaluate(_data.GrabPower);
|
|
var extend = (double) Evaluate(_data.ExtendTime);
|
|
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, radius);
|
|
if (item == null || !zone.TryHold(Entity, item.Id)) return;
|
|
_grabbing = true;
|
|
_target = item.Id;
|
|
_elapsed = 0;
|
|
_published = 0;
|
|
Publish(GrabPhase.Started, item, 0);
|
|
Publish(GrabPhase.Extend, item, (float) extend);
|
|
return;
|
|
}
|
|
|
|
var held = zone.FloatingItems.FirstOrDefault(f => f.Id.Equals(_target));
|
|
if (held == null || !ReferenceEquals(held.Holder, Entity))
|
|
{
|
|
_grabbing = false;
|
|
Entity.Capabilities.PublishGrab(new GrabEvent(_target, GrabPhase.Cancelled, float3(0), 0));
|
|
return;
|
|
}
|
|
|
|
var pulling = _elapsed >= extend + envelop;
|
|
if (!active || _power < grabPower || (!pulling && !InBeam(held.Body.Position, origin, range, radius, out _)))
|
|
{
|
|
_grabbing = false;
|
|
zone.Free(_target);
|
|
Publish(GrabPhase.Cancelled, held, 0);
|
|
return;
|
|
}
|
|
|
|
_elapsed += dt;
|
|
if (_published < 1 && _elapsed >= extend) { _published = 1; Publish(GrabPhase.Envelop, held, (float) envelop); }
|
|
if (_published < 2 && _elapsed >= extend + envelop) { _published = 2; Publish(GrabPhase.Pull, held, (float) pull); }
|
|
if (_published == 2)
|
|
zone.SetHeldProgress(_target, pull > 0 ? (float) ((_elapsed - extend - envelop) / pull) : 1f);
|
|
if (_elapsed < extend + envelop + pull) return;
|
|
|
|
_grabbing = false;
|
|
if (zone.TryPickUp(Entity, _target)) Publish(GrabPhase.Completed, held, 0);
|
|
else
|
|
{
|
|
zone.Free(_target);
|
|
Publish(GrabPhase.Cancelled, held, 0);
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
FloatingItem best = null;
|
|
var bestOff = float.MaxValue;
|
|
foreach (var floating in zone.FloatingItems)
|
|
{
|
|
if (floating.Holder != null) 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;
|
|
bestOff = off;
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
private void Publish(GrabPhase phase, FloatingItem item, float duration) =>
|
|
Entity.Capabilities.PublishGrab(new GrabEvent(item.Id, phase, float3(item.Body.Position.x, DrawAhead.FloaterHeight(Entity.Zone, Entity.ItemManager.GameplaySettings, item.Body.Position, 0), item.Body.Position.y), duration));
|
|
}
|