Add market feedback pricing

This commit is contained in:
CoalGov Deploy
2026-05-06 01:01:01 +00:00
parent 8b5ff1f92f
commit b88e3074fd
8 changed files with 223 additions and 30 deletions

View File

@@ -39,6 +39,7 @@ public final class MarketListener implements Listener {
return;
}
plugin.marketService().buy(player, clicked.getType(), purchaseAmount(event.getClick()));
plugin.marketService().open(player);
} catch (SQLException exception) {
plugin.getLogger().warning("Market click failed: " + exception.getMessage());
player.sendMessage(plugin.messages().text("&cMarket ledger unavailable."));

View File

@@ -52,6 +52,14 @@ public final class GovernmentService {
repository.addTreasury(amount);
}
public void collectMarketRevenue(long amount) throws SQLException {
repository.addTreasury(amount);
}
public boolean spendMarketTransaction(long amount) throws SQLException {
return spendTreasuryBudget(amount, "market.treasury.transaction_budget_percent");
}
public boolean taxExempt(UUID uuid) throws SQLException {
return repository.taxExempt(uuid);
}
@@ -113,6 +121,18 @@ public final class GovernmentService {
return PayFineResult.success(fine.amount(), "");
}
private boolean spendTreasuryBudget(long amount, String configPath) throws SQLException {
if (amount <= 0L) {
return false;
}
long balance = repository.treasuryBalance();
long budget = Math.max(0L, Math.round(balance * Math.max(0.0D, config.getDouble(configPath, 0.0D)) / 100.0D));
if (amount > budget) {
return false;
}
return repository.spendTreasury(amount);
}
private long transactionTax(UUID playerUuid, long gross, String configPath) throws SQLException {
if (gross <= 0L || repository.taxExempt(playerUuid)) {
return 0L;

View File

@@ -2,6 +2,7 @@ package com.librewiki.coalgov.service;
import com.librewiki.coalgov.CoalGovPlugin;
import com.librewiki.coalgov.storage.MarketRepository;
import com.librewiki.coalgov.storage.MarketRepository.Flow;
import com.librewiki.coalgov.util.CoalMoney;
import org.bukkit.Bukkit;
import org.bukkit.Material;
@@ -70,7 +71,9 @@ public final class MarketService {
long stock = simulatedStock.containsKey(material)
? simulatedStock.get(material)
: repository.stock(material, initialStock(material));
grossCredit += sellCredit(material, basePrice, stock, item.getAmount());
Flow flow = repository.flow(material, flowHalfLifeMillis(material));
long alreadySold = sold.getOrDefault(material, 0L);
grossCredit += sellCredit(material, basePrice, stock, flow, alreadySold, item.getAmount());
simulatedStock.put(material, stock + item.getAmount());
sold.merge(material, (long) item.getAmount(), Long::sum);
}
@@ -80,11 +83,20 @@ public final class MarketService {
}
long tax = plugin.governmentService().marketSaleTax(player.getUniqueId(), grossCredit);
long credit = Math.max(0L, grossCredit - tax);
plugin.economyService().credit(player.getUniqueId(), credit, "market_sale");
if (!plugin.governmentService().spendMarketTransaction(grossCredit)) {
player.sendMessage(plugin.messages().text("&cThe Ministry treasury cannot fund that market purchase right now."));
return;
}
if (!plugin.economyService().credit(player.getUniqueId(), credit, "market_sale")) {
plugin.governmentService().collectTax(grossCredit);
player.sendMessage(plugin.messages().text("&cMarket sale failed."));
return;
}
plugin.governmentService().collectTax(tax);
removeSoldItems(player, sold);
for (var entry : sold.entrySet()) {
repository.addStock(entry.getKey(), entry.getValue());
repository.recordSell(entry.getKey(), entry.getValue(), flowHalfLifeMillis(entry.getKey()));
}
if (tax > 0L) {
player.sendMessage(plugin.messages().text("&aSold resources for &f" + CoalMoney.format(credit) + " &aafter &f" + CoalMoney.format(tax) + " &atax."));
@@ -132,7 +144,8 @@ public final class MarketService {
player.sendMessage(plugin.messages().text("&cYou need more inventory space."));
return;
}
plugin.governmentService().collectTax(tax);
plugin.governmentService().collectMarketRevenue(total);
repository.recordBuy(material, amount, flowHalfLifeMillis(material));
if (tax > 0L) {
player.sendMessage(plugin.messages().text("&aBought " + amount + " " + readable(material) + " for &f" + CoalMoney.format(total) + "&a, including &f" + CoalMoney.format(tax) + " &atax."));
} else {
@@ -165,34 +178,47 @@ public final class MarketService {
private Quote quote(Material material) throws SQLException {
double basePrice = basePrices().get(material);
long stock = repository.stock(material, initialStock(material));
return quote(material, basePrice, stock);
Flow flow = repository.flow(material, flowHalfLifeMillis(material));
return quote(material, basePrice, stock, flow);
}
private Quote quote(Material material, double basePrice, long stock) {
return new Quote(stock, sellUnitPrice(material, basePrice, stock), buyUnitPrice(material, basePrice, stock));
private Quote quote(Material material, double basePrice, long stock, Flow flow) {
return new Quote(stock, sellUnitPrice(material, basePrice, stock, flow, 0L), buyUnitPrice(material, basePrice, stock, flow));
}
private long sellCredit(Material material, double basePrice, long startingStock, int amount) {
private long sellCredit(Material material, double basePrice, long startingStock, Flow flow, long alreadySold, int amount) {
double total = 0.0D;
for (int index = 0; index < amount; index++) {
total += sellUnitPrice(material, basePrice, startingStock + index);
total += sellUnitPrice(material, basePrice, startingStock + index, flow, alreadySold + index);
}
return Math.max(1L, CoalMoney.fromCoal(total));
}
private double sellUnitPrice(Material material, double basePrice, long stock) {
double target = Math.max(1.0D, targetStock());
double multiplier = target / Math.max(1.0D, stock);
multiplier = Math.max(minMultiplier(), Math.min(maxMultiplier(), multiplier));
return Math.max(coalEquivalentFloor(material), basePrice * multiplier * sellMultiplier());
private double sellUnitPrice(Material material, double basePrice, long stock, Flow flow, long batchSells) {
double multiplier = stockPressure(material, stock) * flowPressure(material, flow, batchSells);
multiplier = Math.max(minMultiplier(material), Math.min(maxMultiplier(material), multiplier));
return Math.max(coalEquivalentFloor(material), basePrice * multiplier * sellMultiplier(material));
}
private double buyUnitPrice(Material material, double basePrice, long stock) {
double sellPrice = sellUnitPrice(material, basePrice, stock);
double target = Math.max(1.0D, targetStock());
double multiplier = target / Math.max(1.0D, stock);
multiplier = Math.max(minMultiplier(), Math.min(maxMultiplier(), multiplier));
return Math.max(sellPrice, basePrice * multiplier * buyMultiplier());
private double buyUnitPrice(Material material, double basePrice, long stock, Flow flow) {
double sellPrice = sellUnitPrice(material, basePrice, stock, flow, 0L);
double multiplier = stockPressure(material, stock) * flowPressure(material, flow, 0L);
multiplier = Math.max(minMultiplier(material), Math.min(maxMultiplier(material), multiplier));
return Math.max(sellPrice, basePrice * multiplier * buyMultiplier(material));
}
private double stockPressure(Material material, long stock) {
double target = Math.max(1.0D, targetStock(material));
double floorStock = Math.max(1.0D, target * stockFloorRatio(material));
double pressure = target / Math.max(floorStock, stock);
return Math.pow(Math.max(0.0D, pressure), elasticity(material));
}
private double flowPressure(Material material, Flow flow, long batchSells) {
double target = Math.max(1.0D, targetStock(material));
double netDemand = flow.recentBuys() - flow.recentSells() - Math.max(0L, batchSells);
double pressure = 1.0D + (netDemand / target) * flowWeight(material);
return Math.max(flowMinMultiplier(material), Math.min(flowMaxMultiplier(material), pressure));
}
private long buyCost(Quote quote, int amount) {
@@ -262,24 +288,59 @@ public final class MarketService {
return plugin.getConfig().getLong("market.initial_stock." + material.name(), initialStock());
}
private long targetStock() {
return plugin.getConfig().getLong("market.dynamic.target_stock", 1024L);
private long targetStock(Material material) {
return materialLong(material, "target_stock", 1024L);
}
private double minMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.min_multiplier", 0.35D);
private double elasticity(Material material) {
return materialDouble(material, "elasticity", 1.0D);
}
private double maxMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.max_multiplier", 3.0D);
private double minMultiplier(Material material) {
return materialDouble(material, "min_multiplier", 0.35D);
}
private double sellMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.sell_multiplier", 0.85D);
private double maxMultiplier(Material material) {
return materialDouble(material, "max_multiplier", 3.0D);
}
private double buyMultiplier() {
return plugin.getConfig().getDouble("market.dynamic.buy_multiplier", 1.15D);
private double sellMultiplier(Material material) {
return materialDouble(material, "sell_multiplier", 0.85D);
}
private double buyMultiplier(Material material) {
return materialDouble(material, "buy_multiplier", 1.15D);
}
private double stockFloorRatio(Material material) {
return materialDouble(material, "stock_floor_ratio", 0.05D);
}
private double flowWeight(Material material) {
return materialDouble(material, "flow_weight", 0.5D);
}
private double flowMinMultiplier(Material material) {
return materialDouble(material, "flow_min_multiplier", 0.70D);
}
private double flowMaxMultiplier(Material material) {
return materialDouble(material, "flow_max_multiplier", 1.75D);
}
private long flowHalfLifeMillis(Material material) {
long minutes = materialLong(material, "flow_half_life_minutes", 180L);
return Math.max(1L, minutes) * 60_000L;
}
private long materialLong(Material material, String key, long fallback) {
return plugin.getConfig().getLong("market.dynamic.materials." + material.name() + "." + key,
plugin.getConfig().getLong("market.dynamic." + key, fallback));
}
private double materialDouble(Material material, String key, double fallback) {
return plugin.getConfig().getDouble("market.dynamic.materials." + material.name() + "." + key,
plugin.getConfig().getDouble("market.dynamic." + key, fallback));
}
private ItemStack button(Material material, String name, List<String> lore) {

View File

@@ -133,6 +133,14 @@ public final class Database {
updated_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS market_flow (
material TEXT PRIMARY KEY,
recent_buys REAL NOT NULL DEFAULT 0,
recent_sells REAL NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL
)
""");
statement.execute("""
CREATE TABLE IF NOT EXISTS treasury (
id TEXT PRIMARY KEY,

View File

@@ -66,4 +66,71 @@ public final class MarketRepository {
return statement.executeUpdate() == 1;
}
}
public Flow flow(Material material, long halfLifeMillis) throws SQLException {
long now = System.currentTimeMillis();
try (PreparedStatement insert = database.connection().prepareStatement("""
INSERT INTO market_flow(material, recent_buys, recent_sells, updated_at)
VALUES (?, 0, 0, ?)
ON CONFLICT(material) DO NOTHING
""")) {
insert.setString(1, material.name());
insert.setLong(2, now);
insert.executeUpdate();
}
try (PreparedStatement select = database.connection().prepareStatement(
"SELECT recent_buys, recent_sells, updated_at FROM market_flow WHERE material = ?")) {
select.setString(1, material.name());
try (ResultSet result = select.executeQuery()) {
if (!result.next()) {
return new Flow(0.0D, 0.0D);
}
double decay = decayFactor(now - result.getLong("updated_at"), halfLifeMillis);
return new Flow(result.getDouble("recent_buys") * decay, result.getDouble("recent_sells") * decay);
}
}
}
public void recordBuy(Material material, long amount, long halfLifeMillis) throws SQLException {
recordFlow(material, Math.max(0L, amount), 0L, halfLifeMillis);
}
public void recordSell(Material material, long amount, long halfLifeMillis) throws SQLException {
recordFlow(material, 0L, Math.max(0L, amount), halfLifeMillis);
}
private void recordFlow(Material material, long buys, long sells, long halfLifeMillis) throws SQLException {
if (buys <= 0L && sells <= 0L) {
return;
}
long now = System.currentTimeMillis();
Flow flow = flow(material, halfLifeMillis);
try (PreparedStatement statement = database.connection().prepareStatement("""
INSERT INTO market_flow(material, recent_buys, recent_sells, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(material) DO UPDATE SET
recent_buys = excluded.recent_buys,
recent_sells = excluded.recent_sells,
updated_at = excluded.updated_at
""")) {
statement.setString(1, material.name());
statement.setDouble(2, flow.recentBuys() + buys);
statement.setDouble(3, flow.recentSells() + sells);
statement.setLong(4, now);
statement.executeUpdate();
}
}
private double decayFactor(long elapsedMillis, long halfLifeMillis) {
if (elapsedMillis <= 0L) {
return 1.0D;
}
if (halfLifeMillis <= 0L) {
return 0.0D;
}
return Math.pow(0.5D, (double) elapsedMillis / halfLifeMillis);
}
public record Flow(double recentBuys, double recentSells) {
}
}