Aetheria/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeStatRecipeSurfaceBuilder.cs
2026-06-29 11:24:40 +01:00

915 lines
36 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace GameCult.Aetheria.State.Verse
{
public sealed class AetheriaRuntimeStatRecipeSurfaceState
{
public AetheriaRuntimeStatRecipeSurfaceState(
IReadOnlyList<AetheriaRuntimeStatRecipeState> recipes,
string selectedStatName,
AetheriaRuntimeStatRecipePreviewState preview,
string updatedAtUtc)
{
Recipes = recipes ?? Array.Empty<AetheriaRuntimeStatRecipeState>();
SelectedStatName = selectedStatName ?? "";
Preview = preview ?? AetheriaRuntimeStatRecipePreviewState.Default;
UpdatedAtUtc = updatedAtUtc ?? "";
}
public IReadOnlyList<AetheriaRuntimeStatRecipeState> Recipes { get; }
public string SelectedStatName { get; }
public AetheriaRuntimeStatRecipePreviewState Preview { get; }
public string UpdatedAtUtc { get; }
public AetheriaRuntimeStatRecipeState SelectedRecipe =>
Recipes.FirstOrDefault(recipe => string.Equals(recipe.StatName, SelectedStatName, StringComparison.Ordinal)) ??
Recipes.FirstOrDefault();
}
public sealed class AetheriaRuntimeStatRecipeState
{
public AetheriaRuntimeStatRecipeState(
string recipeKey,
string statName,
double baseValue,
IReadOnlyList<AetheriaRuntimeStatInfluenceState> influences)
{
RecipeKey = recipeKey ?? "";
StatName = statName ?? "";
BaseValue = baseValue;
Influences = influences ?? Array.Empty<AetheriaRuntimeStatInfluenceState>();
}
public string RecipeKey { get; }
public string StatName { get; }
public double BaseValue { get; }
public IReadOnlyList<AetheriaRuntimeStatInfluenceState> Influences { get; }
}
public sealed class AetheriaRuntimeStatInfluenceState
{
public AetheriaRuntimeStatInfluenceState(
string condition,
string operation,
double amount,
string curveLabel,
double previewSample,
bool enabled)
{
Condition = condition ?? "";
Operation = operation ?? AetheriaRuntimeStatRecipeOperations.Add;
Amount = amount;
CurveLabel = curveLabel ?? "";
PreviewSample = previewSample;
Enabled = enabled;
}
public string Condition { get; }
public string Operation { get; }
public double Amount { get; }
public string CurveLabel { get; }
public double PreviewSample { get; }
public bool Enabled { get; }
}
public sealed class AetheriaRuntimeStatRecipePreviewState
{
public static readonly AetheriaRuntimeStatRecipePreviewState Default =
new AetheriaRuntimeStatRecipePreviewState(
quality: 1,
durability: 1,
heat: 0,
charge: 1,
ammo: 1,
range: 1,
integrity: 1,
pilotSkill: 0,
environment: 0);
public AetheriaRuntimeStatRecipePreviewState(
double quality,
double durability,
double heat,
double charge,
double ammo,
double range,
double integrity,
double pilotSkill,
double environment)
{
Quality = quality;
Durability = durability;
Heat = heat;
Charge = charge;
Ammo = ammo;
Range = range;
Integrity = integrity;
PilotSkill = pilotSkill;
Environment = environment;
}
public double Quality { get; }
public double Durability { get; }
public double Heat { get; }
public double Charge { get; }
public double Ammo { get; }
public double Range { get; }
public double Integrity { get; }
public double PilotSkill { get; }
public double Environment { get; }
public double GetConditionValue(string condition)
{
switch (condition ?? "")
{
case AetheriaRuntimeStatRecipeConditions.Quality:
return Quality;
case AetheriaRuntimeStatRecipeConditions.Durability:
return Durability;
case AetheriaRuntimeStatRecipeConditions.Heat:
return Heat;
case AetheriaRuntimeStatRecipeConditions.Charge:
return Charge;
case AetheriaRuntimeStatRecipeConditions.Ammo:
return Ammo;
case AetheriaRuntimeStatRecipeConditions.Range:
return Range;
case AetheriaRuntimeStatRecipeConditions.Integrity:
return Integrity;
case AetheriaRuntimeStatRecipeConditions.PilotSkill:
return PilotSkill;
case AetheriaRuntimeStatRecipeConditions.Environment:
return Environment;
default:
return 0;
}
}
}
public static class AetheriaRuntimeStatRecipeConditions
{
public const string Quality = "quality";
public const string Durability = "durability";
public const string Heat = "heat";
public const string Charge = "charge";
public const string Ammo = "ammo";
public const string Range = "range";
public const string Integrity = "integrity";
public const string PilotSkill = "pilotSkill";
public const string Environment = "environment";
public static readonly IReadOnlyList<string> All = new[]
{
Quality,
Durability,
Heat,
Charge,
Ammo,
Range,
Integrity,
PilotSkill,
Environment
};
}
public static class AetheriaRuntimeStatRecipeOperations
{
public const string Add = "add";
public const string Multiply = "multiply";
public const string Override = "override";
}
public static class AetheriaRuntimeStatRecipeSurfaceBuilder
{
public static AetheriaRuntimeSurfaceDocument BuildFromCatalog(
AetheriaRuntimeCatalogSnapshot? catalog,
long version = 1)
{
var recipes = (catalog?.Items ?? Array.Empty<AetheriaRuntimeCatalogItem>())
.SelectMany(ProjectRows)
.OrderBy(recipe => recipe.StatName, StringComparer.Ordinal)
.ToArray();
return Build(
new AetheriaRuntimeStatRecipeSurfaceState(
recipes,
recipes.FirstOrDefault()?.StatName ?? "",
AetheriaRuntimeStatRecipePreviewState.Default,
DateTime.UtcNow.ToString("O")),
version);
}
public static AetheriaRuntimeSurfaceDocument Build(
AetheriaRuntimeStatRecipeSurfaceState state,
long version = 1)
{
state ??= new AetheriaRuntimeStatRecipeSurfaceState(
Array.Empty<AetheriaRuntimeStatRecipeState>(),
"",
AetheriaRuntimeStatRecipePreviewState.Default,
"");
var selected = state.SelectedRecipe;
var previewValue = selected == null ? 0 : EvaluatePreview(selected, state.Preview);
var enabledInfluenceCount = selected?.Influences.Count(influence => influence.Enabled) ?? 0;
return new AetheriaRuntimeSurfaceDocument(
providerId: "aetheria",
providerKind: "game.design",
title: "Aetheria Stat Recipes",
version: version,
updatedAtUtc: state.UpdatedAtUtc,
surface: new AetheriaRuntimeSurfaceTree(
AetheriaRuntimeStatRecipeCommands.SurfaceId,
Node(
"aetheria.statRecipes.root",
"surface",
Array.Empty<(string Key, string Value)>(),
BuildSummaryCard(selected, previewValue, enabledInfluenceCount),
BuildRecipeList(state, selected),
BuildSelectedRecipeCard(selected),
BuildConditionPalette(selected),
BuildInfluenceList(selected, state.Preview),
BuildPreviewCard(state.Preview)),
Array.Empty<AetheriaRuntimeSurfaceStyleToken>()),
commands: BuildCommandTemplates());
}
private static AetheriaRuntimeSurfaceComponent BuildSummaryCard(
AetheriaRuntimeStatRecipeState selected,
double previewValue,
int enabledInfluenceCount)
{
var statName = string.IsNullOrWhiteSpace(selected?.StatName) ? "No stat selected" : selected.StatName;
return Node(
"aetheria.statRecipes.summary",
"card",
new[] { ("title", "Stat Recipe") },
Metric("aetheria.statRecipes.summary.stat", "Selected Stat", statName),
Metric("aetheria.statRecipes.summary.base", "Base", Format(selected?.BaseValue ?? 0)),
Metric("aetheria.statRecipes.summary.preview", "Preview", Format(previewValue)),
Metric("aetheria.statRecipes.summary.influences", "Active Conditions", enabledInfluenceCount.ToString()),
Text(
"aetheria.statRecipes.summary.note",
"Each recipe owns only the conditions it actually cares about; runtime evaluation can sample those modifiers instead of walking every global performance axis."));
}
private static AetheriaRuntimeSurfaceComponent BuildRecipeList(
AetheriaRuntimeStatRecipeSurfaceState state,
AetheriaRuntimeStatRecipeState selected)
{
var children = state.Recipes
.Select((recipe, index) => Button(
$"aetheria.statRecipes.recipe.{index}.select",
string.Equals(recipe.StatName, selected?.StatName, StringComparison.Ordinal) ? $"{recipe.StatName} (Selected)" : recipe.StatName,
AetheriaRuntimeStatRecipeCommands.SelectStat,
("recipeKey", recipe.RecipeKey),
("statName", recipe.StatName)))
.Concat(new[]
{
Button(
"aetheria.statRecipes.recipe.add",
"Add Stat",
AetheriaRuntimeStatRecipeCommands.AddStat)
})
.ToArray();
return Node(
"aetheria.statRecipes.recipes",
"card",
new[] { ("title", "Recipes") },
Node("aetheria.statRecipes.recipes.list", "grid", Array.Empty<(string Key, string Value)>(), children));
}
private static AetheriaRuntimeSurfaceComponent BuildSelectedRecipeCard(AetheriaRuntimeStatRecipeState selected)
{
if (selected == null)
{
return Node(
"aetheria.statRecipes.selected",
"card",
new[] { ("title", "Selected Recipe") },
Text("aetheria.statRecipes.selected.empty", "Add or select a stat recipe to start authoring modifiers."));
}
return Node(
"aetheria.statRecipes.selected",
"card",
new[] { ("title", "Selected Recipe") },
TextInput(
"aetheria.statRecipes.selected.name",
"Stat Name",
selected.StatName,
AetheriaRuntimeStatRecipeCommands.SetStatName,
("recipeKey", selected.RecipeKey),
("statName", selected.StatName)),
NumberInput(
"aetheria.statRecipes.selected.base",
"Base Value",
selected.BaseValue,
AetheriaRuntimeStatRecipeCommands.SetBaseValue,
("recipeKey", selected.RecipeKey),
("statName", selected.StatName)),
ButtonRow(
"aetheria.statRecipes.selected.actions",
Button(
"aetheria.statRecipes.selected.remove",
"Remove Stat",
AetheriaRuntimeStatRecipeCommands.RemoveStat,
("recipeKey", selected.RecipeKey),
("statName", selected.StatName)),
Button(
"aetheria.statRecipes.selected.refresh",
"Refresh",
AetheriaRuntimeStatRecipeCommands.Refresh)));
}
private static AetheriaRuntimeSurfaceComponent BuildConditionPalette(AetheriaRuntimeStatRecipeState selected)
{
var children = AetheriaRuntimeStatRecipeConditions.All
.Select(condition =>
{
var influence = selected?.Influences.FirstOrDefault(candidate => string.Equals(candidate.Condition, condition, StringComparison.Ordinal));
var enabled = influence?.Enabled ?? false;
return Toggle(
$"aetheria.statRecipes.conditions.{condition}",
ConditionLabel(condition),
enabled,
AetheriaRuntimeStatRecipeCommands.ToggleCondition,
("recipeKey", selected?.RecipeKey ?? ""),
("statName", selected?.StatName ?? ""),
("condition", condition),
("enabled", enabled ? "false" : "true"));
})
.ToArray();
return Node(
"aetheria.statRecipes.conditions",
"card",
new[] { ("title", "Affected By") },
Node("aetheria.statRecipes.conditions.grid", "grid", Array.Empty<(string Key, string Value)>(), children));
}
private static AetheriaRuntimeSurfaceComponent BuildInfluenceList(
AetheriaRuntimeStatRecipeState selected,
AetheriaRuntimeStatRecipePreviewState preview)
{
if (selected == null || selected.Influences.Count == 0)
{
return Node(
"aetheria.statRecipes.influences",
"card",
new[] { ("title", "Modifiers") },
Text("aetheria.statRecipes.influences.empty", "Choose a condition above to create the first modifier for this stat."));
}
return Node(
"aetheria.statRecipes.influences",
"card",
new[] { ("title", "Modifiers") },
Node(
"aetheria.statRecipes.influences.grid",
"grid",
Array.Empty<(string Key, string Value)>(),
selected.Influences.Select(influence => BuildInfluenceCard(selected, influence, preview)).ToArray()));
}
private static AetheriaRuntimeSurfaceComponent BuildInfluenceCard(
AetheriaRuntimeStatRecipeState recipe,
AetheriaRuntimeStatInfluenceState influence,
AetheriaRuntimeStatRecipePreviewState preview)
{
var conditionValue = preview.GetConditionValue(influence.Condition);
var sample = ResolvePreviewSample(influence, conditionValue);
return Node(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}",
"card",
new[] { ("title", ConditionLabel(influence.Condition)) },
Metric(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}.operation",
"Operation",
OperationLabel(influence.Operation)),
NumberInput(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}.amount",
"Amount",
influence.Amount,
AetheriaRuntimeStatRecipeCommands.SetInfluenceAmount,
("recipeKey", recipe.RecipeKey),
("statName", recipe.StatName),
("condition", influence.Condition)),
TextInput(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}.curve",
"Curve",
influence.CurveLabel,
AetheriaRuntimeStatRecipeCommands.SetInfluenceCurve,
("recipeKey", recipe.RecipeKey),
("statName", recipe.StatName),
("condition", influence.Condition)),
Metric(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}.sample",
"Preview Sample",
Format(sample)),
ButtonRow(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}.actions",
Button(
$"aetheria.statRecipes.influence.{StableToken(recipe.RecipeKey)}.{influence.Condition}.operation.cycle",
"Cycle Operation",
AetheriaRuntimeStatRecipeCommands.CycleInfluenceOperation,
("recipeKey", recipe.RecipeKey),
("statName", recipe.StatName),
("condition", influence.Condition))));
}
private static AetheriaRuntimeSurfaceComponent BuildPreviewCard(AetheriaRuntimeStatRecipePreviewState preview)
{
return Node(
"aetheria.statRecipes.preview",
"card",
new[] { ("title", "Preview Context") },
Node(
"aetheria.statRecipes.preview.grid",
"grid",
Array.Empty<(string Key, string Value)>(),
AetheriaRuntimeStatRecipeConditions.All
.Select(condition => Slider(
$"aetheria.statRecipes.preview.{condition}",
ConditionLabel(condition),
preview.GetConditionValue(condition),
0,
1,
AetheriaRuntimeStatRecipeCommands.SetPreviewCondition,
("condition", condition)))
.ToArray()));
}
private static IReadOnlyList<AetheriaRuntimeSurfaceCommandTemplate> BuildCommandTemplates()
{
return new[]
{
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.Refresh, "Refresh", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.SelectStat, "Select Stat", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.AddStat, "Add Stat", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.RemoveStat, "Remove Stat", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.SetStatName, "Set Stat Name", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.SetBaseValue, "Set Base Value", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.ToggleCondition, "Toggle Condition", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.CycleInfluenceOperation, "Cycle Operation", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.SetInfluenceAmount, "Set Influence Amount", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.SetInfluenceCurve, "Set Influence Curve", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport),
new AetheriaRuntimeSurfaceCommandTemplate(AetheriaRuntimeStatRecipeCommands.SetPreviewCondition, "Set Preview Condition", AetheriaRuntimeSurfaceCommandTemplate.CultMeshTransport)
};
}
private static double EvaluatePreview(
AetheriaRuntimeStatRecipeState recipe,
AetheriaRuntimeStatRecipePreviewState preview)
{
var value = recipe.BaseValue;
foreach (var influence in recipe.Influences.Where(influence => influence.Enabled))
{
var sample = ResolvePreviewSample(influence, preview.GetConditionValue(influence.Condition));
switch (influence.Operation)
{
case AetheriaRuntimeStatRecipeOperations.Multiply:
value *= 1 + ((influence.Amount - 1) * sample);
break;
case AetheriaRuntimeStatRecipeOperations.Override:
value = Lerp(value, influence.Amount, sample);
break;
default:
value += influence.Amount * sample;
break;
}
}
return value;
}
private static double ResolvePreviewSample(
AetheriaRuntimeStatInfluenceState influence,
double conditionValue)
{
if (influence.PreviewSample > 0)
return Clamp01(influence.PreviewSample);
return Clamp01(conditionValue);
}
private static double Clamp01(double value)
{
if (value < 0)
return 0;
if (value > 1)
return 1;
return value;
}
private static double Lerp(double from, double to, double sample)
{
return from + ((to - from) * Clamp01(sample));
}
private static string Format(double value)
{
return value.ToString("0.###", CultureInfo.InvariantCulture);
}
private static string ConditionLabel(string condition)
{
switch (condition ?? "")
{
case AetheriaRuntimeStatRecipeConditions.Quality:
return "Quality";
case AetheriaRuntimeStatRecipeConditions.Durability:
return "Durability";
case AetheriaRuntimeStatRecipeConditions.Heat:
return "Heat";
case AetheriaRuntimeStatRecipeConditions.Charge:
return "Charge";
case AetheriaRuntimeStatRecipeConditions.Ammo:
return "Ammo";
case AetheriaRuntimeStatRecipeConditions.Range:
return "Range";
case AetheriaRuntimeStatRecipeConditions.Integrity:
return "Integrity";
case AetheriaRuntimeStatRecipeConditions.PilotSkill:
return "Pilot Skill";
case AetheriaRuntimeStatRecipeConditions.Environment:
return "Environment";
default:
return condition ?? "";
}
}
private static string OperationLabel(string operation)
{
switch (operation ?? "")
{
case AetheriaRuntimeStatRecipeOperations.Multiply:
return "Multiply";
case AetheriaRuntimeStatRecipeOperations.Override:
return "Override";
default:
return "Add";
}
}
private static string StableToken(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "none";
return new string(value
.Select(character => char.IsLetterOrDigit(character) ? character : '_')
.ToArray());
}
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 Button(
string id,
string label,
string command,
params (string Key, string Value)[] extraProps)
{
var props = new List<(string Key, string Value)>
{
("label", label ?? ""),
("command", command ?? "")
};
if (extraProps != null)
{
props.AddRange(extraProps);
}
return Node(id, "control.button", props);
}
private static AetheriaRuntimeSurfaceComponent Toggle(
string id,
string label,
bool value,
string command,
params (string Key, string Value)[] extraProps)
{
var props = new List<(string Key, string Value)>
{
("label", label ?? ""),
("value", value ? "true" : "false"),
("command", command ?? "")
};
if (extraProps != null)
{
props.AddRange(extraProps);
}
return Node(id, "control.toggle", props);
}
private static AetheriaRuntimeSurfaceComponent Slider(
string id,
string label,
double value,
double min,
double max,
string command,
params (string Key, string Value)[] extraProps)
{
var props = new List<(string Key, string Value)>
{
("label", label ?? ""),
("value", Format(value)),
("min", Format(min)),
("max", Format(max)),
("command", command ?? "")
};
if (extraProps != null)
{
props.AddRange(extraProps);
}
return Node(id, "control.slider", props);
}
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 TextInput(
string id,
string label,
string value,
string command,
params (string Key, string Value)[] extraProps)
{
var props = new List<(string Key, string Value)>
{
("label", label ?? ""),
("value", value ?? ""),
("command", command ?? "")
};
if (extraProps != null)
{
props.AddRange(extraProps);
}
return Node(id, "control.text", props);
}
private static IEnumerable<AetheriaRuntimeStatRecipeState> ProjectRows(
AetheriaRuntimeCatalogItem item)
{
foreach (var behavior in item.BehaviorPayloads ?? Array.Empty<AetheriaRuntimeBehaviorPayload>())
{
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;
yield return ProjectRow(item, behavior, field, fieldMetadata.Name);
}
}
}
private static AetheriaRuntimeStatRecipeState ProjectRow(
AetheriaRuntimeCatalogItem item,
AetheriaRuntimeBehaviorPayload behavior,
AetheriaRuntimeBehaviorField field,
string fieldName)
{
var value = field.Value;
var recipe = ReadRecipe(value);
var statName = string.Join(
" / ",
new[] { item.Name, behavior.Kind, fieldName }
.Where(part => !string.IsNullOrWhiteSpace(part)));
var recipeKey = $"{item.ItemKey}|{behavior.Kind}|{behavior.Group}|{field.Key}";
var baseValue = recipe?.BaseValue ?? ReadBaseValue(value);
return new AetheriaRuntimeStatRecipeState(
recipeKey,
statName,
baseValue,
(recipe?.Modifiers ?? Array.Empty<AetheriaRuntimeStatRecipeModifier>())
.Select(ProjectInfluence)
.ToArray());
}
private static double ReadBaseValue(AetheriaRuntimeBehaviorValue value)
{
if (value?.Children == null || value.Children.Count < 2)
return 0;
return value.Children[1].NumberValue;
}
private static AetheriaRuntimeStatRecipe? ReadRecipe(AetheriaRuntimeBehaviorValue? value)
{
if (value?.Children == null || value.Children.Count <= 5)
return null;
var recipeValue = value.Children[5];
if (recipeValue?.Children == null || recipeValue.Children.Count == 0)
return null;
var baseValue = recipeValue.Children[0].NumberValue;
var modifierValues = recipeValue.Children.Count > 1
? recipeValue.Children[1].Children ?? Array.Empty<AetheriaRuntimeBehaviorValue>()
: Array.Empty<AetheriaRuntimeBehaviorValue>();
var modifiers = modifierValues
.Select(ReadModifier)
.OfType<AetheriaRuntimeStatRecipeModifier>()
.ToArray();
return new AetheriaRuntimeStatRecipe(baseValue, modifiers);
}
private static AetheriaRuntimeStatRecipeModifier? ReadModifier(
AetheriaRuntimeBehaviorValue? value)
{
if (value?.Children == null)
return null;
return new AetheriaRuntimeStatRecipeModifier(
ReadChildString(value, 0),
ReadChildString(value, 1),
ReadChildNumber(value, 2),
ReadCurveKeys(ReadChildValue(value, 3)),
value.Children.Count <= 4 || ReadChildValue(value, 4)?.BoolValue != false);
}
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 != null && key.Children.Count >= 4)
.Select(key => new AetheriaRuntimeCurveKey(
ReadChildNumber(key, 0),
ReadChildNumber(key, 1),
ReadChildNumber(key, 2),
ReadChildNumber(key, 3)))
.ToArray();
}
private static AetheriaRuntimeStatInfluenceState ProjectInfluence(
AetheriaRuntimeStatRecipeModifier modifier)
{
return new AetheriaRuntimeStatInfluenceState(
modifier.Condition,
string.IsNullOrWhiteSpace(modifier.Operation) ? AetheriaRuntimeStatRecipeOperations.Add : modifier.Operation,
modifier.Amount,
CurveLabel(modifier.CurveKeys),
SampleCurve(modifier.CurveKeys, AetheriaRuntimeStatRecipePreviewState.Default.GetConditionValue(modifier.Condition)),
modifier.Enabled);
}
private static string CurvePresetLabel(IReadOnlyList<AetheriaRuntimeCurveKey> keys)
{
if (keys == null || keys.Count == 0)
return "";
var ordered = keys.OrderBy(key => key.Time).ToArray();
if (ordered.Length == 2 &&
Math.Abs(ordered[0].Time) < 0.0001 &&
Math.Abs(ordered[0].Value) < 0.0001 &&
Math.Abs(ordered[1].Time - 1) < 0.0001 &&
Math.Abs(ordered[1].Value - 1) < 0.0001)
return "linear";
return "";
}
private static string CurveLabel(IReadOnlyList<AetheriaRuntimeCurveKey> keys)
{
if (keys == null || keys.Count == 0)
return "linear";
var preset = CurvePresetLabel(keys);
return string.IsNullOrWhiteSpace(preset) ? $"{keys.Count} keys" : preset;
}
private static double SampleCurve(
IReadOnlyList<AetheriaRuntimeCurveKey> keys,
double value)
{
if (keys == null || keys.Count == 0)
return ClampCurveSample(value);
var ordered = keys.OrderBy(key => key.Time).ToArray();
if (value <= ordered[0].Time)
return ClampCurveSample(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 : ClampCurveSample((value - previous.Time) / span);
return ClampCurveSample(previous.Value + ((next.Value - previous.Value) * t));
}
return ClampCurveSample(ordered[ordered.Length - 1].Value);
}
private static double ClampCurveSample(double value)
{
if (value < 0)
return 0;
return value > 1 ? 1 : value;
}
private static string ReadChildString(AetheriaRuntimeBehaviorValue? value, int index)
{
return ReadChildValue(value, index)?.StringValue ?? "";
}
private static double ReadChildNumber(AetheriaRuntimeBehaviorValue? value, int index)
{
return ReadChildValue(value, index)?.NumberValue ?? 0;
}
private static AetheriaRuntimeBehaviorValue? ReadChildValue(
AetheriaRuntimeBehaviorValue? value,
int index)
{
return value?.Children != null && value.Children.Count > index ? value.Children[index] : null;
}
private static AetheriaRuntimeSurfaceComponent ButtonRow(
string id,
params AetheriaRuntimeSurfaceComponent[] children)
{
return Node(id, "row", Array.Empty<(string Key, string Value)>(), children);
}
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>());
}
}
}