From cf20ed827e614d2f0f0b3be9259fc79e9a794e51 Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 16 Feb 2026 16:55:17 +0900 Subject: [PATCH] chute --- include/Components/Chute.h | 40 -------- include/Components/Chute.hpp | 148 ++++++++++++++++++++++++++++ include/Components/Inventory.hpp | 5 +- src/Core/WorldInstance.cpp | 2 + tests/Components/test_Chute.cpp | 162 +++++++++++++++++++++++++++++++ 5 files changed, 316 insertions(+), 41 deletions(-) delete mode 100644 include/Components/Chute.h create mode 100644 include/Components/Chute.hpp create mode 100644 tests/Components/test_Chute.cpp diff --git a/include/Components/Chute.h b/include/Components/Chute.h deleted file mode 100644 index 98870e9..0000000 --- a/include/Components/Chute.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include -#include "Types/Item.hpp" -#include "Util/SharedBuffer.h" - -struct Chute -{ - struct ChuteLink - { - int8_t RelativeX{}; - int8_t RelativeY{}; - uint16_t Tick{}; - }; - struct ChuteItem - { - Item Item{}; - uint16_t ChuteEntered{}; - }; - struct ChuteData - { - std::deque ItemsInChute{}; - }; - -public: - Chute() = default; - Chute(Vector2i position, const Vector& chuteLinks) - : Data{static_cast(chuteLinks.size()), ChuteData{}} - { - for (int i{}; i < chuteLinks.size(); ++i) - { - Data.GetData()[i].RelativeX = position.x - chuteLinks[i].x; - Data.GetData()[i].RelativeY = position.y - chuteLinks[i].y; - } - } - - -public: - SharedBuffer Data{}; -}; \ No newline at end of file diff --git a/include/Components/Chute.hpp b/include/Components/Chute.hpp new file mode 100644 index 0000000..ca805d4 --- /dev/null +++ b/include/Components/Chute.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include +#include + +#include "flecs.h" + +#include "Types/Item.hpp" +#include "Components/Misc.hpp" +#include "Components/Inventory.hpp" +#include "Util/SharedBuffer.h" + +struct ChuteConfig +{ + float Gravity{ 1.0f }; + float MinSpeed{ 0.5f }; +}; + +struct ChuteInventoryInput : public Inventory {}; +struct ChuteInventoryOutput : public Inventory {}; + +struct Chute +{ + struct ChuteLink + { + int8_t RelativeX{}; + int8_t RelativeY{}; + uint16_t Tick{}; + }; + struct ChuteItem + { + Item ItemInfo{}; + uint16_t ChuteEntered{}; // use TickCounter - ChuteEntered to get the time it has been in the chute + }; + struct ChuteData + { + std::deque ItemsInChute{}; + uint16_t TicksToReachEnd{}; + uint16_t TickCounter{}; + }; + +public: + Chute() = default; + Chute(const std::vector& positions, const ChuteConfig& config = {}) + : Data{static_cast(positions.size() - 1), ChuteData{}} + { + float velocity = 0.0f; + float totalTicks = 0.0f; + + for (size_t i = 1; i < positions.size(); ++i) + { + int dx = positions[i].X - positions[i - 1].X; + int dy = positions[i].Y - positions[i - 1].Y; + + // accumulate velocity from vertical drop (dy <= 0, so -dy >= 0) + velocity += config.Gravity * static_cast(std::abs(dy)); + velocity = std::max(velocity, config.MinSpeed); + + float distance = std::max(1.0f, std::sqrt(static_cast(dx * dx + dy * dy))); + float linkTicks = distance / velocity; + + Data[i - 1].RelativeX = static_cast(dx); + Data[i - 1].RelativeY = static_cast(dy); + Data[i - 1].Tick = static_cast(std::ceil(linkTicks)); + + totalTicks += linkTicks; + } + + Data.GetMetaData()->TicksToReachEnd = static_cast(std::ceil(totalTicks)); + } + + void PushItem(Item item) + { + auto* meta = Data.GetMetaData(); + meta->ItemsInChute.push_back(ChuteItem{ .ItemInfo = item, .ChuteEntered = meta->TickCounter }); + } + +public: + SharedBuffer Data{}; +}; + +inline void Flecs_Chute(flecs::world& world) +{ + world.component().is_a(); + world.component().is_a(); + + world.component() + .member("Gravity") + .member("MinSpeed"); + + world.component(); + + // tick the chute counter + world.system("Chute Tick") + .kind(flecs::PreUpdate) + .each([](Chute& chute) { + chute.Data.GetMetaData()->TickCounter++; + }); + + // pull items from input inventory into chute + world.system("Chute Input") + .kind(flecs::OnUpdate) + .each([](Chute& chute, ChuteInventoryInput& input) { + for (uint16_t i = 0; i < input.Slots.GetSize(); ++i) + { + while (input.Slots[i] > 0) + { + input.Slots[i]--; + chute.PushItem(Item{ i }); + } + } + }); + + // pop items that have reached the end and deposit into output inventory + world.system("Chute Output") + .kind(flecs::OnUpdate) + .each([](Chute& chute, ChuteInventoryOutput& output, WorldInventory& worldInv) { + auto* meta = chute.Data.GetMetaData(); + while (!meta->ItemsInChute.empty()) + { + auto& front = meta->ItemsInChute.front(); + uint16_t elapsed = meta->TickCounter - front.ChuteEntered; + if (elapsed < meta->TicksToReachEnd) + break; + + uint16_t itemID = front.ItemInfo.ItemID; + meta->ItemsInChute.pop_front(); + + if (!output.IsFull(itemID)) + output.AddItems(itemID, 1); + else + worldInv.AddItems(itemID, 1); + } + }); +} + +inline void Chute_Helper(const flecs::entity& entity, + const std::vector& positions, + const Inventory& sourceInventory, + const Inventory& destInventory, + const ChuteConfig& config = {}) +{ + entity.set(Chute{positions, config}); + entity.set(ChuteInventoryInput{sourceInventory}); + entity.set(ChuteInventoryOutput{destInventory}); +} diff --git a/include/Components/Inventory.hpp b/include/Components/Inventory.hpp index dafc13b..a4a799e 100644 --- a/include/Components/Inventory.hpp +++ b/include/Components/Inventory.hpp @@ -170,7 +170,9 @@ inline void Flecs_Inventory(flecs::world& world) world.component().is_a(); world.component().is_a(); - world.component() + auto inv = world.component(); + inv.add(flecs::Inheritable); + inv .opaque(world.vector()) .serialize([](const flecs::serializer *s, const Inventory *data) { if (!data->Slots) return 0; @@ -216,6 +218,7 @@ inline void Flecs_Inventory(flecs::world& world) .member("ProcessedTicks"); world.component(); + } inline void Inventory_Helper(const flecs::entity& entity, const WorldConfig& config, uint32_t maxPerSlot) diff --git a/src/Core/WorldInstance.cpp b/src/Core/WorldInstance.cpp index 4ecda52..40d3ec5 100644 --- a/src/Core/WorldInstance.cpp +++ b/src/Core/WorldInstance.cpp @@ -5,6 +5,7 @@ #include "Components/Resource.hpp" #include "Components/Inventory.hpp" #include "Components/Tick.hpp" +#include "Components/Chute.hpp" WorldInstance::WorldInstance(const WorldConfig& worldConfig) { @@ -24,6 +25,7 @@ void WorldInstance::RegisterTypes(flecs::world &world) Flecs_Tick(world); Flecs_Inventory(world); Flecs_Resource(world); + Flecs_Chute(world); } void WorldInstance::ProcessFrame() diff --git a/tests/Components/test_Chute.cpp b/tests/Components/test_Chute.cpp new file mode 100644 index 0000000..dffcaac --- /dev/null +++ b/tests/Components/test_Chute.cpp @@ -0,0 +1,162 @@ +#include +#include "Components/Configs/WorldConfig.hpp" +#include "Core/WorldInstance.h" +#include "Components/Chute.hpp" + +TEST_SUITE("Chute") { + TEST_CASE("chute transports item from source to destination") { + WorldConfig config{}; + uint16_t stoneID = config.RegisterItem("Stone"); + + WorldInstance world{ config }; + + // source inventory with items, destination empty + auto source = world.GetEcsWorld().entity(); + Inventory_Helper(source, config, 100); + source.ensure().AddItems(stoneID, 3); + + auto dest = world.GetEcsWorld().entity(); + Inventory_Helper(dest, config, 100); + + // vertical drop: (0,10) -> (0,0), should be fast + std::vector path = { {0, 10}, {0, 0} }; + auto chuteEntity = world.GetEcsWorld().entity(); + Chute_Helper(chuteEntity, path, + source.get(), + dest.get()); + + // get the transit time + auto chute = chuteEntity.get(); + uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd; + CHECK(transitTicks > 0); + + // tick once to pull items from source into chute + world.ProcessFrame(); + + auto srcInv = source.get(); + CHECK(srcInv.GetItemsAmount(stoneID) == 0); + + // tick until items arrive + for (uint16_t i = 1; i < transitTicks; ++i) + world.ProcessFrame(); + + auto destInv = dest.get(); + CHECK(destInv.GetItemsAmount(stoneID) == 0); + + world.ProcessFrame(); + + destInv = dest.get(); + CHECK(destInv.GetItemsAmount(stoneID) == 3); + } + + TEST_CASE("chute respects transit time for longer paths") { + WorldConfig config{}; + uint16_t ironID = config.RegisterItem("Iron"); + + WorldInstance world{ config }; + + auto source = world.GetEcsWorld().entity(); + Inventory_Helper(source, config, 100); + source.ensure().AddItems(ironID, 1); + + auto dest = world.GetEcsWorld().entity(); + Inventory_Helper(dest, config, 100); + + // multi-link path with gradual descent + std::vector path = { {0, 10}, {1, 9}, {2, 8}, {3, 7}, {4, 6}, {5, 5} }; + auto chuteEntity = world.GetEcsWorld().entity(); + Chute_Helper(chuteEntity, path, + source.get(), + dest.get()); + + auto chute = chuteEntity.get(); + uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd; + CHECK(transitTicks > 1); + + // pull items into chute + world.ProcessFrame(); + + // tick one less than transit time — item should not have arrived + for (uint16_t i = 1; i < transitTicks; ++i) + world.ProcessFrame(); + + auto destInv = dest.get(); + CHECK(destInv.GetItemsAmount(ironID) == 0); + + // one more tick — item arrives + world.ProcessFrame(); + + destInv = dest.get(); + CHECK(destInv.GetItemsAmount(ironID) == 1); + } + + TEST_CASE("chute overflows to world inventory when destination is full") { + WorldConfig config{}; + uint16_t stoneID = config.RegisterItem("Stone"); + + WorldInstance world{ config }; + + auto source = world.GetEcsWorld().entity(); + Inventory_Helper(source, config, 100); + source.ensure().AddItems(stoneID, 3); + + // destination can only hold 1 + auto dest = world.GetEcsWorld().entity(); + Inventory_Helper(dest, config, 1); + + std::vector path = { {0, 10}, {0, 0} }; + auto chuteEntity = world.GetEcsWorld().entity(); + Chute_Helper(chuteEntity, path, + source.get(), + dest.get()); + + auto chute = chuteEntity.get(); + uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd; + + // tick enough for all items to arrive + for (uint16_t i = 0; i <= transitTicks; ++i) + world.ProcessFrame(); + + auto destInv = dest.get(); + CHECK(destInv.GetItemsAmount(stoneID) == 1); + + auto& worldInv = world.GetEcsWorld().ensure(); + CHECK(worldInv.GetItemsAmount(stoneID) == 2); + } + + TEST_CASE("horizontal chute uses minimum speed") { + WorldConfig config{}; + uint16_t woodID = config.RegisterItem("Wood"); + + WorldInstance world{ config }; + + auto source = world.GetEcsWorld().entity(); + Inventory_Helper(source, config, 100); + source.ensure().AddItems(woodID, 1); + + auto dest = world.GetEcsWorld().entity(); + Inventory_Helper(dest, config, 100); + + // flat path: dy=0 throughout, should use MinSpeed + ChuteConfig chuteConfig{ .Gravity = 1.0f, .MinSpeed = 0.5f }; + std::vector path = { {0, 0}, {5, 0} }; + auto chuteEntity = world.GetEcsWorld().entity(); + Chute_Helper(chuteEntity, path, + source.get(), + dest.get(), + chuteConfig); + + auto chute = chuteEntity.get(); + uint16_t transitTicks = chute.Data.GetMetaData()->TicksToReachEnd; + + // distance=5, speed=0.5 -> 10 ticks + CHECK(transitTicks == 10); + + // tick enough for item to arrive + for (uint16_t i = 0; i <= transitTicks; ++i) + world.ProcessFrame(); + + auto destInv = dest.get(); + CHECK(destInv.GetItemsAmount(woodID) == 1); + } +}