support
This commit is contained in:
@@ -28,6 +28,7 @@ FetchContent_MakeAvailable(flecs doctest)
|
||||
# Only compile sources needed for the core library
|
||||
set(SOURCES
|
||||
src/Components/Config/WorldConfig.cpp
|
||||
src/Components/Support.cpp
|
||||
src/Core/WorldInstance.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -2,18 +2,33 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "flecs.h"
|
||||
|
||||
#include "Components/Misc.hpp"
|
||||
#include "Util/Span.h"
|
||||
|
||||
struct Support
|
||||
{
|
||||
uint8_t MaxSupport : 5;
|
||||
bool SupportsUp : 1;
|
||||
bool SupportsRight : 1;
|
||||
bool SupportsLeft : 1;
|
||||
uint8_t SupportsAvailable : 5;
|
||||
bool SupportedByBottom : 1;
|
||||
bool SupportedByRight : 1;
|
||||
bool SupportedByLeft : 1;
|
||||
uint8_t MaxSupport{};
|
||||
uint8_t SupportsAvailable{};
|
||||
};
|
||||
|
||||
struct RequiresSupport
|
||||
struct GroundedSupport {};
|
||||
struct RequiresSupport {};
|
||||
|
||||
flecs::entity GetSupport(flecs::world& world, Vector2 pos);
|
||||
|
||||
void RecalculateSupport(flecs::world& world,
|
||||
tcb::span<const Vector2> skip = {},
|
||||
tcb::span<const Vector2> unground = {});
|
||||
|
||||
bool CanRemove(flecs::world& world, tcb::span<const Vector2> positions);
|
||||
|
||||
void Flecs_Support(flecs::world& world);
|
||||
|
||||
inline void Support_Helper(const flecs::entity& entity, Vector2 pos, uint8_t maxSupport, bool grounded = false)
|
||||
{
|
||||
};
|
||||
entity.set<TilePosition>({ pos });
|
||||
if (grounded) entity.add<GroundedSupport>(); // before set<Support> so OnAdd fires with grounded state visible
|
||||
entity.set<Support>({ maxSupport });
|
||||
}
|
||||
|
||||
186
src/Components/Support.cpp
Normal file
186
src/Components/Support.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "Components/Support.h"
|
||||
|
||||
#include <queue>
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct SupportNode
|
||||
{
|
||||
flecs::entity Entity;
|
||||
Vector2 Pos;
|
||||
};
|
||||
|
||||
bool InSpan(tcb::span<const Vector2> span, Vector2 pos)
|
||||
{
|
||||
return std::any_of(span.begin(), span.end(), [&](const Vector2& p) {
|
||||
return p.X == pos.X && p.Y == pos.Y;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
flecs::entity GetSupport(flecs::world& world, Vector2 pos)
|
||||
{
|
||||
flecs::entity result{};
|
||||
world.each([&](flecs::entity e, const TilePosition& tp, const Support&) {
|
||||
if (tp.Position.X == pos.X && tp.Position.Y == pos.Y)
|
||||
result = e;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void RecalculateSupport(flecs::world& world,
|
||||
tcb::span<const Vector2> skip,
|
||||
tcb::span<const Vector2> unground)
|
||||
{
|
||||
// Collect all support nodes first (avoid nested world.each() re-entrancy)
|
||||
std::vector<SupportNode> nodes;
|
||||
world.each([&](flecs::entity e, const TilePosition& tp, const Support&) {
|
||||
if (!InSpan(skip, tp.Position))
|
||||
nodes.push_back({ e, tp.Position });
|
||||
});
|
||||
|
||||
// Reset all SupportsAvailable to 0
|
||||
for (auto& node : nodes)
|
||||
node.Entity.ensure<Support>().SupportsAvailable = 0;
|
||||
|
||||
// Seed queue with grounded supports
|
||||
std::queue<SupportNode> queue;
|
||||
for (auto& node : nodes)
|
||||
{
|
||||
if (node.Entity.has<GroundedSupport>() && !InSpan(unground, node.Pos))
|
||||
{
|
||||
uint8_t maxSupport = node.Entity.get<Support>().MaxSupport;
|
||||
node.Entity.ensure<Support>().SupportsAvailable = maxSupport;
|
||||
queue.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// BFS: propagate support outward from grounded anchors
|
||||
const Vector2 offsets[] = { {0, 1}, {0, -1}, {1, 0}, {-1, 0} };
|
||||
while (!queue.empty())
|
||||
{
|
||||
SupportNode current = queue.front();
|
||||
queue.pop();
|
||||
|
||||
uint8_t currentAvailable = current.Entity.get<Support>().SupportsAvailable;
|
||||
|
||||
for (const auto& offset : offsets)
|
||||
{
|
||||
uint8_t cost = (offset.X != 0) ? 2 : 1;
|
||||
if (currentAvailable < cost) continue;
|
||||
|
||||
Vector2 neighborPos = current.Pos + offset;
|
||||
|
||||
auto it = std::find_if(nodes.begin(), nodes.end(), [&](const SupportNode& n) {
|
||||
return n.Pos.X == neighborPos.X && n.Pos.Y == neighborPos.Y;
|
||||
});
|
||||
if (it == nodes.end()) continue;
|
||||
|
||||
uint8_t neighborMax = it->Entity.get<Support>().MaxSupport;
|
||||
uint8_t candidate = std::min(static_cast<uint8_t>(currentAvailable - cost), neighborMax);
|
||||
uint8_t neighborCurrent = it->Entity.get<Support>().SupportsAvailable;
|
||||
|
||||
if (candidate > neighborCurrent)
|
||||
{
|
||||
it->Entity.ensure<Support>().SupportsAvailable = candidate;
|
||||
queue.push(*it);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool CanRemove(flecs::world& world, tcb::span<const Vector2> positions)
|
||||
{
|
||||
RecalculateSupport(world, positions);
|
||||
|
||||
bool valid = true;
|
||||
|
||||
// Collect remaining positions with active support; flag any that lost all support
|
||||
std::vector<Vector2> supportedPositions;
|
||||
world.each([&](const TilePosition& tp, const Support& s) {
|
||||
if (InSpan(positions, tp.Position)) return;
|
||||
if (s.SupportsAvailable == 0)
|
||||
valid = false;
|
||||
else
|
||||
supportedPositions.push_back(tp.Position);
|
||||
});
|
||||
|
||||
// Check every RequiresSupport entity has an active support one tile below it
|
||||
world.each([&](flecs::entity e, const TilePosition& tp) {
|
||||
if (e.has<RequiresSupport>())
|
||||
{
|
||||
Vector2 supportPos = { tp.Position.X, tp.Position.Y - 1 };
|
||||
tcb::span<const Vector2> suppSpan(supportedPositions.data(), supportedPositions.size());
|
||||
if (!InSpan(suppSpan, supportPos))
|
||||
valid = false;
|
||||
}
|
||||
});
|
||||
|
||||
RecalculateSupport(world);
|
||||
return valid;
|
||||
}
|
||||
|
||||
void Flecs_Support(flecs::world& world)
|
||||
{
|
||||
world.component<Support>()
|
||||
.member<uint8_t>("MaxSupport")
|
||||
.member<uint8_t>("SupportsAvailable");
|
||||
|
||||
world.component<GroundedSupport>();
|
||||
world.component<RequiresSupport>();
|
||||
|
||||
// OnSet fires after the value is written (unlike OnAdd which fires during archetype move,
|
||||
// before the actual struct values are copied in — MaxSupport would still be 0 at that point).
|
||||
world.observer<Support>("On Support Set")
|
||||
.event(flecs::OnSet)
|
||||
.each([](flecs::entity e, Support&) {
|
||||
auto w = e.world();
|
||||
RecalculateSupport(w);
|
||||
});
|
||||
|
||||
world.observer<Support>("On Support Remove")
|
||||
.event(flecs::OnRemove)
|
||||
.each([](flecs::entity e, Support&) {
|
||||
auto w = e.world();
|
||||
if (e.has<TilePosition>())
|
||||
{
|
||||
Vector2 pos = e.get<TilePosition>().Position;
|
||||
RecalculateSupport(w, tcb::span<const Vector2>(&pos, 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
RecalculateSupport(w);
|
||||
}
|
||||
});
|
||||
|
||||
// GroundedSupport is a tag (no data) — use .with<>() filter so the lambda
|
||||
// takes only flecs::entity, avoiding the empty-type column issue.
|
||||
world.observer("On GroundedSupport Add")
|
||||
.with<GroundedSupport>()
|
||||
.event(flecs::OnAdd)
|
||||
.each([](flecs::entity e) {
|
||||
auto w = e.world();
|
||||
RecalculateSupport(w);
|
||||
});
|
||||
|
||||
// OnRemove fires before actual removal, so entity still has GroundedSupport.
|
||||
// Pass position to unground so the entity is not treated as a grounded seed.
|
||||
world.observer("On GroundedSupport Remove")
|
||||
.with<GroundedSupport>()
|
||||
.event(flecs::OnRemove)
|
||||
.each([](flecs::entity e) {
|
||||
auto w = e.world();
|
||||
if (e.has<TilePosition>())
|
||||
{
|
||||
Vector2 pos = e.get<TilePosition>().Position;
|
||||
RecalculateSupport(w, {}, tcb::span<const Vector2>(&pos, 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
RecalculateSupport(w);
|
||||
}
|
||||
});
|
||||
}
|
||||
0
src/Components/components.cpp
Normal file
0
src/Components/components.cpp
Normal file
@@ -6,6 +6,7 @@
|
||||
#include "Components/Inventory.hpp"
|
||||
#include "Components/Tick.hpp"
|
||||
#include "Components/Chute.hpp"
|
||||
#include "Components/Support.h"
|
||||
|
||||
WorldInstance::WorldInstance(const WorldConfig& worldConfig)
|
||||
{
|
||||
@@ -26,6 +27,7 @@ void WorldInstance::RegisterTypes(flecs::world &world)
|
||||
Flecs_Inventory(world);
|
||||
Flecs_Resource(world);
|
||||
Flecs_Chute(world);
|
||||
Flecs_Support(world);
|
||||
}
|
||||
|
||||
void WorldInstance::ProcessFrame()
|
||||
|
||||
122
tests/Components/test_Support.cpp
Normal file
122
tests/Components/test_Support.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include <doctest/doctest.h>
|
||||
#include "Components/Configs/WorldConfig.hpp"
|
||||
#include "Core/WorldInstance.h"
|
||||
#include "Components/Support.h"
|
||||
|
||||
TEST_SUITE("Support") {
|
||||
TEST_CASE("grounded support has SupportsAvailable equal to MaxSupport") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
auto e = world.GetEcsWorld().entity();
|
||||
Support_Helper(e, { 0, 0 }, 5, true);
|
||||
|
||||
auto s = e.get<Support>();
|
||||
CHECK(s.SupportsAvailable == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("vertical neighbor costs 1 point") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
auto ground = world.GetEcsWorld().entity();
|
||||
Support_Helper(ground, { 0, 0 }, 5, true);
|
||||
|
||||
auto above = world.GetEcsWorld().entity();
|
||||
Support_Helper(above, { 0, 1 }, 5);
|
||||
|
||||
auto s = above.get<Support>();
|
||||
CHECK(s.SupportsAvailable == 4);
|
||||
}
|
||||
|
||||
TEST_CASE("horizontal neighbor costs 2 points") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
auto ground = world.GetEcsWorld().entity();
|
||||
Support_Helper(ground, { 0, 0 }, 5, true);
|
||||
|
||||
auto beside = world.GetEcsWorld().entity();
|
||||
Support_Helper(beside, { 1, 0 }, 5);
|
||||
|
||||
auto s = beside.get<Support>();
|
||||
CHECK(s.SupportsAvailable == 3);
|
||||
}
|
||||
|
||||
TEST_CASE("floating support has SupportsAvailable zero") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
auto e = world.GetEcsWorld().entity();
|
||||
Support_Helper(e, { 5, 5 }, 5);
|
||||
|
||||
auto s = e.get<Support>();
|
||||
CHECK(s.SupportsAvailable == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("CanRemove returns false when removal disconnects a support") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
// chain: ground(0,0) -> (0,1) -> (0,2)
|
||||
auto ground = world.GetEcsWorld().entity();
|
||||
Support_Helper(ground, { 0, 0 }, 5, true);
|
||||
|
||||
auto mid = world.GetEcsWorld().entity();
|
||||
Support_Helper(mid, { 0, 1 }, 5);
|
||||
|
||||
auto top = world.GetEcsWorld().entity();
|
||||
Support_Helper(top, { 0, 2 }, 5);
|
||||
|
||||
// removing (0,1) disconnects (0,2)
|
||||
std::vector<Vector2> toRemove = { { 0, 1 } };
|
||||
CHECK_FALSE(CanRemove(world.GetEcsWorld(), toRemove));
|
||||
}
|
||||
|
||||
TEST_CASE("CanRemove returns true when removing a leaf node") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
auto ground = world.GetEcsWorld().entity();
|
||||
Support_Helper(ground, { 0, 0 }, 5, true);
|
||||
|
||||
auto leaf = world.GetEcsWorld().entity();
|
||||
Support_Helper(leaf, { 0, 1 }, 5);
|
||||
|
||||
// removing the leaf leaves only the grounded anchor — valid
|
||||
std::vector<Vector2> toRemove = { { 0, 1 } };
|
||||
CHECK(CanRemove(world.GetEcsWorld(), toRemove));
|
||||
}
|
||||
|
||||
TEST_CASE("CanRemove returns false when removal would unsupport a RequiresSupport entity") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
// grounded support at (0,0); machine one tile above at (0,1)
|
||||
auto ground = world.GetEcsWorld().entity();
|
||||
Support_Helper(ground, { 0, 0 }, 5, true);
|
||||
|
||||
auto machine = world.GetEcsWorld().entity();
|
||||
machine.set<TilePosition>({ { 0, 1 } });
|
||||
machine.add<RequiresSupport>();
|
||||
|
||||
// removing the only support at (0,0) leaves the machine at (0,1) unsupported
|
||||
std::vector<Vector2> toRemove = { { 0, 0 } };
|
||||
CHECK_FALSE(CanRemove(world.GetEcsWorld(), toRemove));
|
||||
}
|
||||
|
||||
TEST_CASE("CanRemove returns true when RequiresSupport entity still has support") {
|
||||
WorldInstance world{ WorldConfig{} };
|
||||
|
||||
// chain: ground(0,0) -> leaf(0,1); machine one tile above leaf at (0,2)
|
||||
auto ground = world.GetEcsWorld().entity();
|
||||
Support_Helper(ground, { 0, 0 }, 5, true);
|
||||
|
||||
auto leaf = world.GetEcsWorld().entity();
|
||||
Support_Helper(leaf, { 0, 1 }, 5);
|
||||
|
||||
auto machine = world.GetEcsWorld().entity();
|
||||
machine.set<TilePosition>({ { 0, 2 } });
|
||||
machine.add<RequiresSupport>();
|
||||
|
||||
// sibling at (1,0) is a horizontal leaf; removing it doesn't affect (0,1)
|
||||
auto sibling = world.GetEcsWorld().entity();
|
||||
Support_Helper(sibling, { 1, 0 }, 5);
|
||||
|
||||
std::vector<Vector2> toRemove = { { 1, 0 } };
|
||||
CHECK(CanRemove(world.GetEcsWorld(), toRemove));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user