diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index f6e4fcd..8b82b3d 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -17,6 +17,7 @@ namespace GameCult.Eve.UnityUIToolkit private Vector2 _inventoryDragStart; private bool _inventoryPointerMoved; private bool _suppressInventoryClick; + private readonly List _inventoryPreviewCells = new(); public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null) { @@ -273,6 +274,7 @@ namespace GameCult.Eve.UnityUIToolkit return; if (TryEmitInventoryDrop(document, _inventoryDragSource, component, grid, evt.position, commandSink)) _inventoryDragSource = null; + ClearInventoryPreview(); }); return grid; } @@ -291,6 +293,7 @@ namespace GameCult.Eve.UnityUIToolkit _inventoryDragSource = component; _inventoryDragStart = evt.position; _inventoryPointerMoved = false; + ClearInventoryPreview(); item.CapturePointer(evt.pointerId); evt.StopPropagation(); }); @@ -300,7 +303,15 @@ namespace GameCult.Eve.UnityUIToolkit return; var pointerPosition = new Vector2(evt.position.x, evt.position.y); if ((pointerPosition - _inventoryDragStart).sqrMagnitude > 16f) + { _inventoryPointerMoved = true; + var picked = item.panel?.Pick(evt.position); + var targetElement = InventoryGridAncestor(picked); + if (targetElement?.userData is EveSurfaceComponent target) + ShowInventoryPreview(component, target, targetElement, evt.position); + else + ClearInventoryPreview(); + } }); item.RegisterCallback(evt => { @@ -315,6 +326,7 @@ namespace GameCult.Eve.UnityUIToolkit TryEmitInventoryDrop(document, component, target, targetElement, evt.position, commandSink); _inventoryDragSource = null; _suppressInventoryClick = true; + ClearInventoryPreview(); } evt.StopPropagation(); }); @@ -399,6 +411,9 @@ namespace GameCult.Eve.UnityUIToolkit Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f))); var x = Math.Max(0, (int)Math.Floor(local.x / pitch)); var y = Math.Max(0, (int)Math.Floor(local.y / pitch)); + if (!EveInventoryInteraction.TryCreatePlacementPreview(source, target, x, y, out var preview) || + preview == null || !preview.IsValid) + return false; if (!EveInventoryInteraction.TryCreateDropRequest( document, source, target, x, y, "unity-uitoolkit", out var request) || request == null) return false; @@ -406,6 +421,50 @@ namespace GameCult.Eve.UnityUIToolkit return true; } + private void ShowInventoryPreview( + EveSurfaceComponent source, + EveSurfaceComponent target, + VisualElement targetElement, + Vector2 panelPosition) + { + ClearInventoryPreview(); + var local = targetElement.WorldToLocal(panelPosition); + var cellSize = Math.Max(1f, ParseNumber(target.GetProp("cellSize"), 72f)); + var gap = Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f)); + var pitch = cellSize + gap; + var x = Math.Max(0, (int)Math.Floor(local.x / Math.Max(1f, pitch))); + var y = Math.Max(0, (int)Math.Floor(local.y / Math.Max(1f, pitch))); + if (!EveInventoryInteraction.TryCreatePlacementPreview(source, target, x, y, out var preview) || + preview == null) + return; + + foreach (var cell in preview.Cells) + { + var element = new VisualElement { pickingMode = PickingMode.Ignore }; + element.AddToClassList("eve-inventory-placement-preview"); + element.AddToClassList(preview.IsValid + ? "eve-inventory-placement-valid" + : "eve-inventory-placement-invalid"); + element.style.position = Position.Absolute; + element.style.left = cell.X * pitch; + element.style.top = cell.Y * pitch; + element.style.width = cellSize; + element.style.height = cellSize; + element.style.backgroundColor = preview.IsValid + ? new Color(0.2f, 0.9f, 0.65f, 0.34f) + : new Color(1f, 0.25f, 0.2f, 0.38f); + targetElement.Add(element); + _inventoryPreviewCells.Add(element); + } + } + + private void ClearInventoryPreview() + { + foreach (var cell in _inventoryPreviewCells) + cell.RemoveFromHierarchy(); + _inventoryPreviewCells.Clear(); + } + private static int ParseInt(string value, int fallback) => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback; diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs index 93b83c5..85d664b 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -244,18 +245,64 @@ namespace GameCult.Eve.UnityUIToolkit.Tests ["targetKind"] = "equipment", ["targetEntityKey"] = "zone.0.entity.1", ["targetIndex"] = "-1", - ["dropCommand.cargo"] = "aetheria.daemon.commands.EquipItem" + ["dropCommand.cargo"] = "aetheria.daemon.commands.EquipItem", + ["payload.shipId"] = "hangar.ship.1", + ["payload.expectedHangarRevision"] = "42" }); Assert.That(EveInventoryInteraction.TryCreateDropRequest( Document(target), source, target, 7, 9, "unity-test", out var request), Is.True); Assert.That(request, Is.Not.Null); Assert.That(request!.Command, Is.EqualTo("aetheria.daemon.commands.EquipItem")); + Assert.That(request.CommandId, Is.Not.Empty); Assert.That(request.Payload.GetString("originEntityKey"), Is.EqualTo("zone.0.entity.1")); Assert.That(request.Payload.GetString("originCargoIndex"), Is.EqualTo("2")); Assert.That(request.Payload.GetString("destinationX"), Is.EqualTo("7")); Assert.That(request.Payload.GetString("destinationY"), Is.EqualTo("9")); Assert.That(request.Payload.GetString("hasDestinationPosition"), Is.EqualTo("true")); + Assert.That(request.Payload.GetString("shipId"), Is.EqualTo("hangar.ship.1")); + Assert.That(request.Payload.GetString("expectedHangarRevision"), Is.EqualTo("42")); + } + + [Test] + public void InventoryPlacementPreviewUsesIrregularShapeAndCurrentOccupancy() + { + var moving = Component("moving", EveInventoryInteraction.ItemKind, new Dictionary + { + ["shapeCells"] = "0,0;1,0;0,1" + }); + var installed = Component("installed", EveInventoryInteraction.ItemKind, new Dictionary + { + ["x"] = "2", + ["y"] = "1", + ["shapeCells"] = "0,0" + }); + var grid = new EveSurfaceComponent( + "grid", + EveInventoryInteraction.GridKind, + new Dictionary + { + ["columns"] = "4", + ["rows"] = "3", + ["validCells"] = "0,0;1,0;2,0;3,0;0,1;1,1;2,1;3,1;0,2;1,2;2,2;3,2" + }, + new[] { installed }); + + Assert.That(EveInventoryInteraction.TryCreatePlacementPreview( + moving, grid, 0, 0, out var valid), Is.True); + Assert.That(valid!.IsValid, Is.True); + Assert.That(valid.Cells.Select(cell => (cell.X, cell.Y)), + Is.EquivalentTo(new[] { (0, 0), (1, 0), (0, 1) })); + + Assert.That(EveInventoryInteraction.TryCreatePlacementPreview( + moving, grid, 1, 1, out var occupied), Is.True); + Assert.That(occupied!.IsValid, Is.False); + Assert.That(occupied.Reason, Is.EqualTo("occupied")); + + Assert.That(EveInventoryInteraction.TryCreatePlacementPreview( + moving, grid, 3, 2, out var outside), Is.True); + Assert.That(outside!.IsValid, Is.False); + Assert.That(outside.Reason, Is.EqualTo("outside-grid")); } [Test] diff --git a/scripts/run-uitoolkit-tests.ps1 b/scripts/run-uitoolkit-tests.ps1 index d34cb92..12b317a 100644 --- a/scripts/run-uitoolkit-tests.ps1 +++ b/scripts/run-uitoolkit-tests.ps1 @@ -14,9 +14,21 @@ foreach ($required in @($UnityExe, $projectPath, $cultLibBuilder)) { if (-not (Test-Path -LiteralPath $required)) { throw "Required EveUnity UI Toolkit test path not found: $required" } } -powershell -ExecutionPolicy Bypass -File $cultLibBuilder +& $cultLibBuilder if ($LASTEXITCODE -ne 0) { throw "CultLib Unity package build failed with exit code $LASTEXITCODE" } +$builtCultLibPackage = Join-Path $CultLibRoot "artifacts\unity\org.gamecult.cultlib" +$stagedDependenciesRoot = Join-Path $repoRoot "artifacts\test-dependencies" +$stagedCultLibPackage = Join-Path $stagedDependenciesRoot "org.gamecult.cultlib" +if (-not (Test-Path -LiteralPath $builtCultLibPackage)) { + throw "CultLib Unity package build did not produce: $builtCultLibPackage" +} +New-Item -ItemType Directory -Force -Path $stagedDependenciesRoot | Out-Null +if (Test-Path -LiteralPath $stagedCultLibPackage) { + [IO.Directory]::Delete($stagedCultLibPackage, $true) +} +Copy-Item -LiteralPath $builtCultLibPackage -Destination $stagedCultLibPackage -Recurse + $runRoot = Join-Path $output (Get-Date -Format "yyyyMMddTHHmmss") New-Item -ItemType Directory -Force -Path $runRoot | Out-Null $resultsPath = Join-Path $runRoot "unity-editmode-results.xml" @@ -27,10 +39,16 @@ $arguments = @( "-assemblyNames", "GameCult.Eve.UnityUIToolkit.Tests", "-testResults", $resultsPath, "-logFile", $logPath ) -$process = Start-Process -FilePath $UnityExe -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden -if ($process.ExitCode -ne 0) { +$process = Start-Process -FilePath $UnityExe -ArgumentList $arguments -PassThru -WindowStyle Hidden +if (-not $process.WaitForExit([int][TimeSpan]::FromMinutes(10).TotalMilliseconds)) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue if (Test-Path -LiteralPath $logPath) { Get-Content -LiteralPath $logPath -Tail 120 } - throw "EveUnity UI Toolkit tests failed with exit code $($process.ExitCode)" + throw "EveUnity UI Toolkit tests timed out after 10 minutes (Unity PID $($process.Id))." +} +$unityExitCode = $process.ExitCode +if ($unityExitCode -ne 0) { + if (Test-Path -LiteralPath $logPath) { Get-Content -LiteralPath $logPath -Tail 120 } + throw "EveUnity UI Toolkit tests failed with exit code $unityExitCode" } [xml]$results = Get-Content -LiteralPath $resultsPath -Raw