diff --git a/core/include/vf/Fluid.h b/core/include/vf/Fluid.h new file mode 100644 index 0000000..1db4b89 --- /dev/null +++ b/core/include/vf/Fluid.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include "World.h" + +namespace vf { + +constexpr uint8_t fluidMax = 8; // a full source cell; flow decays to 1 at the edge + +// Cellular water automaton over a fixed world box. Levels run 0 (dry) to +// fluidMax. Source cells stay full every tick; flowing water spreads to lower +// horizontal neighbours losing one level per step, and pours straight down a +// gap filling the cell below fully. Updates are double buffered, so a tick is a +// pure function of the previous state (deterministic). Solid world voxels block +// and support the flow. +class FluidSim { +public: + FluidSim(World& world, const Vec3i& origin, const Vec3i& dims); + + void addSource(int gx, int gy, int gz); + void removeSource(int gx, int gy, int gz); + void setLevel(int gx, int gy, int gz, uint8_t level); + + uint8_t level(int gx, int gy, int gz) const; + bool isSource(int gx, int gy, int gz) const; + bool isWater(int gx, int gy, int gz) const { return level(gx, gy, gz) > 0; } + + // Advances one tick. Returns true if any cell changed. + bool step(); + // Steps until stable or maxSteps reached. Returns the number of steps taken. + int settle(int maxSteps); + + // Writes Block::Water into the world for every wet, currently-air cell. + void applyToWorld() const; + + const Vec3i& regionOrigin() const { return origin; } + const Vec3i& regionDims() const { return dims; } + const std::vector& levels() const { return level_; } + const std::vector& sourceFlags() const { return source_; } + void loadRaw(const std::vector& levels, const std::vector& sources); + +private: + int index(int lx, int ly, int lz) const { return (ly * dims.z + lz) * dims.x + lx; } + bool inBox(int lx, int ly, int lz) const; + bool solidWorld(int gx, int gy, int gz) const; + uint8_t levelLocal(int lx, int ly, int lz) const; + bool sourceLocal(int lx, int ly, int lz) const; + bool spreader(int lx, int ly, int lz) const; + uint8_t computeNext(int lx, int ly, int lz) const; + + World& world; + Vec3i origin; + Vec3i dims; + std::vector level_; + std::vector source_; +}; + +} // namespace vf diff --git a/core/src/Fluid.cpp b/core/src/Fluid.cpp new file mode 100644 index 0000000..ac7c66b --- /dev/null +++ b/core/src/Fluid.cpp @@ -0,0 +1,117 @@ +#include "vf/Fluid.h" + +namespace vf { + +FluidSim::FluidSim(World& world, const Vec3i& origin, const Vec3i& dims) + : world(world), origin(origin), dims(dims) { + std::size_t cells = static_cast(dims.x) * dims.y * dims.z; + level_.assign(cells, 0); + source_.assign(cells, 0); +} + +bool FluidSim::inBox(int lx, int ly, int lz) const { + return lx >= 0 && lx < dims.x && ly >= 0 && ly < dims.y && lz >= 0 && lz < dims.z; +} + +bool FluidSim::solidWorld(int gx, int gy, int gz) const { return world.solidAt(gx, gy, gz); } + +uint8_t FluidSim::levelLocal(int lx, int ly, int lz) const { + if (!inBox(lx, ly, lz)) return 0; + return level_[index(lx, ly, lz)]; +} + +bool FluidSim::sourceLocal(int lx, int ly, int lz) const { + if (!inBox(lx, ly, lz)) return false; + return source_[index(lx, ly, lz)] != 0; +} + +// A cell that actively pushes water sideways: a source, or water that rests on a +// solid floor, or a surface pool cell (nothing pouring onto it from above but +// water beneath). Mid-fall water is deliberately excluded so a vertical stream +// does not wet every level it passes. +bool FluidSim::spreader(int lx, int ly, int lz) const { + if (sourceLocal(lx, ly, lz)) return true; + if (levelLocal(lx, ly, lz) == 0) return false; + if (solidWorld(origin.x + lx, origin.y + ly - 1, origin.z + lz)) return true; + if (levelLocal(lx, ly - 1, lz) > 0 && levelLocal(lx, ly + 1, lz) == 0) return true; + return false; +} + +uint8_t FluidSim::computeNext(int lx, int ly, int lz) const { + if (solidWorld(origin.x + lx, origin.y + ly, origin.z + lz)) return 0; + if (sourceLocal(lx, ly, lz)) return fluidMax; + if (levelLocal(lx, ly + 1, lz) > 0) return fluidMax; // fed from above + + uint8_t best = 0; + const int horiz[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (auto& h : horiz) { + int nx = lx + h[0], nz = lz + h[1]; + uint8_t lh = levelLocal(nx, ly, nz); + if (lh <= 1 || !spreader(nx, ly, nz)) continue; + uint8_t give = static_cast(lh - 1); + if (give > best) best = give; + } + return best; +} + +bool FluidSim::step() { + std::vector next(level_.size(), 0); + for (int lx = 0; lx < dims.x; ++lx) + for (int ly = 0; ly < dims.y; ++ly) + for (int lz = 0; lz < dims.z; ++lz) + next[index(lx, ly, lz)] = computeNext(lx, ly, lz); + bool changed = next != level_; + level_.swap(next); + return changed; +} + +int FluidSim::settle(int maxSteps) { + int steps = 0; + while (steps < maxSteps && step()) ++steps; + return steps; +} + +void FluidSim::addSource(int gx, int gy, int gz) { + int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z; + if (!inBox(lx, ly, lz)) return; + source_[index(lx, ly, lz)] = 1; + level_[index(lx, ly, lz)] = fluidMax; +} + +void FluidSim::removeSource(int gx, int gy, int gz) { + int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z; + if (!inBox(lx, ly, lz)) return; + source_[index(lx, ly, lz)] = 0; +} + +void FluidSim::setLevel(int gx, int gy, int gz, uint8_t value) { + int lx = gx - origin.x, ly = gy - origin.y, lz = gz - origin.z; + if (!inBox(lx, ly, lz)) return; + level_[index(lx, ly, lz)] = value > fluidMax ? fluidMax : value; +} + +uint8_t FluidSim::level(int gx, int gy, int gz) const { + return levelLocal(gx - origin.x, gy - origin.y, gz - origin.z); +} + +bool FluidSim::isSource(int gx, int gy, int gz) const { + return sourceLocal(gx - origin.x, gy - origin.y, gz - origin.z); +} + +void FluidSim::applyToWorld() const { + for (int lx = 0; lx < dims.x; ++lx) + for (int ly = 0; ly < dims.y; ++ly) + for (int lz = 0; lz < dims.z; ++lz) { + if (level_[index(lx, ly, lz)] == 0) continue; + int gx = origin.x + lx, gy = origin.y + ly, gz = origin.z + lz; + if (world.blockAt(gx, gy, gz) == Block::Air) + world.setBlock(gx, gy, gz, Block::Water); + } +} + +void FluidSim::loadRaw(const std::vector& levels, const std::vector& sources) { + if (levels.size() == level_.size()) level_ = levels; + if (sources.size() == source_.size()) source_ = sources; +} + +} // namespace vf