Merge origin/master into damage-falloff

Conflicts: FusedRounds scenario brief and fixture comment, SmokeRoundsSteppedTests, and AetherDb's usage line keep both sides, with
DeathCluster renamed Cluster Cannon. GameData/Aetheria.cc is master's blob; range-falloff is rerun on it in the next commit.
This commit's catalog does not carry the falloff stats, so CatalogRole/falloff catalog tests do not pass here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-14 09:54:44 +02:00
commit 97b7566ee9
65 changed files with 3360 additions and 336 deletions

View file

@ -191,6 +191,8 @@ public interface IPowerConsumer
Union(47, typeof(MunitionFuelData)),
Union(48, typeof(PickupData)),
Union(49, typeof(LaunchSuppressorData)),
Union(50, typeof(ListenerData)),
Union(51, typeof(ScannerData)),
JsonObject(MemberSerialization.OptIn)]
public abstract class BehaviorData
{

View file

@ -0,0 +1,44 @@
/* 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 MessagePack;
using Newtonsoft.Json;
// Passive listening gear (ruling sensor-stat-set): hears other ships' emissions at a multiple of their own range,
// bearing only. It emits nothing; Entity.HearingMultiple asks each active listener at hearing time. Its power is the
// design's EnergyDraw and its activity is the item's Active, as for sensors.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
public class ListenerData : BehaviorData
{
// How many times an emitter's own reach this listener hears it at.
[Inspectable, JsonProperty("multiple"), Key(1), RuntimeInspectable]
public PerformanceStat Multiple = new PerformanceStat();
public override Behavior CreateInstance(EquippedItem item)
{
return new Listener(this, item);
}
public override Behavior CreateInstance(ConsumableItemEffect item)
{
return new Listener(this, item);
}
}
public class Listener : Behavior
{
private readonly ListenerData _data;
public Listener(ListenerData data, EquippedItem item) : base(data, item)
{
_data = data;
}
public Listener(ListenerData data, ConsumableItemEffect item) : base(data, item)
{
_data = data;
}
public float Multiple => Evaluate(_data.Multiple);
public override bool Execute(float dt) => true;
}

View file

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

View file

@ -0,0 +1,98 @@
/* 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 MessagePack;
using Newtonsoft.Json;
using static CultMath.math;
// Inspection (campaign aetheria-release, rulings quiet-gear-identifiable-hull-layout-known, scan-held-beam,
// scan-is-an-act): a gear that, held on the ship's tracked target within Range for ScanTime, reveals everything fitted to
// that target, quiet gear that never acts included. The scan is an emission like a lock: the target is painted and
// third parties hear it at Range over the intercept factor.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
public class ScannerData : BehaviorData
{
// The planar distance to the target within which a scan builds.
[Inspectable, JsonProperty("range"), Key(1), RuntimeInspectable]
public PerformanceStat Range = new PerformanceStat();
// Seconds a held scan takes to finish; a broken scan decays over the same span.
[Inspectable, JsonProperty("scanTime"), Key(2), RuntimeInspectable]
public PerformanceStat ScanTime = new PerformanceStat();
// The ratio of how far this scanner sees to how far others hear it (ruling sensor-stat-set); a design without it
// reads as 1.
[Inspectable, JsonProperty("interceptFactor"), Key(3)]
public PerformanceStat InterceptFactor;
public override Behavior CreateInstance(EquippedItem item)
{
return new Scanner(this, item);
}
public override Behavior CreateInstance(ConsumableItemEffect item)
{
return new Scanner(this, item);
}
}
public class Scanner : Behavior, IProgressBehavior
{
private readonly ScannerData _data;
private Entity _target;
private float _built;
private double _heldAt;
// The scan's progress now: what Execute last built, less ScanTime-relative decay over the time since a tick last held
// it (ruling scan-held-beam: a broken scan decays over ScanTime, the item inactive or destroyed included). Derived from
// the zone clock, never from tick order, so a scanner decays on the tick it breaks whether its host runs Execute
// before the always-updated pass (EquippedItem) or after it (ConsumableItemEffect). The one decay site.
public float Progress => Remaining(_heldAt);
public Scanner(ScannerData data, EquippedItem item) : base(data, item)
{
_data = data;
}
public Scanner(ScannerData data, ConsumableItemEffect item) : base(data, item)
{
_data = data;
}
private float Remaining(double since)
{
if (_built <= 0f) return 0f;
var now = Entity.Zone?.ExactTime ?? since;
return saturate(_built - (float) (now - since) / Evaluate(_data.ScanTime));
}
// Runs only while the item is Active. The scan builds while the ship's own Target is a tracked ship within Range, and
// is painted and emitted on every tick it builds; at 1 the target's fitted items enter the book and the scan starts over.
// A tick that does not hold the scan builds nothing, and the zone clock carries its decay (Progress).
public override bool Execute(float dt)
{
var target = Entity.Target.Value.Entity;
if (_target != target)
{
_built = 0f;
_target = target;
}
var range = Evaluate(_data.Range);
if (target != null && Entity.VisibleEntities.Contains(target) && Entity.TargetRange <= range)
{
// Decay over the ticks since the last held one, not counting this tick, which holds.
var progress = saturate(Remaining(_heldAt + dt) + dt / Evaluate(_data.ScanTime));
_built = progress;
_heldAt = Entity.Zone?.ExactTime ?? _heldAt;
target.ReceivePaint(Entity, Item, PaintMode.Scan, progress);
Entity.Emit(Item, range / (_data.InterceptFactor == null ? 1f : Evaluate(_data.InterceptFactor)));
if (progress >= 1f)
{
Entity.RecordInspection(target);
_built = 0f;
}
}
return true;
}
}

View file

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

View file

@ -247,7 +247,7 @@ public abstract class Entity
// sensor-stat-set): written in Update, read by Sense in the same tick, cleared by the next PublishInbox.
private readonly List<ActiveEmission> _emissions = new List<ActiveEmission>();
// What this observer has seen each ship's items do (ruling visibility-per-capability-reveal), keyed (subject,
// item). Written only by RecordReveal (an act enters) and EndReveals (an end the observer perceives, or a departed
// item). Written only by RecordReveal (an act, or a scan through RecordInspection, enters) and EndReveals (an end the observer perceives, or a departed
// subject's entries aged past BearingMemory), cleared by Deactivate (ruling reveal-ends-on-perception).
private readonly Dictionary<Entity, Dictionary<EquippedItem, Reveal>> _reveals = new Dictionary<Entity, Dictionary<EquippedItem, Reveal>>();
private readonly List<EquippedItem> _endedItems = new List<EquippedItem>();
@ -714,8 +714,23 @@ public abstract class Entity
}
// How much of an emission's reach this observer's instruments hear it at (ruling sensor-stat-set: listeners hear at
// a multiple of an emitter's own range): 1 with an active sensor, 0 without. Listener gear extends it.
private int HearingMultiple() => ActiveSensors().Count > 0 ? 1 : 0;
// a multiple of an emitter's own range): the largest of 1 (any active sensor) and each active listener's Multiple,
// 0 with neither. Catalog values come from AetherDb sensor-stat-set.
private float HearingMultiple()
{
var multiple = ActiveSensors().Count > 0 ? 1f : 0f;
for (var i = 0; i < Equipment.Count; i++)
{
var item = Equipment[i];
if (!item.Active.Value) continue;
foreach (var behavior in item.Behaviors)
if (behavior is Listener listener) multiple = max(multiple, listener.Multiple);
}
foreach (var effect in _activeConsumables)
foreach (var behavior in effect.Behaviors)
if (behavior is Listener listener) multiple = max(multiple, listener.Multiple);
return multiple;
}
// Cut 12.3 (docs/fire-control-cut.md): "armour absorbs first" -- the per-cell armour phase, split out of
// what used to be Absorb's own first half (moved verbatim from DamageSchematic's per-cell body, Cut 3) so
@ -934,14 +949,14 @@ public abstract class Entity
_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):
// this observer saw `item` of `subject` act on `channel`, now, from this bearing. An item-less act (hull
// The reveal book's one way in and its one membership gate (callers: Sense, PublishInbox and RecordInspection, all in
// this file): this observer saw `item` of `subject` act on `channel`, now, from this bearing. An item-less act (hull
// 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. `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)
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))
@ -949,6 +964,21 @@ public abstract class Entity
book[item] = new Reveal(item, channel, normalize((subject.Position - Position).xz), Zone.Time, claimed);
}
// A finished scan (Scanner.Execute; rulings scan-is-an-act, quiet-gear-identifiable-hull-layout-known: quiet gear
// that never acts stays identifiable through special gear): this observer learns every fitted item of `subject`,
// channel null. It records what the item claims, not what it is (ClaimOf, as every act's carrier stamps it: the
// aetheria-ew one judge, classify-from-belief), skips the hull (its class is the track's to give) and a destroyed
// item, and goes through RecordReveal's gate like any act. It calls no EndReveals: a scan ends nothing
// (reveal-ends-on-perception).
internal void RecordInspection(Entity subject)
{
foreach (var item in subject.Equipment)
{
if (item == subject.EquippedHull || item.EquippableItem.IsDestroyed) continue;
RecordReveal(subject, item, null, subject.ClaimOf(item));
}
}
// This observer resolves `subject`'s gear (ContactOf's Identified gate) and `item` of it is destroyed: the one rule
// for a destroyed item an observer perceives, read by the book's entry (RecordReveal) and its end (Sense).
private bool PerceivesDestroyed(Entity subject, EquippedItem item) =>

View file

@ -9,7 +9,7 @@ using float2 = CultMath.float2;
public class ZoneEnvironment
{
[JsonProperty("nebula"), Key(0)] public NebulaSettings Nebula;
[JsonProperty("flow"), Key(1)] public FlowSettings Flow;
// Key 1 is retired (it held the flow, now PlanetSettings.Flow); never reuse it.
[JsonProperty("noise"), Key(2)] public NoiseSettings Noise;
[JsonProperty("lighting"), Key(3)] public AmbientLightingSettings Lighting;
[JsonProperty("grid"), Key(4)] public GridSettings Grid;

View file

@ -0,0 +1,78 @@
using CultMath;
using static CultMath.math;
using float2 = CultMath.float2;
using float3 = CultMath.float3;
/// <summary>
/// The global flow law: the one definition of the drift that stardust and nebula follow and that the sim knows.
/// A pure function of the zone's FlowSettings, a position, a scroll and a downhill gradient; nothing here keeps
/// a clock. Zone.Flow evaluates it at zone time. Assets/Shaders/Volumetric.cginc (globalFlow, slopeFlow) is its
/// HLSL lowering, driven by VolumeSampling.LowerFlow and pinned to it by Aetheria.EditorTools.FlowFieldParityVerify.
/// </summary>
public static class FlowField
{
// The second octave of the noise samples at position * Golden (Volumetric.cginc Tri3D).
const float Golden = 1.61803398875f;
// The scroll of the noise field, in field units: speed times zone time, for each of the two octaves, as a phase.
// Every term of Tri3D reaches the scrolled axis through frac, so a phase is only meaningful modulo 1: the fold
// happens here in double, where zone time is exact, and the float that reaches the shaders is in [0, 1) however
// old the zone is. x is the first octave's phase, y the second's (the second octave's axis is scaled by Golden).
public static float2 Scroll(FlowSettings settings, double zoneTime)
{
var scroll = (double) settings.GlobalScrollSpeed * zoneTime;
return float2(Phase(scroll), Phase(scroll * Golden));
}
// A phase in [0, 1) strictly: a double just below an integer rounds up to 1f in the cast, and 1 is 0 modulo 1.
static float Phase(double value)
{
var phase = (float) frac(value);
return phase < 1f ? phase : 0f;
}
// The phase of a cycle of `period` seconds at zone time, in [0, 1). The nebula's flow-advected noise cycles and
// Stardust's particle lifetimes take their phase from here, so drift follows sim time and stops with the sim.
public static float Cycle(double zoneTime, float period) => period == 0f ? 0f : Phase(zoneTime / period);
// The procedural term (Volumetric.cginc globalFlow): a Tri3D field of position / scale, scrolled along y.
// Skipped when its amplitude is zero, as the shader's FLOW_GLOBAL keyword skips it.
public static float3 Global(FlowSettings settings, float3 position, float2 scroll)
{
if (settings.GlobalAmplitude == 0f) return float3.zero;
var field = position / settings.GlobalScale;
var sample = Tri3D(field - float3(0f, scroll.x, 0f), field * Golden - float3(0f, scroll.y, 0f)) * settings.GlobalAmplitude;
sample.y *= .5f;
return sample;
}
// The slope term (Volumetric.cginc slopeFlow): swirl around and slide along the gravity gradient.
// `downhill` is the DOWNHILL gradient, -grad(height) in world xz. The shader's texel gradient is that same
// vector (getUV negates the world x and z axes), and Zone.GetNormal's xz is proportional to it.
// Skipped when both amplitudes are zero (the FLOW_SLOPE keyword), and zero for a zero gradient.
public static float3 Slope(FlowSettings settings, float2 downhill)
{
if (settings.SlopeAmplitude == 0f && settings.SwirlAmplitude == 0f) return float3.zero;
if (dot(downhill, downhill) == 0f) return float3.zero;
var direction = normalize(downhill);
return float3(direction.y, 0f, -direction.x) * settings.SwirlAmplitude
+ float3(direction.x, 0f, direction.y) * settings.SlopeAmplitude;
}
public static float3 At(FlowSettings settings, float3 position, float2 scroll, float2 downhill) =>
Global(settings, position, scroll) + Slope(settings, downhill);
// Volumetric.cginc tri / tri3 / Tri3D, term for term.
static float Tri(float x) => abs(frac(x) - .5f);
static float3 Tri3(float3 p) =>
float3(Tri(p.z + Tri(p.y)), Tri(p.z + Tri(p.x)), Tri(p.y + Tri(p.x)));
// The two octaves' positions are given already scrolled (see Scroll).
static float3 Tri3D(float3 p, float3 pGolden)
{
var t1 = normalize(Tri3(p));
var t2 = normalize(Tri3(pGolden));
return cross(t1, t2);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8105a45840c4552459aaf44e6c0e19ce

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 the DeathCluster at the fragile hull and, once it is hurt, a Smoke " +
"ahead and it stops and bursts on it; fire the Cluster Cannon at the fragile hull and, once it is hurt, a Smoke " +
"Proximity round at it: the hull dies 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 effective range (full damage; the scenario test reads the bound from the weapon); Close is inside Smoke Proximity's blast radius
// mount's arc. Fragile is a bare Longinus inside the Cluster Cannon's effective range (full damage; the scenario test reads the bound from the weapon); 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(80, 240),
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

@ -24,19 +24,19 @@ public sealed class LockOn : 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), facing: float2(0, 1));
stage.Place(fighter, HostileAt, facing: normalize(-HostileAt), stance: ScenarioStance.Hostile, piloted: true);

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

@ -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

@ -38,11 +38,13 @@ public readonly struct Signature
// What a painted ship learns of a beam on it (ruling sensor-stat-set item 4): a Search paint is a ping sweeping
// it (Strength 0); a Track paint is a lock (Strength is the emitter's own lock progress, 0..1, ruling
// lock-warning-strength). Never summed.
// lock-warning-strength). A Scan paint is a held scanner: Strength is its scan progress 0..1 (ruling scan-is-an-act).
// Never summed.
public enum PaintMode
{
Search,
Track
Track,
Scan
}
// An ally's ping crossed `Subject` at `IlluminatedRange` through `IlluminatedObscuration`; `MaxRange` is that
@ -100,16 +102,17 @@ public readonly struct PaintWarning
// 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. `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).
// (aetheria-ew, classify-from-belief). A null `Channel` is a reveal by the observer's own scan (Entity.RecordInspection),
// whose `Claimed` is the subject's claim for the item.
public readonly struct Reveal
{
public readonly EquippedItem Item;
public readonly EquippableItemData Claimed;
public readonly SignatureChannel Channel;
public readonly SignatureChannel? Channel;
public readonly float2 Bearing;
public readonly float Time;
public Reveal(EquippedItem item, SignatureChannel channel, float2 bearing, float time, EquippableItemData claimed)
public Reveal(EquippedItem item, SignatureChannel? channel, float2 bearing, float time, EquippableItemData claimed)
{
Item = item;
Claimed = claimed;

View file

@ -32,6 +32,9 @@ public class PlanetSettings
public float SecureAreaRadiusMultiplier = .45f;
public ExponentialCurve OrbitPeriod;
// The global flow stardust and nebula follow and the sim knows (FlowField). Zero flow by default.
public FlowSettings Flow = new FlowSettings();
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]

View file

@ -110,7 +110,7 @@ public class Zone
}
// Zone time at full precision, for the index's orbital keys, which must agree with ChunkPose's own.
internal double ExactTime => _time;
public double ExactTime => _time;
public ZonePack Pack { get; }
public GalaxyZone GalaxyZone { get; }
public Galaxy Galaxy { get; }
@ -695,6 +695,20 @@ public class Zone
return GravityAcceleration(new float2(normal.x, normal.z), Settings.GravityStrength);
}
// The global flow velocity at `position` in zone time, in u/s (FlowField is the law). The slope term reads the
// downhill gradient, which GetNormal's xz is proportional to.
public float3 Flow(float3 position)
{
var normal = GetNormal(position.xz);
return FlowField.At(Settings.Flow, position, FlowScroll, new float2(normal.x, normal.z));
}
// The flow's noise scroll at zone time (Zone.Flow evaluates at exactly this); lowered to the shaders' _FlowScroll.
public float2 FlowScroll => FlowField.Scroll(Settings.Flow, _time);
// The phase of a cycle of `period` seconds at zone time, for presenters that animate on the flow.
public float CyclePhase(float period) => FlowField.Cycle(_time, period);
public static float PowerPulse(float x, float exponent)
{
x *= 2;