Merge cut vector-rows-record (Soul verdict cut-vector-rows-record.s3)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
27094e8859
7 changed files with 614 additions and 40 deletions
|
|
@ -25,6 +25,11 @@ public class ThrusterData : BehaviorData
|
|||
|
||||
[Inspectable, CultInspectorAssetGuid, JsonProperty("Particles"), Key(5)]
|
||||
public string ParticlesPrefab;
|
||||
|
||||
// The gimbal's slew rate in degrees per second: the part's own stat, as a weapon's traverse is
|
||||
// (ruling mount-arc-gun-traverse). The swing limit is the hardpoint's FiringArc, the row its Row.
|
||||
[Inspectable, JsonProperty("slew"), Key(6), RuntimeInspectable]
|
||||
public PerformanceStat Slew = new PerformanceStat();
|
||||
|
||||
public override Behavior CreateInstance(EquippedItem item)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -566,7 +566,12 @@ public class HardpointData
|
|||
[Inspectable, JsonProperty("armor"), Key(5)] public float Armor;
|
||||
// Full arc width in degrees a weapon on this hardpoint may bear across; 0 means "use
|
||||
// GameplaySettings.FiringArc" (docs/fire-control-cut.md R6). Turrets author 360 (Q1).
|
||||
// On a Thruster hardpoint it is the full gimbal width: the row swings +-FiringArc/2 about Rotation, 0 is a
|
||||
// fixed thruster, and there is no GameplaySettings fallback.
|
||||
[Inspectable, JsonProperty("firingArc"), Key(6)] public float FiringArc;
|
||||
// Thruster hardpoints sharing a non-empty Row swing as one row; a Thruster hardpoint with FiringArc > 0 and no Row
|
||||
// is a row of one. Only a Thruster hardpoint may state one.
|
||||
[Inspectable, JsonProperty("row"), Key(7)] public string Row;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -302,6 +302,11 @@ public static class ShipAuthoringStore
|
|||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} has an unknown rotation {(int)hardpoint.Rotation}.");
|
||||
if (!float.IsFinite(hardpoint.Armor) || hardpoint.Armor < 0 || !float.IsFinite(hardpoint.FiringArc) || hardpoint.FiringArc < 0)
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} armor and firing arc must be finite and not negative.");
|
||||
// A thruster's gimbal range is the judge's alone, a row of one included: no thruster swings further than a turn.
|
||||
if (hardpoint.Type == HardpointType.Thruster && hardpoint.FiringArc > 360f)
|
||||
throw new InvalidOperationException($"{ship.Id}: thruster hardpoint {hardpoint.Transform} firing arc may be at most 360 degrees.");
|
||||
if (!string.IsNullOrEmpty(hardpoint.Row) && hardpoint.Type != HardpointType.Thruster)
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} is a {hardpoint.Type} hardpoint, and only a Thruster hardpoint may state a row.");
|
||||
if (hardpoint.Shape?.Cells == null || !hardpoint.Shape.Cells.Cast<bool>().Any(occupied => occupied))
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} has no cells.");
|
||||
for (var cellX = 0; cellX < hardpoint.Shape.Width; cellX++)
|
||||
|
|
@ -325,14 +330,23 @@ public static class ShipAuthoringStore
|
|||
if (mountRole != null && mountAnchor.Role != mountRole)
|
||||
throw new InvalidOperationException($"{ship.Id}: {hardpoint.Type} hardpoint {hardpoint.Transform} needs a {mountRole} anchor of that id.");
|
||||
}
|
||||
foreach (var row in mounts.Values.Where(hardpoint => hardpoint.Type == HardpointType.Thruster && !string.IsNullOrEmpty(hardpoint.Row))
|
||||
.GroupBy(hardpoint => hardpoint.Row, StringComparer.Ordinal))
|
||||
{
|
||||
var lead = row.First();
|
||||
foreach (var hardpoint in row)
|
||||
if (hardpoint.Rotation != lead.Rotation || hardpoint.FiringArc != lead.FiringArc)
|
||||
throw new InvalidOperationException($"{ship.Id}: thruster hardpoints {lead.Transform} and {hardpoint.Transform} are in row {row.Key} and must share Rotation and FiringArc.");
|
||||
if (!(lead.FiringArc > 0f))
|
||||
throw new InvalidOperationException($"{ship.Id}: thruster row {row.Key} must state a gimbal FiringArc above 0.");
|
||||
}
|
||||
foreach (var anchor in anchors)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(anchor.ParentId) && !mounts.ContainsKey(anchor.ParentId))
|
||||
throw new InvalidOperationException($"{ship.Id}: anchor {anchor.Id} has unknown hardpoint parent {anchor.ParentId}.");
|
||||
// A mount-role anchor is the mount of a hardpoint of its type, never a loose node. The hardpoint loop above has
|
||||
// already refused one whose id names a hardpoint of another type, so here it only has to name a hardpoint.
|
||||
if ((anchor.Role == "thruster-emitter" || anchor.Role == "radiator-mesh" || anchor.Role == "weapon-mount") &&
|
||||
!mounts.ContainsKey(anchor.Id))
|
||||
if ((RigChains.Actuates(anchor.Role) || anchor.Role == "radiator-mesh") && !mounts.ContainsKey(anchor.Id))
|
||||
throw new InvalidOperationException($"{ship.Id}: {anchor.Role} anchor {anchor.Id} must be the mount of a hardpoint of its type.");
|
||||
if (anchor.Role == "weapon-muzzle" &&
|
||||
(string.IsNullOrEmpty(anchor.ParentId) || !IsWeapon(mounts[anchor.ParentId].Type)))
|
||||
|
|
@ -411,29 +425,66 @@ public static class ShipAuthoringStore
|
|||
|
||||
foreach (var anchor in anchors.Where(anchor => !string.IsNullOrEmpty(anchor.Joint)))
|
||||
{
|
||||
if (anchor.Role == "thruster-emitter" || anchor.Role == "radiator-mesh")
|
||||
throw new InvalidOperationException($"{ship.Id}: {anchor.Role} anchor {anchor.Id} may not ride a joint (follow-up gimballed-thrusters).");
|
||||
if (anchor.Role != "weapon-mount")
|
||||
throw new InvalidOperationException($"{ship.Id}: {anchor.Role} anchor {anchor.Id} may not name a joint; only a weapon mount rides one.");
|
||||
if (anchor.Role == "radiator-mesh")
|
||||
throw new InvalidOperationException($"{ship.Id}: {anchor.Role} anchor {anchor.Id} may not ride a joint.");
|
||||
if (!RigChains.Actuates(anchor.Role))
|
||||
throw new InvalidOperationException($"{ship.Id}: {anchor.Role} anchor {anchor.Id} may not name a joint; only a weapon mount or a thruster emitter rides one.");
|
||||
if (!byId.ContainsKey(anchor.Joint))
|
||||
throw new InvalidOperationException($"{ship.Id}: weapon mount {anchor.Id} names an unknown joint.");
|
||||
throw new InvalidOperationException($"{ship.Id}: {anchor.Role} anchor {anchor.Id} names an unknown joint.");
|
||||
}
|
||||
|
||||
var ridden = anchors.Where(anchor => anchor.Role == "weapon-mount" && !string.IsNullOrEmpty(anchor.Joint))
|
||||
.Select(anchor => (Hardpoint: mounts[anchor.Id], Joint: anchor.Joint)).ToArray();
|
||||
foreach (var (hardpoint, _) in ridden)
|
||||
// A joint has one actuator (ruling one-joint-one-actuator), and RigChains.Drives is the only place that says what drives a
|
||||
// joint: a joint carrying two actuators, or whose reach passes through a joint another actuator drives, is refused here,
|
||||
// whatever the two actuators are.
|
||||
var drives = RigChains.Drives(ship).Select(drive => (drive.Joint, Hardpoint: mounts[drive.Anchor.Id])).ToArray();
|
||||
foreach (var onJoint in drives.GroupBy(drive => drive.Joint, StringComparer.Ordinal))
|
||||
{
|
||||
var lead = onJoint.First().Hardpoint;
|
||||
var other = onJoint.Select(drive => drive.Hardpoint).FirstOrDefault(hardpoint => RigChains.Actuator(hardpoint) != RigChains.Actuator(lead));
|
||||
if (other != null)
|
||||
throw new InvalidOperationException($"{ship.Id}: joint {onJoint.Key} carries more than one actuator ({lead.Transform} and {other.Transform}); a joint has one actuator.");
|
||||
}
|
||||
var driven = RigChains.Driven(ship);
|
||||
foreach (var joint in driven)
|
||||
{
|
||||
var through = RigChains.Chain(ship, joint).SkipLast(1).FirstOrDefault(driven.Contains);
|
||||
if (through != null)
|
||||
throw new InvalidOperationException($"{ship.Id}: joint {joint} reaches through joint {through}, which another actuator drives; a joint has one actuator.");
|
||||
}
|
||||
|
||||
var ridden = drives.Where(drive => IsWeapon(drive.Hardpoint.Type)).ToArray();
|
||||
foreach (var (_, hardpoint) in ridden)
|
||||
if (!(hardpoint.FiringArc > 0f))
|
||||
throw new InvalidOperationException($"{ship.Id}: weapon hardpoint {hardpoint.Transform} rides a joint, so it must state its firing arc above 0.");
|
||||
foreach (var group in ridden.GroupBy(mount => mount.Joint, StringComparer.Ordinal))
|
||||
{
|
||||
var lead = group.First().Hardpoint;
|
||||
foreach (var (hardpoint, _) in group)
|
||||
foreach (var (_, hardpoint) in group)
|
||||
if (hardpoint.Rotation != lead.Rotation || hardpoint.FiringArc != lead.FiringArc)
|
||||
throw new InvalidOperationException($"{ship.Id}: weapon hardpoints {lead.Transform} and {hardpoint.Transform} ride joint {group.Key} and must share Rotation and FiringArc.");
|
||||
var reach = YawReach(ship, group.Key);
|
||||
if (reach < lead.FiringArc)
|
||||
throw new InvalidOperationException($"{ship.Id}: joint {group.Key} chain reaches {reach} degrees of yaw, short of the {lead.FiringArc} degree firing arc of {lead.Transform}.");
|
||||
}
|
||||
|
||||
// A thruster row is a gimbal: its hardpoints' stated FiringArc is how far the row swings, so a row on a joint rides it
|
||||
// whole, and the joint's chain must be able to sweep that arc. A hardpoint with no Row is a row of one.
|
||||
var thrusters = mounts.Values.Where(hardpoint => hardpoint.Type == HardpointType.Thruster).ToArray();
|
||||
var tilted = drives.Where(drive => drive.Hardpoint.Type == HardpointType.Thruster).ToArray();
|
||||
foreach (var (_, hardpoint) in tilted)
|
||||
if (!(hardpoint.FiringArc > 0f))
|
||||
throw new InvalidOperationException($"{ship.Id}: thruster hardpoint {hardpoint.Transform} rides a joint, so it must state its firing arc above 0; a fixed thruster may not ride a joint.");
|
||||
foreach (var onJoint in tilted.GroupBy(drive => drive.Joint, StringComparer.Ordinal))
|
||||
{
|
||||
var lead = onJoint.First().Hardpoint;
|
||||
var members = string.IsNullOrEmpty(lead.Row) ? new[] { lead } : thrusters.Where(hardpoint => hardpoint.Row == lead.Row).ToArray();
|
||||
foreach (var member in members)
|
||||
if (!tilted.Any(drive => drive.Hardpoint == member && drive.Joint == onJoint.Key))
|
||||
throw new InvalidOperationException($"{ship.Id}: thruster hardpoint {member.Transform} is in the row of {lead.Transform} on joint {onJoint.Key} and must ride it too; a row rides one joint whole.");
|
||||
var reach = YawReach(ship, onJoint.Key);
|
||||
if (reach < lead.FiringArc)
|
||||
throw new InvalidOperationException($"{ship.Id}: joint {onJoint.Key} chain reaches {reach} degrees of yaw, short of the {lead.FiringArc} degree gimbal arc of {lead.Transform}.");
|
||||
}
|
||||
}
|
||||
|
||||
// The yaw a driven joint's chain can sweep: the summed range of every degree of freedom whose axis is yaw, capped at a turn.
|
||||
|
|
@ -449,21 +500,31 @@ public static class ShipAuthoringStore
|
|||
}
|
||||
}
|
||||
|
||||
// A rig's driven joints and the chains that move them. A driven joint is one a weapon-mount anchor names. Its chain runs
|
||||
// from the joint up through its ancestors and stops before the nearest ancestor that is itself driven (that joint has
|
||||
// its own gun, and its pose is that gun's business), listed root-first. The judge and the plan both read this one rule.
|
||||
// A rig's actuators and the joints they drive. An actuator is a weapon mount or a thruster emitter that names a joint; a gun
|
||||
// group (every gun on a joint) or a thruster row is one actuator, and a joint has at most one (ruling one-joint-one-actuator).
|
||||
// This is the only place that says which anchors are actuators, so the judge, the binder and the plan all follow it.
|
||||
public static class RigChains
|
||||
{
|
||||
public static HashSet<string> Driven(ShipAuthoring ship) =>
|
||||
new HashSet<string>(ship.Anchors.Where(anchor => anchor.Role == "weapon-mount" && !string.IsNullOrEmpty(anchor.Joint)).Select(anchor => anchor.Joint),
|
||||
StringComparer.Ordinal);
|
||||
public static bool Actuates(string role) => role == "weapon-mount" || role == "thruster-emitter";
|
||||
|
||||
// Every actuator anchor that names a joint, with the joint it drives.
|
||||
public static IEnumerable<(string Joint, ShipAnchor Anchor)> Drives(ShipAuthoring ship) =>
|
||||
ship.Anchors.Where(anchor => Actuates(anchor.Role) && !string.IsNullOrEmpty(anchor.Joint)).Select(anchor => (anchor.Joint, anchor));
|
||||
|
||||
public static HashSet<string> Driven(ShipAuthoring ship) =>
|
||||
new HashSet<string>(Drives(ship).Select(drive => drive.Joint), StringComparer.Ordinal);
|
||||
|
||||
// The actuator a mount belongs to: all guns on a joint are one, a thruster is its row, and a thruster of no row is its own.
|
||||
public static string Actuator(HardpointData hardpoint) =>
|
||||
ShipAuthoringStore.IsWeapon(hardpoint.Type) ? "weapon" : string.IsNullOrEmpty(hardpoint.Row) ? "thruster:" + hardpoint.Transform : "row:" + hardpoint.Row;
|
||||
|
||||
// The joint and its ancestors, root-first. The judge refuses a driven joint whose ancestors another actuator drives,
|
||||
// so for an accepted ship this is the whole chain the actuator's reach is summed over.
|
||||
public static string[] Chain(ShipAuthoring ship, string jointId)
|
||||
{
|
||||
var joints = ship.Joints.ToDictionary(joint => joint.Id, StringComparer.Ordinal);
|
||||
var driven = Driven(ship);
|
||||
var chain = new List<string> { jointId };
|
||||
for (var parent = joints[jointId].Parent; !string.IsNullOrEmpty(parent) && !driven.Contains(parent); parent = joints[parent].Parent)
|
||||
for (var parent = joints[jointId].Parent; !string.IsNullOrEmpty(parent); parent = joints[parent].Parent)
|
||||
chain.Add(parent);
|
||||
chain.Reverse();
|
||||
return chain.ToArray();
|
||||
|
|
|
|||
|
|
@ -216,8 +216,8 @@ public static class ShipModCatalog
|
|||
}
|
||||
|
||||
// The rig the record states must be the rig the model has: every joint is a node, each joint's Parent and each mount's
|
||||
// Joint is the nearest joint above its node, ancestors without an id being walked through by ReadNodeIds, and a thruster or radiator
|
||||
// mesh sits under no joint (follow-up gimballed-thrusters).
|
||||
// Joint is the nearest joint above its node, ancestors without an id being walked through by ReadNodeIds, and a radiator
|
||||
// mesh sits under no joint.
|
||||
private static void BindRig(ShipAuthoring ship, Dictionary<string, (uint Index, bool HasMesh, int Parent)> nodes)
|
||||
{
|
||||
var joints = new HashSet<string>((ship.Joints ?? new List<ShipJoint>()).Select(joint => joint.Id), StringComparer.Ordinal);
|
||||
|
|
@ -239,13 +239,13 @@ public static class ShipModCatalog
|
|||
if (!string.Equals(JointAbove(joint.Id) ?? "", joint.Parent ?? "", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException($"{ship.Id}: joint {joint.Id} must name the nearest joint above its model node as its Parent.");
|
||||
}
|
||||
foreach (var anchor in ship.Anchors.Where(anchor => anchor.Role == "weapon-mount" || anchor.Role == "thruster-emitter" || anchor.Role == "radiator-mesh"))
|
||||
foreach (var anchor in ship.Anchors.Where(anchor => RigChains.Actuates(anchor.Role) || anchor.Role == "radiator-mesh"))
|
||||
{
|
||||
var above = JointAbove(anchor.ModelNodeId);
|
||||
if (string.Equals(above ?? "", anchor.Joint ?? "", StringComparison.Ordinal)) continue;
|
||||
throw new InvalidOperationException(anchor.Role == "weapon-mount"
|
||||
? $"{ship.Id}: weapon mount {anchor.Id} must name the nearest joint above its model node as its Joint."
|
||||
: $"{ship.Id}: {anchor.Role} anchor {anchor.Id} sits under a joint in the model (follow-up gimballed-thrusters).");
|
||||
throw new InvalidOperationException(anchor.Role == "radiator-mesh"
|
||||
? $"{ship.Id}: {anchor.Role} anchor {anchor.Id} sits under a joint in the model."
|
||||
: $"{ship.Id}: {(anchor.Role == "weapon-mount" ? "weapon mount" : "thruster emitter")} {anchor.Id} must name the nearest joint above its model node as its Joint.");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ public sealed class ShipModPlan
|
|||
var muzzles = visual.Anchors.Where(anchor => anchor.Role == "weapon-muzzle")
|
||||
.OrderBy(anchor => anchor.Order).ThenBy(anchor => anchor.Id, StringComparer.Ordinal).ToArray();
|
||||
|
||||
var jointOf = visual.Anchors.Where(anchor => anchor.Role == "weapon-mount" && !string.IsNullOrEmpty(anchor.Joint))
|
||||
.ToDictionary(anchor => anchor.Id, anchor => anchor.Joint, StringComparer.Ordinal);
|
||||
var weapons = hardpoints.Where(hardpoint => ShipAuthoringStore.IsWeapon(hardpoint.Type)).Select(hardpoint => hardpoint.Transform).ToHashSet(StringComparer.Ordinal);
|
||||
var jointOf = RigChains.Drives(visual).Where(drive => weapons.Contains(drive.Anchor.Id))
|
||||
.ToDictionary(drive => drive.Anchor.Id, drive => drive.Joint, StringComparer.Ordinal);
|
||||
var leads = hardpoints.Where(hardpoint => jointOf.ContainsKey(hardpoint.Transform)).GroupBy(hardpoint => jointOf[hardpoint.Transform], StringComparer.Ordinal)
|
||||
.Select(group => (Joint: group.Key, Lead: group.First().Transform));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue