diff --git a/core/include/vf/TimeOfDay.h b/core/include/vf/TimeOfDay.h new file mode 100644 index 0000000..41c87e0 --- /dev/null +++ b/core/include/vf/TimeOfDay.h @@ -0,0 +1,73 @@ +#pragma once + +#include + +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 diff --git a/core/src/TimeOfDay.cpp b/core/src/TimeOfDay.cpp new file mode 100644 index 0000000..90d3c04 --- /dev/null +++ b/core/src/TimeOfDay.cpp @@ -0,0 +1,159 @@ +#include "vf/TimeOfDay.h" + +#include + +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(std::llround(config.sunriseFraction * period)); + WorldTick sunsetTick = static_cast(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(timeOfDayTicks()) / static_cast(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(config.nightSkyLight) / + static_cast(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(config.daySkyLight); + int rounded = static_cast(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(m); +} + +double TimeOfDay::moonBrightness() const { + int half = config.moonPhaseCount / 2; + int distance = std::abs(moonPhase() - half); + return 1.0 - static_cast(distance) / static_cast(half); +} + +} // namespace vf