Merge origin/master into sensor-catalog-r2 (code; catalog is master's)

Conflicts: EvasionTermTests keeps the branch's EvLonginusDrive split with master's generic
design names; AetherDb Program.cs keeps both the sensor-stat-set and generic-designs cases and
lists both in the usage line. GameData/Aetheria.cc is taken from origin/master unchanged;
sensor-stat-set apply is rerun on it in the next commit. Tests that read the sensor catalog
(TheCatalogCarriesSensorStats and the roles checks) do not pass at this commit: it builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 03:34:19 +02:00
commit 005e2fc57b
103 changed files with 5392 additions and 668 deletions

View file

@ -0,0 +1,208 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using GameCult.Caching;
using UnityEditor;
using UnityEngine;
using CultMath;
using static CultMath.math;
// 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. 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 CapabilityPresentationSmoke.Run -logFile <log>
//
// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
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(), $"capability-presentation-smoke-{Guid.NewGuid():N}");
GameObject[] objects = null;
try
{
Directory.CreateDirectory(work);
objects = Check(work);
Clean(objects, work);
EditorApplication.Exit(0);
}
catch (Exception exception)
{
Debug.LogException(exception);
Clean(objects, work);
EditorApplication.Exit(1);
}
}
// Edit mode runs no Awake or Start; the smoke runs the ones the game's scene would have.
private static void Awaken(Component component) => component.GetType().GetMethod("Awake", Any)?.Invoke(component, null);
private static GameObject[] Check(string work)
{
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
?? throw new InvalidOperationException("No Settings.asset.");
var shape = new Shape(5, 4);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
var hullData = new HullData
{
Name = "Smoke Hull", HullType = HullType.Ship, Shape = shape, Durability = 1000, Mass = 1000, Armor = 0
};
var cache = AetheriaStores.Open(Path.Combine(work, "smoke.cc"), catalogWritable: true);
cache.Upsert(new VerseGrammar { Revision = 1 });
cache.Upsert(hullData);
cache.FlushAsync().Wait();
var ledger = new ProvenanceLedger();
for (var i = 1; i <= 8; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
var items = new ItemManager(cache, ledger, settings, _ => { });
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Smoke", Owner = null }, null);
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Smoke Hull"));
var ship = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 1 }, new EntitySettings());
var other = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 2 }, new EntitySettings());
zone.Admit(ship, piloted: false);
zone.Admit(other, piloted: false);
// The scene's side: the tuning object with a pooled effect, and the game manager that names the player's ship.
var feedbackObject = new GameObject("Smoke Impact Feedback");
var feedback = feedbackObject.AddComponent<ImpactFeedback>();
Awaken(feedback);
var effectObject = new GameObject("Smoke Hull Effect");
feedback.HullEffect = effectObject.AddComponent<Prototype>();
var managerObject = new GameObject("Smoke Game Manager");
var manager = managerObject.AddComponent<ActionGameManager>();
typeof(ActionGameManager).GetProperty("Instance", Any).SetValue(null, manager);
manager.CurrentEntity = ship;
var beamPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Content/Prefabs/Tractor Beam.prefab")
?? throw new InvalidOperationException("No Tractor Beam prefab.");
// An entity's instance as the game builds it: the beam is a child before SetEntity on both ship paths, so the
// binder finds it as the game does.
(GameObject Object, EntityInstance Instance, TractorBeam Beam) Bound(string name, Entity entity)
{
var go = new GameObject(name);
var bound = go.AddComponent<EntityInstance>();
Awaken(bound);
bound.HullColliders = new HullCollider[0];
bound.RadiatorHardpoints = new RadiatorHardpoint[0];
bound.ThrusterHardpoints = new ThrusterHardpoint[0];
bound.WeaponHardpoints = new WeaponHardpoint[0];
bound.ArticulationPoints = new ArticulationPoint[0];
var tractor = ((GameObject)PrefabUtility.InstantiatePrefab(beamPrefab, go.transform)).GetComponent<TractorBeam>();
tractor.Power = .8f;
tractor.Direction = Vector3.forward;
bound.SetEntity(null, entity);
return (go, bound, tractor);
}
var (instanceObject, instance, beam) = Bound("Smoke Entity Instance", ship);
var (otherObject, _, otherBeam) = Bound("Smoke Other Instance", other);
int Effects() => instance.GetComponentsInChildren<Prototype>(true).Count(p => p != null && !p.isOriginalPrototype);
void Expect(string what, int effects, int kicks)
{
if (Effects() != effects || feedback.KicksRaised != kicks)
throw new InvalidOperationException($"{what}: {Effects()} hull effects (want {effects}), {feedback.KicksRaised} impulses (want {kicks}).");
}
AbsorbEvent Hit(AbsorbLayer layer) => new AbsorbEvent(float3(1f, 0f, 2f), float3(0f, 0f, 1f), 100f, DamageType.Kinetic, layer);
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("player's hull hit", 1, 1);
var effect = instance.GetComponentsInChildren<Prototype>(true).Single(p => !p.isOriginalPrototype);
var drawn = DrawAhead.Carried(ship, float3(1f, 0f, 2f), ActionGameManager.Clock.Lead);
if (abs(effect.transform.position.x - drawn.x) > 1e-3f || abs(effect.transform.position.z - drawn.z) > 1e-3f)
throw new InvalidOperationException("The hull effect is not at the drawn impact point.");
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 0f;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("hull hit at ScreenShake 0", 2, 1);
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f;
manager.CurrentEntity = other;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
// 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);
// Once the player's instruments track it, the same hit is drawn.
other.VisibleEntities.Add(ship);
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("hull hit on a tracked ship", 3, 1);
other.VisibleEntities.Remove(ship);
manager.CurrentEntity = ship;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Shield));
Expect("player's shield hit", 3, 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);
// Aim is on the ground plane: a beam above the held point does not pitch toward it.
var beamHome = beam.transform.position;
beam.transform.position = beamHome + new Vector3(0f, 10f, 0f);
ship.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 7f, 0f)));
ExpectBeam("grab from above", .8f, true);
ship.Capabilities.PublishGrab(Grab(GrabPhase.Completed, float3(20f, 7f, 0f)));
beam.transform.position = beamHome;
// A grab held at the beam's own position has no direction to take: the beam keeps the aim it had.
beam.Direction = Vector3.right;
var atBeam = float3(beam.transform.position.x, 0f, beam.transform.position.z);
ship.Capabilities.PublishGrab(Grab(GrabPhase.Started, atBeam));
ExpectBeam("grab at the beam", .8f, true);
ship.Capabilities.PublishGrab(Grab(GrabPhase.Completed, atBeam));
beam.Direction = Vector3.forward;
ExpectBeam("after the grab at the beam", 0f, false);
// The beam belongs to the ship's entity: another entity's grab, with its own beam bound, moves only that beam.
other.Capabilities.PublishGrab(Grab(GrabPhase.Started, float3(20f, 0f, 0f)));
ExpectBeam("another entity's grab", 0f, false);
otherBeam.Update();
if (abs(otherBeam.ParticleSystem.emission.rateOverTimeMultiplier - .8f) > 1e-4f)
throw new InvalidOperationException("The other entity's own beam did not present its grab.");
Debug.Log("CAPABILITY_PRESENTATION_SMOKE ok effects=3 impulses=2 grabs=ok");
return new[] { feedbackObject, effectObject, managerObject, instanceObject, otherObject };
}
private static void Clean(GameObject[] objects, string work)
{
if (objects != null)
foreach (var go in objects)
if (go != null) UnityEngine.Object.DestroyImmediate(go);
try { if (Directory.Exists(work)) Directory.Delete(work, true); }
catch (Exception error) { Debug.LogWarning($"Impact presenter smoke left {work}: {error.Message}"); }
}
}

View file

@ -180,6 +180,32 @@ public static class FloatingItemsDrawnSmoke
At(drawn, released, "step");
Require(drawn.transform.position != before, "the pickup did not move with the body.");
// A held item keeps its label lit, so a pull is read as it happens. The label is let go dark first (nothing views
// it), then the zone holds the item through its own TryHold and the label must come up.
var earlyPickup = Pickups().Single(p => p.Item == early.Item);
earlyPickup.LabelPersistDuration = 1;
earlyPickup.LabelFadeDuration = .2f;
for (var i = 0; i < 1200 && earlyPickup.ScanLabel.color.a > .05f; i++) yield return null;
Require(earlyPickup.ScanLabel.color.a <= .05f, "label: an unviewed, unheld label did not go dark.");
Require(zone.TryHold(taker, early.Id), "label: the zone refused to hold the item.");
for (var i = 0; i < 1200 && earlyPickup.ScanLabel.color.a < .95f; i++) yield return null;
Require(earlyPickup.ScanLabel.color.a >= .95f, "label: a held item's label is not lit.");
// The test scene's stand-in sim and tendril ride the zone's own held-pose curve: its pose at each point of a grab
// is where the zone puts a held item at the same progress.
var (extend, envelop, pull) = (1.5f, .75f, 1.5f);
var holderAt = new CultMath.float3(taker.Position.x, 0f, taker.Position.z);
var claimedAt = new CultMath.float3(early.HeldFrom.x, 0f, early.HeldFrom.y);
foreach (var elapsed in new[] { 0f, extend + envelop - .01f, extend + envelop + .3f, extend + envelop + .75f, extend + envelop + pull })
{
zone.SetHeldProgress(early.Id, Mathf.Clamp01((elapsed - extend - envelop) / pull)); // the pull starts after Extend and Envelop
zone.Update(.01f);
var want = FieldTester.PulledPosition(claimedAt, holderAt, elapsed, extend, envelop, pull);
Require(Mathf.Abs(early.Body.Position.x - want.x) < 1e-3f && Mathf.Abs(early.Body.Position.y - want.z) < 1e-3f,
$"stand-in pose at {elapsed}s is {want}, the zone holds the item at {early.Body.Position}.");
}
zone.Free(early.Id);
// Pickup destroys exactly the picked-up item's pickup.
Require(zone.TryPickUp(taker, released.Id), "the station refused a one-cell stack.");
yield return null;

View file

@ -1,132 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using GameCult.Caching;
using UnityEditor;
using UnityEngine;
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
// 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.
//
// Unity -batchmode -nographics -projectPath <project> -executeMethod ImpactPresenterSmoke.Run -logFile <log>
//
// Do not pass -quit: Run exits the Editor itself, 0 on success and 1 on any failure.
public static class ImpactPresenterSmoke
{
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}");
GameObject[] objects = null;
try
{
Directory.CreateDirectory(work);
objects = Check(work);
Clean(objects, work);
EditorApplication.Exit(0);
}
catch (Exception exception)
{
Debug.LogException(exception);
Clean(objects, work);
EditorApplication.Exit(1);
}
}
// Edit mode runs no Awake or Start; the smoke runs the ones the game's scene would have.
private static void Awaken(Component component) => component.GetType().GetMethod("Awake", Any)?.Invoke(component, null);
private static GameObject[] Check(string work)
{
var settings = AssetDatabase.LoadAssetAtPath<GameSettings>("Assets/Resources/Settings.asset")?.GameplaySettings
?? throw new InvalidOperationException("No Settings.asset.");
var shape = new Shape(5, 4);
foreach (var cell in shape.AllCoordinates) shape[cell] = true;
var hullData = new HullData
{
Name = "Smoke Hull", HullType = HullType.Ship, Shape = shape, Durability = 1000, Mass = 1000, Armor = 0
};
var cache = AetheriaStores.Open(Path.Combine(work, "smoke.cc"), catalogWritable: true);
cache.Upsert(new VerseGrammar { Revision = 1 });
cache.Upsert(hullData);
cache.FlushAsync().Wait();
var ledger = new ProvenanceLedger();
for (var i = 1; i <= 8; i++) ledger.Lots[i] = new Lot { Origin = new Attributed(), Quality = 1 };
var items = new ItemManager(cache, ledger, settings, _ => { });
var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Smoke", Owner = null }, null);
var hullRef = items.ItemData.RefOf<ItemData>(items.ItemData.GetByName<HullData>("Smoke Hull"));
var ship = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 1 }, new EntitySettings());
var other = new Ship(items, zone, new EquippableItem { Data = hullRef, Durability = 1000, Lot = 2 }, new EntitySettings());
zone.Admit(ship, piloted: false);
zone.Admit(other, piloted: false);
// The scene's side: the tuning object with a pooled effect, and the game manager that names the player's ship.
var feedbackObject = new GameObject("Smoke Impact Feedback");
var feedback = feedbackObject.AddComponent<ImpactFeedback>();
Awaken(feedback);
var effectObject = new GameObject("Smoke Hull Effect");
feedback.HullEffect = effectObject.AddComponent<Prototype>();
var managerObject = new GameObject("Smoke Game Manager");
var manager = managerObject.AddComponent<ActionGameManager>();
typeof(ActionGameManager).GetProperty("Instance", Any).SetValue(null, manager);
manager.CurrentEntity = ship;
var instanceObject = new GameObject("Smoke Entity Instance");
var instance = instanceObject.AddComponent<EntityInstance>();
Awaken(instance);
instance.HullColliders = new HullCollider[0];
instance.RadiatorHardpoints = new RadiatorHardpoint[0];
instance.ThrusterHardpoints = new ThrusterHardpoint[0];
instance.WeaponHardpoints = new WeaponHardpoint[0];
instance.ArticulationPoints = new ArticulationPoint[0];
instance.SetEntity(null, ship);
int Effects() => instance.GetComponentsInChildren<Prototype>(true).Count(p => p != null && !p.isOriginalPrototype);
void Expect(string what, int effects, int kicks)
{
if (Effects() != effects || feedback.KicksRaised != kicks)
throw new InvalidOperationException($"{what}: {Effects()} hull effects (want {effects}), {feedback.KicksRaised} impulses (want {kicks}).");
}
AbsorbEvent Hit(AbsorbLayer layer) => new AbsorbEvent(float3(1f, 0f, 2f), float3(0f, 0f, 1f), 100f, DamageType.Kinetic, layer);
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("player's hull hit", 1, 1);
var effect = instance.GetComponentsInChildren<Prototype>(true).Single(p => !p.isOriginalPrototype);
var drawn = DrawAhead.Carried(ship, float3(1f, 0f, 2f), ActionGameManager.Clock.Lead);
if (abs(effect.transform.position.x - drawn.x) > 1e-3f || abs(effect.transform.position.z - drawn.z) > 1e-3f)
throw new InvalidOperationException("The hull effect is not at the drawn impact point.");
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 0f;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("hull hit at ScreenShake 0", 2, 1);
ActionGameManager.PlayerSettings.GraphicsSettings.ScreenShake = 1f;
manager.CurrentEntity = other;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Hull));
Expect("hull hit on another ship", 3, 1);
manager.CurrentEntity = ship;
ship.Capabilities.PublishAbsorb(Hit(AbsorbLayer.Shield));
Expect("player's shield hit", 3, 2);
Debug.Log("IMPACT_PRESENTER_SMOKE ok effects=3 impulses=2");
return new[] { feedbackObject, effectObject, managerObject, instanceObject };
}
private static void Clean(GameObject[] objects, string work)
{
if (objects != null)
foreach (var go in objects)
if (go != null) UnityEngine.Object.DestroyImmediate(go);
try { if (Directory.Exists(work)) Directory.Delete(work, true); }
catch (Exception error) { Debug.LogWarning($"Impact presenter smoke left {work}: {error.Message}"); }
}
}

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 curve of Zone's held pose (Zone.HeldPull): the tip rides the item the simulation pulls in.
_tendrilTargetPos = lerp(_grabStart.ToCultMath(), transform.position.ToCultMath(), Zone.HeldPull(_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

@ -36,18 +36,20 @@ public class CombatState : BaseState
// Cut 2 (docs/fire-control-cut.md): aim at whichever revealed weapon on the target hits hardest at
// this range, or clear the aim point when nothing qualifies -- the same predicate
// (FireControl.IsRevealed) and the same writer (TrySelectTargetItem) the player path uses.
// (FireControl.IsRevealed) and the same writer (TrySelectTargetItem) the player path uses. Which weapons it
// believes the target has, and how hard each hits, come from its book's claims, not the target's live gear
// (aetheria-ew, classify-from-belief; ew-claim-weapon-measure-ruled: the claimed design's rated figures); the
// reveal's Item is the aim and stays the truth.
EquippedItem bestTargetItem = null;
var bestTargetDps = float.MinValue;
foreach (var targetItem in target.Equipment)
foreach (var reveal in _agent.Ship.RevealedWeapons(target))
{
if (!(targetItem.Behaviors.FirstOrDefault(b => b is Weapon) is Weapon targetWeapon)) continue;
if (!FireControl.IsRevealed(_agent.Ship, targetItem)) continue;
var dps = targetWeapon.RangeDamagePerSecond(targetDistance);
if (!FireControl.IsRevealed(_agent.Ship, reveal.Item)) continue;
var dps = reveal.Claimed.Behaviors.OfType<WeaponData>().First().RatedRangeDamagePerSecond(targetDistance);
if (dps > bestTargetDps)
{
bestTargetDps = dps;
bestTargetItem = targetItem;
bestTargetItem = reveal.Item;
}
}
_agent.Ship.TrySelectTargetItem(bestTargetItem);

View file

@ -54,6 +54,15 @@ public class ChargedWeaponData : InstantWeaponData
{
return new ChargedWeapon(this, item);
}
// The one formula of a charged weapon's damage per second at a range (see InstantWeaponData.RangeDamagePerSecond).
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float chargeFiringDamageMultiplier,
float minRange, float range, float cooldown, float chargeTime, float atRange) =>
damage * chargeFiringDamageMultiplier * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange))) / (cooldown + chargeTime);
public override float RatedRangeDamagePerSecond(float range) =>
RangeDamagePerSecond(DamageCurve, PerRound(Damage.Max, Count.Max), ChargeFiringDamageMultiplier, MinRange.Max, Range.Max,
Cooldown.Max, ChargeTime.Max, range);
}
public class ChargedWeapon : InstantWeapon
@ -75,13 +84,14 @@ public class ChargedWeapon : InstantWeapon
// authored charge-up speed IS its sustained rate of fire, not a second, disconnected number.
protected override float RateOverride => Evaluate(_data.ChargeEnergy);
public override float DamagePerSecond => Damage * _data.ChargeFiringDamageMultiplier / (Cooldown + ChargeTime);
// The charge multiplier lives in the formula alone: Damage is lerped by charge for the shot itself (UpdateStats), so the
// live reads take the uncharged damage, and a nominal weapon reads its design's rated figure at every charge.
private float _unchargedDamage;
public override float DamagePerSecond => _unchargedDamage * _data.ChargeFiringDamageMultiplier / (Cooldown + ChargeTime);
public override float RangeDamagePerSecond(float range)
{
return Damage *
_data.ChargeFiringDamageMultiplier *
_data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) /
(Cooldown + ChargeTime);
return ChargedWeaponData.RangeDamagePerSecond(_data.DamageCurve, _unchargedDamage, _data.ChargeFiringDamageMultiplier,
MinRange, Range, Cooldown, ChargeTime, range);
}
public event Action OnStartCharging;
@ -121,6 +131,7 @@ public class ChargedWeapon : InstantWeapon
ChargeTime = Evaluate(_data.ChargeTime);
ChargeEnergy = Evaluate(_data.ChargeEnergy);
ChargeHeat = Evaluate(_data.ChargeHeat);
_unchargedDamage = Damage;
Damage *= lerp(1, _data.ChargeFiringDamageMultiplier, saturate(_charge));
Heat *= lerp(1, _data.ChargeFiringHeatMultiplier, saturate(_charge));
Spread *= lerp(1, _data.ChargeFiringSpreadMultiplier, saturate(_charge));

View file

@ -23,6 +23,13 @@ public class ConstantWeaponData : WeaponData
{
return new ConstantWeapon(this, item);
}
// The one formula of a beam's damage per second at a range (see InstantWeaponData.RangeDamagePerSecond).
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float minRange, float range, float atRange) =>
damage * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange)));
public override float RatedRangeDamagePerSecond(float range) =>
RangeDamagePerSecond(DamageCurve, Damage.Max, MinRange.Max, Range.Max, range);
}
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
@ -49,7 +56,7 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
public override float DamagePerSecond => Damage;
public override float RangeDamagePerSecond(float range)
{
return Damage * _data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range)));
return ConstantWeaponData.RangeDamagePerSecond(_data.DamageCurve, Damage, MinRange, Range, range);
}
public event Action OnReloadBegin;

View file

@ -30,6 +30,17 @@ public class InstantWeaponData : WeaponData
{
return new InstantWeapon(this, item);
}
// The one formula of an instant weapon's damage per second at a range; the live weapon calls it with its evaluated
// stats, the design with its rated ones. A burst's round carries its share of the totals: PerRound, the one divisor
// UpdateStats and the rated read both use.
internal static float PerRound(float total, float burstCount) => total / (int) burstCount;
internal static float RangeDamagePerSecond(BezierCurve damageCurve, float damage, float minRange, float range, float cooldown, float atRange) =>
damage * damageCurve.Evaluate(saturate(unlerp(minRange, range, atRange))) / cooldown;
public override float RatedRangeDamagePerSecond(float range) =>
RangeDamagePerSecond(DamageCurve, PerRound(Damage.Max, Count.Max), MinRange.Max, Range.Max, Cooldown.Max, range);
}
public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
@ -61,7 +72,7 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
public override float DamagePerSecond => Damage / Cooldown;
public override float RangeDamagePerSecond(float range)
{
return Damage * _data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) / Cooldown;
return InstantWeaponData.RangeDamagePerSecond(_data.DamageCurve, Damage, MinRange, Range, Cooldown, range);
}
public override int Ammo
@ -129,9 +140,9 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
BurstTime = Evaluate(_data.BurstTime);
Cooldown = Evaluate(_data.Cooldown);
Damage /= (int) BurstCount;
Heat /= (int) BurstCount;
Energy /= (int) BurstCount;
Damage = InstantWeaponData.PerRound(Damage, BurstCount);
Heat = InstantWeaponData.PerRound(Heat, BurstCount);
Energy = InstantWeaponData.PerRound(Energy, BurstCount);
}
// Cut 4 (docs/stats-and-power-cut.md §7 Q4): capacity and rate, resolved fresh rather than off the cached

View file

@ -71,6 +71,13 @@ public abstract class WeaponData : BehaviorData
// weapon-tracking-authored). Unauthored is a mount that follows anything: FireControl.PMount reads +infinity as 1.
[Inspectable, JsonProperty("tracking"), Key(33), RuntimeInspectable]
public PerformanceStat Tracking = new PerformanceStat { Min = float.PositiveInfinity, Max = float.PositiveInfinity };
// What an observer who believes this is the design measures it by (aetheria-ew, ew-claim-weapon-measure-ruled): the
// design's rated figures, each stat's Max (what a stat with no terms resolves to), through the same formula the
// live weapon's RangeDamagePerSecond runs over its evaluated stats. Never the carrier's quality, heat, wear or power.
public abstract float RatedRangeDamagePerSecond(float range);
[IgnoreMember]
public float RatedRange => Range.Max;
}
public abstract class Weapon : Behavior, IActivatedBehavior

View file

@ -103,7 +103,27 @@ public abstract class Entity
// each equipped suppressor folds the strength in equipment order and pays for what it alone removed.
foreach (var suppressor in GetBehaviors<LaunchSuppressor>())
strength = suppressor.Suppress(channel, strength);
_visibilitySources[source] = new Signature(channel, strength, item);
_visibilitySources[source] = new Signature(channel, strength, item, ClaimOf(item));
}
// What this ship's acts claim (aetheria-ew, lie-is-own-emission, classify-from-belief): per equipped item, the
// equippable class every act of that item is stamped with (SetSignature, Emit, ReceivePaint). The hull is the
// item EquippedHull, so its claim lives in the same slot as every other: the hull class its own acts carry and
// the profile an observer derives (ClaimedHull) are one value. The truth unless claimed otherwise. Claim is the
// one write port; its production writer is the EW behaviour of cut ew-masquerade, tests only until then.
// Deactivate clears the slots. Aim, damage and physics keep the truth.
private readonly Dictionary<EquippedItem, EquippableItemData> _itemClaims = new Dictionary<EquippedItem, EquippableItemData>();
public HullData ClaimedHull => (ClaimOf(EquippedHull) as HullData) ?? HullData;
public EquippableItemData ClaimOf(EquippedItem item) =>
item == null ? null : _itemClaims.TryGetValue(item, out var claimed) ? claimed : item.Data;
internal void Claim(HullData hull) => Claim(EquippedHull, hull);
internal void Claim(EquippedItem item, EquippableItemData claimed)
{
// One hull class in the book: a hull class is claimed by the hull and only by the hull, whichever side asks.
if (claimed != null && (item == EquippedHull) != (claimed is HullData))
throw new ArgumentException("Only the hull claims a hull class, and the hull claims nothing else.", nameof(claimed));
if (claimed == null) _itemClaims.Remove(item);
else _itemClaims[item] = claimed;
}
public readonly ReactiveDictionary<Entity, float> EntityInfoGathered = new ReactiveDictionary<Entity, float>();
@ -883,6 +903,7 @@ public abstract class Entity
_pingReturns.Clear();
_emissions.Clear();
_reveals.Clear();
_itemClaims.Clear();
}
// Zone.Update runs this on every entity before any entity updates, so a paint made during tick N is published
@ -895,7 +916,7 @@ public abstract class Entity
foreach (var paint in _paintInbox)
{
_incomingPaints.Add(paint.Warning);
RecordReveal(paint.Emitter, paint.Item, SignatureChannel.Emission);
RecordReveal(paint.Emitter, paint.Item, SignatureChannel.Emission, paint.Warning.EmitterClass);
}
_paintInbox.Clear();
_pingReturns.Clear();
@ -915,16 +936,17 @@ public abstract class Entity
internal void Emit(EquippedItem item, float hearRange)
{
if (!_active) return;
_emissions.Add(new ActiveEmission(item, hearRange));
_emissions.Add(new ActiveEmission(item, hearRange, ClaimOf(item)));
}
// The callers are Sensor.Execute (a Search paint, Strength 0) and LockWeapon.Execute (a Track paint, Strength
// the emitter's own Lock at that paint). The emitter's class is the item's data (null with no item: a
// consumable's effect, which has no item and so is never revealed).
// the emitter's own Lock at that paint). The emitter's class is the class its ship claims for the item (ClaimOf: the
// item's data unless claimed otherwise; null with no item: a consumable's effect, which has no item and so is never
// revealed).
internal void ReceivePaint(Entity emitter, EquippedItem item, PaintMode mode, float strength)
{
if (!_active) return;
_paintInbox.Add(new PendingPaint(new PaintWarning(normalize((emitter.Position - Position).xz), item?.Data, mode, strength), emitter, item));
_paintInbox.Add(new PendingPaint(new PaintWarning(normalize((emitter.Position - Position).xz), emitter.ClaimOf(item), mode, strength), emitter, item));
}
// The reveal book's one way in and its one membership gate (callers: Sense and PublishInbox, both in this file):
@ -932,13 +954,14 @@ public abstract class Entity
// radiation, a consumable) reveals nothing, and neither does a subject that is not in the zone: a departed
// subject's entries are never created or refreshed, from any caller, and neither does an item this observer
// perceives destroyed (PerceivesDestroyed). A later act of the same item replaces the
// earlier one.
internal void RecordReveal(Entity subject, EquippedItem item, SignatureChannel channel)
// earlier one. `claimed` is the class the carrier (signature, emission or paint) stamped: what this observer's
// classifying readers will believe of the act; `item` stays the aim and book key.
internal void RecordReveal(Entity subject, EquippedItem item, SignatureChannel channel, EquippableItemData claimed)
{
if (!_active || item == null || !Zone.Entities.Contains(subject) || PerceivesDestroyed(subject, item)) return;
if (!_reveals.TryGetValue(subject, out var book))
_reveals[subject] = book = new Dictionary<EquippedItem, Reveal>();
book[item] = new Reveal(item, channel, normalize((subject.Position - Position).xz), Zone.Time);
book[item] = new Reveal(item, channel, normalize((subject.Position - Position).xz), Zone.Time, claimed);
}
// This observer resolves `subject`'s gear (ContactOf's Identified gate) and `item` of it is destroyed: the one rule
@ -970,6 +993,31 @@ public abstract class Entity
public IEnumerable<Reveal> RevealsOf(Entity subject) =>
_reveals.TryGetValue(subject, out var book) ? book.Values : Enumerable.Empty<Reveal>();
// The weapons this observer believes `subject` has: each reveal whose claimed class carries a weapon behaviour, in the
// subject's equipment order (never the book's slot order, which follows when reveals were made and ended). The one
// classifying read of the book for weapons, shared by the enemy schematic and Combat (aetheria-ew, classify-from-belief);
// a gun claimed as a non-weapon is not listed, though RevealsOf holds it for aim.
public IEnumerable<Reveal> RevealedWeapons(Entity subject) =>
RevealsOf(subject).Where(r => r.Claimed != null && r.Claimed.Behaviors.Any(b => b is WeaponData))
.OrderBy(r => FittedIndex(subject, r.Item)).ThenBy(r => r.Item.EquippableItem.Lot);
// A revealed item's place in the subject's equipment; gear the subject no longer carries follows all fitted gear
// (ties among it fall to the lot, a stable id), never ahead of it.
private static int FittedIndex(Entity subject, EquippedItem item)
{
var index = subject.Equipment.IndexOf(item);
return index < 0 ? int.MaxValue : index;
}
// The weapon rows of `subject`'s schematic as this ship reads it: its own equipment when `subject` is this ship (each
// row its own data and live range, nothing to doubt about your own ship), otherwise RevealedWeapons (each row the
// claimed class and that design's rated range). The one owner of which rows, which class and which range an
// enemy schematic shows; the Unity SchematicDisplay renders it.
public IEnumerable<SchematicRow> SchematicRows(Entity subject) =>
subject == this
? subject.Equipment.Where(i => i.Behaviors.Any(b => b.Data is WeaponData)).Select(i => new SchematicRow(i, i.Data, true))
: RevealedWeapons(subject).Select(r => new SchematicRow(r.Item, r.Claimed, false));
// Whether this observer's book for `subject` holds `item`: the book's membership, no live gear read.
internal bool HasReveal(Entity subject, EquippedItem item) =>
_reveals.TryGetValue(subject, out var book) && book.ContainsKey(item);
@ -991,7 +1039,9 @@ public abstract class Entity
var remembered = any && Zone.Time - freshest.Time < settings.BearingMemory;
var detected = tracked || remembered;
var bearing = tracked ? normalize((subject.Position - Position).xz) : remembered ? freshest.Bearing : default;
return new Contact(detected, tracked, detected && any, info >= settings.TargetGearInfoThreshold, bearing, info);
var identified = info >= settings.TargetGearInfoThreshold;
// Profile is the one derivation of the hull this observer believes (no stored belief until the judge, ew-masquerade).
return new Contact(detected, tracked, detected && any, identified, bearing, info, identified ? subject.ClaimedHull : null);
}
// What this observer's instruments receive of other ships' acts this tick (Zone.Update calls it once, after every
@ -1015,7 +1065,7 @@ public abstract class Entity
{
if (signature.Item == null) continue;
if (perSignature < 0f) perSignature = PassiveInfo(other.Position.xz, 1f);
if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel);
if (signature.Strength * perSignature > threshold) RecordReveal(other, signature.Item, signature.Channel, signature.Claimed);
}
// Every emission this ship made this tick that reaches this observer is revealed, bearing only (ruling
// sensor-stat-set), through RecordReveal's one gate. It feeds no track.
@ -1026,7 +1076,7 @@ public abstract class Entity
for (var j = 0; j < other._emissions.Count; j++)
{
var emission = other._emissions[j];
if (planar <= emission.HearRange * hearing * obscuration) RecordReveal(other, emission.Item, SignatureChannel.Emission);
if (planar <= emission.HearRange * hearing * obscuration) RecordReveal(other, emission.Item, SignatureChannel.Emission, emission.Claimed);
}
}
// The observer resolves this ship's gear (PerceivesDestroyed), so it perceives an item of it destroyed and

View file

@ -20,13 +20,13 @@ public sealed class AiFusedDiscipline : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
(weapon, int2(1, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
stage.Player(stage.Generated("Djinni"), float2(0, 0));

View file

@ -27,27 +27,27 @@ public sealed class Arcs : Scenario
("Cockpit 2x2", int2(4, 6), None),
("Large Drive", int2(3, 0), Reversed),
("Large Drive", int2(5, 0), Reversed),
("Talaria", int2(4, 14), CounterClockwise),
("Talaria", int2(5, 14), Clockwise),
("Core Power", int2(4, 4), None),
("GT 3K", int2(2, 5), None),
("GT 3K", int2(7, 5), None),
("FastBlast+-", int2(3, 8), None),
("FastBlast+-", int2(6, 8), None),
("Iapyx", int2(4, 2), CounterClockwise),
("Iapyx", int2(5, 2), Clockwise),
("not if i see you first", int2(5, 10), None),
("Rugged Drive", int2(4, 14), CounterClockwise),
("Rugged Drive", int2(5, 14), Clockwise),
("Compact Reactor", int2(4, 4), None),
("Guided Missile Launcher", int2(2, 5), None),
("Guided Missile Launcher", int2(7, 5), None),
("Cell Blaster", int2(3, 8), None),
("Cell Blaster", int2(6, 8), None),
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise),
("Autocannon", int2(8, 4), Clockwise),
("ChargeBlast SG", int2(0, 4), CounterClockwise));
("Charged Scatter Blaster", int2(0, 4), CounterClockwise));
var turret = stage.Fit("Turret",
("ClearPath", int2(1, 2), None),
("ClearPath", int2(5, 2), None),
("Skiron", int2(0, 2), CounterClockwise),
("Skiron", int2(7, 2), Clockwise),
("Core Power", int2(3, 3), None),
("Light Machine Gun", int2(1, 2), None),
("Light Machine Gun", int2(5, 2), None),
("Long Radiator", int2(0, 2), CounterClockwise),
("Long Radiator", int2(7, 2), Clockwise),
("Compact Reactor", int2(3, 3), None),
("Turret Control Module", int2(3, 5), None),
("not if i see you first", int2(4, 5), None),
("Compact Sensor", int2(4, 5), None),
("Fire Control Array", int2(3, 6), None));
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));

View file

@ -23,16 +23,16 @@ public sealed class Belt : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("plight", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Lightning Projector", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
// The belt is a ring about its parent's position at the belt orbit's distance. The player stands 100 m outside

View file

@ -4,8 +4,8 @@ using static ItemRotation;
public sealed class Crossing : Scenario
{
public override string Name => "Crossing";
public override string Brief => "plight at the starboard gun and a FastBlast+- at the port gun, a fast Djinni crossing close ahead and an " +
"unpiloted one waiting at range. The Djinni starts to port moving across the bow; the arena's gravity bends its run. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely (thrusting from the start carries you past it). Verify: as the Djinni crosses close, FastBlast lands some and plight " +
public override string Brief => "A Lightning Projector at the starboard gun and a Cell Blaster at the port gun, a fast Djinni crossing close ahead and an " +
"unpiloted one waiting at range. The Djinni starts to port moving across the bow; the arena's gravity bends its run. Make the pass coasting: give no throttle until the Djinni has crossed the bow, then fly freely (thrusting from the start carries you past it). Verify: as the Djinni crosses close, FastBlast lands some and projector " +
"almost none; against the Djinni at range both land. Fly it again without the Fire Control Array: " +
"with it the small gun lands more.";
public override uint Seed => 112;
@ -13,11 +13,11 @@ public sealed class Crossing : Scenario
// Where the Djinni sit: the player is at the origin facing +z. The crosser starts ahead and to port, facing across
// the bow and already moving at the Djinni's 50 m/s cap, so its run sweeps across the bow within seconds (the arena
// pulls the player to port, which brings the two together). The waiting Djinni sits inside plight's arc at 80% of
// pulls the player to port, which brings the two together). The waiting Djinni sits inside the Lightning Projector's arc at 80% of
// its Range.
public static readonly CultMath.float2 CrosserAt = float2(-150, 200), CrosserFacing = float2(1, 0), CrosserVelocity = float2(50, 0);
public const float PlightRange = 1216f; // plight's Range in metres, 1216.4 as the fit stands settled at rest
public static readonly CultMath.float2 Waiting = float2(0, PlightRange * .8f);
public const float ProjectorRange = 1216f; // the Lightning Projector's Range in metres, 1216.4 as the fit stands settled at rest
public static readonly CultMath.float2 Waiting = float2(0, ProjectorRange * .8f);
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast);
@ -27,16 +27,16 @@ public sealed class Crossing : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("plight", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Lightning Projector", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));

View file

@ -4,7 +4,7 @@ using static ItemRotation;
public sealed class Duel : Scenario
{
public override string Name => "Duel";
public override string Brief => "One piloted, hostile Longinus with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " +
public override string Brief => "One piloted, hostile Longinus with slow guided missiles and a Light Laser in its hold. Verify: rolled " +
"hits and near-misses, subsystem aim, beam reads above bow, armour takes the missile, loot keeps its brand.";
public override uint Seed => 104;
public override bool Ambient => false;
@ -17,22 +17,22 @@ public sealed class Duel : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("PotaT+-", int2(1, 5), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Capacitor", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Store-All Plus", int2(2, 8), None));
("Cargo Bay", int2(2, 8), None));
stage.Player(fighter, float2(0, 0));
var rival = stage.Place(fighter, float2(0, 900), facing: float2(0, -1), stance: ScenarioStance.Hostile, piloted: true);
stage.Cargo(rival, "Spectra");
stage.Cargo(rival, "Light Laser");
}
}

View file

@ -4,20 +4,20 @@ using static ItemRotation;
public sealed class FusedRounds : Scenario
{
public override string Name => "Fused Rounds";
public override string Brief => "The Smoke Test Hull with Smoke Proximity, Smoke Delayed and a DeathCluster; a hostile bare Longinus at mid range, a bare " +
public override string Brief => "The Smoke Test Hull with Smoke Proximity, Smoke Delayed and a Cluster Cannon; a hostile bare Longinus at mid range, a bare " +
"hull dead ahead, a fragile one to starboard, a close one inside the proximity blast radius, and one astern. " +
"Verify with nothing selected: aim past every hull and the round bursts at max range along the aim; aim at the hull " +
"ahead and it stops and bursts on it; fire at the fragile hull and kill it with the DeathCluster before the round " +
"ahead and it stops and bursts on it; fire at the fragile hull and kill it with the Cluster Cannon before the round " +
"lands: no burst. Select the astern hull: the round fires and bursts at its range along the arc edge. Select the " +
"close hull: the burst is pushed out to the blast radius from your centre. A Smoke Proximity burst beside the " +
"Longinus damages where the model shows the blast; a Smoke Delayed round into its nose bursts inside. The HUD " +
"reads Direct, Burst at N m or Refused for the fused weapons and a percentage for the DeathCluster, and the " +
"reads Direct, Burst at N m or Refused for the fused weapons and a percentage for the Cluster Cannon, and the " +
"smoke weapons show no Manufacturer row.";
public override uint Seed => 108;
public override bool Ambient => false;
// Where everything sits: the player is at the origin facing +z. Ahead lies on the initial aim line, inside every forward
// mount's arc. Fragile is a bare Longinus inside the DeathCluster's range; Close is inside Smoke Proximity's blast radius
// mount's arc. Fragile is a bare Longinus inside the Cluster Cannon's range; Close is inside Smoke Proximity's blast radius
// (8 m from the ship's centre) and beside the aim line, not across it; Stern is outside every mount's arc.
public static readonly CultMath.float2 Mid = float2(-200, 600), Ahead = float2(0, 300), Fragile = float2(140, 420),
Close = float2(6.4f, 4f), Stern = float2(0, -400);
@ -30,15 +30,15 @@ public sealed class FusedRounds : Scenario
("Cockpit 2x2", int2(4, 6), None),
("Large Drive", int2(3, 0), Reversed),
("Large Drive", int2(5, 0), Reversed),
("Talaria", int2(4, 14), CounterClockwise),
("Talaria", int2(5, 14), Clockwise),
("Core Power", int2(4, 4), None),
("Rugged Drive", int2(4, 14), CounterClockwise),
("Rugged Drive", int2(5, 14), Clockwise),
("Compact Reactor", int2(4, 4), None),
("Smoke Proximity", int2(3, 8), None),
("Smoke Delayed", int2(6, 8), None),
("DeathCluster", int2(5, 17), None),
("Iapyx", int2(4, 2), CounterClockwise),
("Iapyx", int2(5, 2), Clockwise),
("not if i see you first", int2(5, 10), None),
("Cluster Cannon", int2(5, 17), None),
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise));
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));

View file

@ -4,8 +4,8 @@ using static ItemRotation;
public sealed class LauncherAngles : Scenario
{
public override string Name => "Launcher Angles";
public override string Brief => "A GT 3K, a pswarm and two FastBlasts; a hostile bare Longinus broadside on, another beside it. " +
"Verify: hits pulse only the facing edge; a pswarm round whose target dies fades unexploded.";
public override string Brief => "A Guided Missile Launcher, a Swarm Rocket Pod and two Cell Blasters; a hostile bare Longinus broadside on, another beside it. " +
"Verify: hits pulse only the facing edge; a Swarm Rocket Pod round whose target dies fades unexploded.";
public override uint Seed => 105;
public override bool Ambient => false;
@ -17,16 +17,16 @@ public sealed class LauncherAngles : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("pswarm", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Swarm Rocket Pod", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
stage.Player(launcher, float2(0, 0));

View file

@ -0,0 +1,44 @@
using static CultMath.math;
using static ItemRotation;
public sealed class LockOn : Scenario
{
public override string Name => "Lock On";
public override string Brief => "A hostile closes from out of sight, finds you and locks. Verify: no warning at first; once it locks, one " +
"indicator appears toward it and tightens as the lock grows; break line of sight or range and it vanishes; " +
"your own ping shows no incoming paint on its side, only the lock does.";
public override uint Seed => 113;
public override bool Ambient => false;
// The hostile starts about 4000 m off, to the side of the player's axis, facing the player. At that range its passive
// sensors hold no track of the player (the test asks the sim's own PassiveInfo against the detection threshold); the
// player sees the hostile a little sooner, at about tick 20. The hostile closes, sees the player some eight seconds in,
// and locks two ticks later. At Duel's 900 m (and at 600 m) it sees and locks within a second.
public static readonly CultMath.float2 HostileAt = float2(1500, 3700);
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(DemoTerminus.Cast);
public override void Stage(ScenarioStage stage)
{
var fighter = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Capacitor", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Cargo Bay", int2(2, 8), None));
stage.Player(fighter, float2(0, 0), facing: float2(0, 1));
stage.Place(fighter, HostileAt, facing: normalize(-HostileAt), stance: ScenarioStance.Hostile, piloted: true);
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f0b6d2a9c7e4a1b8d5e2c6f7a9b0c14
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -23,35 +23,35 @@ public sealed class Pursuit : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Mine Launcher", int2(0, 5), None),
("pswarm", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Swarm Rocket Pod", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Store-All Plus", int2(2, 8), None));
("Cargo Bay", int2(2, 8), None));
var pursuer = stage.Fit("Longinus",
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("Core Power", int2(2, 4), None),
("GT 3K", int2(0, 5), None),
("GT 3K", int2(5, 5), None),
("FastBlast+-", int2(1, 8), None),
("FastBlast+-", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("PotaT+-", int2(1, 5), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Compact Reactor", int2(2, 4), None),
("Guided Missile Launcher", int2(0, 5), None),
("Guided Missile Launcher", int2(5, 5), None),
("Cell Blaster", int2(1, 8), None),
("Cell Blaster", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Capacitor", int2(1, 5), None),
("Fire Control Array", int2(1, 6), Clockwise),
("Store-All Plus", int2(2, 8), None));
("Cargo Bay", int2(2, 8), None));
var player = stage.Player(layer, float2(0, 0), facing: float2(0, 1));
stage.Cargo(player, "Coolant Vent", "Coolant Vent", "Coolant Vent", "Thruster Overdrive", "Thruster Overdrive");

View file

@ -22,16 +22,16 @@ public sealed class RefusedRounds : Scenario
("Cockpit 2x2", int2(4, 6), None),
("Large Drive", int2(3, 0), Reversed),
("Large Drive", int2(5, 0), Reversed),
("Talaria", int2(4, 14), CounterClockwise),
("Talaria", int2(5, 14), Clockwise),
("Core Power", int2(4, 4), None),
("Rugged Drive", int2(4, 14), CounterClockwise),
("Rugged Drive", int2(5, 14), Clockwise),
("Compact Reactor", int2(4, 4), None),
("Smoke Refused", int2(3, 8), None),
("Smoke Refused Beam", int2(6, 8), None),
("Smoke Refused Charge", int2(3, 17), None),
("Autocannon", int2(5, 17), None),
("Iapyx", int2(4, 2), CounterClockwise),
("Iapyx", int2(5, 2), Clockwise),
("not if i see you first", int2(5, 10), None),
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise));
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));

View file

@ -9,6 +9,6 @@ public static class Scenarios
public static readonly Scenario[] Tests =
{
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new WeaponFeel(), new FusedRounds(), new RefusedRounds(), new AiFusedDiscipline(), new LongHaul(), new Pursuit(), new Crossing(), new Belt()
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new WeaponFeel(), new FusedRounds(), new RefusedRounds(), new AiFusedDiscipline(), new LongHaul(), new Pursuit(), new Crossing(), new Belt(), new LockOn()
};
}

View file

@ -4,7 +4,7 @@ using static ItemRotation;
public sealed class StarvedReactor : Scenario
{
public override string Name => "Starved Reactor";
public override string Brief => "Two Spectras on a MoveOnPro, which can fall to a quarter of Core Power's charge, and a bare hull " +
public override string Brief => "Two Light Lasers on a Muon Fusion Reactor, which can fall to a quarter of the Compact Reactor's charge, and a bare hull " +
"to shoot. Verify: starvation reads as starvation, brownout degrades, refills stutter.";
public override uint Seed => 102;
public override bool Ambient => false;
@ -17,14 +17,14 @@ public sealed class StarvedReactor : Scenario
("Cockpit 2x2", int2(2, 6), None),
("Large Drive", int2(1, 0), Reversed),
("Large Drive", int2(3, 0), Reversed),
("Talaria", int2(2, 14), CounterClockwise),
("Talaria", int2(3, 14), Clockwise),
("MoveOnPro", int2(2, 4), None),
("Spectra", int2(1, 8), None),
("Spectra", int2(4, 8), None),
("Iapyx", int2(2, 2), CounterClockwise),
("Iapyx", int2(3, 2), Clockwise),
("not if i see you first", int2(3, 10), None),
("Rugged Drive", int2(2, 14), CounterClockwise),
("Rugged Drive", int2(3, 14), Clockwise),
("Muon Fusion Reactor", int2(2, 4), None),
("Light Laser", int2(1, 8), None),
("Light Laser", int2(4, 8), None),
("Radiator", int2(2, 2), CounterClockwise),
("Radiator", int2(3, 2), Clockwise),
("Compact Sensor", int2(3, 10), None),
("Fire Control Array", int2(1, 6), Clockwise));
stage.Player(starved, float2(0, 0));
stage.Place(stage.Bare("Longinus"), float2(0, 400));

View file

@ -4,9 +4,9 @@ using static ItemRotation;
public sealed class WeaponFeel : Scenario
{
public override string Name => "Weapon Feel";
public override string Brief => "The Smoke Test Hull with a ChargeBlast SG, an Autocannon and a FastBlast+- burst weapon; a bare Longinus dead ahead. " +
"Verify: ChargeBlast SG and Autocannon fire with effects, particles and action-bar icons; the burst weapon's " +
"cadence is as before; a fast release-and-press on the ChargeBlast SG starts no second charge.";
public override string Brief => "The Smoke Test Hull with a Charged Scatter Blaster, an Autocannon and a Cell Blaster burst weapon; a bare Longinus dead ahead. " +
"Verify: Charged Scatter Blaster and Autocannon fire with effects, particles and action-bar icons; the burst weapon's " +
"cadence is as before; a fast release-and-press on the Charged Scatter Blaster starts no second charge.";
public override uint Seed => 107;
public override bool Ambient => false;
@ -21,15 +21,15 @@ public sealed class WeaponFeel : Scenario
("Cockpit 2x2", int2(4, 6), None),
("Large Drive", int2(3, 0), Reversed),
("Large Drive", int2(5, 0), Reversed),
("Talaria", int2(4, 14), CounterClockwise),
("Talaria", int2(5, 14), Clockwise),
("Core Power", int2(4, 4), None),
("ChargeBlast SG", int2(3, 17), None),
("Rugged Drive", int2(4, 14), CounterClockwise),
("Rugged Drive", int2(5, 14), Clockwise),
("Compact Reactor", int2(4, 4), None),
("Charged Scatter Blaster", int2(3, 17), None),
("Autocannon", int2(5, 17), None),
("FastBlast+-", int2(3, 8), None),
("Iapyx", int2(4, 2), CounterClockwise),
("Iapyx", int2(5, 2), Clockwise),
("not if i see you first", int2(5, 10), None),
("Cell Blaster", int2(3, 8), None),
("Radiator", int2(4, 2), CounterClockwise),
("Radiator", int2(5, 2), Clockwise),
("Compact Sensor", int2(5, 10), None),
("Fire Control Array", int2(3, 6), Clockwise));
stage.Player(shooter, float2(0, 0), facing: float2(0, 1));

View file

@ -2,6 +2,7 @@
* 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.Linq;
using CultMath;
// Light signatures split by channel (ruling visibility-per-capability-reveal): an observer learns what a ship
@ -20,15 +21,19 @@ public readonly struct Signature
public readonly SignatureChannel Channel;
public readonly float Strength;
public readonly EquippedItem Item;
// The class this act claims: the true item's Data unless its ship's EW claims otherwise (aetheria-ew,
// classify-from-belief). Stamped by Entity.SetSignature from the ship's claim.
public readonly EquippableItemData Claimed;
public Signature(SignatureChannel channel, float strength, EquippedItem item)
public Signature(SignatureChannel channel, float strength, EquippedItem item, EquippableItemData claimed)
{
Channel = channel;
Strength = strength;
Item = item;
Claimed = claimed;
}
public Signature WithStrength(float strength) => new Signature(Channel, strength, Item);
public Signature WithStrength(float strength) => new Signature(Channel, strength, Item, Claimed);
}
// What a painted ship learns of a beam on it (ruling sensor-stat-set item 4): a Search paint is a ping sweeping
@ -64,11 +69,14 @@ public readonly struct ActiveEmission
{
public readonly EquippedItem Item;
public readonly float HearRange;
// The class this emission claims (see Signature.Claimed); stamped by Entity.Emit.
public readonly EquippableItemData Claimed;
public ActiveEmission(EquippedItem item, float hearRange)
public ActiveEmission(EquippedItem item, float hearRange, EquippableItemData claimed)
{
Item = item;
HearRange = hearRange;
Claimed = claimed;
}
}
@ -90,23 +98,53 @@ public readonly struct PaintWarning
// What an observer has seen `Item` do (ruling visibility-per-capability-reveal): the channel it showed on, the
// bearing at that act and the zone clock then. Held per (subject, item) in the observer's Entity; the latest act
// replaces the earlier one. Position is never carried: only the track gives it.
// replaces the earlier one. Position is never carried: only the track gives it. `Item` stays the true item: it is the
// book's key and what aim reads. `Claimed` is the class the act claimed, and is what every classifying reader reads
// (aetheria-ew, classify-from-belief).
public readonly struct Reveal
{
public readonly EquippedItem Item;
public readonly EquippableItemData Claimed;
public readonly SignatureChannel Channel;
public readonly float2 Bearing;
public readonly float Time;
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time)
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time, EquippableItemData claimed)
{
Item = item;
Claimed = claimed;
Channel = channel;
Bearing = bearing;
Time = time;
}
}
// One weapon row of a ship's schematic as its viewer is entitled to read it (aetheria-ew, classify-from-belief; ruling
// ew-claim-weapon-measure-ruled). Made only by Entity.SchematicRows. `Item` is the true item: the selection mark's key and,
// on the viewer's own ship, the live gear. `Claimed` is the class the row shows. The range label is the live range on
// the viewer's own ship and the claimed design's rated range on any other; the viewer never sees a target weapon's
// quality, wear or power. A renderer shows these and decides nothing.
public readonly struct SchematicRow
{
public readonly EquippedItem Item;
public readonly EquippableItemData Claimed;
private readonly bool _own;
internal SchematicRow(EquippedItem item, EquippableItemData claimed, bool own)
{
Item = item;
Claimed = claimed;
_own = own;
}
// What a renderer compares to know the rows changed. Range is a reading, not part of the key.
public (EquippedItem, EquippableItemData) Key => (Item, Claimed);
public float Range => _own
? Item.Behaviors.OfType<Weapon>().First().Range
: Claimed.Behaviors.OfType<WeaponData>().First().RatedRange;
}
// A subject's removal from a zone as the zone recorded it (death, dock, warp, any removal): where it was and how
// bright its light was (its Visibility at removal). Judged by each observer's Sense under the
// light-act gate; notification only, one tick long, it decides nothing.
@ -127,7 +165,9 @@ public readonly struct Departure
// How much an observer holds of one subject, derived on read by Entity.ContactOf from its track, its reveals and
// the zone clock. Detected: tracked, or a reveal younger than GameplaySettings.BearingMemory. Tracked: exactly
// membership of VisibleEntities. Classified: detected, with any reveal in the book. Identified: the track has reached
// TargetGearInfoThreshold. Bearing is the true bearing while tracked, else the freshest reveal's.
// TargetGearInfoThreshold, and holds a hull belief: Profile, the hull class this observer believes the subject is (null
// below Identified; aetheria-ew, classify-from-belief; seen-through arrives with the judge). Bearing is the true
// bearing while tracked, else the freshest reveal's.
public readonly struct Contact
{
public readonly bool Detected;
@ -136,9 +176,11 @@ public readonly struct Contact
public readonly bool Identified;
public readonly float2 Bearing;
public readonly float Info;
public readonly HullData Profile;
public Contact(bool detected, bool tracked, bool classified, bool identified, float2 bearing, float info)
public Contact(bool detected, bool tracked, bool classified, bool identified, float2 bearing, float info, HullData profile)
{
Profile = profile;
Detected = detected;
Tracked = tracked;
Classified = classified;

View file

@ -394,6 +394,10 @@ public class Zone
// After StepClouds and before FireControl.Step: each floating item drifts as a KinematicBody on the zone's force
// and is removed once its lifetime has run out. A held item is neither integrated nor expired: it is placed
// between where it was claimed and its holder, now that the entities have moved.
// The curve a held item rides from where it was claimed to its holder: the one owner, which the test scene's
// stand-in sim and tendril call rather than spell again.
public static float HeldPull(float progress) => smoothstep(0f, 1f, progress);
private void StepFloatingItems(float deltaTime)
{
var settings = _itemManager.GameplaySettings;
@ -401,7 +405,7 @@ public class Zone
{
if (floating.Holder != null)
{
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, smoothstep(0f, 1f, floating.HeldProgress));
floating.Body.Position = lerp(floating.HeldFrom, floating.Holder.Position.xz, HeldPull(floating.HeldProgress));
continue;
}
floating.Body.Step(deltaTime, GetForce(floating.Body.Position), settings);

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,45 @@ 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;
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();
@ -144,10 +196,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);

View file

@ -58,7 +58,8 @@ public class SchematicDisplay : MonoBehaviour
public class SchematicDisplayItem
{
public EquippedItem Item;
// The row Entity.SchematicRows derived: which item, which class and which range it shows.
public SchematicRow Row;
public SchematicListElement ListElement;
public IProgressBehavior Cooldown;
// public ItemUsage ItemUsage;
@ -93,15 +94,12 @@ public class SchematicDisplay : MonoBehaviour
CargoTemperatureLabel.text = "N/A";
}
BuildRows(_enemy ? RevealedWeapons() : entity.Equipment.Where(IsWeapon));
BuildRows(WeaponRows());
}
private static bool IsWeapon(EquippedItem item) => item.Behaviors.Any(b => b.Data is WeaponData);
// An enemy schematic lists what `_player` has seen the ship's weapons do: the weapon items of the player's book on it,
// in book order (ruling reveal-ends-on-perception), never the ship's live gear. The player's own schematic is its
// equipment; there is nothing to reveal about your own ship.
private IEnumerable<EquippedItem> RevealedWeapons() => _player.RevealsOf(_entity).Select(r => r.Item).Where(IsWeapon);
// Which rows, which class and which range label are Entity.SchematicRows' decision (aetheria-ew, classify-from-belief);
// this display only renders them.
private IEnumerable<SchematicRow> WeaponRows() => (_player ?? _entity).SchematicRows(_entity);
private void ReturnRows()
{
@ -111,21 +109,21 @@ public class SchematicDisplay : MonoBehaviour
_schematicItems = null;
}
private void BuildRows(IEnumerable<EquippedItem> items)
private void BuildRows(IEnumerable<SchematicRow> rows)
{
_schematicItems = items
_schematicItems = rows
.Select(x => new SchematicDisplayItem
{
Item = x,
Row = x,
ListElement = ListElementPrototype.Instantiate<SchematicListElement>(),
Cooldown = _enemy ? null : (IProgressBehavior) x.Behaviors.FirstOrDefault(b=> b is IProgressBehavior),
Cooldown = _enemy ? null : (IProgressBehavior) x.Item.Behaviors.FirstOrDefault(b=> b is IProgressBehavior),
// ItemUsage = _enemy ? null : (ItemUsage) x.Behaviors.FirstOrDefault(b=> b is ItemUsage),
Weapon = (Weapon) x.Behaviors.FirstOrDefault(b=>b is Weapon)
Weapon = _enemy ? null : (Weapon) x.Item.Behaviors.FirstOrDefault(b=>b is Weapon)
})
.ToArray();
foreach (var x in _schematicItems)
{
if(x.Item.Data is WeaponItemData weaponItemData)
if(x.Row.Claimed is WeaponItemData weaponItemData)
x.ListElement.ShowWeapon(weaponItemData);
//x.ListElement.Label.text = x.Item.EquippableItem.Name;
if (!_enemy)
@ -197,10 +195,10 @@ public class SchematicDisplay : MonoBehaviour
// A reveal gained or ended after selection changes the rows with no event to hook: rebuild when the
// revealed weapon set differs from the rows' items.
if (_enemy && !RevealedWeapons().SequenceEqual(_schematicItems.Select(x => x.Item)))
if (_enemy && !WeaponRows().Select(r => r.Key).SequenceEqual(_schematicItems.Select(x => x.Row.Key)))
{
ReturnRows();
BuildRows(RevealedWeapons());
BuildRows(WeaponRows());
}
// Cut 2 (docs/fire-control-cut.md): marks whichever row is the player's current aim point.
@ -208,7 +206,7 @@ public class SchematicDisplay : MonoBehaviour
// as info decays, with no event to hook.
if (_enemy && _schematicItems != null)
foreach (var x in _schematicItems)
x.ListElement.SetSelected(x.Item == _player.ResolvedTargetItem);
x.ListElement.SetSelected(x.Row.Item == _player.ResolvedTargetItem);
if(VisibilityLabel && !_enemy)
VisibilityLabel.text = ((int)_entity.Visibility).ToString();
@ -226,11 +224,11 @@ public class SchematicDisplay : MonoBehaviour
{
if (!_enemy)
{
var itemData = _entity.ItemManager.GetData(x.Item.EquippableItem);
x.ListElement.HeatFill.anchorMax = new Vector2(unlerp(itemData.MinimumTemperature, itemData.MaximumTemperature, x.Item.Temperature), 0);
var itemData = _entity.ItemManager.GetData(x.Row.Item.EquippableItem);
x.ListElement.HeatFill.anchorMax = new Vector2(unlerp(itemData.MinimumTemperature, itemData.MaximumTemperature, x.Row.Item.Temperature), 0);
if (x.Cooldown != null)
x.ListElement.CooldownFill.anchorMax = new Vector2(x.Cooldown.Progress, 1);
x.ListElement.DurabilityLabel.text = $"{(int)(x.Item.EquippableItem.Durability / itemData.Durability * 100)}%";
x.ListElement.DurabilityLabel.text = $"{(int)(x.Row.Item.EquippableItem.Durability / itemData.Durability * 100)}%";
if (x.Weapon.WeaponData.AmmoType.IsSet())
{
if(x.Weapon.WeaponData.MagazineSize > 1)
@ -240,11 +238,8 @@ public class SchematicDisplay : MonoBehaviour
}
}
if (x.Weapon.WeaponData != null)
{
if(x.ListElement.RangeLabel)
x.ListElement.RangeLabel.text = ((int) x.Weapon.Range).ToString();
}
if (x.ListElement.RangeLabel)
x.ListElement.RangeLabel.text = ((int) x.Row.Range).ToString();
}
}
}