From 567fe463e3504f41a4511a0c148ba8cc9a29fe8f Mon Sep 17 00:00:00 2001 From: XxFran10xX <318299142+XxFran10xX@users.noreply.github.com> Date: Sat, 17 Oct 2026 11:29:05 +0200 Subject: [PATCH] feat: show finish percents and hits done like the other crafting benches Finishing a woodworking project now shows Recipe, Hits and Total percents in chat, as goldsmithing does. A ruined piece still loses its materials and keeps its title, but now shows how close it came. Only an exact recipe and exact hits read 100. The branding tool's right-click status now lists every tool with the hits done and their total, plus the finish hint, like AdvancedCrafting and GemInfusion. Hit-type needed counts are no longer listed there. Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- .../woodworking/station/StationManager.java | 29 ++++++++++--- .../woodworking/station/WoodStation.java | 33 +++++++++++++++ .../tfminecraft/woodworking/DomainTest.java | 42 +++++++++++++++++++ .../woodworking/StationManagerTest.java | 28 +++++++++++++ 5 files changed, 128 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bbc8948..1b5fedf 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Woodworking turns furniture production into a practical workshop activity. Playe ## From materials to furniture -Players need to know their chosen design: the bench accepts materials and tool actions even when they do not match it. A completed attempt produces furniture only when the recipe is correct; a wrong mix ruins the project. Cancelling unfinished work returns its materials. See the project guide for the full workbench rules. +Players need to know their chosen design: the bench accepts materials and tool actions even when they do not match it. A completed attempt produces furniture only when the recipe is correct; a wrong mix ruins the project. Either way, finishing shows the Recipe, Hits and Total percents, like the other crafting stations. Cancelling unfinished work returns its materials. See the project guide for the full workbench rules. ## Documentation diff --git a/src/main/java/net/tfminecraft/woodworking/station/StationManager.java b/src/main/java/net/tfminecraft/woodworking/station/StationManager.java index 6c30e29..c4b4556 100644 --- a/src/main/java/net/tfminecraft/woodworking/station/StationManager.java +++ b/src/main/java/net/tfminecraft/woodworking/station/StationManager.java @@ -363,13 +363,29 @@ private void sendStatus(Player p, WoodStation station) { IntCounter c = e.getValue(); p.sendMessage(MaterialTypeLoader.display(e.getKey()) + "§7: §e" + c.getCurrent() + "/" + c.getNeeded()); } - for (Map.Entry e : station.getHitTypes().entrySet()) { - IntCounter c = e.getValue(); - p.sendMessage(e.getKey().getName() + "§7: §e" + c.getCurrent() + "/" + c.getNeeded()); + // Like the other crafting benches: each tool with the hits done, never what it needs. + p.sendMessage("§7Hits done:"); + int total = 0; + for (CraftingHit hit : HitLoader.get().values()) { + IntCounter c = station.getHits().get(hit); + int done = c == null ? 0 : c.getCurrent(); + total += done; + p.sendMessage(hit.getName() + "§7: §e" + done); } + p.sendMessage("§7Total: §e" + total); + p.sendMessage("§7Left-click branding to finish"); p.sendMessage("§cSHIFT + LEFT CLICK with the branding tool to cancel the project!"); } + /** The finish percents, as goldsmithing shows them, so a ruined piece still tells how close it came. */ + private void sendPercents(Player p, WoodStation station) { + int recipe = station.getRecipePercent(); + int hits = station.getHitPercent(); + p.sendMessage("§7Recipe: §e" + recipe + "%"); + p.sendMessage("§7Hits: §e" + hits + "%"); + p.sendMessage("§7Total: §e" + Math.min(recipe, hits) + "%"); + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") private void completeCraft(Player p, WoodStation station) { @@ -393,18 +409,21 @@ private void completeCraft(Player p, WoodStation station) { Quality q = station.getQuality(); String qName = q == null ? "" : q.getName(); p.sendTitle("§aYou made a " + project.getName(), qName, 5, 40, 10); + sendPercents(p, station); + if (q != null) p.sendMessage("§7Quality: " + qName); station.cancel(); remove(station.getLoc()); } - /** A wrong recipe or hit mix destroys the work. Deposited materials are not refunded. */ + /** A wrong recipe or hit mix destroys the work. Deposited materials are not refunded; only the percents are shown. */ // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") private void failCraft(Player p, WoodStation station) { String name = station.getProject().getName(); + p.sendTitle("§cThe craft failed", "§7" + name + " §7was ruined and the materials were lost", 5, 40, 10); + sendPercents(p, station); station.cancel(); remove(station.getLoc()); - p.sendTitle("§cThe craft failed", "§7" + name + " §7was ruined and the materials were lost", 5, 40, 10); Location loc = station.getLoc().clone().add(0.5, 1, 0.5); if (loc.getWorld() != null) { loc.getWorld().playSound(loc, Sound.ENTITY_ITEM_BREAK, 1f, 0.8f); diff --git a/src/main/java/net/tfminecraft/woodworking/station/WoodStation.java b/src/main/java/net/tfminecraft/woodworking/station/WoodStation.java index c376998..3314a7d 100644 --- a/src/main/java/net/tfminecraft/woodworking/station/WoodStation.java +++ b/src/main/java/net/tfminecraft/woodworking/station/WoodStation.java @@ -255,6 +255,39 @@ public Quality getQuality() { return QualityLoader.getByAmount(calculatePercentage()); } + /** + * Recipe share shown on finishing, as goldsmithing shows it. Missing, wrong and extra + * materials all count against it, so only the exact recipe reads 100. + */ + public int getRecipePercent() { + if (project == null) return 0; + Map recipe = project.getRecipe(); + int needed = 0; + double sum = 0; + for (Map.Entry e : recipe.entrySet()) { + int required = e.getValue(); + needed += required; + sum += Math.min(depositedByMaterial.getOrDefault(e.getKey(), 0), required) / (double) required; + } + double percent = recipe.isEmpty() ? 0 : sum / recipe.size() * 100.0; + int total = 0; + for (int amount : depositedByMaterial.values()) total += amount; + // Materials beyond the recipe would leave the share at 100, so they dilute it. + if (total > needed) percent = Math.min(percent, Math.floor(100.0 * needed / total)); + return shownPercent(percent, checkExactRecipe()); + } + + /** Hits share shown on finishing. Hits with tools the piece does not need count against it. */ + public int getHitPercent() { + return shownPercent(calculatePercentage(), checkExactHits()); + } + + /** Only an exact match reads 100, so a near miss that rounds up shows 99. */ + private static int shownPercent(double percent, boolean exact) { + if (exact) return 100; + return (int) Math.min(99, Math.round(percent)); + } + /** Clears the project and returns deposited items for the caller to refund. */ public List cancel() { List refund = new ArrayList<>(deposited); diff --git a/src/test/java/net/tfminecraft/woodworking/DomainTest.java b/src/test/java/net/tfminecraft/woodworking/DomainTest.java index 1ba764a..f071ea0 100644 --- a/src/test/java/net/tfminecraft/woodworking/DomainTest.java +++ b/src/test/java/net/tfminecraft/woodworking/DomainTest.java @@ -165,6 +165,48 @@ void stationRequiresMaterialsAndExactHitsBeforeFinishing() { assertTrue(s.getTypes().isEmpty()); } + @Test + void finishPercentsReadOneHundredOnlyForExactWork() throws Exception { + var none = new WoodStation(new Location(null, 0, 0, 0)); + assertEquals(0, none.getRecipePercent()); + var s = new WoodStation(new Location(null, 0, 0, 0), project); + assertEquals(0, s.getRecipePercent()); + s.addMaterial(wood, null); + assertEquals(50, s.getRecipePercent()); + s.addMaterial(wood, null); + assertEquals(100, s.getRecipePercent()); + s.addMaterial(extra, null); + assertEquals(66, s.getRecipePercent()); + s.applySavedProgress(Map.of("birch", 2), Map.of("whittle", 1), null); + assertEquals(0, s.getRecipePercent()); + assertEquals(50, s.getHitPercent()); + s.applySavedProgress(Map.of("oak", 2), Map.of("whittle", 2), null); + assertEquals(100, s.getHitPercent()); + s.hit(hit); + assertEquals(50, s.getHitPercent()); + var otherType = new HitType("hammer", yaml("")); + HitTypeLoader.get().put("hammer", otherType); + var other = new CraftingHit("hammer", yaml("type: hammer")); + HitLoader.get().put("hammer", other); + s.applySavedProgress(Map.of("oak", 2), Map.of("whittle", 2, "hammer", 1), null); + assertEquals(50, s.getHitPercent()); + + var big = + new WoodProject("big", "furniture", yaml("hits: [whittle.200]\nrecipe: [oak.200]")); + var near = new WoodStation(new Location(null, 0, 0, 0), big); + near.applySavedProgress(Map.of("oak", 199), Map.of("whittle", 199), null); + assertEquals(99, near.getRecipePercent()); + assertEquals(99, near.getHitPercent()); + + var bare = + new WoodStation( + new Location(null, 0, 0, 0), new WoodProject("bare", "furniture", yaml(""))); + assertEquals(100, bare.getRecipePercent()); + assertEquals(100, bare.getHitPercent()); + bare.addMaterial(wood, null); + assertEquals(0, bare.getRecipePercent()); + } + @Test void wrongIngredientsAndUnexpectedHitsCountButFailExactRecipe() throws Exception { var s = new WoodStation(new Location(null, 0, 0, 0), project); diff --git a/src/test/java/net/tfminecraft/woodworking/StationManagerTest.java b/src/test/java/net/tfminecraft/woodworking/StationManagerTest.java index 104b429..1c44e2f 100644 --- a/src/test/java/net/tfminecraft/woodworking/StationManagerTest.java +++ b/src/test/java/net/tfminecraft/woodworking/StationManagerTest.java @@ -227,6 +227,26 @@ void materialsStatusAndToolHitsFollowProgress() throws Exception { verify(p).sendMessage("§cYou have to add all the items before working"); } + @Test + void brandingStatusListsEveryHitDoneWithoutTargets() throws Exception { + var saw = new CraftingHit("saw", config("tool: TOOL.SAW\ntype: cut")); + HitLoader.get().put("saw", saw); + var s = station(); + s.addMaterial(oak, item(Material.OAK_LOG)); + s.hit(cut); + hand(item(Material.STICK), true); + click(Action.RIGHT_CLICK_BLOCK); + var order = inOrder(p); + order.verify(p).sendMessage("§7Project: chair"); + order.verify(p).sendMessage("§7Hits done:"); + order.verify(p).sendMessage("cut§7: §e1"); + order.verify(p).sendMessage("saw§7: §e0"); + order.verify(p).sendMessage("§7Total: §e1"); + order.verify(p).sendMessage("§7Left-click branding to finish"); + order.verify(p).sendMessage(contains("to cancel the project")); + verify(p, never()).sendMessage("cut§7: §e1/1"); + } + @Test void brandingFinishesCancelsOrFailsWorkAndValidatesOutput() throws Exception { hand(item(Material.STICK), true); @@ -247,12 +267,19 @@ void brandingFinishesCancelsOrFailsWorkAndValidatesOutput() throws Exception { assertNull(manager.get(loc)); verify(world) .dropItemNaturally(any(Location.class), argThat(x -> x.getType() == Material.CHEST)); + verify(p).sendMessage("§7Hits: §e100%"); + verify(p).sendMessage("§7Total: §e100%"); + verify(p, never()).sendMessage(startsWith("§7Quality: ")); s = station(); s.addMaterial(oak, item(Material.OAK_LOG)); s.hit(cut); s.hit(cut); click(Action.LEFT_CLICK_BLOCK); assertNull(manager.get(loc)); + verify(p).sendTitle(eq("§cThe craft failed"), contains("was ruined"), eq(5), eq(40), eq(10)); + verify(p, times(2)).sendMessage("§7Recipe: §e100%"); + verify(p).sendMessage("§7Hits: §e0%"); + verify(p).sendMessage("§7Total: §e0%"); s = station(); s.addMaterial(oak, item(Material.OAK_LOG)); when(p.isSneaking()).thenReturn(true); @@ -426,6 +453,7 @@ void outputFallbacksQualityAndDetachedWorldEffects() throws Exception { if ("ia.bad".equals(path)) assertTrue(station.hasProject()); else assertFalse(station.hasProject()); } + verify(p, times(3)).sendMessage("§7Quality: Good"); var detached = new WoodStation(new Location(null, 1, 2, 3), project); invoke(manager, "failCraft", method, p, detached); assertFalse(detached.hasProject());