From cf0a71f900580a84999c55bebeca397e0b26a850 Mon Sep 17 00:00:00 2001 From: CoalGov Deploy Date: Wed, 6 May 2026 02:34:52 +0000 Subject: [PATCH] Add real farm worker harvesting --- CoalGov/README.md | 3 +- .../librewiki/coalgov/service/NpcService.java | 121 +++++++++++++++++- CoalGov/src/main/resources/config.yml | 11 ++ coalgov-wiki-pages/commands.txt | 2 +- coalgov-wiki-pages/npcs.txt | 14 +- 5 files changed, 144 insertions(+), 7 deletions(-) diff --git a/CoalGov/README.md b/CoalGov/README.md index 9ebe6a4..d8c0c01 100644 --- a/CoalGov/README.md +++ b/CoalGov/README.md @@ -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 diff --git a/CoalGov/src/main/java/com/librewiki/coalgov/service/NpcService.java b/CoalGov/src/main/java/com/librewiki/coalgov/service/NpcService.java index 80df4b4..2343b27 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/service/NpcService.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/service/NpcService.java @@ -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 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 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 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 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 inputs) { + private record WorkerRecipe(String name, String behavior, Material output, int outputAmount, + Map inputs, List crops, boolean replant) { + private boolean farmBehavior() { + return behavior.equalsIgnoreCase("FARM"); + } } } diff --git a/CoalGov/src/main/resources/config.yml b/CoalGov/src/main/resources/config.yml index 7d46376..8432a7d 100644 --- a/CoalGov/src/main/resources/config.yml +++ b/CoalGov/src/main/resources/config.yml @@ -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 diff --git a/coalgov-wiki-pages/commands.txt b/coalgov-wiki-pages/commands.txt index 9f5886f..e544f43 100644 --- a/coalgov-wiki-pages/commands.txt +++ b/coalgov-wiki-pages/commands.txt @@ -73,4 +73,4 @@ Craft a CoalGov Super Furnace with a furnace in the center and stone in the eigh * /cgnpc haggle - 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. diff --git a/coalgov-wiki-pages/npcs.txt b/coalgov-wiki-pages/npcs.txt index 4a2c19b..377de83 100644 --- a/coalgov-wiki-pages/npcs.txt +++ b/coalgov-wiki-pages/npcs.txt @@ -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