Add NPC supply chain workers
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
61
coalgov-wiki-pages/npcs.txt
Normal file
61
coalgov-wiki-pages/npcs.txt
Normal 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>
|
||||||
Reference in New Issue
Block a user