Ship mounts: anchors only for visible hardpoints, every role rule in Validate
ShipAuthoringStore.Validate now alone decides which hardpoints carry anchors and which role each plays. A thruster, radiator, energy, ballistic or launcher hardpoint needs an anchor of its mount id with role thruster-emitter, radiator-mesh or the new weapon-mount; every other hardpoint is internal and no anchor may take its id. A mount-role anchor must be a hardpoint's mount, a muzzle must hang off a weapon mount, and each weapon needs a muzzle; these checks move out of ShipModPlan.Build, which now only maps a validated pair to slots and throws nothing itself. 'articulation' stays in the vocabulary for pivots and is never a mount. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
23976aada7
commit
976b711905
6 changed files with 171 additions and 123 deletions
|
|
@ -24,8 +24,8 @@ public sealed class ShipAnchor
|
|||
{
|
||||
// Stable semantic identity. HullData.Hardpoints[].Transform names this ID for mounts.
|
||||
[Key(0)] public string Id;
|
||||
// One of: map-icon, hull-collider, shield, tractor, thruster-emitter,
|
||||
// weapon-muzzle, radiator-mesh, or articulation.
|
||||
// One of: map-icon, hull-collider, shield, tractor, thruster-emitter, weapon-mount,
|
||||
// weapon-muzzle, radiator-mesh, or articulation (a pivot, never a mount).
|
||||
[Key(1)] public string Role;
|
||||
// Stable node ID in the compiled model, never a Blender display name.
|
||||
[Key(2)] public string ModelNodeId;
|
||||
|
|
@ -53,9 +53,19 @@ public static class ShipAuthoringStore
|
|||
// ShipAnchor.Role's vocabulary.
|
||||
private static readonly HashSet<string> Roles = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
"map-icon", "hull-collider", "shield", "tractor", "thruster-emitter", "weapon-muzzle", "radiator-mesh", "articulation"
|
||||
"map-icon", "hull-collider", "shield", "tractor", "thruster-emitter", "weapon-mount", "weapon-muzzle", "radiator-mesh",
|
||||
"articulation"
|
||||
};
|
||||
|
||||
// The anchor role a visible hardpoint's mount plays, or null for an internal hardpoint, which has no anchor.
|
||||
private static string MountRole(HardpointType type) =>
|
||||
type == HardpointType.Thruster ? "thruster-emitter" :
|
||||
type == HardpointType.Radiator ? "radiator-mesh" :
|
||||
IsWeapon(type) ? "weapon-mount" : null;
|
||||
|
||||
public static bool IsWeapon(HardpointType type) =>
|
||||
type == HardpointType.Energy || type == HardpointType.Ballistic || type == HardpointType.Launcher;
|
||||
|
||||
public static CultCache Open(string path, bool writable = false)
|
||||
{
|
||||
var cache = new CultCache();
|
||||
|
|
@ -193,13 +203,13 @@ public static class ShipAuthoringStore
|
|||
if (anchors.Count(anchor => anchor.Role == role) != 1)
|
||||
throw new InvalidOperationException($"{ship.Id}: exactly one {role} anchor is required.");
|
||||
|
||||
var mounts = new HashSet<string>(StringComparer.Ordinal);
|
||||
var mounts = new Dictionary<string, HardpointData>(StringComparer.Ordinal);
|
||||
var occupiedHardpointCells = new HashSet<(int x, int y)>();
|
||||
foreach (var hardpoint in hull.Hardpoints ?? new List<HardpointData>())
|
||||
{
|
||||
if (hardpoint == null || string.IsNullOrWhiteSpace(hardpoint.Transform) ||
|
||||
!mounts.Add(hardpoint.Transform))
|
||||
if (hardpoint == null || string.IsNullOrWhiteSpace(hardpoint.Transform) || mounts.ContainsKey(hardpoint.Transform))
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint IDs must be present and unique.");
|
||||
mounts.Add(hardpoint.Transform, hardpoint);
|
||||
if (!Enum.IsDefined(typeof(HardpointType), hardpoint.Type))
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} has an unknown type {(int)hardpoint.Type}.");
|
||||
if (!Enum.IsDefined(typeof(ItemRotation), hardpoint.Rotation))
|
||||
|
|
@ -219,19 +229,32 @@ public static class ShipAuthoringStore
|
|||
if (!occupiedHardpointCells.Add((x, y)))
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} overlaps another hardpoint.");
|
||||
}
|
||||
// Only a hardpoint that shows on the model has a mount anchor, and its type fixes the role that anchor plays.
|
||||
var mountAnchor = anchors.FirstOrDefault(anchor => anchor.Id == hardpoint.Transform);
|
||||
if (mountAnchor == null)
|
||||
throw new InvalidOperationException($"{ship.Id}: hardpoint {hardpoint.Transform} has no model anchor.");
|
||||
// A mount's anchor plays the mount, so its role is fixed by the hardpoint type; the structural roles (map-icon,
|
||||
// hull-collider, shield, tractor, weapon-muzzle) name other nodes and never double as a mount.
|
||||
var mountRole = hardpoint.Type == HardpointType.Thruster ? "thruster-emitter" :
|
||||
hardpoint.Type == HardpointType.Radiator ? "radiator-mesh" : "articulation";
|
||||
if (mountAnchor.Role != mountRole)
|
||||
var mountRole = MountRole(hardpoint.Type);
|
||||
if (mountRole == null && mountAnchor != null)
|
||||
throw new InvalidOperationException($"{ship.Id}: {hardpoint.Type} hardpoint {hardpoint.Transform} is internal, so no anchor may take its id.");
|
||||
if (mountRole != null && mountAnchor == null)
|
||||
throw new InvalidOperationException($"{ship.Id}: {hardpoint.Type} hardpoint {hardpoint.Transform} has no model anchor.");
|
||||
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 anchor in anchors.Where(anchor => !string.IsNullOrEmpty(anchor.ParentId)))
|
||||
if (!mounts.Contains(anchor.ParentId))
|
||||
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))
|
||||
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)))
|
||||
throw new InvalidOperationException($"{ship.Id}: muzzle {anchor.Id} must be parented to a weapon hardpoint's mount.");
|
||||
}
|
||||
foreach (var hardpoint in mounts.Values.Where(hardpoint => IsWeapon(hardpoint.Type)))
|
||||
if (!anchors.Any(anchor => anchor.Role == "weapon-muzzle" && anchor.ParentId == hardpoint.Transform))
|
||||
throw new InvalidOperationException($"{ship.Id}: weapon hardpoint {hardpoint.Transform} needs at least one muzzle anchor.");
|
||||
|
||||
foreach (var line in ship.SchematicLines ?? new List<ShipPolyline>())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,37 +13,18 @@ public sealed class ShipModPlan
|
|||
// Mount ids whose anchor (role thruster-emitter / radiator-mesh) is the emitter / mesh node itself.
|
||||
public string[] Thrusters;
|
||||
public string[] Radiators;
|
||||
// A weapon mount's muzzle anchors, by Order then id.
|
||||
// Each weapon mount (role weapon-mount) and its muzzle anchors, by Order then id.
|
||||
public (string Mount, string[] Muzzles)[] Weapons;
|
||||
|
||||
// Anchors with role "articulation" become ordinary mount nodes: ArticulationPoint's limits are not authorable until
|
||||
// pivots move onto HullData, so no ArticulationPoint is planned.
|
||||
// Maps a pair Validate accepts; every rule about which anchor may play which part is Validate's, so this refuses nothing.
|
||||
public static ShipModPlan Build(HullData hull, ShipAuthoring visual)
|
||||
{
|
||||
ShipAuthoringStore.Validate(hull, visual);
|
||||
var hardpoints = hull.Hardpoints ?? new List<HardpointData>();
|
||||
var byMount = hardpoints.ToDictionary(hardpoint => hardpoint.Transform, StringComparer.Ordinal);
|
||||
string Only(string wanted) => visual.Anchors.Single(anchor => anchor.Role == wanted).Id;
|
||||
|
||||
foreach (var anchor in visual.Anchors.Where(anchor => anchor.Role == "thruster-emitter" || anchor.Role == "radiator-mesh"))
|
||||
{
|
||||
var wanted = anchor.Role == "thruster-emitter" ? HardpointType.Thruster : HardpointType.Radiator;
|
||||
if (!byMount.ContainsKey(anchor.Id))
|
||||
throw new InvalidOperationException($"{visual.Id}: {anchor.Role} anchor {anchor.Id} must be a {wanted} hardpoint's mount.");
|
||||
}
|
||||
string[] Mounts(HardpointType type) => hardpoints.Where(hardpoint => hardpoint.Type == type).Select(hardpoint => hardpoint.Transform).ToArray();
|
||||
|
||||
var muzzles = visual.Anchors.Where(anchor => anchor.Role == "weapon-muzzle")
|
||||
.OrderBy(anchor => anchor.Order).ThenBy(anchor => anchor.Id, StringComparer.Ordinal).ToArray();
|
||||
foreach (var muzzle in muzzles)
|
||||
if (string.IsNullOrEmpty(muzzle.ParentId) || !IsWeapon(byMount[muzzle.ParentId].Type))
|
||||
throw new InvalidOperationException($"{visual.Id}: muzzle {muzzle.Id} must be parented to a weapon hardpoint's mount.");
|
||||
var weapons = hardpoints.Where(hardpoint => IsWeapon(hardpoint.Type)).Select(hardpoint => (
|
||||
hardpoint.Transform,
|
||||
muzzles.Where(muzzle => muzzle.ParentId == hardpoint.Transform).Select(muzzle => muzzle.Id).ToArray())).ToArray();
|
||||
foreach (var (mount, ids) in weapons)
|
||||
if (ids.Length == 0)
|
||||
throw new InvalidOperationException($"{visual.Id}: weapon hardpoint {mount} needs at least one muzzle anchor.");
|
||||
|
||||
return new ShipModPlan
|
||||
{
|
||||
|
|
@ -53,10 +34,9 @@ public sealed class ShipModPlan
|
|||
Tractor = Only("tractor"),
|
||||
Thrusters = Mounts(HardpointType.Thruster),
|
||||
Radiators = Mounts(HardpointType.Radiator),
|
||||
Weapons = weapons
|
||||
Weapons = hardpoints.Where(hardpoint => ShipAuthoringStore.IsWeapon(hardpoint.Type)).Select(hardpoint => (
|
||||
hardpoint.Transform,
|
||||
muzzles.Where(muzzle => muzzle.ParentId == hardpoint.Transform).Select(muzzle => muzzle.Id).ToArray())).ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsWeapon(HardpointType type) =>
|
||||
type == HardpointType.Energy || type == HardpointType.Ballistic || type == HardpointType.Launcher;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -230,49 +230,137 @@ public sealed class ShipAuthoringTests
|
|||
Assert.Contains("records must be stored under", Assert.Throws<InvalidOperationException>(() => ShipAuthoringStore.Load(wrongVisualKey)).Message);
|
||||
}
|
||||
|
||||
// A mount's anchor plays the mount, so a structural anchor (the hull collider, the shield, a muzzle...) is never one.
|
||||
// Interim ruling until an `equipment` role exists: a mount that is not a thruster or radiator is an articulation anchor.
|
||||
[Theory]
|
||||
[InlineData("map-icon", "map")]
|
||||
[InlineData("hull-collider", "collider")]
|
||||
[InlineData("shield", "shield")]
|
||||
[InlineData("tractor", "tractor")]
|
||||
[InlineData("weapon-muzzle", "muzzle")]
|
||||
[InlineData("thruster-emitter", "emitter")]
|
||||
[InlineData("radiator-mesh", "mesh")]
|
||||
public void AStructuralAnchorCannotBeAnEquipmentMount(string role, string anchorId)
|
||||
private static readonly string[] AllRoles =
|
||||
{
|
||||
"map-icon", "hull-collider", "shield", "tractor", "thruster-emitter", "weapon-mount", "weapon-muzzle", "radiator-mesh", "articulation"
|
||||
};
|
||||
|
||||
// The fixture with its thruster swapped for one hardpoint of the given type at mount id "mount", and no anchor for it.
|
||||
private static ShipParts WithOnly(HardpointType type)
|
||||
{
|
||||
var ship = Fixture();
|
||||
// The extra anchors exist so the role-count and parent checks pass; the Tool hardpoint then claims the anchor.
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "muzzle", Role = "weapon-muzzle", ModelNodeId = "muzzle", ParentId = "thruster.port" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "emitter", Role = "thruster-emitter", ModelNodeId = "emitter" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "mesh", Role = "radiator-mesh", ModelNodeId = "mesh" });
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Tool, Position = new int2(0, 0), Shape = new Shape(), Transform = anchorId });
|
||||
ship.Hull.Hardpoints.Clear();
|
||||
ship.Visual.Anchors.RemoveAll(anchor => anchor.Id == "thruster.port");
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = type, Position = new int2(0, 0), Shape = new Shape(), Transform = "mount" });
|
||||
return ship;
|
||||
}
|
||||
|
||||
Assert.Contains($"Tool hardpoint {anchorId} needs a articulation anchor of that id", Assert.Throws<InvalidOperationException>(ship.Validate).Message);
|
||||
ship = Fixture();
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "tool", Role = "articulation", ModelNodeId = "tool" });
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Tool, Position = new int2(0, 0), Shape = new Shape(), Transform = "tool" });
|
||||
ship.Validate();
|
||||
private static string Refusal(ShipParts ship) => Assert.Throws<InvalidOperationException>(ship.Validate).Message;
|
||||
|
||||
// Mount anchors exist only for hardpoints that show on the model: weapons, radiators and thrusters.
|
||||
[Theory]
|
||||
[InlineData(HardpointType.Hull), InlineData(HardpointType.Tool), InlineData(HardpointType.Thermal), InlineData(HardpointType.WarpDrive),
|
||||
InlineData(HardpointType.Reactor), InlineData(HardpointType.Shield), InlineData(HardpointType.Sensors),
|
||||
InlineData(HardpointType.ControlModule), InlineData(HardpointType.AetherDrive)]
|
||||
public void AnInternalHardpointCarriesNoAnchor(HardpointType type)
|
||||
{
|
||||
WithOnly(type).Validate();
|
||||
|
||||
// Any anchor taking the mount's id is refused, a mount role or not. (The four structural roles are one-of-a-kind,
|
||||
// so they are covered by renaming the existing anchor.)
|
||||
foreach (var role in AllRoles)
|
||||
{
|
||||
var ship = WithOnly(type);
|
||||
var existing = ship.Visual.Anchors.FirstOrDefault(anchor => anchor.Role == role);
|
||||
if (existing != null) existing.Id = "mount";
|
||||
else ship.Visual.Anchors.Add(new ShipAnchor { Id = "mount", Role = role, ModelNodeId = "mount" });
|
||||
Assert.Contains($"mod.skiff: {type} hardpoint mount is internal, so no anchor may take its id", Refusal(ship));
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HardpointType.Thruster, "thruster-emitter")]
|
||||
[InlineData(HardpointType.Radiator, "radiator-mesh")]
|
||||
[InlineData(HardpointType.Energy, "weapon-mount")]
|
||||
[InlineData(HardpointType.Ballistic, "weapon-mount")]
|
||||
[InlineData(HardpointType.Launcher, "weapon-mount")]
|
||||
public void EachVisibleHardpointNeedsItsRoleAnchor(HardpointType type, string wanted)
|
||||
{
|
||||
ShipParts Rig()
|
||||
{
|
||||
var ship = WithOnly(type);
|
||||
if (ShipAuthoringStore.IsWeapon(type))
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "mount.muzzle", Role = "weapon-muzzle", ModelNodeId = "muzzle", ParentId = "mount" });
|
||||
return ship;
|
||||
}
|
||||
|
||||
Assert.Contains($"mod.skiff: {type} hardpoint mount has no model anchor", Refusal(Rig()));
|
||||
|
||||
// A structural anchor (the map icon, the collider...) whose id is the mount, or a mount anchor of the wrong role.
|
||||
foreach (var role in AllRoles.Where(role => role != wanted))
|
||||
{
|
||||
var ship = Rig();
|
||||
var existing = ship.Visual.Anchors.FirstOrDefault(anchor => anchor.Role == role);
|
||||
if (existing != null) existing.Id = "mount";
|
||||
else ship.Visual.Anchors.Add(new ShipAnchor { Id = "mount", Role = role, ModelNodeId = "mount" });
|
||||
Assert.Contains($"mod.skiff: {type} hardpoint mount needs a {wanted} anchor of that id", Refusal(ship));
|
||||
}
|
||||
|
||||
var valid = Rig();
|
||||
valid.Visual.Anchors.Add(new ShipAnchor { Id = "mount", Role = wanted, ModelNodeId = "mount" });
|
||||
valid.Validate();
|
||||
}
|
||||
|
||||
// Called on Validate itself: a mount-role anchor is the mount of a hardpoint of its own type, never a loose node and
|
||||
// never another type's mount.
|
||||
[Theory]
|
||||
[InlineData("thruster-emitter", HardpointType.Radiator)]
|
||||
[InlineData("thruster-emitter", HardpointType.Energy)]
|
||||
[InlineData("thruster-emitter", HardpointType.Reactor)]
|
||||
[InlineData("radiator-mesh", HardpointType.Thruster)]
|
||||
[InlineData("radiator-mesh", HardpointType.Launcher)]
|
||||
[InlineData("radiator-mesh", HardpointType.Sensors)]
|
||||
[InlineData("weapon-mount", HardpointType.Thruster)]
|
||||
[InlineData("weapon-mount", HardpointType.Radiator)]
|
||||
[InlineData("weapon-mount", HardpointType.Tool)]
|
||||
public void AMountRoleAnchorMustBeAMountOfItsType(string role, HardpointType otherType)
|
||||
{
|
||||
string Refused(ShipParts ship) =>
|
||||
Assert.Throws<InvalidOperationException>(() => ShipAuthoringStore.Validate(ship.Hull, ship.Visual)).Message;
|
||||
|
||||
var loose = Fixture();
|
||||
loose.Visual.Anchors.Add(new ShipAnchor { Id = "loose", Role = role, ModelNodeId = "loose" });
|
||||
Assert.Contains($"mod.skiff: {role} anchor loose must be the mount of a hardpoint of its type", Refused(loose));
|
||||
|
||||
// The anchor takes the id of a hardpoint of another type; whichever side the validator reads first, it refuses.
|
||||
var other = WithOnly(otherType);
|
||||
other.Visual.Anchors.Add(new ShipAnchor { Id = "mount", Role = role, ModelNodeId = "mount" });
|
||||
if (ShipAuthoringStore.IsWeapon(otherType))
|
||||
other.Visual.Anchors.Add(new ShipAnchor { Id = "mount.muzzle", Role = "weapon-muzzle", ModelNodeId = "muzzle", ParentId = "mount" });
|
||||
Assert.Contains($"mod.skiff: {otherType} hardpoint mount ", Refused(other));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EveryHardpointTypeHasOneAnchorRoleForItsMount()
|
||||
public void MuzzleRulesBelongToTheValidator()
|
||||
{
|
||||
foreach (HardpointType type in Enum.GetValues(typeof(HardpointType)))
|
||||
ShipParts Armed()
|
||||
{
|
||||
var wanted = type == HardpointType.Thruster ? "thruster-emitter" : type == HardpointType.Radiator ? "radiator-mesh" : "articulation";
|
||||
foreach (var role in new[] { "articulation", "thruster-emitter", "radiator-mesh" })
|
||||
{
|
||||
var ship = Fixture();
|
||||
ship.Hull.Hardpoints.Clear();
|
||||
ship.Visual.Anchors.RemoveAll(anchor => anchor.Id == "thruster.port");
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = type, Position = new int2(0, 0), Shape = new Shape(), Transform = "mount" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "mount", Role = role, ModelNodeId = "mount" });
|
||||
if (role == wanted) ship.Validate();
|
||||
else Assert.Contains($"{type} hardpoint mount needs a {wanted} anchor of that id", Assert.Throws<InvalidOperationException>(ship.Validate).Message);
|
||||
}
|
||||
var ship = Fixture();
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = new int2(0, 0), Shape = new Shape(), Transform = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun", Role = "weapon-mount", ModelNodeId = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun.b", Role = "weapon-muzzle", ModelNodeId = "gun-b", ParentId = "gun", Order = 0 });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun.a", Role = "weapon-muzzle", ModelNodeId = "gun-a", ParentId = "gun", Order = 0 });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun.z", Role = "weapon-muzzle", ModelNodeId = "gun-z", ParentId = "gun", Order = -1 });
|
||||
return ship;
|
||||
}
|
||||
|
||||
var orphan = Armed();
|
||||
orphan.Visual.Anchors.Single(anchor => anchor.Id == "gun.z").ParentId = null;
|
||||
Assert.Contains("mod.skiff: muzzle gun.z must be parented to a weapon hardpoint's mount", Refusal(orphan));
|
||||
|
||||
var onThruster = Armed();
|
||||
onThruster.Visual.Anchors.Single(anchor => anchor.Id == "gun.z").ParentId = "thruster.port";
|
||||
Assert.Contains("mod.skiff: muzzle gun.z must be parented to a weapon hardpoint's mount", Refusal(onThruster));
|
||||
|
||||
var unarmed = Armed();
|
||||
unarmed.Visual.Anchors.RemoveAll(anchor => anchor.Role == "weapon-muzzle");
|
||||
Assert.Contains("mod.skiff: weapon hardpoint gun needs at least one muzzle anchor", Refusal(unarmed));
|
||||
|
||||
// A valid pair plans without a refusal of the plan's own; muzzles by Order, then id ordinally.
|
||||
var valid = Armed();
|
||||
var (mount, muzzles) = Assert.Single(ShipModPlan.Build(valid.Hull, valid.Visual).Weapons);
|
||||
Assert.Equal("gun", mount);
|
||||
Assert.Equal(new[] { "gun.z", "gun.a", "gun.b" }, muzzles);
|
||||
}
|
||||
|
||||
internal static ShipParts Fixture()
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ internal static class ShipFixture
|
|||
var ship = ShipAuthoringTests.Fixture();
|
||||
// A playable rig: the fixture thruster plus one energy weapon mount on the hull's other cell, with its muzzle.
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = new int2(0, 0), Shape = new Shape(), Transform = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun", Role = "articulation", ModelNodeId = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun", Role = "weapon-mount", ModelNodeId = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun.muzzle", Role = "weapon-muzzle", ModelNodeId = "gun-muzzle", ParentId = "gun" });
|
||||
tweak?.Invoke(ship);
|
||||
ship.WithId(id);
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public sealed class ShipModPlanTests
|
|||
ship.Hull.Hardpoints[0].Position = new int2(2, 0);
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = new int2(0, 0), Shape = new Shape(), Transform = "gun" });
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Radiator, Position = new int2(1, 0), Shape = new Shape(), Transform = "fin" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun", Role = "articulation", ModelNodeId = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun", Role = "weapon-mount", ModelNodeId = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun.a", Role = "weapon-muzzle", ModelNodeId = "gun-a", ParentId = "gun", Order = 1 });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun.z", Role = "weapon-muzzle", ModelNodeId = "gun-z", ParentId = "gun", Order = 0 });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "fin", Role = "radiator-mesh", ModelNodeId = "fin" });
|
||||
|
|
@ -40,31 +40,6 @@ public sealed class ShipModPlanTests
|
|||
Assert.Equal(new[] { "gun.z", "gun.a" }, muzzles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AThrusterOrRadiatorMountMustCarryItsRole()
|
||||
{
|
||||
var ship = Rig();
|
||||
ship.Visual.Anchors.Single(anchor => anchor.Id == "thruster.port").Role = "articulation";
|
||||
Assert.Contains("Thruster hardpoint thruster.port needs a thruster-emitter anchor", Refusal(ship));
|
||||
|
||||
ship = Rig();
|
||||
ship.Visual.Anchors.Single(anchor => anchor.Id == "fin").Role = "articulation";
|
||||
Assert.Contains("Radiator hardpoint fin needs a radiator-mesh anchor", Refusal(ship));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnEmitterOrMeshAnchorMustBeItsHardpointsMount()
|
||||
{
|
||||
// A thruster-emitter anchor on a weapon mount, and a radiator-mesh anchor that is no mount at all.
|
||||
var ship = Rig();
|
||||
ship.Visual.Anchors.Single(anchor => anchor.Id == "gun").Role = "thruster-emitter";
|
||||
Assert.Contains("Energy hardpoint gun needs a articulation anchor of that id", Refusal(ship));
|
||||
|
||||
ship = Rig();
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "loose", Role = "radiator-mesh", ModelNodeId = "loose" });
|
||||
Assert.Contains("radiator-mesh anchor loose must be a Radiator hardpoint's mount", Refusal(ship));
|
||||
}
|
||||
|
||||
// Equal Order falls back to the id, compared ordinally, whatever order the anchors were authored in.
|
||||
[Fact]
|
||||
public void MuzzlesOfEqualOrderAreTiedByIdOrdinally()
|
||||
|
|
@ -78,22 +53,6 @@ public sealed class ShipModPlanTests
|
|||
Assert.Equal(new[] { "gun.z", "gun.B", "gun.a", "gun.b", "gun.c" }, Assert.Single(Plan(ship).Weapons).Muzzles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MuzzlesBelongToWeaponMountsAndEveryWeaponHasOne()
|
||||
{
|
||||
var ship = Rig();
|
||||
ship.Visual.Anchors.Single(anchor => anchor.Id == "gun.z").ParentId = null;
|
||||
Assert.Contains("muzzle gun.z must be parented to a weapon hardpoint's mount", Refusal(ship));
|
||||
|
||||
ship = Rig();
|
||||
ship.Visual.Anchors.Single(anchor => anchor.Id == "gun.z").ParentId = "thruster.port";
|
||||
Assert.Contains("muzzle gun.z must be parented to a weapon hardpoint's mount", Refusal(ship));
|
||||
|
||||
ship = Rig();
|
||||
ship.Visual.Anchors.RemoveAll(anchor => anchor.Role == "weapon-muzzle");
|
||||
Assert.Contains("weapon hardpoint gun needs at least one muzzle anchor", Refusal(ship));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HardpointType.Energy)]
|
||||
[InlineData(HardpointType.Ballistic)]
|
||||
|
|
@ -103,10 +62,6 @@ public sealed class ShipModPlanTests
|
|||
var ship = Rig();
|
||||
ship.Hull.Hardpoints.Single(hardpoint => hardpoint.Transform == "gun").Type = type;
|
||||
Assert.Equal("gun", Assert.Single(Plan(ship).Weapons).Mount);
|
||||
|
||||
// Any other type refuses the same muzzles.
|
||||
ship.Hull.Hardpoints.Single(hardpoint => hardpoint.Transform == "gun").Type = HardpointType.Tool;
|
||||
Assert.Contains("must be parented to a weapon hardpoint's mount", Refusal(ship));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public sealed class ShipValidationTests
|
|||
ship.Hull.Hardpoints[0].Armor = 5;
|
||||
ship.Hull.Hardpoints[0].FiringArc = 360;
|
||||
ship.Hull.Hardpoints[0].Type = HardpointType.AetherDrive;
|
||||
ship.Visual.Anchors.Single(anchor => anchor.Id == "thruster.port").Role = "articulation";
|
||||
ship.Visual.Anchors.RemoveAll(anchor => anchor.Id == "thruster.port");
|
||||
ship.Hull.Hardpoints[0].Rotation = ItemRotation.Clockwise;
|
||||
ship.Validate();
|
||||
}
|
||||
|
|
@ -128,7 +128,9 @@ public sealed class ShipValidationTests
|
|||
public void AnAnchorMayNameItsHardpointAsParent()
|
||||
{
|
||||
var ship = ShipAuthoringTests.Fixture();
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "muzzle", Role = "weapon-muzzle", ModelNodeId = "muzzle", ParentId = "thruster.port" });
|
||||
ship.Hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = new int2(0, 0), Shape = new Shape(), Transform = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "gun", Role = "weapon-mount", ModelNodeId = "gun" });
|
||||
ship.Visual.Anchors.Add(new ShipAnchor { Id = "muzzle", Role = "weapon-muzzle", ModelNodeId = "muzzle", ParentId = "gun" });
|
||||
ship.Validate();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue