Merge pull request 'Probes never write item.Rotation; ItemFits reports the seat via out' (#25) from eureka/aetheria-release-itemfits-rotation-1 into master

This commit is contained in:
Nibu 2026-11-13 13:28:50 +00:00
commit 3f6a2fde4f
3 changed files with 90 additions and 6 deletions

View file

@ -1547,7 +1547,6 @@ public abstract class Entity
{
hardpoint = HardpointAt(itemData, hullData, hullCoord, item.Rotation, moving);
if (hardpoint == null) return false;
item.Rotation = hardpoint.Rotation;
}
return true;
@ -1582,14 +1581,22 @@ public abstract class Entity
// Check whether the given item will fit when its origin is placed at the given coordinate on the hull. An item that
// is already equipped here is judged without its own cells, so a gun can be moved within its own hardpoint;
// TryEquip stays strict (it never sees an item as its own occupant), so equipping an equipped item twice is refused.
public bool ItemFits(EquippableItem item, int2 hullCoord)
public bool ItemFits(EquippableItem item, int2 hullCoord) => ItemFits(item, hullCoord, out _);
// As above, and reports the rotation TryEquip would seat the item at: its hardpoint's for hardpoint gear, otherwise
// its own. A probe never writes item.Rotation; TryEquip is the only seat.
public bool ItemFits(EquippableItem item, int2 hullCoord, out ItemRotation seated)
{
seated = item.Rotation;
// Don't allow equipping while deployed
if (_active) return false;
var itemData = ItemManager.GetData(item);
var hullData = ItemManager.GetData(Hull) as HullData;
return ItemFits(itemData, hullData, item, hullCoord, out _, Equipment.FirstOrDefault(e => e.EquippableItem == item));
if (!ItemFits(itemData, hullData, item, hullCoord, out var hardpoint, Equipment.FirstOrDefault(e => e.EquippableItem == item)))
return false;
seated = hardpoint?.Rotation ?? item.Rotation;
return true;
}
public bool TryFindSpace(EquippableItem item, out int2 hullCoord)
@ -1662,7 +1669,9 @@ public abstract class Entity
var hullData = ItemManager.GetData(Hull) as HullData;
if (!ItemFits(itemData, hullData, item, hullCoord, out var hardpoint)) return false;
// The one seat of hardpoint gear's rotation, before EquippedItem's ctor reads it.
if (hardpoint != null) item.Rotation = hardpoint.Rotation;
EquippedItem equippedItem;
if (itemData.HardpointType == HardpointType.Tool)
{

View file

@ -386,11 +386,11 @@ public class InventoryPanel : MonoBehaviour, IPointerClickHandler
var itemData = GameManager.ItemManager.GetData(item);
if (!(item is EquippableItem equippableItem)) return;
var placementPosition = v + itemDragObject.OriginCellOffset;
if (entity.ItemFits(equippableItem, placementPosition))
if (entity.ItemFits(equippableItem, placementPosition, out var seated))
{
//foreach (var cell in _dragCells) cell.gameObject.SetActive(false);
FakeItem = item;
FakeOccupancy = hullData.Shape.Inset(itemData.Shape, placementPosition, item.Rotation);
FakeOccupancy = hullData.Shape.Inset(itemData.Shape, placementPosition, seated);
RefreshCells();
GameManager.RegisterDragTarget(drag =>
{

View file

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using CultMath;
using Xunit;
@ -64,4 +65,78 @@ public sealed partial class RunStartTests
Assert.Same(a, held.Hardpoint);
Assert.Equal(ItemRotation.None, held.EquippableItem.Rotation);
}
// A probe reports; only TryEquip seats. The probe used to turn the item to the hardpoint it found, so the same item
// probed twice could resolve differently, and a failed or merely previewed placement left it turned.
[Fact]
public void AProbeLeavesTheItemsRotationAlone()
{
var (hull, a, b) = TwinMountHull("Origin Twin Mounts Probe");
var edge = ScratchGear("Origin Edge Gun Probe", HardpointType.Ballistic, EdgeShape());
var entity = BareHull(hull);
var item = Instance(edge);
item.Rotation = ItemRotation.Clockwise;
Assert.True(entity.ItemFits(item, int2(5, 5)));
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.True(entity.ItemFits(item, int2(5, 5), out var seated));
Assert.Equal(ItemRotation.None, seated);
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.True(entity.TryFindSpace(item, out _));
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.False(entity.ItemFits(item, int2(0, 0), out var unseated));
Assert.Equal(ItemRotation.Clockwise, unseated);
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.True(entity.TryEquip(item, int2(5, 5)));
var held = entity.Equipment.Single(i => i.Data == edge);
Assert.Same(a, held.Hardpoint);
Assert.Equal(ItemRotation.None, held.EquippableItem.Rotation);
var reversed = Instance(edge);
reversed.Rotation = ItemRotation.Reversed;
var fresh = BareHull(hull);
Assert.True(fresh.ItemFits(reversed, int2(5, 5), out var first));
Assert.True(fresh.ItemFits(reversed, int2(5, 5), out var second));
Assert.Equal(ItemRotation.Reversed, first);
Assert.Equal(first, second);
Assert.Equal(b.Rotation, first);
}
}
// The shipped catalog authors no origin that two differently turned hardpoints of one type both take. The fit rule alone
// over-approximates what Entity.HardpointAt sees (occupancy only removes candidates), so zero here is stricter than needed.
public sealed class ShippedHardpointOriginTests
{
[Fact]
public void TheShippedCatalogHasNoAmbiguousOrigin()
{
using var cache = RestoredHullsTests.OpenCatalog();
var designs = cache.GetAll<EquippableItemData>().Where(d => !(d is HullData) && d.HardpointType != HardpointType.Tool).ToArray();
var ambiguous = new List<string>();
var pairs = 0;
var probes = 0;
foreach (var hull in cache.GetAll<HullData>())
for (var i = 0; i < hull.Hardpoints.Count; i++)
for (var j = i + 1; j < hull.Hardpoints.Count; j++)
{
var a = hull.Hardpoints[i];
var b = hull.Hardpoints[j];
if (a.Type != b.Type || a.Rotation == b.Rotation) continue;
pairs++;
foreach (var design in designs.Where(d => d.HardpointType == a.Type))
for (var x = -20; x < hull.Shape.Width; x++)
for (var y = -20; y < hull.Shape.Height; y++)
{
var origin = int2(x, y);
probes++;
if (a.TakesAt(design, origin - a.Position) && b.TakesAt(design, origin - b.Position))
ambiguous.Add($"{hull.Name} {design.Name} at ({x},{y}): hardpoints {i} and {j}");
}
}
System.Console.WriteLine($"ShippedHardpointOriginTests visited {pairs} same-type different-rotation pairs");
// A scan that visits nothing would pass; the catalog has such pairs today, so losing them must fail loudly.
Assert.True(pairs > 0 && probes > 0, $"the scan visited {pairs} pairs and {probes} origins");
Assert.True(ambiguous.Count == 0, $"{ambiguous.Count} ambiguous origins: " + string.Join("; ", ambiguous.Take(10)));
}
}