chute
This commit is contained in:
@@ -1,40 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <deque>
|
|
||||||
#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<ChuteItem> ItemsInChute{};
|
|
||||||
};
|
|
||||||
|
|
||||||
public:
|
|
||||||
Chute() = default;
|
|
||||||
Chute(Vector2i position, const Vector<Vector2i>& chuteLinks)
|
|
||||||
: Data{static_cast<int>(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<ChuteLink, ChuteData> Data{};
|
|
||||||
};
|
|
||||||
148
include/Components/Chute.hpp
Normal file
148
include/Components/Chute.hpp
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<ChuteItem> ItemsInChute{};
|
||||||
|
uint16_t TicksToReachEnd{};
|
||||||
|
uint16_t TickCounter{};
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
Chute() = default;
|
||||||
|
Chute(const std::vector<Vector2>& positions, const ChuteConfig& config = {})
|
||||||
|
: Data{static_cast<int>(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<float>(std::abs(dy));
|
||||||
|
velocity = std::max(velocity, config.MinSpeed);
|
||||||
|
|
||||||
|
float distance = std::max(1.0f, std::sqrt(static_cast<float>(dx * dx + dy * dy)));
|
||||||
|
float linkTicks = distance / velocity;
|
||||||
|
|
||||||
|
Data[i - 1].RelativeX = static_cast<int8_t>(dx);
|
||||||
|
Data[i - 1].RelativeY = static_cast<int8_t>(dy);
|
||||||
|
Data[i - 1].Tick = static_cast<uint16_t>(std::ceil(linkTicks));
|
||||||
|
|
||||||
|
totalTicks += linkTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
Data.GetMetaData()->TicksToReachEnd = static_cast<uint16_t>(std::ceil(totalTicks));
|
||||||
|
}
|
||||||
|
|
||||||
|
void PushItem(Item item)
|
||||||
|
{
|
||||||
|
auto* meta = Data.GetMetaData();
|
||||||
|
meta->ItemsInChute.push_back(ChuteItem{ .ItemInfo = item, .ChuteEntered = meta->TickCounter });
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
SharedBuffer<ChuteLink, ChuteData> Data{};
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void Flecs_Chute(flecs::world& world)
|
||||||
|
{
|
||||||
|
world.component<ChuteInventoryInput>().is_a<Inventory>();
|
||||||
|
world.component<ChuteInventoryOutput>().is_a<Inventory>();
|
||||||
|
|
||||||
|
world.component<ChuteConfig>()
|
||||||
|
.member<float>("Gravity")
|
||||||
|
.member<float>("MinSpeed");
|
||||||
|
|
||||||
|
world.component<Chute>();
|
||||||
|
|
||||||
|
// tick the chute counter
|
||||||
|
world.system<Chute>("Chute Tick")
|
||||||
|
.kind(flecs::PreUpdate)
|
||||||
|
.each([](Chute& chute) {
|
||||||
|
chute.Data.GetMetaData()->TickCounter++;
|
||||||
|
});
|
||||||
|
|
||||||
|
// pull items from input inventory into chute
|
||||||
|
world.system<Chute, ChuteInventoryInput>("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, ChuteInventoryOutput, WorldInventory>("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<Vector2>& positions,
|
||||||
|
const Inventory& sourceInventory,
|
||||||
|
const Inventory& destInventory,
|
||||||
|
const ChuteConfig& config = {})
|
||||||
|
{
|
||||||
|
entity.set<Chute>(Chute{positions, config});
|
||||||
|
entity.set<ChuteInventoryInput>(ChuteInventoryInput{sourceInventory});
|
||||||
|
entity.set<ChuteInventoryOutput>(ChuteInventoryOutput{destInventory});
|
||||||
|
}
|
||||||
@@ -170,7 +170,9 @@ inline void Flecs_Inventory(flecs::world& world)
|
|||||||
world.component<FixedInventory7>().is_a<FixedInventory1>();
|
world.component<FixedInventory7>().is_a<FixedInventory1>();
|
||||||
world.component<FixedInventory8>().is_a<FixedInventory1>();
|
world.component<FixedInventory8>().is_a<FixedInventory1>();
|
||||||
|
|
||||||
world.component<Inventory>()
|
auto inv = world.component<Inventory>();
|
||||||
|
inv.add(flecs::Inheritable);
|
||||||
|
inv
|
||||||
.opaque(world.vector<uint32_t>())
|
.opaque(world.vector<uint32_t>())
|
||||||
.serialize([](const flecs::serializer *s, const Inventory *data) {
|
.serialize([](const flecs::serializer *s, const Inventory *data) {
|
||||||
if (!data->Slots) return 0;
|
if (!data->Slots) return 0;
|
||||||
@@ -216,6 +218,7 @@ inline void Flecs_Inventory(flecs::world& world)
|
|||||||
.member<uint32_t>("ProcessedTicks");
|
.member<uint32_t>("ProcessedTicks");
|
||||||
|
|
||||||
world.component<InventoryOwner>();
|
world.component<InventoryOwner>();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Inventory_Helper(const flecs::entity& entity, const WorldConfig& config, uint32_t maxPerSlot)
|
inline void Inventory_Helper(const flecs::entity& entity, const WorldConfig& config, uint32_t maxPerSlot)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "Components/Resource.hpp"
|
#include "Components/Resource.hpp"
|
||||||
#include "Components/Inventory.hpp"
|
#include "Components/Inventory.hpp"
|
||||||
#include "Components/Tick.hpp"
|
#include "Components/Tick.hpp"
|
||||||
|
#include "Components/Chute.hpp"
|
||||||
|
|
||||||
WorldInstance::WorldInstance(const WorldConfig& worldConfig)
|
WorldInstance::WorldInstance(const WorldConfig& worldConfig)
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,7 @@ void WorldInstance::RegisterTypes(flecs::world &world)
|
|||||||
Flecs_Tick(world);
|
Flecs_Tick(world);
|
||||||
Flecs_Inventory(world);
|
Flecs_Inventory(world);
|
||||||
Flecs_Resource(world);
|
Flecs_Resource(world);
|
||||||
|
Flecs_Chute(world);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldInstance::ProcessFrame()
|
void WorldInstance::ProcessFrame()
|
||||||
|
|||||||
162
tests/Components/test_Chute.cpp
Normal file
162
tests/Components/test_Chute.cpp
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#include <doctest/doctest.h>
|
||||||
|
#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<Inventory>().AddItems(stoneID, 3);
|
||||||
|
|
||||||
|
auto dest = world.GetEcsWorld().entity();
|
||||||
|
Inventory_Helper(dest, config, 100);
|
||||||
|
|
||||||
|
// vertical drop: (0,10) -> (0,0), should be fast
|
||||||
|
std::vector<Vector2> path = { {0, 10}, {0, 0} };
|
||||||
|
auto chuteEntity = world.GetEcsWorld().entity();
|
||||||
|
Chute_Helper(chuteEntity, path,
|
||||||
|
source.get<Inventory>(),
|
||||||
|
dest.get<Inventory>());
|
||||||
|
|
||||||
|
// get the transit time
|
||||||
|
auto chute = chuteEntity.get<Chute>();
|
||||||
|
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<Inventory>();
|
||||||
|
CHECK(srcInv.GetItemsAmount(stoneID) == 0);
|
||||||
|
|
||||||
|
// tick until items arrive
|
||||||
|
for (uint16_t i = 1; i < transitTicks; ++i)
|
||||||
|
world.ProcessFrame();
|
||||||
|
|
||||||
|
auto destInv = dest.get<Inventory>();
|
||||||
|
CHECK(destInv.GetItemsAmount(stoneID) == 0);
|
||||||
|
|
||||||
|
world.ProcessFrame();
|
||||||
|
|
||||||
|
destInv = dest.get<Inventory>();
|
||||||
|
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<Inventory>().AddItems(ironID, 1);
|
||||||
|
|
||||||
|
auto dest = world.GetEcsWorld().entity();
|
||||||
|
Inventory_Helper(dest, config, 100);
|
||||||
|
|
||||||
|
// multi-link path with gradual descent
|
||||||
|
std::vector<Vector2> path = { {0, 10}, {1, 9}, {2, 8}, {3, 7}, {4, 6}, {5, 5} };
|
||||||
|
auto chuteEntity = world.GetEcsWorld().entity();
|
||||||
|
Chute_Helper(chuteEntity, path,
|
||||||
|
source.get<Inventory>(),
|
||||||
|
dest.get<Inventory>());
|
||||||
|
|
||||||
|
auto chute = chuteEntity.get<Chute>();
|
||||||
|
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<Inventory>();
|
||||||
|
CHECK(destInv.GetItemsAmount(ironID) == 0);
|
||||||
|
|
||||||
|
// one more tick — item arrives
|
||||||
|
world.ProcessFrame();
|
||||||
|
|
||||||
|
destInv = dest.get<Inventory>();
|
||||||
|
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<Inventory>().AddItems(stoneID, 3);
|
||||||
|
|
||||||
|
// destination can only hold 1
|
||||||
|
auto dest = world.GetEcsWorld().entity();
|
||||||
|
Inventory_Helper(dest, config, 1);
|
||||||
|
|
||||||
|
std::vector<Vector2> path = { {0, 10}, {0, 0} };
|
||||||
|
auto chuteEntity = world.GetEcsWorld().entity();
|
||||||
|
Chute_Helper(chuteEntity, path,
|
||||||
|
source.get<Inventory>(),
|
||||||
|
dest.get<Inventory>());
|
||||||
|
|
||||||
|
auto chute = chuteEntity.get<Chute>();
|
||||||
|
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<Inventory>();
|
||||||
|
CHECK(destInv.GetItemsAmount(stoneID) == 1);
|
||||||
|
|
||||||
|
auto& worldInv = world.GetEcsWorld().ensure<WorldInventory>();
|
||||||
|
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<Inventory>().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<Vector2> path = { {0, 0}, {5, 0} };
|
||||||
|
auto chuteEntity = world.GetEcsWorld().entity();
|
||||||
|
Chute_Helper(chuteEntity, path,
|
||||||
|
source.get<Inventory>(),
|
||||||
|
dest.get<Inventory>(),
|
||||||
|
chuteConfig);
|
||||||
|
|
||||||
|
auto chute = chuteEntity.get<Chute>();
|
||||||
|
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<Inventory>();
|
||||||
|
CHECK(destInv.GetItemsAmount(woodID) == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user