feat(core): add rain and clear weather state machine

This commit is contained in:
dualfroz committed 2026-08-23 16:34:27 +02:00
1 parent 7d38a1184e
commit e295652051
2 files changed
+215

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include <cstdint>
#include <vector>
#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<LightningStrike>& 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<LightningStrike> tickStrikes;
};
} // namespace vf
+116
View File
@@ -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<long long>(nextRandom() % static_cast<uint32_t>(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<uint32_t>(config.strikeInterval) != 0) return;
int spanX = config.strikeMaxX - config.strikeMinX;
int spanZ = config.strikeMaxZ - config.strikeMinZ;
int x = spanX > 0 ? config.strikeMinX + static_cast<int>(nextRandom() % static_cast<uint32_t>(spanX))
: config.strikeMinX;
int z = spanZ > 0 ? config.strikeMinZ + static_cast<int>(nextRandom() % static_cast<uint32_t>(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