Add real farm worker harvesting

This commit is contained in:
CoalGov Deploy
2026-05-06 02:34:52 +00:00
parent 3fb2b5c323
commit cf0a71f900
5 changed files with 144 additions and 7 deletions

View File

@@ -135,13 +135,14 @@ CoalGov uses Citizens for NPC agents.
- Trader NPCs have their own inventory and coal account
- Worker NPCs patrol assigned polygon zones
- Worker roles are matched from NPC names, such as `Farmer` or `Baker`
- Farm workers can harvest mature crops in their assigned zone, collect drops, and replant from their inventory
- Workers can consume inputs, pull supplies from same-zone NPCs, produce outputs, and stock same-zone traders
- Traders can haggle through `/cgnpc haggle`
- Optional OpenRouter-backed AI can produce trader messages and counters
Default worker chain:
- `Farmer` produces `WHEAT`
- `Farmer` harvests and replants mature crops in its zone
- `Baker` consumes `WHEAT` and produces `BREAD`
- Same-zone traders accept worker outputs up to the configured restock target

View File

@@ -12,6 +12,9 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.data.Ageable;
import org.bukkit.block.data.BlockData;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
@@ -20,6 +23,7 @@ import org.bukkit.inventory.ItemStack;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -166,7 +170,15 @@ public final class NpcService {
}
}
WorkerRecipe recipe = recipeFor(coalNpc);
if (recipe != null && produce(coalNpc, recipe, npcs)) {
if (recipe == null) {
continue;
}
if (recipe.farmBehavior()) {
harvestCrop(coalNpc, recipe, zone.get(), npc);
supplyTraders(coalNpc, recipe, npcs);
continue;
}
if (produce(coalNpc, recipe, npcs)) {
supplyTraders(coalNpc, recipe, npcs);
}
}
@@ -190,7 +202,7 @@ public final class NpcService {
}
Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD"));
int amount = plugin.getConfig().getInt("npc.worker.produce_amount", 2);
return material == null || amount <= 0 ? null : new WorkerRecipe("default", material, amount, Map.of());
return material == null || amount <= 0 ? null : new WorkerRecipe("default", "PRODUCE", material, amount, Map.of(), List.of(), true);
}
private WorkerRecipe recipe(String name, ConfigurationSection section) {
@@ -213,7 +225,104 @@ public final class NpcService {
}
}
}
return new WorkerRecipe(name, output, outputAmount, inputs);
List<Material> crops = new ArrayList<>();
for (String raw : section.getStringList("crops")) {
Material crop = Material.matchMaterial(raw);
if (crop != null) {
crops.add(crop);
}
}
String behavior = section.getString("behavior", inputs.isEmpty() ? "PRODUCE" : "CRAFT");
boolean replant = section.getBoolean("replant", true);
return new WorkerRecipe(name, behavior.toUpperCase(), output, outputAmount, inputs, crops, replant);
}
private boolean harvestCrop(CoalNpc worker, WorkerRecipe recipe, NpcZone zone, NPC npc) throws SQLException {
if (!npc.isSpawned()) {
return false;
}
Block crop = findMatureCrop(zone, recipe);
if (crop == null) {
return false;
}
Location cropLocation = crop.getLocation().add(0.5D, 0.0D, 0.5D);
double actionDistance = Math.max(1.0D, plugin.getConfig().getDouble("npc.worker.farm.action_distance", 3.0D));
if (npc.getEntity().getLocation().distanceSquared(cropLocation) > actionDistance * actionDistance) {
npc.getNavigator().setTarget(cropLocation);
return false;
}
collectDrops(worker, crop.getDrops());
Material cropType = crop.getType();
crop.setType(Material.AIR);
if (recipe.replant()) {
replant(worker, crop, cropType);
}
return true;
}
private Block findMatureCrop(NpcZone zone, WorkerRecipe recipe) {
World world = Bukkit.getWorld(zone.world());
if (world == null) {
return null;
}
List<Material> crops = recipe.crops().isEmpty() ? List.of(Material.WHEAT, Material.CARROTS, Material.POTATOES, Material.BEETROOTS) : recipe.crops();
int maxScans = Math.max(16, plugin.getConfig().getInt("npc.worker.farm.max_scan_blocks", 512));
for (int scan = 0; scan < maxScans; scan++) {
int x = zone.x1() + random.nextInt(Math.max(1, zone.x2() - zone.x1() + 1));
int z = zone.z1() + random.nextInt(Math.max(1, zone.z2() - zone.z1() + 1));
if (!zone.contains(world.getName(), x, z)) {
continue;
}
int topY = world.getHighestBlockYAt(x, z) + 1;
for (int y = topY; y >= Math.max(world.getMinHeight(), topY - 4); y--) {
Block block = world.getBlockAt(x, y, z);
if (crops.contains(block.getType()) && mature(block)) {
return block;
}
}
}
return null;
}
private boolean mature(Block block) {
BlockData data = block.getBlockData();
return data instanceof Ageable ageable && ageable.getAge() >= ageable.getMaximumAge();
}
private void collectDrops(CoalNpc worker, Collection<ItemStack> drops) throws SQLException {
for (ItemStack drop : drops) {
if (drop == null || drop.getType().isAir() || drop.getAmount() <= 0) {
continue;
}
repository.addInventory(worker.id(), drop.getType(), drop.getAmount());
}
}
private boolean replant(CoalNpc worker, Block block, Material cropType) throws SQLException {
Material seed = plantingMaterial(cropType);
if (seed == null || repository.inventoryAmount(worker.id(), seed) <= 0L) {
return false;
}
if (!repository.removeInventory(worker.id(), seed, 1L)) {
return false;
}
block.setType(cropType);
BlockData data = block.getBlockData();
if (data instanceof Ageable ageable) {
ageable.setAge(0);
block.setBlockData(ageable);
}
return true;
}
private Material plantingMaterial(Material cropType) {
return switch (cropType) {
case WHEAT -> Material.WHEAT_SEEDS;
case CARROTS -> Material.CARROT;
case POTATOES -> Material.POTATO;
case BEETROOTS -> Material.BEETROOT_SEEDS;
default -> null;
};
}
private boolean produce(CoalNpc worker, WorkerRecipe recipe, List<CoalNpc> npcs) throws SQLException {
@@ -336,6 +445,10 @@ public final class NpcService {
long counter, boolean accepted, String reason) {
}
private record WorkerRecipe(String name, Material output, int outputAmount, Map<Material, Long> inputs) {
private record WorkerRecipe(String name, String behavior, Material output, int outputAmount,
Map<Material, Long> inputs, List<Material> crops, boolean replant) {
private boolean farmBehavior() {
return behavior.equalsIgnoreCase("FARM");
}
}
}

View File

@@ -23,13 +23,24 @@ npc:
produce_amount: 2
roles:
farmer:
behavior: FARM
output_material: WHEAT
output_amount: 4
crops:
- WHEAT
- CARROTS
- POTATOES
- BEETROOTS
replant: true
baker:
behavior: CRAFT
output_material: BREAD
output_amount: 2
inputs:
WHEAT: 3
farm:
action_distance: 3.0
max_scan_blocks: 512
supply_chain:
max_transfer_per_tick: 64
source_reserve: 16

View File

@@ -73,4 +73,4 @@ Craft a CoalGov Super Furnace with a furnace in the center and stone in the eigh
* /cgnpc haggle <id> <buy|sell> <material> <amount> <offer> - negotiate with a trader.
* /cgnpc list - list NPC zones and CoalGov NPCs.
Worker roles are matched from the NPC name. By default, a worker named ''Farmer'' produces wheat, a worker named ''Baker'' consumes wheat to produce bread, and workers move surplus output to traders in the same NPC zone.
Worker roles are matched from the NPC name. By default, a worker named ''Farmer'' harvests and replants mature crops in its zone, a worker named ''Baker'' consumes wheat to produce bread, and workers move surplus output to traders in the same NPC zone.

View File

@@ -18,7 +18,8 @@ Create workers and a trader in that zone:
The default supply chain is:
* Farmer produces WHEAT.
* Farmer scans the farm zone for mature WHEAT, CARROTS, POTATOES, and BEETROOTS.
* Farmer walks to mature crops, harvests drops into NPC inventory, and replants if it has the seed or crop item.
* Baker pulls WHEAT from same-zone NPC inventories.
* Baker consumes 3 WHEAT to produce 2 BREAD.
* Workers keep a reserve and move surplus output to same-zone traders.
@@ -38,13 +39,24 @@ Worker roles are matched by NPC name under ''npc.worker.roles''. For example, an
tick_interval_seconds: 20
roles:
farmer:
behavior: FARM
output_material: WHEAT
output_amount: 4
crops:
- WHEAT
- CARROTS
- POTATOES
- BEETROOTS
replant: true
baker:
behavior: CRAFT
output_material: BREAD
output_amount: 2
inputs:
WHEAT: 3
farm:
action_distance: 3.0
max_scan_blocks: 512
supply_chain:
max_transfer_per_tick: 64
source_reserve: 16