Volver al índice
src/main/java/ar/com/companeros/horror/HorrorTests.java
package ar.com.companeros.horror;
import ar.com.companeros.Companeros;
import ar.com.companeros.WorkArea;
import ar.com.companeros.identity.*;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.*;
import net.minecraft.nbt.*;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.*;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.gametest.*;
/** Persistencia, exclusión de huéspedes y seguridad se prueban sin llamadas a un modelo. */
@GameTestHolder(Companeros.ID)
@PrefixGameTestTemplate(false)
public final class HorrorTests {
@GameTest(template = "empty", timeoutTicks = 20)
public void savedWorldClockAndSingleHostSurviveRestart(GameTestHelper helper) {
HorrorConfig config = new HorrorConfig(true, 100, 50, 20, 4, 32);
HorrorState initial = new HorrorState(500);
helper.assertTrue(!initial.meteorDue(599, config) && initial.meteorDue(600, config), "El meteorito espera tiempo guardado del mundo");
initial.landed(new BlockPos(120, 64, 120), 600);
HorrorState state = HorrorState.load(initial.save(new CompoundTag()));
helper.assertTrue(!state.meteorDue(10_000, config) && !state.infectionDue(649, config) && state.infectionDue(650, config),
"El reinicio no repite el meteorito ni adelanta la infección");
UUID host = UUID.randomUUID(); state.selectHost(host, "TestHost", 650); state.selectHost(host, "TestHost", 900);
helper.assertTrue(state.infectionTime() == 650 && !state.infectionDue(10_000, config), "La elección es idempotente y no contagia otro bot");
try { state.selectHost(UUID.randomUUID(), "OtherHost", 900); helper.fail("Debe rechazarse otro huésped"); }
catch (IllegalStateException expected) { }
state.forceStage(2);
HorrorState restarted = HorrorState.load(state.save(new CompoundTag()));
helper.assertTrue(restarted.host().orElseThrow().equals(host) && restarted.stage(650, config) == 2
&& restarted.stage(710, config) == 3 && restarted.stage(10_000, config) == 4,
"El huésped y las fases permanecen después del reinicio y alcanzan un máximo");
helper.succeed();
}
@GameTest(template = "empty", timeoutTicks = 20)
public void quarantineIsSealedAndTransferIdempotent(GameTestHelper helper) {
HorrorState state = new HorrorState(0); UUID id = UUID.randomUUID(); state.selectHost(id, "Iron", 10);
CompoundTag snapshot = new CompoundTag(); ListTag inventory = new ListTag();
CompoundTag stack = new ItemStack(Items.IRON_INGOT, 7).save(new CompoundTag()); stack.putByte("Slot", (byte) 0); inventory.add(stack);
snapshot.put("Inventory", inventory); snapshot.put("EnderItems", new ListTag());
state.prepareQuarantine(snapshot, "minecraft:overworld", new BlockPos(1, 64, 1));
inventory.clear();
helper.assertTrue(state.quarantine().getList("Inventory", Tag.TAG_COMPOUND).getCompound(0).getByte("Count") == 7,
"El depósito copia el inventario y no depende de una referencia mutable");
CompoundTag escaped = state.quarantine(); escaped.getList("Inventory", Tag.TAG_COMPOUND).clear();
state.commitTransfer(); state.commitTransfer();
HorrorState restored = HorrorState.load(state.save(new CompoundTag()));
helper.assertTrue(restored.transferred() && restored.quarantine().getList("Inventory", Tag.TAG_COMPOUND).size() == 1,
"No se duplican ni borran pertenencias al repetir el commit o reiniciar");
try { restored.prepareQuarantine(snapshot, "minecraft:overworld", BlockPos.ZERO); helper.fail("No se reemplaza un depósito sellado"); }
catch (IllegalStateException expected) { }
helper.succeed();
}
@GameTest(template = "empty", timeoutTicks = 20)
public void protectedMeteorSiteNeverDestroysBlocks(GameTestHelper helper) {
BlockPos center = helper.absolutePos(new BlockPos(1, 2, 1));
helper.getLevel().setBlock(center, Blocks.DIAMOND_BLOCK.defaultBlockState(), 3);
WorkArea.get(helper.getLevel()).protect(center);
helper.assertTrue(!Meteorite.land(helper.getLevel(), center, 32)
&& helper.getLevel().getBlockState(center).is(Blocks.DIAMOND_BLOCK), "El meteorito rechaza el lugar antes de tocar una construcción");
var materials = Meteorite.plan(center).values();
helper.assertTrue(materials.stream().anyMatch(block -> block.is(Blocks.RAW_IRON_BLOCK))
&& materials.stream().anyMatch(block -> block.is(Blocks.GRANITE))
&& materials.stream().anyMatch(block -> block.is(Blocks.DIORITE)), "El depósito planificado usa minerales físicos");
helper.succeed();
}
@GameTest(template = "empty", timeoutTicks = 20)
public void sensitivityPersistsAndCreatureNeverTargetsNonHumans(GameTestHelper helper) {
UUID player = UUID.randomUUID(); HorrorState state = new HorrorState(0); state.sensitivity(player, true);
HorrorState restored = HorrorState.load(state.save(new CompoundTag()));
helper.assertTrue(restored.sensitive(player), "El modo sensible queda ligado al UUID tras reiniciar");
restored.sensitivity(player, false);
helper.assertTrue(!restored.sensitive(player), "El jugador puede retractarse sin cambiar de identidad");
var pig = EntityType.PIG.create(helper.getLevel()); var entity = HorrorModule.STALKER.get().create(helper.getLevel());
helper.assertTrue(pig != null && entity != null, "Se crean cuerpos de prueba");
entity.setTarget(pig);
helper.assertTrue(entity.getTarget() == null && !entity.doHurtTarget(pig)
&& !HorrorModule.mayTarget(helper.getLevel().getServer(), pig), "Un animal o entidad bot jamás es un objetivo de terror");
helper.succeed();
}
@GameTest(template = "empty", timeoutTicks = 20)
public void safeHumanAndRegisteredBotRejectTargetsAndDamage(GameTestHelper helper) {
var server = helper.getLevel().getServer();
var human = warmedPlayer(helper, UUID.randomUUID(), "SafeHuman");
var entity = HorrorModule.STALKER.get().create(helper.getLevel());
helper.assertTrue(entity != null && HorrorModule.mayTarget(server, human), "Un humano normal es un objetivo posible");
float normalHealth = human.getHealth();
helper.assertTrue(human.hurt(entity.damageSources().mobAttack(entity), 3) && human.getHealth() < normalHealth,
"El ataque causa consecuencias físicas sobre un humano sin modo seguro");
entity.setTarget(human);
helper.assertTrue(entity.getTarget() == human, "La criatura puede elegir al humano sin modo seguro");
HorrorState.get(server).sensitivity(human.getUUID(), true);
entity.setTarget(human);
human.invulnerableTime = 0;
float before = human.getHealth();
helper.assertTrue(entity.getTarget() == null && !entity.doHurtTarget(human)
&& !human.hurt(entity.damageSources().mobAttack(entity), 6) && human.getHealth() == before,
"El modo seguro bloquea objetivo, golpe directo y daño despachado por eventos");
UUID botId = UUID.randomUUID(); var bot = warmedPlayer(helper, botId, "BotNoFear");
float initialBotHealth = bot.getHealth();
helper.assertTrue(bot.hurt(entity.damageSources().mobAttack(entity), 3) && bot.getHealth() < initialBotHealth,
"Antes de registrarse, ese mismo cuerpo recibe daño real como humano");
bot.invulnerableTime = 0;
AgentRegistry.get(server).register(botId, "BotNoFear", null, null, "quieto");
AgentRegistry.get(server).markState(botId, AgentState.PAUSED);
try {
entity.setTarget(bot);
helper.assertTrue(entity.getTarget() == null && !entity.doHurtTarget(bot)
&& !bot.hurt(entity.damageSources().mobAttack(entity), 6), "Los bots no reciben ataques ni daño del parásito");
helper.succeed();
} finally {
AgentRegistry.get(server).markState(botId, AgentState.RETIRED);
HorrorState.get(server).sensitivity(human.getUUID(), false);
}
}
@GameTest(template = "empty", timeoutTicks = 190)
public void realGazeStartsEncounterOnceAndOnlyARealHitAnimates(GameTestHelper helper) {
var server = helper.getLevel().getServer();
var human = warmedPlayer(helper, UUID.randomUUID(), "GazeHuman");
var entity = HorrorModule.STALKER.get().create(helper.getLevel());
helper.assertTrue(entity != null, "Existe un cuerpo para la escena");
BlockPos pos = helper.absolutePos(new BlockPos(3, 2, 3));
entity.moveTo(pos.getX() + .5, pos.getY(), pos.getZ() + .5, 0, 0);
human.moveTo(entity.getX(), entity.getY(), entity.getZ() + 6, 180, 0);
helper.assertTrue(!entity.tryBeginGazeEncounter(human), "La fase humana inicial no dispara terror");
entity.setMutationStage(2);
human.setYRot(0);
helper.assertTrue(!entity.tryBeginGazeEncounter(human), "Mirar en sentido contrario no activa la escena");
human.setYRot(180);
helper.assertTrue(entity.tryBeginGazeEncounter(human) && entity.getEncounterTicks() == 0
&& entity.getTarget() == human && human.getYRot() == 180,
"Una mirada visible inicia la escena sin alterar la cámara del jugador");
float health = human.getHealth();
helper.assertTrue(!entity.doHurtTarget(human) && human.getHealth() == health && entity.getAttackAnimationTicks() == 0,
"La contorsión inicial no daña al jugador ni simula golpes");
CompoundTag saved = new CompoundTag(); entity.addAdditionalSaveData(saved);
helper.assertTrue(saved.getLong("NextEncounterAt") >= helper.getLevel().getGameTime() + ParasiteEntity.ENCOUNTER_COOLDOWN,
"El encuentro conserva al menos cinco minutos de espera del mundo");
saved.putLong("EncounterStart", -1); saved.remove("EncounterAudience");
var restored = HorrorModule.STALKER.get().create(helper.getLevel());
restored.readAdditionalSaveData(saved);
restored.moveTo(entity.getX(), entity.getY(), entity.getZ(), 0, 0);
helper.assertTrue(!restored.tryBeginGazeEncounter(human) && restored.getEncounterTicks() == -1,
"El tiempo de espera permanece guardado y no repite la voz tras una recarga");
UUID botId = UUID.randomUUID();
var bot = warmedPlayer(helper, botId, "GazeBot");
bot.moveTo(human.getX(), human.getY(), human.getZ(), 180, 0);
AgentRegistry.get(server).register(botId, "GazeBot", null, null, "quieto");
AgentRegistry.get(server).markState(botId, AgentState.PAUSED);
try {
var excluded = HorrorModule.STALKER.get().create(helper.getLevel());
excluded.setMutationStage(2); excluded.moveTo(entity.getX(), entity.getY(), entity.getZ(), 0, 0);
helper.assertTrue(!excluded.tryBeginGazeEncounter(bot), "Un bot que mira a la criatura nunca dispara el encuentro");
HorrorState.get(server).sensitivity(human.getUUID(), true);
helper.assertTrue(!excluded.tryBeginGazeEncounter(human), "Un humano sensible tampoco dispara el encuentro");
} finally {
AgentRegistry.get(server).markState(botId, AgentState.RETIRED);
HorrorState.get(server).sensitivity(human.getUUID(), false);
}
helper.runAfterDelay(141, () -> {
human.moveTo(entity.getX(), entity.getY(), entity.getZ() + 1.6, 180, 0);
helper.assertTrue(!entity.doHurtTarget(human), "Terminar la introducción no fuerza un ataque sin tensión suficiente");
HorrorPacing pacing = HorrorPacing.get(server);
for (int provocation = 0; provocation < 3; provocation++) pacing.provoked(human.getUUID(), helper.getLevel().getGameTime());
entity.setIntent(ParasiteEntity.Intent.AMBUSH, 20);
helper.assertTrue(entity.getEncounterTicks() >= ParasiteEntity.ENCOUNTER_INTRO && entity.doHurtTarget(human)
&& human.getHealth() < health && entity.getAttackAnimationTicks() > 0,
"Después de la introducción, un golpe físico exitoso activa la animación breve");
helper.succeed();
});
}
@GameTest(batch = "horror_02_encounter", template = "empty", timeoutTicks = 30)
public void sensitivityImmediatelyCancelsActiveEncounterAndStopsAudio(GameTestHelper helper) {
var server = helper.getLevel().getServer();
HorrorState previous = HorrorState.get(server);
var entity = HorrorModule.STALKER.get().create(helper.getLevel());
try {
HorrorState state = new HorrorState(helper.getLevel().getGameTime());
server.overworld().getDataStorage().set("raps_parasite", state);
UUID host = UUID.randomUUID(); state.selectHost(host, "LoreHost", helper.getLevel().getGameTime());
CompoundTag inventory = new CompoundTag(); inventory.put("Inventory", new ListTag()); inventory.put("EnderItems", new ListTag());
BlockPos pos = helper.absolutePos(new BlockPos(3, 2, 3));
state.prepareQuarantine(inventory, helper.getLevel().dimension().location().toString(), pos); state.commitTransfer(); state.created();
entity.setUUID(state.entityId().orElseThrow()); entity.setHost(host, "LoreHost"); entity.setMutationStage(2);
entity.moveTo(pos.getX() + .5, pos.getY(), pos.getZ() + .5, 0, 0);
helper.assertTrue(helper.getLevel().addFreshEntity(entity), "El cuerpo canónico se añade al mundo");
var human = warmedPlayer(helper, UUID.randomUUID(), "LoreSafe");
human.moveTo(entity.getX(), entity.getY(), entity.getZ() + 6, 180, 0);
helper.assertTrue(entity.beginEncounter(human), "El humano sin sensibilidad inicia una escena real");
float before = human.getHealth();
HorrorModule.setSensitivity(human, true);
helper.assertTrue(entity.getTarget() == null && entity.getEncounterTicks() == -1 && !entity.isPursuing()
&& !entity.doHurtTarget(human) && human.getHealth() == before,
"Activar sensibilidad cancela inmediatamente la presa, la escena y cualquier golpe");
helper.assertTrue(((FixtureConnection) human.connection.connection).stoppedSounds == 6,
"La preferencia detiene voz, pasos, lore, ataque, trepada y respiración que ya estuvieran sonando");
helper.succeed();
} finally {
if (entity != null) entity.discard();
server.overworld().getDataStorage().set("raps_parasite", previous); HorrorModule.durableState(server);
}
}
// Tick oficial: expira la protección de entrada vanilla sin modificar campos privados.
private static net.minecraft.server.level.ServerPlayer warmedPlayer(GameTestHelper helper, UUID id, String name) {
var player = new net.minecraft.server.level.ServerPlayer(helper.getLevel().getServer(), helper.getLevel(),
new com.mojang.authlib.GameProfile(id, name));
player.connection = new net.minecraft.server.network.ServerGamePacketListenerImpl(helper.getLevel().getServer(),
new FixtureConnection(), player);
for (int tick = 0; tick < 61; tick++) player.tick();
return player;
}
private static final class FixtureConnection extends net.minecraft.network.Connection {
private int stoppedSounds;
private FixtureConnection() { super(net.minecraft.network.protocol.PacketFlow.SERVERBOUND); }
@Override public void send(net.minecraft.network.protocol.Packet<?> packet) {
if (packet instanceof net.minecraft.network.protocol.game.ClientboundStopSoundPacket) stoppedSounds++;
}
@Override public void send(net.minecraft.network.protocol.Packet<?> packet, net.minecraft.network.PacketSendListener listener) { send(packet); }
}
@GameTest(batch = "horror_01_transfer", template = "empty", timeoutTicks = 40)
public void realHostTransferSurvivesSavedStateAndNeverDuplicatesInventory(GameTestHelper helper) {
var server = helper.getLevel().getServer();
HorrorState previous = HorrorState.get(server);
UUID id = UUID.randomUUID(); String name = "Hp_" + id.toString().substring(0, 8);
ParasiteEntity parasite = null;
try {
server.overworld().getDataStorage().set("raps_parasite", new HorrorState(server.overworld().getGameTime()));
for (int x = 0; x <= 13; x++) for (int z = 0; z <= 13; z++) helper.setBlock(new BlockPos(x, 1, z), Blocks.DIRT);
BlockPos position = helper.absolutePos(new BlockPos(3, 2, 3));
AgentRegistry registry = AgentRegistry.get(server);
registry.register(id, name, null, net.minecraft.core.GlobalPos.of(helper.getLevel().dimension(), position), "quieto");
registry.markState(id, AgentState.ACTIVE);
var player = ar.com.companeros.body.PlayerSessions.create(server, id, name, helper.getLevel(), position);
player.getInventory().setItem(0, new ItemStack(Items.IRON_INGOT, 7));
player.getEnderChestInventory().setItem(0, new ItemStack(Items.DIAMOND, 2));
ar.com.companeros.memory.MemoryStore memory = ar.com.companeros.memory.MemoryStore.get(server);
UUID memoryId = UUID.randomUUID(); long now = System.currentTimeMillis();
memory.record(new ar.com.companeros.memory.MemoryRecord(memoryId, id, registry.worldId(),
net.minecraft.core.GlobalPos.of(helper.getLevel().dimension(), position), now, helper.getLevel().getGameTime(),
ar.com.companeros.memory.MemoryType.EXPERIENCE, 1, 1, now, helper.getLevel().getGameTime(),
ar.com.companeros.memory.MemoryOrigin.OBSERVED, "horror-fixture", java.util.List.of(), "Recuerdo anterior a la infección",
java.util.Set.of("identity"), java.util.Set.of(), null, true));
// Simula reinicio con depósito preparado pero inventario todavía intacto.
HorrorState pending = HorrorState.get(server);
pending.selectHost(id, name, helper.getLevel().getGameTime());
pending.prepareQuarantine(player.saveWithoutId(new CompoundTag()), helper.getLevel().dimension().location().toString(), position);
server.overworld().getDataStorage().set("raps_parasite", HorrorState.load(pending.save(new CompoundTag())));
HorrorModule.infect(server, registry.find(id).orElseThrow());
HorrorState committed = HorrorState.get(server);
parasite = HorrorModule.findParasite(server).orElseThrow(); UUID bodyId = parasite.getUUID();
helper.assertTrue(committed.transferred() && committed.entityCreated()
&& ar.com.companeros.body.PlayerSessions.find(server, id) == null
&& parasite.hostId().orElseThrow().equals(id), "El cuerpo único conserva el UUID de su huésped y cierra su sesión normal");
CompoundTag savedPlayer = NbtIo.readCompressed(server.getWorldPath(net.minecraft.world.level.storage.LevelResource.PLAYER_DATA_DIR).resolve(id + ".dat").toFile());
helper.assertTrue(savedPlayer.getList("Inventory", Tag.TAG_COMPOUND).isEmpty()
&& savedPlayer.getList("EnderItems", Tag.TAG_COMPOUND).isEmpty()
&& committed.quarantine().getList("Inventory", Tag.TAG_COMPOUND).getCompound(0).getByte("Count") == 7
&& committed.quarantine().getList("EnderItems", Tag.TAG_COMPOUND).getCompound(0).getByte("Count") == 2,
"El playerdata queda vacío y las pertenencias reales existen únicamente en cuarentena");
server.overworld().getDataStorage().set("raps_parasite", HorrorState.load(committed.save(new CompoundTag())));
HorrorModule.infect(server, registry.find(id).orElseThrow());
helper.assertTrue(HorrorModule.spawn(server).getUUID().equals(bodyId)
&& HorrorModule.findParasite(server).orElseThrow() == parasite && memory.find(memoryId).isPresent()
&& registry.find(id).orElseThrow().id().equals(id), "Repetir infección/entrada tras recarga no duplica cuerpo, objetos ni borra memoria");
helper.succeed();
} catch (java.io.IOException failure) { throw new IllegalStateException(failure); }
finally {
if (parasite != null) parasite.discard();
ar.com.companeros.body.PlayerSessions.remove(server, id);
if (AgentRegistry.get(server).find(id).isPresent()) AgentRegistry.get(server).markState(id, AgentState.RETIRED);
server.overworld().getDataStorage().set("raps_parasite", previous); HorrorModule.durableState(server);
}
}
}