using AcDream.App.Rendering.Packs; using AcDream.Plugin.Abstractions.Rendering; namespace AcDream.App.Tests.Rendering.Packs; /// /// Campaign VM VM6: /// (the exact per-day-group mean/gust lookup) and /// (the /// smoothing step that keeps a day-group change from snapping). /// public sealed class RenderPackAtmospherePolicyEvaluationTests { [Fact] public void FoliageWindLooksUpExactDayGroupMatchOnly() { FoliageWindDayGroupPoint[] table = [ new(0, 0.25, 0.15), new(1, 0.45, 0.30), new(2, 0.60, 0.35), new(3, 0.85, 0.60), ]; (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(table, 3); Assert.Equal(0.85f, mean); Assert.Equal(0.60f, gust); } [Fact] public void FoliageWindReturnsZeroForADayGroupAbsentFromTheTable() { FoliageWindDayGroupPoint[] table = [new(0, 0.25, 0.15)]; (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(table, 99); Assert.Equal(0f, mean); Assert.Equal(0f, gust); } [Fact] public void FoliageWindReturnsZeroForANullTable() { (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind(null, 0); Assert.Equal(0f, mean); Assert.Equal(0f, gust); } [Fact] public void FoliageWindDoesNotInterpolateBetweenDayGroupIndices() { // Day-group ids are not ordered by "how windy" — index 1 sitting // between 0 and 2 in the table must not produce a value between // their mean/gust. This asserts the lookup is an exact match, not // continuous interpolation across the index axis. FoliageWindDayGroupPoint[] table = [new(0, 0.0, 0.0), new(5, 1.0, 1.0)]; (float meanAtUnlistedMidpoint, float gustAtUnlistedMidpoint) = RenderPackAtmospherePolicyEvaluation.FoliageWind(table, 2); Assert.Equal(0f, meanAtUnlistedMidpoint); Assert.Equal(0f, gustAtUnlistedMidpoint); } [Fact] public void EaseTowardTargetDoesNotMoveWithZeroDelta() { float result = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget( current: 0.25f, target: 0.85f, deltaSeconds: 0f, transitionSeconds: 10f); Assert.Equal(0.25f, result); } [Fact] public void EaseTowardTargetConvergesWithoutOvershootOrDiscontinuity() { // Sample the same weather change (Clear 0.25 -> Rainy 0.85) many // times over the transition window at a fixed per-tick delta and // assert the sequence is monotonically non-decreasing and never // jumps by more than one tick's proportional share — "no // discontinuity across the delta window." const float start = 0.25f; const float target = 0.85f; const float transitionSeconds = 10f; const float tickSeconds = 0.1f; float current = start; float previous = current; // A fixed-rate-per-tick ease is exponential decay toward the // target, not a linear ramp: after N ticks the remaining gap is // (1 - tickSeconds/transitionSeconds)^N of the original gap. 4000 // ticks (400 s of simulated time, 40x the transition window) leaves // a remaining fraction of (0.99)^4000 ~= 4e-18 — comfortably // converged without asserting a false linear-ramp expectation. const int ticks = 4000; for (int i = 0; i < ticks; i++) { current = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget( current, target, tickSeconds, transitionSeconds); Assert.True( current >= previous - 1e-6f, $"tick {i}: value regressed from {previous} to {current}"); Assert.True( current <= target + 1e-6f, $"tick {i}: value {current} overshot target {target}"); previous = current; } Assert.True(MathF.Abs(current - target) < 0.01f, $"did not converge: {current}"); } [Fact] public void EaseTowardTargetSnapsInOneStepWhenTransitionSecondsIsZeroOrLess() { float result = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget( current: 0f, target: 1f, deltaSeconds: 0.001f, transitionSeconds: 0f); Assert.Equal(1f, result); } [Fact] public void EaseTowardTargetClampsAnOversizedDeltaInsteadOfOvershooting() { // A delta larger than the transition window (e.g. after a long // pause) must not overshoot past the target. float result = RenderPackAtmospherePolicyEvaluation.EaseTowardTarget( current: 0f, target: 1f, deltaSeconds: 1000f, transitionSeconds: 10f); Assert.Equal(1f, result); } }