From 0864df9434eb5387c00c7d84d254705653f43286 Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:31:50 +0100 Subject: [PATCH 1/2] feat: optional filler tier for /ac give equipment /ac give equipment [player] [quality] [filler tier] fills each non-main slot with the highest-tier ingredient of that type at or below the given tier (lowest tier when all are above it). Without the argument it still uses the lowest tier, so existing commands are unchanged. Lets kits hand out gear made with legendary animal materials. Co-Authored-By: Claude Opus 5.5 --- .../managers/EquipmentGiveCommand.java | 41 +++++++++++++++---- .../EquipmentGiveCommandTest.java | 24 ++++++++++- 2 files changed, 56 insertions(+), 9 deletions(-) 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/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(); From 06ac985b0fcc4801420b51dc79898c55e9b2f8bb Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:31:51 +0100 Subject: [PATCH 2/2] config: halve animal toughness and lower armour toughness bases Animal materials (feather, leather, wool) now add 0.25 / 0.375 / 0.5 toughness per piece at rare / epic / legendary. Template toughness bases: light none, medium 0.5, heavy 1.5, infantry none and it now takes toughness from its feathers. Mythril with legendary animal materials gives the intended sets: light 8/2, medium 12/4, heavy 16/8, infantry 12/2. Co-Authored-By: Claude Opus 5.5 --- src/main/resources/ingredients.yml | 18 +++++++++--------- src/main/resources/stats.yml | 7 +++---- 2 files changed, 12 insertions(+), 13 deletions(-) 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