diff --git a/src/AcDream.Core/World/WeatherState.cs b/src/AcDream.Core/World/WeatherState.cs
index e4c54f1..51219fd 100644
--- a/src/AcDream.Core/World/WeatherState.cs
+++ b/src/AcDream.Core/World/WeatherState.cs
@@ -96,47 +96,42 @@ public readonly record struct AtmosphereSnapshot(
///
public sealed class WeatherSystem
{
+ ///
+ /// Kept as a public constant because a handful of callers / tests
+ /// reference it, but unused internally post-Phase-7: retail does
+ /// not cross-fade between s (no such
+ /// concept in the decompile). The SkyTimeOfDay keyframe interp
+ /// does all time-based fog/light blending directly.
+ ///
public const float TransitionSeconds = 10f;
- // Flash visual parameters (r12 §9). The spike rises to 1.0 in ~50ms
- // and decays exponentially with a time constant of ~200ms.
- private const float FlashDecay = 1f / 0.200f; // 1 / τ seconds
+ // Flash decay kept so TriggerFlash() is still a usable test hook;
+ // production code (PlayScript-driven lightning, Phase 6) does NOT
+ // drive the flash uniform — it spawns particle emitters directly.
+ private const float FlashDecay = 1f / 0.200f; // 1 / τ sec
private const float FlashPeakHoldS = 0.05f;
- // Retail storm cadence: 8–30 seconds between strikes.
- private const float StrikeIntervalMinS = 8f;
- private const float StrikeIntervalMaxS = 30f;
-
- // Overcast-kind fog feels like ~40–150m retail range (r12 §5.1).
- private const float OvercastFogStart = 40f;
- private const float OvercastFogEnd = 150f;
- private const float StormFogStart = 25f;
- private const float StormFogEnd = 90f;
-
- private WeatherKind _kind = WeatherKind.Clear;
+ private WeatherKind _kind = WeatherKind.Clear;
private WeatherKind _previousKind = WeatherKind.Clear;
- private float _transitionT; // 0..1 through the cross-fade
private float _flashLevel;
- private float _flashAge; // seconds since last strike
- private float _nextStrikeInS;
+ private float _flashAge;
private EnvironOverride _override;
- private int _rolledDayIndex = int.MinValue; // unrolled == "pick one"
+ private int _rolledDayIndex = int.MinValue;
- // Phase 3e — when GameWindow (via RefreshSkyForCurrentDay) pushes
- // the active retail DayGroup name through SetKindFromDayGroupName,
- // the internal RollKind hash becomes unused. This flag stops Tick's
- // auto-roll so external control can't fight the internal one.
+ // Phase 3e — when GameWindow pushes the retail DayGroup name via
+ // SetKindFromDayGroupName, the internal RollKind hash is disabled.
private bool _externallyDriven;
- private readonly Random _strikeJitter;
-
public WeatherSystem(Random? rng = null)
{
- _strikeJitter = rng ?? new Random(unchecked((int)0xDCAE_5001u));
- _nextStrikeInS = 12f;
+ // The random-seed ctor argument remains for test API compat,
+ // but no longer drives any production behaviour (Phase 7: the
+ // Storm-kind random lightning timer was deleted — retail is
+ // server-driven via PlayScript; see Agents #3 and #5).
+ _ = rng;
}
/// Current active weather.
@@ -232,15 +227,19 @@ public sealed class WeatherSystem
///
public void Tick(double nowSeconds, int dayIndex, float dtSeconds)
{
- // Cross-fade progression: transitionT advances toward 1 over
- // TransitionSeconds. Capped; no further rollover.
- if (_transitionT < 1f)
- _transitionT = Math.Min(1f, _transitionT + dtSeconds / TransitionSeconds);
+ // Phase 7 — dropped:
+ // - per-Kind cross-fade (_transitionT drove the now-removed
+ // FogForKind lerp; retail has no such machinery).
+ // - Storm-kind random lightning timer (retail lightning is
+ // server-driven via PlayScript per Agent #5 — purely visual
+ // through the particle system, no UBO flash channel).
+ //
+ // What remains: day-index auto-roll as a TEST-ONLY fallback
+ // (externally driven callers set _externallyDriven=true through
+ // SetKindFromDayGroupName and this block never fires), plus
+ // flash-level decay so the TriggerFlash() test hook still works.
- // Day changed → re-roll. Skip the sentinel (forced). Also skip
- // when weather is externally driven by the retail DayGroup name
- // (Phase 3e) — the internal RollKind is a fallback only for
- // tests / offline code paths.
+ // Day changed → re-roll (fallback only — disabled when externally driven).
if (!_externallyDriven
&& dayIndex != _rolledDayIndex
&& _rolledDayIndex != int.MaxValue)
@@ -250,19 +249,9 @@ public sealed class WeatherSystem
if (newKind != _kind) BeginTransition(newKind);
}
- // Lightning timer only ticks in Storm kind.
- if (_kind == WeatherKind.Storm && _override == EnvironOverride.None)
- {
- _nextStrikeInS -= dtSeconds;
- if (_nextStrikeInS <= 0f)
- {
- TriggerFlash();
- _nextStrikeInS = StrikeIntervalMinS
- + (float)_strikeJitter.NextDouble() * (StrikeIntervalMaxS - StrikeIntervalMinS);
- }
- }
-
- // Decay the flash level with a 200ms time constant.
+ // Flash decay — 50ms hold then exponential decay (~200ms τ).
+ // Production never TriggerFlashes; this exists for tests that
+ // exercise the UBO channel.
if (_flashLevel > 0f)
{
_flashAge += dtSeconds;
@@ -284,40 +273,45 @@ public sealed class WeatherSystem
}
///
- /// Produce the per-frame snapshot consumed by the shader UBO +
- /// particle emitter spawners. Combines the sky keyframe's fog with
- /// the weather state's fog overlay, then applies the server
- /// tint if any.
+ /// Produce the per-frame atmosphere snapshot from the sky keyframe.
+ ///
+ ///
+ /// Retail-faithful since Phase 7 (2026-04-23): fog is the
+ /// keyframe's fog, passed through directly (color + distances).
+ /// The only override channel is set
+ /// by the server's AdminEnvirons packet (opcode 0xEA60) —
+ /// in that case we substitute the fog COLOR with the preset tint
+ /// and keep the keyframe's distances untouched. There is no
+ /// per- fog manipulation: retail's
+ /// decompile (Agent #3, 2026-04-23) contains no such logic. The
+ /// enum is now purely informational — it
+ /// labels the current sky style for debug overlays but doesn't
+ /// drive any rendering.
+ ///
///
public AtmosphereSnapshot Snapshot(in SkyKeyframe kf)
{
- // Cross-fade fog distance + color from previous-kind to new-kind.
- var prev = FogForKind(_previousKind, kf);
- var curr = FogForKind(_kind, kf);
+ // Fog passthrough from the keyframe (retail semantics).
+ Vector3 fogColor = kf.FogColor;
+ float fogStart = kf.FogStart;
+ float fogEnd = kf.FogEnd;
- float t = _transitionT;
- var fogColor = Vector3.Lerp(prev.color, curr.color, t);
- float fogStart = prev.start + (curr.start - prev.start) * t;
- float fogEnd = prev.end + (curr.end - prev.end) * t;
-
- // Server environ override wins.
+ // AdminEnvirons server override: replace fog COLOR only.
+ // Keyframe distances unchanged until we find evidence retail
+ // changes those too (Agent #3 notes the in-game crossfade
+ // lerps distances via SkyTimeOfDay keyframe interp, NOT via
+ // AdminEnvirons directly).
if (_override != EnvironOverride.None)
- {
fogColor = EnvironOverrideColor(_override);
- fogStart = 15f;
- fogEnd = 80f; // Dense override fog
- }
-
- float intensity = _kind == WeatherKind.Clear ? 1f - t : t;
return new AtmosphereSnapshot(
- Kind: _kind,
- Intensity: Math.Clamp(intensity, 0f, 1f),
+ Kind: _kind, // informational
+ Intensity: 1f, // no per-Kind easing in retail
FogColor: fogColor,
FogStart: fogStart,
FogEnd: fogEnd,
FogMode: kf.FogMode,
- LightningFlash: _flashLevel,
+ LightningFlash: _flashLevel, // 0 in production; TriggerFlash hook for tests
Override: _override);
}
@@ -329,7 +323,6 @@ public sealed class WeatherSystem
{
_previousKind = _kind;
_kind = newKind;
- _transitionT = 0f;
}
///
@@ -354,23 +347,6 @@ public sealed class WeatherSystem
return WeatherKind.Storm;
}
- private static (Vector3 color, float start, float end) FogForKind(WeatherKind kind, in SkyKeyframe kf)
- {
- return kind switch
- {
- WeatherKind.Clear => (kf.FogColor, kf.FogStart, kf.FogEnd),
- WeatherKind.Overcast => (Vector3.Lerp(kf.FogColor, new Vector3(0.55f, 0.55f, 0.55f), 0.6f),
- OvercastFogStart, OvercastFogEnd),
- WeatherKind.Rain => (Vector3.Lerp(kf.FogColor, new Vector3(0.45f, 0.48f, 0.55f), 0.7f),
- OvercastFogStart, OvercastFogEnd),
- WeatherKind.Snow => (Vector3.Lerp(kf.FogColor, new Vector3(0.80f, 0.82f, 0.90f), 0.6f),
- OvercastFogStart, OvercastFogEnd * 1.2f),
- WeatherKind.Storm => (Vector3.Lerp(kf.FogColor, new Vector3(0.25f, 0.25f, 0.30f), 0.8f),
- StormFogStart, StormFogEnd),
- _ => (kf.FogColor, kf.FogStart, kf.FogEnd),
- };
- }
-
private static Vector3 EnvironOverrideColor(EnvironOverride o) => o switch
{
EnvironOverride.RedFog => new Vector3(0.60f, 0.05f, 0.05f),
diff --git a/tests/AcDream.Core.Tests/World/WeatherSystemTests.cs b/tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
index f13a308..20d490b 100644
--- a/tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
+++ b/tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
@@ -38,19 +38,30 @@ public sealed class WeatherSystemTests
}
[Fact]
- public void Transition_EasesAcrossTenSeconds()
+ public void Snapshot_AlwaysPassesKeyframeFog_RegardlessOfKind()
{
- // Force Storm, then Clear, sample snapshot fog distance mid-transition.
- var sys = new WeatherSystem();
- sys.ForceWeather(WeatherKind.Storm);
- sys.Tick(0, 1, 100f); // finalize
-
+ // Phase 7: retail DOES NOT override fog by WeatherKind — Storm
+ // doesn't produce denser fog, Overcast doesn't shrink distance.
+ // Every Kind renders the keyframe's fog directly. This test
+ // replaces the old "Transition_EasesAcrossTenSeconds" which
+ // codified the invented per-Kind fog behaviour.
var kf = SkyStateProvider.Default().Interpolate(0.5f);
- var stormFog = sys.Snapshot(in kf);
- Assert.Equal(WeatherKind.Storm, stormFog.Kind);
- // Snapshot should have a small fog end (storm fog is dense).
- Assert.True(stormFog.FogEnd < 120f, $"storm fog end too large: {stormFog.FogEnd}");
+ foreach (var kind in new[] {
+ WeatherKind.Clear, WeatherKind.Overcast,
+ WeatherKind.Rain, WeatherKind.Snow, WeatherKind.Storm,
+ })
+ {
+ var sys = new WeatherSystem();
+ sys.ForceWeather(kind);
+ sys.Tick(0, 1, 100f); // finalize any transition
+ var snap = sys.Snapshot(in kf);
+
+ Assert.Equal(kind, snap.Kind);
+ Assert.Equal(kf.FogStart, snap.FogStart, precision: 2);
+ Assert.Equal(kf.FogEnd, snap.FogEnd, precision: 2);
+ Assert.Equal(kf.FogColor, snap.FogColor);
+ }
}
[Fact]