From 4a9edc15c1bc850cb387bce172ddb6d9849d7ee0 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 8 Jun 2026 17:20:09 +0000 Subject: [PATCH] 0.7.0: combined cash+bank spend + auto-bound card on first login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two polish features that complete the "one number" wealth UX: 1. NumismaticsBridge.spend(player, amount): combined deduction. Pulls inventory coins first (most immediate), then bank for the remainder. Atomic — if total is short, nothing is removed. Used by /bns tier upgrade (KubeJS calls Java.loadClass). Future plot/bounty spend operations should call this too. 2. NumismaticsBridge.ensureBoundCard: on first login, the player gets a white_card pre-bound to their own bank account via the numismatics:card_account_id DataComponent. Means a player visiting a Numismatics Vendor block can pay from their bank without manually crafting + binding a card first. If they die and lose it, the next login re-issues. Bounty pool: dropped 4 Alex's Mobs entries; added 6 vanilla replacements (Breeze, Bogged, Evoker, Vindicator, Piglin Brute, Ravager). All high-tier targets are vanilla 1.21.x mobs now. Co-Authored-By: Claude Opus 4.7 --- gradle.properties | 2 +- .../uk/sijbers/bnstoolkit/BnsToolkit.java | 1 + .../uk/sijbers/bnstoolkit/data/Bounty.java | 12 +- .../bnstoolkit/economy/NumismaticsBridge.java | 115 ++++++++++++++++++ 4 files changed, 124 insertions(+), 6 deletions(-) diff --git a/gradle.properties b/gradle.properties index e3e61d9..64e17dc 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.6.2 +mod_version=0.7.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 427c174..9894ebd 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/BnsToolkit.java +++ b/src/main/java/uk/sijbers/bnstoolkit/BnsToolkit.java @@ -53,6 +53,7 @@ public final class BnsToolkit { // this point bnstoolkit treats the bank as canonical — no cards // required, balance is queryable via the bridge. NumismaticsBridge.ensureAccount(sp); + NumismaticsBridge.ensureBoundCard(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/data/Bounty.java b/src/main/java/uk/sijbers/bnstoolkit/data/Bounty.java index e7bfa13..af343a2 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/data/Bounty.java +++ b/src/main/java/uk/sijbers/bnstoolkit/data/Bounty.java @@ -29,11 +29,13 @@ public record Bounty(String id, String name, String targetEntity, int count, lon new Bounty("b_enderman_5", "Slay 5 Endermen", "minecraft:enderman", 5, 90L, "Adventurer's Guild"), new Bounty("b_wither_skel_3", "Slay 3 Wither Skeletons","minecraft:wither_skeleton", 3, 150L, "Adventurer's Guild"), new Bounty("b_shulker_3", "Slay 3 Shulkers", "minecraft:shulker", 3, 200L, "Adventurer's Guild"), - new Bounty("b_warden_1", "Slay the Warden", "minecraft:warden", 1, 800L, "Adventurer's Guild"), - new Bounty("b_voidworm_1", "Destroy a Void Worm", "alexsmobs:void_worm", 1, 1200L, "Adventurer's Guild"), - new Bounty("b_warped_mosco_1", "Slay a Warped Mosco", "alexsmobs:warped_mosco", 1, 700L, "Adventurer's Guild"), - new Bounty("b_farseer_1", "Slay the Farseer", "alexsmobs:farseer", 1, 900L, "Adventurer's Guild"), - new Bounty("b_skreecher_1", "Slay a Skreecher", "alexsmobs:skreecher", 1, 400L, "Adventurer's Guild") + new Bounty("b_warden_1", "Slay the Warden", "minecraft:warden", 1, 800L, "Adventurer's Guild"), + new Bounty("b_breeze_3", "Hunt 3 Breezes", "minecraft:breeze", 3, 250L, "Adventurer's Guild"), + new Bounty("b_bogged_5", "Slay 5 Bogged", "minecraft:bogged", 5, 120L, "Adventurer's Guild"), + new Bounty("b_evoker_2", "Slay 2 Evokers", "minecraft:evoker", 2, 350L, "Adventurer's Guild"), + new Bounty("b_vindicator_4", "Slay 4 Vindicators", "minecraft:vindicator", 4, 180L, "Adventurer's Guild"), + new Bounty("b_piglin_brute_2", "Slay 2 Piglin Brutes", "minecraft:piglin_brute", 2, 400L, "Adventurer's Guild"), + new Bounty("b_ravager_1", "Hunt down a Ravager", "minecraft:ravager", 1, 500L, "Adventurer's Guild") ); public static Bounty byId(String id) { diff --git a/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java b/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java index 1f38065..36b3f84 100644 --- a/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java +++ b/src/main/java/uk/sijbers/bnstoolkit/economy/NumismaticsBridge.java @@ -1,7 +1,13 @@ package uk.sijbers.bnstoolkit.economy; +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; import uk.sijbers.bnstoolkit.BnsToolkit; import uk.sijbers.bnstoolkit.data.SpurBalance; @@ -176,4 +182,113 @@ public final class NumismaticsBridge { return false; } } + + /** + * Combined cash+bank spend. Pulls inventory coins first (most direct), + * then deducts the remainder from the player's bank. Returns true if + * the full amount was successfully removed, false if the player didn't + * have enough (in which case NOTHING is removed — atomic). + * + * This is the canonical "pay X spurs" operation. /bns tier upgrade, + * /bns plot buy, and any future spend op should call this. + */ + public static boolean spend(Player player, long spurs) { + if (spurs <= 0) return true; + long cash = SpurBalance.inventoryCoins(player); + long bank = balanceOf(player); + if (cash + bank < spurs) return false; + + long fromCash = Math.min(cash, spurs); + long fromBank = spurs - fromCash; + + // Try the bank deduction first — that's the more likely to fail + // ("account locked", "missing capability"). If bank fails, the + // inventory coins haven't been touched yet. + if (fromBank > 0 && !deduct(player, fromBank)) return false; + + // Take from inventory after bank succeeded. + if (fromCash > 0) { + long taken = SpurBalance.takeCoins(player, fromCash); + if (taken < fromCash) { + // Race / impossibility — refund bank if needed and bail. + long refund = fromCash - taken; + BnsToolkit.LOG.warn("[bns/bank] inventory short by {} during spend; refunding bank", refund); + deposit(player, fromBank + refund); + return false; + } + } + return true; + } + + // ─── Card auto-issue ───────────────────────────────────────────── + + private static final ResourceLocation DEFAULT_CARD_ID = + ResourceLocation.parse("numismatics:white_card"); + private static final ResourceLocation CARD_ACCOUNT_COMPONENT_ID = + ResourceLocation.parse("numismatics:card_account_id"); + + /** + * On first login (or if their card was lost), give the player a + * white_card pre-bound to their own bank account. Eliminates the + * "I need a card to spend at Vendor blocks" friction without + * forcing players to learn the card system. + * + * Detection: if the player has any bound card already (CardItem with + * the CARD_ACCOUNT_ID component set to their UUID), no-op. + * Otherwise: hand them a fresh bound card. + * + * Note: cards are normal items; they drop on death like anything + * else. We don't keep-inventory force; if they lose it, this method + * re-issues on next login (which is mild friction but at least + * recoverable). + */ + public static void ensureBoundCard(ServerPlayer sp) { + try { + UUID myUuid = sp.getUUID(); + Inventory inv = sp.getInventory(); + + // Resolve the component type once + DataComponentType componentType = BuiltInRegistries.DATA_COMPONENT_TYPE.get(CARD_ACCOUNT_COMPONENT_ID); + if (componentType == null) { + BnsToolkit.LOG.debug("[bns/bank] CARD_ACCOUNT_ID component not registered — skipping card issue"); + return; + } + + // Scan inventory for an existing bound card + for (int i = 0; i < inv.getContainerSize(); i++) { + ItemStack stack = inv.getItem(i); + if (stack.isEmpty()) continue; + ResourceLocation id = stack.getItemHolder().unwrapKey().map(k -> k.location()).orElse(null); + if (id == null) continue; + // Cards are tagged with "_card" suffix in Numismatics + if (!id.getNamespace().equals("numismatics") || !id.getPath().endsWith("_card")) continue; + if (id.getPath().endsWith("_id_card")) continue; // skip ID cards + Object boundUuid = stack.get(componentType); + if (myUuid.equals(boundUuid)) { + return; // already has a bound card + } + } + + // Issue a new bound white_card + Item cardItem = BuiltInRegistries.ITEM.get(DEFAULT_CARD_ID); + if (cardItem == null) { + BnsToolkit.LOG.debug("[bns/bank] numismatics:white_card not registered — skipping card issue"); + return; + } + ItemStack newCard = new ItemStack(cardItem, 1); + // The set() signature is set(DataComponentType, T) — UUID for this component. + @SuppressWarnings("unchecked") + DataComponentType typed = (DataComponentType) componentType; + newCard.set(typed, myUuid); + + if (!inv.add(newCard)) { + // Inventory full — drop at feet + sp.drop(newCard, false); + } + BnsToolkit.LOG.info("[bns/bank] issued bound card to {}", sp.getName().getString()); + } catch (Throwable t) { + BnsToolkit.LOG.warn("[bns/bank] ensureBoundCard failed for {}: {}", + sp.getName().getString(), t.toString()); + } + } }