diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 0664023e3..d48b67760 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -38,6 +38,7 @@ RequireRuntimeMenuTabsUseEveSurface(root); RequireInventoryShipSettingsUseEveSurface(root); RequireTradeCargoSelectorUseEveSurface(root); RequireTradeFilterAndRowActionsUseEveSurface(root); +RequireTradeItemDetailsUseEveSurface(root); RequireInventoryDropdownUseEveSurface(root); RequirePlayerSettingsEveSurface(root); RequireMainMenuContinueRunState(root); @@ -509,6 +510,7 @@ Console.WriteLine("Runtime menu tab shell: tab navigation lowers through an Eve Console.WriteLine("Inventory ship-settings shell: background ship tuning lowers through an Eve UI Toolkit surface instead of PropertiesPanel.AddField"); Console.WriteLine("Trade cargo-selector shell: target cargo selection lowers through an Eve UI Toolkit surface instead of ContextMenu.AddOption"); Console.WriteLine("Trade filter and row-action shells: filter selection and buy-quantity entry lower through Eve UI Toolkit surfaces instead of ContextMenu dropdowns"); +Console.WriteLine("Trade item-details shell: typed item inspection lowers through an Eve UI Toolkit surface instead of PropertiesPanel.Inspect"); Console.WriteLine("Inventory dropdown shell: entity and loadout navigation lowers through an Eve UI Toolkit surface instead of ContextMenu.AddDropdown"); Console.WriteLine("Main-menu Continue authority: Continue selects typed run state instead of a null button"); Console.WriteLine("PropertiesPanel inspector authority: dead generic reflection inspector path is deleted"); @@ -2760,6 +2762,57 @@ static void RequireTradeFilterAndRowActionsUseEveSurface(string root) } } +static void RequireTradeItemDetailsUseEveSurface(string root) +{ + var tradeMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "TradeMenu.cs"); + if (!File.Exists(tradeMenuPath)) + { + throw new InvalidOperationException("Cannot verify trade item-details shell; TradeMenu.cs is missing."); + } + + var source = File.ReadAllText(tradeMenuPath); + var requiredSymbols = new[] + { + "TradeItemSurfaceId", + "RenderTradeItemDetailsSurface(", + "HandleTradeItemDetailsSurfaceCommand(", + "ResolveTradeItemDetailsSurfaceDocument(", + "BuildTradeItemDetailsSurfaceDefinition(", + "BuildTradeItemBehaviorCards(", + "BuildTradeItemBehaviorMetric(", + "CloseTradeItemDetailsCommand", + "new EveUiToolkitSurfaceLowerer()" + }; + + var missingSymbols = requiredSymbols + .Where(symbol => !source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (missingSymbols.Length > 0) + { + throw new InvalidOperationException( + "TradeMenu no longer lowers typed item inspection through an Eve surface: " + + string.Join(", ", missingSymbols)); + } + + var forbiddenSymbols = new[] + { + "public PropertiesPanel Properties;", + "OnClick = () => Properties.Inspect(i.TypedItem)" + }; + + var hits = forbiddenSymbols + .Where(symbol => source.Contains(symbol, StringComparison.Ordinal)) + .ToArray(); + + if (hits.Length > 0) + { + throw new InvalidOperationException( + "TradeMenu still owns typed item inspection through the old PropertiesPanel path: " + + string.Join(", ", hits)); + } +} + static void RequireInventoryDropdownUseEveSurface(string root) { var inventoryPanelPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryPanel.cs"); diff --git a/Assets/Scripts/UI/Menu/TradeMenu.cs b/Assets/Scripts/UI/Menu/TradeMenu.cs index d3a8a2e28..6cb9d7fc8 100644 --- a/Assets/Scripts/UI/Menu/TradeMenu.cs +++ b/Assets/Scripts/UI/Menu/TradeMenu.cs @@ -24,6 +24,8 @@ public class TradeMenu : MonoBehaviour private const string CloseFilterSurfaceCommand = "aetheria.trade.filter_selector.close"; private const string RowActionSurfaceId = "aetheria.trade.row_actions"; private const string CloseRowActionSurfaceCommand = "aetheria.trade.row_actions.close"; + private const string TradeItemSurfaceId = "aetheria.trade.item_details"; + private const string CloseTradeItemDetailsCommand = "aetheria.trade.item_details.close"; public ActionGameManager GameManager; public ConfirmationDialog Dialog; @@ -31,7 +33,6 @@ public class TradeMenu : MonoBehaviour public Prototype FilterPrototype; public SizeFilter MinimumSizeFilter; public SizeFilter MaximumSizeFilter; - public PropertiesPanel Properties; public Spreadsheet Spreadsheet; public TextMeshProUGUI TargetCargoLabel; public UnityEngine.UI.Button FoldoutButton; @@ -49,6 +50,7 @@ public class TradeMenu : MonoBehaviour private UIDocument _cargoSelectorSurfaceDocument; private UIDocument _filterSurfaceDocument; private UIDocument _rowActionSurfaceDocument; + private UIDocument _tradeItemSurfaceDocument; private string _rowActionTitle = "Trade Actions"; public EquippedCargoBay Inventory { get; set; } @@ -61,7 +63,7 @@ public class TradeMenu : MonoBehaviour HideCargoSelectorSurface(); HideFilterSurface(); HideRowActionSurface(); - Properties.GameManager = GameManager; + HideTradeItemDetailsSurface(); UpdateCreditsLabel(); MinimumSizeFilter.Width.onEndEdit.RemoveAllListeners(); @@ -223,7 +225,7 @@ public class TradeMenu : MonoBehaviour Output = x.output(i), SortKey = x.sortKey(i) }).ToArray(), - OnClick = () => Properties.Inspect(i.TypedItem), + OnClick = () => RenderTradeItemDetailsSurface(i.TypedItem), OnDoubleClick = () => { switch (i.Item) @@ -498,6 +500,295 @@ public class TradeMenu : MonoBehaviour Dialog.MoveToCursor(); } + private void RenderTradeItemDetailsSurface(AetheriaRuntimeCatalogItem item) + { + if (item == null) + return; + + var document = ResolveTradeItemDetailsSurfaceDocument(); + document.gameObject.SetActive(true); + + var root = document.rootVisualElement; + root.Clear(); + root.style.flexGrow = 1; + root.style.position = Position.Absolute; + root.style.left = 0; + root.style.top = 0; + root.style.right = 0; + root.style.bottom = 0; + root.style.alignItems = Align.FlexStart; + root.style.justifyContent = Justify.FlexStart; + root.style.paddingTop = 16; + root.style.paddingLeft = 16; + root.pickingMode = PickingMode.Ignore; + + var shell = new VisualElement(); + shell.style.width = 420; + shell.style.maxWidth = 520; + shell.style.backgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f); + shell.style.borderTopLeftRadius = 8; + shell.style.borderTopRightRadius = 8; + shell.style.borderBottomLeftRadius = 8; + shell.style.borderBottomRightRadius = 8; + shell.style.paddingLeft = 18; + shell.style.paddingRight = 18; + shell.style.paddingTop = 18; + shell.style.paddingBottom = 18; + shell.style.borderLeftWidth = 1; + shell.style.borderRightWidth = 1; + shell.style.borderTopWidth = 1; + shell.style.borderBottomWidth = 1; + shell.style.borderLeftColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.style.borderRightColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.style.borderTopColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.style.borderBottomColor = new Color(0.3f, 0.47f, 0.71f, 0.8f); + shell.pickingMode = PickingMode.Position; + root.Add(shell); + + var lowerer = new EveUiToolkitSurfaceLowerer(); + shell.Add(lowerer.Lower(BuildTradeItemDetailsSurfaceDefinition(item), HandleTradeItemDetailsSurfaceCommand)); + } + + private void HandleTradeItemDetailsSurfaceCommand(EveSurfaceCommandRequest request) + { + if (string.Equals(request.Command, CloseTradeItemDetailsCommand, StringComparison.Ordinal)) + { + HideTradeItemDetailsSurface(); + return; + } + + Debug.LogWarning($"Unknown trade item details command: {request.Command}"); + } + + private void HideTradeItemDetailsSurface() + { + if (_tradeItemSurfaceDocument == null) + return; + + _tradeItemSurfaceDocument.rootVisualElement.Clear(); + _tradeItemSurfaceDocument.gameObject.SetActive(false); + } + + private UIDocument ResolveTradeItemDetailsSurfaceDocument() + { + if (_tradeItemSurfaceDocument != null) + return _tradeItemSurfaceDocument; + + var host = new GameObject("Aetheria Trade Item Details Surface"); + host.transform.SetParent(transform, false); + var document = host.AddComponent(); + document.sortingOrder = 1003; + host.SetActive(false); + _tradeItemSurfaceDocument = document; + return document; + } + + private EveSurfaceDocument BuildTradeItemDetailsSurfaceDefinition(AetheriaRuntimeCatalogItem item) + { + var children = new List + { + Card( + $"{TradeItemSurfaceId}.summary", + item.Name, + Text($"{TradeItemSurfaceId}.description", item.Description ?? "No typed item description is available."), + Text( + $"{TradeItemSurfaceId}.note", + "Trade still owns purchase and row population. This surface replaces the old PropertiesPanel item-inspection shell."), + Metric( + $"{TradeItemSurfaceId}.manufacturer", + "Manufacturer", + ActionGameManager.RuntimeCatalog?.GetManufacturer(item)?.Name ?? "GameCult"), + Metric( + $"{TradeItemSurfaceId}.mass", + "Mass", + ActionGameManager.RuntimePlayerSettings.Format((float)item.Mass)), + Metric( + $"{TradeItemSurfaceId}.price", + "Price", + item.Price.ToString("N0"))) + }; + + if (!string.IsNullOrWhiteSpace(item.HardpointType)) + { + children.Add(Card( + $"{TradeItemSurfaceId}.durability.card", + "Durability", + Metric( + $"{TradeItemSurfaceId}.durability", + "Max Durability", + ActionGameManager.RuntimePlayerSettings.Format((float)item.Durability)), + Metric( + $"{TradeItemSurfaceId}.temperature_range", + "Thermal Range", + FormatTemperatureRange(item)))); + + foreach (var behaviorCard in BuildTradeItemBehaviorCards(item)) + { + children.Add(behaviorCard); + } + } + + children.Add(ButtonRow( + $"{TradeItemSurfaceId}.actions", + Button($"{TradeItemSurfaceId}.close", "Close", CloseTradeItemDetailsCommand))); + + return new EveSurfaceDocument( + CargoSelectorSurfaceType, + CargoSelectorSurfaceSchema, + CargoSelectorSurfaceProviderId, + CargoSelectorSurfaceProviderKind, + "Trade Item Details", + version: 1, + DateTime.UtcNow.ToString("O"), + new EveSurfaceTree( + TradeItemSurfaceId, + Node( + $"{TradeItemSurfaceId}.root", + "surface", + Array.Empty<(string Key, string Value)>(), + children.ToArray()), + Array.Empty()), + new[] + { + new EveCommandTemplate(CloseTradeItemDetailsCommand, "Close", "unity-uitoolkit") + }); + } + + private IEnumerable BuildTradeItemBehaviorCards(AetheriaRuntimeCatalogItem item) + { + foreach (var behavior in item.BehaviorPayloads ?? Array.Empty()) + { + if (string.Equals(behavior.Kind, AetheriaRuntimeBehaviorKinds.StatModifier, StringComparison.Ordinal)) + { + var statReference = ReadTypedStatReference(FindTypedBehaviorField(behavior, 1)?.Value); + var modifier = ReadTypedPerformanceStat(FindTypedBehaviorField(behavior, 2)?.Value); + var modifierType = ReadTypedEnum(FindTypedBehaviorField(behavior, 3)?.Value, StatModifierType.Constant); + yield return Card( + $"{TradeItemSurfaceId}.behavior.{behavior.Kind}.stat_modifier", + "Stat Modifier", + Metric( + $"{TradeItemSurfaceId}.behavior.{behavior.Kind}.target", + $"{statReference.target.SplitCamelCase()}:{statReference.stat.SplitCamelCase()}", + $"{(modifierType == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.RuntimePlayerSettings.Format(modifier.Min)}")); + continue; + } + + var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind); + if (metadata == null) + continue; + + var fields = metadata.DisplayFields + .Select(field => BuildTradeItemBehaviorMetric(behavior, field)) + .Where(metric => metric != null) + .ToArray(); + + if (fields.Length == 0) + continue; + + yield return Card( + $"{TradeItemSurfaceId}.behavior.{behavior.Kind}", + behavior.Kind.FormatTypeName(), + fields); + } + } + + private EveSurfaceComponent BuildTradeItemBehaviorMetric( + AetheriaRuntimeBehaviorPayload behavior, + AetheriaRuntimeBehaviorFieldMetadata field) + { + var payloadField = FindTypedBehaviorField(behavior, field.Key); + if (payloadField == null) + return null; + + string value; + switch (field.ValueKind) + { + case AetheriaRuntimeBehaviorFieldValueKind.Number: + value = ActionGameManager.RuntimePlayerSettings.Format((float)payloadField.Value.NumberValue); + break; + case AetheriaRuntimeBehaviorFieldValueKind.Temperature: + value = ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)payloadField.Value.NumberValue); + break; + case AetheriaRuntimeBehaviorFieldValueKind.Integer: + value = ((int)payloadField.Value.NumberValue).ToString(); + break; + case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat: + value = ActionGameManager.RuntimePlayerSettings.Format(ReadTypedPerformanceStat(payloadField.Value).Min); + break; + default: + return null; + } + + return Metric( + $"{TradeItemSurfaceId}.behavior.{behavior.Kind}.{field.Key}", + field.Name.SplitCamelCase(), + value); + } + + private static string FormatTemperatureRange(AetheriaRuntimeCatalogItem item) + { + if (item.MaximumTemperature > item.MinimumTemperature) + { + return + $"{ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)item.MinimumTemperature)} to " + + $"{ActionGameManager.RuntimePlayerSettings.FormatTemperature((float)item.MaximumTemperature)}"; + } + + return "No typed thermal range"; + } + + private static AetheriaRuntimeBehaviorField FindTypedBehaviorField(AetheriaRuntimeBehaviorPayload behavior, int? key) + { + return key == null + ? null + : behavior.Fields.FirstOrDefault(field => field.Key == key.Value); + } + + private static PerformanceStat ReadTypedPerformanceStat(AetheriaRuntimeBehaviorValue value) + { + return new PerformanceStat + { + Min = ReadTypedChildNumber(value, 0), + Max = ReadTypedChildNumber(value, 1), + HeatExponentMultiplier = ReadTypedChildNumber(value, 2), + DurabilityExponentMultiplier = ReadTypedChildNumber(value, 3), + QualityExponent = ReadTypedChildNumber(value, 4) + }; + } + + private static (string target, string stat) ReadTypedStatReference(AetheriaRuntimeBehaviorValue value) + { + return ( + ReadTypedChildString(value, 1), + ReadTypedChildString(value, 2)); + } + + private static float ReadTypedChildNumber(AetheriaRuntimeBehaviorValue value, int index) + { + return value != null && value.Children.Count > index + ? (float)value.Children[index].NumberValue + : 0f; + } + + private static string ReadTypedChildString(AetheriaRuntimeBehaviorValue value, int index) + { + return value != null && value.Children.Count > index + ? value.Children[index].StringValue ?? "" + : ""; + } + + private static T ReadTypedEnum(AetheriaRuntimeBehaviorValue value, T fallback) where T : struct + { + if (!string.IsNullOrWhiteSpace(value?.StringValue) && Enum.TryParse(value.StringValue, true, out T parsed)) + { + return parsed; + } + + return value != null && Enum.IsDefined(typeof(T), (int)value.NumberValue) + ? (T)Enum.ToObject(typeof(T), (int)value.NumberValue) + : fallback; + } + void Start() { FoldoutButton.onClick.AddListener(() => @@ -1275,6 +1566,7 @@ public class TradeMenu : MonoBehaviour HideCargoSelectorSurface(); HideFilterSurface(); HideRowActionSurface(); + HideTradeItemDetailsSurface(); } private void OnDestroy() @@ -1296,5 +1588,11 @@ public class TradeMenu : MonoBehaviour Destroy(_rowActionSurfaceDocument.gameObject); _rowActionSurfaceDocument = null; } + + if (_tradeItemSurfaceDocument != null) + { + Destroy(_tradeItemSurfaceDocument.gameObject); + _tradeItemSurfaceDocument = null; + } } } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 655a9c645..b3f722d14 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -129,6 +129,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. Trade filter selection and simple-commodity buy-quantity entry now lower through local Eve/UI Toolkit surfaces instead of rebuilding `ContextMenu` dropdowns/options in `TradeMenu`. + Trade typed-item inspection now lowers through a local Eve/UI Toolkit surface + instead of routing spreadsheet row clicks into `PropertiesPanel.Inspect`. - The combat schematic HUD uses typed runtime catalog weapon facets for its static weapon icon strip; missing typed weapon facets no longer fall back to legacy `WeaponItemData`. Schematic weapon-row selection now uses typed @@ -1585,6 +1587,10 @@ First Aetheria surfaces to publish: filter state and the quantity dialog still delegates to gameplay-owned purchase commits, but the old `ContextMenu` dropdown/option tree no longer owns any live trade-menu shell. + - Done: replace `TradeMenu` typed-item inspection with an Eve-owned UI + Toolkit surface. The surface still projects typed catalog facts from local + trade rows, but spreadsheet row clicks no longer route through the old + `PropertiesPanel` shell. - Done: move the shared `org.gamecult.eve.surface` and `org.gamecult.eve.unity-uitoolkit` packages into the Eve repo and retarget Aetheria to import them from there instead of carrying local staged @@ -1963,6 +1969,9 @@ First Aetheria surfaces to publish: through shared UI Toolkit `UIDocument` hosts and local Eve surface documents instead of rebuilding `ContextMenu.AddDropdown(...)`, `ContextMenu.AddOption(...)`, and `ContextMenu.Show()` shells in `TradeMenu`. +- Trade typed-item inspection now lowers through a shared UI Toolkit + `UIDocument` host and a local Eve surface document instead of routing + spreadsheet row clicks into `PropertiesPanel.Inspect(...)`. - Keyboard display layout parsing is no longer JSON-backed: `InputDisplayLayout` builds its static ANSI-104 display projection from typed `InputLayout` rows/columns. The dead commented Ink `ToJson` write path and