496 lines
16 KiB
C#
496 lines
16 KiB
C#
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using GameCult.Aetheria.State.Verse;
|
|
|
|
public class PerformanceStat
|
|
{
|
|
public float Min;
|
|
public float Max;
|
|
public float HeatExponentMultiplier;
|
|
public float DurabilityExponentMultiplier;
|
|
public float QualityExponent;
|
|
public StatRecipe Recipe;
|
|
|
|
private Dictionary<Entity, Dictionary<Behavior, float>> _scaleModifiers;
|
|
private Dictionary<Entity, Dictionary<Behavior, float>> _constantModifiers;
|
|
|
|
private Dictionary<Entity, Dictionary<Behavior, float>> ScaleModifiers =>
|
|
_scaleModifiers ??= new Dictionary<Entity, Dictionary<Behavior, float>>();
|
|
|
|
private Dictionary<Entity, Dictionary<Behavior, float>> ConstantModifiers =>
|
|
_constantModifiers ??= new Dictionary<Entity, Dictionary<Behavior, float>>();
|
|
|
|
public Dictionary<Behavior, float> GetScaleModifiers(Entity entity)
|
|
{
|
|
if (!ScaleModifiers.ContainsKey(entity))
|
|
ScaleModifiers[entity] = new Dictionary<Behavior, float>();
|
|
|
|
return ScaleModifiers[entity];
|
|
}
|
|
|
|
public bool TryGetScaleModifiers(Entity entity, out Dictionary<Behavior, float> modifiers)
|
|
{
|
|
modifiers = null;
|
|
return entity != null &&
|
|
_scaleModifiers != null &&
|
|
_scaleModifiers.TryGetValue(entity, out modifiers) &&
|
|
modifiers.Count > 0;
|
|
}
|
|
|
|
public Dictionary<Behavior, float> GetConstantModifiers(Entity entity)
|
|
{
|
|
if (!ConstantModifiers.ContainsKey(entity))
|
|
ConstantModifiers[entity] = new Dictionary<Behavior, float>();
|
|
|
|
return ConstantModifiers[entity];
|
|
}
|
|
|
|
public bool TryGetConstantModifiers(Entity entity, out Dictionary<Behavior, float> modifiers)
|
|
{
|
|
modifiers = null;
|
|
return entity != null &&
|
|
_constantModifiers != null &&
|
|
_constantModifiers.TryGetValue(entity, out modifiers) &&
|
|
modifiers.Count > 0;
|
|
}
|
|
|
|
public float ApplyEntityModifiers(Entity entity, float baseValue)
|
|
{
|
|
var scaleModifier = 1.0f;
|
|
if (TryGetScaleModifiers(entity, out var scaleModifiers))
|
|
{
|
|
foreach (var modifier in scaleModifiers.Values)
|
|
scaleModifier *= modifier;
|
|
}
|
|
|
|
float constantModifier = 0;
|
|
if (TryGetConstantModifiers(entity, out var constantModifiers))
|
|
{
|
|
foreach (var modifier in constantModifiers.Values)
|
|
constantModifier += modifier;
|
|
}
|
|
|
|
return baseValue * scaleModifier + constantModifier;
|
|
}
|
|
|
|
public StatConditionMask GetDependencyMask(bool includeHeat)
|
|
{
|
|
if (Recipe != null)
|
|
return Recipe.DependencyMask;
|
|
|
|
var mask = StatConditionMask.None;
|
|
if (QualityExponent != 0)
|
|
mask |= StatConditionMask.Quality;
|
|
if (DurabilityExponentMultiplier != 0)
|
|
mask |= StatConditionMask.Durability;
|
|
if (includeHeat && HeatExponentMultiplier != 0)
|
|
mask |= StatConditionMask.Heat;
|
|
return mask;
|
|
}
|
|
|
|
public float EvaluateRecipeOrLegacy(
|
|
in StatEvaluationContext context,
|
|
float durabilityExponent,
|
|
float thermalExponent,
|
|
bool includeHeat)
|
|
{
|
|
if (Recipe != null)
|
|
return Recipe.Evaluate(context);
|
|
|
|
var heat = !includeHeat || HeatExponentMultiplier == 0
|
|
? 1
|
|
: CultMath.math.pow(context.Heat, thermalExponent * HeatExponentMultiplier);
|
|
var durability = DurabilityExponentMultiplier == 0
|
|
? 1
|
|
: CultMath.math.pow(context.Durability, durabilityExponent * DurabilityExponentMultiplier);
|
|
var quality = QualityExponent == 0
|
|
? 1
|
|
: CultMath.math.pow(context.Quality, QualityExponent);
|
|
|
|
return CultMath.math.lerp(Min, Max, durability * quality * heat);
|
|
}
|
|
|
|
public float EvaluateConsumableBaseline(in StatEvaluationContext context)
|
|
{
|
|
if (Recipe != null)
|
|
return Recipe.Evaluate(context);
|
|
|
|
return CultMath.math.lerp(Min, Max, context.Durability * CultMath.math.pow(context.Quality, QualityExponent));
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
public enum StatConditionMask
|
|
{
|
|
None = 0,
|
|
Quality = 1 << 0,
|
|
Durability = 1 << 1,
|
|
Heat = 1 << 2,
|
|
Charge = 1 << 3,
|
|
Ammo = 1 << 4,
|
|
Range = 1 << 5,
|
|
Integrity = 1 << 6,
|
|
PilotSkill = 1 << 7,
|
|
Environment = 1 << 8
|
|
}
|
|
|
|
public enum StatModifierOperation
|
|
{
|
|
Add,
|
|
Multiply,
|
|
Override
|
|
}
|
|
|
|
public static class StatRecipeTokens
|
|
{
|
|
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 const string Add = "add";
|
|
public const string Multiply = "multiply";
|
|
public const string Override = "override";
|
|
|
|
public static StatConditionMask ToConditionMask(string condition)
|
|
{
|
|
switch (condition ?? "")
|
|
{
|
|
case Quality:
|
|
return StatConditionMask.Quality;
|
|
case Durability:
|
|
return StatConditionMask.Durability;
|
|
case Heat:
|
|
return StatConditionMask.Heat;
|
|
case Charge:
|
|
return StatConditionMask.Charge;
|
|
case Ammo:
|
|
return StatConditionMask.Ammo;
|
|
case Range:
|
|
return StatConditionMask.Range;
|
|
case Integrity:
|
|
return StatConditionMask.Integrity;
|
|
case PilotSkill:
|
|
return StatConditionMask.PilotSkill;
|
|
case Environment:
|
|
return StatConditionMask.Environment;
|
|
default:
|
|
return StatConditionMask.None;
|
|
}
|
|
}
|
|
|
|
public static StatModifierOperation ToModifierOperation(string operation)
|
|
{
|
|
switch (operation ?? "")
|
|
{
|
|
case Multiply:
|
|
return StatModifierOperation.Multiply;
|
|
case Override:
|
|
return StatModifierOperation.Override;
|
|
default:
|
|
return StatModifierOperation.Add;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class AetheriaRuntimeBehaviorValueReader
|
|
{
|
|
public static PerformanceStat ReadPerformanceStat(AetheriaRuntimeBehaviorValue value)
|
|
{
|
|
return new PerformanceStat
|
|
{
|
|
Min = ChildFloat(value, 0),
|
|
Max = ChildFloat(value, 1),
|
|
HeatExponentMultiplier = ChildFloat(value, 2),
|
|
DurabilityExponentMultiplier = ChildFloat(value, 3),
|
|
QualityExponent = ChildFloat(value, 4),
|
|
Recipe = ReadStatRecipe(ChildValue(value, 5))
|
|
};
|
|
}
|
|
|
|
public static StatRecipe ReadStatRecipe(AetheriaRuntimeBehaviorValue value)
|
|
{
|
|
if (value == null || value.Children.Count == 0)
|
|
return null;
|
|
|
|
return new StatRecipe
|
|
{
|
|
BaseValue = ChildFloat(value, 0),
|
|
Modifiers = ChildValue(value, 1)?.Children
|
|
.Select(ReadStatRecipeModifier)
|
|
.Where(modifier => modifier != null)
|
|
.ToArray() ?? Array.Empty<StatRecipeModifier>()
|
|
};
|
|
}
|
|
|
|
public static StatRecipeModifier ReadStatRecipeModifier(AetheriaRuntimeBehaviorValue value)
|
|
{
|
|
if (value == null)
|
|
return null;
|
|
|
|
return new StatRecipeModifier
|
|
{
|
|
Condition = StatRecipeTokens.ToConditionMask(ChildString(value, 0)),
|
|
Operation = StatRecipeTokens.ToModifierOperation(ChildString(value, 1)),
|
|
Amount = ChildFloat(value, 2),
|
|
Curve = ReadBezierCurve(ChildValue(value, 3)),
|
|
Enabled = value.Children.Count <= 4 || ChildBool(value, 4, fallback: true)
|
|
};
|
|
}
|
|
|
|
public static BezierCurve ReadBezierCurve(AetheriaRuntimeBehaviorValue value)
|
|
{
|
|
if (value == null || value.Children.Count == 0)
|
|
return null;
|
|
|
|
return BezierCurve.FromKeys(
|
|
value.Children
|
|
.Where(key => key.Children.Count >= 4)
|
|
.Select(key => (
|
|
ChildFloat(key, 0),
|
|
ChildFloat(key, 1),
|
|
ChildFloat(key, 2),
|
|
ChildFloat(key, 3))));
|
|
}
|
|
|
|
public static StatReference ReadStatReference(AetheriaRuntimeBehaviorValue value)
|
|
{
|
|
return new StatReference
|
|
{
|
|
Target = ChildString(value, 1),
|
|
Stat = ChildString(value, 2)
|
|
};
|
|
}
|
|
|
|
public static T ReadEnum<T>(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;
|
|
}
|
|
|
|
private static float ChildFloat(AetheriaRuntimeBehaviorValue value, int index)
|
|
{
|
|
return value != null && value.Children.Count > index ? (float)value.Children[index].NumberValue : 0;
|
|
}
|
|
|
|
private static bool ChildBool(AetheriaRuntimeBehaviorValue value, int index, bool fallback = false)
|
|
{
|
|
return value != null && value.Children.Count > index ? value.Children[index].BoolValue : fallback;
|
|
}
|
|
|
|
private static string ChildString(AetheriaRuntimeBehaviorValue value, int index)
|
|
{
|
|
return value != null && value.Children.Count > index ? value.Children[index].StringValue ?? "" : "";
|
|
}
|
|
|
|
private static AetheriaRuntimeBehaviorValue ChildValue(AetheriaRuntimeBehaviorValue value, int index)
|
|
{
|
|
return value != null && value.Children.Count > index ? value.Children[index] : null;
|
|
}
|
|
}
|
|
|
|
public sealed class StatRecipe
|
|
{
|
|
public float BaseValue;
|
|
public StatRecipeModifier[] Modifiers = Array.Empty<StatRecipeModifier>();
|
|
|
|
private StatConditionMask? _dependencyMask;
|
|
private StatRecipeModifier[] _enabledModifiers;
|
|
|
|
public StatConditionMask DependencyMask
|
|
{
|
|
get
|
|
{
|
|
if (_dependencyMask.HasValue)
|
|
return _dependencyMask.Value;
|
|
|
|
var mask = StatConditionMask.None;
|
|
foreach (var modifier in EnabledModifiers)
|
|
mask |= modifier.Condition;
|
|
|
|
_dependencyMask = mask;
|
|
return mask;
|
|
}
|
|
}
|
|
|
|
private StatRecipeModifier[] EnabledModifiers
|
|
{
|
|
get
|
|
{
|
|
if (_enabledModifiers != null)
|
|
return _enabledModifiers;
|
|
|
|
var modifiers = Modifiers ?? Array.Empty<StatRecipeModifier>();
|
|
var enabledCount = 0;
|
|
for (var i = 0; i < modifiers.Length; i++)
|
|
{
|
|
if (modifiers[i] != null && modifiers[i].Enabled && modifiers[i].Condition != StatConditionMask.None)
|
|
enabledCount++;
|
|
}
|
|
|
|
if (enabledCount == 0)
|
|
{
|
|
_enabledModifiers = Array.Empty<StatRecipeModifier>();
|
|
return _enabledModifiers;
|
|
}
|
|
|
|
_enabledModifiers = new StatRecipeModifier[enabledCount];
|
|
var next = 0;
|
|
for (var i = 0; i < modifiers.Length; i++)
|
|
{
|
|
var modifier = modifiers[i];
|
|
if (modifier != null && modifier.Enabled && modifier.Condition != StatConditionMask.None)
|
|
_enabledModifiers[next++] = modifier;
|
|
}
|
|
|
|
return _enabledModifiers;
|
|
}
|
|
}
|
|
|
|
public void InvalidateCache()
|
|
{
|
|
_dependencyMask = null;
|
|
_enabledModifiers = null;
|
|
}
|
|
|
|
public float Evaluate(in StatEvaluationContext context)
|
|
{
|
|
var value = BaseValue;
|
|
foreach (var modifier in EnabledModifiers)
|
|
{
|
|
var sample = modifier.Sample(context.GetConditionValue(modifier.Condition));
|
|
switch (modifier.Operation)
|
|
{
|
|
case StatModifierOperation.Multiply:
|
|
value *= 1 + ((modifier.Amount - 1) * sample);
|
|
break;
|
|
case StatModifierOperation.Override:
|
|
value = CultMath.math.lerp(value, modifier.Amount, sample);
|
|
break;
|
|
default:
|
|
value += modifier.Amount * sample;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return value;
|
|
}
|
|
}
|
|
|
|
public sealed class StatRecipeModifier
|
|
{
|
|
public StatConditionMask Condition;
|
|
public StatModifierOperation Operation;
|
|
public float Amount;
|
|
public BezierCurve Curve;
|
|
public bool Enabled = true;
|
|
|
|
public float Sample(float conditionValue)
|
|
{
|
|
var value = CultMath.math.saturate(conditionValue);
|
|
return Curve == null ? value : CultMath.math.saturate(Curve.Evaluate(value));
|
|
}
|
|
}
|
|
|
|
public readonly struct StatEvaluationContext
|
|
{
|
|
public StatEvaluationContext(
|
|
float quality,
|
|
float durability,
|
|
float heat,
|
|
float charge = 0,
|
|
float ammo = 0,
|
|
float range = 0,
|
|
float integrity = 0,
|
|
float pilotSkill = 0,
|
|
float environment = 0)
|
|
{
|
|
Quality = quality;
|
|
Durability = durability;
|
|
Heat = heat;
|
|
Charge = charge;
|
|
Ammo = ammo;
|
|
Range = range;
|
|
Integrity = integrity;
|
|
PilotSkill = pilotSkill;
|
|
Environment = environment;
|
|
}
|
|
|
|
public float Quality { get; }
|
|
public float Durability { get; }
|
|
public float Heat { get; }
|
|
public float Charge { get; }
|
|
public float Ammo { get; }
|
|
public float Range { get; }
|
|
public float Integrity { get; }
|
|
public float PilotSkill { get; }
|
|
public float Environment { get; }
|
|
|
|
public float GetConditionValue(StatConditionMask condition)
|
|
{
|
|
switch (condition)
|
|
{
|
|
case StatConditionMask.Quality:
|
|
return Quality;
|
|
case StatConditionMask.Durability:
|
|
return Durability;
|
|
case StatConditionMask.Heat:
|
|
return Heat;
|
|
case StatConditionMask.Charge:
|
|
return Charge;
|
|
case StatConditionMask.Ammo:
|
|
return Ammo;
|
|
case StatConditionMask.Range:
|
|
return Range;
|
|
case StatConditionMask.Integrity:
|
|
return Integrity;
|
|
case StatConditionMask.PilotSkill:
|
|
return PilotSkill;
|
|
case StatConditionMask.Environment:
|
|
return Environment;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
public StatEvaluationContext WithCondition(StatConditionMask condition, float value)
|
|
{
|
|
switch (condition)
|
|
{
|
|
case StatConditionMask.Quality:
|
|
return new StatEvaluationContext(value, Durability, Heat, Charge, Ammo, Range, Integrity, PilotSkill, Environment);
|
|
case StatConditionMask.Durability:
|
|
return new StatEvaluationContext(Quality, value, Heat, Charge, Ammo, Range, Integrity, PilotSkill, Environment);
|
|
case StatConditionMask.Heat:
|
|
return new StatEvaluationContext(Quality, Durability, value, Charge, Ammo, Range, Integrity, PilotSkill, Environment);
|
|
case StatConditionMask.Charge:
|
|
return new StatEvaluationContext(Quality, Durability, Heat, value, Ammo, Range, Integrity, PilotSkill, Environment);
|
|
case StatConditionMask.Ammo:
|
|
return new StatEvaluationContext(Quality, Durability, Heat, Charge, value, Range, Integrity, PilotSkill, Environment);
|
|
case StatConditionMask.Range:
|
|
return new StatEvaluationContext(Quality, Durability, Heat, Charge, Ammo, value, Integrity, PilotSkill, Environment);
|
|
case StatConditionMask.Integrity:
|
|
return new StatEvaluationContext(Quality, Durability, Heat, Charge, Ammo, Range, value, PilotSkill, Environment);
|
|
case StatConditionMask.PilotSkill:
|
|
return new StatEvaluationContext(Quality, Durability, Heat, Charge, Ammo, Range, Integrity, value, Environment);
|
|
case StatConditionMask.Environment:
|
|
return new StatEvaluationContext(Quality, Durability, Heat, Charge, Ammo, Range, Integrity, PilotSkill, value);
|
|
default:
|
|
return this;
|
|
}
|
|
}
|
|
}
|