From 1d38d86a2e5a25db129e0258219bb24c5b3a6d9a Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 8 Jun 2026 15:32:10 +0000 Subject: [PATCH] =?UTF-8?q?0.6.0:=20Counting=20House=20refactor=20?= =?UTF-8?q?=E2=80=94=20bank-credited=20wealth=20exchange?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- gradle.properties | 2 +- .../uk/sijbers/bnstoolkit/BnsToolkit.java | 5 + .../bnstoolkit/client/BnsKeyBindings.java | 4 +- .../bnstoolkit/client/ClientBnsState.java | 4 +- .../client/OpenScreenDispatcher.java | 5 +- .../bnstoolkit/client/hud/SpurHud.java | 26 +- ...erScreen.java => CountingHouseScreen.java} | 28 ++- .../bnstoolkit/client/screens/HubScreen.java | 120 +++++---- .../bnstoolkit/data/SellCatalogue.java | 26 +- .../sijbers/bnstoolkit/data/SpurBalance.java | 75 +++++- .../bnstoolkit/economy/NumismaticsBridge.java | 179 ++++++++++++++ .../bnstoolkit/network/BnsNetwork.java | 2 +- .../network/ServerEconomyBridge.java | 6 +- .../network/s2c/TierStateUpdatePacket.java | 18 +- .../bnstoolkit/server/BnsServerCommands.java | 2 +- .../server/CountingHouseCommands.java | 230 ++++++++++++++++++ .../assets/bnstoolkit/lang/en_us.json | 5 +- 17 files changed, 635 insertions(+), 102 deletions(-) rename src/main/java/uk/sijbers/bnstoolkit/client/screens/{ShopBrowserScreen.java => CountingHouseScreen.java} (87%) create mode 100644 src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java create mode 100644 src/main/java/uk/sijbers/bnstoolkit/server/CountingHouseCommands.java diff --git a/gradle.properties b/gradle.properties index 83a6274..f100321 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,5 +19,5 @@ loader_version_range=[1,) mod_id=bnstoolkit mod_name=Brass and Sigil Toolkit mod_license=All Rights Reserved -mod_version=0.5.1 +mod_version=0.6.0 mod_group_id=uk.sijbers.bnstoolkit diff --git a/src/main/java/uk/sijbers/bnstoolkit/BnsToolkit.java b/src/main/java/uk/sijbers/bnstoolkit/BnsToolkit.java index 4b57132..427c174 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/BnsToolkit.java +++ b/src/main/java/uk/sijbers/bnstoolkit/BnsToolkit.java @@ -10,6 +10,7 @@ import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import uk.sijbers.bnstoolkit.economy.NumismaticsBridge; import uk.sijbers.bnstoolkit.network.BnsNetwork; import uk.sijbers.bnstoolkit.network.ServerEconomyBridge; @@ -48,6 +49,10 @@ public final class BnsToolkit { @SubscribeEvent public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { 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"); LOG.info("[bnstoolkit] pushed initial economy snapshot to {}", sp.getName().getString()); } diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/BnsKeyBindings.java b/src/main/java/uk/sijbers/bnstoolkit/client/BnsKeyBindings.java index 5de9dd4..b91b0c9 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/client/BnsKeyBindings.java +++ b/src/main/java/uk/sijbers/bnstoolkit/client/BnsKeyBindings.java @@ -10,10 +10,10 @@ import net.neoforged.neoforge.client.settings.KeyConflictContext; import org.lwjgl.glfw.GLFW; import uk.sijbers.bnstoolkit.BnsToolkit; 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.PlotPurchaseScreen; 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.network.NetSend; @@ -114,7 +114,7 @@ public final class BnsKeyBindings { } if (OPEN_SHOP.consumeClick()) { NetSend.requestSnapshot("SELL"); - new ShopBrowserScreen().openGui(); + new CountingHouseScreen().openGui(); } if (OPEN_QUESTLOG.consumeClick()) { NetSend.requestSnapshot("ALL"); diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/ClientBnsState.java b/src/main/java/uk/sijbers/bnstoolkit/client/ClientBnsState.java index eea78d0..842468a 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/client/ClientBnsState.java +++ b/src/main/java/uk/sijbers/bnstoolkit/client/ClientBnsState.java @@ -27,7 +27,9 @@ public final class ClientBnsState { private ClientBnsState() {} 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; // SellSnapshot: diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/OpenScreenDispatcher.java b/src/main/java/uk/sijbers/bnstoolkit/client/OpenScreenDispatcher.java index af8517b..31341c6 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/client/OpenScreenDispatcher.java +++ b/src/main/java/uk/sijbers/bnstoolkit/client/OpenScreenDispatcher.java @@ -2,10 +2,10 @@ package uk.sijbers.bnstoolkit.client; import uk.sijbers.bnstoolkit.BnsToolkit; 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.PlotPurchaseScreen; 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.network.NetSend; @@ -25,7 +25,8 @@ public final class OpenScreenDispatcher { case "tier" -> { NetSend.requestSnapshot("TIER"); new TierUpgradeScreen().openGui(); } case "bounty" -> { NetSend.requestSnapshot("BOUNTY"); new BountyBoardScreen().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 "hub" -> { NetSend.requestSnapshot("ALL"); new HubScreen().openGui(); } default -> BnsToolkit.LOG.warn("[bnstoolkit] unknown screen requested: {}", which); diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/hud/SpurHud.java b/src/main/java/uk/sijbers/bnstoolkit/client/hud/SpurHud.java index 960152c..4e4d319 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/client/hud/SpurHud.java +++ b/src/main/java/uk/sijbers/bnstoolkit/client/hud/SpurHud.java @@ -29,9 +29,11 @@ public final class SpurHud { // Cache the rendered Component objects, rebuild only when the state changes. // Keeps allocation off the 60 Hz render path. private int cachedTier = -1; - private long cachedSpurs = -1L; + private long cachedCash = -1L; + private long cachedBank = -1L; 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) { 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.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); 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; - cachedSpurs = ClientBnsState.spurs; + cachedCash = ClientBnsState.cashOnHand; + cachedBank = ClientBnsState.bankBalance; } int x = 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 h = 24; + int h = 34; gfx.fill(x, y, x + w, y + h, 0x80000000); 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.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); } } diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/screens/ShopBrowserScreen.java b/src/main/java/uk/sijbers/bnstoolkit/client/screens/CountingHouseScreen.java similarity index 87% rename from src/main/java/uk/sijbers/bnstoolkit/client/screens/ShopBrowserScreen.java rename to src/main/java/uk/sijbers/bnstoolkit/client/screens/CountingHouseScreen.java index 2dd9e41..24eed9b 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/client/screens/ShopBrowserScreen.java +++ b/src/main/java/uk/sijbers/bnstoolkit/client/screens/CountingHouseScreen.java @@ -22,21 +22,25 @@ import uk.sijbers.bnstoolkit.network.NetSend; 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 - * shows the selected item's effective price after fee + diminishing - * returns. Three quick-sell buttons: 1 / 16 / 64 (or whatever fits in - * the player's inventory). + * Not a general shop. Players bring diamonds / netherite / ancient + * debris and convert them to spurs, credited straight to their bank + * account. Iron / gold / coal / etc. are NOT here — they belong on + * player-run Numismatics Vendor blocks at market prices. * - * Sell action: sends a "sell " routed C2S packet. Server - * runs the KubeJS-owned /bns sell verb; KubeJS subtracts items and pays - * the spurs; server pushes both an updated SellSnapshot AND a fresh - * tier/balance. + * Right pane shows current effective price for the selected wealth + * token after the player's tier fee + DR. Quick-exchange buttons cover + * 1 / 16 / 64. + * + * 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 ShopBrowserScreen() { - super(Component.translatable("screen.bnstoolkit.shop_browser")); +public final class CountingHouseScreen extends BaseBnsScreen { + public CountingHouseScreen() { + super(Component.translatable("screen.bnstoolkit.counting_house")); } private static ResourceLocation selected = ResourceLocation.parse("minecraft:diamond"); diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/screens/HubScreen.java b/src/main/java/uk/sijbers/bnstoolkit/client/screens/HubScreen.java index 1f16726..a39970e 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/client/screens/HubScreen.java +++ b/src/main/java/uk/sijbers/bnstoolkit/client/screens/HubScreen.java @@ -1,7 +1,6 @@ package uk.sijbers.bnstoolkit.client.screens; 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.ui.Panel; import dev.ftb.mods.ftblibrary.ui.SimpleTextButton; @@ -14,15 +13,17 @@ import uk.sijbers.bnstoolkit.data.CivicTier; 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. - * Body: 5 large buttons that route to the sub-screens. Clicking a button - * fires the matching snapshot request and opens the screen — same flow - * as the (now mostly-unbound) direct shortcut keys. + *

Status-only. Shows your tier, cash on hand, bank balance, total + * wealth, current shop fee, slot caps, active bounties count, owned + * plots count. The Quest Log button opens the read-only quest overview. * - * Closing a sub-screen returns to the hub (so this hub becomes a real - * navigation surface, not a one-shot menu). + *

Transactional features (Counting House exchange, Bounty Board, + * 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 } on the + * clicking player via Easy NPC dialogue. */ public final class HubScreen extends BaseBnsScreen { @@ -32,64 +33,75 @@ public final class HubScreen extends BaseBnsScreen { @Override public void addWidgets() { - add(new StatusHeader(this)); - - add(makeButton("Civic Tier", Icons.PLAYER, "TIER", () -> new TierUpgradeScreen())); - add(makeButton("Bazaar Shop", Icons.ADD, "SELL", () -> new ShopBrowserScreen())); - add(makeButton("Bounty Board", Icons.ACCEPT, "BOUNTY", () -> new BountyBoardScreen())); - add(makeButton("Plot Office", Icons.FRIENDS, "PLOT", () -> new PlotPurchaseScreen())); - add(makeButton("Quest Log", Icons.INFO, "ALL", () -> new QuestLogScreen())); + add(new StatusPanel(this)); + add(SimpleTextButton.create(this, Component.literal("Quest Log"), Icons.INFO, + btn -> { NetSend.requestSnapshot("ALL"); new QuestLogScreen().openGui(); })); + add(SimpleTextButton.create(this, Component.literal("Refresh"), Icons.REFRESH, + btn -> NetSend.requestSnapshot("ALL"))); add(SimpleTextButton.create(this, Component.literal("Close"), Icons.CLOSE, btn -> closeGui())); } @Override public void alignWidgets() { - // All coordinates here are RELATIVE to the panel's top-left — FTB - // Library's renderer adds the panel's own (getX(), getY()) for us. - Widget header = getWidgets().get(0); - header.setPos(8, contentTop()); - header.setSize(getWidth() - 16, 20); + // Status panel fills most of the body + getWidgets().get(0).setPos(8, contentTop()); + getWidgets().get(0).setSize(getWidth() - 16, getHeight() - 50); - int cols = 2; - int btnW = (getWidth() - 24) / cols; - int btnH = 22; - int gap = 4; - int top = contentTop() + 28; - for (int i = 0; i < 5; i++) { - int row = i / cols; - int col = i % cols; - Widget w = getWidgets().get(1 + i); - w.setSize(btnW, btnH); - w.setPos(8 + col * (btnW + gap), top + row * (btnH + gap)); + // Bottom action row + int btnY = getHeight() - 22; + int btnH = 16; + Widget quest = getWidgets().get(1); + quest.setSize(72, btnH); quest.setPos(8, btnY); + Widget refresh = getWidgets().get(2); + refresh.setSize(60, btnH); refresh.setPos(84, btnY); + Widget close = getWidgets().get(3); + close.setSize(60, btnH); close.setPos(getWidth() - 68, btnY); + } + + /** 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); - close.setSize(64, 16); - close.setPos(getWidth() - 72, getHeight() - 22); + @Override public void alignWidgets() { + int y = 0; + for (Widget w : getWidgets()) { + w.setSize(width, 12); + w.setPos(0, y); + y += 12; + } + } } - private SimpleTextButton makeButton(String label, Icon icon, String snapshot, - java.util.function.Supplier ctor) { - return SimpleTextButton.create(this, Component.literal(label), icon, btn -> { - 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); } + private static final class Row extends Widget { + private final String text; + Row(Panel parent, String text) { super(parent); this.text = text; } @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); - 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); + theme.drawString(gfx, Component.literal(text), x + 2, y + 2, Color4I.WHITE, Theme.SHADOW); } } } diff --git a/src/main/java/uk/sijbers/bnstoolkit/data/SellCatalogue.java b/src/main/java/uk/sijbers/bnstoolkit/data/SellCatalogue.java index 8399e02..3199df5 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/data/SellCatalogue.java +++ b/src/main/java/uk/sijbers/bnstoolkit/data/SellCatalogue.java @@ -6,30 +6,34 @@ import java.util.LinkedHashMap; import java.util.Map; /** - * Bazaar sell prices. Mirrors SELL_PRICES in - * pack/overrides/kubejs/server_scripts/economy/02_sell_shop.js. + * Counting House exchange rates. * - * The map is insertion-ordered so the screen grid matches the KubeJS - * config order without separate display ordering. + *

The Counting House is the realm's wealth exchange — not a general + * shop. It accepts the universal wealth-store items (diamond, netherite, + * ancient debris) and credits the player's bank account in spurs. + * + *

Iron, gold, coal, redstone, mob drops etc. are not here. + * Players who want to sell those operate their own shops with + * Numismatics Vendor blocks — free market, player-set prices. + * + *

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 { private SellCatalogue() {} public static final int DR_STEP = 64; 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 PRICES; static { Map m = new LinkedHashMap<>(); 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: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); PRICES = java.util.Collections.unmodifiableMap(m); } diff --git a/src/main/java/uk/sijbers/bnstoolkit/data/SpurBalance.java b/src/main/java/uk/sijbers/bnstoolkit/data/SpurBalance.java index f527602..d38a864 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/data/SpurBalance.java +++ b/src/main/java/uk/sijbers/bnstoolkit/data/SpurBalance.java @@ -4,6 +4,7 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; +import uk.sijbers.bnstoolkit.economy.NumismaticsBridge; import java.util.LinkedHashMap; import java.util.Map; @@ -34,8 +35,17 @@ public final class SpurBalance { 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) { + 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(); long total = 0L; for (int i = 0; i < inv.getContainerSize(); i++) { @@ -62,4 +72,67 @@ public final class SpurBalance { } 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; + } + } + } } diff --git a/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java b/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java new file mode 100644 index 0000000..1f38065 --- /dev/null +++ b/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java @@ -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. + * + *

Why reflection

+ * 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}. + * + *

What we use

+ *
    + *
  • {@code Numismatics.BANK} — public static {@code GlobalBankManager} singleton
  • + *
  • {@code GlobalBankManager.getAccount(Player)} — null if no account yet
  • + *
  • {@code GlobalBankManager.getOrCreateAccount(UUID, BankAccount.Type)} — auto-create with type=PLAYER
  • + *
  • {@code BankAccount.getBalance() / deposit(int) / deduct(int)} — int-typed balance, base spur denomination
  • + *
+ * + *

Lifecycle

+ * 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; + } + } +} diff --git a/src/main/java/uk/sijbers/bnstoolkit/network/BnsNetwork.java b/src/main/java/uk/sijbers/bnstoolkit/network/BnsNetwork.java index 84719fb..68de98d 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/network/BnsNetwork.java +++ b/src/main/java/uk/sijbers/bnstoolkit/network/BnsNetwork.java @@ -32,7 +32,7 @@ import uk.sijbers.bnstoolkit.network.s2c.TierStateUpdatePacket; public final class BnsNetwork { private BnsNetwork() {} - public static final String VERSION = "0.4"; + public static final String VERSION = "0.5"; public static void register(IEventBus modBus) { modBus.addListener(BnsNetwork::onRegisterPayloads); diff --git a/src/main/java/uk/sijbers/bnstoolkit/network/ServerEconomyBridge.java b/src/main/java/uk/sijbers/bnstoolkit/network/ServerEconomyBridge.java index 603c3d9..f599d9b 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/network/ServerEconomyBridge.java +++ b/src/main/java/uk/sijbers/bnstoolkit/network/ServerEconomyBridge.java @@ -11,6 +11,7 @@ import uk.sijbers.bnstoolkit.BnsToolkit; import uk.sijbers.bnstoolkit.data.CivicTier; import uk.sijbers.bnstoolkit.data.Plot; 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.OpenScreenPacket; import uk.sijbers.bnstoolkit.network.s2c.PlotSnapshotPacket; @@ -214,9 +215,10 @@ public final class ServerEconomyBridge { int tier = data.contains("bnsTier") ? data.getInt("bnsTier") : 1; if (tier < 1 || tier > CivicTier.MAX) tier = 1; CivicTier cfg = CivicTier.byId(tier); - long spurs = SpurBalance.totalSpurs(sp); + long cash = SpurBalance.inventoryCoins(sp); + long bank = NumismaticsBridge.balanceOf(sp); 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) { diff --git a/src/main/java/uk/sijbers/bnstoolkit/network/s2c/TierStateUpdatePacket.java b/src/main/java/uk/sijbers/bnstoolkit/network/s2c/TierStateUpdatePacket.java index 60defa4..8805fe4 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/network/s2c/TierStateUpdatePacket.java +++ b/src/main/java/uk/sijbers/bnstoolkit/network/s2c/TierStateUpdatePacket.java @@ -9,15 +9,23 @@ import net.neoforged.neoforge.network.handling.IPayloadContext; import uk.sijbers.bnstoolkit.BnsToolkit; 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 TYPE = new Type<>( ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "tier_state_update")); public static final StreamCodec STREAM_CODEC = StreamCodec.composite( 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, TierStateUpdatePacket::new); @@ -29,7 +37,9 @@ public record TierStateUpdatePacket(int tier, long spurs, int effectiveFeePct) i public static void handle(TierStateUpdatePacket pkt, IPayloadContext ctx) { ctx.enqueueWork(() -> { 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; }); } diff --git a/src/main/java/uk/sijbers/bnstoolkit/server/BnsServerCommands.java b/src/main/java/uk/sijbers/bnstoolkit/server/BnsServerCommands.java index 7ab114f..dd99261 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/server/BnsServerCommands.java +++ b/src/main/java/uk/sijbers/bnstoolkit/server/BnsServerCommands.java @@ -35,7 +35,7 @@ public final class BnsServerCommands { private BnsServerCommands() {} private static final String[] SCREEN_NAMES = { - "hub", "tier", "bounty", "plot", "shop", "questlog" + "hub", "tier", "bounty", "plot", "counthouse", "shop", "questlog" }; @SubscribeEvent diff --git a/src/main/java/uk/sijbers/bnstoolkit/server/CountingHouseCommands.java b/src/main/java/uk/sijbers/bnstoolkit/server/CountingHouseCommands.java new file mode 100644 index 0000000..4446f02 --- /dev/null +++ b/src/main/java/uk/sijbers/bnstoolkit/server/CountingHouseCommands.java @@ -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. + * + *
    + *
  • {@code /bns sell } — 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.
  • + *
  • {@code /bns wallet} — show inventory cash, bank balance, total.
  • + *
  • {@code /bns wallet deposit } — move coins from inventory + * to bank.
  • + *
  • {@code /bns wallet withdraw } — bank to inventory as + * Numismatics coins (uses the largest denomination that fits).
  • + *
+ * + * 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 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; + } +} diff --git a/src/main/resources/assets/bnstoolkit/lang/en_us.json b/src/main/resources/assets/bnstoolkit/lang/en_us.json index 0fff03c..58a636c 100644 --- a/src/main/resources/assets/bnstoolkit/lang/en_us.json +++ b/src/main/resources/assets/bnstoolkit/lang/en_us.json @@ -4,7 +4,7 @@ "key.bnstoolkit.tier": "Open Tier Ladder (direct)", "key.bnstoolkit.bounties": "Open Bounty Board (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)", "screen.bnstoolkit.hub": "Brass and Sigil", @@ -12,5 +12,6 @@ "screen.bnstoolkit.bounty_board": "Bounty Board", "screen.bnstoolkit.quest_log": "Quest Log", "screen.bnstoolkit.plot_purchase": "Plot Office", - "screen.bnstoolkit.shop_browser": "Bazaar" + "screen.bnstoolkit.shop_browser": "Counting House", + "screen.bnstoolkit.counting_house": "Counting House" }