#include "catch.hpp" #include #include #include #include "DespawnAtComponent.h" #include "EntityAdmin.h" #include "EntityHitTest.h" #include "ScrapDataComponent.h" #include "ScrapSystem.h" namespace { bool contains(const std::vector& v, entt::entity e) { return std::find(v.begin(), v.end(), e) != v.end(); } } // namespace // --------------------------------------------------------------------------- // Spawn // --------------------------------------------------------------------------- TEST_CASE("ScrapSystem: spawn returns a valid entity with correct scrap data", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); REQUIRE(admin.isValid(e)); REQUIRE(admin.get(e).amount == 5); REQUIRE(admin.get(e).tick == 100); } // --------------------------------------------------------------------------- // Despawn timing // --------------------------------------------------------------------------- TEST_CASE("ScrapSystem: scrap still present one tick before despawnAt", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50); ss.tickDespawn(49); REQUIRE(admin.isValid(e)); } TEST_CASE("ScrapSystem: scrap removed at despawnAt tick", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 1, 50); ss.tickDespawn(50); REQUIRE_FALSE(admin.isValid(e)); } // --------------------------------------------------------------------------- // Selective removal // --------------------------------------------------------------------------- TEST_CASE("ScrapSystem: tickDespawn removes only expired scraps", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity earlyE = ss.spawn(QVector2D(0.0f, 0.0f), 1, 30); const entt::entity lateE = ss.spawn(QVector2D(1.0f, 0.0f), 2, 60); ss.tickDespawn(30); REQUIRE_FALSE(admin.isValid(earlyE)); REQUIRE(admin.isValid(lateE)); } // --------------------------------------------------------------------------- // Consume // --------------------------------------------------------------------------- TEST_CASE("ScrapSystem: consume returns amount and destroys entity", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 7, 100); const std::optional amount = ss.consume(e); REQUIRE(amount.has_value()); REQUIRE(*amount == 7); REQUIRE_FALSE(admin.isValid(e)); } TEST_CASE("ScrapSystem: consume returns nullopt for invalid entity", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const std::optional amount = ss.consume(entt::null); REQUIRE_FALSE(amount.has_value()); } // --------------------------------------------------------------------------- // collectOne // --------------------------------------------------------------------------- TEST_CASE("ScrapSystem: collectOne depletes one scrap and keeps the pile until empty", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100); REQUIRE(ss.collectOne(e)); REQUIRE(admin.isValid(e)); REQUIRE(admin.get(e).amount == 2); REQUIRE(ss.collectOne(e)); REQUIRE(admin.isValid(e)); REQUIRE(admin.get(e).amount == 1); // Final unit collected: the pile is removed once depleted. REQUIRE(ss.collectOne(e)); REQUIRE_FALSE(admin.isValid(e)); } TEST_CASE("ScrapSystem: collectOne returns false for an invalid entity", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); REQUIRE_FALSE(ss.collectOne(entt::null)); } // --------------------------------------------------------------------------- // allScrapInfo // --------------------------------------------------------------------------- TEST_CASE("ScrapSystem: allScrapInfo returns all spawned scrap", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); ss.spawn(QVector2D(1.0f, 2.0f), 3, 100); ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); const std::vector info = ss.getAllScrapInfo(); REQUIRE(info.size() == 2); } TEST_CASE("ScrapSystem: allScrapInfo reports each pile's remaining amount", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100); const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200); const std::vector info = ss.getAllScrapInfo(); REQUIRE(info.size() == 2); for (const ScrapInfo& i : info) { if (i.entity == a) { REQUIRE(i.amount == 3); } else if (i.entity == b) { REQUIRE(i.amount == 6); } else { FAIL("unexpected scrap entity"); } } } // --------------------------------------------------------------------------- // Selection hit-testing (REQ-UI-SCRAP-CLICK-SELECT, REQ-UI-SCRAP-MULTI-SELECT) // --------------------------------------------------------------------------- TEST_CASE("scrapAtWorldPos returns the pile near a point and null when far", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity e = ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); // Extra parens keep Catch from decomposing the comparison, which is ambiguous // between Catch's expression templates and entt's entity operator==. REQUIRE((scrapAtWorldPos(admin, QVector2D(3.1f, 4.0f)) == e)); REQUIRE((scrapAtWorldPos(admin, QVector2D(10.0f, 10.0f)) == entt::null)); } TEST_CASE("scrapAtWorldPos returns the nearest of several piles", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity near = ss.spawn(QVector2D(2.0f, 2.0f), 1, 100); ss.spawn(QVector2D(2.4f, 2.0f), 1, 100); REQUIRE((scrapAtWorldPos(admin, QVector2D(2.05f, 2.0f)) == near)); } TEST_CASE("entityAtWorldPos never returns a scrap pile", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); ss.spawn(QVector2D(3.0f, 4.0f), 5, 100); // Scrap has no HealthComponent, so the actor hit-test ignores it entirely. REQUIRE((entityAtWorldPos(admin, QVector2D(3.0f, 4.0f)) == entt::null)); } TEST_CASE("scrapInBox returns exactly the piles inside the tile rectangle", "[scrap]") { EntityAdmin admin; ScrapSystem ss(admin); const entt::entity inA = ss.spawn(QVector2D(1.2f, 2.7f), 1, 100); // tile (1,2) const entt::entity inB = ss.spawn(QVector2D(4.9f, 5.1f), 1, 100); // tile (4,5) const entt::entity outX = ss.spawn(QVector2D(10.0f, 10.0f), 1, 100); // Box given in reversed corner order to confirm normalization. const std::vector hit = scrapInBox(admin, QPoint(5, 5), QPoint(0, 0)); REQUIRE(hit.size() == 2); REQUIRE(contains(hit, inA)); REQUIRE(contains(hit, inB)); REQUIRE_FALSE(contains(hit, outX)); } TEST_CASE("actorsInBox returns living ships and stations, excluding scrap and dead actors", "[actor]") { EntityAdmin admin; // Two living ships inside the box: one player, one enemy. const entt::entity playerShip = admin.spawnShip( QVector2D(1.5f, 2.5f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f, "fighter", false); // tile (1,2) const entt::entity enemyShip = admin.spawnShip( QVector2D(4.2f, 5.8f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f, "raider", true); // tile (4,5) // A dead ship inside the box is excluded. const entt::entity deadShip = admin.spawnShip( QVector2D(3.0f, 3.0f), 0.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f, "fighter", false); // A ship outside the box is excluded. const entt::entity outsideShip = admin.spawnShip( QVector2D(20.0f, 20.0f), 100.0f, 100.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 5.0f, "fighter", false); // A station is included when any body cell lies inside the box. const std::vector stationCells{ QPoint(2, 2), QPoint(3, 2) }; const entt::entity station = admin.spawnStation( QPoint(2, 2), QSize(2, 1), stationCells, 200.0f, 200.0f, true); // Scrap and the HQ proxy are never actors. admin.spawnScrap(QVector2D(1.0f, 1.0f), 5, Tick(1000)); admin.spawnHqProxy(QVector2D(0.5f, 0.5f), 500.0f, 500.0f); const std::vector hit = actorsInBox(admin, QPoint(5, 5), QPoint(0, 0)); REQUIRE(hit.size() == 3); REQUIRE(contains(hit, playerShip)); REQUIRE(contains(hit, enemyShip)); REQUIRE(contains(hit, station)); REQUIRE_FALSE(contains(hit, deadShip)); REQUIRE_FALSE(contains(hit, outsideShip)); }