Add real farm worker harvesting
This commit is contained in:
@@ -135,13 +135,14 @@ CoalGov uses Citizens for NPC agents.
|
|||||||
- Trader NPCs have their own inventory and coal account
|
- Trader NPCs have their own inventory and coal account
|
||||||
- Worker NPCs patrol assigned polygon zones
|
- Worker NPCs patrol assigned polygon zones
|
||||||
- Worker roles are matched from NPC names, such as `Farmer` or `Baker`
|
- 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
|
- Workers can consume inputs, pull supplies from same-zone NPCs, produce outputs, and stock same-zone traders
|
||||||
- Traders can haggle through `/cgnpc haggle`
|
- Traders can haggle through `/cgnpc haggle`
|
||||||
- Optional OpenRouter-backed AI can produce trader messages and counters
|
- Optional OpenRouter-backed AI can produce trader messages and counters
|
||||||
|
|
||||||
Default worker chain:
|
Default worker chain:
|
||||||
|
|
||||||
- `Farmer` produces `WHEAT`
|
- `Farmer` harvests and replants mature crops in its zone
|
||||||
- `Baker` consumes `WHEAT` and produces `BREAD`
|
- `Baker` consumes `WHEAT` and produces `BREAD`
|
||||||
- Same-zone traders accept worker outputs up to the configured restock target
|
- Same-zone traders accept worker outputs up to the configured restock target
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.Location;
|
import org.bukkit.Location;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
import org.bukkit.World;
|
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.configuration.ConfigurationSection;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
import org.bukkit.entity.Player;
|
import org.bukkit.entity.Player;
|
||||||
@@ -20,6 +23,7 @@ import org.bukkit.inventory.ItemStack;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -166,7 +170,15 @@ public final class NpcService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkerRecipe recipe = recipeFor(coalNpc);
|
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);
|
supplyTraders(coalNpc, recipe, npcs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +202,7 @@ public final class NpcService {
|
|||||||
}
|
}
|
||||||
Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD"));
|
Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD"));
|
||||||
int amount = plugin.getConfig().getInt("npc.worker.produce_amount", 2);
|
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) {
|
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 {
|
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) {
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,24 @@ npc:
|
|||||||
produce_amount: 2
|
produce_amount: 2
|
||||||
roles:
|
roles:
|
||||||
farmer:
|
farmer:
|
||||||
|
behavior: FARM
|
||||||
output_material: WHEAT
|
output_material: WHEAT
|
||||||
output_amount: 4
|
output_amount: 4
|
||||||
|
crops:
|
||||||
|
- WHEAT
|
||||||
|
- CARROTS
|
||||||
|
- POTATOES
|
||||||
|
- BEETROOTS
|
||||||
|
replant: true
|
||||||
baker:
|
baker:
|
||||||
|
behavior: CRAFT
|
||||||
output_material: BREAD
|
output_material: BREAD
|
||||||
output_amount: 2
|
output_amount: 2
|
||||||
inputs:
|
inputs:
|
||||||
WHEAT: 3
|
WHEAT: 3
|
||||||
|
farm:
|
||||||
|
action_distance: 3.0
|
||||||
|
max_scan_blocks: 512
|
||||||
supply_chain:
|
supply_chain:
|
||||||
max_transfer_per_tick: 64
|
max_transfer_per_tick: 64
|
||||||
source_reserve: 16
|
source_reserve: 16
|
||||||
|
|||||||
@@ -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 haggle <id> <buy|sell> <material> <amount> <offer> - negotiate with a trader.
|
||||||
* /cgnpc list - list NPC zones and CoalGov NPCs.
|
* /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.
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ Create workers and a trader in that zone:
|
|||||||
|
|
||||||
The default supply chain is:
|
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 pulls WHEAT from same-zone NPC inventories.
|
||||||
* Baker consumes 3 WHEAT to produce 2 BREAD.
|
* Baker consumes 3 WHEAT to produce 2 BREAD.
|
||||||
* Workers keep a reserve and move surplus output to same-zone traders.
|
* 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
|
tick_interval_seconds: 20
|
||||||
roles:
|
roles:
|
||||||
farmer:
|
farmer:
|
||||||
|
behavior: FARM
|
||||||
output_material: WHEAT
|
output_material: WHEAT
|
||||||
output_amount: 4
|
output_amount: 4
|
||||||
|
crops:
|
||||||
|
- WHEAT
|
||||||
|
- CARROTS
|
||||||
|
- POTATOES
|
||||||
|
- BEETROOTS
|
||||||
|
replant: true
|
||||||
baker:
|
baker:
|
||||||
|
behavior: CRAFT
|
||||||
output_material: BREAD
|
output_material: BREAD
|
||||||
output_amount: 2
|
output_amount: 2
|
||||||
inputs:
|
inputs:
|
||||||
WHEAT: 3
|
WHEAT: 3
|
||||||
|
farm:
|
||||||
|
action_distance: 3.0
|
||||||
|
max_scan_blocks: 512
|
||||||
supply_chain:
|
supply_chain:
|
||||||
max_transfer_per_tick: 64
|
max_transfer_per_tick: 64
|
||||||
source_reserve: 16
|
source_reserve: 16
|
||||||
|
|||||||
Reference in New Issue
Block a user