feat(core): add day/night tick clock
This commit is contained in:
1 parent
3b1bbbc02c
commit
125ee956b2
2 files changed
+232
No files matched your search
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace vf {
|
||||
|
||||
using WorldTick = long long;
|
||||
|
||||
enum class DayPhase : uint8_t {
|
||||
Dawn = 0,
|
||||
Day = 1,
|
||||
Dusk = 2,
|
||||
Night = 3,
|
||||
};
|
||||
|
||||
const char* dayPhaseName(DayPhase phase);
|
||||
|
||||
struct TimeOfDayConfig {
|
||||
WorldTick ticksPerDay = 24000;
|
||||
double dawnStart = 0.20;
|
||||
double dawnEnd = 0.30;
|
||||
double duskStart = 0.70;
|
||||
double duskEnd = 0.80;
|
||||
double sunriseFraction = 0.25;
|
||||
double sunsetFraction = 0.75;
|
||||
int nightSkyLight = 4;
|
||||
int daySkyLight = 15;
|
||||
int moonPhaseCount = 8;
|
||||
};
|
||||
|
||||
// Deterministic world clock. Maps an absolute tick counter onto a day cycle:
|
||||
// within-day fraction, four named phases, a sky-light curve peaking at noon and
|
||||
// bottoming at midnight, a moon phase advancing once per day, and sunrise/sunset
|
||||
// crossing detection for the interval covered by the most recent advance.
|
||||
class TimeOfDay {
|
||||
public:
|
||||
explicit TimeOfDay(WorldTick startTick = 0, const TimeOfDayConfig& config = {});
|
||||
|
||||
void advance(WorldTick ticks = 1);
|
||||
void setTick(WorldTick absoluteTick);
|
||||
void reset(WorldTick absoluteTick = 0);
|
||||
|
||||
WorldTick absoluteTick() const { return tickCounter; }
|
||||
WorldTick timeOfDayTicks() const;
|
||||
double dayFraction() const;
|
||||
WorldTick dayCount() const;
|
||||
|
||||
DayPhase phase() const;
|
||||
bool isDaylight() const;
|
||||
bool isNight() const;
|
||||
|
||||
int skyLightLevel() const;
|
||||
double skyBrightness() const;
|
||||
double sunHeight() const;
|
||||
|
||||
int moonPhase() const;
|
||||
double moonBrightness() const;
|
||||
|
||||
bool crossedSunrise() const { return sunriseCrossed; }
|
||||
bool crossedSunset() const { return sunsetCrossed; }
|
||||
|
||||
const TimeOfDayConfig& settings() const { return config; }
|
||||
|
||||
private:
|
||||
void recomputeCrossings(WorldTick previous, WorldTick current);
|
||||
|
||||
TimeOfDayConfig config;
|
||||
WorldTick tickCounter;
|
||||
bool sunriseCrossed = false;
|
||||
bool sunsetCrossed = false;
|
||||
};
|
||||
|
||||
} // namespace vf
|
||||
@@ -0,0 +1,159 @@
|
||||
#include "vf/TimeOfDay.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace vf {
|
||||
|
||||
namespace {
|
||||
|
||||
WorldTick floorDivTick(WorldTick a, WorldTick b) {
|
||||
WorldTick q = a / b;
|
||||
WorldTick r = a % b;
|
||||
if (r != 0 && ((r < 0) != (b < 0))) --q;
|
||||
return q;
|
||||
}
|
||||
|
||||
WorldTick floorModTick(WorldTick a, WorldTick b) {
|
||||
WorldTick r = a % b;
|
||||
if (r != 0 && ((r < 0) != (b < 0))) r += b;
|
||||
return r;
|
||||
}
|
||||
|
||||
double clamp01(double v) {
|
||||
if (v < 0.0) return 0.0;
|
||||
if (v > 1.0) return 1.0;
|
||||
return v;
|
||||
}
|
||||
|
||||
WorldTick countBoundaryCrossings(WorldTick previous, WorldTick current,
|
||||
WorldTick boundary, WorldTick period) {
|
||||
WorldTick after = floorDivTick(current - boundary, period);
|
||||
WorldTick before = floorDivTick(previous - boundary, period);
|
||||
return after - before;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* dayPhaseName(DayPhase phase) {
|
||||
switch (phase) {
|
||||
case DayPhase::Dawn: return "dawn";
|
||||
case DayPhase::Day: return "day";
|
||||
case DayPhase::Dusk: return "dusk";
|
||||
case DayPhase::Night: return "night";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
TimeOfDay::TimeOfDay(WorldTick startTick, const TimeOfDayConfig& config)
|
||||
: config(config), tickCounter(startTick) {}
|
||||
|
||||
void TimeOfDay::advance(WorldTick ticks) {
|
||||
if (ticks <= 0) {
|
||||
sunriseCrossed = false;
|
||||
sunsetCrossed = false;
|
||||
return;
|
||||
}
|
||||
WorldTick previous = tickCounter;
|
||||
tickCounter += ticks;
|
||||
recomputeCrossings(previous, tickCounter);
|
||||
}
|
||||
|
||||
void TimeOfDay::setTick(WorldTick absoluteTick) {
|
||||
WorldTick previous = tickCounter;
|
||||
tickCounter = absoluteTick;
|
||||
if (absoluteTick > previous) recomputeCrossings(previous, tickCounter);
|
||||
else {
|
||||
sunriseCrossed = false;
|
||||
sunsetCrossed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TimeOfDay::reset(WorldTick absoluteTick) {
|
||||
tickCounter = absoluteTick;
|
||||
sunriseCrossed = false;
|
||||
sunsetCrossed = false;
|
||||
}
|
||||
|
||||
void TimeOfDay::recomputeCrossings(WorldTick previous, WorldTick current) {
|
||||
WorldTick period = config.ticksPerDay;
|
||||
WorldTick sunriseTick = static_cast<WorldTick>(std::llround(config.sunriseFraction * period));
|
||||
WorldTick sunsetTick = static_cast<WorldTick>(std::llround(config.sunsetFraction * period));
|
||||
sunriseCrossed = countBoundaryCrossings(previous, current, sunriseTick, period) > 0;
|
||||
sunsetCrossed = countBoundaryCrossings(previous, current, sunsetTick, period) > 0;
|
||||
}
|
||||
|
||||
WorldTick TimeOfDay::timeOfDayTicks() const {
|
||||
return floorModTick(tickCounter, config.ticksPerDay);
|
||||
}
|
||||
|
||||
double TimeOfDay::dayFraction() const {
|
||||
return static_cast<double>(timeOfDayTicks()) / static_cast<double>(config.ticksPerDay);
|
||||
}
|
||||
|
||||
WorldTick TimeOfDay::dayCount() const {
|
||||
return floorDivTick(tickCounter, config.ticksPerDay);
|
||||
}
|
||||
|
||||
DayPhase TimeOfDay::phase() const {
|
||||
double f = dayFraction();
|
||||
if (f >= config.dawnStart && f < config.dawnEnd) return DayPhase::Dawn;
|
||||
if (f >= config.dawnEnd && f < config.duskStart) return DayPhase::Day;
|
||||
if (f >= config.duskStart && f < config.duskEnd) return DayPhase::Dusk;
|
||||
return DayPhase::Night;
|
||||
}
|
||||
|
||||
bool TimeOfDay::isDaylight() const {
|
||||
DayPhase p = phase();
|
||||
return p == DayPhase::Day || p == DayPhase::Dawn || p == DayPhase::Dusk;
|
||||
}
|
||||
|
||||
bool TimeOfDay::isNight() const { return phase() == DayPhase::Night; }
|
||||
|
||||
double TimeOfDay::skyBrightness() const {
|
||||
double f = dayFraction();
|
||||
double nightLevel = static_cast<double>(config.nightSkyLight) /
|
||||
static_cast<double>(config.daySkyLight);
|
||||
switch (phase()) {
|
||||
case DayPhase::Day:
|
||||
return 1.0;
|
||||
case DayPhase::Night:
|
||||
return nightLevel;
|
||||
case DayPhase::Dawn: {
|
||||
double t = (f - config.dawnStart) / (config.dawnEnd - config.dawnStart);
|
||||
return nightLevel + clamp01(t) * (1.0 - nightLevel);
|
||||
}
|
||||
case DayPhase::Dusk: {
|
||||
double t = (f - config.duskStart) / (config.duskEnd - config.duskStart);
|
||||
return 1.0 - clamp01(t) * (1.0 - nightLevel);
|
||||
}
|
||||
}
|
||||
return nightLevel;
|
||||
}
|
||||
|
||||
int TimeOfDay::skyLightLevel() const {
|
||||
double level = skyBrightness() * static_cast<double>(config.daySkyLight);
|
||||
int rounded = static_cast<int>(std::lround(level));
|
||||
if (rounded < 0) rounded = 0;
|
||||
if (rounded > config.daySkyLight) rounded = config.daySkyLight;
|
||||
return rounded;
|
||||
}
|
||||
|
||||
double TimeOfDay::sunHeight() const {
|
||||
double f = dayFraction();
|
||||
return std::sin((f - config.sunriseFraction) * 2.0 * M_PI);
|
||||
}
|
||||
|
||||
int TimeOfDay::moonPhase() const {
|
||||
WorldTick day = dayCount();
|
||||
WorldTick m = day % config.moonPhaseCount;
|
||||
if (m < 0) m += config.moonPhaseCount;
|
||||
return static_cast<int>(m);
|
||||
}
|
||||
|
||||
double TimeOfDay::moonBrightness() const {
|
||||
int half = config.moonPhaseCount / 2;
|
||||
int distance = std::abs(moonPhase() - half);
|
||||
return 1.0 - static_cast<double>(distance) / static_cast<double>(half);
|
||||
}
|
||||
|
||||
} // namespace vf
|
||||
Reference in new issue
Block a user