From 3fb2b5c3239572bacd1b87e7085e282655c4a6e5 Mon Sep 17 00:00:00 2001 From: CoalGov Deploy Date: Wed, 6 May 2026 02:28:32 +0000 Subject: [PATCH] Add NPC supply chain workers --- CoalGov/README.md | 9 +- .../librewiki/coalgov/service/NpcService.java | 133 +++++++++++++++++- CoalGov/src/main/resources/config.yml | 15 ++ coalgov-wiki-pages/commands.txt | 2 + coalgov-wiki-pages/npcs.txt | 61 ++++++++ 5 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 coalgov-wiki-pages/npcs.txt diff --git a/CoalGov/README.md b/CoalGov/README.md index f73fdde..9ebe6a4 100644 --- a/CoalGov/README.md +++ b/CoalGov/README.md @@ -134,10 +134,17 @@ CoalGov uses Citizens for NPC agents. - Trader NPCs have their own inventory and coal account - Worker NPCs patrol assigned polygon zones -- Workers periodically produce configured goods into their inventory +- Worker roles are matched from NPC names, such as `Farmer` or `Baker` +- 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` +- `Baker` consumes `WHEAT` and produces `BREAD` +- Same-zone traders accept worker outputs up to the configured restock target + ## Super Furnace CoalGov adds a persisted Super Furnace recipe. Craft a furnace with stone in all eight surrounding slots. Super Furnaces smelt faster and consume fuel faster. 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 eeaa96c..80df4b4 100644 --- a/CoalGov/src/main/java/com/librewiki/coalgov/service/NpcService.java +++ b/CoalGov/src/main/java/com/librewiki/coalgov/service/NpcService.java @@ -12,12 +12,15 @@ import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import java.nio.charset.StandardCharsets; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -140,7 +143,8 @@ public final class NpcService { public void tickWorkers() { try { - for (CoalNpc coalNpc : repository.listNpcs()) { + List npcs = repository.listNpcs(); + for (CoalNpc coalNpc : npcs) { if (!coalNpc.worker()) { continue; } @@ -161,10 +165,9 @@ public final class NpcService { npc.getNavigator().setTarget(target); } } - Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD")); - int amount = plugin.getConfig().getInt("npc.worker.produce_amount", 2); - if (material != null && amount > 0) { - repository.addInventory(coalNpc.id(), material, amount); + WorkerRecipe recipe = recipeFor(coalNpc); + if (recipe != null && produce(coalNpc, recipe, npcs)) { + supplyTraders(coalNpc, recipe, npcs); } } } catch (SQLException exception) { @@ -172,6 +175,123 @@ public final class NpcService { } } + private WorkerRecipe recipeFor(CoalNpc npc) { + ConfigurationSection roles = plugin.getConfig().getConfigurationSection("npc.worker.roles"); + if (roles != null) { + String npcName = npc.name().toLowerCase(); + for (String role : roles.getKeys(false)) { + if (npcName.contains(role.toLowerCase())) { + WorkerRecipe recipe = recipe(role, roles.getConfigurationSection(role)); + if (recipe != null) { + return recipe; + } + } + } + } + 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()); + } + + private WorkerRecipe recipe(String name, ConfigurationSection section) { + if (section == null) { + return null; + } + Material output = Material.matchMaterial(section.getString("output_material", "")); + int outputAmount = section.getInt("output_amount", 0); + if (output == null || outputAmount <= 0) { + return null; + } + Map inputs = new LinkedHashMap<>(); + ConfigurationSection inputSection = section.getConfigurationSection("inputs"); + if (inputSection != null) { + for (String key : inputSection.getKeys(false)) { + Material material = Material.matchMaterial(key); + long amount = inputSection.getLong(key); + if (material != null && amount > 0L) { + inputs.put(material, amount); + } + } + } + return new WorkerRecipe(name, output, outputAmount, inputs); + } + + private boolean produce(CoalNpc worker, WorkerRecipe recipe, List npcs) throws SQLException { + for (var input : recipe.inputs().entrySet()) { + long stocked = repository.inventoryAmount(worker.id(), input.getKey()); + long missing = input.getValue() - stocked; + if (missing > 0L) { + requestInput(worker, input.getKey(), missing, npcs); + } + if (repository.inventoryAmount(worker.id(), input.getKey()) < input.getValue()) { + return false; + } + } + for (var input : recipe.inputs().entrySet()) { + if (!repository.removeInventory(worker.id(), input.getKey(), input.getValue())) { + return false; + } + } + repository.addInventory(worker.id(), recipe.output(), recipe.outputAmount()); + return true; + } + + private void requestInput(CoalNpc requester, Material material, long missing, List npcs) throws SQLException { + long remaining = missing; + long reserve = Math.max(0L, plugin.getConfig().getLong("npc.supply_chain.source_reserve", 16L)); + long maxTransfer = Math.max(1L, plugin.getConfig().getLong("npc.supply_chain.max_transfer_per_tick", 64L)); + for (CoalNpc source : npcs) { + if (remaining <= 0L) { + return; + } + if (source.id() == requester.id() || source.zoneId() != requester.zoneId()) { + continue; + } + long available = Math.max(0L, repository.inventoryAmount(source.id(), material) - reserve); + long moved = Math.min(Math.min(available, remaining), maxTransfer); + if (moved <= 0L || !repository.removeInventory(source.id(), material, moved)) { + continue; + } + repository.addInventory(requester.id(), material, moved); + remaining -= moved; + } + } + + private void supplyTraders(CoalNpc worker, WorkerRecipe recipe, List npcs) throws SQLException { + if (!plugin.getConfig().getBoolean("npc.supply_chain.traders_accept_outputs", true)) { + return; + } + long reserve = Math.max(0L, plugin.getConfig().getLong("npc.supply_chain.worker_output_reserve", 16L)); + long target = Math.max(0L, plugin.getConfig().getLong("npc.supply_chain.trader_restock_target", 256L)); + long maxTransfer = Math.max(1L, plugin.getConfig().getLong("npc.supply_chain.max_transfer_per_tick", 64L)); + long available = Math.max(0L, repository.inventoryAmount(worker.id(), recipe.output()) - reserve); + if (available <= 0L) { + return; + } + for (CoalNpc trader : tradersInZone(worker, npcs)) { + long needed = target - repository.inventoryAmount(trader.id(), recipe.output()); + long moved = Math.min(Math.min(needed, available), maxTransfer); + if (moved <= 0L || !repository.removeInventory(worker.id(), recipe.output(), moved)) { + continue; + } + repository.addInventory(trader.id(), recipe.output(), moved); + available -= moved; + if (available <= 0L) { + return; + } + } + } + + private List tradersInZone(CoalNpc worker, List npcs) { + List traders = new ArrayList<>(); + for (CoalNpc npc : npcs) { + if (npc.trader() && npc.zoneId() == worker.zoneId()) { + traders.add(npc); + } + } + return traders; + } + private Location randomPoint(NpcZone zone) { World world = Bukkit.getWorld(zone.world()); if (world == null) { @@ -215,4 +335,7 @@ public final class NpcService { public record HaggleQuote(String mode, Material material, int amount, long offer, long floor, long ceiling, long counter, boolean accepted, String reason) { } + + private record WorkerRecipe(String name, Material output, int outputAmount, Map inputs) { + } } diff --git a/CoalGov/src/main/resources/config.yml b/CoalGov/src/main/resources/config.yml index f9e8223..7d46376 100644 --- a/CoalGov/src/main/resources/config.yml +++ b/CoalGov/src/main/resources/config.yml @@ -21,6 +21,21 @@ npc: tick_interval_seconds: 20 produce_material: BREAD produce_amount: 2 + roles: + farmer: + output_material: WHEAT + output_amount: 4 + baker: + output_material: BREAD + output_amount: 2 + inputs: + WHEAT: 3 + supply_chain: + max_transfer_per_tick: 64 + source_reserve: 16 + worker_output_reserve: 16 + traders_accept_outputs: true + trader_restock_target: 256 claims: resale: diff --git a/coalgov-wiki-pages/commands.txt b/coalgov-wiki-pages/commands.txt index 6b51a48..9f5886f 100644 --- a/coalgov-wiki-pages/commands.txt +++ b/coalgov-wiki-pages/commands.txt @@ -72,3 +72,5 @@ Craft a CoalGov Super Furnace with a furnace in the center and stone in the eigh * /cgnpc funds - add coal or Coal Cents to an NPC account. * /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. diff --git a/coalgov-wiki-pages/npcs.txt b/coalgov-wiki-pages/npcs.txt new file mode 100644 index 0000000..4a2c19b --- /dev/null +++ b/coalgov-wiki-pages/npcs.txt @@ -0,0 +1,61 @@ +====== NPC Supply Chains ====== + +CoalGov NPCs use Citizens and are managed with: + + /cgnpc + +===== Farm-To-Market Example ===== + +Select a polygon around the farm with ''/claim wand'', then create an NPC zone: + + /cgnpc zone create farm + +Create workers and a trader in that zone: + + /cgnpc create worker Farmer farm + /cgnpc create worker Baker farm + /cgnpc create trader Marketman farm + +The default supply chain is: + + * Farmer produces WHEAT. + * 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. + * Players can haggle with traders for stocked goods. + +Example player trade: + + /cgnpc list + /cgnpc haggle buy BREAD 8 8 + +===== Configuration ===== + +Worker roles are matched by NPC name under ''npc.worker.roles''. For example, an NPC named ''Farmer Joe'' uses the ''farmer'' role. + + npc: + worker: + tick_interval_seconds: 20 + roles: + farmer: + output_material: WHEAT + output_amount: 4 + baker: + output_material: BREAD + output_amount: 2 + inputs: + WHEAT: 3 + supply_chain: + max_transfer_per_tick: 64 + source_reserve: 16 + worker_output_reserve: 16 + traders_accept_outputs: true + trader_restock_target: 256 + +Admins can seed inventories with: + + /cgnpc stock + +and fund trader purchases with: + + /cgnpc funds