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