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; } }