0.7.0: combined cash+bank spend + auto-bound card on first login
Build / build (push) Has been cancelled
Build / build (push) Has been cancelled
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 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -30,10 +30,12 @@ public record Bounty(String id, String name, String targetEntity, int count, lon
|
||||
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_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) {
|
||||
|
||||
@@ -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>, T) — UUID for this component.
|
||||
@SuppressWarnings("unchecked")
|
||||
DataComponentType<UUID> typed = (DataComponentType<UUID>) 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user