Files
purpur-fantasy-stick/src/nativeTest/java/games/dmg/fantasystick/DungeonNativeTest.java
T
dmg a57b59f4f1
Release / release (push) Failing after 10m29s
fix(dungeon): acknowledge exported completion once
2026-09-27 01:24:54 -04:00

430 lines
73 KiB
Java

package games.dmg.fantasystick;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import games.dmg.fantasystick.adventure.AdventureQuestController;
import games.dmg.fantasystick.adventure.AdventureQuestDirectory;
import games.dmg.fantasystick.adventure.AdventureQuestState;
import games.dmg.fantasystick.dungeon.DungeonDirectory;
import games.dmg.fantasystick.dungeon.DungeonState;
import games.dmg.fantasystick.persistence.CharacterStorage;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.TimeUnit;
import org.bukkit.*;
import org.bukkit.entity.*;
import org.bukkit.event.entity.*;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.ItemStack;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.io.TempDir;
class DungeonNativeTest {
@TempDir Path data;
@BeforeAll static void bootstrap() throws Exception { NativeRuntime.bootstrap(); }
@Test void registeredDungeonTeleportUsesExactEntranceForDisabledAndEnabledDefinitions() throws Exception {
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\n");
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var admin=new FantasyStickLifecycleNativeTest.Actor(data);var destination=new FantasyStickLifecycleNativeTest.Actor(data);
f.players.put(admin.core.id,admin.player);f.players.put(destination.core.id,destination.player);
doReturn(true).when(admin.player).hasPermission("fantasystick.admin");
UUID startWorld=UUID.randomUUID(),targetWorld=UUID.randomUUID();when(admin.world.getUID()).thenReturn(startWorld);
when(destination.world.getUID()).thenReturn(targetWorld);when(destination.world.getName()).thenReturn("Dungeon Realm");
var directory=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");
var disabled=directory.create("Disabled Vault",targetWorld,34.75,66.5,18.25,91.5f,-7.25f).get(5,TimeUnit.SECONDS);
var enabled=directory.create("Enabled Depths",targetWorld,-20.5,55.25,-33.75,-135f,22f).get(5,TimeUnit.SECONDS);
var room=directory.addRadiusRoom(enabled.id(),"Boss",-20,54,-34,2,3).get(5,TimeUnit.SECONDS);
directory.setBoss(enabled.id(),room.id(),EntityType.HUSK,"Keeper",-20,55,-34,0,0).get(5,TimeUnit.SECONDS);
directory.setCompletionReward(enabled.id(),BigInteger.ZERO,BigInteger.ZERO).get(5,TimeUnit.SECONDS);
enabled=directory.enable(enabled.id(),List.of()).get(5,TimeUnit.SECONDS);
var before=directory.snapshot();var teleports=new ArrayList<Location>();
doReturn(java.util.concurrent.CompletableFuture.completedFuture(mock(Chunk.class))).when(destination.world).getChunkAtAsync(anyInt(),anyInt());
doAnswer(call->{teleports.add(((Location)call.getArgument(0)).clone());return java.util.concurrent.CompletableFuture.completedFuture(true);})
.when(admin.player).teleportAsync(any(Location.class));
assertTrue(f.command.tabComplete(admin.player,"fantasystick",new String[]{"dungeon",""}).contains("teleport"));
assertEquals(List.of(disabled.id().toString(),enabled.id().toString()).stream().sorted().toList(),
f.command.tabComplete(admin.player,"fantasystick",new String[]{"dungeon","teleport",""}));
f.command.execute(admin.player,"fantasystick",new String[]{"dungeon","teleport",disabled.id().toString()});drain(f);drain(f);
var first=teleports.getFirst();assertSame(destination.world,first.getWorld());assertEquals(34.75,first.getX());assertEquals(66.5,first.getY());assertEquals(18.25,first.getZ());assertEquals(91.5f,first.getYaw());assertEquals(-7.25f,first.getPitch());
assertEquals("Teleported to Disabled Vault (dungeon entrance) at Dungeon Realm (34, 66, 18).",admin.messages.getLast());
f.command.execute(admin.player,"fantasystick",new String[]{"dungeon","teleport",enabled.id().toString()});drain(f);drain(f);
var second=teleports.getLast();assertEquals(-20.5,second.getX());assertEquals(55.25,second.getY());assertEquals(-33.75,second.getZ());assertEquals(-135f,second.getYaw());assertEquals(22f,second.getPitch());
assertTrue(admin.messages.getLast().contains("Enabled Depths (dungeon entrance) at Dungeon Realm (-21, 55, -34)"));
assertEquals(before,directory.snapshot());
verify(destination.world).getChunkAtAsync(2,1);verify(destination.world).getChunkAtAsync(-2,-3);
}
}
@SuppressWarnings({"unchecked","rawtypes"})
@Test void registeredAuthoringKillRewardsAndCompletionTrophiesAreConnected() throws Exception {
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\nhostile-coin-drop-min: 6\nhostile-coin-drop-max: 6\n");
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);doReturn(true).when(actor.player).hasPermission("fantasystick.admin");
var actorEffects=new HashMap<org.bukkit.potion.PotionEffectType,org.bukkit.potion.PotionEffect>();doAnswer(call->Set.copyOf(actorEffects.values())).when(actor.player).getActivePotionEffects();doAnswer(call->{var effect=(org.bukkit.potion.PotionEffect)call.getArgument(0);actorEffects.put(effect.getType(),effect);return true;}).when(actor.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());doAnswer(call->{actorEffects.remove((org.bukkit.potion.PotionEffectType)call.getArgument(0));return null;}).when(actor.player).removePotionEffect(any(org.bukkit.potion.PotionEffectType.class));UUID worldId=UUID.randomUUID();when(actor.world.getUID()).thenReturn(worldId);when(actor.world.getName()).thenReturn("Dungeon Realm");when(actor.world.isChunkLoaded(anyInt(),anyInt())).thenReturn(true);
var spawned=new ArrayList<LivingEntity>();
doAnswer(call->{Class<?> type=call.getArgument(1);LivingEntity entity=mock(type.asSubclass(LivingEntity.class));var pdc=new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry());when(entity.getPersistentDataContainer()).thenReturn(pdc);when(entity.getUniqueId()).thenReturn(UUID.randomUUID());when(entity.getType()).thenReturn(type==Husk.class?EntityType.HUSK:type==Villager.class?EntityType.VILLAGER:EntityType.VINDICATOR);when(entity.getWorld()).thenReturn(actor.world);when(entity.getLocation()).thenReturn(((Location)call.getArgument(0)).clone());((java.util.function.Consumer<LivingEntity>)call.getArgument(2)).accept(entity);spawned.add(entity);return entity;}).when(actor.world).spawn(any(Location.class),any(Class.class),any(java.util.function.Consumer.class));
when(actor.world.getEntities()).thenAnswer(call->new ArrayList<Entity>(spawned));
assertEquals(List.of("dungeon"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"dung"}));var denied=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(denied.core.id,denied.player);doReturn(false).when(denied.player).hasPermission("fantasystick.admin");f.command.execute(denied.player,"fantasystick",new String[]{"dungeon","create","Forbidden"});assertTrue(denied.messages.getLast().contains("Permission denied"));assertTrue(f.command.tabComplete(denied.player,"fantasystick",new String[]{"dungeon",""}).isEmpty());
command(f,actor,"dungeon","create","Sunken","Keep");var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");var dungeon=dungeons.snapshot().dungeons().getFirst();command(f,actor,"dungeon","reward",dungeon.id().toString());assertTrue(actor.messages.getLast().contains("not configured"));
command(f,actor,"dungeon","edit","start",dungeon.id().toString());var dungeonController=(games.dmg.fantasystick.dungeon.DungeonController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeonController");await(f,()->dungeonController.editing(actor.core.id).isPresent(),actor.messages);
command(f,actor,"dungeon","room","create","corner","4","4","3","Entry");await(f,()->!dungeons.find(dungeon.id()).orElseThrow().rooms().isEmpty(),actor.messages);var first=dungeons.find(dungeon.id()).orElseThrow().rooms().getFirst();
command(f,actor,"dungeon","room","mob","add","husk","Guard A");command(f,actor,"dungeon","room","mob","add","husk","Guard B");command(f,actor,"dungeon","room","effect","add","players","speed","1","false");
actor.core.location.setX(actor.core.location.getBlockX()+4.2);
command(f,actor,"dungeon","room","create","corner","3","4","3","Boss");var bossRoom=dungeons.find(dungeon.id()).orElseThrow().rooms().getLast();
command(f,actor,"dungeon","reward",dungeon.id().toString(),"30","5");command(f,actor,"dungeon","reward",dungeon.id().toString());assertTrue(actor.messages.getLast().contains("30 XP and 5 coins"));assertEquals(List.of("30"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"dungeon","reward",dungeon.id().toString(),"3"}));assertEquals(List.of("5"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"dungeon","reward",dungeon.id().toString(),"30","5"}));f.command.execute(actor.player,"fantasystick",new String[]{"dungeon","room","reward","set","99","99"});assertTrue(actor.messages.getLast().contains("Usage:"));assertEquals(BigInteger.valueOf(30),dungeons.find(dungeon.id()).orElseThrow().completionReward().xp());command(f,actor,"dungeon","room","boss","mob","vindicator","Keeper");actor.core.location.setX(actor.core.location.getX()+3.2);command(f,actor,"dungeon","room","create","corner","2","4","3","Skipped");var skipped=dungeons.find(dungeon.id()).orElseThrow().rooms().getLast();
actor.particleCalls.clear();pulse(f);assertFalse(actor.particleCalls.isEmpty());assertTrue(actor.particleCalls.size()<=1024);assertTrue(actor.particleCalls.stream().allMatch(call->call[0]==Particle.DUST));var particlePreferences=(games.dmg.fantasystick.persistence.PlayerFeedbackPreferences)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"feedbackPreferences");assertTrue(particlePreferences.publish(particlePreferences.setAllParticles(actor.core.id,false).toCompletableFuture().get(5,TimeUnit.SECONDS)));actor.particleCalls.clear();pulse(f);assertTrue(actor.particleCalls.isEmpty(),"All-particle opt-out suppresses dungeon-editor boundaries");assertTrue(particlePreferences.publish(particlePreferences.setAllParticles(actor.core.id,true).toCompletableFuture().get(5,TimeUnit.SECONDS)));var outsider=new FantasyStickLifecycleNativeTest.Actor(data);outsider.core.location.setWorld(actor.world);f.players.put(outsider.core.id,outsider.player);doReturn(true).when(outsider.player).hasPermission("fantasystick.admin");command(f,outsider,"dungeon","edit","start",dungeon.id().toString());assertTrue(outsider.messages.getLast().contains("editing lease"));f.command.execute(outsider.player,"fantasystick",new String[]{"dungeon","reward",dungeon.id().toString(),"90","90"});assertTrue(outsider.messages.getLast().contains("another administrator"));assertEquals(BigInteger.valueOf(30),dungeons.find(dungeon.id()).orElseThrow().completionReward().xp());assertTrue(outsider.particleCalls.isEmpty());
command(f,actor,"dungeon","edit","done");actor.particleCalls.clear();pulse(f);assertTrue(actor.particleCalls.isEmpty());command(f,actor,"dungeon","enable",dungeon.id().toString());command(f,actor,"dungeon","open",dungeon.id().toString());
var adventures=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");assertTrue(adventures.snapshot().catalog().sites().contains(dungeon.id()));assertEquals(new AdventureQuestDirectory.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5)),adventures.reward(adventures.findSite(dungeon.id()).orElseThrow()));
f.activate(actor);var questController=(AdventureQuestController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestController");questController.open(actor.player);drain(f);var menu=actor.inventories.getLast();assertEquals(Material.PAPER,menu.getItem(0).getType());var offerLore=menu.getItem(0).getItemMeta().lore().toString();assertTrue(offerLore.contains("Completion trophy pool: 30 XP, 5 coins"));assertTrue(offerLore.contains("additional variable XP, coins and native loot"));var click=mock(InventoryClickEvent.class);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getInventory()).thenReturn(menu);when(click.getCurrentItem()).thenReturn(menu.getItem(0));questController.click(click);drain(f);assertEquals(dungeon.id(),adventures.active().getFirst().site());var partyA=new FantasyStickLifecycleNativeTest.Actor(data);var partyB=new FantasyStickLifecycleNativeTest.Actor(data);for(var member:List.of(partyA,partyB)){member.core.location.setWorld(actor.world);member.core.location.setX(100);f.players.put(member.core.id,member.player);doReturn(java.util.Set.of()).when(member.player).getActivePotionEffects();doReturn(true).when(member.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());f.activate(member);}var parties=PartyLifecycleNativeTest.parties(f);var party=parties.create(partyA.core.id).orElseThrow();assertTrue(parties.accept(partyB.core.id,parties.invite(partyA.core.id,party.id(),partyB.core.id).orElseThrow().id()));var catalog=adventures.snapshot().catalog();assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.PARTY,party.id(),Set.of(partyA.core.record.id(),partyB.core.record.id())).get(5,TimeUnit.SECONDS).status());
var storage=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");var before=storage.find(actor.core.record.id()).get().orElseThrow();var partyABefore=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBBefore=storage.find(partyB.core.record.id()).get().orElseThrow();for(var member:List.of(actor,partyA)){member.core.location.setX(first.bounds().minX()+.5);member.core.location.setY(first.bounds().minY()+1);member.core.location.setZ(first.bounds().minZ()+.5);}
pulse(f);drain(f);pulse(f);drain(f);var initialMobs=spawned.stream().filter(Husk.class::isInstance).toList();assertEquals(2,initialMobs.size());var ordinary=initialMobs.getFirst();assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.DUNGEON,dungeonController.encounterOrigin(ordinary).orElseThrow().kind());var copiedSlot=mock(Husk.class);when(copiedSlot.getUniqueId()).thenReturn(UUID.randomUUID());var copiedProof=ordinary.getPersistentDataContainer();when(copiedSlot.getPersistentDataContainer()).thenReturn(copiedProof);assertTrue(dungeonController.encounterOrigin(copiedSlot).isEmpty(),"a copied valid dungeon slot tag is not an authenticated registered spawn");var nativeDrop=new ItemStack(Material.ROTTEN_FLESH);var forgedDeath=new EntityDeathEvent(copiedSlot,mock(org.bukkit.damage.DamageSource.class),new ArrayList<>(List.of(nativeDrop)),7);f.dispatcher.accept(forgedDeath);assertEquals(7,forgedDeath.getDroppedExp());assertEquals(List.of(nativeDrop),forgedDeath.getDrops());assertTrue(dungeonController.encounterOrigin(ordinary).isPresent(),"copied callback did not consume the real dungeon slot");var survivor=initialMobs.getLast();assertNotEquals(dungeonController.encounterOrigin(ordinary),dungeonController.encounterOrigin(survivor),"each slot has independent kill authority");var survivorOrigin=dungeonController.encounterOrigin(survivor);var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(actor.player);when(source.getDirectEntity()).thenReturn(actor.player);var partySource=mock(org.bukkit.damage.DamageSource.class);when(partySource.getCausingEntity()).thenReturn(partyA.player);when(partySource.getDirectEntity()).thenReturn(partyA.player);f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,ordinary,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,ordinary,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));var drops=new ArrayList<ItemStack>();drops.add(new ItemStack(Material.ROTTEN_FLESH));var ordinaryDeath=new EntityDeathEvent(ordinary,source,drops,7);f.dispatcher.accept(ordinaryDeath);await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).map(run->run.defeatedSlots().size()==1).orElse(false),actor.messages);assertEquals(7,ordinaryDeath.getDroppedExp());assertFalse(ordinaryDeath.getDrops().isEmpty());var partial=dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow();assertEquals(1,partial.defeatedSlots().size());assertFalse(partial.clearedRooms().contains(first.id()));for(var member:List.of(actor,partyA))member.core.location.setX(100);pulse(f);verify(survivor,atLeastOnce()).remove();assertFalse(actorEffects.containsKey(org.bukkit.potion.PotionEffectType.SPEED));for(var member:List.of(actor,partyA)){member.core.location.setX(first.bounds().minX()+.5);member.core.location.setY(first.bounds().minY()+1);member.core.location.setZ(first.bounds().minZ()+.5);}pulse(f);drain(f);var respawned=spawned.stream().filter(Husk.class::isInstance).toList().getLast();assertNotSame(survivor,respawned);assertEquals(survivorOrigin,dungeonController.encounterOrigin(respawned),"respawned slot retains duplicate-safe kill authority");f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,respawned,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,respawned,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));f.dispatcher.accept(new EntityDeathEvent(respawned,source,new ArrayList<>(),0));await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow().clearedRooms().contains(first.id()),actor.messages);assertTrue(storage.find(partyB.core.record.id()).get().orElseThrow().experience().compareTo(partyBBefore.experience())>0,"ordinary party combat sharing remains active");
var actorAfterKills=storage.find(actor.core.record.id()).get().orElseThrow();var partyAAfterKills=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBAfterKills=storage.find(partyB.core.record.id()).get().orElseThrow();for(var member:List.of(actor,partyA,partyB)){member.core.location.setX(bossRoom.bounds().minX()+.5);member.core.location.setY(bossRoom.bounds().minY()+1);member.core.location.setZ(bossRoom.bounds().minZ()+.5);}pulse(f);drain(f);var firstBoss=spawned.stream().filter(Vindicator.class::isInstance).findFirst().orElseThrow();for(var member:List.of(actor,partyA,partyB))member.core.location.setX(100);pulse(f);verify(firstBoss,atLeastOnce()).remove();for(var member:List.of(actor,partyA,partyB)){member.core.location.setX(bossRoom.bounds().minX()+.5);member.core.location.setY(bossRoom.bounds().minY()+1);member.core.location.setZ(bossRoom.bounds().minZ()+.5);}pulse(f);drain(f);var boss=spawned.stream().filter(Vindicator.class::isInstance).toList().getLast();assertNotSame(firstBoss,boss);f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,boss,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,1));f.dispatcher.accept(new EntityDamageByEntityEvent(partyA.player,boss,org.bukkit.event.entity.EntityDamageEvent.DamageCause.ENTITY_ATTACK,partySource,1));f.dispatcher.accept(new EntityDeathEvent(boss,source,new ArrayList<>(),0));assertEquals(games.dmg.fantasystick.progression.ManagedEncounterOrigin.Kind.DUNGEON,dungeonController.encounterOrigin(boss).orElseThrow().kind(),"boss cleanup retains immutable managed origin authority");
await(f,()->dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).map(DungeonState.Run::defeated).orElse(false)&&adventures.active().isEmpty()&&storage.find(actor.core.record.id()).join().orElseThrow().experience().compareTo(actorAfterKills.experience())>0,actor.messages);var run=dungeons.runView(dungeon.id(),adventures.snapshot().catalog().revision()).orElseThrow();assertTrue(run.defeated(),"dungeon run not defeated");assertEquals(boss.getUniqueId().toString(),run.completionAudit().get("boss_id"),"global completion must retain immutable native boss proof for recovery");assertFalse(run.clearedRooms().contains(skipped.id()),"boss death completes without clearing every room");assertTrue(adventures.active().isEmpty(),"boss did not close all dungeon quests");assertTrue(adventures.characterClaimed(catalog.revision(),dungeon.id(),actor.core.record.id()),"solo completion claim missing");assertTrue(adventures.characterClaimed(catalog.revision(),dungeon.id(),partyA.core.record.id()),"represented party member claim missing");assertTrue(adventures.characterClaimed(catalog.revision(),dungeon.id(),partyB.core.record.id()),"present snapshotted party member claim missing");var after=storage.find(actor.core.record.id()).get().orElseThrow();assertTrue(after.experience().compareTo(before.experience())>0,"ordinary dungeon kills award standard combat XP");assertTrue(after.coins().compareTo(before.coins())>0,"ordinary dungeon kills award standard character coins");assertTrue(after.experience().compareTo(actorAfterKills.experience())>0,"genuinely killed dungeon boss grants native-classified per-kill XP");assertTrue(after.coins().compareTo(actorAfterKills.coins())>0,"genuinely killed dungeon boss grants native-classified per-kill coins");assertEquals(AdventureQuestDirectory.Reward.empty(),adventures.unclaimed(catalog.revision(),dungeon.id(),actor.core.record.id()));var partyAAfter=storage.find(partyA.core.record.id()).get().orElseThrow();var partyBAfter=storage.find(partyB.core.record.id()).get().orElseThrow();assertTrue(partyAAfter.experience().compareTo(partyABefore.experience())>0,"party contributor receives normal kill XP");assertTrue(partyBAfter.experience().compareTo(partyBBefore.experience())>0);assertTrue(partyAAfter.experience().compareTo(partyAAfterKills.experience())>0);assertTrue(partyBAfter.experience().compareTo(partyBAfterKills.experience())>0);var trophies=(games.dmg.fantasystick.adventure.FishingDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishing");var soloTrophy=trophies.trophies(actor.core.record.id()).getFirst();var partyTrophy=trophies.trophies(partyA.core.record.id()).getFirst();assertEquals(games.dmg.fantasystick.adventure.FishingState.TrophyType.DUNGEON,soloTrophy.type());assertEquals("Trophy of Sunken Keep",soloTrophy.name());assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5)),soloTrophy.total());assertEquals(new games.dmg.fantasystick.adventure.FishingState.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5)),partyTrophy.total());assertEquals(Set.of(partyA.core.record.id(),partyB.core.record.id()),partyTrophy.rewards().keySet());assertEquals(AdventureQuestDirectory.Reward.empty(),adventures.unclaimed(catalog.revision(),dungeon.id(),partyA.core.record.id()));var defeatedGeneration=adventures.snapshot().catalog().revision();command(f,actor,"dungeon","close",dungeon.id().toString(),"confirm");command(f,actor,"dungeon","open",dungeon.id().toString());assertTrue(actor.messages.getLast().contains("open refused"),actor.messages.toString());assertTrue(dungeons.runView(dungeon.id(),defeatedGeneration).orElseThrow().defeated(),"defeated generation changed");command(f,actor,"adventure","reroll","confirm");await(f,()->adventures.snapshot().reset()==null&&!adventures.snapshot().catalog().revision().equals(defeatedGeneration),actor.messages);assertEquals(1,trophies.trophies(actor.core.record.id()).size(),"reroll preserves earned dungeon trophy");var claimBase=storage.find(actor.core.record.id()).get().orElseThrow();var turn=(games.dmg.fantasystick.adventure.FishingController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"fishingController");var npcChunk=mock(Chunk.class);when(npcChunk.getWorld()).thenReturn(actor.world);when(npcChunk.getX()).thenReturn(actor.core.location.getBlockX()>>4);when(npcChunk.getZ()).thenReturn(actor.core.location.getBlockZ()>>4);when(npcChunk.getEntities()).thenReturn(new Entity[0]);when(actor.world.getChunkAt(anyInt(),anyInt())).thenReturn(npcChunk);when(actor.world.getLoadedChunks()).thenReturn(new Chunk[]{npcChunk});command(f,actor,"quest-giver","place","Dungeon Steward");var giverLocation=adventures.snapshot().givers().getFirst();actor.core.location.setX(giverLocation.x());actor.core.location.setZ(giverLocation.z());questController.reconcile(npcChunk);var giver=spawned.stream().filter(Villager.class::isInstance).map(Villager.class::cast).findFirst().orElseThrow();int menusBeforeGiver=actor.inventories.size();f.dispatcher.accept(new org.bukkit.event.player.PlayerInteractEntityEvent(actor.player,giver,org.bukkit.inventory.EquipmentSlot.HAND));await(f,()->actor.inventories.size()>menusBeforeGiver,actor.messages);var giverMenu=actor.inventories.getLast();var plain=net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer.plainText();var entry=java.util.stream.IntStream.range(0,giverMenu.getSize()).mapToObj(giverMenu::getItem).filter(Objects::nonNull).filter(item->item.getItemMeta().displayName()!=null&&plain.serialize(item.getItemMeta().displayName()).startsWith("Turn in quest items")).findFirst().orElseThrow();var giverClick=mock(InventoryClickEvent.class);when(giverClick.getWhoClicked()).thenReturn(actor.player);when(giverClick.getInventory()).thenReturn(giverMenu);when(giverClick.getCurrentItem()).thenReturn(entry);questController.click(giverClick);var trophyMenu=actor.inventories.getLast();var redeem=java.util.stream.IntStream.range(0,trophyMenu.getSize()).mapToObj(trophyMenu::getItem).filter(Objects::nonNull).filter(item->item.getItemMeta().displayName()!=null&&plain.serialize(item.getItemMeta().displayName()).equals("Redeem Trophy of Sunken Keep")).findFirst().orElseThrow();var redeemClick=mock(InventoryClickEvent.class);when(redeemClick.getWhoClicked()).thenReturn(actor.player);when(redeemClick.getInventory()).thenReturn(trophyMenu);when(redeemClick.getCurrentItem()).thenReturn(redeem);turn.click(redeemClick);await(f,()->trophies.trophies(actor.core.record.id()).isEmpty()&&trophies.unclaimed(actor.core.record.id()).equals(new games.dmg.fantasystick.adventure.FishingState.Reward(BigInteger.valueOf(30),BigInteger.valueOf(5))),actor.messages);await(f,()->actor.inventories.getLast()!=trophyMenu,actor.messages);var claimMenu=actor.inventories.getLast();var claim=java.util.stream.IntStream.range(0,claimMenu.getSize()).mapToObj(claimMenu::getItem).filter(Objects::nonNull).filter(item->item.getItemMeta().displayName()!=null&&plain.serialize(item.getItemMeta().displayName()).equals("Claim earned quest rewards")).findFirst().orElseThrow();var claimClick=mock(InventoryClickEvent.class);when(claimClick.getWhoClicked()).thenReturn(actor.player);when(claimClick.getInventory()).thenReturn(claimMenu);when(claimClick.getCurrentItem()).thenReturn(claim);turn.click(claimClick);await(f,()->trophies.unclaimed(actor.core.record.id()).equals(games.dmg.fantasystick.adventure.FishingState.Reward.empty())&&storage.find(actor.core.record.id()).join().orElseThrow().experience().compareTo(claimBase.experience().add(BigInteger.valueOf(30)))>=0,actor.messages);var claimed=storage.find(actor.core.record.id()).get().orElseThrow();assertEquals(claimBase.coins().add(BigInteger.valueOf(5)),claimed.coins());assertTrue(dungeons.snapshot().runs().isEmpty(),"full reset retained dungeon run");command(f,actor,"dungeon","open",dungeon.id().toString());var resetCatalog=adventures.snapshot().catalog();assertTrue(resetCatalog.sites().contains(dungeon.id()),"reopened dungeon absent from replacement catalog");assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(resetCatalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,actor.core.record.id(),Set.of(actor.core.record.id())).get(5,TimeUnit.SECONDS).status());command(f,actor,"dungeon","edit","start",dungeon.id().toString());assertTrue(actor.messages.getLast().contains("exact confirm"),actor.messages.toString());assertFalse(adventures.active().isEmpty(),"fresh dungeon quest missing");command(f,actor,"dungeon","edit","start",dungeon.id().toString(),"confirm");await(f,()->dungeonController.editing(actor.core.id).isPresent(),actor.messages);assertTrue(adventures.active().isEmpty(),"confirmed edit retained accepted quest");assertFalse(dungeons.find(dungeon.id()).orElseThrow().enabled());
await(f,()->exportedDungeonJourneyReady(data),actor.messages);
}
var exported=exportedDungeonEvents(data);
var changes=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("dungeon.reward_changed")).toList();
assertEquals(1,changes.size(),
"the registered persisted dungeon-wide reward change needs one exported event");
var changed=changes.getFirst().getAsJsonObject("data");
assertFalse(changed.get("before_configured").getAsBoolean());
assertEquals("0",changed.get("before_xp").getAsString());
assertEquals("30",changed.get("after_xp").getAsString());
assertEquals("5",changed.get("after_coins").getAsString());
assertNotEquals(changed.get("before_revision").getAsString(),
changed.get("after_revision").getAsString());
assertEquals("future_acceptances",changed.get("application_scope").getAsString());
assertEquals(2,exported.stream().filter(event->event.get("type").getAsString()
.endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).count(),
"the two represented units need distinct exported earned receipts");
assertEquals(1,exported.stream().filter(event->event.get("type").getAsString()
.endsWith("dungeon.completed")).count(),
"boss death closes the whole dungeon once, not once per trophy");
var kills=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("combat.reward_resolved")&&event.getAsJsonObject("data")
.get("managed_origin_kind").getAsString().equals("dungeon")).toList();
assertTrue(kills.size()>=3,"ordinary room and boss kills each keep their per-recipient result");
assertTrue(kills.stream().allMatch(event->event.getAsJsonObject("data").has("dungeon_id")
&&event.getAsJsonObject("data").has("room_id")
&&event.getAsJsonObject("data").has("generation_id")
&&event.getAsJsonObject("data").has("spawn_slot_or_boss")),
"managed kill awards must preserve authenticated room and spawn evidence");
var earned=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).toList();
assertTrue(earned.stream().allMatch(event->event.getAsJsonObject("data")
.getAsJsonObject("boss_death_location").has("world_id")));
var completed=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("dungeon.completed")).findFirst().orElseThrow().getAsJsonObject("data");
assertEquals(2,completed.getAsJsonArray("receipt_ids").size());
assertEquals(2,completed.getAsJsonArray("represented_units").size());
assertTrue(completed.getAsJsonObject("death_location").has("world_id"));
assertTrue(earned.stream().allMatch(event->event.getAsJsonObject("data")
.get("boss_id").getAsString().equals(completed.get("boss_id").getAsString())));
var redemptions=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("adventure.trophy_redeemed")&&event.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).toList();
assertEquals(1,redemptions.size(),
"the exact consumed dungeon head must have one independent redemption event");
assertEquals("entered_via_registered_giver",redemptions.getFirst().getAsJsonObject("data")
.get("giver_status").getAsString());
assertTrue(exported.stream().anyMatch(event->event.get("type").getAsString()
.endsWith("adventure.reward_claimed")&&event.getAsJsonObject("data")
.getAsJsonArray("sources").asList().stream().anyMatch(source->source.getAsJsonObject()
.get("source_kind").getAsString().equals("dungeon_trophy"))
&&event.getAsJsonObject("data").get("giver_status").getAsString()
.equals("entered_via_registered_giver")),
"the later individual claim must retain dungeon receipt provenance and registered Giver origin");
}
private static boolean exportedDungeonJourneyReady(Path root){
try{var events=exportedDungeonEvents(root);
return events.stream().filter(value->value.get("type").getAsString()
.endsWith("dungeon.completed")).count()==1
&&events.stream().filter(value->value.get("type").getAsString()
.endsWith("adventure.trophy_earned")&&value.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).count()==2
&&events.stream().filter(value->value.get("type").getAsString()
.endsWith("adventure.trophy_redeemed")&&value.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).count()==1
&&events.stream().anyMatch(value->value.get("type").getAsString()
.endsWith("adventure.reward_claimed")&&value.getAsJsonObject("data")
.getAsJsonArray("sources").asList().stream().anyMatch(source->source.getAsJsonObject()
.get("source_kind").getAsString().equals("dungeon_trophy")));
}catch(Exception transientExport){return false;}
}
private static List<com.google.gson.JsonObject> exportedDungeonEvents(Path root)throws Exception{
try(var files=java.nio.file.Files.walk(root.resolve("events"))){
return files.filter(path->path.toString().endsWith(".jsonl"))
.flatMap(path->{try{return java.nio.file.Files.lines(path);}
catch(java.io.IOException failure){throw new java.io.UncheckedIOException(failure);}})
.map(line->com.google.gson.JsonParser.parseString(line).getAsJsonObject()).toList();
}
}
@Test void failedDungeonRewardSaveAndReadOnlyAdminCommandsCannotInventChangeEvent() throws Exception {
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\n");
UUID dungeonId;
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);
f.players.put(actor.core.id,actor.player);
doReturn(true).when(actor.player).hasPermission("fantasystick.admin");
when(actor.world.getUID()).thenReturn(UUID.randomUUID());
command(f,actor,"dungeon","create","Acknowledged Vault");
var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");
dungeonId=dungeons.snapshot().dungeons().getFirst().id();
command(f,actor,"dungeon","reward",dungeonId.toString());
f.command.execute(actor.player,"fantasystick",
new String[]{"dungeon","reward",dungeonId.toString(),"-1","4"});
assertFalse(dungeons.find(dungeonId).orElseThrow().rewardConfigured());
var file=data.resolve("dungeons.yml");var prior=java.nio.file.Files.readAllBytes(file);
java.nio.file.Files.delete(file);java.nio.file.Files.createDirectory(file);
command(f,actor,"dungeon","reward",dungeonId.toString(),"6","4");
assertFalse(dungeons.find(dungeonId).orElseThrow().rewardConfigured());
java.nio.file.Files.delete(file);java.nio.file.Files.write(file,prior);
command(f,actor,"dungeon","reward",dungeonId.toString(),"6","4");
assertEquals(BigInteger.valueOf(6),dungeons.find(dungeonId).orElseThrow()
.completionReward().xp());
}
var changes=exportedDungeonEvents(data).stream().filter(event->event.get("type")
.getAsString().endsWith("dungeon.reward_changed")).toList();
assertEquals(1,changes.size(),"only a saved administrator change can publish");
var evidence=changes.getFirst().getAsJsonObject("data");
assertEquals(dungeonId.toString(),evidence.get("dungeon_id").getAsString());
assertEquals("6",evidence.get("after_xp").getAsString());
assertEquals("4",evidence.get("after_coins").getAsString());
}
@Test void roomCreationReportsOverlapDistanceLimitsAndPersistenceSeparately() throws Exception {
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\ndungeon-room-link-distance: 10\n");
try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){
f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);doReturn(true).when(actor.player).hasPermission("fantasystick.admin");when(actor.world.getUID()).thenReturn(UUID.randomUUID());
command(f,actor,"dungeon","create","Feedback");var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");await(f,()->!dungeons.snapshot().dungeons().isEmpty(),actor.messages);var dungeon=dungeons.snapshot().dungeons().getFirst();command(f,actor,"dungeon","edit","start",dungeon.id().toString());var controller=(games.dmg.fantasystick.dungeon.DungeonController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeonController");await(f,()->controller.editing(actor.core.id).isPresent(),actor.messages);
command(f,actor,"dungeon","room","create","corner","1","1","1","First");await(f,()->dungeons.find(dungeon.id()).orElseThrow().rooms().size()==1,actor.messages);var first=dungeons.find(dungeon.id()).orElseThrow().rooms().getFirst();
command(f,actor,"dungeon","room","create","corner","1","1","1","Overlap");await(f,()->actor.messages.stream().anyMatch(message->message.contains("overlaps an existing room")),actor.messages);
actor.core.location.setX(first.bounds().maxX()+11.1);actor.core.location.setY(first.bounds().minY()+1);actor.core.location.setZ(first.bounds().minZ()+.5);command(f,actor,"dungeon","room","create","corner","1","1","1","Too Far");await(f,()->actor.messages.stream().anyMatch(message->message.contains("configured 10-block link distance")),actor.messages);
f.command.execute(actor.player,"fantasystick",new String[]{"config","dungeon-room-link-distance","11"});var settings=(games.dmg.fantasystick.persistence.AdventureQuestSettings)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestSettings");await(f,()->settings.rules().dungeonRoomLinkDistance()==11,actor.messages);command(f,actor,"dungeon","room","create","corner","1","1","1","Now Linked");await(f,()->dungeons.find(dungeon.id()).orElseThrow().rooms().size()==2,actor.messages);var linked=dungeons.find(dungeon.id()).orElseThrow().rooms().getLast();
command(f,actor,"dungeon","room","create","corner","257","1","1","Too Large");await(f,()->actor.messages.stream().anyMatch(message->message.contains("dimensions or room limit")),actor.messages);
var file=data.resolve("dungeons.yml");java.nio.file.Files.delete(file);java.nio.file.Files.createDirectory(file);actor.core.location.setX(linked.bounds().maxX()+11.1);command(f,actor,"dungeon","room","create","corner","1","1","1","Unsaved");await(f,()->actor.messages.stream().anyMatch(message->message.contains("persistence is unavailable")),actor.messages);assertEquals(2,dungeons.find(dungeon.id()).orElseThrow().rooms().size());
}
}
@Test void authenticSheetAssignmentBlocksActivationAndRecoveryReturnsExactCharacter() throws Exception {
var root=data.resolve("custody");java.nio.file.Files.createDirectories(root);
try(var f=new FantasyStickLifecycleNativeTest.Fixture(root)){f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(root);f.players.put(actor.core.id,actor.player);doReturn(true).when(actor.player).hasPermission("fantasystick.admin");UUID world=UUID.randomUUID();when(actor.world.getUID()).thenReturn(world);var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");var dungeon=dungeons.create("Custody",world,0,64,0,0,0).get(5,TimeUnit.SECONDS);var room=dungeons.addRadiusRoom(dungeon.id(),"Boss",0,64,0,3,3).get(5,TimeUnit.SECONDS);dungeons.setBoss(dungeon.id(),room.id(),EntityType.HUSK,"Temporary",0,65,0,0,0).get(5,TimeUnit.SECONDS);command(f,actor,"dungeon","edit","start",dungeon.id().toString());var controller=(games.dmg.fantasystick.dungeon.DungeonController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeonController");await(f,()->controller.editing(actor.core.id).isPresent(),actor.messages);actor.core.location.setX(.5);actor.core.location.setY(65);actor.core.location.setZ(.5);assertEquals(List.of("assign-sheet"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"dungeon","room","boss","ass"}));assertEquals(List.of("0.1","1","1.5","10","100"),f.command.tabComplete(actor.player,"fantasystick",new String[]{"dungeon","room","boss","assign-sheet",""}));var sheet=actor.core.inventory.getItemInOffHand();actor.core.inventory.setItemInOffHand(null);actor.core.inventory.setItemInMainHand(sheet);actor.core.inventory.setItem(5,sheet.clone());command(f,actor,"dungeon","room","boss","assign-sheet","1.125");assertTrue(dungeons.custody(actor.core.record.id()).isEmpty());actor.core.inventory.setItem(5,null);var partyDirectory=(games.dmg.fantasystick.party.CharacterPartyDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characterParties");var forbiddenParty=partyDirectory.create(actor.core.record.id(),"Before Custody").get(5,TimeUnit.SECONDS).party().orElseThrow();command(f,actor,"dungeon","room","boss","assign-sheet","1.125");assertTrue(dungeons.custody(actor.core.record.id()).isEmpty());assertFalse(actor.core.inventory.getItemInMainHand().isEmpty());partyDirectory.disband(actor.core.record.id(),forbiddenParty.id()).get(5,TimeUnit.SECONDS);command(f,actor,"dungeon","room","boss","assign-sheet","1.125");await(f,()->dungeons.custody(actor.core.record.id()).isPresent(),actor.messages);assertTrue(actor.core.inventory.getItemInMainHand().isEmpty());assertEquals(actor.core.record,dungeons.custody(actor.core.record.id()).orElseThrow().character());actor.core.inventory.setItemInOffHand(new games.dmg.fantasystick.items.CharacterSheets().create(actor.core.record));var sessions=(games.dmg.fantasystick.session.EquipmentSessions)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"sessions");sessions.enter(actor.player);assertTrue(sessions.active(actor.core.id).isEmpty());assertTrue(actor.messages.stream().anyMatch(message->message.contains("dungeon boss")));var storage=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");assertThrows(java.util.concurrent.ExecutionException.class,()->storage.awardExperience(actor.core.record.id(),actor.core.record.revision(),BigInteger.ONE,games.dmg.fantasystick.progression.ProgressionRules.defaults()).get(5,TimeUnit.SECONDS));assertEquals(games.dmg.fantasystick.party.CharacterPartyDirectory.Status.INVALID,partyDirectory.create(actor.core.record.id(),"Forbidden").get(5,TimeUnit.SECONDS).status());actor.core.inventory.setItemInOffHand(null);actor.core.location.setX(100);command(f,actor,"dungeon","edit","done");assertEquals(List.of(dungeon.id().toString()),f.command.tabComplete(actor.player,"fantasystick",new String[]{"dungeon","boss","recover-sheet",""}));command(f,actor,"dungeon","boss","recover-sheet",dungeon.id().toString());await(f,()->dungeons.custody(actor.core.record.id()).isEmpty(),actor.messages);var returned=Arrays.stream(actor.core.inventory.getContents()).filter(Objects::nonNull).filter(item->new games.dmg.fantasystick.items.CharacterSheets().identify(item).filter(actor.core.record.id()::equals).isPresent()).toList();assertEquals(1,returned.size());assertEquals(actor.core.record,storage.find(actor.core.record.id()).get(5,TimeUnit.SECONDS).orElseThrow());assertNull(dungeons.room(dungeon.id(),room.id()).orElseThrow().boss());}
}
@SuppressWarnings({"unchecked","rawtypes"})
@Test void committedNativeBossDefeatReplaysCompletionAfterMissingLivePublication() throws Exception {
var root=data.resolve("completion-replay");Files.createDirectories(root);
Files.writeString(root.resolve("config.yml"),
"adventure-daily-site-count: 0\nmaximum-level: 1\nhostile-coin-drop-min: 1\nhostile-coin-drop-max: 1\n");
var defeatedBoss=new UUID[1];var absentLeader=new UUID[1];var presentMember=new UUID[1];
var completionOperation=new UUID[1];
try(var f=new FantasyStickLifecycleNativeTest.Fixture(root)){
f.plugin.setEnabled(true);
var actor=new FantasyStickLifecycleNativeTest.Actor(root);
f.players.put(actor.core.id,actor.player);
doReturn(true).when(actor.player).hasPermission("fantasystick.admin");
var world=UUID.randomUUID();when(actor.world.getUID()).thenReturn(world);
when(actor.world.isChunkLoaded(anyInt(),anyInt())).thenReturn(true);
actor.core.location.setX(.5);actor.core.location.setY(65);actor.core.location.setZ(.5);
var spawned=new ArrayList<Vindicator>();
doAnswer(call->{var mob=mock(Vindicator.class);
when(mob.getPersistentDataContainer()).thenReturn(
new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(
new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry()));
when(mob.getUniqueId()).thenReturn(UUID.randomUUID());
when(mob.getType()).thenReturn(EntityType.VINDICATOR);
when(mob.getWorld()).thenReturn(actor.world);
when(mob.getLocation()).thenReturn(((Location)call.getArgument(0)).clone());
((java.util.function.Consumer<Vindicator>)call.getArgument(2)).accept(mob);
spawned.add(mob);return mob;
}).when(actor.world).spawn(any(Location.class),eq(Vindicator.class),any(java.util.function.Consumer.class));
when(actor.world.getEntities()).thenAnswer(ignored->new ArrayList<Entity>(spawned));
var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");
var dungeon=dungeons.create("Replay Vault",world,0,64,0,0,0).get(5,TimeUnit.SECONDS);
var room=dungeons.addRadiusRoom(dungeon.id(),"Boss",0,64,0,3,3).get(5,TimeUnit.SECONDS);
dungeons.setBoss(dungeon.id(),room.id(),EntityType.VINDICATOR,"Keeper",0,65,0,0,0)
.get(5,TimeUnit.SECONDS);
dungeons.setCompletionReward(dungeon.id(),BigInteger.ONE,BigInteger.ZERO)
.get(5,TimeUnit.SECONDS);
command(f,actor,"dungeon","enable",dungeon.id().toString());
command(f,actor,"dungeon","open",dungeon.id().toString());
var adventures=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(
f.plugin,"adventureQuests");
f.activate(actor);
var catalog=adventures.snapshot().catalog();
completionOperation[0]=UUID.nameUUIDFromBytes(("dungeon:completed:"+dungeon.id()+":"+
catalog.revision()).getBytes(java.nio.charset.StandardCharsets.UTF_8));
assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(
catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,
actor.core.record.id(),Set.of(actor.core.record.id())).get(5,TimeUnit.SECONDS).status());
var leader=new FantasyStickLifecycleNativeTest.Actor(root);
var member=new FantasyStickLifecycleNativeTest.Actor(root);
absentLeader[0]=leader.core.record.id();presentMember[0]=member.core.record.id();
for(var participant:List.of(leader,member)){
participant.core.location.setWorld(actor.world);
participant.core.location.setY(65);
participant.core.location.setZ(.5);
f.players.put(participant.core.id,participant.player);
f.activate(participant);
}
leader.core.location.setX(100);member.core.location.setX(1.5);
var parties=PartyLifecycleNativeTest.parties(f);
var party=parties.create(leader.core.id).orElseThrow();
assertTrue(parties.accept(member.core.id,
parties.invite(leader.core.id,party.id(),member.core.id).orElseThrow().id()));
assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(
catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.PARTY,party.id(),
Set.of(leader.core.record.id(),member.core.record.id()))
.get(5,TimeUnit.SECONDS).status());
f.players.remove(leader.core.id);
assertNull(f.plugin.getServer().getPlayer(leader.core.id),
"the trophy carrier is offline when the boss dies");
pulse(f);drain(f);pulse(f);drain(f);
var boss=spawned.getLast();defeatedBoss[0]=boss.getUniqueId();
var source=mock(org.bukkit.damage.DamageSource.class);
when(source.getCausingEntity()).thenReturn(actor.player);
when(source.getDirectEntity()).thenReturn(actor.player);
f.dispatcher.accept(new EntityDamageByEntityEvent(actor.player,boss,
EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,2));
var memberSource=mock(org.bukkit.damage.DamageSource.class);
when(memberSource.getCausingEntity()).thenReturn(member.player);
when(memberSource.getDirectEntity()).thenReturn(member.player);
f.dispatcher.accept(new EntityDamageByEntityEvent(member.player,boss,
EntityDamageEvent.DamageCause.ENTITY_ATTACK,memberSource,2));
var owner=(games.dmg.fantasystick.dungeon.DungeonController)
FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeonController");
var eventField=owner.getClass().getDeclaredField("auditEvents");
eventField.setAccessible(true);eventField.set(owner,null);
f.dispatcher.accept(new EntityDeathEvent(boss,source,new ArrayList<>(),0));
await(f,()->dungeons.runView(dungeon.id(),catalog.revision())
.map(DungeonState.Run::defeated).orElse(false),actor.messages);
assertEquals(defeatedBoss[0].toString(),dungeons.runView(dungeon.id(),catalog.revision())
.orElseThrow().completionAudit().get("boss_id"));
assertEquals(0,exportedDungeonEvents(root).stream().filter(event->event.get("type")
.getAsString().endsWith("dungeon.completed")).count());
}
assertTrue(new games.dmg.fantasystick.persistence.YamlDungeonRepository(root).load().runs()
.stream().anyMatch(run->run.defeated()&&defeatedBoss[0].toString()
.equals(run.completionAudit().get("boss_id"))));
try(var restarted=new FantasyStickLifecycleNativeTest.Fixture(root)){
restarted.plugin.setEnabled(true);
await(restarted,()->{try{return exportedDungeonEvents(root).stream().anyMatch(event->
event.get("type").getAsString().endsWith("dungeon.completed"));}
catch(Exception transientExport){return false;}},List.of());
var runs=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(restarted.plugin,"dungeons");
await(restarted,()->runs.snapshot().runs().stream().anyMatch(run->run.defeated()
&&defeatedBoss[0].toString().equals(run.completionAudit().get("boss_id"))
&&run.completionPublished()),List.of());
}
var repository=new games.dmg.fantasystick.persistence.YamlDungeonRepository(root);
var exportedOnce=repository.load();
assertTrue(exportedOnce.runs().stream().anyMatch(run->run.defeated()
&&defeatedBoss[0].toString().equals(run.completionAudit().get("boss_id"))
&&run.completionPublished()),
"the first exported recovery must durably acknowledge this exact boss");
repository.save(new DungeonState(exportedOnce.dungeons(),exportedOnce.runs().stream()
.map(run->new DungeonState.Run(run.dungeon(),run.generation(),run.defeatedSlots(),
run.clearedRooms(),run.unitClaims(),run.characterClaims(),run.defeated(),
run.completionAudit(),false)).toList()));
// Simulate a crash after JSONL append but before the separate acknowledgement save.
try(var replayAgain=new FantasyStickLifecycleNativeTest.Fixture(root)){
replayAgain.plugin.setEnabled(true);
var owner=(games.dmg.fantasystick.dungeon.DungeonController)
FantasyStickLifecycleNativeTest.Fixture.field(replayAgain.plugin,"dungeonController");
owner.replayCompletions(); // Force the normal startup replay even on fast test machines.
var events=(games.dmg.fantasystick.events.FishingEventBridge)
FantasyStickLifecycleNativeTest.Fixture.field(replayAgain.plugin,"fishingAuditEvents");
replayAgain.plugin.setEnabled(false);
events.drained().toCompletableFuture().get(5,TimeUnit.SECONDS);
}
var exportedId=UUID.nameUUIDFromBytes(("fantasystick:fishing:"+
completionOperation[0]+":dungeon.completed")
.getBytes(java.nio.charset.StandardCharsets.UTF_8));
assertFalse(Files.exists(root.resolve("fishing-event-intents").resolve(exportedId+".pending")),
"a previously exported completion must not queue a fresh intent on a later restart");
assertTrue(repository.load().runs().stream().anyMatch(run->run.defeated()
&&defeatedBoss[0].toString().equals(run.completionAudit().get("boss_id"))
&&run.completionPublished()),
"the exported record repairs a lost acknowledgement before any new publication");
var completed=exportedDungeonEvents(root).stream().filter(event->event.get("type")
.getAsString().endsWith("dungeon.completed")).toList();
assertEquals(1,completed.size(),"a later restart must not repeat an acknowledged completion");
assertEquals(defeatedBoss[0].toString(),completed.getFirst().getAsJsonObject("data")
.get("boss_id").getAsString());
var earned=exportedDungeonEvents(root).stream().filter(event->event.get("type")
.getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).toList();
assertEquals(2,earned.size(),"both represented solo and party units earn a receipt");
var partyEarned=earned.stream().filter(event->event.getAsJsonObject("data")
.get("unit_kind").getAsString().equals("party")).findFirst().orElseThrow()
.getAsJsonObject("data");
assertEquals(absentLeader[0].toString(),partyEarned.get("carrier_character_id").getAsString());
assertEquals(presentMember[0].toString(),partyEarned.getAsJsonArray("beneficiaries")
.get(0).getAsJsonObject().get("character_id").getAsString());
assertEquals(1,partyEarned.getAsJsonArray("beneficiaries").size());
var bossAwards=exportedDungeonEvents(root).stream().filter(event->event.get("type")
.getAsString().endsWith("combat.reward_resolved")&&event.getAsJsonObject("data")
.get("target_id").getAsString().equals(defeatedBoss[0].toString())).toList();
assertTrue(bossAwards.stream().anyMatch(event->{var award=event.getAsJsonObject("data");
return award.get("allocated_experience").getAsBigInteger().signum()>0
&&award.get("applied_experience").getAsString().equals("0")
&&award.get("loss_reason").getAsString().equals("maximum_level");}),
"native boss kill at the configured maximum must distinguish allocated from applied XP");
}
@SuppressWarnings({"unchecked","rawtypes"})
@Test void characterBossMaterializesScaledVindicatorAndFixatesOnAcceptedThreat() throws Exception {
var root=data.resolve("character-runtime");java.nio.file.Files.createDirectories(root);
java.nio.file.Files.writeString(root.resolve("config.yml"),"adventure-daily-site-count: 0\n");
var killedBossId=new UUID[1];var savedBossCharacter=new UUID[1];
try(var f=new FantasyStickLifecycleNativeTest.Fixture(root)){
f.plugin.setEnabled(true);
var admin=new FantasyStickLifecycleNativeTest.Actor(root);f.players.put(admin.core.id,admin.player);doReturn(true).when(admin.player).hasPermission("fantasystick.admin");var bossStats=new java.util.EnumMap<games.dmg.fantasystick.progression.Stat,BigInteger>(games.dmg.fantasystick.progression.Stat.class);for(var stat:games.dmg.fantasystick.progression.Stat.values())bossStats.put(stat,BigInteger.valueOf(30));var abilities=List.of("fireball","frost_nova","hex","second_wind","purify","stone_skin","guardian_bond");var configuredBoss=new games.dmg.fantasystick.persistence.CharacterRecord(admin.core.record.id(),admin.core.record.name(),BigInteger.ONE,bossStats,admin.core.record.level(),admin.core.record.experience(),admin.core.record.unspentPoints(),java.math.BigDecimal.valueOf(40),Set.copyOf(abilities),abilities,List.of("speed_i","low_light_vision","quicker_regeneration_i"),Map.of("fireball",java.time.Instant.parse("2099-01-01T00:00:00Z")),Map.of(),false,null,null,admin.core.record.coins(),admin.core.record.coinsInitialized(),admin.core.record.rewardReceipts(),admin.core.record.pvpCoinEscrow(),new games.dmg.fantasystick.persistence.CharacterGear(games.dmg.fantasystick.persistence.GearTier.COPPER,games.dmg.fantasystick.persistence.GearTier.GOLD,games.dmg.fantasystick.persistence.GearTier.IRON));new games.dmg.fantasystick.persistence.YamlCharacterRepository(root).replace(configuredBoss,BigInteger.ZERO);savedBossCharacter[0]=configuredBoss.id();
UUID worldId=UUID.randomUUID();when(admin.world.getUID()).thenReturn(worldId);when(admin.world.isChunkLoaded(anyInt(),anyInt())).thenReturn(true);
var spawned=new ArrayList<Vindicator>();var maximumBases=new IdentityHashMap<Vindicator,double[]>();var attackBases=new IdentityHashMap<Vindicator,double[]>();var bossEffects=new IdentityHashMap<Vindicator,Set<org.bukkit.potion.PotionEffect>>();var bossEquipment=new IdentityHashMap<Vindicator,org.bukkit.inventory.EntityEquipment>();
doAnswer(call->{var mob=mock(Vindicator.class);var pdc=new org.bukkit.craftbukkit.persistence.CraftPersistentDataContainer(new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry());var maximum=mock(org.bukkit.attribute.AttributeInstance.class);var damage=mock(org.bukkit.attribute.AttributeInstance.class);var equipment=mock(org.bukkit.inventory.EntityEquipment.class);var maximumBase=new double[]{20};var attackBase=new double[]{1};var health=new double[]{20};var effects=new LinkedHashSet<org.bukkit.potion.PotionEffect>();effects.add(new org.bukkit.potion.PotionEffect(org.bukkit.potion.PotionEffectType.WEAKNESS,200,0));maximumBases.put(mob,maximumBase);attackBases.put(mob,attackBase);bossEffects.put(mob,effects);bossEquipment.put(mob,equipment);when(mob.getPersistentDataContainer()).thenReturn(pdc);when(mob.getUniqueId()).thenReturn(UUID.randomUUID());when(mob.getType()).thenReturn(EntityType.VINDICATOR);when(mob.getWorld()).thenReturn(admin.world);when(mob.getLocation()).thenReturn(((Location)call.getArgument(0)).clone());when(mob.getAttribute(org.bukkit.attribute.Attribute.MAX_HEALTH)).thenReturn(maximum);when(mob.getAttribute(org.bukkit.attribute.Attribute.ATTACK_DAMAGE)).thenReturn(damage);when(maximum.getValue()).thenAnswer(ignored->maximumBase[0]);doAnswer(invocation->{maximumBase[0]=(Double)invocation.getArgument(0);return null;}).when(maximum).setBaseValue(anyDouble());doAnswer(invocation->{attackBase[0]=(Double)invocation.getArgument(0);return null;}).when(damage).setBaseValue(anyDouble());when(mob.getHealth()).thenAnswer(ignored->health[0]);doAnswer(invocation->{health[0]=(Double)invocation.getArgument(0);return null;}).when(mob).setHealth(anyDouble());when(mob.getEquipment()).thenReturn(equipment);when(mob.hasLineOfSight(any(Entity.class))).thenReturn(true);when(mob.getActivePotionEffects()).thenAnswer(ignored->Set.copyOf(effects));doAnswer(invocation->{effects.add(invocation.getArgument(0));return true;}).when(mob).addPotionEffect(any(org.bukkit.potion.PotionEffect.class));doAnswer(invocation->{effects.add(invocation.getArgument(0));return true;}).when(mob).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());doAnswer(invocation->{effects.removeIf(effect->effect.getType().equals(invocation.getArgument(0)));return null;}).when(mob).removePotionEffect(any(org.bukkit.potion.PotionEffectType.class));((java.util.function.Consumer<Vindicator>)call.getArgument(2)).accept(mob);spawned.add(mob);return mob;}).when(admin.world).spawn(any(Location.class),eq(Vindicator.class),any(java.util.function.Consumer.class));
when(admin.world.getEntities()).thenAnswer(ignored->new ArrayList<Entity>(spawned));
var dungeons=(DungeonDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeons");var dungeon=dungeons.create("Mirror",worldId,0,64,0,0,0).get(5,TimeUnit.SECONDS);var room=dungeons.addRadiusRoom(dungeon.id(),"Boss",0,64,0,3,3).get(5,TimeUnit.SECONDS);dungeons.setBoss(dungeon.id(),room.id(),EntityType.HUSK,"Temporary",0,65,0,0,0).get(5,TimeUnit.SECONDS);dungeons.setCompletionReward(dungeon.id(),BigInteger.ZERO,BigInteger.ZERO).get(5,TimeUnit.SECONDS);command(f,admin,"dungeon","edit","start",dungeon.id().toString());var controller=(games.dmg.fantasystick.dungeon.DungeonController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"dungeonController");await(f,()->controller.editing(admin.core.id).isPresent(),admin.messages);admin.core.location.setX(.5);admin.core.location.setY(65);admin.core.location.setZ(.5);var sheet=admin.core.inventory.getItemInOffHand();admin.core.inventory.setItemInOffHand(null);admin.core.inventory.setItemInMainHand(sheet);command(f,admin,"dungeon","room","boss","assign-sheet","2");await(f,()->dungeons.custody(admin.core.record.id()).isPresent(),admin.messages);command(f,admin,"dungeon","edit","done");command(f,admin,"dungeon","enable",dungeon.id().toString());command(f,admin,"dungeon","open",dungeon.id().toString());var adventures=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");await(f,()->adventures.snapshot().catalog()!=null&&adventures.snapshot().catalog().sites().contains(dungeon.id()),admin.messages);
var fighter=new FantasyStickLifecycleNativeTest.Actor(root);fighter.core.location.setWorld(admin.world);fighter.core.location.setX(.5);fighter.core.location.setY(65);fighter.core.location.setZ(.5);f.players.put(fighter.core.id,fighter.player);doReturn(false).when(fighter.player).isDead();doReturn(true).when(fighter.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());var abilityHits=new ArrayList<Double>();fighter.damage=(amount,source)->abilityHits.add(amount);f.activate(fighter);var catalog=adventures.snapshot().catalog();assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,fighter.core.record.id(),Set.of(fighter.core.record.id())).get(5,TimeUnit.SECONDS).status());var challenger=new FantasyStickLifecycleNativeTest.Actor(root);challenger.core.location.setWorld(admin.world);challenger.core.location.setX(1.5);challenger.core.location.setY(65);challenger.core.location.setZ(.5);f.players.put(challenger.core.id,challenger.player);doReturn(false).when(challenger.player).isDead();doReturn(true).when(challenger.player).addPotionEffect(any(org.bukkit.potion.PotionEffect.class),anyBoolean());challenger.damage=(amount,sourceEntity)->{};f.activate(challenger);assertEquals(AdventureQuestDirectory.Acceptance.APPLIED,adventures.accept(catalog.revision(),dungeon.id(),AdventureQuestState.UnitType.SOLO,challenger.core.record.id(),Set.of(challenger.core.record.id())).get(5,TimeUnit.SECONDS).status());pulse(f);drain(f);pulse(f);drain(f);assertFalse(spawned.isEmpty());var boss=spawned.getLast();assertEquals(70d,maximumBases.get(boss)[0]);assertEquals(26d,attackBases.get(boss)[0]);var equipment=bossEquipment.get(boss);verify(equipment).setChestplate(argThat(item->item!=null&&item.getType()==Material.COPPER_CHESTPLATE));verify(equipment).setLeggings(argThat(item->item!=null&&item.getType()==Material.GOLDEN_LEGGINGS));verify(equipment).setBoots(argThat(item->item!=null&&item.getType()==Material.IRON_BOOTS));assertTrue(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.SPEED)),bossEffects.get(boss)::toString);assertTrue(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.NIGHT_VISION)),bossEffects.get(boss)::toString);boss.setHealth(50);var source=mock(org.bukkit.damage.DamageSource.class);when(source.getCausingEntity()).thenReturn(fighter.player);when(source.getDirectEntity()).thenReturn(fighter.player);f.dispatcher.accept(new EntityDamageByEntityEvent(fighter.player,boss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,source,3));pulse(f);verify(boss,atLeastOnce()).setTarget(fighter.player);assertTrue(abilityHits.size()>=2,"Every ready offensive slot casts in the same pulse");int firstCasts=abilityHits.size();pulse(f);assertEquals(firstCasts,abilityHits.size(),"Boss-only cooldowns suppress immediate recasts");assertFalse(bossEffects.get(boss).stream().anyMatch(effect->effect.getType().equals(org.bukkit.potion.PotionEffectType.WEAKNESS)),"Purify removes a real harmful effect");var challengerSource=mock(org.bukkit.damage.DamageSource.class);when(challengerSource.getCausingEntity()).thenReturn(challenger.player);when(challengerSource.getDirectEntity()).thenReturn(challenger.player);f.dispatcher.accept(new EntityDamageByEntityEvent(challenger.player,boss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,challengerSource,10));pulse(f);var targets=org.mockito.ArgumentCaptor.forClass(LivingEntity.class);verify(boss,atLeastOnce()).setTarget(targets.capture());assertSame(fighter.player,targets.getAllValues().getLast(),"Higher later threat cannot break fixation");fighter.core.location.setX(100);pulse(f);targets=org.mockito.ArgumentCaptor.forClass(LivingEntity.class);verify(boss,atLeastOnce()).setTarget(targets.capture());assertSame(challenger.player,targets.getAllValues().getLast(),"Ineligible fixation retargets highest remaining threat");
challenger.core.location.setX(100);pulse(f);verify(boss,atLeastOnce()).remove();challenger.core.location.setX(.5);pulse(f);drain(f);var freshBoss=spawned.getLast();killedBossId[0]=freshBoss.getUniqueId();assertNotSame(boss,freshBoss);assertEquals(70d,maximumBases.get(freshBoss)[0]);f.dispatcher.accept(new EntityDamageByEntityEvent(challenger.player,freshBoss,EntityDamageEvent.DamageCause.ENTITY_ATTACK,challengerSource,2));f.dispatcher.accept(new EntityDeathEvent(freshBoss,challengerSource,new ArrayList<>(),0));await(f,()->dungeons.runView(dungeon.id(),catalog.revision()).map(DungeonState.Run::defeated).orElse(false),admin.messages);assertTrue(dungeons.custody(admin.core.record.id()).isPresent(),"Boss defeat keeps character custody for later days");await(f,()->{try{var published=exportedDungeonEvents(root);return published.stream().anyMatch(event->event.get("type").getAsString().endsWith("dungeon.completed"))&&published.stream().anyMatch(event->event.get("type").getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data").get("trophy_type").getAsString().equals("dungeon_trophy"));}catch(Exception transientExport){return false;}},admin.messages);challenger.core.location.setX(100);command(f,admin,"dungeon","disable",dungeon.id().toString(),"confirm");admin.core.location.setX(100);command(f,admin,"dungeon","boss","recover-sheet",dungeon.id().toString());await(f,()->dungeons.custody(admin.core.record.id()).isEmpty(),admin.messages);var storage=(CharacterStorage)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"characters");assertEquals(configuredBoss,storage.find(configuredBoss.id()).get(5,TimeUnit.SECONDS).orElseThrow());assertTrue(Arrays.stream(admin.core.inventory.getContents()).filter(Objects::nonNull).anyMatch(item->new games.dmg.fantasystick.items.CharacterSheets().identify(item).filter(configuredBoss.id()::equals).isPresent()));
await(f,()->{try{var published=exportedDungeonEvents(root);return published.stream().anyMatch(event->event.get("type").getAsString().endsWith("dungeon.completed"))&&published.stream().anyMatch(event->event.get("type").getAsString().endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data").get("trophy_type").getAsString().equals("dungeon_trophy"));}catch(Exception transientExport){return false;}},admin.messages);
}
var exported=exportedDungeonEvents(root);
var bossAwards=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("combat.reward_resolved")&&event.getAsJsonObject("data")
.get("target_id").getAsString().equals(killedBossId[0].toString())).toList();
assertFalse(bossAwards.isEmpty(),"native character-sheet boss keeps its ordinary mob kill reward");
assertTrue(bossAwards.stream().allMatch(event->event.getAsJsonObject("data")
.get("managed_origin_kind").getAsString().equals("dungeon")
&&event.getAsJsonObject("data").get("target_type").getAsString().equals("vindicator")));
var earned=exported.stream().filter(event->event.get("type").getAsString()
.endsWith("adventure.trophy_earned")&&event.getAsJsonObject("data")
.get("trophy_type").getAsString().equals("dungeon_trophy")).toList();
assertEquals(1,earned.size(),"only the present represented quest unit receives a head");
assertEquals(killedBossId[0].toString(),earned.getFirst().getAsJsonObject("data")
.get("boss_id").getAsString());
assertFalse(earned.getFirst().toString().contains(savedBossCharacter[0].toString()),
"the boss head must not export the character held in dungeon custody");
assertEquals(1,exported.stream().filter(event->event.get("type").getAsString()
.endsWith("dungeon.completed")).count());
}
@Test void questGiverPaginatesManualDungeonOffersBeyondAutomaticCount() throws Exception {
java.nio.file.Files.writeString(data.resolve("config.yml"),"adventure-daily-site-count: 0\n");try(var f=new FantasyStickLifecycleNativeTest.Fixture(data)){f.plugin.setEnabled(true);var actor=new FantasyStickLifecycleNativeTest.Actor(data);f.players.put(actor.core.id,actor.player);UUID world=UUID.randomUUID();when(actor.world.getUID()).thenReturn(world);f.activate(actor);var directory=(AdventureQuestDirectory)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuests");var sites=new ArrayList<AdventureQuestState.Site>();for(int i=0;i<46;i++)sites.add(new AdventureQuestState.Site(UUID.randomUUID(),AdventureQuestState.SiteType.DUNGEON,"Dungeon "+i,world,i,64,0));var catalog=new AdventureQuestState.Catalog(java.time.LocalDate.now(java.time.ZoneOffset.UTC),UUID.randomUUID(),java.time.Instant.now(),sites.stream().map(AdventureQuestState.Site::id).toList());var field=directory.getClass().getDeclaredField("snapshot");field.setAccessible(true);field.set(directory,new AdventureQuestState(sites,List.of(),catalog,List.of(),Set.of(),Set.of(),Map.of()));var controller=(AdventureQuestController)FantasyStickLifecycleNativeTest.Fixture.field(f.plugin,"adventureQuestController");controller.open(actor.player);drain(f);var first=actor.inventories.getLast();assertEquals(54,first.getSize());for(int slot=0;slot<45;slot++)assertEquals(Material.PAPER,first.getItem(slot).getType());assertEquals(Material.ARROW,first.getItem(53).getType());var click=mock(InventoryClickEvent.class);when(click.getWhoClicked()).thenReturn(actor.player);when(click.getInventory()).thenReturn(first);when(click.getCurrentItem()).thenReturn(first.getItem(53));controller.click(click);drain(f);var second=actor.inventories.getLast();assertNotSame(first,second);assertEquals(Material.PAPER,second.getItem(0).getType());assertTrue(second.getItem(0).getItemMeta().displayName().toString().contains("Dungeon 45"));}
}
private static void command(FantasyStickLifecycleNativeTest.Fixture f,FantasyStickLifecycleNativeTest.Actor actor,String... args)throws Exception{f.command.execute(actor.player,"fantasystick",args);drain(f);}
private static void pulse(FantasyStickLifecycleNativeTest.Fixture f){for(var task:List.copyOf(f.pulses))task.run();}
private static void drain(FantasyStickLifecycleNativeTest.Fixture f)throws Exception{Runnable task;for(int attempt=0;attempt<20;attempt++){task=f.tasks.poll(50,TimeUnit.MILLISECONDS);if(task==null)break;task.run();}}
private static void await(FantasyStickLifecycleNativeTest.Fixture f,java.util.function.BooleanSupplier condition,Object evidence)throws Exception{for(int attempt=0;attempt<50&&!condition.getAsBoolean();attempt++){drain(f);Thread.sleep(5);}assertTrue(condition.getAsBoolean(),String.valueOf(evidence));}
}