From 48d677dd5eb7189732dc223af31c8735b7fd17f3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 13:20:19 +0200 Subject: [PATCH] feat(core): add minecart movement along rail networks --- core/include/vf/Minecart.h | 66 +++++++++++++++ core/src/Minecart.cpp | 168 +++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 core/include/vf/Minecart.h create mode 100644 core/src/Minecart.cpp diff --git a/core/include/vf/Minecart.h b/core/include/vf/Minecart.h new file mode 100644 index 0000000..3df9c55 --- /dev/null +++ b/core/include/vf/Minecart.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include "Rail.h" +#include "Container.h" +#include "World.h" +#include "Math.h" + +namespace vf { + +enum class CartKind : uint8_t { + Empty, + Chest, + Hopper, +}; + +constexpr int chestCartSlots = 27; +constexpr int hopperCartSlots = 5; + +struct CartConfig { + float maxSpeed = 0.4f; + float friction = 0.02f; + float slopeAccel = 0.05f; + float poweredAccel = 0.06f; + float poweredLaunch = 0.06f; + float brakeFactor = 0.5f; + float stopThreshold = 0.005f; + int hopperCooldownTicks = 4; +}; + +struct Minecart { + Vec3i railPos; + RailDir moveDir = RailDir::East; + float speed = 0.0f; + float offset = 0.0f; + CartKind kind = CartKind::Empty; + bool derailed = false; + int hopperCooldown = 0; + Container inventory; +}; + +Minecart makeMinecart(const Vec3i& railPos, RailDir moveDir, CartKind kind); + +// World-space centre of the cart: the rail cell centre pushed along the heading +// by the sub-cell offset. +Vec3 cartWorldPos(const Minecart& cart); + +// Advances the cart one simulation tick: applies powered-rail boost or braking, +// slope gravity and rolling friction, then walks the track by the resulting +// speed, switching heading through curves and derailing at track ends. Hopper +// carts also pull one item per cooldown from a container above their cell. +void tickMinecart(World& world, Minecart& cart, const CartConfig& config, + const std::function& poweredRailActive, + ContainerStore& store); + +// Ticks a batch of carts, then resolves collisions deterministically in index +// order: two carts that land on the same rail cell exchange momentum (equal +// mass, elastic 1D) and separate. A head-on pair bounces apart; a moving cart +// hitting a stalled one hands over its speed and stops. +void stepMinecarts(World& world, std::vector& carts, const CartConfig& config, + const std::function& poweredRailActive, + ContainerStore& store); + +} // namespace vf diff --git a/core/src/Minecart.cpp b/core/src/Minecart.cpp new file mode 100644 index 0000000..65bf961 --- /dev/null +++ b/core/src/Minecart.cpp @@ -0,0 +1,168 @@ +#include "vf/Minecart.h" + +#include + +namespace vf { + +namespace { + +int slotsFor(CartKind kind) { + switch (kind) { + case CartKind::Chest: return chestCartSlots; + case CartKind::Hopper: return hopperCartSlots; + case CartKind::Empty: return 0; + } + return 0; +} + +void applyRailForces(World& world, Minecart& cart, const CartConfig& config, + const std::function& poweredRailActive, + RailShape shape) { + Block block = world.blockAt(cart.railPos.x, cart.railPos.y, cart.railPos.z); + + if (block == Block::PoweredRail) { + if (poweredRailActive(cart.railPos)) { + cart.speed += config.poweredAccel; + if (cart.speed < config.poweredLaunch) cart.speed = config.poweredLaunch; + } else { + cart.speed *= config.brakeFactor; + } + } else { + cart.speed *= (1.0f - config.friction); + } + + if (isAscending(shape)) { + RailDir up = ascendUpDir(shape); + if (cart.moveDir == up) cart.speed -= config.slopeAccel; + else if (cart.moveDir == opposite(up)) cart.speed += config.slopeAccel; + } + + if (cart.speed < 0.0f) { + cart.moveDir = opposite(cart.moveDir); + cart.speed = -cart.speed; + } + + cart.speed = std::min(cart.speed, config.maxSpeed); + if (cart.speed < config.stopThreshold) cart.speed = 0.0f; +} + +void serviceHopperCart(Minecart& cart, const CartConfig& config, ContainerStore& store) { + if (cart.kind != CartKind::Hopper) return; + if (cart.hopperCooldown > 0) { + --cart.hopperCooldown; + return; + } + Container* srcAbove = store.at(cart.railPos + Vec3i{0, 1, 0}); + if (srcAbove && srcAbove->transferTo(cart.inventory, 1) > 0) + cart.hopperCooldown = config.hopperCooldownTicks; +} + +Vec3 velocityVector(const Minecart& cart) { + Vec3i step = railDelta(cart.moveDir); + return Vec3{static_cast(step.x) * cart.speed, + static_cast(step.y) * cart.speed, + static_cast(step.z) * cart.speed}; +} + +void assignVelocity(Minecart& cart, const Vec3& vel) { + float ax = std::fabs(vel.x); + float az = std::fabs(vel.z); + if (ax < 1e-6f && az < 1e-6f) { + cart.speed = 0.0f; + return; + } + if (ax >= az) { + cart.moveDir = vel.x >= 0.0f ? RailDir::East : RailDir::West; + cart.speed = ax; + } else { + cart.moveDir = vel.z >= 0.0f ? RailDir::South : RailDir::North; + cart.speed = az; + } +} + +} // namespace + +Minecart makeMinecart(const Vec3i& railPos, RailDir moveDir, CartKind kind) { + Minecart cart; + cart.railPos = railPos; + cart.moveDir = moveDir; + cart.kind = kind; + cart.inventory = Container{slotsFor(kind)}; + return cart; +} + +Vec3 cartWorldPos(const Minecart& cart) { + Vec3i step = railDelta(cart.moveDir); + return Vec3{ + static_cast(cart.railPos.x) + 0.5f + static_cast(step.x) * cart.offset, + static_cast(cart.railPos.y) + 0.5f + static_cast(step.y) * cart.offset, + static_cast(cart.railPos.z) + 0.5f + static_cast(step.z) * cart.offset, + }; +} + +void tickMinecart(World& world, Minecart& cart, const CartConfig& config, + const std::function& poweredRailActive, + ContainerStore& store) { + if (cart.derailed) return; + if (!isRailBlock(world.blockAt(cart.railPos.x, cart.railPos.y, cart.railPos.z))) { + cart.derailed = true; + cart.speed = 0.0f; + return; + } + + bool switched = poweredRailActive(cart.railPos); + RailShape shape = inferRailShape(world, cart.railPos, switched); + applyRailForces(world, cart, config, poweredRailActive, shape); + + cart.offset += cart.speed; + while (cart.offset >= 1.0f && cart.speed > 0.0f) { + cart.offset -= 1.0f; + bool junction = poweredRailActive(cart.railPos); + RailStep next = advanceOnRail(world, cart.railPos, cart.moveDir, junction); + if (!next.valid) { + cart.derailed = true; + cart.speed = 0.0f; + cart.offset = 0.0f; + return; + } + cart.railPos = next.nextPos; + cart.moveDir = next.exitDir; + } + + serviceHopperCart(cart, config, store); +} + +void stepMinecarts(World& world, std::vector& carts, const CartConfig& config, + const std::function& poweredRailActive, + ContainerStore& store) { + std::vector previous(carts.size()); + for (std::size_t i = 0; i < carts.size(); ++i) previous[i] = carts[i].railPos; + + for (Minecart& cart : carts) + tickMinecart(world, cart, config, poweredRailActive, store); + + for (std::size_t i = 0; i < carts.size(); ++i) { + for (std::size_t j = i + 1; j < carts.size(); ++j) { + Minecart& a = carts[i]; + Minecart& b = carts[j]; + if (a.derailed || b.derailed) continue; + if (previous[i] == previous[j]) continue; + + bool sameCell = a.railPos == b.railPos; + bool passedThrough = a.railPos == previous[j] && b.railPos == previous[i]; + if (!sameCell && !passedThrough) continue; + + Vec3 va = velocityVector(a); + Vec3 vb = velocityVector(b); + assignVelocity(a, vb); + assignVelocity(b, va); + + a.railPos = previous[i]; + b.railPos = previous[j]; + a.offset = 0.0f; + b.offset = 0.0f; + } + } +} + +} // namespace vf