From 337150dca9065f2abdec7005d75935844fce6df0 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 23 Aug 2026 17:14:13 +0200 Subject: [PATCH] feat(core): add temperature and humidity climate sampling --- core/include/vf/Climate.h | 74 +++++++++++++++++ core/src/Climate.cpp | 162 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 core/include/vf/Climate.h create mode 100644 core/src/Climate.cpp diff --git a/core/include/vf/Climate.h b/core/include/vf/Climate.h new file mode 100644 index 0000000..7948333 --- /dev/null +++ b/core/include/vf/Climate.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include "World.h" +#include "Math.h" +#include "BiomeGen.h" +#include "Weather.h" + +namespace vf { + +struct ClimateChange { + Vec3i pos; + Block from; + Block to; +}; + +struct ClimateConfig { + int seaLevel = 26; + // Temperature drop per block of elevation above sea level. + float heightLapse = 0.006f; + // Temperature drop per block of distance from the z=0 "equator". + float latitudeLapse = 0.0009f; + // Cold enough to lay snow and freeze exposed water. + float freezeTemperature = 0.15f; + // Warm enough to thaw ice and melt snow (hysteresis above freezing). + float meltTemperature = 0.5f; + // Milder threshold at which water freezes if it already touches ice, so a + // frozen patch creeps outward across a surface. + float frostSpreadTemperature = 0.30f; + int maxSnowDepth = 8; + // Ticks a block of ice must survive in the cold before it compacts to + // packed ice. + int packThreshold = 4; +}; + +// Deterministic per-region climate simulation layered on a BiomeGen. Column +// temperature is the biome sample adjusted for elevation and latitude. During +// snowfall it accumulates SnowLayer depth on exposed surfaces; exposed water +// freezes to Ice when cold; Ice and snow thaw when warm or beside a heat source +// (lava, torches, glowstone). tick scans the region in a fixed order and emits +// the resulting block changes without mutating the world; apply commits them. +class Climate { +public: + Climate(const BiomeGen& biomes, const ClimateConfig& config = {}); + + float temperatureAt(int gx, int gy, int gz) const; + float humidityAt(int gx, int gz) const; + bool freezingAt(int gx, int gy, int gz) const; + bool thawingAt(int gx, int gy, int gz) const; + + std::vector tick(World& world, const Vec3i& origin, const Vec3i& dims, + Precipitation precip); + + static void apply(World& world, const std::vector& changes); + + int snowDepthAt(const Vec3i& pos) const; + int iceAgeAt(const Vec3i& pos) const; + std::size_t trackedSnowColumns() const { return snowDepth.size(); } + + const ClimateConfig& settings() const { return config; } + +private: + bool heatSourceNear(const World& world, const Vec3i& pos) const; + bool iceNear(const World& world, const Vec3i& pos) const; + + const BiomeGen& biomes; + ClimateConfig config; + std::unordered_map snowDepth; + std::unordered_map iceAge; +}; + +} // namespace vf diff --git a/core/src/Climate.cpp b/core/src/Climate.cpp new file mode 100644 index 0000000..f3b4802 --- /dev/null +++ b/core/src/Climate.cpp @@ -0,0 +1,162 @@ +#include "vf/Climate.h" + +#include + +namespace vf { + +namespace { + +bool isHeatSource(Block b) { + switch (b) { + case Block::Lava: + case Block::Torch: + case Block::Glowstone: + case Block::FurnaceLit: + case Block::RedstoneTorch: + return true; + default: + return false; + } +} + +bool isFreezable(Block b) { return b == Block::Water; } + +const Vec3i neighbourOffsets[6] = { + {1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}, +}; + +} // namespace + +Climate::Climate(const BiomeGen& biomes, const ClimateConfig& config) + : biomes(biomes), config(config) {} + +float Climate::temperatureAt(int gx, int gy, int gz) const { + ClimateSample sample = biomes.climateAt(gx, gz); + float elevation = static_cast(gy - config.seaLevel); + float heightAdjust = elevation > 0.0f ? -config.heightLapse * elevation : 0.0f; + float latitudeAdjust = -config.latitudeLapse * static_cast(std::abs(gz)); + return sample.temperature + heightAdjust + latitudeAdjust; +} + +float Climate::humidityAt(int gx, int gz) const { + return biomes.climateAt(gx, gz).humidity; +} + +bool Climate::freezingAt(int gx, int gy, int gz) const { + return temperatureAt(gx, gy, gz) < config.freezeTemperature; +} + +bool Climate::thawingAt(int gx, int gy, int gz) const { + return temperatureAt(gx, gy, gz) >= config.meltTemperature; +} + +bool Climate::heatSourceNear(const World& world, const Vec3i& pos) const { + for (const Vec3i& off : neighbourOffsets) { + if (isHeatSource(world.blockAt(pos.x + off.x, pos.y + off.y, pos.z + off.z))) return true; + } + return false; +} + +bool Climate::iceNear(const World& world, const Vec3i& pos) const { + for (const Vec3i& off : neighbourOffsets) { + Block b = world.blockAt(pos.x + off.x, pos.y + off.y, pos.z + off.z); + if (b == Block::Ice || b == Block::PackedIce) return true; + } + return false; +} + +int Climate::snowDepthAt(const Vec3i& pos) const { + auto it = snowDepth.find(pos); + return it == snowDepth.end() ? 0 : it->second; +} + +int Climate::iceAgeAt(const Vec3i& pos) const { + auto it = iceAge.find(pos); + return it == iceAge.end() ? 0 : it->second; +} + +std::vector Climate::tick(World& world, const Vec3i& origin, const Vec3i& dims, + Precipitation precip) { + std::vector changes; + int x0 = origin.x, x1 = origin.x + dims.x; + int y0 = origin.y, y1 = origin.y + dims.y; + int z0 = origin.z, z1 = origin.z + dims.z; + + for (int x = x0; x < x1; ++x) { + for (int z = z0; z < z1; ++z) { + for (int y = y1 - 1; y >= y0; --y) { + Vec3i cell{x, y, z}; + Block here = world.blockAt(x, y, z); + Block above = world.blockAt(x, y + 1, z); + Block below = world.blockAt(x, y - 1, z); + float temperature = temperatureAt(x, y, z); + bool cold = temperature < config.freezeTemperature; + bool warm = temperature >= config.meltTemperature; + bool warmOrHeated = warm || heatSourceNear(world, cell); + + if (isFreezable(here) && above == Block::Air) { + bool frostCreep = temperature < config.frostSpreadTemperature && + iceNear(world, cell); + if (cold || frostCreep) { + changes.push_back({cell, here, Block::Ice}); + continue; + } + } + + if (here == Block::Ice) { + if (warmOrHeated) { + changes.push_back({cell, here, Block::Water}); + snowDepth.erase(cell); + iceAge.erase(cell); + } else if (cold) { + int age = iceAgeAt(cell) + 1; + if (age >= config.packThreshold) { + changes.push_back({cell, here, Block::PackedIce}); + iceAge.erase(cell); + } else { + iceAge[cell] = age; + } + } + continue; + } + + if (here == Block::PackedIce) { + if (warmOrHeated) changes.push_back({cell, here, Block::Water}); + continue; + } + + if (here == Block::SnowLayer) { + if (warmOrHeated) { + int depth = snowDepthAt(cell) - 1; + if (depth <= 0) { + changes.push_back({cell, here, Block::Air}); + snowDepth.erase(cell); + } else { + snowDepth[cell] = depth; + } + } else if (precip == Precipitation::Snow && cold) { + int depth = snowDepthAt(cell); + if (depth < config.maxSnowDepth) snowDepth[cell] = depth + 1; + } + continue; + } + + if (precip == Precipitation::Snow && cold && here == Block::Air && + isOpaque(below) && below != Block::Ice) { + changes.push_back({cell, Block::Air, Block::SnowLayer}); + snowDepth[cell] = 1; + continue; + } + } + } + } + return changes; +} + +void Climate::apply(World& world, const std::vector& changes) { + for (const ClimateChange& change : changes) { + world.setBlock(change.pos.x, change.pos.y, change.pos.z, change.to); + } +} + +} // namespace vf