namespace MrGameEng.AI;
/// Shape of a mapping a normalized input to a utility.
public enum CurveType
{
/// Straight line: y = slope·(x − xShift) + yShift.
Linear,
/// Power curve: y = slope·(x − xShift)^exponent + yShift; the exponent eases in/out.
Polynomial,
/// S-shaped logistic centred on xShift; exponent is the steepness.
Logistic,
/// Hermite smoothstep over [xShift, xShift + 1/slope]; flat ends, smooth middle.
SmoothStep,
}
///
/// Maps a normalized input in [0,1] to a utility in [0,1] through one of a few
/// shapes. The input is clamped before evaluation and the output is clamped after, so a curve is
/// always safe to feed a raw normalized value. Curves are
/// immutable value types — build them once and reuse them across evaluations.
///
public readonly struct ResponseCurve
{
/// The shape applied by .
public CurveType Type { get; }
/// Vertical scale / steepness (the m term). See per shape.
public float Slope { get; }
/// Power for and steepness for .
public float Exponent { get; }
/// Horizontal shift of the curve (the c term): the input value mapped to the origin.
public float XShift { get; }
/// Vertical shift of the curve (the b term) added after scaling.
public float YShift { get; }
///
/// Builds a curve from raw parameters. Prefer the named factories
/// (, , , )
/// which document the meaning of each term for their shape.
///
public ResponseCurve(
CurveType type,
float slope = 1f,
float exponent = 1f,
float xShift = 0f,
float yShift = 0f
)
{
Type = type;
Slope = slope;
Exponent = exponent;
XShift = xShift;
YShift = yShift;
}
/// The identity curve: y = x. The default when a consideration needs no shaping.
public static ResponseCurve Identity => new(CurveType.Linear);
/// Straight line y = slope·(x − xShift) + yShift. A negative slope inverts the input.
public static ResponseCurve Linear(float slope = 1f, float xShift = 0f, float yShift = 0f) =>
new(CurveType.Linear, slope, 1f, xShift, yShift);
///
/// Power curve y = slope·(x − xShift)^exponent + yShift. An exponent above 1 eases in
/// (slow start), below 1 eases out (fast start). Quadratic is exponent = 2.
///
public static ResponseCurve Polynomial(
float exponent,
float slope = 1f,
float xShift = 0f,
float yShift = 0f
) => new(CurveType.Polynomial, slope, exponent, xShift, yShift);
///
/// Logistic S-curve centred on ; controls how
/// sharp the transition is (≈10 gives a soft threshold, larger is more switch-like).
///
public static ResponseCurve Logistic(float steepness = 10f, float midpoint = 0.5f) =>
new(CurveType.Logistic, 1f, steepness, midpoint);
///
/// Hermite smoothstep rising from 0 to 1 over [xShift, xShift + 1/slope]: flat below the
/// start, flat above the end, smooth in between. Default rises across the whole [0,1] range.
///
public static ResponseCurve SmoothStep(float slope = 1f, float xShift = 0f) =>
new(CurveType.SmoothStep, slope, 1f, xShift);
/// Evaluates the curve. is clamped to [0,1]; the result is clamped to [0,1].
public float Evaluate(float x)
{
x = Math.Clamp(x, 0f, 1f);
var y = Type switch
{
CurveType.Linear => Slope * (x - XShift) + YShift,
CurveType.Polynomial => Slope * MathF.Pow(x - XShift, Exponent) + YShift,
CurveType.Logistic => 1f / (1f + MathF.Exp(-Exponent * (x - XShift))) * Slope + YShift,
CurveType.SmoothStep => SmoothStepValue(x),
_ => x,
};
return Math.Clamp(y, 0f, 1f);
}
private float SmoothStepValue(float x)
{
var t = Math.Clamp((x - XShift) * Slope, 0f, 1f);
return t * t * (3f - 2f * t) + YShift;
}
}