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
|
||||
- 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.
|
||||
|
||||
@@ -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<CoalNpc> 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<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) {
|
||||
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<Material, Long> inputs) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 haggle <id> <buy|sell> <material> <amount> <offer> - 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.
|
||||
|
||||
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