feat(harvest): add bounded crop traversal and drops
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-003: Harvest connected mature crops"
|
||||
description: Animate bounded harvesting across connected mature crops when a player performs a normal crop break.
|
||||
status: backlog
|
||||
status: in-progress
|
||||
---
|
||||
|
||||
# US-003: Harvest connected mature crops
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
type: User Story
|
||||
title: "US-004: Replant crops and distribute drops"
|
||||
description: Automatically replant harvested crops and deliver their remaining drops safely to the player.
|
||||
status: backlog
|
||||
status: in-progress
|
||||
---
|
||||
|
||||
# US-004: Replant crops and distribute drops
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package games.dmg.spigotharvest;
|
||||
|
||||
/** World-local integer block coordinates. */
|
||||
public record BlockPosition(int x, int y, int z) {
|
||||
public BlockPosition offset(int deltaX, int deltaY, int deltaZ) {
|
||||
return new BlockPosition(x + deltaX, y + deltaY, z + deltaZ);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package games.dmg.spigotharvest;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/** Deterministic bounded search over diagonally and gradually connected crops. */
|
||||
public final class ConnectedCropSearch {
|
||||
private ConnectedCropSearch() {
|
||||
}
|
||||
|
||||
public static List<BlockPosition> find(
|
||||
BlockPosition origin,
|
||||
Predicate<BlockPosition> eligible,
|
||||
int limit
|
||||
) {
|
||||
if (limit <= 0 || !eligible.test(origin)) {
|
||||
return List.of();
|
||||
}
|
||||
List<BlockPosition> result = new ArrayList<>(limit);
|
||||
Queue<BlockPosition> queue = new ArrayDeque<>();
|
||||
Set<BlockPosition> visited = new HashSet<>();
|
||||
queue.add(origin);
|
||||
visited.add(origin);
|
||||
while (!queue.isEmpty() && result.size() < limit) {
|
||||
BlockPosition current = queue.remove();
|
||||
if (!eligible.test(current)) {
|
||||
continue;
|
||||
}
|
||||
result.add(current);
|
||||
enqueueNeighbors(current, queue, visited);
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static void enqueueNeighbors(
|
||||
BlockPosition position,
|
||||
Queue<BlockPosition> queue,
|
||||
Set<BlockPosition> visited
|
||||
) {
|
||||
for (int deltaY = -1; deltaY <= 1; deltaY++) {
|
||||
for (int deltaX = -1; deltaX <= 1; deltaX++) {
|
||||
for (int deltaZ = -1; deltaZ <= 1; deltaZ++) {
|
||||
if (deltaX == 0 && deltaZ == 0) {
|
||||
continue;
|
||||
}
|
||||
BlockPosition neighbor = position.offset(deltaX, deltaY, deltaZ);
|
||||
if (visited.add(neighbor)) {
|
||||
queue.add(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package games.dmg.spigotharvest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
/** Drop result after reserving exactly one item for replanting. */
|
||||
public record CropDropPlan(boolean canReplant, List<ItemStack> remainingDrops) {
|
||||
public CropDropPlan {
|
||||
remainingDrops = List.copyOf(remainingDrops);
|
||||
}
|
||||
|
||||
public static CropDropPlan create(CropType crop, Collection<ItemStack> generatedDrops) {
|
||||
List<ItemStack> drops = new ArrayList<>();
|
||||
boolean consumed = false;
|
||||
for (ItemStack generated : generatedDrops) {
|
||||
ItemStack item = generated.clone();
|
||||
if (!consumed && item.getType() == crop.plantingMaterial() && item.getAmount() > 0) {
|
||||
item.setAmount(item.getAmount() - 1);
|
||||
consumed = true;
|
||||
}
|
||||
if (item.getAmount() > 0) {
|
||||
drops.add(item);
|
||||
}
|
||||
}
|
||||
return consumed ? new CropDropPlan(true, drops) : new CropDropPlan(false, List.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package games.dmg.spigotharvest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class ConnectedCropSearchTest {
|
||||
@Test
|
||||
void followsEightDirectionsAndOneVerticalBlockPerConnection() {
|
||||
BlockPosition origin = new BlockPosition(0, 64, 0);
|
||||
Set<BlockPosition> mature = Set.of(
|
||||
origin,
|
||||
new BlockPosition(1, 65, 1),
|
||||
new BlockPosition(2, 66, 2),
|
||||
new BlockPosition(3, 68, 3),
|
||||
new BlockPosition(-1, 63, 0)
|
||||
);
|
||||
|
||||
List<BlockPosition> result = ConnectedCropSearch.find(origin, mature::contains, 10);
|
||||
|
||||
assertEquals(4, result.size());
|
||||
assertTrue(result.contains(new BlockPosition(2, 66, 2)));
|
||||
assertTrue(!result.contains(new BlockPosition(3, 68, 3)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isDeterministicUniqueAndBounded() {
|
||||
BlockPosition origin = new BlockPosition(0, 64, 0);
|
||||
List<BlockPosition> first = ConnectedCropSearch.find(origin, ignored -> true, 4);
|
||||
List<BlockPosition> second = ConnectedCropSearch.find(origin, ignored -> true, 4);
|
||||
|
||||
assertEquals(first, second);
|
||||
assertEquals(4, Set.copyOf(first).size());
|
||||
assertEquals(origin, first.get(0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package games.dmg.spigotharvest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.List;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
final class CropDropPlanTest {
|
||||
@Test
|
||||
void consumesExactlyOnePlantingItemAndKeepsRemainingDrops() {
|
||||
CropDropPlan plan = CropDropPlan.create(
|
||||
CropType.WHEAT,
|
||||
List.of(new ItemStack(Material.WHEAT, 1), new ItemStack(Material.WHEAT_SEEDS, 3))
|
||||
);
|
||||
|
||||
assertTrue(plan.canReplant());
|
||||
assertEquals(2, plan.remainingDrops().stream()
|
||||
.filter(item -> item.getType() == Material.WHEAT_SEEDS)
|
||||
.mapToInt(ItemStack::getAmount).sum());
|
||||
assertEquals(1, plan.remainingDrops().stream()
|
||||
.filter(item -> item.getType() == Material.WHEAT)
|
||||
.mapToInt(ItemStack::getAmount).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void refusesHarvestWhenDropsCannotReplant() {
|
||||
CropDropPlan plan = CropDropPlan.create(
|
||||
CropType.BEETROOT,
|
||||
List.of(new ItemStack(Material.BEETROOT, 2))
|
||||
);
|
||||
|
||||
assertTrue(!plan.canReplant());
|
||||
assertTrue(plan.remainingDrops().isEmpty());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user