feat(gear): add Tyrant legacy tools and weapons
This commit is contained in:
@@ -11,10 +11,18 @@ public final class BukkitTyrantArmorItemFactory implements TyrantArmorItemFactor
|
||||
public ItemStack create(TyrantArmorPiece piece, String tyrantName) {
|
||||
TyrantArmorSpec spec = TyrantArmorSpec.forPiece(piece, tyrantName);
|
||||
ItemStack item = new ItemStack(spec.material());
|
||||
ItemMeta meta = Objects.requireNonNull(item.getItemMeta(), "Armor metadata is unavailable");
|
||||
ItemMeta meta = Objects.requireNonNull(item.getItemMeta(), "Legacy reward metadata is unavailable");
|
||||
meta.setDisplayName(ChatColor.GOLD + spec.displayName());
|
||||
meta.setLore(spec.lore().stream().map(line -> ChatColor.GRAY + line).toList());
|
||||
meta.addEnchant(Enchantment.PROTECTION, spec.protectionLevel(), true);
|
||||
if (spec.protectionLevel() > 0) {
|
||||
meta.addEnchant(Enchantment.PROTECTION, spec.protectionLevel(), true);
|
||||
}
|
||||
if (spec.sharpnessLevel() > 0) {
|
||||
meta.addEnchant(Enchantment.SHARPNESS, spec.sharpnessLevel(), true);
|
||||
}
|
||||
if (spec.efficiencyLevel() > 0) {
|
||||
meta.addEnchant(Enchantment.EFFICIENCY, spec.efficiencyLevel(), true);
|
||||
}
|
||||
meta.addEnchant(Enchantment.MENDING, spec.mendingLevel(), true);
|
||||
item.setItemMeta(meta);
|
||||
return item;
|
||||
|
||||
@@ -53,9 +53,9 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
inventory.setItem(16, item(Material.SPYGLASS, ChatColor.LIGHT_PURPLE + "Roster Intelligence",
|
||||
List.of(intelligence, "Click to activate when ready.")));
|
||||
inventory.setItem(22, item(Material.NETHERITE_CHESTPLATE,
|
||||
ChatColor.GOLD + "Legacy Armor", List.of(
|
||||
ChatColor.GOLD + "Legacy Rewards", List.of(
|
||||
model.allStandardUnlocksPurchased()
|
||||
? "Choose a once-per-reign armor reward."
|
||||
? "Choose a once-per-reign armor, tool, or weapon."
|
||||
: "Purchase all six standard unlocks first.",
|
||||
"Claimed: " + (model.claimedArmor().isEmpty()
|
||||
? "none" : model.claimedArmor())
|
||||
@@ -113,9 +113,9 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
private void openArmor(Player player) {
|
||||
TyrantControlPanelModel model = model(player);
|
||||
MenuHolder holder = new MenuHolder(MenuView.ARMOR, null, null);
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Legacy Armor");
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Legacy Rewards");
|
||||
TyrantArmorPiece[] pieces = TyrantArmorPiece.values();
|
||||
int[] slots = {10, 12, 14, 16};
|
||||
int[] slots = legacyRewardSlots();
|
||||
for (int index = 0; index < pieces.length; index++) {
|
||||
TyrantArmorPiece piece = pieces[index];
|
||||
String status = model.claimedArmor().contains(piece) ? "CLAIMED"
|
||||
@@ -136,7 +136,7 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
return;
|
||||
}
|
||||
TyrantArmorPiece[] pieces = TyrantArmorPiece.values();
|
||||
int[] slots = {10, 12, 14, 16};
|
||||
int[] slots = legacyRewardSlots();
|
||||
for (int index = 0; index < slots.length; index++) {
|
||||
if (slot == slots[index] && model(player).armorAvailable(pieces[index])) {
|
||||
openArmorConfirmation(player, pieces[index]);
|
||||
@@ -145,16 +145,20 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
}
|
||||
}
|
||||
|
||||
static int[] legacyRewardSlots() {
|
||||
return new int[] {9, 10, 11, 12, 13, 14, 15, 16, 17};
|
||||
}
|
||||
|
||||
private void openArmorConfirmation(Player player, TyrantArmorPiece piece) {
|
||||
MenuHolder holder = new MenuHolder(
|
||||
MenuView.ARMOR_CONFIRMATION, null, null, null, piece
|
||||
);
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Confirm Armor");
|
||||
Inventory inventory = create(holder, 27, TITLE + " — Confirm Reward");
|
||||
inventory.setItem(11, item(Material.RED_WOOL, ChatColor.RED + "Cancel", List.of()));
|
||||
inventory.setItem(13, item(
|
||||
TyrantArmorSpec.forPiece(piece, player.getName()).material(),
|
||||
ChatColor.GOLD + "Tyrant's " + player.getName() + " " + readable(piece.name()),
|
||||
List.of("Protection V", "Mending I", "Cost: 1 choice")
|
||||
TyrantArmorSpec.forPiece(piece, player.getName()).confirmationLore()
|
||||
));
|
||||
inventory.setItem(15, item(Material.LIME_WOOL, ChatColor.GREEN + "Confirm", List.of()));
|
||||
player.openInventory(inventory);
|
||||
@@ -164,7 +168,8 @@ public final class BukkitTyrantControlPanelRenderer
|
||||
if (slot == 11) {
|
||||
openArmor(player);
|
||||
} else if (slot == 15 && piece != null) {
|
||||
player.performCommand("tyrant armor " + piece.commandName());
|
||||
player.performCommand("tyrant " + (piece.isArmor() ? "armor " : "gear ")
|
||||
+ piece.commandName());
|
||||
openArmor(player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ public final class TyrantAdminCommand implements CommandExecutor {
|
||||
sender.sendMessage("Level: " + game.tyrantLevel()
|
||||
+ ", choices: " + game.unspentChoices()
|
||||
+ ", purchases: " + game.purchases()
|
||||
+ ", claimed armor: " + game.claimedArmor());
|
||||
+ ", claimed legacy rewards: " + game.claimedArmor());
|
||||
sender.sendMessage("Pending Tyrant: " + game.pendingTyrant());
|
||||
sender.sendMessage("Pending Vigilante: " + game.pendingVigilante());
|
||||
sender.sendMessage("Shared role arena: " + arenaLocations.location()
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
/** Legacy rewards; the historical type and stored armor key also cover tools and weapons. */
|
||||
public enum TyrantArmorPiece {
|
||||
HELMET,
|
||||
CHESTPLATE,
|
||||
LEGGINGS,
|
||||
BOOTS;
|
||||
BOOTS,
|
||||
AXE,
|
||||
PICKAXE,
|
||||
SWORD,
|
||||
HOE,
|
||||
SHOVEL;
|
||||
|
||||
public boolean isArmor() {
|
||||
return switch (this) {
|
||||
case HELMET, CHESTPLATE, LEGGINGS, BOOTS -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static TyrantArmorPiece fromGearCommand(String value) {
|
||||
return switch (value.toLowerCase(java.util.Locale.ROOT)) {
|
||||
case "axe" -> AXE;
|
||||
case "pickaxe" -> PICKAXE;
|
||||
case "sword" -> SWORD;
|
||||
case "hoe" -> HOE;
|
||||
case "shovel" -> SHOVEL;
|
||||
default -> throw new IllegalArgumentException("Unknown gear piece");
|
||||
};
|
||||
}
|
||||
|
||||
public static TyrantArmorPiece fromCommand(String value) {
|
||||
return switch (value.toLowerCase(java.util.Locale.ROOT)) {
|
||||
|
||||
@@ -8,7 +8,9 @@ public record TyrantArmorSpec(
|
||||
String displayName,
|
||||
List<String> lore,
|
||||
int protectionLevel,
|
||||
int mendingLevel
|
||||
int mendingLevel,
|
||||
int sharpnessLevel,
|
||||
int efficiencyLevel
|
||||
) {
|
||||
public TyrantArmorSpec {
|
||||
lore = List.copyOf(lore);
|
||||
@@ -20,6 +22,11 @@ public record TyrantArmorSpec(
|
||||
case CHESTPLATE -> Material.NETHERITE_CHESTPLATE;
|
||||
case LEGGINGS -> Material.NETHERITE_LEGGINGS;
|
||||
case BOOTS -> Material.NETHERITE_BOOTS;
|
||||
case AXE -> Material.NETHERITE_AXE;
|
||||
case PICKAXE -> Material.NETHERITE_PICKAXE;
|
||||
case SWORD -> Material.NETHERITE_SWORD;
|
||||
case HOE -> Material.NETHERITE_HOE;
|
||||
case SHOVEL -> Material.NETHERITE_SHOVEL;
|
||||
};
|
||||
String pieceName = readable(piece);
|
||||
return new TyrantArmorSpec(
|
||||
@@ -29,11 +36,29 @@ public record TyrantArmorSpec(
|
||||
"Forged for Tyrant " + tyrantName + ".",
|
||||
"A lasting reward from " + tyrantName + "'s reign."
|
||||
),
|
||||
5,
|
||||
1
|
||||
piece.isArmor() ? 5 : 0,
|
||||
1,
|
||||
piece == TyrantArmorPiece.AXE || piece == TyrantArmorPiece.SWORD ? 6 : 0,
|
||||
!piece.isArmor() && piece != TyrantArmorPiece.SWORD ? 6 : 0
|
||||
);
|
||||
}
|
||||
|
||||
public List<String> confirmationLore() {
|
||||
java.util.ArrayList<String> lines = new java.util.ArrayList<>();
|
||||
if (protectionLevel > 0) {
|
||||
lines.add("Protection V");
|
||||
}
|
||||
if (sharpnessLevel > 0) {
|
||||
lines.add("Sharpness VI");
|
||||
}
|
||||
if (efficiencyLevel > 0) {
|
||||
lines.add("Efficiency VI");
|
||||
}
|
||||
lines.add("Mending I");
|
||||
lines.add("Cost: 1 choice");
|
||||
return List.copyOf(lines);
|
||||
}
|
||||
|
||||
private static String readable(TyrantArmorPiece piece) {
|
||||
String lower = piece.name().toLowerCase(java.util.Locale.ROOT);
|
||||
return Character.toUpperCase(lower.charAt(0)) + lower.substring(1);
|
||||
|
||||
@@ -207,8 +207,12 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
buy(player, arguments[1]);
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("gear")) {
|
||||
claimLegacyReward(player, arguments[1], false);
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 2 && arguments[0].equalsIgnoreCase("armor")) {
|
||||
claimArmor(player, arguments[1]);
|
||||
claimLegacyReward(player, arguments[1], true);
|
||||
return true;
|
||||
}
|
||||
if (arguments.length == 3 && arguments[0].equalsIgnoreCase("assign")) {
|
||||
@@ -241,7 +245,7 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
}
|
||||
player.sendMessage(
|
||||
ChatColor.YELLOW
|
||||
+ "Usage: /tyrant <menu|status|choices|buy|armor|assign|item|intelligence|optout|optin|"
|
||||
+ "Usage: /tyrant <menu|status|choices|buy|armor|gear|assign|item|intelligence|optout|optin|"
|
||||
+ "relinquish confirm>"
|
||||
);
|
||||
return true;
|
||||
@@ -264,10 +268,11 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
player.sendMessage(ChatColor.YELLOW + "[" + status + "] " + unlock.name());
|
||||
}
|
||||
if (state.purchases().size() == TyrantUnlock.values().length) {
|
||||
player.sendMessage(ChatColor.GOLD + "Legacy armor claimed: "
|
||||
player.sendMessage(ChatColor.GOLD + "Legacy rewards claimed: "
|
||||
+ (state.claimedArmor().isEmpty() ? "none" : state.claimedArmor()));
|
||||
player.sendMessage(ChatColor.YELLOW
|
||||
+ "Use /tyrant armor <helmet|chestplate|leggings|boots>.");
|
||||
+ "Use /tyrant armor <helmet|chestplate|leggings|boots> or "
|
||||
+ "/tyrant gear <axe|pickaxe|sword|hoe|shovel>.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,13 +300,15 @@ public final class TyrantCommand implements CommandExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private void claimArmor(Player player, String requestedPiece) {
|
||||
private void claimLegacyReward(Player player, String requestedPiece, boolean armorCommand) {
|
||||
TyrantArmorPiece piece;
|
||||
try {
|
||||
piece = TyrantArmorPiece.fromCommand(requestedPiece);
|
||||
piece = armorCommand ? TyrantArmorPiece.fromCommand(requestedPiece)
|
||||
: TyrantArmorPiece.fromGearCommand(requestedPiece);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendMessage(ChatColor.RED
|
||||
+ "Unknown armor piece. Use helmet, chestplate, leggings, or boots.");
|
||||
player.sendMessage(ChatColor.RED + (armorCommand
|
||||
? "Unknown armor piece. Use helmet, chestplate, leggings, or boots."
|
||||
: "Unknown gear piece. Use axe, pickaxe, sword, hoe, or shovel."));
|
||||
return;
|
||||
}
|
||||
PersistentState snapshot = stateManager.snapshot();
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.bukkit.entity.Player;
|
||||
|
||||
public final class TyrantTabCompleter implements TabCompleter {
|
||||
private static final List<String> SUBCOMMANDS = List.of(
|
||||
"menu", "status", "choices", "buy", "armor", "assign", "item", "intelligence",
|
||||
"menu", "status", "choices", "buy", "armor", "gear", "assign", "item", "intelligence",
|
||||
"optout", "optin", "relinquish"
|
||||
);
|
||||
private static final List<String> UNLOCKS = java.util.Arrays.stream(TyrantUnlock.values())
|
||||
@@ -20,6 +20,11 @@ public final class TyrantTabCompleter implements TabCompleter {
|
||||
.toList();
|
||||
private static final List<String> CLASSES = List.of("assassin", "fixer", "tamer");
|
||||
private static final List<String> ARMOR = java.util.Arrays.stream(TyrantArmorPiece.values())
|
||||
.filter(TyrantArmorPiece::isArmor)
|
||||
.map(TyrantArmorPiece::commandName)
|
||||
.toList();
|
||||
private static final List<String> GEAR = java.util.Arrays.stream(TyrantArmorPiece.values())
|
||||
.filter(piece -> !piece.isArmor())
|
||||
.map(TyrantArmorPiece::commandName)
|
||||
.toList();
|
||||
private final TyrantStateManager stateManager;
|
||||
@@ -51,6 +56,7 @@ public final class TyrantTabCompleter implements TabCompleter {
|
||||
return switch (subcommand) {
|
||||
case "buy" -> TabSuggestions.matching(arguments[1], UNLOCKS);
|
||||
case "armor" -> TabSuggestions.matching(arguments[1], ARMOR);
|
||||
case "gear" -> TabSuggestions.matching(arguments[1], GEAR);
|
||||
case "assign" -> TabSuggestions.matching(arguments[1], CLASSES);
|
||||
case "relinquish" -> TabSuggestions.matching(
|
||||
arguments[1], List.of("confirm")
|
||||
|
||||
@@ -193,6 +193,7 @@ public final class YamlTyrantStateRepository {
|
||||
yaml.set("game.tyrant-level", game.tyrantLevel());
|
||||
yaml.set("game.unspent-choices", game.unspentChoices());
|
||||
yaml.set("game.purchases", game.purchases().stream().map(Enum::name).sorted().toList());
|
||||
// Historical key deliberately stores all legacy rewards, including tools and weapons.
|
||||
yaml.set(
|
||||
"game.claimed-armor",
|
||||
game.claimedArmor().stream().map(Enum::name).sorted().toList()
|
||||
|
||||
@@ -7,7 +7,7 @@ author: dmg.games
|
||||
commands:
|
||||
tyrant:
|
||||
description: View and use Spigot Tyrant game features.
|
||||
usage: /tyrant <menu|status|choices|buy|armor <helmet|chestplate|leggings|boots>|assign|item|intelligence|optout|optin|relinquish confirm>
|
||||
usage: /tyrant <menu|status|choices|buy|armor <helmet|chestplate|leggings|boots>|gear <axe|pickaxe|sword|hoe|shovel>|assign|item|intelligence|optout|optin|relinquish confirm>
|
||||
vigilante:
|
||||
description: Manage Vigilante Followers.
|
||||
usage: /vigilante <menu|item|invite <player>|accept|dismiss <player>|leave>
|
||||
|
||||
@@ -36,8 +36,9 @@ final class TyrantArmorServiceTest {
|
||||
assertEquals(before, result.state());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsIncompleteUnlocksNoChoicesNonTyrantsAndPausedGames() {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(TyrantArmorPiece.class)
|
||||
void rejectsIncompleteUnlocksNoChoicesNonTyrantsAndPausedGames(TyrantArmorPiece piece) {
|
||||
GameState complete = game(0, allUnlocks(), Set.of());
|
||||
GameState incomplete = game(1, Set.of(TyrantUnlock.ASSASSIN), Set.of());
|
||||
GameState paused = new GameState(
|
||||
@@ -48,14 +49,32 @@ final class TyrantArmorServiceTest {
|
||||
);
|
||||
|
||||
assertEquals(ArmorClaimStatus.NO_CHOICES,
|
||||
service.claim(complete, TYRANT, TyrantArmorPiece.HELMET).status());
|
||||
service.claim(complete, TYRANT, piece).status());
|
||||
assertEquals(ArmorClaimStatus.UNLOCKS_INCOMPLETE,
|
||||
service.claim(incomplete, TYRANT, TyrantArmorPiece.HELMET).status());
|
||||
service.claim(incomplete, TYRANT, piece).status());
|
||||
assertEquals(ArmorClaimStatus.NOT_TYRANT,
|
||||
service.claim(game(1, allUnlocks(), Set.of()), OTHER,
|
||||
TyrantArmorPiece.HELMET).status());
|
||||
service.claim(game(1, allUnlocks(), Set.of()), OTHER, piece).status());
|
||||
assertEquals(ArmorClaimStatus.GAME_NOT_RUNNING,
|
||||
service.claim(paused, TYRANT, TyrantArmorPiece.HELMET).status());
|
||||
service.claim(paused, TYRANT, piece).status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nineIndependentRewardsCanBePurchasedToolsFirstAndNeverRepeated() {
|
||||
GameState state = game(12, allUnlocks(), Set.of());
|
||||
String[] order = {"AXE", "PICKAXE", "SWORD", "HOE", "SHOVEL",
|
||||
"BOOTS", "HELMET", "LEGGINGS", "CHESTPLATE"};
|
||||
for (String name : order) {
|
||||
TyrantArmorPiece piece = TyrantArmorPiece.valueOf(name);
|
||||
ArmorClaimResult claim = service.claim(state, TYRANT, piece);
|
||||
assertEquals(ArmorClaimStatus.CLAIMED, claim.status());
|
||||
assertEquals(state.unspentChoices() - 1, claim.state().unspentChoices());
|
||||
state = claim.state();
|
||||
assertEquals(ArmorClaimStatus.ALREADY_CLAIMED,
|
||||
service.claim(state, TYRANT, piece).status());
|
||||
assertEquals(state, service.claim(state, TYRANT, piece).state());
|
||||
}
|
||||
assertEquals(9, state.claimedArmor().size());
|
||||
assertEquals(3, state.unspentChoices());
|
||||
}
|
||||
|
||||
private static Set<TyrantUnlock> allUnlocks() {
|
||||
|
||||
@@ -7,6 +7,21 @@ import org.bukkit.Material;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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"
|
||||
})
|
||||
void toolsHaveExactApprovedEnchantments(String name, int sharpness, int efficiency) {
|
||||
TyrantArmorSpec spec = TyrantArmorSpec.forPiece(TyrantArmorPiece.valueOf(name), "Alex");
|
||||
assertEquals(Material.valueOf("NETHERITE_" + name), spec.material());
|
||||
assertEquals(0, 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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyPieceIsNetheriteWithNamedLoreAndApprovedEnchantments() {
|
||||
assertEquals(Material.NETHERITE_HELMET,
|
||||
|
||||
@@ -101,8 +101,9 @@ final class TyrantCommandTest {
|
||||
&& message.contains("ASSASSIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullyUnlockedTyrantCanClaimNamedArmorIntoInventory() {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(TyrantArmorPiece.class)
|
||||
void fullyUnlockedTyrantCanClaimNamedArmorIntoInventory(TyrantArmorPiece piece) {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
@@ -121,25 +122,26 @@ final class TyrantCommandTest {
|
||||
when(tyrant.getInventory()).thenReturn(inventory);
|
||||
ItemStack reward = mock(ItemStack.class);
|
||||
TyrantArmorItemFactory factory = mock(TyrantArmorItemFactory.class);
|
||||
when(factory.create(TyrantArmorPiece.HELMET, "Alex")).thenReturn(reward);
|
||||
when(factory.create(piece, "Alex")).thenReturn(reward);
|
||||
TyrantCommand command = new TyrantCommand(
|
||||
manager, new TyrantProgressionService(), factory
|
||||
);
|
||||
|
||||
command.onCommand(
|
||||
tyrant, mock(Command.class), "tyrant", new String[] {"armor", "helmet"}
|
||||
tyrant, mock(Command.class), "tyrant", new String[] {piece.isArmor() ? "armor" : "gear", piece.commandName()}
|
||||
);
|
||||
|
||||
verify(manager).replaceState(org.mockito.ArgumentMatchers.argThat(state ->
|
||||
state.game().unspentChoices() == 1
|
||||
&& state.game().claimedArmor().equals(Set.of(TyrantArmorPiece.HELMET))
|
||||
&& state.game().claimedArmor().equals(Set.of(piece))
|
||||
));
|
||||
verify(inventory).setItem(4, reward);
|
||||
verify(manager).saveIfDirty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullInventoryDoesNotConsumeChoiceOrCreateArmor() {
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.EnumSource(TyrantArmorPiece.class)
|
||||
void fullInventoryDoesNotConsumeChoiceOrCreateArmor(TyrantArmorPiece piece) {
|
||||
UUID tyrantId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrantId), Optional.empty(),
|
||||
@@ -161,7 +163,7 @@ final class TyrantCommandTest {
|
||||
);
|
||||
|
||||
command.onCommand(
|
||||
tyrant, mock(Command.class), "tyrant", new String[] {"armor", "boots"}
|
||||
tyrant, mock(Command.class), "tyrant", new String[] {piece.isArmor() ? "armor" : "gear", piece.commandName()}
|
||||
);
|
||||
|
||||
verify(manager, org.mockito.Mockito.never()).replaceState(any());
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package games.dmg.spigottyrant;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
final class TyrantGearCommandSafetyTest {
|
||||
@ParameterizedTest
|
||||
@CsvSource({"gear,helmet", "armor,axe", "gear,unknown", "gear,chest"})
|
||||
void malformedAndCrossCategoryRequestsDoNotMutateState(String category, String name) {
|
||||
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[] {category, name});
|
||||
verify(manager, never()).replaceState(any());
|
||||
verify(factory, never()).create(any(), any());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = TyrantArmorPiece.class, names = {"AXE", "PICKAXE", "SWORD", "HOE", "SHOVEL"})
|
||||
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()
|
||||
);
|
||||
AtomicReference<PersistentState> state = new AtomicReference<>(new PersistentState(game, Map.of()));
|
||||
TyrantStateManager manager = mock(TyrantStateManager.class);
|
||||
when(manager.snapshot()).thenAnswer(invocation -> state.get());
|
||||
org.mockito.Mockito.doAnswer(invocation -> {
|
||||
LifecycleState replacement = invocation.getArgument(0);
|
||||
state.set(new PersistentState(replacement.game(), replacement.players()));
|
||||
return null;
|
||||
}).when(manager).replaceState(any());
|
||||
Player player = mock(Player.class);
|
||||
PlayerInventory inventory = mock(PlayerInventory.class);
|
||||
when(player.getUniqueId()).thenReturn(id);
|
||||
when(player.getName()).thenReturn("Alex");
|
||||
when(player.getInventory()).thenReturn(inventory);
|
||||
when(inventory.firstEmpty()).thenReturn(0);
|
||||
ItemStack item = mock(ItemStack.class);
|
||||
TyrantArmorItemFactory factory = mock(TyrantArmorItemFactory.class);
|
||||
when(factory.create(piece, "Alex")).thenReturn(item);
|
||||
TyrantCommand command = new TyrantCommand(manager, new TyrantProgressionService(), factory);
|
||||
String[] args = {"GEAR", piece.name()};
|
||||
command.onCommand(player, mock(Command.class), "tyrant", args);
|
||||
command.onCommand(player, mock(Command.class), "tyrant", args);
|
||||
verify(inventory).setItem(0, item);
|
||||
verify(factory).create(piece, "Alex");
|
||||
verify(manager).saveIfDirty();
|
||||
assertEquals(1, state.get().game().unspentChoices());
|
||||
assertEquals(Set.of(piece), state.get().game().claimedArmor());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class TyrantLegacyRewardPanelTest {
|
||||
@Test
|
||||
void allNineRewardsFitInDistinctSlotsWithoutOverwritingNavigation() {
|
||||
int[] slots = BukkitTyrantControlPanelRenderer.legacyRewardSlots();
|
||||
assertEquals(9, slots.length);
|
||||
assertEquals(TyrantArmorPiece.values().length, slots.length);
|
||||
assertEquals(9, Arrays.stream(slots).distinct().count());
|
||||
assertTrue(Arrays.stream(slots).allMatch(slot -> slot >= 0 && slot < 27 && slot != 22));
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmationShowsExactEnchantmentsAndCostForEveryReward() {
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.values()) {
|
||||
TyrantArmorSpec spec = TyrantArmorSpec.forPiece(piece, "Alex");
|
||||
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")));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void toolAvailabilityDoesNotRequireArmorAndClaimedToolsBecomeUnavailable() {
|
||||
UUID tyrant = UUID.randomUUID();
|
||||
GameState game = new GameState(
|
||||
GameLifecycle.RUNNING, Optional.of(tyrant), Optional.empty(),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
6, 9, EnumSet.allOf(TyrantUnlock.class), Set.of()
|
||||
);
|
||||
TyrantArmorService service = new TyrantArmorService();
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.values()) {
|
||||
TyrantControlPanelModel before = model(game);
|
||||
assertTrue(before.armorAvailable(piece));
|
||||
game = service.claim(game, tyrant, piece).state();
|
||||
assertFalse(model(game).armorAvailable(piece));
|
||||
}
|
||||
}
|
||||
|
||||
private static TyrantControlPanelModel model(GameState game) {
|
||||
return TyrantControlPanelModel.create(game,
|
||||
PlayerState.newPlayer(game.tyrantId().orElseThrow(), "Alex"), Map.of(),
|
||||
java.time.Instant.EPOCH);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ final class TyrantSuccessionServiceTest {
|
||||
GameLifecycle.RUNNING, Optional.of(OLD_TYRANT), Optional.of(OLD_VIGILANTE),
|
||||
Optional.empty(), Optional.empty(), Optional.empty(), Duration.ZERO,
|
||||
5, 2, Set.of(TyrantUnlock.ASSASSIN, TyrantUnlock.STRENGTH),
|
||||
Set.of(TyrantArmorPiece.CHESTPLATE)
|
||||
java.util.EnumSet.allOf(TyrantArmorPiece.class)
|
||||
);
|
||||
Map<UUID, PlayerState> players = Map.of(
|
||||
OLD_TYRANT, active(new PlayerState(
|
||||
|
||||
@@ -41,6 +41,15 @@ final class TyrantTabCompleterTest {
|
||||
assertTrue(root.containsAll(List.of(
|
||||
"menu", "status", "buy", "armor", "assign", "item"
|
||||
)));
|
||||
assertTrue(root.contains("gear"));
|
||||
assertEquals(List.of("axe", "hoe", "pickaxe", "shovel", "sword"),
|
||||
completer.onTabComplete(player, command, "tyrant", new String[] {"gear", ""}));
|
||||
assertEquals(List.of("shovel", "sword"),
|
||||
completer.onTabComplete(player, command, "tyrant", new String[] {"GEAR", "S"}));
|
||||
assertEquals(List.of(), completer.onTabComplete(
|
||||
player, command, "tyrant", new String[] {"gear", "axe", ""}));
|
||||
assertEquals(List.of("boots", "chestplate", "helmet", "leggings"),
|
||||
completer.onTabComplete(player, command, "tyrant", new String[] {"armor", ""}));
|
||||
assertFalse(root.contains("SomePlayer"));
|
||||
assertEquals(List.of("roster_intelligence"), buy);
|
||||
assertEquals(List.of("chestplate"), armor);
|
||||
|
||||
@@ -57,6 +57,35 @@ final class YamlTyrantStateRepositoryTest {
|
||||
assertEquals(expected, repository.load());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyArmorLoadsWithoutGrantingToolsAndMixedClaimsSurviveRestart() throws Exception {
|
||||
Path stateFile = temporaryDirectory.resolve("state.yml");
|
||||
Files.writeString(stateFile, """
|
||||
game:
|
||||
lifecycle: RUNNING
|
||||
tyrant-id: 11111111-1111-1111-1111-111111111111
|
||||
tyrant-level: 6
|
||||
unspent-choices: 9
|
||||
purchases: [ASSASSIN, FIXER, TAMER, ROSTER_INTELLIGENCE, RESISTANCE, STRENGTH]
|
||||
claimed-armor: [HELMET, BOOTS]
|
||||
""");
|
||||
YamlTyrantStateRepository repository = new YamlTyrantStateRepository(stateFile);
|
||||
GameState loaded = repository.load().game();
|
||||
assertEquals(Set.of(TyrantArmorPiece.HELMET, TyrantArmorPiece.BOOTS), loaded.claimedArmor());
|
||||
TyrantArmorService service = new TyrantArmorService();
|
||||
for (TyrantArmorPiece piece : TyrantArmorPiece.values()) {
|
||||
if (!piece.isArmor()) {
|
||||
ArmorClaimResult result = service.claim(loaded, loaded.tyrantId().orElseThrow(), piece);
|
||||
assertEquals(ArmorClaimStatus.CLAIMED, result.status());
|
||||
loaded = result.state();
|
||||
}
|
||||
}
|
||||
repository.save(new PersistentState(loaded, Map.of()));
|
||||
assertEquals(loaded, new YamlTyrantStateRepository(stateFile).load().game());
|
||||
Files.writeString(stateFile, "game: {}\n");
|
||||
assertEquals(Set.of(), new YamlTyrantStateRepository(stateFile).load().game().claimedArmor());
|
||||
}
|
||||
|
||||
@Test
|
||||
void preservesUnknownFieldsForRetainedState() throws Exception {
|
||||
UUID playerId = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
||||
|
||||
Reference in New Issue
Block a user