Add NPC supply chain workers

This commit is contained in:
CoalGov Deploy
2026-05-06 02:28:32 +00:00
parent be82f0b486
commit 3fb2b5c323
5 changed files with 214 additions and 6 deletions

View File

@@ -134,10 +134,17 @@ 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
- 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` - 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:
- `Farmer` produces `WHEAT`
- `Baker` consumes `WHEAT` and produces `BREAD`
- Same-zone traders accept worker outputs up to the configured restock target
## Super Furnace ## 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. 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.

View File

@@ -12,12 +12,15 @@ 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.configuration.ConfigurationSection;
import org.bukkit.entity.EntityType; import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack; 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.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -140,7 +143,8 @@ public final class NpcService {
public void tickWorkers() { public void tickWorkers() {
try { try {
for (CoalNpc coalNpc : repository.listNpcs()) { List<CoalNpc> npcs = repository.listNpcs();
for (CoalNpc coalNpc : npcs) {
if (!coalNpc.worker()) { if (!coalNpc.worker()) {
continue; continue;
} }
@@ -161,10 +165,9 @@ public final class NpcService {
npc.getNavigator().setTarget(target); npc.getNavigator().setTarget(target);
} }
} }
Material material = Material.matchMaterial(plugin.getConfig().getString("npc.worker.produce_material", "BREAD")); WorkerRecipe recipe = recipeFor(coalNpc);
int amount = plugin.getConfig().getInt("npc.worker.produce_amount", 2); if (recipe != null && produce(coalNpc, recipe, npcs)) {
if (material != null && amount > 0) { supplyTraders(coalNpc, recipe, npcs);
repository.addInventory(coalNpc.id(), material, amount);
} }
} }
} catch (SQLException exception) { } 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<Material, Long> 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<CoalNpc> 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<CoalNpc> 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<CoalNpc> 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<CoalNpc> tradersInZone(CoalNpc worker, List<CoalNpc> npcs) {
List<CoalNpc> traders = new ArrayList<>();
for (CoalNpc npc : npcs) {
if (npc.trader() && npc.zoneId() == worker.zoneId()) {
traders.add(npc);
}
}
return traders;
}
private Location randomPoint(NpcZone zone) { private Location randomPoint(NpcZone zone) {
World world = Bukkit.getWorld(zone.world()); World world = Bukkit.getWorld(zone.world());
if (world == null) { 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, public record HaggleQuote(String mode, Material material, int amount, long offer, long floor, long ceiling,
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) {
}
} }

View File

@@ -21,6 +21,21 @@ npc:
tick_interval_seconds: 20 tick_interval_seconds: 20
produce_material: BREAD produce_material: BREAD
produce_amount: 2 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: claims:
resale: resale:

View File

@@ -72,3 +72,5 @@ Craft a CoalGov Super Furnace with a furnace in the center and stone in the eigh
* /cgnpc funds <id> <amount> - add coal or Coal Cents to an NPC account. * /cgnpc funds <id> <amount> - add coal or Coal Cents to an NPC account.
* /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.

View File

@@ -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 <traderId> 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 <id> <material> <amount>
and fund trader purchases with:
/cgnpc funds <id> <amount>