Merge cut missile-stats (Soul verdict cut-missile-stats.s2)

This commit is contained in:
Meta Vens 2026-10-12 20:54:30 +02:00
commit a19e3ba647
11 changed files with 539 additions and 29 deletions

View file

@ -80,11 +80,13 @@ public static class CultRecordRefs
{
StatValidation.ValidateHeatResponse(data);
StatValidation.ValidateStatModifiers(data.Name, data.Behaviors);
StatValidation.ValidateMunitionParts(data.Name, data.Behaviors);
StatValidation.ValidateRoleUsage(data.Name, data.Roles, data.Behaviors);
}
if (document is ConsumableItemData consumable)
{
StatValidation.ValidateStatModifiers(consumable.Name, consumable.Behaviors);
StatValidation.ValidateMunitionParts(consumable.Name, consumable.Behaviors);
StatValidation.ValidateRoleUsage(consumable.Name, consumable.Roles, consumable.Behaviors);
}
}

View file

@ -184,6 +184,11 @@ public interface IPowerConsumer
Union(40, typeof(MineLayerData)),
Union(41, typeof(ThrottleLockData)),
Union(42, typeof(VapourDumpData)),
Union(43, typeof(MunitionHullData)),
Union(44, typeof(MunitionThrusterData)),
Union(45, typeof(MunitionSeekerData)),
Union(46, typeof(MunitionControlData)),
Union(47, typeof(MunitionFuelData)),
JsonObject(MemberSerialization.OptIn)]
public abstract class BehaviorData
{

View file

@ -18,14 +18,7 @@ public class LauncherData : LockWeaponData
[InspectableAnimationCurve, JsonProperty("liftCurve"), Key(28)]
public float4[] LiftCurve;
[Inspectable, JsonProperty("thrust"), Key(29)]
public PerformanceStat Thrust = new PerformanceStat();
[Inspectable, JsonProperty("frequency"), Key(30)]
public float DodgeFrequency;
[Inspectable, JsonProperty("missileSpeed"), Key(31), RuntimeInspectable]
public PerformanceStat MissileVelocity = new PerformanceStat();
// Keys 29-31 were Thrust, DodgeFrequency and MissileVelocity, retired for the munition parts (MunitionParts.cs); do not reuse.
public override Behavior CreateInstance(EquippedItem item)
{
@ -45,14 +38,7 @@ public class GuidedWeaponData : InstantWeaponData
[InspectableAnimationCurve, JsonProperty("liftCurve"), Key(23)]
public float4[] LiftCurve;
[Inspectable, JsonProperty("thrust"), Key(24)]
public PerformanceStat Thrust = new PerformanceStat();
[Inspectable, JsonProperty("frequency"), Key(25)]
public float DodgeFrequency;
[Inspectable, JsonProperty("missileSpeed"), Key(26), RuntimeInspectable]
public PerformanceStat MissileVelocity = new PerformanceStat();
// Keys 24-26 were Thrust, DodgeFrequency and MissileVelocity, retired for the munition parts (MunitionParts.cs); do not reuse.
public override Behavior CreateInstance(EquippedItem item)
{

View file

@ -0,0 +1,125 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Linq;
using MessagePack;
using Newtonsoft.Json;
// A missile type's stats, as five dedicated gear-part behaviours on the launcher's own design, one per gear
// category the entity cut will derive them from (ruling drones-and-munitions-are-entities: "dedicated gear so a
// missile thruster is not the same as a ship thruster"). Ruling missile-stats-now-gear-later, in the operator's
// words: "For now these stats will belong to the missile, later they will be derived from gear." Each part becomes
// a derivation from (or moves onto) that munition gear when munitions are entities.
//
// Each part is its own BehaviorData because every stat walker (role validation, StatModifier targeting, AetherDb
// roles, the trade panel) reads PerformanceStat fields one level deep. The parts are inert: nothing here executes,
// and no ship-wide reader (thrust allocator, sensing, signatures) reads them, because a missile thruster is not a
// ship thruster and a seeker is not a ship sensor. MunitionParts.Of is the one read path.
public abstract class MunitionPartData : BehaviorData
{
public override Behavior CreateInstance(EquippedItem item) => new MunitionPart(this, item);
public override Behavior CreateInstance(ConsumableItemEffect consumable) => new MunitionPart(this, consumable);
}
// What a part instantiates to: no state and no override, so Execute stays the default true.
public sealed class MunitionPart : Behavior
{
public MunitionPart(MunitionPartData data, EquippedItem item) : base(data, item) { }
public MunitionPart(MunitionPartData data, ConsumableItemEffect consumable) : base(data, consumable) { }
}
public static class MunitionParts
{
// The design's one part of that type. Catalog load and Upsert refuse a munition without exactly one of each.
public static T Of<T>(EquippedItem item) where T : MunitionPartData =>
item.Behaviors.Select(b => b.Data).OfType<T>().Single();
}
// The missile hull.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public sealed class MunitionHullData : MunitionPartData
{
[Inspectable, JsonProperty("durability"), Key(1), RuntimeInspectable]
public PerformanceStat Durability = new PerformanceStat();
// Metres.
[Inspectable, JsonProperty("radius"), Key(2)]
public PerformanceStat Radius = new PerformanceStat();
}
// The missile thruster.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public sealed class MunitionThrusterData : MunitionPartData
{
// The main motor, m/s^2.
[Inspectable, JsonProperty("acceleration"), Key(1), RuntimeInspectable]
public PerformanceStat Acceleration = new PerformanceStat();
// The sideways acceleration cap, m/s^2.
[Inspectable, JsonProperty("lateral"), Key(2)]
public PerformanceStat Lateral = new PerformanceStat();
// m/s.
[Inspectable, JsonProperty("maxSpeed"), Key(3), RuntimeInspectable]
public PerformanceStat MaxSpeed = new PerformanceStat();
// The strength of this motor's DrivePlume at full throttle, as ThrusterData.Visibility. A datum only: it is
// never registered as the launching ship's signature.
[Inspectable, JsonProperty("visibility"), Key(4)]
public PerformanceStat Visibility = new PerformanceStat();
}
// The seeker. Passive here: it reads a target and emits nothing.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public sealed class MunitionSeekerData : MunitionPartData
{
// Detection power, as SensorData.Sensitivity.
[Inspectable, JsonProperty("sensitivity"), Key(1)]
public PerformanceStat Sensitivity = new PerformanceStat();
// The full cone, in degrees (ruling sensor-stat-set's name and unit).
[Inspectable, JsonProperty("beamWidth"), Key(2)]
public PerformanceStat BeamWidth = new PerformanceStat();
// Metres.
[Inspectable, JsonProperty("range"), Key(3)]
public PerformanceStat Range = new PerformanceStat();
}
// Guidance and its cognition.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public sealed class MunitionControlData : MunitionPartData
{
// The proportional-navigation constant.
[Inspectable, JsonProperty("navigation"), Key(1)]
public PerformanceStat Navigation = new PerformanceStat();
// The share of Lateral spent jinking.
[Inspectable, JsonProperty("evasion"), Key(2)]
public PerformanceStat Evasion = new PerformanceStat();
// Hz.
[Inspectable, JsonProperty("evasionFrequency"), Key(3), RuntimeInspectable]
public PerformanceStat EvasionFrequency = new PerformanceStat();
// Seconds.
[Inspectable, JsonProperty("lifetime"), Key(4)]
public PerformanceStat Lifetime = new PerformanceStat();
// The missile's own skill, 0 to 1 (ruling missile-cognition: "a smarter missile can better spend its delta v").
// Four uses: guidance efficiency, when to jink against point defense, how much fuel it keeps for the terminal,
// and the price of the hit.
[Inspectable, JsonProperty("cognition"), Key(5)]
public PerformanceStat Cognition = new PerformanceStat();
}
// The tank.
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public sealed class MunitionFuelData : MunitionPartData
{
// Total manoeuvre budget, m/s.
[Inspectable, JsonProperty("deltaV"), Key(1)]
public PerformanceStat DeltaV = new PerformanceStat();
}

View file

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

View file

@ -815,6 +815,23 @@ public static class StatValidation
}
}
// Ruling missile-stats-now-gear-later: a munition design (a LauncherData or GuidedWeaponData behaviour) carries exactly
// one of each of the five munition parts, and any other design carries none. Runs wherever ValidateStatModifiers runs,
// so a launcher missing a part is named at catalog load and Upsert rather than at its first launch.
public static void ValidateMunitionParts(string ownerName, IEnumerable<BehaviorData> behaviors)
{
var all = (behaviors ?? Enumerable.Empty<BehaviorData>()).Where(b => b != null).ToArray();
var isMunition = all.Any(b => b is LauncherData || b is GuidedWeaponData);
foreach (var part in new[] { typeof(MunitionHullData), typeof(MunitionThrusterData), typeof(MunitionSeekerData), typeof(MunitionControlData), typeof(MunitionFuelData) })
{
var count = all.Count(b => b.GetType() == part);
var expected = isMunition ? 1 : 0;
if (count != expected)
throw new InvalidOperationException(
$"{ownerName}: carries {count} {part.Name}, but a {(isMunition ? "munition" : "non-munition")} design carries {expected}");
}
}
// Cut 7 (docs/stats-and-power-cut.md): "a stat naming a role its design lacks" must be refused, not silently
// fall back to the lot's own workmanship (Lot.QualityForRole already falls back for a role a lot never rolled
// -- that is the intended forward-compat path for an old lot meeting a newly-authored role. This is the other