From 9f0cbc3410f7ea74a3b3a1bb9bbc2b27ac1957f0 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 10:37:13 +0200 Subject: [PATCH] feat(core): add hopper item transfer between containers --- core/include/vf/Hopper.h | 29 ++++++++++++++++++ core/src/Hopper.cpp | 64 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 core/include/vf/Hopper.h create mode 100644 core/src/Hopper.cpp diff --git a/core/include/vf/Hopper.h b/core/include/vf/Hopper.h new file mode 100644 index 0000000..245a42f --- /dev/null +++ b/core/include/vf/Hopper.h @@ -0,0 +1,29 @@ +#pragma once + +#include "Container.h" +#include "World.h" +#include "Block.h" +#include "Math.h" + +namespace vf { + +constexpr int hopperSlots = 5; +constexpr int hopperCooldownTicks = 8; + +struct HopperState { + Face facing = Face::NegY; + int cooldown = 0; +}; + +// One deterministic hopper step at `pos`. Pulls a single item from the +// container directly above and pushes a single item into the container the +// hopper faces, then arms the cooldown when anything moved. The hopper owns a +// five slot inventory registered in `store` at its own cell. No-op while the +// cooldown is running or when the world block is no longer a hopper. +bool tickHopper(const World& world, const Vec3i& pos, HopperState& state, ContainerStore& store); + +// True when the hopper has an item ready to push and a target that can take it, +// or a source above with an item it can pull. Used to gate cooldown re-arming. +bool hopperCanTransfer(const Vec3i& pos, const HopperState& state, ContainerStore& store); + +} // namespace vf diff --git a/core/src/Hopper.cpp b/core/src/Hopper.cpp new file mode 100644 index 0000000..3adf6cc --- /dev/null +++ b/core/src/Hopper.cpp @@ -0,0 +1,64 @@ +#include "vf/Hopper.h" + +namespace vf { + +namespace { + +Vec3i above(const Vec3i& pos) { return pos + Vec3i{0, 1, 0}; } + +bool pushOne(Container& source, ContainerStore& store, const Vec3i& target) { + Container* dest = store.at(target); + if (!dest) return false; + return source.transferTo(*dest, 1) > 0; +} + +bool pullOne(ContainerStore& store, const Vec3i& source, Container& sink) { + Container* src = store.at(source); + if (!src) return false; + return src->transferTo(sink, 1) > 0; +} + +} // namespace + +bool hopperCanTransfer(const Vec3i& pos, const HopperState& state, ContainerStore& store) { + Container* self = store.at(pos); + if (!self) return false; + + Vec3i target = pos + faceDelta(state.facing); + if (!self->isEmpty()) { + Container* dest = store.at(target); + if (dest && !dest->isFull()) { + Container probe = *self; + Container destCopy = *dest; + if (probe.transferTo(destCopy, 1) > 0) return true; + } + } + + Container* src = store.at(above(pos)); + if (src && !src->isEmpty() && !self->isFull()) { + Container srcCopy = *src; + Container selfCopy = *self; + if (srcCopy.transferTo(selfCopy, 1) > 0) return true; + } + return false; +} + +bool tickHopper(const World& world, const Vec3i& pos, HopperState& state, ContainerStore& store) { + if (world.blockAt(pos.x, pos.y, pos.z) != Block::Hopper) return false; + + Container& self = store.ensure(pos, hopperSlots); + if (state.cooldown > 0) { + --state.cooldown; + return false; + } + + bool moved = false; + Vec3i target = pos + faceDelta(state.facing); + moved |= pushOne(self, store, target); + moved |= pullOne(store, above(pos), self); + + if (moved) state.cooldown = hopperCooldownTicks; + return moved; +} + +} // namespace vf