From e295652051e53cab2ba477a3535f13faa27d5a87 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 16:34:27 +0200 Subject: [PATCH] feat(core): add rain and clear weather state machine --- core/include/vf/Weather.h | 99 ++++++++++++++++++++++++++++++++ core/src/Weather.cpp | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 core/include/vf/Weather.h create mode 100644 core/src/Weather.cpp diff --git a/core/include/vf/Weather.h b/core/include/vf/Weather.h new file mode 100644 index 0000000..23cb910 --- /dev/null +++ b/core/include/vf/Weather.h @@ -0,0 +1,99 @@ +#pragma once + +#include +#include +#include "Math.h" + +namespace vf { + +enum class WeatherState : uint8_t { + Clear = 0, + Rain = 1, + Thunder = 2, +}; + +enum class Precipitation : uint8_t { + None = 0, + Rain = 1, + Snow = 2, +}; + +const char* weatherStateName(WeatherState state); + +struct LightningStrike { + Vec3i pos; + long long tick; +}; + +struct WeatherConfig { + long long clearMin = 6000; + long long clearMax = 18000; + long long rainMin = 6000; + long long rainMax = 12000; + long long thunderMin = 3000; + long long thunderMax = 9000; + // Odds (in 256ths) that a rain spell escalates into a thunderstorm. + uint32_t thunderChance = 96; + // Mean ticks between lightning strikes while thundering. + long long strikeInterval = 120; + // Horizontal window strikes are placed in, and their nominal ground height. + int strikeMinX = -128; + int strikeMaxX = 128; + int strikeMinZ = -128; + int strikeMaxZ = 128; + int strikeGroundY = 64; + // Per-tick gradient ramp toward the target intensity. + double rainRampPerTick = 0.01; + double thunderRampPerTick = 0.01; + // Below this biome temperature falling precipitation is snow, not rain. + float snowTemperature = 0.15f; +}; + +// Deterministic weather state machine. Clear, Rain and Thunder alternate on +// seeded durations; a rain spell may escalate to thunder and always decays back +// through rain before clearing. Rain and thunder gradients ramp smoothly so the +// sky fades rather than snapping. While thundering, lightning strikes are placed +// at seeded positions. Two Weather instances with the same seed and config +// produce identical state, gradient and strike sequences. +class Weather { +public: + explicit Weather(uint32_t seed = 1u, const WeatherConfig& config = {}); + + void tick(); + void advance(long long ticks); + + WeatherState state() const { return currentState; } + bool isRaining() const { return currentState != WeatherState::Clear; } + bool isThundering() const { return currentState == WeatherState::Thunder; } + + double rainGradient() const { return rainLevel; } + double thunderGradient() const { return thunderLevel; } + long long ticksUntilChange() const { return stateTimer; } + long long currentTick() const { return tickCounter; } + + Precipitation precipitationAt(float temperature) const; + + const std::vector& strikesThisTick() const { return tickStrikes; } + long long totalStrikes() const { return strikeCount; } + + const WeatherConfig& settings() const { return config; } + +private: + uint32_t nextRandom(); + long long rollDuration(long long lo, long long hi); + void enterState(WeatherState next); + void stepGradients(); + void stepLightning(); + + WeatherConfig config; + uint32_t rngState; + WeatherState currentState = WeatherState::Clear; + long long stateTimer; + long long tickCounter = 0; + double rainLevel = 0.0; + double thunderLevel = 0.0; + long long strikeCount = 0; + std::vector tickStrikes; +}; + +} // namespace vf diff --git a/core/src/Weather.cpp b/core/src/Weather.cpp new file mode 100644 index 0000000..9d1b991 --- /dev/null +++ b/core/src/Weather.cpp @@ -0,0 +1,116 @@ +#include "vf/Weather.h" + +namespace vf { + +namespace { + +double approach(double value, double target, double step) { + if (value < target) { + value += step; + if (value > target) value = target; + } else if (value > target) { + value -= step; + if (value < target) value = target; + } + return value; +} + +} // namespace + +const char* weatherStateName(WeatherState state) { + switch (state) { + case WeatherState::Clear: return "clear"; + case WeatherState::Rain: return "rain"; + case WeatherState::Thunder: return "thunder"; + } + return "unknown"; +} + +Weather::Weather(uint32_t seed, const WeatherConfig& config) + : config(config), rngState(seed == 0 ? 0x9e3779b9u : seed) { + stateTimer = rollDuration(config.clearMin, config.clearMax); +} + +uint32_t Weather::nextRandom() { + rngState ^= rngState << 13; + rngState ^= rngState >> 17; + rngState ^= rngState << 5; + return rngState; +} + +long long Weather::rollDuration(long long lo, long long hi) { + if (hi <= lo) return lo; + long long span = hi - lo; + return lo + static_cast(nextRandom() % static_cast(span)); +} + +void Weather::enterState(WeatherState next) { + currentState = next; + switch (next) { + case WeatherState::Clear: + stateTimer = rollDuration(config.clearMin, config.clearMax); + break; + case WeatherState::Rain: + stateTimer = rollDuration(config.rainMin, config.rainMax); + break; + case WeatherState::Thunder: + stateTimer = rollDuration(config.thunderMin, config.thunderMax); + break; + } +} + +void Weather::stepGradients() { + double rainTarget = currentState == WeatherState::Clear ? 0.0 : 1.0; + double thunderTarget = currentState == WeatherState::Thunder ? 1.0 : 0.0; + rainLevel = approach(rainLevel, rainTarget, config.rainRampPerTick); + thunderLevel = approach(thunderLevel, thunderTarget, config.thunderRampPerTick); +} + +void Weather::stepLightning() { + if (currentState != WeatherState::Thunder) return; + if (config.strikeInterval <= 0) return; + if (nextRandom() % static_cast(config.strikeInterval) != 0) return; + + int spanX = config.strikeMaxX - config.strikeMinX; + int spanZ = config.strikeMaxZ - config.strikeMinZ; + int x = spanX > 0 ? config.strikeMinX + static_cast(nextRandom() % static_cast(spanX)) + : config.strikeMinX; + int z = spanZ > 0 ? config.strikeMinZ + static_cast(nextRandom() % static_cast(spanZ)) + : config.strikeMinZ; + tickStrikes.push_back({Vec3i{x, config.strikeGroundY, z}, tickCounter}); + ++strikeCount; +} + +void Weather::tick() { + tickStrikes.clear(); + --stateTimer; + if (stateTimer <= 0) { + switch (currentState) { + case WeatherState::Clear: + enterState(WeatherState::Rain); + break; + case WeatherState::Rain: + if (nextRandom() % 256u < config.thunderChance) enterState(WeatherState::Thunder); + else enterState(WeatherState::Clear); + break; + case WeatherState::Thunder: + enterState(WeatherState::Rain); + break; + } + } + stepGradients(); + stepLightning(); + ++tickCounter; +} + +void Weather::advance(long long ticks) { + for (long long i = 0; i < ticks; ++i) tick(); +} + +Precipitation Weather::precipitationAt(float temperature) const { + if (rainLevel <= 0.0 && currentState == WeatherState::Clear) return Precipitation::None; + if (currentState == WeatherState::Clear) return Precipitation::None; + return temperature < config.snowTemperature ? Precipitation::Snow : Precipitation::Rain; +} + +} // namespace vf