using AcDream.App.Rendering.Packs; using AcDream.Core.World; using AcDream.Plugin.Abstractions.Rendering; namespace AcDream.App.Tests.Rendering.Packs; /// /// Campaign VM VM6: /// (the exact per-weather-kind mean/gust lookup, keyed by the DAT-classified /// — corrected in the fix round from the raw /// activeDayGroup index, which carries no weather meaning by itself) /// and /// (the smoothing step that keeps a weather change from snapping). /// public sealed class RenderPackAtmospherePolicyEvaluationTests { private static readonly FoliageWindWeatherPoint[] FiveKindTable = [ new("Clear", 0.25, 0.15), new("Overcast", 0.60, 0.35), new("Rain", 0.85, 0.60), new("Snow", 0.35, 0.20), new("Storm", 1.00, 0.75), ]; [Theory] [InlineData(WeatherKind.Clear, 0.25f, 0.15f)] [InlineData(WeatherKind.Overcast, 0.60f, 0.35f)] [InlineData(WeatherKind.Rain, 0.85f, 0.60f)] [InlineData(WeatherKind.Snow, 0.35f, 0.20f)] [InlineData(WeatherKind.Storm, 1.00f, 0.75f)] public void FoliageWindResolvesEachOfTheFiveDeclaredWeatherKinds( WeatherKind kind, float expectedMean, float expectedGust) { (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind( FiveKindTable, kind); Assert.Equal(expectedMean, mean); Assert.Equal(expectedGust, gust); } [Fact] public void FoliageWindFallsBackToTheClearRowForAnUnlistedKind() { // A table that only declares Clear and Storm — a kind absent from // the table (Overcast here) falls back to the declared Clear row, // not to (0, 0), since an unresolved weather kind is closer to "no // weather data" than to "assume it's calm." FoliageWindWeatherPoint[] table = [ new("Clear", 0.25, 0.15), new("Storm", 1.00, 0.75), ]; (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind( table, WeatherKind.Overcast); Assert.Equal(0.25f, mean); Assert.Equal(0.15f, gust); } [Fact] public void FoliageWindReturnsZeroWhenNeitherTheKindNorClearIsDeclared() { FoliageWindWeatherPoint[] table = [new("Storm", 1.00, 0.75)]; (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind( table, WeatherKind.Overcast); Assert.Equal(0f, mean); Assert.Equal(0f, gust); } [Fact] public void FoliageWindReturnsZeroForANullTable() { (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind( null, WeatherKind.Clear); Assert.Equal(0f, mean); Assert.Equal(0f, gust); } [Fact] public void FoliageWindMatchesByExactNameNotSubstringOrCase() { // "Rain" must not match "Rainy" or any case variant — the lookup is // an ordinal exact match against the WeatherKind member name. FoliageWindWeatherPoint[] table = [ new("Rainy", 0.99, 0.99), new("rain", 0.99, 0.99), new("Clear", 0.10, 0.05), ]; (float mean, float gust) = RenderPackAtmospherePolicyEvaluation.FoliageWind( table, WeatherKind.Rain); // Neither "Rainy" nor "rain" matches WeatherKind.Rain.ToString() // ("Rain", exact case) — falls back to the declared Clear row. Assert.Equal(0.10f, mean); Assert.Equal(0.05f, gust); } [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); } }