Add daemon item stat query refs
This commit is contained in:
parent
c664e963c8
commit
c89f8ea480
5 changed files with 564 additions and 11 deletions
|
|
@ -4076,22 +4076,48 @@ static void RequireInventoryShipSettingsUseEveSurface(string root)
|
|||
static void RequireInventoryCargoItemDetailsUseEveSurface(string root)
|
||||
{
|
||||
var inventoryMenuPath = Path.Combine(root, "Assets", "Scripts", "UI", "Menu", "InventoryMenu.cs");
|
||||
var actionGameManagerPath = Path.Combine(root, "Assets", "Scripts", "Gameplay", "ActionGameManager.cs");
|
||||
var surfaceDocumentPath = Path.Combine(
|
||||
root,
|
||||
"Packages",
|
||||
"org.gamecult.aetheria.state",
|
||||
"Runtime",
|
||||
"AetheriaRuntimePlayerSettingsSurfaceBuilder.cs");
|
||||
var daemonItemStatQueriesPath = Path.Combine(
|
||||
root,
|
||||
"Packages",
|
||||
"org.gamecult.aetheria.state",
|
||||
"Runtime",
|
||||
"AetheriaRuntimeDaemonItemStatQueries.cs");
|
||||
var cargoItemSurfaceBuilderPath = Path.Combine(
|
||||
root,
|
||||
"Packages",
|
||||
"org.gamecult.aetheria.state",
|
||||
"Runtime",
|
||||
"AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs");
|
||||
if (!File.Exists(inventoryMenuPath))
|
||||
var requiredFiles = new[]
|
||||
{
|
||||
throw new InvalidOperationException("Cannot verify inventory cargo-item shell; InventoryMenu.cs is missing.");
|
||||
}
|
||||
if (!File.Exists(cargoItemSurfaceBuilderPath))
|
||||
inventoryMenuPath,
|
||||
actionGameManagerPath,
|
||||
surfaceDocumentPath,
|
||||
daemonItemStatQueriesPath,
|
||||
cargoItemSurfaceBuilderPath
|
||||
};
|
||||
var missingFiles = requiredFiles
|
||||
.Where(path => !File.Exists(path))
|
||||
.Select(path => Path.GetRelativePath(root, path))
|
||||
.ToArray();
|
||||
if (missingFiles.Length > 0)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot verify inventory cargo-item shell; AetheriaRuntimeCargoItemDetailsSurfaceBuilder.cs is missing.");
|
||||
throw new InvalidOperationException(
|
||||
"Cannot verify inventory cargo-item shell; required files are missing: " +
|
||||
string.Join(", ", missingFiles));
|
||||
}
|
||||
|
||||
var source = File.ReadAllText(inventoryMenuPath);
|
||||
var actionGameManager = File.ReadAllText(actionGameManagerPath);
|
||||
var surfaceDocument = File.ReadAllText(surfaceDocumentPath);
|
||||
var daemonItemStatQueries = File.ReadAllText(daemonItemStatQueriesPath);
|
||||
var cargoItemSurfaceBuilder = File.ReadAllText(cargoItemSurfaceBuilderPath);
|
||||
var requiredSymbols = new[]
|
||||
{
|
||||
|
|
@ -4123,6 +4149,26 @@ static void RequireInventoryCargoItemDetailsUseEveSurface(string root)
|
|||
throw new InvalidOperationException("InventoryMenu cargo click path no longer routes item inspection through the Eve surface.");
|
||||
}
|
||||
|
||||
if (!source.Contains("AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(", StringComparison.Ordinal) ||
|
||||
source.Contains("GameManager.ItemManager.GetTier", StringComparison.Ordinal) ||
|
||||
source.Contains("GameManager.ItemManager.Evaluate", StringComparison.Ordinal) ||
|
||||
actionGameManager.Contains("ObservedItemStat(", StringComparison.Ordinal) ||
|
||||
actionGameManager.Contains("ItemManager.Evaluate(stat", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"InventoryMenu must project current item stats through shared daemon item-stat queries instead of direct Unity ItemManager stat authority.");
|
||||
}
|
||||
|
||||
if (!surfaceDocument.Contains("public static class AetheriaRuntimeSurfaceStateRefs", StringComparison.Ordinal) ||
|
||||
!surfaceDocument.Contains("public const string Value = \"valueRef\"", StringComparison.Ordinal) ||
|
||||
!daemonItemStatQueries.Contains("public const string StateRefPrefix = \"aetheria.state/items\"", StringComparison.Ordinal) ||
|
||||
!daemonItemStatQueries.Contains("public static string ItemStatRef(", StringComparison.Ordinal) ||
|
||||
!daemonItemStatQueries.Contains("public string ValueRef =>", StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Eve/CultUI item stat surfaces must expose typed daemon state refs that the UI runtime can resolve.");
|
||||
}
|
||||
|
||||
var forbiddenSymbols = new[]
|
||||
{
|
||||
"BuildCargoItemDetailsSurfaceDefinition(",
|
||||
|
|
|
|||
|
|
@ -542,6 +542,11 @@ public class ActionGameManager : MonoBehaviour
|
|||
return _observedLoadoutTemplates;
|
||||
}
|
||||
|
||||
public (RarityTier tier, int upgrades) ObservedItemTier(CraftedItemInstance item)
|
||||
{
|
||||
return ItemManager.GetTier(item);
|
||||
}
|
||||
|
||||
private readonly (float2 direction, string name)[] _directions = {
|
||||
(new float2(0, 1), "Front"),
|
||||
(new float2(1, 0), "Right"),
|
||||
|
|
|
|||
|
|
@ -506,7 +506,7 @@ public class InventoryMenu : MonoBehaviour
|
|||
|
||||
if (item is EquippableItem equippableItem)
|
||||
{
|
||||
var (itemTier, upgrades) = GameManager.ItemManager.GetTier(equippableItem);
|
||||
var (itemTier, upgrades) = GameManager.ObservedItemTier(equippableItem);
|
||||
tier = $"{itemTier.Name}{new string('+', upgrades)}";
|
||||
durability = $"{(int)(equippableItem.Durability / GetMaxDurability(typedItem, equippableItem) * 100)}%";
|
||||
thermalRange = FormatTemperatureRange(typedItem);
|
||||
|
|
@ -548,7 +548,7 @@ public class InventoryMenu : MonoBehaviour
|
|||
new AetheriaRuntimeCargoItemMetric(
|
||||
$"{AetheriaRuntimeCargoItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.target",
|
||||
$"{statReference.Target.SplitCamelCase()}:{statReference.Stat.SplitCamelCase()}",
|
||||
$"{(modifierType == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.RuntimePlayerSettings.Format(GameManager.ItemManager.Evaluate(modifier, equippableItem))}")
|
||||
$"{(modifierType == StatModifierType.Constant ? "+" : "x")}{FormatCurrentItemStat(modifier, equippableItem)}")
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -594,7 +594,7 @@ public class InventoryMenu : MonoBehaviour
|
|||
value = ((int)payloadField.Value.NumberValue).ToString();
|
||||
break;
|
||||
case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat:
|
||||
value = ActionGameManager.RuntimePlayerSettings.Format(GameManager.ItemManager.Evaluate(AetheriaRuntimeBehaviorValueReader.ReadPerformanceStat(payloadField.Value), equippableItem));
|
||||
value = FormatCurrentItemStat(payloadField.Value, equippableItem);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
|
|
@ -627,7 +627,7 @@ public class InventoryMenu : MonoBehaviour
|
|||
new AetheriaRuntimeEquippedItemMetric(
|
||||
$"{AetheriaRuntimeEquippedItemDetailsSurfaceBuilder.SurfaceId}.behavior.{behavior.Kind}.target",
|
||||
$"{statReference.Target.SplitCamelCase()}:{statReference.Stat.SplitCamelCase()}",
|
||||
$"{(modifierType == StatModifierType.Constant ? "+" : "x")}{ActionGameManager.RuntimePlayerSettings.Format(GameManager.ItemManager.Evaluate(modifier, equippableItem))}")
|
||||
$"{(modifierType == StatModifierType.Constant ? "+" : "x")}{FormatCurrentItemStat(modifier, equippableItem)}")
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
|
@ -673,7 +673,7 @@ public class InventoryMenu : MonoBehaviour
|
|||
value = ((int)payloadField.Value.NumberValue).ToString();
|
||||
break;
|
||||
case AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat:
|
||||
value = ActionGameManager.RuntimePlayerSettings.Format(GameManager.ItemManager.Evaluate(AetheriaRuntimeBehaviorValueReader.ReadPerformanceStat(payloadField.Value), equippableItem));
|
||||
value = FormatCurrentItemStat(payloadField.Value, equippableItem);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
|
|
@ -687,10 +687,185 @@ public class InventoryMenu : MonoBehaviour
|
|||
|
||||
private string BuildEquippedItemTitle(EquippedItem item, AetheriaRuntimeCatalogItem typedItem)
|
||||
{
|
||||
var (tier, upgrades) = GameManager.ItemManager.GetTier(item.EquippableItem);
|
||||
var (tier, upgrades) = GameManager.ObservedItemTier(item.EquippableItem);
|
||||
return $"{typedItem?.Name ?? "Unknown Item"} ({tier.Name}{new string('+', upgrades)})";
|
||||
}
|
||||
|
||||
private static string FormatCurrentItemStat(PerformanceStat stat, EquippableItem item)
|
||||
{
|
||||
return FormatCurrentItemStat(ToRuntimeBehaviorValue(stat), item);
|
||||
}
|
||||
|
||||
private static string FormatCurrentItemStat(AetheriaRuntimeBehaviorValue stat, EquippableItem item)
|
||||
{
|
||||
return ActionGameManager.RuntimePlayerSettings.Format(
|
||||
AetheriaRuntimeDaemonItemStatQueries.EvaluatePerformanceStat(
|
||||
stat,
|
||||
ToRuntimeLoadoutItem(item),
|
||||
item?.Temperature ?? 0));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeLoadoutItemCommit ToRuntimeLoadoutItem(EquippableItem item)
|
||||
{
|
||||
return AetheriaRuntimeDaemonItemStatQueries.ItemCommit(
|
||||
item?.ItemKey ?? "",
|
||||
item?.Quality ?? 1,
|
||||
item?.Durability ?? 1,
|
||||
enabled: true,
|
||||
overrideShutdown: item != null && item.OverrideShutdown);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue ToRuntimeBehaviorValue(PerformanceStat stat)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"performance-stat",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
Number(stat?.Min ?? 0),
|
||||
Number(stat?.Max ?? 0),
|
||||
Number(stat?.HeatExponentMultiplier ?? 0),
|
||||
Number(stat?.DurabilityExponentMultiplier ?? 0),
|
||||
Number(stat?.QualityExponent ?? 0),
|
||||
ToRuntimeStatRecipeValue(stat?.Recipe)
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue ToRuntimeStatRecipeValue(StatRecipe recipe)
|
||||
{
|
||||
if (recipe == null)
|
||||
return Number(0);
|
||||
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"stat-recipe",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
Number(recipe.BaseValue),
|
||||
new AetheriaRuntimeBehaviorValue(
|
||||
"stat-recipe-modifiers",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
(recipe.Modifiers ?? Array.Empty<StatRecipeModifier>())
|
||||
.Where(modifier => modifier != null)
|
||||
.Select(ToRuntimeStatRecipeModifierValue)
|
||||
.ToArray(),
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>())
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue ToRuntimeStatRecipeModifierValue(StatRecipeModifier modifier)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"stat-recipe-modifier",
|
||||
"",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
new[]
|
||||
{
|
||||
Text(ConditionToken(modifier.Condition)),
|
||||
Text(OperationToken(modifier.Operation)),
|
||||
Number(modifier.Amount),
|
||||
Number(0),
|
||||
Bool(modifier.Enabled)
|
||||
},
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
|
||||
private static string ConditionToken(StatConditionMask condition)
|
||||
{
|
||||
switch (condition)
|
||||
{
|
||||
case StatConditionMask.Quality:
|
||||
return AetheriaRuntimeStatRecipeConditions.Quality;
|
||||
case StatConditionMask.Durability:
|
||||
return AetheriaRuntimeStatRecipeConditions.Durability;
|
||||
case StatConditionMask.Heat:
|
||||
return AetheriaRuntimeStatRecipeConditions.Heat;
|
||||
case StatConditionMask.Charge:
|
||||
return AetheriaRuntimeStatRecipeConditions.Charge;
|
||||
case StatConditionMask.Ammo:
|
||||
return AetheriaRuntimeStatRecipeConditions.Ammo;
|
||||
case StatConditionMask.Range:
|
||||
return AetheriaRuntimeStatRecipeConditions.Range;
|
||||
case StatConditionMask.Integrity:
|
||||
return AetheriaRuntimeStatRecipeConditions.Integrity;
|
||||
case StatConditionMask.PilotSkill:
|
||||
return AetheriaRuntimeStatRecipeConditions.PilotSkill;
|
||||
case StatConditionMask.Environment:
|
||||
return AetheriaRuntimeStatRecipeConditions.Environment;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static string OperationToken(StatModifierOperation operation)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case StatModifierOperation.Multiply:
|
||||
return AetheriaRuntimeStatRecipeOperations.Multiply;
|
||||
case StatModifierOperation.Override:
|
||||
return AetheriaRuntimeStatRecipeOperations.Override;
|
||||
default:
|
||||
return AetheriaRuntimeStatRecipeOperations.Add;
|
||||
}
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue Number(double value)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"number",
|
||||
"",
|
||||
value,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue Text(string value)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"string",
|
||||
value ?? "",
|
||||
0,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue Bool(bool value)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorValue(
|
||||
"bool",
|
||||
"",
|
||||
0,
|
||||
value,
|
||||
"",
|
||||
"",
|
||||
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
||||
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
||||
}
|
||||
|
||||
private static bool IsWeaponGroupAssigned(EquippedItem item, int groupIndex)
|
||||
{
|
||||
return item?.Entity?.WeaponGroups != null &&
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public readonly struct AetheriaRuntimeDaemonItemStatValue
|
||||
{
|
||||
public AetheriaRuntimeDaemonItemStatValue(
|
||||
string itemKey,
|
||||
string behaviorKind,
|
||||
int behaviorGroup,
|
||||
int fieldKey,
|
||||
string label,
|
||||
double value)
|
||||
{
|
||||
ItemKey = itemKey ?? "";
|
||||
BehaviorKind = behaviorKind ?? "";
|
||||
BehaviorGroup = behaviorGroup;
|
||||
FieldKey = fieldKey;
|
||||
Label = label ?? "";
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string ItemKey { get; }
|
||||
public string BehaviorKind { get; }
|
||||
public int BehaviorGroup { get; }
|
||||
public int FieldKey { get; }
|
||||
public string Label { get; }
|
||||
public double Value { get; }
|
||||
public string ValueRef => AetheriaRuntimeDaemonItemStatQueries.ItemStatRef(
|
||||
ItemKey,
|
||||
BehaviorKind,
|
||||
BehaviorGroup,
|
||||
FieldKey);
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeDaemonItemStatQueries
|
||||
{
|
||||
public const string StateRefPrefix = "aetheria.state/items";
|
||||
|
||||
public static string ItemStatRef(
|
||||
string itemKey,
|
||||
string behaviorKind,
|
||||
int behaviorGroup,
|
||||
int fieldKey)
|
||||
{
|
||||
return $"{StateRefPrefix}/{Token(itemKey)}/behaviors/{Token(behaviorKind)}/{behaviorGroup}/stats/{fieldKey}";
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeStatRecipePreviewState ConditionsFor(
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
double heat = 0,
|
||||
double charge = 1,
|
||||
double ammo = 1,
|
||||
double range = 1,
|
||||
double integrity = 1,
|
||||
double pilotSkill = 0,
|
||||
double environment = 0)
|
||||
{
|
||||
return new AetheriaRuntimeStatRecipePreviewState(
|
||||
quality: Clamp01(item?.Quality ?? 1),
|
||||
durability: Clamp01(item?.Durability ?? 1),
|
||||
heat: Clamp01(heat),
|
||||
charge: Clamp01(charge),
|
||||
ammo: Clamp01(ammo),
|
||||
range: Clamp01(range),
|
||||
integrity: Clamp01(integrity),
|
||||
pilotSkill: Clamp01(pilotSkill),
|
||||
environment: Clamp01(environment));
|
||||
}
|
||||
|
||||
public static double EvaluatePerformanceStat(
|
||||
AetheriaRuntimeBehaviorValue? value,
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
double heat = 0)
|
||||
{
|
||||
return EvaluatePerformanceStat(
|
||||
ReadPerformanceStat(value),
|
||||
ConditionsFor(item, heat));
|
||||
}
|
||||
|
||||
public static double EvaluatePerformanceStat(
|
||||
AetheriaRuntimePerformanceStat? stat,
|
||||
AetheriaRuntimeStatRecipePreviewState conditions)
|
||||
{
|
||||
if (stat == null)
|
||||
return 0;
|
||||
|
||||
conditions ??= AetheriaRuntimeStatRecipePreviewState.Default;
|
||||
if (stat.Recipe != null)
|
||||
return EvaluateRecipe(stat.Recipe, conditions);
|
||||
|
||||
var heat = stat.HeatExponentMultiplier == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(conditions.Heat), stat.HeatExponentMultiplier);
|
||||
var durability = stat.DurabilityExponentMultiplier == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(conditions.Durability), stat.DurabilityExponentMultiplier);
|
||||
var quality = stat.QualityExponent == 0
|
||||
? 1
|
||||
: Math.Pow(Clamp01(conditions.Quality), stat.QualityExponent);
|
||||
|
||||
return Lerp(stat.Min, stat.Max, durability * quality * heat);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AetheriaRuntimeDaemonItemStatValue> QueryPerformanceStats(
|
||||
AetheriaRuntimeCatalogItem? catalogItem,
|
||||
AetheriaRuntimeLoadoutItemCommit? item,
|
||||
double heat = 0)
|
||||
{
|
||||
if (catalogItem?.BehaviorPayloads == null || catalogItem.BehaviorPayloads.Count == 0)
|
||||
return Array.Empty<AetheriaRuntimeDaemonItemStatValue>();
|
||||
|
||||
var conditions = ConditionsFor(item, heat);
|
||||
var stats = new List<AetheriaRuntimeDaemonItemStatValue>();
|
||||
foreach (var behavior in catalogItem.BehaviorPayloads)
|
||||
{
|
||||
var metadata = AetheriaRuntimeBehaviorMetadataCatalog.Get(behavior.Kind);
|
||||
foreach (var field in behavior.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
{
|
||||
var fieldMetadata = metadata?.DisplayFields.FirstOrDefault(candidate => candidate.Key == field.Key);
|
||||
if (fieldMetadata?.ValueKind != AetheriaRuntimeBehaviorFieldValueKind.PerformanceStat)
|
||||
continue;
|
||||
|
||||
stats.Add(new AetheriaRuntimeDaemonItemStatValue(
|
||||
item?.ItemKey ?? catalogItem.ItemKey,
|
||||
behavior.Kind,
|
||||
behavior.Group,
|
||||
field.Key,
|
||||
fieldMetadata.Name,
|
||||
EvaluatePerformanceStat(ReadPerformanceStat(field.Value), conditions)));
|
||||
}
|
||||
}
|
||||
|
||||
return stats.Count == 0 ? Array.Empty<AetheriaRuntimeDaemonItemStatValue>() : stats;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeLoadoutItemCommit ItemCommit(
|
||||
string itemKey,
|
||||
double quality,
|
||||
double durability,
|
||||
int quantity = 1,
|
||||
bool enabled = true,
|
||||
bool overrideShutdown = false)
|
||||
{
|
||||
return new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = itemKey ?? "",
|
||||
Quality = quality,
|
||||
Durability = durability,
|
||||
Quantity = quantity,
|
||||
Enabled = enabled,
|
||||
OverrideShutdown = overrideShutdown
|
||||
};
|
||||
}
|
||||
|
||||
private static AetheriaRuntimePerformanceStat ReadPerformanceStat(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
return new AetheriaRuntimePerformanceStat(
|
||||
ChildNumber(value, 0),
|
||||
ChildNumber(value, 1),
|
||||
ChildNumber(value, 2),
|
||||
ChildNumber(value, 3),
|
||||
ChildNumber(value, 4),
|
||||
ReadStatRecipe(ChildValue(value, 5)));
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipe? ReadStatRecipe(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
if (value == null || value.Children.Count == 0)
|
||||
return null;
|
||||
|
||||
var modifiers = ChildValue(value, 1)?.Children
|
||||
.Select(ReadStatRecipeModifier)
|
||||
.Where(modifier => modifier != null)
|
||||
.Cast<AetheriaRuntimeStatRecipeModifier>()
|
||||
.ToArray() ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>();
|
||||
|
||||
return new AetheriaRuntimeStatRecipe(ChildNumber(value, 0), modifiers);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeStatRecipeModifier? ReadStatRecipeModifier(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return new AetheriaRuntimeStatRecipeModifier(
|
||||
ChildString(value, 0),
|
||||
ChildString(value, 1),
|
||||
ChildNumber(value, 2),
|
||||
ReadCurveKeys(ChildValue(value, 3)),
|
||||
value.Children.Count <= 4 || ChildValue(value, 4)?.BoolValue == true);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeCurveKey> ReadCurveKeys(AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
if (value?.Children == null || value.Children.Count == 0)
|
||||
return Array.Empty<AetheriaRuntimeCurveKey>();
|
||||
|
||||
return value.Children
|
||||
.Where(key => key.Children.Count >= 4)
|
||||
.Select(key => new AetheriaRuntimeCurveKey(
|
||||
ChildNumber(key, 0),
|
||||
ChildNumber(key, 1),
|
||||
ChildNumber(key, 2),
|
||||
ChildNumber(key, 3)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double EvaluateRecipe(
|
||||
AetheriaRuntimeStatRecipe recipe,
|
||||
AetheriaRuntimeStatRecipePreviewState conditions)
|
||||
{
|
||||
var value = recipe.BaseValue;
|
||||
foreach (var modifier in recipe.Modifiers ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>())
|
||||
{
|
||||
if (modifier == null || !modifier.Enabled || string.IsNullOrWhiteSpace(modifier.Condition))
|
||||
continue;
|
||||
|
||||
var sample = SampleCurve(modifier.CurveKeys, conditions.GetConditionValue(modifier.Condition));
|
||||
switch (modifier.Operation ?? "")
|
||||
{
|
||||
case AetheriaRuntimeStatRecipeOperations.Multiply:
|
||||
value *= 1 + ((modifier.Amount - 1) * sample);
|
||||
break;
|
||||
case AetheriaRuntimeStatRecipeOperations.Override:
|
||||
value = Lerp(value, modifier.Amount, sample);
|
||||
break;
|
||||
default:
|
||||
value += modifier.Amount * sample;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static double SampleCurve(IReadOnlyList<AetheriaRuntimeCurveKey>? keys, double value)
|
||||
{
|
||||
if (keys == null || keys.Count == 0)
|
||||
return Clamp01(value);
|
||||
|
||||
var ordered = keys.OrderBy(key => key.Time).ToArray();
|
||||
if (value <= ordered[0].Time)
|
||||
return Clamp01(ordered[0].Value);
|
||||
|
||||
for (var index = 1; index < ordered.Length; index++)
|
||||
{
|
||||
var next = ordered[index];
|
||||
var previous = ordered[index - 1];
|
||||
if (value > next.Time)
|
||||
continue;
|
||||
|
||||
var span = next.Time - previous.Time;
|
||||
var t = span <= double.Epsilon ? 1 : Clamp01((value - previous.Time) / span);
|
||||
return Clamp01(Lerp(previous.Value, next.Value, t));
|
||||
}
|
||||
|
||||
return Clamp01(ordered[ordered.Length - 1].Value);
|
||||
}
|
||||
|
||||
private static double ChildNumber(AetheriaRuntimeBehaviorValue? value, int index)
|
||||
{
|
||||
return ChildValue(value, index)?.NumberValue ?? 0;
|
||||
}
|
||||
|
||||
private static string ChildString(AetheriaRuntimeBehaviorValue? value, int index)
|
||||
{
|
||||
return ChildValue(value, index)?.StringValue ?? "";
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorValue? ChildValue(AetheriaRuntimeBehaviorValue? value, int index)
|
||||
{
|
||||
return value != null && value.Children.Count > index ? value.Children[index] : null;
|
||||
}
|
||||
|
||||
private static double Clamp01(double value)
|
||||
{
|
||||
if (value < 0)
|
||||
return 0;
|
||||
return value > 1 ? 1 : value;
|
||||
}
|
||||
|
||||
private static double Lerp(double from, double to, double t)
|
||||
{
|
||||
return from + ((to - from) * t);
|
||||
}
|
||||
|
||||
private static string Token(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value)
|
||||
? "_"
|
||||
: value.Trim().Replace("/", "%2F");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +81,34 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public IReadOnlyList<AetheriaRuntimeSurfaceComponent> Children { get; }
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeSurfaceStateRefs
|
||||
{
|
||||
public const string Source = "stateRef";
|
||||
public const string Value = "valueRef";
|
||||
public const string Label = "labelRef";
|
||||
public const string Format = "stateFormat";
|
||||
|
||||
public static (string Key, string Value) SourceRef(string reference)
|
||||
{
|
||||
return (Source, reference ?? "");
|
||||
}
|
||||
|
||||
public static (string Key, string Value) ValueRef(string reference)
|
||||
{
|
||||
return (Value, reference ?? "");
|
||||
}
|
||||
|
||||
public static (string Key, string Value) LabelRef(string reference)
|
||||
{
|
||||
return (Label, reference ?? "");
|
||||
}
|
||||
|
||||
public static (string Key, string Value) FormatRef(string format)
|
||||
{
|
||||
return (Format, format ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeSurfaceStyleToken
|
||||
{
|
||||
public AetheriaRuntimeSurfaceStyleToken(string name, string value)
|
||||
|
|
|
|||
Loading…
Reference in a new issue