Volver al índice

src/main/java/ar/com/companeros/horror/ParasiteClearanceTests.java

package ar.com.companeros.horror;

import ar.com.companeros.Companeros;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.gametest.framework.*;
import net.minecraft.nbt.*;
import net.minecraft.world.entity.MoverType;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.TrapDoorBlock;
import net.minecraft.world.level.block.state.properties.Half;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.gametest.*;

/** Altura, túneles y escotillas se verifican con ticks y colisiones del servidor real. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class ParasiteClearanceTests {
    @GameTest(batch = "horror_04_pose_preferences", template = "empty", timeoutTicks = 75)
    public void lunaPostureHintsRequireRealSupportsAndExpire(GameTestHelper helper) {
        Fixture fixture = new Fixture(helper); ParasiteEntity body = fixture.body;
        after(helper, fixture, 2, () -> {
            helper.assertTrue(!body.bodyPoseEnvironment().wallAvailable()
                && !body.requestBodyPosture("wall_brace", 20), "Una sugerencia no fabrica una pared en un espacio abierto");
            helper.assertTrue(body.requestBodyPosture("all_fours", 20), "Los cuatro apoyos pueden elegirse sobre suelo real");
            helper.assertTrue(!body.isCrawling() && body.getBodyPosture() == ParasiteEntity.BodyPosture.ALL_FOURS,
                "Los cuatro apoyos no se confunden con el arrastre de un bloque");
        });
        after(helper, fixture, 16, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 1.85) < .003, "La preferencia cambia también el volumen físico de forma gradual");
            helper.setBlock(new BlockPos(3, 3, 4), Blocks.STONE);
            helper.assertTrue(body.bodyPoseEnvironment().wallAvailable() && body.requestBodyPosture("wall_brace", 20),
                "Una pared lateral dentro del alcance habilita el apoyo preferido");
        });
        after(helper, fixture, 19, () -> helper.setBlock(new BlockPos(3, 3, 4), Blocks.AIR));
        after(helper, fixture, 21, () -> {
            helper.assertTrue(body.getBodyPosture() == ParasiteEntity.BodyPosture.AUTO,
                "Al quitar el bloque vuelve a auto y deja de pedir un apoyo en el aire");
            helper.assertTrue(body.requestBodyPosture("crawl", 20), "El arrastre sigue disponible con suelo");
        });
        after(helper, fixture, 44, () -> helper.assertTrue(body.getBodyPosture() == ParasiteEntity.BodyPosture.AUTO,
            "Las preferencias caducan aunque Luna no vuelva a responder"));
        // Caduca en tick 41, conserva el último plegado 8 ticks y necesita 12 para incorporarse.
        // La aserción espera ambas transiciones físicas; no exige que el collider salte de altura.
        after(helper, fixture, 65, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 3.6) < .003 && helper.getLevel().noCollision(body, body.getBoundingBox()),
                "Sin restricciones vuelve a la postura normal sin teletransportarse ni atravesar bloques");
            fixture.close(); helper.succeed();
        });
    }

    @GameTest(batch = "horror_04_clearance_graded", template = "empty", timeoutTicks = 110)
    public void bodyUsesAvailableHeightRatherThanAlwaysFlattening(GameTestHelper helper) {
        Fixture fixture = new Fixture(helper); ParasiteEntity body = fixture.body;
        helper.setBlock(new BlockPos(2, 4, 4), Blocks.STONE);
        after(helper, fixture, 25, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 1.85) < .002 && helper.getLevel().noCollision(body, body.getBoundingBox()),
                "Un paso de dos bloques conserva una postura de 1.85 bloques");
            helper.setBlock(new BlockPos(2, 4, 4), Blocks.AIR); helper.setBlock(new BlockPos(2, 5, 4), Blocks.STONE);
        });
        after(helper, fixture, 55, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 2.85) < .002, "Un paso de tres bloques permite incorporarse parcialmente");
            helper.setBlock(new BlockPos(2, 5, 4), Blocks.AIR);
        });
        after(helper, fixture, 85, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 3.6) < .002, "Cuatro bloques de altura permiten la postura completa");
            fixture.close(); helper.succeed();
        });
    }
    @GameTest(batch = "horror_05_clearance_height", template = "empty", timeoutTicks = 150)
    public void bodyFoldsGraduallyAndNeverExpandsThroughCeiling(GameTestHelper helper) {
        Fixture fixture = new Fixture(helper);
        ParasiteEntity body = fixture.body;
        helper.assertTrue(Math.abs(body.getBbHeight() - 3.6F) < .001 && Math.abs(body.getBbWidth() - .6F) < .001,
            "El cuerpo final mide 3.6 bloques y conserva su ancho físico");
        body.setBodyPosture(ParasiteEntity.BodyPosture.CRAWL);
        after(helper, fixture, 3, () -> {
            helper.assertTrue(body.getCompactProgress(1) > 0 && body.getCompactProgress(1) < 1 && body.getBbHeight() > .85,
                "La postura cambia durante varios ticks, sin un encogimiento instantáneo");
        });
        after(helper, fixture, 15, () -> {
            helper.assertTrue(body.getCompactProgress(1) == 1 && Math.abs(body.getBbHeight() - .85) < .001,
                "Al terminar de plegarse cabe físicamente en un pasaje de un bloque");
            helper.setBlock(new BlockPos(2, 3, 4), Blocks.STONE);
            body.setBodyPosture(ParasiteEntity.BodyPosture.HUNCHED);
        });
        after(helper, fixture, 40, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - .85) < .001 && helper.getLevel().noCollision(body, body.getBoundingBox()),
                "No se incorpora dentro del techo aunque la intención pida la postura normal");
            CompoundTag saved = new CompoundTag(); body.addAdditionalSaveData(saved);
            ParasiteEntity restored = HorrorModule.STALKER.get().create(helper.getLevel());
            restored.readAdditionalSaveData(saved);
            helper.assertTrue(restored.isCompact() && Math.abs(restored.getBbHeight() - .85) < .001,
                "La recarga conserva la postura física compacta");
            helper.setBlock(new BlockPos(2, 3, 4), Blocks.AIR);
        });
        after(helper, fixture, 66, () -> {
            helper.assertTrue(!body.isCompact() && Math.abs(body.getBbHeight() - 3.6) < .001,
                "Al recuperar espacio se incorpora gradualmente hasta su altura original");
            var human = new net.minecraft.server.level.ServerPlayer(helper.getLevel().getServer(), helper.getLevel(),
                new com.mojang.authlib.GameProfile(UUID.randomUUID(), "LowStalkWitness"));
            human.moveTo(body.getX(), body.getY(), body.getZ() + 6, 180, 0);
            body.setTarget(human); body.setIntent(ParasiteEntity.Intent.STALK, 120);
            body.pauseForTool(120); body.tickCount = 60;
        });
        after(helper, fixture, 86, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 1.85) < .003
                && helper.getLevel().noCollision(body, body.getBoundingBox()),
                "El acecho en cuatro apoyos reduce realmente el volumen físico, sin atravesar el suelo");
            var witness = (net.minecraft.server.level.ServerPlayer) body.getTarget();
            HorrorState.get(helper.getLevel().getServer()).sensitivity(witness.getUUID(), true);
            body.excludeSensitivePlayer(witness);
            helper.assertTrue(body.getTarget() == null, "El modo sensible también cancela el acecho bajo");
        });
        after(helper, fixture, 120, () -> {
            helper.assertTrue(Math.abs(body.getBbHeight() - 3.6) < .003,
                "Sin un humano elegible abandona la postura de acecho y recupera su altura");
            fixture.close(); helper.succeed();
        });
    }

    @GameTest(batch = "horror_06_clearance_open", template = "empty", timeoutTicks = 230)
    public void navigationFoldsBeforeEnteringAndPassesAnOpenTrapdoor(GameTestHelper helper) {
        Fixture fixture = new Fixture(helper); corridor(helper, true);
        ParasiteEntity body = fixture.body;
        BlockPos destination = helper.absolutePos(new BlockPos(7, 2, 4));
        after(helper, fixture, 2, () -> {
            helper.assertTrue(body.onGround(), "La física apoya el cuerpo recién creado antes de pedir una ruta terrestre");
            helper.assertTrue(body.getNavigation().moveTo(destination.getX() + .5, destination.getY(), destination.getZ() + .5, 1.35),
                "La navegación encuentra una ruta que exige plegarse ante una escotilla abierta");
            helper.assertTrue(body.getNavigation().getPath().canReach(), "La ruta llega al destino, sin detenerse antes de la escotilla");
        });
        boolean[] compactObserved = { false };
        helper.onEachTick(() -> {
            if (fixture.closed) return;
            try {
                helper.assertTrue(helper.getLevel().noCollision(body, body.getBoundingBox().deflate(.00001)),
                    "Cada tick respeta el techo, los muros y la placa vertical de la escotilla");
                if (body.getX() > helper.absolutePos(new BlockPos(3, 2, 4)).getX() && body.getX() < destination.getX() - 1)
                    compactObserved[0] |= body.getBbHeight() <= .86;
                helper.assertTrue(Math.abs(body.getY() - destination.getY()) < .05,
                    "El recorrido plano no usa saltos ni flotación para atravesar el túnel");
                // La navegación considera alcanzado el último nodo dentro de su radio de llegada.
                if (body.getNavigation().isDone() && body.position().distanceToSqr(Vec3.atBottomCenterOf(destination)) <= .75 * .75) {
                    helper.assertTrue(compactObserved[0], "La criatura recorrió el tramo bajo con su cuerpo físico plegado");
                    fixture.close(); helper.succeed();
                }
            } catch (Throwable failure) { fixture.close(); throw failure; }
        });
        after(helper, fixture, 210, () -> {
            var path = body.getNavigation().getPath();
            String detail = "posición=" + body.position() + ", altura=" + body.getBbHeight()
                + ", plegado=" + body.getCompactProgress(1) + ", velocidad=" + body.getSpeed()
                + ", ruta=" + (path == null ? "null" : path.getNextNodeIndex() + "/" + path.getNodeCount())
                + ", siguiente=" + ((ParasiteNavigation) body.getNavigation()).nextWaypoint();
            fixture.close(); helper.fail("No completó el túnel abierto: " + detail);
        });
    }

    @GameTest(batch = "horror_07_clearance_closed", template = "empty", timeoutTicks = 75)
    public void aClosedTrapdoorAndSolidBlockRemainRealObstacles(GameTestHelper helper) {
        Fixture fixture = new Fixture(helper); corridor(helper, false);
        ParasiteEntity body = fixture.body;
        BlockPos obstacle = helper.absolutePos(new BlockPos(4, 2, 4));
        BlockPos destination = helper.absolutePos(new BlockPos(7, 2, 4));
        after(helper, fixture, 2, () -> {
            helper.assertTrue(body.onGround(), "Se evalúa una ruta después del contacto físico con el suelo");
            var route = body.getNavigation().createPath(destination, 0);
            helper.assertTrue(route == null || !route.canReach(), "La escotilla cerrada no se ofrece como ruta completa");
        });
        body.setBodyPosture(ParasiteEntity.BodyPosture.CRAWL);
        after(helper, fixture, 15, () -> {
            body.moveTo(obstacle.getX() - .5, obstacle.getY(), obstacle.getZ() + .5, 0, 0);
            for (int step = 0; step < 20; step++) body.move(MoverType.SELF, new Vec3(.2, 0, 0));
            helper.assertTrue(body.getX() < obstacle.getX() && helper.getLevel().noCollision(body, body.getBoundingBox().deflate(.00001)),
                "Incluso al intentar avanzar, la placa cerrada detiene el movimiento físico");
            helper.setBlock(new BlockPos(4, 2, 4), Blocks.STONE);
            for (int step = 0; step < 20; step++) body.move(MoverType.SELF, new Vec3(.2, 0, 0));
            helper.assertTrue(body.getX() < obstacle.getX() && !body.noPhysics
                && helper.getLevel().noCollision(body, body.getBoundingBox().deflate(.00001)),
                "Un bloque sólido también detiene al cuerpo sin activar noclip ni atravesarlo");
            fixture.close(); helper.succeed();
        });
    }

    // Cámara de ensayo de una sola entrada: no existen rodeos abiertos ni un techo escalable.
    private static void corridor(GameTestHelper helper, boolean open) {
        for (int x = 0; x <= 9; x++) for (int y = 2; y <= 6; y++) {
            helper.setBlock(new BlockPos(x, y, 3), Blocks.STONE);
            helper.setBlock(new BlockPos(x, y, 5), Blocks.STONE);
        }
        for (int y = 2; y <= 6; y++) {
            helper.setBlock(new BlockPos(0, y, 4), Blocks.STONE);
            helper.setBlock(new BlockPos(9, y, 4), Blocks.STONE);
        }
        for (int x = 3; x <= 5; x++) for (int y = 3; y <= 6; y++) helper.setBlock(new BlockPos(x, y, 4), Blocks.STONE);
        helper.setBlock(new BlockPos(4, 2, 4), Blocks.OAK_TRAPDOOR.defaultBlockState()
            .setValue(TrapDoorBlock.OPEN, open).setValue(TrapDoorBlock.HALF, Half.BOTTOM).setValue(TrapDoorBlock.FACING, Direction.NORTH));
    }

    private static void after(GameTestHelper helper, Fixture fixture, int delay, Runnable action) {
        helper.runAfterDelay(delay, () -> {
            if (fixture.closed) return;
            try { action.run(); } catch (Throwable failure) { fixture.close(); throw failure; }
        });
    }

    private static final class Fixture implements AutoCloseable {
        final GameTestHelper helper; final HorrorState previous; final ParasiteEntity body; boolean closed;
        Fixture(GameTestHelper helper) {
            this.helper = helper; var server = helper.getLevel().getServer(); previous = HorrorState.get(server);
            for (int x = 0; x <= 10; x++) for (int z = 0; z <= 8; z++) helper.setBlock(new BlockPos(x, 1, z), Blocks.STONE);
            BlockPos feet = helper.absolutePos(new BlockPos(2, 2, 4));
            HorrorState state = new HorrorState(helper.getLevel().getGameTime());
            server.overworld().getDataStorage().set("raps_parasite", state);
            UUID host = UUID.randomUUID(); state.selectHost(host, "ClearanceHost", helper.getLevel().getGameTime());
            CompoundTag inventory = new CompoundTag(); inventory.put("Inventory", new ListTag()); inventory.put("EnderItems", new ListTag());
            state.prepareQuarantine(inventory, helper.getLevel().dimension().location().toString(), feet); state.commitTransfer(); state.created();
            body = HorrorModule.STALKER.get().create(helper.getLevel()); body.setUUID(state.entityId().orElseThrow()); body.setHost(host, "ClearanceHost");
            body.moveTo(feet.getX() + .5, feet.getY(), feet.getZ() + .5, 0, 0); body.setMutationStage(4);
            helper.assertTrue(helper.getLevel().addFreshEntity(body), "El cuerpo canónico existe en el mundo de prueba");
        }
        @Override public void close() {
            if (closed) return; closed = true; body.discard();
            helper.getLevel().getServer().overworld().getDataStorage().set("raps_parasite", previous);
        }
    }
}