GrabEvent carries a FloatingBodyId; Zone gains TryHold/SetHeldProgress/Free and places a held item instead of integrating it; Entity.TractorPower is derived from Pickup.Power and TractorCommand is the pilot's input (ActionGameManager writes it undamped, the Time.deltaTime ramp is deleted). Binder and FieldTester change the delegate type only. PickupTests and a save round-trip pin for the player's zone index. Group A builds; no beam is on any generated ship until group B. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
5.2 KiB
C#
128 lines
5.2 KiB
C#
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
using System;
|
|
using UniRx;
|
|
using CultMath;
|
|
|
|
// Capability event contract (docs/headless-playground-cut.md fork L, operator ruling 2026-09-17):
|
|
// pickup, impact absorption, melee blade and propulsion are separate item capabilities. Simulation owns
|
|
// each capability's rules and emits these events (FireControl publishes Absorb; the others are not yet
|
|
// built); any number of presentations (the field shield today, later an IFS shatter plane or an advected
|
|
// particle field) subscribe to the event kinds they visualize and decide nothing about the rule. This file
|
|
// has no engine dependency: it compiles into the headless Aetheria.Shared build (see Aetheria.Shared.csproj).
|
|
|
|
// Which layer of the receiver took an absorbed impact.
|
|
public enum AbsorbLayer { Shield, Hull }
|
|
|
|
// One absorbed impact on a shield or a hull, published once per receiver by FireControl on that entity's
|
|
// Entity.Capabilities. Position is a world point on the receiver's sim pose at the tick the impact lands (a
|
|
// presenter draws it with DrawAhead.Carried); Direction is the way the impact travels. DamageType rides along
|
|
// so presenters can vary visuals per type; the damage-type mask itself belongs to the absorb capability's own
|
|
// data and is out of scope (operator, 2026-09-17) -- this struct only carries the hit's existing type through.
|
|
public readonly struct AbsorbEvent
|
|
{
|
|
public readonly float3 Position;
|
|
public readonly float3 Direction;
|
|
public readonly float Magnitude;
|
|
public readonly DamageType DamageType;
|
|
public readonly AbsorbLayer Layer;
|
|
|
|
public AbsorbEvent(float3 position, float3 direction, float magnitude, DamageType damageType, AbsorbLayer layer)
|
|
{
|
|
Position = position;
|
|
Direction = direction;
|
|
Magnitude = magnitude;
|
|
DamageType = damageType;
|
|
Layer = layer;
|
|
}
|
|
}
|
|
|
|
// A timed grab's lifecycle (docs fork L: "a pickup is a timed grab of a selected item in range:
|
|
// extend, envelop, pull"), generalized to any item capability that reaches out and returns with
|
|
// something.
|
|
public enum GrabPhase
|
|
{
|
|
Started,
|
|
Extend,
|
|
Envelop,
|
|
Pull,
|
|
Completed,
|
|
Cancelled
|
|
}
|
|
|
|
// One phase of a grab. The target is the floating item the Pickup behaviour holds (the FieldShieldTest scene
|
|
// publishes its own handles as ids); nothing here interprets it.
|
|
public readonly struct GrabEvent
|
|
{
|
|
public readonly FloatingBodyId Target;
|
|
public readonly GrabPhase Phase;
|
|
public readonly float3 TargetPosition;
|
|
public readonly float PhaseDuration;
|
|
|
|
public GrabEvent(FloatingBodyId target, GrabPhase phase, float3 targetPosition, float phaseDuration)
|
|
{
|
|
Target = target;
|
|
Phase = phase;
|
|
TargetPosition = targetPosition;
|
|
PhaseDuration = phaseDuration;
|
|
}
|
|
}
|
|
|
|
// One melee swing.
|
|
public readonly struct MeleeEvent
|
|
{
|
|
public readonly float3 Direction;
|
|
public readonly float Range;
|
|
public readonly float Arc;
|
|
public readonly float Duration;
|
|
|
|
public MeleeEvent(float3 direction, float range, float arc, float duration)
|
|
{
|
|
Direction = direction;
|
|
Range = range;
|
|
Arc = arc;
|
|
Duration = duration;
|
|
}
|
|
}
|
|
|
|
// One frame of propulsion input. Twist is packed as a float2 (x: front, y: rear) rather than a
|
|
// single scalar, mirroring the existing FieldDriver split between front and rear turn effect;
|
|
// Throttle is carried separately from the planar thrust vector for presentations that want overall
|
|
// engine intensity independent of direction (the field shield presentation does not use it today).
|
|
public readonly struct ThrustEvent
|
|
{
|
|
public readonly float2 PlanarThrust;
|
|
public readonly float Throttle;
|
|
public readonly float2 Twist;
|
|
|
|
public ThrustEvent(float2 planarThrust, float throttle, float2 twist)
|
|
{
|
|
PlanarThrust = planarThrust;
|
|
Throttle = throttle;
|
|
Twist = twist;
|
|
}
|
|
}
|
|
|
|
// Per-entity source of capability events: one instance per Entity (Entity.Capabilities), published by
|
|
// FireControl for Absorb; the FieldShieldTest scene keeps its own instance. Follows the Subject<T>-per-event-kind
|
|
// idiom already used on Entity (IncomingHit, HullDamage, etc): each event kind gets its own stream, and a
|
|
// subscriber to one kind never sees another.
|
|
public class CapabilityEvents
|
|
{
|
|
private readonly Subject<AbsorbEvent> _absorb = new Subject<AbsorbEvent>();
|
|
private readonly Subject<GrabEvent> _grab = new Subject<GrabEvent>();
|
|
private readonly Subject<MeleeEvent> _melee = new Subject<MeleeEvent>();
|
|
private readonly Subject<ThrustEvent> _thrust = new Subject<ThrustEvent>();
|
|
|
|
public UniRx.IObservable<AbsorbEvent> Absorb => _absorb;
|
|
public UniRx.IObservable<GrabEvent> Grab => _grab;
|
|
public UniRx.IObservable<MeleeEvent> Melee => _melee;
|
|
public UniRx.IObservable<ThrustEvent> Thrust => _thrust;
|
|
|
|
public void PublishAbsorb(AbsorbEvent e) => _absorb.OnNext(e);
|
|
public void PublishGrab(GrabEvent e) => _grab.OnNext(e);
|
|
public void PublishMelee(MeleeEvent e) => _melee.OnNext(e);
|
|
public void PublishThrust(ThrustEvent e) => _thrust.OnNext(e);
|
|
}
|