feat(core): add rain and clear weather state machine
This commit is contained in:
1 parent
7d38a1184e
commit
e295652051
2 files changed
+215
No files matched your search
@@ -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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user