0.6.0: Counting House refactor — bank-credited wealth exchange
Build / build (push) Has been cancelled
Build / build (push) Has been cancelled
Major economic-design pivot per design conversation: * Bazaar renamed to "Counting House" — wealth exchange, not general shop * Players bring wealth-tokens (diamond, netherite_ingot, ancient_debris, diamond_block); transactions credit their Numismatics bank account directly (not inventory) * SellCatalogue cut from 9 items to 4; iron/gold/coal/lapis/redstone/ emerald removed (those belong on player-run Numismatics Vendor blocks) * Diminishing-returns floor tightened 50% -> 30% so deep-grinders feel the friction Numismatics integration via reflection (NumismaticsBridge): * Auto-creates personal bank account on PlayerLoggedInEvent * SpurBalance.totalSpurs() now sums inventory coins + bank balance * TierStateUpdatePacket carries cashOnHand + bankBalance separately so the hub can break them out * HUD overlay shows three lines: tier / cash / bank — no card lookup, always shows total wealth regardless of what's on the player * /bns sell handled in Java (CountingHouseCommands); KubeJS 02_sell_ shop.js retired to .retired * /bns wallet (+ deposit/withdraw) for managing cash<->bank flow * Wire-version bumped 0.4 -> 0.5 Hub becomes status-only: * Removed Civic Tier / Bazaar / Bounty / Plot transactional buttons * Kept Quest Log, Refresh, Close * Status panel shows tier, cash on hand, bank balance, total wealth, fee%, plot slots, bounty slots * Footer: "Visit spawn NPCs for trades, bounties, plots, tier upgrades" Screen rename: ShopBrowserScreen -> CountingHouseScreen. OpenScreenDispatcher accepts both "counthouse" and "shop" keys (latter as backward-compat alias). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -19,5 +19,5 @@ loader_version_range=[1,)
|
|||||||
mod_id=bnstoolkit
|
mod_id=bnstoolkit
|
||||||
mod_name=Brass and Sigil Toolkit
|
mod_name=Brass and Sigil Toolkit
|
||||||
mod_license=All Rights Reserved
|
mod_license=All Rights Reserved
|
||||||
mod_version=0.5.1
|
mod_version=0.6.0
|
||||||
mod_group_id=uk.sijbers.bnstoolkit
|
mod_group_id=uk.sijbers.bnstoolkit
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
|||||||
import net.neoforged.neoforge.event.tick.PlayerTickEvent;
|
import net.neoforged.neoforge.event.tick.PlayerTickEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import uk.sijbers.bnstoolkit.economy.NumismaticsBridge;
|
||||||
import uk.sijbers.bnstoolkit.network.BnsNetwork;
|
import uk.sijbers.bnstoolkit.network.BnsNetwork;
|
||||||
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
||||||
|
|
||||||
@@ -48,6 +49,10 @@ public final class BnsToolkit {
|
|||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
||||||
if (event.getEntity() instanceof ServerPlayer sp) {
|
if (event.getEntity() instanceof ServerPlayer sp) {
|
||||||
|
// Every player gets a personal Numismatics bank account. After
|
||||||
|
// this point bnstoolkit treats the bank as canonical — no cards
|
||||||
|
// required, balance is queryable via the bridge.
|
||||||
|
NumismaticsBridge.ensureAccount(sp);
|
||||||
ServerEconomyBridge.pushSnapshot(sp, "ALL");
|
ServerEconomyBridge.pushSnapshot(sp, "ALL");
|
||||||
LOG.info("[bnstoolkit] pushed initial economy snapshot to {}", sp.getName().getString());
|
LOG.info("[bnstoolkit] pushed initial economy snapshot to {}", sp.getName().getString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import net.neoforged.neoforge.client.settings.KeyConflictContext;
|
|||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.BountyBoardScreen;
|
import uk.sijbers.bnstoolkit.client.screens.BountyBoardScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.CountingHouseScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.HubScreen;
|
import uk.sijbers.bnstoolkit.client.screens.HubScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.PlotPurchaseScreen;
|
import uk.sijbers.bnstoolkit.client.screens.PlotPurchaseScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.QuestLogScreen;
|
import uk.sijbers.bnstoolkit.client.screens.QuestLogScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.ShopBrowserScreen;
|
|
||||||
import uk.sijbers.bnstoolkit.client.screens.TierUpgradeScreen;
|
import uk.sijbers.bnstoolkit.client.screens.TierUpgradeScreen;
|
||||||
import uk.sijbers.bnstoolkit.network.NetSend;
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ public final class BnsKeyBindings {
|
|||||||
}
|
}
|
||||||
if (OPEN_SHOP.consumeClick()) {
|
if (OPEN_SHOP.consumeClick()) {
|
||||||
NetSend.requestSnapshot("SELL");
|
NetSend.requestSnapshot("SELL");
|
||||||
new ShopBrowserScreen().openGui();
|
new CountingHouseScreen().openGui();
|
||||||
}
|
}
|
||||||
if (OPEN_QUESTLOG.consumeClick()) {
|
if (OPEN_QUESTLOG.consumeClick()) {
|
||||||
NetSend.requestSnapshot("ALL");
|
NetSend.requestSnapshot("ALL");
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ public final class ClientBnsState {
|
|||||||
private ClientBnsState() {}
|
private ClientBnsState() {}
|
||||||
|
|
||||||
public static volatile int tier = 1;
|
public static volatile int tier = 1;
|
||||||
public static volatile long spurs = 0L;
|
public static volatile long spurs = 0L; // inventory + bank (kept for HUD compatibility)
|
||||||
|
public static volatile long cashOnHand = 0L; // inventory coins only
|
||||||
|
public static volatile long bankBalance = 0L; // Numismatics bank balance
|
||||||
public static volatile int effectiveFeePct = 25;
|
public static volatile int effectiveFeePct = 25;
|
||||||
|
|
||||||
// SellSnapshot:
|
// SellSnapshot:
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package uk.sijbers.bnstoolkit.client;
|
|||||||
|
|
||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.BountyBoardScreen;
|
import uk.sijbers.bnstoolkit.client.screens.BountyBoardScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.CountingHouseScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.HubScreen;
|
import uk.sijbers.bnstoolkit.client.screens.HubScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.PlotPurchaseScreen;
|
import uk.sijbers.bnstoolkit.client.screens.PlotPurchaseScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.QuestLogScreen;
|
import uk.sijbers.bnstoolkit.client.screens.QuestLogScreen;
|
||||||
import uk.sijbers.bnstoolkit.client.screens.ShopBrowserScreen;
|
|
||||||
import uk.sijbers.bnstoolkit.client.screens.TierUpgradeScreen;
|
import uk.sijbers.bnstoolkit.client.screens.TierUpgradeScreen;
|
||||||
import uk.sijbers.bnstoolkit.network.NetSend;
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
@@ -25,7 +25,8 @@ public final class OpenScreenDispatcher {
|
|||||||
case "tier" -> { NetSend.requestSnapshot("TIER"); new TierUpgradeScreen().openGui(); }
|
case "tier" -> { NetSend.requestSnapshot("TIER"); new TierUpgradeScreen().openGui(); }
|
||||||
case "bounty" -> { NetSend.requestSnapshot("BOUNTY"); new BountyBoardScreen().openGui(); }
|
case "bounty" -> { NetSend.requestSnapshot("BOUNTY"); new BountyBoardScreen().openGui(); }
|
||||||
case "plot" -> { NetSend.requestSnapshot("PLOT"); new PlotPurchaseScreen().openGui(); }
|
case "plot" -> { NetSend.requestSnapshot("PLOT"); new PlotPurchaseScreen().openGui(); }
|
||||||
case "shop" -> { NetSend.requestSnapshot("SELL"); new ShopBrowserScreen().openGui(); }
|
case "counthouse", "shop"
|
||||||
|
-> { NetSend.requestSnapshot("SELL"); new CountingHouseScreen().openGui(); }
|
||||||
case "questlog" -> { NetSend.requestSnapshot("ALL"); new QuestLogScreen().openGui(); }
|
case "questlog" -> { NetSend.requestSnapshot("ALL"); new QuestLogScreen().openGui(); }
|
||||||
case "hub" -> { NetSend.requestSnapshot("ALL"); new HubScreen().openGui(); }
|
case "hub" -> { NetSend.requestSnapshot("ALL"); new HubScreen().openGui(); }
|
||||||
default -> BnsToolkit.LOG.warn("[bnstoolkit] unknown screen requested: {}", which);
|
default -> BnsToolkit.LOG.warn("[bnstoolkit] unknown screen requested: {}", which);
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ public final class SpurHud {
|
|||||||
// Cache the rendered Component objects, rebuild only when the state changes.
|
// Cache the rendered Component objects, rebuild only when the state changes.
|
||||||
// Keeps allocation off the 60 Hz render path.
|
// Keeps allocation off the 60 Hz render path.
|
||||||
private int cachedTier = -1;
|
private int cachedTier = -1;
|
||||||
private long cachedSpurs = -1L;
|
private long cachedCash = -1L;
|
||||||
|
private long cachedBank = -1L;
|
||||||
private Component cachedTierLine = Component.empty();
|
private Component cachedTierLine = Component.empty();
|
||||||
private Component cachedSpursLine = Component.empty();
|
private Component cachedCashLine = Component.empty();
|
||||||
|
private Component cachedBankLine = Component.empty();
|
||||||
|
|
||||||
public static void onRegisterGuiLayers(RegisterGuiLayersEvent event) {
|
public static void onRegisterGuiLayers(RegisterGuiLayersEvent event) {
|
||||||
event.registerAbove(VanillaGuiLayers.EXPERIENCE_BAR, LAYER_ID, INSTANCE::render);
|
event.registerAbove(VanillaGuiLayers.EXPERIENCE_BAR, LAYER_ID, INSTANCE::render);
|
||||||
@@ -43,19 +45,26 @@ public final class SpurHud {
|
|||||||
if (mc == null || mc.player == null) return;
|
if (mc == null || mc.player == null) return;
|
||||||
if (mc.options.hideGui) return;
|
if (mc.options.hideGui) return;
|
||||||
|
|
||||||
if (cachedTier != ClientBnsState.tier || cachedSpurs != ClientBnsState.spurs) {
|
if (cachedTier != ClientBnsState.tier
|
||||||
|
|| cachedCash != ClientBnsState.cashOnHand
|
||||||
|
|| cachedBank != ClientBnsState.bankBalance) {
|
||||||
CivicTier cfg = CivicTier.byId(ClientBnsState.tier);
|
CivicTier cfg = CivicTier.byId(ClientBnsState.tier);
|
||||||
cachedTierLine = Component.literal("§" + cfg.colour().getChar() + cfg.name() + "§r");
|
cachedTierLine = Component.literal("§" + cfg.colour().getChar() + cfg.name() + "§r");
|
||||||
cachedSpursLine = Component.literal("§6" + TierUpgradeScreen.formatSpurs(ClientBnsState.spurs) + " spurs§r");
|
cachedCashLine = Component.literal(
|
||||||
|
"§e" + TierUpgradeScreen.formatSpurs(ClientBnsState.cashOnHand) + "§7 cash§r");
|
||||||
|
cachedBankLine = Component.literal(
|
||||||
|
"§6" + TierUpgradeScreen.formatSpurs(ClientBnsState.bankBalance) + "§7 bank§r");
|
||||||
cachedTier = ClientBnsState.tier;
|
cachedTier = ClientBnsState.tier;
|
||||||
cachedSpurs = ClientBnsState.spurs;
|
cachedCash = ClientBnsState.cashOnHand;
|
||||||
|
cachedBank = ClientBnsState.bankBalance;
|
||||||
}
|
}
|
||||||
|
|
||||||
int x = 4;
|
int x = 4;
|
||||||
int y = 4;
|
int y = 4;
|
||||||
int textW = Math.max(mc.font.width(cachedTierLine), mc.font.width(cachedSpursLine));
|
int textW = Math.max(mc.font.width(cachedTierLine),
|
||||||
|
Math.max(mc.font.width(cachedCashLine), mc.font.width(cachedBankLine)));
|
||||||
int w = textW + 8;
|
int w = textW + 8;
|
||||||
int h = 24;
|
int h = 34;
|
||||||
|
|
||||||
gfx.fill(x, y, x + w, y + h, 0x80000000);
|
gfx.fill(x, y, x + w, y + h, 0x80000000);
|
||||||
gfx.fill(x, y, x + w, y + 1, 0xFF666666);
|
gfx.fill(x, y, x + w, y + 1, 0xFF666666);
|
||||||
@@ -64,6 +73,7 @@ public final class SpurHud {
|
|||||||
gfx.fill(x + w - 1, y, x + w, y + h, 0xFF666666);
|
gfx.fill(x + w - 1, y, x + w, y + h, 0xFF666666);
|
||||||
|
|
||||||
gfx.drawString(mc.font, cachedTierLine, x + 4, y + 4, 0xFFFFFFFF, true);
|
gfx.drawString(mc.font, cachedTierLine, x + 4, y + 4, 0xFFFFFFFF, true);
|
||||||
gfx.drawString(mc.font, cachedSpursLine, x + 4, y + 14, 0xFFFFFFFF, true);
|
gfx.drawString(mc.font, cachedCashLine, x + 4, y + 14, 0xFFFFFFFF, true);
|
||||||
|
gfx.drawString(mc.font, cachedBankLine, x + 4, y + 24, 0xFFFFFFFF, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-12
@@ -22,21 +22,25 @@ import uk.sijbers.bnstoolkit.network.NetSend;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bazaar sell-shop screen.
|
* The Counting House — wealth exchange screen.
|
||||||
*
|
*
|
||||||
* Grid of 9 sell-list items. Click an item to select it. The right pane
|
* Not a general shop. Players bring diamonds / netherite / ancient
|
||||||
* shows the selected item's effective price after fee + diminishing
|
* debris and convert them to spurs, credited straight to their bank
|
||||||
* returns. Three quick-sell buttons: 1 / 16 / 64 (or whatever fits in
|
* account. Iron / gold / coal / etc. are NOT here — they belong on
|
||||||
* the player's inventory).
|
* player-run Numismatics Vendor blocks at market prices.
|
||||||
*
|
*
|
||||||
* Sell action: sends a "sell <item> <count>" routed C2S packet. Server
|
* Right pane shows current effective price for the selected wealth
|
||||||
* runs the KubeJS-owned /bns sell verb; KubeJS subtracts items and pays
|
* token after the player's tier fee + DR. Quick-exchange buttons cover
|
||||||
* the spurs; server pushes both an updated SellSnapshot AND a fresh
|
* 1 / 16 / 64.
|
||||||
* tier/balance.
|
*
|
||||||
|
* Reached via the Counting House Clerk NPC at spawn — not openable from
|
||||||
|
* anywhere via keybind. The sell action sends a {@code RunBnsCommand}
|
||||||
|
* packet which calls Java-side {@code /bns sell}; that handler pulls
|
||||||
|
* items, computes payout, and deposits to the Numismatics bank.
|
||||||
*/
|
*/
|
||||||
public final class ShopBrowserScreen extends BaseBnsScreen {
|
public final class CountingHouseScreen extends BaseBnsScreen {
|
||||||
public ShopBrowserScreen() {
|
public CountingHouseScreen() {
|
||||||
super(Component.translatable("screen.bnstoolkit.shop_browser"));
|
super(Component.translatable("screen.bnstoolkit.counting_house"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResourceLocation selected = ResourceLocation.parse("minecraft:diamond");
|
private static ResourceLocation selected = ResourceLocation.parse("minecraft:diamond");
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
package uk.sijbers.bnstoolkit.client.screens;
|
||||||
|
|
||||||
import dev.ftb.mods.ftblibrary.icon.Color4I;
|
import dev.ftb.mods.ftblibrary.icon.Color4I;
|
||||||
import dev.ftb.mods.ftblibrary.icon.Icon;
|
|
||||||
import dev.ftb.mods.ftblibrary.icon.Icons;
|
import dev.ftb.mods.ftblibrary.icon.Icons;
|
||||||
import dev.ftb.mods.ftblibrary.ui.Panel;
|
import dev.ftb.mods.ftblibrary.ui.Panel;
|
||||||
import dev.ftb.mods.ftblibrary.ui.SimpleTextButton;
|
import dev.ftb.mods.ftblibrary.ui.SimpleTextButton;
|
||||||
@@ -14,15 +13,17 @@ import uk.sijbers.bnstoolkit.data.CivicTier;
|
|||||||
import uk.sijbers.bnstoolkit.network.NetSend;
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Brass and Sigil hub — the single entry point opened by the B key.
|
* Brass and Sigil hub — your civic status card.
|
||||||
*
|
*
|
||||||
* Header: current tier (in tier colour) + spurs balance.
|
* <p>Status-only. Shows your tier, cash on hand, bank balance, total
|
||||||
* Body: 5 large buttons that route to the sub-screens. Clicking a button
|
* wealth, current shop fee, slot caps, active bounties count, owned
|
||||||
* fires the matching snapshot request and opens the screen — same flow
|
* plots count. The Quest Log button opens the read-only quest overview.
|
||||||
* as the (now mostly-unbound) direct shortcut keys.
|
|
||||||
*
|
*
|
||||||
* Closing a sub-screen returns to the hub (so this hub becomes a real
|
* <p>Transactional features (Counting House exchange, Bounty Board,
|
||||||
* navigation surface, not a one-shot menu).
|
* Tier Upgrade, Plot Office) are not reachable from the hub by design.
|
||||||
|
* Players visit the corresponding NPC at spawn — that's what makes spawn
|
||||||
|
* worth caring about. NPCs invoke {@code /bns open <screen>} on the
|
||||||
|
* clicking player via Easy NPC dialogue.
|
||||||
*/
|
*/
|
||||||
public final class HubScreen extends BaseBnsScreen {
|
public final class HubScreen extends BaseBnsScreen {
|
||||||
|
|
||||||
@@ -32,64 +33,75 @@ public final class HubScreen extends BaseBnsScreen {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addWidgets() {
|
public void addWidgets() {
|
||||||
add(new StatusHeader(this));
|
add(new StatusPanel(this));
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Quest Log"), Icons.INFO,
|
||||||
add(makeButton("Civic Tier", Icons.PLAYER, "TIER", () -> new TierUpgradeScreen()));
|
btn -> { NetSend.requestSnapshot("ALL"); new QuestLogScreen().openGui(); }));
|
||||||
add(makeButton("Bazaar Shop", Icons.ADD, "SELL", () -> new ShopBrowserScreen()));
|
add(SimpleTextButton.create(this, Component.literal("Refresh"), Icons.REFRESH,
|
||||||
add(makeButton("Bounty Board", Icons.ACCEPT, "BOUNTY", () -> new BountyBoardScreen()));
|
btn -> NetSend.requestSnapshot("ALL")));
|
||||||
add(makeButton("Plot Office", Icons.FRIENDS, "PLOT", () -> new PlotPurchaseScreen()));
|
|
||||||
add(makeButton("Quest Log", Icons.INFO, "ALL", () -> new QuestLogScreen()));
|
|
||||||
add(SimpleTextButton.create(this, Component.literal("Close"), Icons.CLOSE,
|
add(SimpleTextButton.create(this, Component.literal("Close"), Icons.CLOSE,
|
||||||
btn -> closeGui()));
|
btn -> closeGui()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void alignWidgets() {
|
public void alignWidgets() {
|
||||||
// All coordinates here are RELATIVE to the panel's top-left — FTB
|
// Status panel fills most of the body
|
||||||
// Library's renderer adds the panel's own (getX(), getY()) for us.
|
getWidgets().get(0).setPos(8, contentTop());
|
||||||
Widget header = getWidgets().get(0);
|
getWidgets().get(0).setSize(getWidth() - 16, getHeight() - 50);
|
||||||
header.setPos(8, contentTop());
|
|
||||||
header.setSize(getWidth() - 16, 20);
|
|
||||||
|
|
||||||
int cols = 2;
|
// Bottom action row
|
||||||
int btnW = (getWidth() - 24) / cols;
|
int btnY = getHeight() - 22;
|
||||||
int btnH = 22;
|
int btnH = 16;
|
||||||
int gap = 4;
|
Widget quest = getWidgets().get(1);
|
||||||
int top = contentTop() + 28;
|
quest.setSize(72, btnH); quest.setPos(8, btnY);
|
||||||
for (int i = 0; i < 5; i++) {
|
Widget refresh = getWidgets().get(2);
|
||||||
int row = i / cols;
|
refresh.setSize(60, btnH); refresh.setPos(84, btnY);
|
||||||
int col = i % cols;
|
Widget close = getWidgets().get(3);
|
||||||
Widget w = getWidgets().get(1 + i);
|
close.setSize(60, btnH); close.setPos(getWidth() - 68, btnY);
|
||||||
w.setSize(btnW, btnH);
|
}
|
||||||
w.setPos(8 + col * (btnW + gap), top + row * (btnH + gap));
|
|
||||||
|
/** Status display — pure read-only. */
|
||||||
|
private static final class StatusPanel extends Panel {
|
||||||
|
StatusPanel(Panel parent) { super(parent); }
|
||||||
|
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
CivicTier cfg = CivicTier.byId(ClientBnsState.tier);
|
||||||
|
long cash = ClientBnsState.cashOnHand;
|
||||||
|
long bank = ClientBnsState.bankBalance;
|
||||||
|
long total = cash + bank;
|
||||||
|
add(new Row(this, "§e=== Civic Status ==="));
|
||||||
|
add(new Row(this, "§7Tier: §" + cfg.colour().getChar() + cfg.name()
|
||||||
|
+ " §7(" + ClientBnsState.tier + "/" + CivicTier.MAX + ")"));
|
||||||
|
add(new Row(this, ""));
|
||||||
|
add(new Row(this, "§e=== Wealth ==="));
|
||||||
|
add(new Row(this, "§7Cash on hand: §f" + TierUpgradeScreen.formatSpurs(cash) + " spurs"));
|
||||||
|
add(new Row(this, "§7Bank balance: §f" + TierUpgradeScreen.formatSpurs(bank) + " spurs"));
|
||||||
|
add(new Row(this, "§7Total wealth: §a" + TierUpgradeScreen.formatSpurs(total) + " spurs"));
|
||||||
|
add(new Row(this, ""));
|
||||||
|
add(new Row(this, "§e=== Perks ==="));
|
||||||
|
add(new Row(this, "§7Exchange fee: §c" + ClientBnsState.effectiveFeePct + "%"));
|
||||||
|
add(new Row(this, "§7Plot slots: §f" + ClientBnsState.ownedPlotIds.size()
|
||||||
|
+ " / " + ClientBnsState.plotSlotCap));
|
||||||
|
add(new Row(this, "§7Bounty slots: §f" + ClientBnsState.activeBounties.size()
|
||||||
|
+ " / " + ClientBnsState.bountySlotCap));
|
||||||
|
add(new Row(this, ""));
|
||||||
|
add(new Row(this, "§8Visit spawn NPCs for trades, bounties, plots, and tier upgrades."));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget close = getWidgets().get(6);
|
@Override public void alignWidgets() {
|
||||||
close.setSize(64, 16);
|
int y = 0;
|
||||||
close.setPos(getWidth() - 72, getHeight() - 22);
|
for (Widget w : getWidgets()) {
|
||||||
|
w.setSize(width, 12);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SimpleTextButton makeButton(String label, Icon icon, String snapshot,
|
private static final class Row extends Widget {
|
||||||
java.util.function.Supplier<BaseBnsScreen> ctor) {
|
private final String text;
|
||||||
return SimpleTextButton.create(this, Component.literal(label), icon, btn -> {
|
Row(Panel parent, String text) { super(parent); this.text = text; }
|
||||||
NetSend.requestSnapshot(snapshot);
|
|
||||||
ctor.get().openGui();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Status header — tier, balance, fee at the top of the hub. */
|
|
||||||
private static final class StatusHeader extends Widget {
|
|
||||||
StatusHeader(Panel parent) { super(parent); }
|
|
||||||
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
Color4I.rgba(0x40FFFFFF).draw(gfx, x, y, w, h);
|
theme.drawString(gfx, Component.literal(text), x + 2, y + 2, Color4I.WHITE, Theme.SHADOW);
|
||||||
CivicTier cfg = CivicTier.byId(ClientBnsState.tier);
|
|
||||||
String left = "Tier: §" + cfg.colour().getChar() + cfg.name() + "§r";
|
|
||||||
String right = "Balance: §6" + TierUpgradeScreen.formatSpurs(ClientBnsState.spurs)
|
|
||||||
+ "§r §7(fee " + ClientBnsState.effectiveFeePct + "%)§r";
|
|
||||||
theme.drawString(gfx, Component.literal(left), x + 4, y + 6, Color4I.WHITE, Theme.SHADOW);
|
|
||||||
int rightW = theme.getFont().width(right);
|
|
||||||
theme.drawString(gfx, Component.literal(right), x + w - rightW - 4, y + 6,
|
|
||||||
Color4I.WHITE, Theme.SHADOW);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,30 +6,34 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bazaar sell prices. Mirrors SELL_PRICES in
|
* Counting House exchange rates.
|
||||||
* pack/overrides/kubejs/server_scripts/economy/02_sell_shop.js.
|
|
||||||
*
|
*
|
||||||
* The map is insertion-ordered so the screen grid matches the KubeJS
|
* <p>The Counting House is the realm's wealth exchange — not a general
|
||||||
* config order without separate display ordering.
|
* shop. It accepts the universal wealth-store items (diamond, netherite,
|
||||||
|
* ancient debris) and credits the player's bank account in spurs.
|
||||||
|
*
|
||||||
|
* <p>Iron, gold, coal, redstone, mob drops etc. are <em>not</em> here.
|
||||||
|
* Players who want to sell those operate their own shops with
|
||||||
|
* Numismatics Vendor blocks — free market, player-set prices.
|
||||||
|
*
|
||||||
|
* <p>The diminishing-returns floor stops the diamond-mine-forever
|
||||||
|
* exploit even though chest loot is finite: per-player DR depletes
|
||||||
|
* over a sustained grind. Floor 30% means a maxed-out diamond miner
|
||||||
|
* still earns 3 spurs per diamond instead of 10.
|
||||||
*/
|
*/
|
||||||
public final class SellCatalogue {
|
public final class SellCatalogue {
|
||||||
private SellCatalogue() {}
|
private SellCatalogue() {}
|
||||||
|
|
||||||
public static final int DR_STEP = 64;
|
public static final int DR_STEP = 64;
|
||||||
public static final double DR_DECAY = 0.10;
|
public static final double DR_DECAY = 0.10;
|
||||||
public static final double DR_FLOOR = 0.50;
|
public static final double DR_FLOOR = 0.30;
|
||||||
|
|
||||||
public static final Map<ResourceLocation, Long> PRICES;
|
public static final Map<ResourceLocation, Long> PRICES;
|
||||||
static {
|
static {
|
||||||
Map<ResourceLocation, Long> m = new LinkedHashMap<>();
|
Map<ResourceLocation, Long> m = new LinkedHashMap<>();
|
||||||
m.put(ResourceLocation.parse("minecraft:diamond"), 10L);
|
m.put(ResourceLocation.parse("minecraft:diamond"), 10L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:diamond_block"), 90L); // 9 diamonds, slight discount
|
||||||
m.put(ResourceLocation.parse("minecraft:netherite_ingot"), 100L);
|
m.put(ResourceLocation.parse("minecraft:netherite_ingot"), 100L);
|
||||||
m.put(ResourceLocation.parse("minecraft:emerald"), 1L);
|
|
||||||
m.put(ResourceLocation.parse("minecraft:gold_ingot"), 2L);
|
|
||||||
m.put(ResourceLocation.parse("minecraft:iron_ingot"), 1L);
|
|
||||||
m.put(ResourceLocation.parse("minecraft:lapis_lazuli"), 1L);
|
|
||||||
m.put(ResourceLocation.parse("minecraft:redstone"), 1L);
|
|
||||||
m.put(ResourceLocation.parse("minecraft:coal"), 1L);
|
|
||||||
m.put(ResourceLocation.parse("minecraft:ancient_debris"), 500L);
|
m.put(ResourceLocation.parse("minecraft:ancient_debris"), 500L);
|
||||||
PRICES = java.util.Collections.unmodifiableMap(m);
|
PRICES = java.util.Collections.unmodifiableMap(m);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import net.minecraft.resources.ResourceLocation;
|
|||||||
import net.minecraft.world.entity.player.Inventory;
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import uk.sijbers.bnstoolkit.economy.NumismaticsBridge;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -34,8 +35,17 @@ public final class SpurBalance {
|
|||||||
COIN_VALUES = java.util.Collections.unmodifiableMap(m);
|
COIN_VALUES = java.util.Collections.unmodifiableMap(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inventory-only scan (excludes ender chests, curios, backpacks). */
|
/**
|
||||||
|
* Total wealth: inventory coins + the player's personal Numismatics
|
||||||
|
* bank balance. No card lookups required — the bank is canonical and
|
||||||
|
* always accessible by UUID.
|
||||||
|
*/
|
||||||
public static long totalSpurs(Player player) {
|
public static long totalSpurs(Player player) {
|
||||||
|
return inventoryCoins(player) + NumismaticsBridge.balanceOf(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inventory-only coin sum (no bank). Useful for "cash on hand" displays. */
|
||||||
|
public static long inventoryCoins(Player player) {
|
||||||
Inventory inv = player.getInventory();
|
Inventory inv = player.getInventory();
|
||||||
long total = 0L;
|
long total = 0L;
|
||||||
for (int i = 0; i < inv.getContainerSize(); i++) {
|
for (int i = 0; i < inv.getContainerSize(); i++) {
|
||||||
@@ -62,4 +72,67 @@ public final class SpurBalance {
|
|||||||
}
|
}
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove up to {@code amount} spurs worth of Numismatics coins from
|
||||||
|
* the player's inventory, starting with the smallest denomination so
|
||||||
|
* we can pay exact-amount transfers. Returns the actual amount taken
|
||||||
|
* (may be < amount if the inventory didn't have enough).
|
||||||
|
*/
|
||||||
|
public static long takeCoins(Player player, long amount) {
|
||||||
|
if (amount <= 0) return 0;
|
||||||
|
long taken = 0;
|
||||||
|
Inventory inv = player.getInventory();
|
||||||
|
// Build ordered denomination list (low → high) so we deplete small coins first
|
||||||
|
var denoms = new java.util.ArrayList<>(COIN_VALUES.entrySet());
|
||||||
|
denoms.sort(java.util.Map.Entry.comparingByValue());
|
||||||
|
for (var entry : denoms) {
|
||||||
|
long denomValue = entry.getValue();
|
||||||
|
if (taken >= amount) break;
|
||||||
|
long needed = (long) Math.ceil((amount - taken) / (double) denomValue);
|
||||||
|
for (int i = 0; i < inv.getContainerSize() && needed > 0 && taken < amount; i++) {
|
||||||
|
ItemStack stack = inv.getItem(i);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
ResourceLocation id = stack.getItemHolder().unwrapKey().map(k -> k.location()).orElse(null);
|
||||||
|
if (id == null || !entry.getKey().equals(id)) continue;
|
||||||
|
int take = (int) Math.min(stack.getCount(), needed);
|
||||||
|
stack.shrink(take);
|
||||||
|
taken += (long) take * denomValue;
|
||||||
|
needed -= take;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Math.min(taken, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add {@code amount} spurs to the player's inventory using the
|
||||||
|
* largest denomination that fits, smallest-first overflow.
|
||||||
|
* Drops as items if the inventory is full.
|
||||||
|
*/
|
||||||
|
public static void giveCoins(Player player, long amount) {
|
||||||
|
if (amount <= 0) return;
|
||||||
|
Inventory inv = player.getInventory();
|
||||||
|
var denoms = new java.util.ArrayList<>(COIN_VALUES.entrySet());
|
||||||
|
denoms.sort((a, b) -> Long.compare(b.getValue(), a.getValue())); // high → low
|
||||||
|
long remaining = amount;
|
||||||
|
for (var entry : denoms) {
|
||||||
|
long denomValue = entry.getValue();
|
||||||
|
int qty = (int) (remaining / denomValue);
|
||||||
|
if (qty <= 0) continue;
|
||||||
|
remaining %= denomValue;
|
||||||
|
net.minecraft.world.item.Item item =
|
||||||
|
net.minecraft.core.registries.BuiltInRegistries.ITEM.get(entry.getKey());
|
||||||
|
if (item == null) continue;
|
||||||
|
int max = item.getDefaultMaxStackSize();
|
||||||
|
while (qty > 0) {
|
||||||
|
int give = Math.min(qty, max);
|
||||||
|
ItemStack stack = new ItemStack(item, give);
|
||||||
|
if (!inv.add(stack)) {
|
||||||
|
// Inventory full — drop at the player's feet
|
||||||
|
player.drop(stack, false);
|
||||||
|
}
|
||||||
|
qty -= give;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.economy;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.data.SpurBalance;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reflective adapter for Create: Numismatics' bank API.
|
||||||
|
*
|
||||||
|
* <h2>Why reflection</h2>
|
||||||
|
* Numismatics is always in the pack so a hard compileOnly dep would be
|
||||||
|
* fine, but reflection keeps bnstoolkit lighter on the build classpath
|
||||||
|
* and matches the pattern we already use for KubeJS' MinecraftServer
|
||||||
|
* mixin in {@link uk.sijbers.bnstoolkit.network.ServerEconomyBridge}.
|
||||||
|
*
|
||||||
|
* <h2>What we use</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code Numismatics.BANK} — public static {@code GlobalBankManager} singleton</li>
|
||||||
|
* <li>{@code GlobalBankManager.getAccount(Player)} — null if no account yet</li>
|
||||||
|
* <li>{@code GlobalBankManager.getOrCreateAccount(UUID, BankAccount.Type)} — auto-create with type=PLAYER</li>
|
||||||
|
* <li>{@code BankAccount.getBalance() / deposit(int) / deduct(int)} — int-typed balance, base spur denomination</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Lifecycle</h2>
|
||||||
|
* Players auto-get a personal bank account on first {@code PlayerLoggedInEvent}
|
||||||
|
* via {@link #ensureAccount(ServerPlayer)}. After that the HUD always reads
|
||||||
|
* inv + bank ({@link SpurBalance#totalSpurs}) regardless of cards.
|
||||||
|
*/
|
||||||
|
public final class NumismaticsBridge {
|
||||||
|
private NumismaticsBridge() {}
|
||||||
|
|
||||||
|
private static final String NUMISMATICS_FQN = "dev.ithundxr.createnumismatics.Numismatics";
|
||||||
|
private static final String BANK_MGR_FQN = "dev.ithundxr.createnumismatics.content.backend.GlobalBankManager";
|
||||||
|
private static final String BANK_ACCOUNT_FQN = "dev.ithundxr.createnumismatics.content.backend.BankAccount";
|
||||||
|
private static final String BANK_TYPE_FQN = "dev.ithundxr.createnumismatics.content.backend.BankAccount$Type";
|
||||||
|
|
||||||
|
private static volatile boolean lookupDone = false;
|
||||||
|
private static volatile Object bankManager; // GlobalBankManager
|
||||||
|
private static volatile Method getAccountByPlayer; // (Player) -> BankAccount
|
||||||
|
private static volatile Method getAccountByUuid; // (UUID) -> BankAccount
|
||||||
|
private static volatile Method getOrCreateAccount; // (UUID, BankAccount.Type) -> BankAccount
|
||||||
|
private static volatile Method accountGetBalance; // () -> int
|
||||||
|
private static volatile Method accountDeposit; // (int) -> void
|
||||||
|
private static volatile Method accountDeductOk; // (int) -> boolean
|
||||||
|
private static volatile Object typePlayer; // BankAccount.Type.PLAYER enum
|
||||||
|
|
||||||
|
private static synchronized void initLookup() {
|
||||||
|
if (lookupDone) return;
|
||||||
|
try {
|
||||||
|
Class<?> num = Class.forName(NUMISMATICS_FQN);
|
||||||
|
Field f = num.getField("BANK");
|
||||||
|
bankManager = f.get(null);
|
||||||
|
|
||||||
|
Class<?> mgrCls = Class.forName(BANK_MGR_FQN);
|
||||||
|
getAccountByPlayer = mgrCls.getMethod("getAccount", Class.forName("net.minecraft.world.entity.player.Player"));
|
||||||
|
getAccountByUuid = mgrCls.getMethod("getAccount", UUID.class);
|
||||||
|
|
||||||
|
Class<?> typeCls = Class.forName(BANK_TYPE_FQN);
|
||||||
|
getOrCreateAccount = mgrCls.getMethod("getOrCreateAccount", UUID.class, typeCls);
|
||||||
|
|
||||||
|
// Find Type.PLAYER (default the first enum constant if PLAYER is absent)
|
||||||
|
Object[] enumValues = typeCls.getEnumConstants();
|
||||||
|
Object foundPlayer = null;
|
||||||
|
for (Object v : enumValues) {
|
||||||
|
if (((Enum<?>) v).name().equals("PLAYER")) { foundPlayer = v; break; }
|
||||||
|
}
|
||||||
|
typePlayer = foundPlayer != null ? foundPlayer : (enumValues.length > 0 ? enumValues[0] : null);
|
||||||
|
|
||||||
|
Class<?> accountCls = Class.forName(BANK_ACCOUNT_FQN);
|
||||||
|
accountGetBalance = accountCls.getMethod("getBalance");
|
||||||
|
accountDeposit = accountCls.getMethod("deposit", int.class);
|
||||||
|
accountDeductOk = accountCls.getMethod("deduct", int.class);
|
||||||
|
|
||||||
|
BnsToolkit.LOG.info("[bns/bank] linked Numismatics bank API (BANK={}, PLAYER type={})",
|
||||||
|
bankManager != null, typePlayer);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/bank] Numismatics bank API not found ({}) — bank features will no-op", t.toString());
|
||||||
|
} finally {
|
||||||
|
lookupDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean ready() {
|
||||||
|
if (!lookupDone) initLookup();
|
||||||
|
return bankManager != null && accountGetBalance != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Account lookup / creation ───────────────────────────────────
|
||||||
|
|
||||||
|
/** Returns the player's existing account, or null if none + lookup failed. */
|
||||||
|
private static Object lookupAccount(Player player) {
|
||||||
|
if (!ready()) return null;
|
||||||
|
try {
|
||||||
|
Object acc = getAccountByPlayer.invoke(bankManager, player);
|
||||||
|
return acc;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.debug("[bns/bank] getAccount(Player) failed: {}", t.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the given server player has a personal bank account. Called
|
||||||
|
* on PlayerLoggedInEvent. No-op + warn if Numismatics is missing.
|
||||||
|
*/
|
||||||
|
public static void ensureAccount(ServerPlayer sp) {
|
||||||
|
if (!ready() || typePlayer == null || getOrCreateAccount == null) return;
|
||||||
|
try {
|
||||||
|
Object acc = getAccountByUuid.invoke(bankManager, sp.getUUID());
|
||||||
|
if (acc == null) {
|
||||||
|
getOrCreateAccount.invoke(bankManager, sp.getUUID(), typePlayer);
|
||||||
|
BnsToolkit.LOG.info("[bns/bank] created bank account for {}", sp.getName().getString());
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/bank] ensureAccount failed for {}: {}",
|
||||||
|
sp.getName().getString(), t.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Balance / mutate ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Returns the player's bank balance in spurs, or 0 if no account / lookup failed. */
|
||||||
|
public static long balanceOf(Player player) {
|
||||||
|
Object acc = lookupAccount(player);
|
||||||
|
if (acc == null) return 0L;
|
||||||
|
try {
|
||||||
|
return ((Number) accountGetBalance.invoke(acc)).longValue();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Credit the player's bank with {@code spurs}. Auto-creates the account
|
||||||
|
* if missing (the Numismatics-managed flow). Returns true on success.
|
||||||
|
*
|
||||||
|
* Numismatics' deposit takes int. Clamp/loop if we ever overflow but at
|
||||||
|
* 2^31-1 spurs = 2 billion, that's deep into Sovereign territory and not
|
||||||
|
* a near-term concern.
|
||||||
|
*/
|
||||||
|
public static boolean deposit(Player player, long spurs) {
|
||||||
|
if (spurs <= 0) return true;
|
||||||
|
if (!ready()) return false;
|
||||||
|
try {
|
||||||
|
Object acc = lookupAccount(player);
|
||||||
|
if (acc == null && player instanceof ServerPlayer sp) {
|
||||||
|
getOrCreateAccount.invoke(bankManager, sp.getUUID(), typePlayer);
|
||||||
|
acc = lookupAccount(player);
|
||||||
|
}
|
||||||
|
if (acc == null) return false;
|
||||||
|
|
||||||
|
int amount = (int) Math.min(Integer.MAX_VALUE, spurs);
|
||||||
|
accountDeposit.invoke(acc, amount);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/bank] deposit failed: {}", t.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to deduct {@code spurs} from bank. Returns true on success. */
|
||||||
|
public static boolean deduct(Player player, long spurs) {
|
||||||
|
if (spurs <= 0) return true;
|
||||||
|
Object acc = lookupAccount(player);
|
||||||
|
if (acc == null) return false;
|
||||||
|
try {
|
||||||
|
int amount = (int) Math.min(Integer.MAX_VALUE, spurs);
|
||||||
|
Object result = accountDeductOk.invoke(acc, amount);
|
||||||
|
return Boolean.TRUE.equals(result);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ import uk.sijbers.bnstoolkit.network.s2c.TierStateUpdatePacket;
|
|||||||
public final class BnsNetwork {
|
public final class BnsNetwork {
|
||||||
private BnsNetwork() {}
|
private BnsNetwork() {}
|
||||||
|
|
||||||
public static final String VERSION = "0.4";
|
public static final String VERSION = "0.5";
|
||||||
|
|
||||||
public static void register(IEventBus modBus) {
|
public static void register(IEventBus modBus) {
|
||||||
modBus.addListener(BnsNetwork::onRegisterPayloads);
|
modBus.addListener(BnsNetwork::onRegisterPayloads);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import uk.sijbers.bnstoolkit.BnsToolkit;
|
|||||||
import uk.sijbers.bnstoolkit.data.CivicTier;
|
import uk.sijbers.bnstoolkit.data.CivicTier;
|
||||||
import uk.sijbers.bnstoolkit.data.Plot;
|
import uk.sijbers.bnstoolkit.data.Plot;
|
||||||
import uk.sijbers.bnstoolkit.data.SpurBalance;
|
import uk.sijbers.bnstoolkit.data.SpurBalance;
|
||||||
|
import uk.sijbers.bnstoolkit.economy.NumismaticsBridge;
|
||||||
import uk.sijbers.bnstoolkit.network.s2c.BountySnapshotPacket;
|
import uk.sijbers.bnstoolkit.network.s2c.BountySnapshotPacket;
|
||||||
import uk.sijbers.bnstoolkit.network.s2c.OpenScreenPacket;
|
import uk.sijbers.bnstoolkit.network.s2c.OpenScreenPacket;
|
||||||
import uk.sijbers.bnstoolkit.network.s2c.PlotSnapshotPacket;
|
import uk.sijbers.bnstoolkit.network.s2c.PlotSnapshotPacket;
|
||||||
@@ -214,9 +215,10 @@ public final class ServerEconomyBridge {
|
|||||||
int tier = data.contains("bnsTier") ? data.getInt("bnsTier") : 1;
|
int tier = data.contains("bnsTier") ? data.getInt("bnsTier") : 1;
|
||||||
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
||||||
CivicTier cfg = CivicTier.byId(tier);
|
CivicTier cfg = CivicTier.byId(tier);
|
||||||
long spurs = SpurBalance.totalSpurs(sp);
|
long cash = SpurBalance.inventoryCoins(sp);
|
||||||
|
long bank = NumismaticsBridge.balanceOf(sp);
|
||||||
int fee = cfg.effectiveShopFeePct();
|
int fee = cfg.effectiveShopFeePct();
|
||||||
PacketDistributor.sendToPlayer(sp, new TierStateUpdatePacket(tier, spurs, fee));
|
PacketDistributor.sendToPlayer(sp, new TierStateUpdatePacket(tier, cash, bank, fee));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void pushSellSnapshot(ServerPlayer sp) {
|
public static void pushSellSnapshot(ServerPlayer sp) {
|
||||||
|
|||||||
@@ -9,15 +9,23 @@ import net.neoforged.neoforge.network.handling.IPayloadContext;
|
|||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
|
||||||
/** Server -> client: civic tier (1..13), spurs balance, effective shop fee%. */
|
/**
|
||||||
public record TierStateUpdatePacket(int tier, long spurs, int effectiveFeePct) implements CustomPacketPayload {
|
* S2C: civic tier (1..13), cash on hand (inventory coins), bank balance
|
||||||
|
* (Numismatics personal account), and the effective Counting House fee%.
|
||||||
|
*
|
||||||
|
* Cash + bank are sent separately so the hub UI can break them out
|
||||||
|
* ("on hand" vs "in bank") while the HUD overlay sums them for the
|
||||||
|
* "total wealth" display.
|
||||||
|
*/
|
||||||
|
public record TierStateUpdatePacket(int tier, long cashOnHand, long bankBalance, int effectiveFeePct) implements CustomPacketPayload {
|
||||||
public static final Type<TierStateUpdatePacket> TYPE = new Type<>(
|
public static final Type<TierStateUpdatePacket> TYPE = new Type<>(
|
||||||
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "tier_state_update"));
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "tier_state_update"));
|
||||||
|
|
||||||
public static final StreamCodec<RegistryFriendlyByteBuf, TierStateUpdatePacket> STREAM_CODEC =
|
public static final StreamCodec<RegistryFriendlyByteBuf, TierStateUpdatePacket> STREAM_CODEC =
|
||||||
StreamCodec.composite(
|
StreamCodec.composite(
|
||||||
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::tier,
|
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::tier,
|
||||||
ByteBufCodecs.VAR_LONG, TierStateUpdatePacket::spurs,
|
ByteBufCodecs.VAR_LONG, TierStateUpdatePacket::cashOnHand,
|
||||||
|
ByteBufCodecs.VAR_LONG, TierStateUpdatePacket::bankBalance,
|
||||||
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::effectiveFeePct,
|
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::effectiveFeePct,
|
||||||
TierStateUpdatePacket::new);
|
TierStateUpdatePacket::new);
|
||||||
|
|
||||||
@@ -29,7 +37,9 @@ public record TierStateUpdatePacket(int tier, long spurs, int effectiveFeePct) i
|
|||||||
public static void handle(TierStateUpdatePacket pkt, IPayloadContext ctx) {
|
public static void handle(TierStateUpdatePacket pkt, IPayloadContext ctx) {
|
||||||
ctx.enqueueWork(() -> {
|
ctx.enqueueWork(() -> {
|
||||||
ClientBnsState.tier = pkt.tier;
|
ClientBnsState.tier = pkt.tier;
|
||||||
ClientBnsState.spurs = pkt.spurs;
|
ClientBnsState.cashOnHand = pkt.cashOnHand;
|
||||||
|
ClientBnsState.bankBalance = pkt.bankBalance;
|
||||||
|
ClientBnsState.spurs = pkt.cashOnHand + pkt.bankBalance; // kept for HUD compatibility
|
||||||
ClientBnsState.effectiveFeePct = pkt.effectiveFeePct;
|
ClientBnsState.effectiveFeePct = pkt.effectiveFeePct;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public final class BnsServerCommands {
|
|||||||
private BnsServerCommands() {}
|
private BnsServerCommands() {}
|
||||||
|
|
||||||
private static final String[] SCREEN_NAMES = {
|
private static final String[] SCREEN_NAMES = {
|
||||||
"hub", "tier", "bounty", "plot", "shop", "questlog"
|
"hub", "tier", "bounty", "plot", "counthouse", "shop", "questlog"
|
||||||
};
|
};
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.server;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.commands.Commands;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
|
import net.neoforged.fml.common.EventBusSubscriber;
|
||||||
|
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.data.CivicTier;
|
||||||
|
import uk.sijbers.bnstoolkit.data.SellCatalogue;
|
||||||
|
import uk.sijbers.bnstoolkit.data.SpurBalance;
|
||||||
|
import uk.sijbers.bnstoolkit.economy.NumismaticsBridge;
|
||||||
|
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counting House commands — the wealth-exchange + wallet inspection
|
||||||
|
* verbs that used to live in KubeJS.
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code /bns sell <item> <count>} — exchange wealth-token items
|
||||||
|
* for spurs, credited straight to the player's Numismatics bank
|
||||||
|
* account. NPC dialogue at the Counting House Clerk fires this on
|
||||||
|
* the clicking player.</li>
|
||||||
|
* <li>{@code /bns wallet} — show inventory cash, bank balance, total.</li>
|
||||||
|
* <li>{@code /bns wallet deposit <amount>} — move coins from inventory
|
||||||
|
* to bank.</li>
|
||||||
|
* <li>{@code /bns wallet withdraw <amount>} — bank to inventory as
|
||||||
|
* Numismatics coins (uses the largest denomination that fits).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* The KubeJS-side {@code /bns sell} was retired alongside this commit —
|
||||||
|
* Java is now the source of truth so we can credit the bank atomically.
|
||||||
|
*/
|
||||||
|
@EventBusSubscriber(modid = BnsToolkit.MOD_ID)
|
||||||
|
public final class CountingHouseCommands {
|
||||||
|
private CountingHouseCommands() {}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onRegisterCommands(RegisterCommandsEvent event) {
|
||||||
|
LiteralArgumentBuilder<CommandSourceStack> bns = Commands.literal("bns")
|
||||||
|
.then(Commands.literal("sell")
|
||||||
|
.then(Commands.argument("item", StringArgumentType.string())
|
||||||
|
.then(Commands.argument("count", IntegerArgumentType.integer(1, 64 * 36))
|
||||||
|
.executes(ctx -> doSell(
|
||||||
|
ctx.getSource(),
|
||||||
|
StringArgumentType.getString(ctx, "item"),
|
||||||
|
IntegerArgumentType.getInteger(ctx, "count"))))))
|
||||||
|
.then(Commands.literal("wallet")
|
||||||
|
.executes(ctx -> walletStatus(ctx.getSource()))
|
||||||
|
.then(Commands.literal("deposit")
|
||||||
|
.then(Commands.argument("amount", IntegerArgumentType.integer(1))
|
||||||
|
.executes(ctx -> walletDeposit(
|
||||||
|
ctx.getSource(),
|
||||||
|
IntegerArgumentType.getInteger(ctx, "amount")))))
|
||||||
|
.then(Commands.literal("withdraw")
|
||||||
|
.then(Commands.argument("amount", IntegerArgumentType.integer(1))
|
||||||
|
.executes(ctx -> walletWithdraw(
|
||||||
|
ctx.getSource(),
|
||||||
|
IntegerArgumentType.getInteger(ctx, "amount"))))));
|
||||||
|
event.getDispatcher().register(bns);
|
||||||
|
BnsToolkit.LOG.info("[bnstoolkit] registered Counting House + wallet commands");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /bns sell ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static int doSell(CommandSourceStack src, String itemId, int count) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) {
|
||||||
|
src.sendFailure(Component.literal("/bns sell must be run as a player"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
ResourceLocation item;
|
||||||
|
try {
|
||||||
|
item = ResourceLocation.parse(itemId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cInvalid item id: " + itemId));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!SellCatalogue.PRICES.containsKey(item)) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cThe Counting House doesn't exchange '" + itemId
|
||||||
|
+ "'. Try diamonds, netherite, or ancient debris."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory count
|
||||||
|
int have = SpurBalance.countItem(sp, item);
|
||||||
|
if (have < count) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cYou only have " + have + "x " + itemId
|
||||||
|
+ " (need " + count + ")."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier-based fee + lifetime DR
|
||||||
|
CompoundTag pd = sp.getPersistentData();
|
||||||
|
int tier = pd.contains("bnsTier") ? pd.getInt("bnsTier") : 1;
|
||||||
|
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
||||||
|
int feePct = CivicTier.byId(tier).effectiveShopFeePct();
|
||||||
|
|
||||||
|
long lifetimeSold = readSold(pd, item);
|
||||||
|
long payout = SellCatalogue.effectivePayout(item, lifetimeSold, count, feePct);
|
||||||
|
if (payout <= 0) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cPayout would be 0 spurs — sell more later or upgrade tier."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pull items
|
||||||
|
int taken = takeFromInventory(sp.getInventory(), item, count);
|
||||||
|
if (taken != count) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cFailed to remove items — try again."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
writeSold(pd, item, lifetimeSold + count);
|
||||||
|
|
||||||
|
if (!NumismaticsBridge.deposit(sp, payout)) {
|
||||||
|
// Refund items if deposit failed (shouldn't happen post-login since ensureAccount ran)
|
||||||
|
sp.getInventory().add(new ItemStack(
|
||||||
|
net.minecraft.core.registries.BuiltInRegistries.ITEM.get(item), count));
|
||||||
|
sp.sendSystemMessage(Component.literal("§cBank unavailable — items refunded."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sp.sendSystemMessage(Component.literal("§6Sold " + count + "x " + itemId
|
||||||
|
+ " for §a" + payout + " spurs§r§6 → bank."));
|
||||||
|
BnsToolkit.LOG.info("[bns/counthouse] {} sold {}x {} for {} spurs (lifetime now {})",
|
||||||
|
sp.getName().getString(), count, itemId, payout, lifetimeSold + count);
|
||||||
|
|
||||||
|
// Push fresh snapshots so the HUD updates instantly
|
||||||
|
ServerEconomyBridge.pushTierUpdate(sp);
|
||||||
|
ServerEconomyBridge.pushSellSnapshot(sp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── /bns wallet ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static int walletStatus(CommandSourceStack src) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) {
|
||||||
|
src.sendFailure(Component.literal("/bns wallet must be run as a player"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long cash = SpurBalance.inventoryCoins(sp);
|
||||||
|
long bank = NumismaticsBridge.balanceOf(sp);
|
||||||
|
sp.sendSystemMessage(Component.literal("§6═══ Wallet ═══"));
|
||||||
|
sp.sendSystemMessage(Component.literal("§eCash on hand: §f" + cash + " spurs"));
|
||||||
|
sp.sendSystemMessage(Component.literal("§eBank balance: §f" + bank + " spurs"));
|
||||||
|
sp.sendSystemMessage(Component.literal("§eTotal wealth: §a" + (cash + bank) + " spurs"));
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int walletDeposit(CommandSourceStack src, int amount) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) return 0;
|
||||||
|
long cash = SpurBalance.inventoryCoins(sp);
|
||||||
|
if (cash < amount) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cNot enough cash on hand (have " + cash + ", need " + amount + ")"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Take coins from inventory (using SpurBalance helper — needs to be added)
|
||||||
|
long taken = SpurBalance.takeCoins(sp, amount);
|
||||||
|
if (taken < amount) {
|
||||||
|
// Refund any partial (shouldn't happen — SpurBalance.takeCoins is atomic-ish)
|
||||||
|
if (taken > 0) NumismaticsBridge.deposit(sp, taken);
|
||||||
|
sp.sendSystemMessage(Component.literal("§cCoin transfer failed."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!NumismaticsBridge.deposit(sp, amount)) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cBank unavailable."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
sp.sendSystemMessage(Component.literal("§aDeposited " + amount + " spurs to bank."));
|
||||||
|
ServerEconomyBridge.pushTierUpdate(sp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int walletWithdraw(CommandSourceStack src, int amount) {
|
||||||
|
ServerPlayer sp = src.getPlayer();
|
||||||
|
if (sp == null) return 0;
|
||||||
|
long bank = NumismaticsBridge.balanceOf(sp);
|
||||||
|
if (bank < amount) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cNot enough in bank (have " + bank + ", need " + amount + ")"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (!NumismaticsBridge.deduct(sp, amount)) {
|
||||||
|
sp.sendSystemMessage(Component.literal("§cBank deduction failed."));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
SpurBalance.giveCoins(sp, amount);
|
||||||
|
sp.sendSystemMessage(Component.literal("§aWithdrew " + amount + " spurs as coins to inventory."));
|
||||||
|
ServerEconomyBridge.pushTierUpdate(sp);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── NBT helpers (parity with KubeJS bnsSales compound) ─────────
|
||||||
|
|
||||||
|
private static long readSold(CompoundTag pd, ResourceLocation item) {
|
||||||
|
if (!pd.contains("bnsSales")) return 0L;
|
||||||
|
CompoundTag map = pd.getCompound("bnsSales");
|
||||||
|
String key = item.toString();
|
||||||
|
return map.contains(key) ? map.getInt(key) : 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeSold(CompoundTag pd, ResourceLocation item, long value) {
|
||||||
|
CompoundTag map = pd.contains("bnsSales") ? pd.getCompound("bnsSales") : new CompoundTag();
|
||||||
|
map.putInt(item.toString(), (int) Math.min(Integer.MAX_VALUE, value));
|
||||||
|
pd.put("bnsSales", map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int takeFromInventory(Inventory inv, ResourceLocation itemId, int count) {
|
||||||
|
int remaining = count;
|
||||||
|
for (int i = 0; i < inv.getContainerSize() && remaining > 0; i++) {
|
||||||
|
ItemStack stack = inv.getItem(i);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
ResourceLocation id = net.minecraft.core.registries.BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||||
|
if (!itemId.equals(id)) continue;
|
||||||
|
int take = Math.min(stack.getCount(), remaining);
|
||||||
|
stack.shrink(take);
|
||||||
|
remaining -= take;
|
||||||
|
}
|
||||||
|
return count - remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"key.bnstoolkit.tier": "Open Tier Ladder (direct)",
|
"key.bnstoolkit.tier": "Open Tier Ladder (direct)",
|
||||||
"key.bnstoolkit.bounties": "Open Bounty Board (direct)",
|
"key.bnstoolkit.bounties": "Open Bounty Board (direct)",
|
||||||
"key.bnstoolkit.plots": "Open Plot Office (direct)",
|
"key.bnstoolkit.plots": "Open Plot Office (direct)",
|
||||||
"key.bnstoolkit.shop": "Open Bazaar Shop (direct)",
|
"key.bnstoolkit.shop": "Open Counting House (direct)",
|
||||||
"key.bnstoolkit.questlog": "Open Quest Log (direct)",
|
"key.bnstoolkit.questlog": "Open Quest Log (direct)",
|
||||||
|
|
||||||
"screen.bnstoolkit.hub": "Brass and Sigil",
|
"screen.bnstoolkit.hub": "Brass and Sigil",
|
||||||
@@ -12,5 +12,6 @@
|
|||||||
"screen.bnstoolkit.bounty_board": "Bounty Board",
|
"screen.bnstoolkit.bounty_board": "Bounty Board",
|
||||||
"screen.bnstoolkit.quest_log": "Quest Log",
|
"screen.bnstoolkit.quest_log": "Quest Log",
|
||||||
"screen.bnstoolkit.plot_purchase": "Plot Office",
|
"screen.bnstoolkit.plot_purchase": "Plot Office",
|
||||||
"screen.bnstoolkit.shop_browser": "Bazaar"
|
"screen.bnstoolkit.shop_browser": "Counting House",
|
||||||
|
"screen.bnstoolkit.counting_house": "Counting House"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user