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