From f54e79869a4871093ee5596f31eb782706b86394 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 26 Aug 2026 09:54:39 +0200 Subject: [PATCH] feat(core): implement crop growth and farmland hydration --- core/src/Farming.cpp | 406 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 core/src/Farming.cpp diff --git a/core/src/Farming.cpp b/core/src/Farming.cpp new file mode 100644 index 0000000..2c395a4 --- /dev/null +++ b/core/src/Farming.cpp @@ -0,0 +1,406 @@ +#include "vf/Farming.h" + +#include + +namespace vf { + +namespace { + +const Vec3i horizontalDirs[4] = { + {1, 0, 0}, {-1, 0, 0}, {0, 0, 1}, {0, 0, -1}, +}; + +Block cropBlock(CropType type) { + switch (type) { + case CropType::Wheat: return Block::Wheat; + case CropType::Carrot: return Block::Carrot; + case CropType::Potato: return Block::Potato; + case CropType::PumpkinStem: return Block::PumpkinStem; + case CropType::MelonStem: return Block::MelonStem; + case CropType::SugarCane: return Block::SugarCane; + case CropType::Cactus: return Block::Cactus; + case CropType::Sapling: return Block::Sapling; + case CropType::Vine: return Block::Vine; + case CropType::None: return Block::Air; + } + return Block::Air; +} + +bool bonemealable(CropType type) { + switch (type) { + case CropType::Wheat: + case CropType::Carrot: + case CropType::Potato: + case CropType::PumpkinStem: + case CropType::MelonStem: + case CropType::Sapling: + return true; + default: + return false; + } +} + +bool tillable(Block b) { + return b == Block::Farmland || b == Block::Dirt || b == Block::Grass; +} + +bool waterAdjacent(const World& world, const Vec3i& pos) { + for (const Vec3i& dir : horizontalDirs) { + if (world.blockAt(pos.x + dir.x, pos.y + dir.y, pos.z + dir.z) == Block::Water) return true; + } + return false; +} + +} // namespace + +const char* cropTypeName(CropType type) { + switch (type) { + case CropType::None: return "none"; + case CropType::Wheat: return "wheat"; + case CropType::Carrot: return "carrot"; + case CropType::Potato: return "potato"; + case CropType::PumpkinStem: return "pumpkinStem"; + case CropType::MelonStem: return "melonStem"; + case CropType::SugarCane: return "sugarCane"; + case CropType::Cactus: return "cactus"; + case CropType::Sapling: return "sapling"; + case CropType::Vine: return "vine"; + } + return "unknown"; +} + +int cropMaxStage(CropType type) { + switch (type) { + case CropType::Wheat: + case CropType::Carrot: + case CropType::Potato: + case CropType::PumpkinStem: + case CropType::MelonStem: + return 7; + case CropType::SugarCane: + case CropType::Cactus: + return 2; + case CropType::Sapling: + return 3; + case CropType::Vine: + return 4; + case CropType::None: + return 0; + } + return 0; +} + +Farming::Farming(uint32_t seed, const FarmingConfig& config) + : config(config), rngState(seed == 0 ? 0x1234567u : seed) {} + +uint32_t Farming::nextRandom() { + rngState ^= rngState << 13; + rngState ^= rngState >> 17; + rngState ^= rngState << 5; + return rngState; +} + +bool Farming::rollGrowth(uint32_t denominator, int points) { + if (points <= 0) return false; + return nextRandom() % denominator < static_cast(points); +} + +void Farming::plant(World& world, const Vec3i& pos, CropType type) { + world.setBlock(pos.x, pos.y, pos.z, cropBlock(type)); + crops[pos] = CropState{type, 0, cropMaxStage(type)}; +} + +bool Farming::isHydrated(const World& world, const Vec3i& farmland) const { + int r = config.hydrationRadius; + for (int dx = -r; dx <= r; ++dx) { + for (int dz = -r; dz <= r; ++dz) { + for (int dy = 0; dy <= 1; ++dy) { + if (world.blockAt(farmland.x + dx, farmland.y + dy, farmland.z + dz) == Block::Water) + return true; + } + } + } + return false; +} + +const CropState* Farming::stateAt(const Vec3i& pos) const { + auto it = crops.find(pos); + return it == crops.end() ? nullptr : &it->second; +} + +Farming::GrowthResult Farming::growGroundCrop(World& world, const Vec3i& pos, CropState& state, + int skyLight) { + if (state.mature()) return GrowthResult::None; + if (skyLight < config.minGrowthLight) return GrowthResult::None; + Vec3i soil{pos.x, pos.y - 1, pos.z}; + if (world.blockAt(soil.x, soil.y, soil.z) != Block::Farmland) return GrowthResult::None; + if (world.blockAt(pos.x, pos.y, pos.z) != cropBlock(state.type)) return GrowthResult::None; + + int points = 1 + (isHydrated(world, soil) ? 2 : 0); + if (rollGrowth(config.growthDenominator, points)) { + ++state.stage; + return GrowthResult::Advanced; + } + return GrowthResult::None; +} + +Farming::GrowthResult Farming::growStem(World& world, const Vec3i& pos, CropState& state, + int skyLight) { + if (skyLight < config.minGrowthLight) return GrowthResult::None; + Vec3i soil{pos.x, pos.y - 1, pos.z}; + if (world.blockAt(soil.x, soil.y, soil.z) != Block::Farmland) return GrowthResult::None; + + if (!state.mature()) { + int points = 1 + (isHydrated(world, soil) ? 2 : 0); + if (rollGrowth(config.growthDenominator, points)) { + ++state.stage; + return GrowthResult::Advanced; + } + return GrowthResult::None; + } + + Block fruit = state.type == CropType::PumpkinStem ? Block::Pumpkin : Block::Melon; + for (const Vec3i& dir : horizontalDirs) { + if (world.blockAt(pos.x + dir.x, pos.y + dir.y, pos.z + dir.z) == fruit) + return GrowthResult::None; + } + if (!rollGrowth(config.growthDenominator, 1)) return GrowthResult::None; + + for (const Vec3i& dir : horizontalDirs) { + Vec3i spot{pos.x + dir.x, pos.y, pos.z + dir.z}; + if (world.blockAt(spot.x, spot.y, spot.z) != Block::Air) continue; + if (!tillable(world.blockAt(spot.x, spot.y - 1, spot.z))) continue; + world.setBlock(spot.x, spot.y, spot.z, fruit); + return GrowthResult::Advanced; + } + return GrowthResult::None; +} + +Farming::GrowthResult Farming::growColumn(World& world, const Vec3i& pos, CropState& state, + bool cane) { + int maxHeight = cane ? config.caneMaxHeight : config.cactusMaxHeight; + if (state.stage + 1 >= maxHeight) return GrowthResult::None; + + Vec3i top{pos.x, pos.y + state.stage + 1, pos.z}; + if (world.blockAt(top.x, top.y, top.z) != Block::Air) return GrowthResult::None; + + if (cane) { + if (!waterAdjacent(world, Vec3i{pos.x, pos.y - 1, pos.z})) return GrowthResult::None; + } else { + for (const Vec3i& dir : horizontalDirs) { + if (isSolid(world.blockAt(top.x + dir.x, top.y + dir.y, top.z + dir.z))) + return GrowthResult::None; + } + } + + if (!rollGrowth(config.verticalDenominator, 1)) return GrowthResult::None; + world.setBlock(top.x, top.y, top.z, cane ? Block::SugarCane : Block::Cactus); + ++state.stage; + return GrowthResult::Advanced; +} + +Farming::GrowthResult Farming::growVine(World& world, const Vec3i& pos, CropState& state) { + if (state.stage >= state.maxStage) return GrowthResult::None; + + bool anchored = false; + for (const Vec3i& dir : horizontalDirs) { + if (isSolid(world.blockAt(pos.x + dir.x, pos.y + dir.y, pos.z + dir.z))) { + anchored = true; + break; + } + } + if (!anchored) return GrowthResult::None; + + Vec3i below{pos.x, pos.y - state.stage - 1, pos.z}; + if (world.blockAt(below.x, below.y, below.z) != Block::Air) return GrowthResult::None; + if (!rollGrowth(config.verticalDenominator, 1)) return GrowthResult::None; + + world.setBlock(below.x, below.y, below.z, Block::Vine); + ++state.stage; + return GrowthResult::Advanced; +} + +Farming::GrowthResult Farming::growSapling(World& world, const Vec3i& pos, CropState& state, + int skyLight) { + if (skyLight < config.minGrowthLight) return GrowthResult::None; + if (world.blockAt(pos.x, pos.y, pos.z) != Block::Sapling) return GrowthResult::None; + + if (!state.mature()) { + if (rollGrowth(config.saplingDenominator, 1)) { + ++state.stage; + return GrowthResult::Advanced; + } + return GrowthResult::None; + } + + stampTree(world, pos); + return GrowthResult::Removed; +} + +void Farming::stampTree(World& world, const Vec3i& base) { + int span = config.treeTrunkMax - config.treeTrunkMin + 1; + int trunk = config.treeTrunkMin + static_cast(nextRandom() % static_cast(span)); + for (int i = 0; i < trunk; ++i) { + world.setBlock(base.x, base.y + i, base.z, Block::Wood); + } + int topY = base.y + trunk - 1; + for (int dy = -1; dy <= 1; ++dy) { + int radius = dy == 1 ? 1 : 2; + for (int dx = -radius; dx <= radius; ++dx) { + for (int dz = -radius; dz <= radius; ++dz) { + if (dx == 0 && dz == 0 && dy <= 0) continue; + if (std::abs(dx) == radius && std::abs(dz) == radius) continue; + int y = topY + dy; + if (world.blockAt(base.x + dx, y, base.z + dz) == Block::Air) + world.setBlock(base.x + dx, y, base.z + dz, Block::Leaves); + } + } + } + world.setBlock(base.x, topY + 1, base.z, Block::Leaves); +} + +bool Farming::randomTick(World& world, const Vec3i& pos, int skyLight) { + auto it = crops.find(pos); + if (it == crops.end()) return false; + CropState& state = it->second; + + GrowthResult result = GrowthResult::None; + switch (state.type) { + case CropType::Wheat: + case CropType::Carrot: + case CropType::Potato: + result = growGroundCrop(world, pos, state, skyLight); + break; + case CropType::PumpkinStem: + case CropType::MelonStem: + result = growStem(world, pos, state, skyLight); + break; + case CropType::SugarCane: + result = growColumn(world, pos, state, true); + break; + case CropType::Cactus: + result = growColumn(world, pos, state, false); + break; + case CropType::Vine: + result = growVine(world, pos, state); + break; + case CropType::Sapling: + result = growSapling(world, pos, state, skyLight); + break; + case CropType::None: + break; + } + + if (result == GrowthResult::Removed) crops.erase(pos); + return result != GrowthResult::None; +} + +int Farming::growStep(World& world, int skyLight) { + std::vector positions; + positions.reserve(crops.size()); + for (const auto& entry : crops) positions.push_back(entry.first); + std::sort(positions.begin(), positions.end(), [](const Vec3i& a, const Vec3i& b) { + if (a.x != b.x) return a.x < b.x; + if (a.y != b.y) return a.y < b.y; + return a.z < b.z; + }); + + int advanced = 0; + for (const Vec3i& pos : positions) { + if (randomTick(world, pos, skyLight)) ++advanced; + } + return advanced; +} + +bool Farming::applyBonemeal(World& world, const Vec3i& pos) { + auto it = crops.find(pos); + if (it == crops.end()) return false; + CropState& state = it->second; + if (!bonemealable(state.type)) return false; + + if (state.type == CropType::Sapling) { + if (nextRandom() % 100 < config.bonemealSaplingChance) { + stampTree(world, pos); + crops.erase(pos); + } + return true; + } + + if (state.mature()) return false; + int span = config.bonemealMaxStages - config.bonemealMinStages + 1; + int boost = config.bonemealMinStages + static_cast(nextRandom() % static_cast(span)); + state.stage += boost; + if (state.stage > state.maxStage) state.stage = state.maxStage; + return true; +} + +bool Farming::trample(World& world, const Vec3i& farmland) { + if (world.blockAt(farmland.x, farmland.y, farmland.z) != Block::Farmland) return false; + world.setBlock(farmland.x, farmland.y, farmland.z, Block::Dirt); + + Vec3i above{farmland.x, farmland.y + 1, farmland.z}; + auto it = crops.find(above); + if (it != crops.end()) { + CropType type = it->second.type; + if (type == CropType::Wheat || type == CropType::Carrot || type == CropType::Potato || + type == CropType::PumpkinStem || type == CropType::MelonStem) { + world.setBlock(above.x, above.y, above.z, Block::Air); + crops.erase(above); + } + } + return true; +} + +std::vector Farming::harvest(World& world, const Vec3i& pos) { + auto it = crops.find(pos); + if (it == crops.end()) return {}; + CropState state = it->second; + std::vector drops; + + switch (state.type) { + case CropType::Wheat: + if (state.mature()) drops.push_back({Block::Wheat, 1}); + world.setBlock(pos.x, pos.y, pos.z, Block::Air); + break; + case CropType::Carrot: + if (state.mature()) drops.push_back({Block::Carrot, 1 + static_cast(nextRandom() % 3)}); + world.setBlock(pos.x, pos.y, pos.z, Block::Air); + break; + case CropType::Potato: + if (state.mature()) drops.push_back({Block::Potato, 1 + static_cast(nextRandom() % 3)}); + world.setBlock(pos.x, pos.y, pos.z, Block::Air); + break; + case CropType::PumpkinStem: + case CropType::MelonStem: + world.setBlock(pos.x, pos.y, pos.z, Block::Air); + break; + case CropType::SugarCane: { + int height = state.stage + 1; + drops.push_back({Block::SugarCane, height}); + for (int i = 0; i < height; ++i) world.setBlock(pos.x, pos.y + i, pos.z, Block::Air); + break; + } + case CropType::Cactus: { + int height = state.stage + 1; + drops.push_back({Block::Cactus, height}); + for (int i = 0; i < height; ++i) world.setBlock(pos.x, pos.y + i, pos.z, Block::Air); + break; + } + case CropType::Vine: { + int length = state.stage + 1; + for (int i = 0; i < length; ++i) world.setBlock(pos.x, pos.y - i, pos.z, Block::Air); + break; + } + case CropType::Sapling: + drops.push_back({Block::Sapling, 1}); + world.setBlock(pos.x, pos.y, pos.z, Block::Air); + break; + case CropType::None: + break; + } + + crops.erase(pos); + return drops; +} + +} // namespace vf