Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HitType, IntCounter> 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) {
Expand All @@ -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);
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/net/tfminecraft/woodworking/station/WoodStation.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<WoodMaterial, Integer> recipe = project.getRecipe();
int needed = 0;
double sum = 0;
for (Map.Entry<WoodMaterial, Integer> 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<ItemStack> cancel() {
List<ItemStack> refund = new ArrayList<>(deposited);
Expand Down
42 changes: 42 additions & 0 deletions src/test/java/net/tfminecraft/woodworking/DomainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 28 additions & 0 deletions src/test/java/net/tfminecraft/woodworking/StationManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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());
Expand Down