feat(gear): add final unbreakable Elytra reward
This commit is contained in:
@@ -5,6 +5,7 @@ public enum ArmorClaimStatus {
|
||||
GAME_NOT_RUNNING,
|
||||
NOT_TYRANT,
|
||||
UNLOCKS_INCOMPLETE,
|
||||
COLLECTION_INCOMPLETE,
|
||||
NO_CHOICES,
|
||||
ALREADY_CLAIMED
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@ public final class BukkitTyrantArmorItemFactory implements TyrantArmorItemFactor
|
||||
if (spec.efficiencyLevel() > 0) {
|
||||
meta.addEnchant(Enchantment.EFFICIENCY, spec.efficiencyLevel(), true);
|
||||
}
|
||||
meta.addEnchant(Enchantment.MENDING, spec.mendingLevel(), true);
|
||||
if (spec.mendingLevel() > 0) {
|
||||
meta.addEnchant(Enchantment.MENDING, spec.mendingLevel(), true);
|
||||
}
|
||||
meta.setUnbreakable(spec.unbreakable());
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
inventory.setItem(22, item(Material.NETHERITE_CHESTPLATE,
|
||||
ChatColor.GOLD + "Legacy Rewards", List.of(
|
||||
model.allStandardUnlocksPurchased()
|
||||
? "Choose a once-per-reign armor, tool, or weapon."
|
||||
? "Choose legacy gear; complete the collection for Elytra."
|
||||
: "Purchase all six standard unlocks first.",
|
||||
"Claimed: " + (model.claimedArmor().isEmpty()
|
||||
? "none" : model.claimedArmor())
|
||||
@@ -118,12 +118,10 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
int[] slots = legacyRewardSlots();
|
||||
for (int index = 0; index < pieces.length; index++) {
|
||||
TyrantArmorPiece piece = pieces[index];
|
||||
String status = model.claimedArmor().contains(piece) ? "CLAIMED"
|
||||
: model.armorAvailable(piece) ? "AVAILABLE" : "LOCKED";
|
||||
inventory.setItem(slots[index], item(
|
||||
TyrantArmorSpec.forPiece(piece, player.getName()).material(),
|
||||
ChatColor.GOLD + readable(piece.name()),
|
||||
List.of("Status: " + status, "Cost: 1 choice")
|
||||
model.armorLore(piece)
|
||||
));
|
||||
}
|
||||
inventory.setItem(22, backItem());
|
||||
@@ -146,7 +144,7 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
}
|
||||
|
||||
static int[] legacyRewardSlots() {
|
||||
return new int[] {9, 10, 11, 12, 13, 14, 15, 16, 17};
|
||||
return new int[] {9, 10, 11, 12, 13, 14, 15, 16, 17, 4};
|
||||
}
|
||||
|
||||
private void openArmorConfirmation(Player player, TyrantArmorPiece piece) {
|
||||
|
||||
@@ -10,7 +10,14 @@ public enum TyrantArmorPiece {
|
||||
PICKAXE,
|
||||
SWORD,
|
||||
HOE,
|
||||
SHOVEL;
|
||||
SHOVEL,
|
||||
ELYTRA;
|
||||
|
||||
/** Explicitly excludes the final reward and any future reward additions. */
|
||||
public static java.util.Set<TyrantArmorPiece> regularRewards() {
|
||||
return java.util.Set.of(HELMET, CHESTPLATE, LEGGINGS, BOOTS,
|
||||
AXE, PICKAXE, SWORD, HOE, SHOVEL);
|
||||
}
|
||||
|
||||
public boolean isArmor() {
|
||||
return switch (this) {
|
||||
@@ -26,6 +33,7 @@ public enum TyrantArmorPiece {
|
||||
case "sword" -> SWORD;
|
||||
case "hoe" -> HOE;
|
||||
case "shovel" -> SHOVEL;
|
||||
case "elytra" -> ELYTRA;
|
||||
default -> throw new IllegalArgumentException("Unknown gear piece");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ public final class TyrantArmorService {
|
||||
if (state.claimedArmor().contains(piece)) {
|
||||
return result(state, ArmorClaimStatus.ALREADY_CLAIMED);
|
||||
}
|
||||
if (piece == TyrantArmorPiece.ELYTRA
|
||||
&& !state.claimedArmor().containsAll(TyrantArmorPiece.regularRewards())) {
|
||||
return result(state, ArmorClaimStatus.COLLECTION_INCOMPLETE);
|
||||
}
|
||||
Set<TyrantArmorPiece> claimed = EnumSet.noneOf(TyrantArmorPiece.class);
|
||||
claimed.addAll(state.claimedArmor());
|
||||
claimed.add(piece);
|
||||
|
||||
@@ -10,7 +10,8 @@ public record TyrantArmorSpec(
|
||||
int protectionLevel,
|
||||
int mendingLevel,
|
||||
int sharpnessLevel,
|
||||
int efficiencyLevel
|
||||
int efficiencyLevel,
|
||||
boolean unbreakable
|
||||
) {
|
||||
public TyrantArmorSpec {
|
||||
lore = List.copyOf(lore);
|
||||
@@ -27,19 +28,26 @@ public record TyrantArmorSpec(
|
||||
case SWORD -> Material.NETHERITE_SWORD;
|
||||
case HOE -> Material.NETHERITE_HOE;
|
||||
case SHOVEL -> Material.NETHERITE_SHOVEL;
|
||||
case ELYTRA -> Material.ELYTRA;
|
||||
};
|
||||
String pieceName = readable(piece);
|
||||
boolean elytra = piece == TyrantArmorPiece.ELYTRA;
|
||||
java.util.ArrayList<String> lore = new java.util.ArrayList<>(List.of(
|
||||
"Forged for Tyrant " + tyrantName + ".",
|
||||
"A lasting reward from " + tyrantName + "'s reign."
|
||||
));
|
||||
if (elytra) {
|
||||
lore.add("Earned by completing the Tyrant's legacy collection.");
|
||||
}
|
||||
return new TyrantArmorSpec(
|
||||
material,
|
||||
"Tyrant's " + tyrantName + " " + pieceName,
|
||||
List.of(
|
||||
"Forged for Tyrant " + tyrantName + ".",
|
||||
"A lasting reward from " + tyrantName + "'s reign."
|
||||
),
|
||||
lore,
|
||||
piece.isArmor() ? 5 : 0,
|
||||
1,
|
||||
elytra ? 0 : 1,
|
||||
piece == TyrantArmorPiece.AXE || piece == TyrantArmorPiece.SWORD ? 6 : 0,
|
||||
!piece.isArmor() && piece != TyrantArmorPiece.SWORD ? 6 : 0
|
||||
!piece.isArmor() && piece != TyrantArmorPiece.SWORD && !elytra ? 6 : 0,
|
||||
elytra
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +62,12 @@ public record TyrantArmorSpec(
|
||||
if (efficiencyLevel > 0) {
|
||||
lines.add("Efficiency VI");
|
||||
}
|
||||
lines.add("Mending I");
|
||||
if (mendingLevel > 0) {
|
||||
lines.add("Mending I");
|
||||
}
|
||||
if (unbreakable) {
|
||||
lines.add("Unbreakable");
|
||||
}
|
||||
lines.add("Cost: 1 choice");
|
||||
return List.copyOf(lines);
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
+ (state.claimedArmor().isEmpty() ? "none" : state.claimedArmor()));
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
+ "Use /tyrant armor <helmet|chestplate|leggings|boots> or "
|
||||
+ "/tyrant gear <axe|pickaxe|sword|hoe|shovel>.");
|
||||
+ "/tyrant gear <axe|pickaxe|sword|hoe|shovel|elytra>.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,14 +308,16 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendMessage(ChatColor.RED + (armorCommand
|
||||
? "Unknown armor piece. Use helmet, chestplate, leggings, or boots."
|
||||
: "Unknown gear piece. Use axe, pickaxe, sword, hoe, or shovel."));
|
||||
: "Unknown gear piece. Use axe, pickaxe, sword, hoe, shovel, or elytra."));
|
||||
return;
|
||||
}
|
||||
PersistentState snapshot = stateManager.snapshot();
|
||||
ArmorClaimResult result = armor.claim(snapshot.game(), player.getUniqueId(), piece);
|
||||
if (result.status() != ArmorClaimStatus.CLAIMED) {
|
||||
player.sendMessage(ChatColor.RED + "Could not claim " + piece.commandName()
|
||||
+ ": " + readable(result.status()));
|
||||
+ ": " + (result.status() == ArmorClaimStatus.COLLECTION_INCOMPLETE
|
||||
? "claim all nine regular legacy rewards this reign first"
|
||||
: readable(result.status())));
|
||||
return;
|
||||
}
|
||||
int inventorySlot = player.getInventory().firstEmpty();
|
||||
|
||||
@@ -33,7 +33,31 @@ public record TyrantControlPanelModel(
|
||||
|
||||
public boolean armorAvailable(TyrantArmorPiece piece) {
|
||||
return allStandardUnlocksPurchased() && unspentChoices > 0
|
||||
&& !claimedArmor.contains(piece);
|
||||
&& !claimedArmor.contains(piece)
|
||||
&& (piece != TyrantArmorPiece.ELYTRA
|
||||
|| claimedArmor.containsAll(TyrantArmorPiece.regularRewards()));
|
||||
}
|
||||
|
||||
public java.util.List<String> armorLore(TyrantArmorPiece piece) {
|
||||
java.util.List<String> lines = new java.util.ArrayList<>();
|
||||
lines.add("Status: " + (claimedArmor.contains(piece) ? "CLAIMED"
|
||||
: armorAvailable(piece) ? "AVAILABLE" : "LOCKED"));
|
||||
lines.add("Cost: 1 choice");
|
||||
if (piece == TyrantArmorPiece.ELYTRA) {
|
||||
lines.add("Unbreakable");
|
||||
long regularClaims = TyrantArmorPiece.regularRewards().stream()
|
||||
.filter(claimedArmor::contains).count();
|
||||
if (regularClaims < TyrantArmorPiece.regularRewards().size()) {
|
||||
lines.add("Claim all nine regular legacy rewards first (" + regularClaims + "/9).");
|
||||
}
|
||||
}
|
||||
if (!allStandardUnlocksPurchased()) {
|
||||
lines.add("Purchase all six standard unlocks first.");
|
||||
}
|
||||
if (unspentChoices == 0) {
|
||||
lines.add("Earn another choice first.");
|
||||
}
|
||||
return java.util.List.copyOf(lines);
|
||||
}
|
||||
|
||||
public static TyrantControlPanelModel create(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockConstruction;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedConstruction;
|
||||
|
||||
final class BukkitTyrantArmorItemFactoryTest {
|
||||
@Test
|
||||
void elytraReceivesGoldNameGrayLoreAndUnbreakabilityWithoutAnyEnchantments() {
|
||||
ItemMeta meta = mock(ItemMeta.class);
|
||||
try (MockedConstruction<ItemStack> items = mockConstruction(ItemStack.class, (item, context) -> {
|
||||
assertEquals(List.of(Material.ELYTRA), context.arguments());
|
||||
when(item.getItemMeta()).thenReturn(meta);
|
||||
})) {
|
||||
ItemStack item = new BukkitTyrantArmorItemFactory().create(
|
||||
TyrantArmorPiece.valueOf("ELYTRA"), "Alex"
|
||||
);
|
||||
assertSame(items.constructed().get(0), item);
|
||||
verify(meta).setDisplayName(ChatColor.GOLD + "Tyrant's Alex Elytra");
|
||||
verify(meta).setLore(List.of(
|
||||
ChatColor.GRAY + "Forged for Tyrant Alex.",
|
||||
ChatColor.GRAY + "A lasting reward from Alex's reign.",
|
||||
ChatColor.GRAY + "Earned by completing the Tyrant's legacy collection."
|
||||
));
|
||||
verify(meta).setUnbreakable(true);
|
||||
verify(meta, never()).addEnchant(any(), anyInt(), anyBoolean());
|
||||
verify(item).setItemMeta(meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,6 +77,33 @@ final class TyrantArmorServiceTest {
|
||||
assertEquals(3, state.unspentChoices());
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalElytraCostsOneChoiceAfterExactlyNineRegularClaims() {
|
||||
assertEquals(9, TyrantArmorPiece.regularRewards().size());
|
||||
org.junit.jupiter.api.Assertions.assertFalse(
|
||||
TyrantArmorPiece.regularRewards().contains(TyrantArmorPiece.ELYTRA));
|
||||
GameState before = game(2, allUnlocks(), TyrantArmorPiece.regularRewards());
|
||||
ArmorClaimResult result = service.claim(before, TYRANT, TyrantArmorPiece.ELYTRA);
|
||||
assertEquals(ArmorClaimStatus.CLAIMED, result.status());
|
||||
assertEquals(1, result.state().unspentChoices());
|
||||
assertEquals(10, result.state().claimedArmor().size());
|
||||
assertEquals(ArmorClaimStatus.ALREADY_CLAIMED,
|
||||
service.claim(result.state(), TYRANT, TyrantArmorPiece.ELYTRA).status());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(value = TyrantArmorPiece.class,
|
||||
names = "ELYTRA", mode = org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE)
|
||||
void everyRegularRewardIsRequiredButCanBeBoughtWithoutElytra(TyrantArmorPiece missing) {
|
||||
Set<TyrantArmorPiece> claims = EnumSet.copyOf(TyrantArmorPiece.regularRewards());
|
||||
claims.remove(missing);
|
||||
GameState before = game(2, allUnlocks(), claims);
|
||||
ArmorClaimResult rejected = service.claim(before, TYRANT, TyrantArmorPiece.ELYTRA);
|
||||
assertEquals(ArmorClaimStatus.COLLECTION_INCOMPLETE, rejected.status());
|
||||
assertEquals(before, rejected.state());
|
||||
assertEquals(ArmorClaimStatus.CLAIMED, service.claim(before, TYRANT, missing).status());
|
||||
}
|
||||
|
||||
private static Set<TyrantUnlock> allUnlocks() {
|
||||
return EnumSet.allOf(TyrantUnlock.class);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,68 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
final class TyrantArmorSpecTest {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.CsvSource({
|
||||
"AXE,6,6", "PICKAXE,0,6", "SWORD,6,0", "HOE,0,6", "SHOVEL,0,6"
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"HELMET,Helmet,5,0,0", "CHESTPLATE,Chestplate,5,0,0",
|
||||
"LEGGINGS,Leggings,5,0,0", "BOOTS,Boots,5,0,0",
|
||||
"AXE,Axe,0,6,6", "PICKAXE,Pickaxe,0,0,6", "SWORD,Sword,0,6,0",
|
||||
"HOE,Hoe,0,0,6", "SHOVEL,Shovel,0,0,6"
|
||||
})
|
||||
void toolsHaveExactApprovedEnchantments(String name, int sharpness, int efficiency) {
|
||||
void nineRegularRewardsKeepTheirExactSpecifications(
|
||||
String name, String displayPiece, int protection, int sharpness, int efficiency
|
||||
) {
|
||||
TyrantArmorSpec spec = TyrantArmorSpec.forPiece(TyrantArmorPiece.valueOf(name), "Alex");
|
||||
assertEquals(Material.valueOf("NETHERITE_" + name), spec.material());
|
||||
assertEquals(0, spec.protectionLevel());
|
||||
assertEquals("Tyrant's Alex " + displayPiece, spec.displayName());
|
||||
assertEquals(List.of("Forged for Tyrant Alex.", "A lasting reward from Alex's reign."), spec.lore());
|
||||
assertEquals(protection, spec.protectionLevel());
|
||||
assertEquals(sharpness, spec.sharpnessLevel());
|
||||
assertEquals(efficiency, spec.efficiencyLevel());
|
||||
assertEquals(1, spec.mendingLevel());
|
||||
assertTrue(spec.displayName().startsWith("Tyrant's Alex "));
|
||||
assertEquals(TyrantArmorSpec.forPiece(TyrantArmorPiece.HELMET, "Alex").lore(), spec.lore());
|
||||
assertFalse(spec.unbreakable());
|
||||
List<String> expectedConfirmation = protection > 0
|
||||
? List.of("Protection V", "Mending I", "Cost: 1 choice")
|
||||
: sharpness > 0
|
||||
? efficiency > 0
|
||||
? List.of("Sharpness VI", "Efficiency VI", "Mending I", "Cost: 1 choice")
|
||||
: List.of("Sharpness VI", "Mending I", "Cost: 1 choice")
|
||||
: List.of("Efficiency VI", "Mending I", "Cost: 1 choice");
|
||||
assertEquals(expectedConfirmation, spec.confirmationLore());
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyPieceIsNetheriteWithNamedLoreAndApprovedEnchantments() {
|
||||
assertEquals(Material.NETHERITE_HELMET,
|
||||
TyrantArmorSpec.forPiece(TyrantArmorPiece.HELMET, "Alex").material());
|
||||
assertEquals(Material.NETHERITE_CHESTPLATE,
|
||||
TyrantArmorSpec.forPiece(TyrantArmorPiece.CHESTPLATE, "Alex").material());
|
||||
assertEquals(Material.NETHERITE_LEGGINGS,
|
||||
TyrantArmorSpec.forPiece(TyrantArmorPiece.LEGGINGS, "Alex").material());
|
||||
assertEquals(Material.NETHERITE_BOOTS,
|
||||
TyrantArmorSpec.forPiece(TyrantArmorPiece.BOOTS, "Alex").material());
|
||||
void menuHasTenDistinctRewardSlotsWithoutOverlappingBack() {
|
||||
int[] slots = BukkitTyrantControlPanelRenderer.legacyRewardSlots();
|
||||
assertEquals(TyrantArmorPiece.values().length, slots.length);
|
||||
assertEquals(10, java.util.Arrays.stream(slots).distinct().count());
|
||||
assertTrue(java.util.Arrays.stream(slots).allMatch(slot -> slot >= 0 && slot < 27 && slot != 22));
|
||||
}
|
||||
|
||||
TyrantArmorSpec spec = TyrantArmorSpec.forPiece(TyrantArmorPiece.HELMET, "Alex");
|
||||
assertEquals("Tyrant's Alex Helmet", spec.displayName());
|
||||
assertTrue(spec.lore().stream().anyMatch(line -> line.contains("Alex")));
|
||||
assertEquals(5, spec.protectionLevel());
|
||||
assertEquals(1, spec.mendingLevel());
|
||||
@Test
|
||||
void elytraIsUnbreakableWithoutEnchantmentsAndHasCollectionLore() {
|
||||
TyrantArmorSpec spec = TyrantArmorSpec.forPiece(TyrantArmorPiece.valueOf("ELYTRA"), "Alex");
|
||||
assertEquals(Material.ELYTRA, spec.material());
|
||||
assertEquals("Tyrant's Alex Elytra", spec.displayName());
|
||||
assertEquals(List.of(
|
||||
"Forged for Tyrant Alex.",
|
||||
"A lasting reward from Alex's reign.",
|
||||
"Earned by completing the Tyrant's legacy collection."
|
||||
), spec.lore());
|
||||
assertTrue(spec.unbreakable());
|
||||
assertEquals(0, spec.protectionLevel());
|
||||
assertEquals(0, spec.mendingLevel());
|
||||
assertEquals(0, spec.sharpnessLevel());
|
||||
assertEquals(0, spec.efficiencyLevel());
|
||||
assertEquals(List.of("Unbreakable", "Cost: 1 choice"), spec.confirmationLore());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,12 +103,12 @@ final class TyrantCommandTest {
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(TyrantArmorPiece.class)
|
||||
void fullyUnlockedTyrantCanClaimNamedArmorIntoInventory(TyrantArmorPiece piece) {
|
||||
void eligibleTyrantCanClaimNamedRewardIntoInventoryWithoutPossessingEarlierRewards(TyrantArmorPiece piece) {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
6, 2, EnumSet.allOf(TyrantUnlock.class), Set.of()
|
||||
6, 2, EnumSet.allOf(TyrantUnlock.class), prerequisiteClaims(piece)
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenReturn(new PersistentState(
|
||||
@@ -133,7 +133,7 @@ final class TyrantCommandTest {
|
||||
|
||||
verify(manager).replaceState(org.mockito.ArgumentMatchers.argThat(state ->
|
||||
state.game().unspentChoices() == 1
|
||||
&& state.game().claimedArmor().equals(Set.of(piece))
|
||||
&& state.game().claimedArmor().equals(expectedClaims(piece))
|
||||
));
|
||||
verify(inventory).setItem(4, reward);
|
||||
verify(manager).saveIfDirty();
|
||||
@@ -141,12 +141,12 @@ final class TyrantCommandTest {
|
||||
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(TyrantArmorPiece.class)
|
||||
void fullInventoryDoesNotConsumeChoiceOrCreateArmor(TyrantArmorPiece piece) {
|
||||
void fullInventoryDoesNotConsumeChoiceOrCreateReward(TyrantArmorPiece piece) {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
6, 1, EnumSet.allOf(TyrantUnlock.class), Set.of()
|
||||
6, 1, EnumSet.allOf(TyrantUnlock.class), prerequisiteClaims(piece)
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenReturn(new PersistentState(
|
||||
@@ -168,9 +168,22 @@ final class TyrantCommandTest {
|
||||
|
||||
verify(manager, org.mockito.Mockito.never()).replaceState(any());
|
||||
verify(factory, org.mockito.Mockito.never()).create(any(), any());
|
||||
verify(manager, org.mockito.Mockito.never()).saveIfDirty();
|
||||
verify(inventory, org.mockito.Mockito.never()).setItem(org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(tyrant).sendMessage(org.mockito.ArgumentMatchers.contains("inventory is full"));
|
||||
}
|
||||
|
||||
private static Set<TyrantArmorPiece> prerequisiteClaims(TyrantArmorPiece piece) {
|
||||
return piece == TyrantArmorPiece.ELYTRA ? TyrantArmorPiece.regularRewards() : Set.of();
|
||||
}
|
||||
|
||||
private static Set<TyrantArmorPiece> expectedClaims(TyrantArmorPiece piece) {
|
||||
Set<TyrantArmorPiece> claims = EnumSet.noneOf(TyrantArmorPiece.class);
|
||||
claims.addAll(prerequisiteClaims(piece));
|
||||
claims.add(piece);
|
||||
return claims;
|
||||
}
|
||||
|
||||
@Test
|
||||
void assigningClassNotifiesTyrantAndNewHolder() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
@@ -50,6 +50,33 @@ final class TyrantControlPanelModelTest {
|
||||
assertEquals(Duration.ofSeconds(90), model.intelligenceCooldown());
|
||||
}
|
||||
|
||||
@Test
|
||||
void elytraMenuExplainsMissingPrerequisitesAndRefreshesAfterClaims() {
|
||||
Map<TyrantUnlock, UnlockAvailability> unlocks = new java.util.EnumMap<>(TyrantUnlock.class);
|
||||
for (TyrantUnlock unlock : TyrantUnlock.values()) {
|
||||
unlocks.put(unlock, UnlockAvailability.PURCHASED);
|
||||
}
|
||||
TyrantControlPanelModel incomplete = new TyrantControlPanelModel(
|
||||
16, 1, unlocks, Set.of(), Map.of(), Duration.ZERO);
|
||||
assertFalse(incomplete.armorAvailable(TyrantArmorPiece.ELYTRA));
|
||||
assertTrue(incomplete.armorLore(TyrantArmorPiece.ELYTRA).contains(
|
||||
"Claim all nine regular legacy rewards first (0/9)."));
|
||||
TyrantControlPanelModel ready = new TyrantControlPanelModel(
|
||||
16, 1, unlocks, TyrantArmorPiece.regularRewards(), Map.of(), Duration.ZERO);
|
||||
assertTrue(ready.armorAvailable(TyrantArmorPiece.ELYTRA));
|
||||
assertTrue(ready.armorLore(TyrantArmorPiece.ELYTRA).contains("Status: AVAILABLE"));
|
||||
assertTrue(ready.armorLore(TyrantArmorPiece.ELYTRA).contains("Unbreakable"));
|
||||
TyrantControlPanelModel claimed = new TyrantControlPanelModel(
|
||||
17, 1, unlocks, EnumSet.allOf(TyrantArmorPiece.class), Map.of(), Duration.ZERO);
|
||||
assertFalse(claimed.armorAvailable(TyrantArmorPiece.ELYTRA));
|
||||
assertTrue(claimed.armorLore(TyrantArmorPiece.ELYTRA).contains("Status: CLAIMED"));
|
||||
TyrantControlPanelModel locked = new TyrantControlPanelModel(
|
||||
0, 0, Map.of(), Set.of(), Map.of(), Duration.ZERO);
|
||||
assertTrue(locked.armorLore(TyrantArmorPiece.ELYTRA).contains(
|
||||
"Purchase all six standard unlocks first."));
|
||||
assertTrue(locked.armorLore(TyrantArmorPiece.ELYTRA).contains("Earn another choice first."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void armorAvailabilityRequiresAllUnlocksAChoiceAndAnUnclaimedSlot() {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
final class TyrantGearCommandSafetyTest {
|
||||
@ParameterizedTest
|
||||
@CsvSource({"gear,helmet", "armor,axe", "gear,unknown", "gear,chest"})
|
||||
@CsvSource({"gear,helmet", "armor,axe", "armor,elytra", "gear,unknown", "gear,chest"})
|
||||
void malformedAndCrossCategoryRequestsDoNotMutateState(String category, String name) {
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
TyrantArmorItemFactory factory = mock(TyrantArmorItemFactory.class);
|
||||
@@ -35,13 +35,14 @@ final class TyrantGearCommandSafetyTest {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = TyrantArmorPiece.class, names = {"AXE", "PICKAXE", "SWORD", "HOE", "SHOVEL"})
|
||||
@EnumSource(value = TyrantArmorPiece.class, names = {"AXE", "PICKAXE", "SWORD", "HOE", "SHOVEL", "ELYTRA"})
|
||||
void repeatedCommandsReadAuthoritativeClaimsAndDeliverOnlyOnce(TyrantArmorPiece piece) {
|
||||
UUID id = UUID.randomUUID();
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(id), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
6, 2, EnumSet.allOf(TyrantUnlock.class), Set.of()
|
||||
6, 2, EnumSet.allOf(TyrantUnlock.class),
|
||||
piece == TyrantArmorPiece.ELYTRA ? TyrantArmorPiece.regularRewards() : Set.of()
|
||||
);
|
||||
AtomicReference<PersistentState> state = new AtomicReference<>(new PersistentState(game, Map.of()));
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
@@ -68,6 +69,85 @@ final class TyrantGearCommandSafetyTest {
|
||||
verify(factory).create(piece, "Alex");
|
||||
verify(manager).saveIfDirty();
|
||||
assertEquals(1, state.get().game().unspentChoices());
|
||||
assertEquals(Set.of(piece), state.get().game().claimedArmor());
|
||||
Set<TyrantArmorPiece> expected = EnumSet.noneOf(TyrantArmorPiece.class);
|
||||
expected.addAll(game.claimedArmor());
|
||||
expected.add(piece);
|
||||
assertEquals(expected, state.get().game().claimedArmor());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = TyrantArmorPiece.class, names = "ELYTRA", mode = EnumSource.Mode.EXCLUDE)
|
||||
void elytraRequiresEveryRegularClaimInTheCurrentReign(TyrantArmorPiece missing) {
|
||||
Set<TyrantArmorPiece> claims = EnumSet.copyOf(TyrantArmorPiece.regularRewards());
|
||||
claims.remove(missing);
|
||||
assertElytraRejected(GameLifecycle.RUNNING, true, 1,
|
||||
EnumSet.allOf(TyrantUnlock.class), claims);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TyrantUnlock.class)
|
||||
void elytraRequiresEveryStandardUnlockEvenWithAllNineClaims(TyrantUnlock missing) {
|
||||
Set<TyrantUnlock> unlocks = EnumSet.allOf(TyrantUnlock.class);
|
||||
unlocks.remove(missing);
|
||||
assertElytraRejected(GameLifecycle.RUNNING, true, 1,
|
||||
unlocks, TyrantArmorPiece.regularRewards());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"PAUSED,true,1", "UNSTARTED,true,1", "RUNNING,false,1", "RUNNING,true,0"})
|
||||
void elytraCannotBypassLifecycleAuthorizationOrChoiceCost(
|
||||
GameLifecycle lifecycle, boolean activeTyrant, int choices
|
||||
) {
|
||||
assertElytraRejected(lifecycle, activeTyrant, choices,
|
||||
EnumSet.allOf(TyrantUnlock.class), TyrantArmorPiece.regularRewards());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
void noCurrentReignClaimsCannotQualifyForElytra() {
|
||||
assertElytraRejected(GameLifecycle.RUNNING, true, 1,
|
||||
EnumSet.allOf(TyrantUnlock.class), Set.of());
|
||||
}
|
||||
|
||||
@org.junit.jupiter.api.Test
|
||||
void extraElytraArgumentsDoNotCreateOrSaveRewards() {
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
TyrantArmorItemFactory factory = mock(TyrantArmorItemFactory.class);
|
||||
TyrantCommand command = new TyrantCommand(manager, new TyrantProgressionService(), factory);
|
||||
command.onCommand(mock(Player.class), mock(Command.class), "tyrant",
|
||||
new String[] {"gear", "elytra", "confirm"});
|
||||
verify(manager, never()).replaceState(any());
|
||||
verify(manager, never()).saveIfDirty();
|
||||
verify(factory, never()).create(any(), any());
|
||||
}
|
||||
|
||||
private static void assertElytraRejected(
|
||||
GameLifecycle lifecycle, boolean activeTyrant, int choices,
|
||||
Set<TyrantUnlock> unlocks, Set<TyrantArmorPiece> claims
|
||||
) {
|
||||
UUID id = UUID.randomUUID();
|
||||
GameState game = new GameState(
|
||||
lifecycle, Optional.of(id), Optional.empty(), Optional.empty(),
|
||||
Optional.empty(), lifecycle == GameLifecycle.PAUSED
|
||||
? Optional.of(java.time.Instant.EPOCH) : Optional.empty(),
|
||||
Duration.ZERO, 16, choices, unlocks, claims
|
||||
);
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenReturn(new PersistentState(game, Map.of()));
|
||||
Player player = mock(Player.class);
|
||||
when(player.getUniqueId()).thenReturn(activeTyrant ? id : UUID.randomUUID());
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(inventory.firstEmpty()).thenReturn(0);
|
||||
TyrantArmorItemFactory factory = mock(TyrantArmorItemFactory.class);
|
||||
TyrantCommand command = new TyrantCommand(manager, new TyrantProgressionService(), factory);
|
||||
|
||||
command.onCommand(player, mock(Command.class), "tyrant", new String[] {"gear", "elytra"});
|
||||
|
||||
verify(manager, never()).replaceState(any());
|
||||
verify(manager, never()).saveIfDirty();
|
||||
verify(factory, never()).create(any(), any());
|
||||
verify(inventory, never()).setItem(org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(player).sendMessage(org.mockito.ArgumentMatchers.contains("Could not claim elytra"));
|
||||
assertEquals(game, manager.snapshot().game());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantLegacyRewardPanelTest {
|
||||
@Test
|
||||
void allNineRewardsFitInDistinctSlotsWithoutOverwritingNavigation() {
|
||||
void allTenRewardsFitInDistinctSlotsWithoutOverwritingNavigation() {
|
||||
int[] slots = BukkitTyrantControlPanelRenderer.legacyRewardSlots();
|
||||
assertEquals(9, slots.length);
|
||||
assertEquals(10, slots.length);
|
||||
assertEquals(TyrantArmorPiece.values().length, slots.length);
|
||||
assertEquals(9, Arrays.stream(slots).distinct().count());
|
||||
assertEquals(10, Arrays.stream(slots).distinct().count());
|
||||
assertTrue(Arrays.stream(slots).allMatch(slot -> slot >= 0 && slot < 27 && slot != 22));
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ final class TyrantLegacyRewardPanelTest {
|
||||
assertEquals(spec.protectionLevel() > 0, spec.confirmationLore().contains("Protection V"));
|
||||
assertEquals(spec.sharpnessLevel() > 0, spec.confirmationLore().contains("Sharpness VI"));
|
||||
assertEquals(spec.efficiencyLevel() > 0, spec.confirmationLore().contains("Efficiency VI"));
|
||||
assertTrue(spec.confirmationLore().containsAll(java.util.List.of("Mending I", "Cost: 1 choice")));
|
||||
assertEquals(spec.mendingLevel() > 0, spec.confirmationLore().contains("Mending I"));
|
||||
assertEquals(piece == TyrantArmorPiece.ELYTRA, spec.confirmationLore().contains("Unbreakable"));
|
||||
assertTrue(spec.confirmationLore().contains("Cost: 1 choice"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +45,14 @@ final class TyrantLegacyRewardPanelTest {
|
||||
6, 9, EnumSet.allOf(TyrantUnlock.class), Set.of()
|
||||
);
|
||||
TyrantArmorService service = new TyrantArmorService();
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.values()) {
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.regularRewards()) {
|
||||
TyrantControlPanelModel before = model(game);
|
||||
assertTrue(before.armorAvailable(piece));
|
||||
game = service.claim(game, tyrant, piece).state();
|
||||
assertFalse(model(game).armorAvailable(piece));
|
||||
}
|
||||
assertFalse(model(game).armorAvailable(TyrantArmorPiece.ELYTRA));
|
||||
assertTrue(model(game).armorLore(TyrantArmorPiece.ELYTRA).contains("Earn another choice first."));
|
||||
}
|
||||
|
||||
private static TyrantControlPanelModel model(GameState game) {
|
||||
|
||||
@@ -42,8 +42,14 @@ final class TyrantTabCompleterTest {
|
||||
"menu", "status", "buy", "armor", "assign", "item"
|
||||
)));
|
||||
assertTrue(root.contains("gear"));
|
||||
assertEquals(List.of("axe", "hoe", "pickaxe", "shovel", "sword"),
|
||||
assertEquals(List.of("axe", "elytra", "hoe", "pickaxe", "shovel", "sword"),
|
||||
completer.onTabComplete(player, command, "tyrant", new String[] {"gear", ""}));
|
||||
assertEquals(List.of("elytra"), completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"GEAR", "EL"}));
|
||||
assertEquals(List.of(), completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"gear", "elytra", ""}));
|
||||
assertEquals(List.of(), completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"armor", "el"}));
|
||||
assertEquals(List.of("shovel", "sword"),
|
||||
completer.onTabComplete(player, command, "tyrant", new String[] {"GEAR", "S"}));
|
||||
assertEquals(List.of(), completer.onTabComplete(
|
||||
|
||||
@@ -73,7 +73,7 @@ final class YamlTyrantStateRepositoryTest {
|
||||
GameState loaded = repository.load().game();
|
||||
assertEquals(Set.of(TyrantArmorPiece.HELMET, TyrantArmorPiece.BOOTS), loaded.claimedArmor());
|
||||
TyrantArmorService service = new TyrantArmorService();
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.values()) {
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.regularRewards()) {
|
||||
if (!piece.isArmor()) {
|
||||
ArmorClaimResult result = service.claim(loaded, loaded.tyrantId().orElseThrow(), piece);
|
||||
assertEquals(ArmorClaimStatus.CLAIMED, result.status());
|
||||
@@ -86,6 +86,55 @@ final class YamlTyrantStateRepositoryTest {
|
||||
assertEquals(Set.of(), new YamlTyrantStateRepository(stateFile).load().game().claimedArmor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void oldNineRewardSaveEnablesFinalClaimAndElytraRoundTripsUnderHistoricalKey() throws Exception {
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, """
|
||||
game:
|
||||
lifecycle: RUNNING
|
||||
tyrant-id: 11111111-1111-1111-1111-111111111111
|
||||
tyrant-level: 16
|
||||
unspent-choices: 2
|
||||
purchases: [ASSASSIN, FIXER, TAMER, ROSTER_INTELLIGENCE, RESISTANCE, STRENGTH]
|
||||
claimed-armor: [HELMET, CHESTPLATE, LEGGINGS, BOOTS, AXE, PICKAXE, SWORD, HOE, SHOVEL]
|
||||
""");
|
||||
YamlTyrantStateRepository repository = new YamlTyrantStateRepository(stateFile);
|
||||
GameState loaded = repository.load().game();
|
||||
assertEquals(TyrantArmorPiece.regularRewards(), loaded.claimedArmor());
|
||||
assertEquals(9, loaded.claimedArmor().size());
|
||||
TyrantArmorService service = new TyrantArmorService();
|
||||
ArmorClaimResult claim = service.claim(
|
||||
loaded, loaded.tyrantId().orElseThrow(), TyrantArmorPiece.ELYTRA
|
||||
);
|
||||
assertEquals(ArmorClaimStatus.CLAIMED, claim.status());
|
||||
assertEquals(1, claim.state().unspentChoices());
|
||||
assertEquals(java.util.EnumSet.allOf(TyrantArmorPiece.class), claim.state().claimedArmor());
|
||||
PersistentState expected = new PersistentState(claim.state(), Map.of());
|
||||
repository.save(expected);
|
||||
|
||||
org.bukkit.configuration.file.YamlConfiguration yaml =
|
||||
org.bukkit.configuration.file.YamlConfiguration.loadConfiguration(stateFile.toFile());
|
||||
assertEquals(claim.state().claimedArmor().stream().map(Enum::name).sorted().toList(),
|
||||
yaml.getStringList("game.claimed-armor"));
|
||||
PersistentState restarted = new YamlTyrantStateRepository(stateFile).load();
|
||||
assertEquals(expected, restarted);
|
||||
ArmorClaimResult duplicate = service.claim(
|
||||
restarted.game(), loaded.tyrantId().orElseThrow(), TyrantArmorPiece.ELYTRA
|
||||
);
|
||||
assertEquals(ArmorClaimStatus.ALREADY_CLAIMED, duplicate.status());
|
||||
assertEquals(restarted.game(), duplicate.state());
|
||||
|
||||
LifecycleState nextReign = new TyrantSuccessionService().succeed(
|
||||
restarted, Optional.of(UUID.fromString("22222222-2222-2222-2222-222222222222"))
|
||||
);
|
||||
repository.save(new PersistentState(nextReign.game(), nextReign.players()));
|
||||
GameState reset = new YamlTyrantStateRepository(stateFile).load().game();
|
||||
assertEquals(nextReign.game(), reset);
|
||||
assertEquals(Set.of(), reset.claimedArmor());
|
||||
assertEquals(Set.of(), reset.purchases());
|
||||
assertEquals(1, reset.unspentChoices());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesUnknownFieldsForRetainedState() throws Exception {
|
||||
UUID playerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
Reference in New Issue
Block a user