diff --git a/tests/testPortal.cpp b/tests/testPortal.cpp new file mode 100644 index 0000000..656518a --- /dev/null +++ b/tests/testPortal.cpp @@ -0,0 +1,161 @@ +#include "testHarness.h" +#include "vf/Portal.h" + +#include + +using namespace vf; + +namespace { + +// Lays an obsidian ring by hand, corners included, so the scanner has to work +// the geometry out rather than being handed a PortalFrame back. +PortalFrame layFrame(World& world, const Vec3i& origin, int width, int height, bool alongX) { + PortalFrame frame; + frame.origin = origin; + frame.width = width; + frame.height = height; + frame.alongX = alongX; + + for (int across = -1; across <= width; ++across) { + for (int up = -1; up <= height; ++up) { + Vec3i cell = frame.interiorBlock(across, up); + bool ring = across == -1 || across == width || up == -1 || up == height; + world.setBlock(cell.x, cell.y, cell.z, ring ? Block::Obsidian : Block::Air); + } + } + return frame; +} + +void chargeTraveller(PortalNetwork& network, int entityId) { + for (int tick = 0; tick < network.requiredStandingTicks(); ++tick) + network.tickEntity(entityId, true); +} + +int countInterior(const World& world, const PortalFrame& frame, Block block) { + int total = 0; + for (int across = 0; across < frame.width; ++across) + for (int up = 0; up < frame.height; ++up) { + Vec3i cell = frame.interiorBlock(across, up); + if (world.blockAt(cell.x, cell.y, cell.z) == block) ++total; + } + return total; +} + +} // namespace + +VF_TEST(portalScanFindsTheSmallestLegalObsidianFrame) { + World world; + layFrame(world, {4, 40, 4}, 2, 3, true); + + PortalScan scan = PortalScan::detect(world, {5, 41, 4}, DimensionId::Overworld); + CHECK(scan.found); + CHECK_EQ(scan.frame.width, 2); + CHECK_EQ(scan.frame.height, 3); + CHECK(scan.frame.alongX); + CHECK_EQ(scan.frame.origin.x, 4); + CHECK_EQ(scan.frame.origin.y, 40); + CHECK_EQ(scan.frame.origin.z, 4); + CHECK_EQ(scan.interiorArea, 6); + CHECK_EQ(static_cast(scan.frame.dimension), static_cast(DimensionId::Overworld)); +} + +VF_TEST(portalScanReadsFramesStandingInTheZPlane) { + World world; + layFrame(world, {-3, 20, 7}, 3, 4, false); + + PortalScan scan = PortalScan::detect(world, {-3, 22, 8}, DimensionId::Nether); + CHECK(scan.found); + CHECK(!scan.frame.alongX); + CHECK_EQ(scan.frame.width, 3); + CHECK_EQ(scan.frame.height, 4); + CHECK_EQ(scan.frame.origin.z, 7); + CHECK_EQ(scan.frame.origin.x, -3); + CHECK_EQ(static_cast(scan.frame.dimension), static_cast(DimensionId::Nether)); +} + +VF_TEST(portalScanAcceptsAWideFrameUpToTheLimit) { + World world; + layFrame(world, {0, 30, 0}, 21, 21, true); + PortalScan wide = PortalScan::detect(world, {10, 40, 0}, DimensionId::Overworld); + CHECK(wide.found); + CHECK_EQ(wide.frame.width, 21); + CHECK_EQ(wide.frame.height, 21); + + World oversized; + layFrame(oversized, {0, 30, 0}, 22, 3, true); + CHECK(!PortalScan::detect(oversized, {10, 31, 0}, DimensionId::Overworld).found); +} + +VF_TEST(portalScanRejectsFramesUnderTheMinimumSize) { + World narrow; + layFrame(narrow, {0, 30, 0}, 1, 3, true); + CHECK(!PortalScan::detect(narrow, {0, 31, 0}, DimensionId::Overworld).found); + + World squat; + layFrame(squat, {0, 30, 0}, 2, 2, true); + CHECK(!PortalScan::detect(squat, {0, 31, 0}, DimensionId::Overworld).found); +} + +VF_TEST(portalScanIgnoresMissingCornersButNotMissingSides) { + World world; + PortalFrame frame = layFrame(world, {4, 40, 4}, 2, 3, true); + + Vec3i corner = frame.interiorBlock(-1, -1); + world.setBlock(corner.x, corner.y, corner.z, Block::Air); + CHECK(PortalScan::detect(world, {5, 41, 4}, DimensionId::Overworld).found); + + Vec3i side = frame.leftColumn(1); + world.setBlock(side.x, side.y, side.z, Block::Air); + CHECK(!PortalScan::detect(world, {5, 41, 4}, DimensionId::Overworld).found); +} + +VF_TEST(portalScanRefusesAFrameWithSomethingStuckInTheOpening) { + World world; + layFrame(world, {4, 40, 4}, 2, 3, true); + world.setBlock(5, 41, 4, Block::Stone); + + CHECK(!PortalScan::detect(world, {4, 40, 4}, DimensionId::Overworld).found); +} + +VF_TEST(lightingAFrameFillsExactlyItsOpening) { + World world; + PortalFrame frame = layFrame(world, {4, 40, 4}, 3, 4, true); + + CHECK_EQ(PortalScan::light(world, frame), 12); + CHECK_EQ(countInterior(world, frame, Block::NetherPortal), 12); + CHECK_EQ(PortalScan::light(world, frame), 0); + + Vec3i outside = frame.interiorBlock(0, 0); + CHECK_EQ(world.blockAt(outside.x, outside.y, outside.z - 1), Block::Air); + Vec3i ringBlock = frame.bottomRow(0); + CHECK_EQ(world.blockAt(ringBlock.x, ringBlock.y, ringBlock.z), Block::Obsidian); + + CHECK_EQ(PortalScan::douse(world, frame), 12); + CHECK_EQ(countInterior(world, frame, Block::Air), 12); +} + +VF_TEST(breakingTheFrameUnmakesTheLitPortal) { + DimensionSet dimensions(77u); + PortalNetwork network(dimensions); + World& world = dimensions.overworld().world(); + + PortalFrame frame = layFrame(world, {4, 40, 4}, 2, 3, true); + int id = network.ignite(DimensionId::Overworld, {4, 41, 4}); + CHECK(id >= 0); + CHECK_EQ(countInterior(world, frame, Block::NetherPortal), 6); + CHECK_EQ(network.activeCountIn(DimensionId::Overworld), 1); + + // A missing corner is cosmetic; the portal survives a validation sweep. + Vec3i corner = frame.interiorBlock(-1, 3); + world.setBlock(corner.x, corner.y, corner.z, Block::Air); + CHECK_EQ(network.validate(DimensionId::Overworld), 0); + CHECK_EQ(countInterior(world, frame, Block::NetherPortal), 6); + + Vec3i side = frame.topRow(1); + world.setBlock(side.x, side.y, side.z, Block::Air); + CHECK_EQ(network.validate(DimensionId::Overworld), 1); + CHECK_EQ(countInterior(world, frame, Block::NetherPortal), 0); + CHECK_EQ(network.activeCountIn(DimensionId::Overworld), 0); + CHECK(!network.portal(id)->active); +} +