Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeTradeValuePolicySurfaceBuilder.cs
2026-07-05 19:56:18 +01:00

231 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
#nullable enable
namespace GameCult.Aetheria.State.Verse
{
public static class AetheriaRuntimeTradeValuePolicySurfaceBuilder
{
public const string SurfaceId = "aetheria.tradeValuePolicy";
public static AetheriaRuntimeSurfaceDocument BuildFromCatalog(
AetheriaRuntimeCatalogSnapshot? catalog,
long version = 1)
{
var settings = catalog?.TradeValueSettings ?? AetheriaRuntimeTradeValueSettings.Default;
return new AetheriaRuntimeSurfaceDocument(
providerId: "aetheria",
providerKind: "game.design",
title: "Aetheria Trade Value Policy",
version: version,
updatedAtUtc: DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture),
surface: new AetheriaRuntimeSurfaceTree(
SurfaceId,
Node(
"aetheria.tradeValuePolicy.root",
"surface",
Array.Empty<(string Key, string Value)>(),
BuildSummaryCard(settings),
BuildQualityCurveCard(settings.QualityPriceModifier),
BuildTierList(settings.Tiers)),
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
commands: BuildCommandTemplates());
}
private static AetheriaRuntimeSurfaceComponent BuildSummaryCard(
AetheriaRuntimeTradeValueSettings settings)
{
return Node(
"aetheria.tradeValuePolicy.summary",
"card",
new[] { ("title", "Trade Value Policy") },
Metric(
"aetheria.tradeValuePolicy.summary.tiers",
"Rarity Tiers",
(settings?.Tiers.Count ?? 0).ToString(CultureInfo.InvariantCulture)),
Metric(
"aetheria.tradeValuePolicy.summary.qualityModifier",
"Quality Price",
$"{Format(settings?.QualityPriceModifier.Minimum ?? 0)} -> {Format(settings?.QualityPriceModifier.Maximum ?? 0)}"),
Text(
"aetheria.tradeValuePolicy.summary.note",
"Trade value is authored as typed policy state so inventory, trade, loadout pricing, and Starbridge station stock read the same curve."));
}
private static AetheriaRuntimeSurfaceComponent BuildQualityCurveCard(
AetheriaRuntimeExponentialLerp curve)
{
return Node(
"aetheria.tradeValuePolicy.quality",
"card",
new[] { ("title", "Quality Price Modifier") },
NumberInput(
"aetheria.tradeValuePolicy.quality.minimum",
"Minimum",
curve.Minimum,
AetheriaRuntimeTradeValuePolicyCommands.SetQualityMinimum),
NumberInput(
"aetheria.tradeValuePolicy.quality.maximum",
"Maximum",
curve.Maximum,
AetheriaRuntimeTradeValuePolicyCommands.SetQualityMaximum),
NumberInput(
"aetheria.tradeValuePolicy.quality.exponent",
"Exponent",
curve.Exponent,
AetheriaRuntimeTradeValuePolicyCommands.SetQualityExponent),
Row(
"aetheria.tradeValuePolicy.quality.samples",
Metric("aetheria.tradeValuePolicy.quality.sample.low", "Q 0.25", Format(curve.Evaluate(0.25))),
Metric("aetheria.tradeValuePolicy.quality.sample.mid", "Q 0.50", Format(curve.Evaluate(0.50))),
Metric("aetheria.tradeValuePolicy.quality.sample.high", "Q 0.75", Format(curve.Evaluate(0.75))),
Metric("aetheria.tradeValuePolicy.quality.sample.perfect", "Q 1.00", Format(curve.Evaluate(1.00)))));
}
private static AetheriaRuntimeSurfaceComponent BuildTierList(
IReadOnlyList<AetheriaRuntimeItemRarityTier> tiers)
{
var rows = (tiers ?? Array.Empty<AetheriaRuntimeItemRarityTier>())
.Select((tier, index) => TierRow(index, tier))
.ToArray();
return Node(
"aetheria.tradeValuePolicy.tiers",
"card",
new[] { ("title", "Rarity Tiers") },
rows.Length == 0
? Text("aetheria.tradeValuePolicy.tiers.empty", "No rarity tiers are authored.")
: Row("aetheria.tradeValuePolicy.tiers.rows", rows));
}
private static AetheriaRuntimeSurfaceComponent TierRow(
int index,
AetheriaRuntimeItemRarityTier tier)
{
return Node(
$"aetheria.tradeValuePolicy.tiers.{index}",
"inspector.kv",
new[]
{
("name", tier.Name),
("quality", Format(tier.Quality)),
("swatch", ToHex(tier.Red, tier.Green, tier.Blue))
},
NumberInput(
$"aetheria.tradeValuePolicy.tiers.{index}.quality",
"Quality",
tier.Quality,
AetheriaRuntimeTradeValuePolicyCommands.SetTierQuality,
("tierIndex", index.ToString(CultureInfo.InvariantCulture))));
}
private static IReadOnlyList<AetheriaRuntimeSurfaceCommandTemplate> BuildCommandTemplates()
{
return new[]
{
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeTradeValuePolicyCommands.Refresh, "Refresh", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeTradeValuePolicyCommands.SetQualityMinimum, "Set Quality Minimum", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeTradeValuePolicyCommands.SetQualityMaximum, "Set Quality Maximum", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeTradeValuePolicyCommands.SetQualityExponent, "Set Quality Exponent", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeTradeValuePolicyCommands.SetTierQuality, "Set Tier Quality", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport)
};
}
private static AetheriaRuntimeSurfaceComponent Metric(string id, string label, string value)
{
return Node(id, "metric", new[] { ("label", label), ("value", value ?? "") });
}
private static AetheriaRuntimeSurfaceComponent Text(string id, string value)
{
return Node(id, "text", new[] { ("value", value ?? "") });
}
private static AetheriaRuntimeSurfaceComponent Row(
string id,
params AetheriaRuntimeSurfaceComponent[] children)
{
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
}
private static AetheriaRuntimeSurfaceComponent NumberInput(
string id,
string label,
double value,
string command,
params (string Key, string Value)[] extraProps)
{
var props = new List<(string Key, string Value)>
{
("label", label ?? ""),
("value", Format(value)),
("command", command ?? ""),
("valueKind", "number")
};
if (extraProps != null)
{
props.AddRange(extraProps);
}
return Node(id, "control.text", props);
}
private static AetheriaRuntimeSurfaceComponent Node(
string id,
string kind,
IEnumerable<(string Key, string Value)> props,
params AetheriaRuntimeSurfaceComponent[] children)
{
return new AetheriaRuntimeSurfaceComponent(
id,
kind,
props.ToDictionary(prop => prop.Key, prop => prop.Value ?? "", StringComparer.Ordinal),
children ?? Array.Empty<AetheriaRuntimeSurfaceComponent>());
}
private static string Format(double value)
{
return value.ToString("0.###", CultureInfo.InvariantCulture);
}
private static string ToHex(double red, double green, double blue)
{
return "#" +
ToByte(red).ToString("X2", CultureInfo.InvariantCulture) +
ToByte(green).ToString("X2", CultureInfo.InvariantCulture) +
ToByte(blue).ToString("X2", CultureInfo.InvariantCulture);
}
private static int ToByte(double value)
{
if (value <= 0)
return 0;
if (value >= 1)
return 255;
return (int)Math.Round(value * 255);
}
}
public static class AetheriaRuntimeTradeValuePolicyCommands
{
public const string SurfaceId = AetheriaRuntimeTradeValuePolicySurfaceBuilder.SurfaceId;
public const string Refresh = "aetheria.trade_value_policy.refresh";
public const string SetQualityMinimum = "aetheria.trade_value_policy.quality.minimum.set";
public const string SetQualityMaximum = "aetheria.trade_value_policy.quality.maximum.set";
public const string SetQualityExponent = "aetheria.trade_value_policy.quality.exponent.set";
public const string SetTierQuality = "aetheria.trade_value_policy.tier.quality.set";
public static bool IsKnown(string command)
{
return command == Refresh ||
command == SetQualityMinimum ||
command == SetQualityMaximum ||
command == SetQualityExponent ||
command == SetTierQuality;
}
}
}