Add market feedback pricing
This commit is contained in:
@@ -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."));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user