diff --git a/src/main/java/net/tfminecraft/advancedcrafting/managers/EquipmentGiveCommand.java b/src/main/java/net/tfminecraft/advancedcrafting/managers/EquipmentGiveCommand.java index 7fc323f..e4a4285 100644 --- a/src/main/java/net/tfminecraft/advancedcrafting/managers/EquipmentGiveCommand.java +++ b/src/main/java/net/tfminecraft/advancedcrafting/managers/EquipmentGiveCommand.java @@ -31,8 +31,8 @@ boolean execute(CommandSender sender, String[] args) { sender.sendMessage("§cNo permission."); return true; } - if (args.length < 4 || args.length > 6) { - sender.sendMessage("§cUsage: /ac give equipment [player] [quality 0-100]"); + if (args.length < 4 || args.length > 7) { + sender.sendMessage("§cUsage: /ac give equipment [player] [quality 0-100] [filler tier]"); return true; } Player target; @@ -49,7 +49,7 @@ boolean execute(CommandSender sender, String[] args) { return true; } double quality = 100; - if (args.length == 6) { + if (args.length >= 6) { try { quality = Double.parseDouble(args[5]); } catch (NumberFormatException ex) { @@ -61,6 +61,18 @@ boolean execute(CommandSender sender, String[] args) { return true; } } + Integer fillerTier = null; + if (args.length == 7) { + try { + fillerTier = Integer.parseInt(args[6]); + } catch (NumberFormatException ex) { + fillerTier = 0; + } + if (fillerTier < 1) { + sender.sendMessage("§cFiller tier must be a whole number of 1 or more."); + return true; + } + } CraftingRecipe recipe = RecipeLoader.getByString(args[2].toLowerCase(Locale.ROOT)); if (recipe == null) { sender.sendMessage("§cUnknown recipe: §f" + args[2]); @@ -75,10 +87,7 @@ boolean execute(CommandSender sender, String[] args) { for (var entry : recipe.getRecipe().entrySet()) { String key = material; if (!entry.getKey().equalsIgnoreCase(recipe.getMainType())) { - Ingredient filler = IngredientLoader.get().stream() - .filter(i -> i.getIngredientData().getType().getId().equalsIgnoreCase(entry.getKey())) - .min(Comparator.comparingInt((Ingredient i) -> i.getIngredientData().getTier()) - .thenComparing(Ingredient::getId)).orElse(null); + Ingredient filler = filler(entry.getKey(), fillerTier); if (filler == null) { sender.sendMessage("§cNo configured ingredient for required type: §f" + entry.getKey()); return true; @@ -102,6 +111,23 @@ boolean execute(CommandSender sender, String[] args) { return true; } + /** + * Lowest-tier ingredient of the type by default. With a filler tier, the highest tier at or below it + * (falling back to the lowest when every ingredient of the type is above it). + */ + private Ingredient filler(String type, Integer fillerTier) { + List ofType = IngredientLoader.get().stream() + .filter(i -> i.getIngredientData().getType().getId().equalsIgnoreCase(type)) + .toList(); + Comparator byTier = Comparator.comparingInt((Ingredient i) -> i.getIngredientData().getTier()); + Ingredient lowest = ofType.stream().min(byTier.thenComparing(Ingredient::getId)).orElse(null); + if (fillerTier == null) return lowest; + return ofType.stream() + .filter(i -> i.getIngredientData().getTier() <= fillerTier) + .max(byTier.thenComparing(Ingredient::getId, Comparator.reverseOrder())) + .orElse(lowest); + } + private List materialsFor(CraftingRecipe recipe) { if (!recipe.getRecipe().containsKey(recipe.getMainType())) return List.of(); Stream ingredients = IngredientLoader.get().stream() @@ -124,6 +150,7 @@ List complete(CommandSender sender, String[] args) { } case 5 -> options = Bukkit.getOnlinePlayers().stream().map(Player::getName).sorted().toList(); case 6 -> options = List.of("0", "25", "50", "75", "90", "100"); + case 7 -> options = List.of("1", "2", "3", "4"); default -> options = List.of(); } String prefix = args[args.length - 1].toLowerCase(Locale.ROOT); diff --git a/src/main/resources/ingredients.yml b/src/main/resources/ingredients.yml index ac8b292..939a96e 100644 --- a/src/main/resources/ingredients.yml +++ b/src/main/resources/ingredients.yml @@ -793,7 +793,7 @@ rare_leather: scheme: leather model-scheme: default stats: - - armor_toughness(0.5) + - armor_toughness(0.25) - critical_strike_chance(10.0) - damage_reduction(4.0) - movement_speed(0.4) @@ -810,7 +810,7 @@ epic_leather: scheme: leather model-scheme: default stats: - - armor_toughness(0.75) + - armor_toughness(0.375) - critical_strike_chance(15.0) - damage_reduction(8.0) - movement_speed(0.5) @@ -827,7 +827,7 @@ legendary_leather: scheme: leather model-scheme: default stats: - - armor_toughness(1.0) + - armor_toughness(0.5) - critical_strike_chance(20.0) - damage_reduction(16.0) - movement_speed(0.6) @@ -861,7 +861,7 @@ rare_feather: scheme: feather model-scheme: default stats: - - armor_toughness(0.5) + - armor_toughness(0.25) - critical_strike_chance(10.0) - damage_reduction(4.0) - movement_speed(0.4) @@ -879,7 +879,7 @@ epic_feather: scheme: feather model-scheme: default stats: - - armor_toughness(0.75) + - armor_toughness(0.375) - critical_strike_chance(15.0) - damage_reduction(8.0) - movement_speed(0.5) @@ -897,7 +897,7 @@ legendary_feather: scheme: feather model-scheme: default stats: - - armor_toughness(1.0) + - armor_toughness(0.5) - critical_strike_chance(20.0) - damage_reduction(16.0) - movement_speed(0.6) @@ -931,7 +931,7 @@ rare_wool: scheme: wool model-scheme: default stats: - - armor_toughness(0.5) + - armor_toughness(0.25) - critical_strike_chance(10.0) - damage_reduction(4.0) - movement_speed(0.4) @@ -948,7 +948,7 @@ epic_wool: scheme: wool model-scheme: default stats: - - armor_toughness(0.75) + - armor_toughness(0.375) - critical_strike_chance(15.0) - damage_reduction(8.0) - movement_speed(0.5) @@ -965,7 +965,7 @@ legendary_wool: scheme: wool model-scheme: default stats: - - armor_toughness(1.0) + - armor_toughness(0.5) - critical_strike_chance(20.0) - damage_reduction(16.0) - movement_speed(0.6) diff --git a/src/main/resources/stats.yml b/src/main/resources/stats.yml index f378412..20aca6c 100644 --- a/src/main/resources/stats.yml +++ b/src/main/resources/stats.yml @@ -176,7 +176,6 @@ light_armor: icon: v.chainmail_helmet base-stats: - armor(0.5) - - armor_toughness(0.5) - movement_speed(0.4) stats: - armor @@ -192,7 +191,7 @@ medium_armor: icon: v.iron_chestplate base-stats: - armor(1.5) - - armor_toughness(1.0) + - armor_toughness(0.5) - movement_speed(-0.2) stats: - armor @@ -208,7 +207,7 @@ heavy_armor: icon: v.netherite_chestplate base-stats: - armor(2.5) - - armor_toughness(2.0) + - armor_toughness(1.5) - movement_speed(-0.4) stats: - armor @@ -224,10 +223,10 @@ infantry_armor: icon: v.leather_helmet base-stats: - armor(1.5) - - armor_toughness(0.5) - movement_speed(0.2) stats: - armor + - armor_toughness - max_health - physical_damage_reduction - projectile_damage_reduction diff --git a/src/test/java/net/tfminecraft/advancedcrafting/EquipmentGiveCommandTest.java b/src/test/java/net/tfminecraft/advancedcrafting/EquipmentGiveCommandTest.java index 2cfb992..0ce3a9c 100644 --- a/src/test/java/net/tfminecraft/advancedcrafting/EquipmentGiveCommandTest.java +++ b/src/test/java/net/tfminecraft/advancedcrafting/EquipmentGiveCommandTest.java @@ -97,7 +97,11 @@ void rejectsUsageTargetsQualityRecipesAndMaterialsBeforeBuilding() throws Except } run(p, "sword", "ingredient.iron"); assertTrue(p.nextMessage().contains("No configured ingredient")); - run(p, "sword", "ingredient.iron", "Staff", "50", "extra"); + for (String tier : List.of("extra", "0", "-1", "2.5")) { + run(p, "sword", "ingredient.iron", "Staff", "50", tier); + assertTrue(p.nextMessage().contains("Filler tier must")); + } + run(p, "sword", "ingredient.iron", "Staff", "50", "4", "extra"); assertTrue(p.nextMessage().contains("Usage")); assertEquals(0, p.getInventory().all(Material.IRON_SWORD).size()); RecipeLoader.map.put("no_main", recipe("main-type: wood\nrecipe: ['metal.1']")); @@ -130,7 +134,9 @@ void grantsSelfConsoleTargetsAlloysAndOverflowWithExactProvenance() throws Excep assertEquals(List.of(), complete(p, "give", "equipment", "missing", "")); assertEquals(List.of("Target"), complete(p, "give", "equipment", "sword", "", "t")); assertEquals(List.of("100"), complete(p, "give", "equipment", "sword", "", "", "1")); - assertEquals(List.of(), complete(p, "give", "equipment", "sword", "", "", "", "")); + assertEquals(List.of("1", "2", "3", "4"), complete(p, "give", "equipment", "sword", "", "", "", "")); + assertEquals(List.of("4"), complete(p, "give", "equipment", "sword", "", "", "", "4")); + assertEquals(List.of(), complete(p, "give", "equipment", "sword", "", "", "", "", "")); try (var nbt = mockStatic(NBTItem.class); var mmos = mockConstruction(LiveMMOItem.class, withSettings().defaultAnswer(RETURNS_DEEP_STUBS), (mmo, ctx) -> { @@ -150,6 +156,15 @@ void grantsSelfConsoleTargetsAlloysAndOverflowWithExactProvenance() throws Excep provenance.getInputs().stream().collect(java.util.stream.Collectors.toMap( input -> input.getKind() + "." + input.getId(), CraftInput::getAmount))); assertTrue(target.getWorld().getEntitiesByClass(Item.class).isEmpty()); + run(p, "sword", "ingredient.iron", "Staff", "100", "4"); + assertEquals(Map.of("ingredient.iron", 4, "ingredient.expensive_leather", 2), inputs(p.getInventory().getItem(1))); + run(p, "sword", "ingredient.iron", "Staff", "100", "1"); + assertEquals(Map.of("ingredient.iron", 4, "ingredient.a_leather", 2), inputs(p.getInventory().getItem(2))); + TypeLoader.map.put("wool", new IngredientType("wool", yaml("name: Wool"))); + ingredient("fine_wool", "type: wool\ntier: 3"); + RecipeLoader.map.put("cloak", recipe("recipe: ['metal.4', 'wool.1']")); + run(p, "cloak", "ingredient.iron", "Staff", "100", "2"); + assertEquals(Map.of("ingredient.iron", 4, "ingredient.fine_wool", 1), inputs(p.getInventory().getItem(3))); var console = mock(CommandSender.class); when(console.hasPermission(AdminPermissions.PERMISSION)).thenReturn(true); run(console, "sword", "alloy.bronze", "Target", "0"); @@ -167,6 +182,11 @@ void grantsSelfConsoleTargetsAlloysAndOverflowWithExactProvenance() throws Excep } } + static Map inputs(ItemStack item) { + return CraftProvenance.readFrom(item).getInputs().stream().collect(java.util.stream.Collectors.toMap( + input -> input.getKind() + "." + input.getId(), CraftInput::getAmount)); + } + @Test void buildingFailuresAreReportedWithoutGrantAndIncompleteBuilderRejects() throws Exception { var p = server.addPlayer();