This commit is contained in:
Connor
2026-02-20 14:53:28 +09:00
parent cf20ed827e
commit c081aa868f
6 changed files with 336 additions and 10 deletions

View File

@@ -28,6 +28,7 @@ FetchContent_MakeAvailable(flecs doctest)
# Only compile sources needed for the core library # Only compile sources needed for the core library
set(SOURCES set(SOURCES
src/Components/Config/WorldConfig.cpp src/Components/Config/WorldConfig.cpp
src/Components/Support.cpp
src/Core/WorldInstance.cpp src/Core/WorldInstance.cpp
) )

View File

@@ -2,18 +2,33 @@
#include <stdint.h> #include <stdint.h>
#include "flecs.h"
#include "Components/Misc.hpp"
#include "Util/Span.h"
struct Support struct Support
{ {
uint8_t MaxSupport : 5; uint8_t MaxSupport{};
bool SupportsUp : 1; uint8_t SupportsAvailable{};
bool SupportsRight : 1;
bool SupportsLeft : 1;
uint8_t SupportsAvailable : 5;
bool SupportedByBottom : 1;
bool SupportedByRight : 1;
bool SupportedByLeft : 1;
}; };
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
View 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);
}
});
}

View File

View File

@@ -6,6 +6,7 @@
#include "Components/Inventory.hpp" #include "Components/Inventory.hpp"
#include "Components/Tick.hpp" #include "Components/Tick.hpp"
#include "Components/Chute.hpp" #include "Components/Chute.hpp"
#include "Components/Support.h"
WorldInstance::WorldInstance(const WorldConfig& worldConfig) WorldInstance::WorldInstance(const WorldConfig& worldConfig)
{ {
@@ -26,6 +27,7 @@ void WorldInstance::RegisterTypes(flecs::world &world)
Flecs_Inventory(world); Flecs_Inventory(world);
Flecs_Resource(world); Flecs_Resource(world);
Flecs_Chute(world); Flecs_Chute(world);
Flecs_Support(world);
} }
void WorldInstance::ProcessFrame() void WorldInstance::ProcessFrame()

View 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));
}
}