econ: sell shop + bounty engine + player commands + FTB Ranks 13-tier #67
@@ -0,0 +1,170 @@
|
||||
// Brass and Sigil -- tier system (foundation)
|
||||
//
|
||||
// 13-tier civic ladder per docs/economy-system.md §4a. This file owns:
|
||||
// - The TIER_CONFIG table (single source of truth for tier perks)
|
||||
// - Helper functions: getTier(player), setTier, getTierConfig
|
||||
// - Admin commands: /bns admin set-tier, get-tier
|
||||
//
|
||||
// Storage: player.persistentData.bnsTier (int, 1-13). Default 1 (Peasant).
|
||||
//
|
||||
// Eventually FTB Ranks will mirror this — when the bnstoolkit mod ships,
|
||||
// rank promotion will set both the NBT tier here AND the FTB Ranks rank
|
||||
// (which carries the FTB-Chunks permission nodes for wilderness caps).
|
||||
// For now this NBT is the authoritative source.
|
||||
//
|
||||
// All other economy scripts read tier perks through this module's
|
||||
// exposed globals.
|
||||
|
||||
// ─── The ladder ─────────────────────────────────────────────────────
|
||||
// Names + costs + perks per tier. Edit here to rebalance.
|
||||
// Sub-fields:
|
||||
// wildernessChunks — FTB Chunks claim cap (also enforced via FTB Ranks
|
||||
// permission node once configured; this is the
|
||||
// fallback / source-of-truth value)
|
||||
// plotSlots — how many spawn plots the player can own at once
|
||||
// dailyBountySlots — how many bounties they can have active at once
|
||||
// shopFeeDiscount — % discount on the 25% base Bazaar fee (negative %)
|
||||
// chatColour — name colour in chat
|
||||
// joinBroadcast — broadcast level on login
|
||||
const TIER_CONFIG = Object.freeze([
|
||||
null, // tiers are 1-indexed, slot 0 reserved
|
||||
{ id: 1, name: 'Peasant', cost: 0, wildernessChunks: 9, plotSlots: 0, dailyBountySlots: 3, shopFeeDiscount: 0, chatColour: 'gray', joinBroadcast: 'none' },
|
||||
{ id: 2, name: 'Farmer', cost: 200, wildernessChunks: 18, plotSlots: 0, dailyBountySlots: 3, shopFeeDiscount: 0, chatColour: 'gray', joinBroadcast: 'none' },
|
||||
{ id: 3, name: 'Citizen', cost: 750, wildernessChunks: 35, plotSlots: 1, dailyBountySlots: 3, shopFeeDiscount: 0, chatColour: 'white', joinBroadcast: 'none' },
|
||||
{ id: 4, name: 'Merchant', cost: 2500, wildernessChunks: 60, plotSlots: 1, dailyBountySlots: 4, shopFeeDiscount: -2, chatColour: 'yellow', joinBroadcast: 'none' },
|
||||
{ id: 5, name: 'Knight', cost: 6500, wildernessChunks: 100, plotSlots: 2, dailyBountySlots: 4, shopFeeDiscount: -5, chatColour: 'green', joinBroadcast: 'simple' },
|
||||
{ id: 6, name: 'Baron', cost: 15000, wildernessChunks: 150, plotSlots: 2, dailyBountySlots: 5, shopFeeDiscount: -7, chatColour: 'aqua', joinBroadcast: 'simple' },
|
||||
{ id: 7, name: 'Viscount', cost: 35000, wildernessChunks: 220, plotSlots: 3, dailyBountySlots: 5, shopFeeDiscount: -9, chatColour: 'blue', joinBroadcast: 'fancy' },
|
||||
{ id: 8, name: 'Earl', cost: 75000, wildernessChunks: 300, plotSlots: 3, dailyBountySlots: 6, shopFeeDiscount: -11, chatColour: 'dark_purple', joinBroadcast: 'fancy' },
|
||||
{ id: 9, name: 'Marquess', cost: 150000, wildernessChunks: 400, plotSlots: 4, dailyBountySlots: 6, shopFeeDiscount: -13, chatColour: 'gold', joinBroadcast: 'fancy' },
|
||||
{ id: 10, name: 'Duke', cost: 300000, wildernessChunks: 525, plotSlots: 5, dailyBountySlots: 7, shopFeeDiscount: -15, chatColour: 'gold', joinBroadcast: 'epic' },
|
||||
{ id: 11, name: 'Archduke', cost: 700000, wildernessChunks: 700, plotSlots: 6, dailyBountySlots: 7, shopFeeDiscount: -17, chatColour: 'red', joinBroadcast: 'epic' },
|
||||
{ id: 12, name: 'Grand Duke', cost: 1500000, wildernessChunks: 900, plotSlots: 7, dailyBountySlots: 8, shopFeeDiscount: -19, chatColour: 'red', joinBroadcast: 'epic_particles' },
|
||||
{ id: 13, name: 'Sovereign', cost: 10000000, wildernessChunks: 1500, plotSlots: 10, dailyBountySlots: 10, shopFeeDiscount: -25, chatColour: 'light_purple', joinBroadcast: 'epic_spectacular' },
|
||||
]);
|
||||
const MAX_TIER = 13;
|
||||
|
||||
// Bazaar's base sell fee, before tier discount. A Peasant pays 25%; a
|
||||
// Sovereign with -25% discount pays 0%. Read by the sell-shop module.
|
||||
const BAZAAR_BASE_FEE_PCT = 25;
|
||||
|
||||
// ─── Tier accessors ─────────────────────────────────────────────────
|
||||
// Module-local helpers. Each consumer file declares its own TIER_CONFIG
|
||||
// copy because KubeJS Rhino strict-mode doesn't allow cross-script globals.
|
||||
// See feedback-kubejs-rhino-gotchas memory note.
|
||||
const bnsTier = {
|
||||
// get the player's current tier (1-13). Defaults to 1 if unset.
|
||||
get: function(player) {
|
||||
const data = player.persistentData;
|
||||
if (!data.contains('bnsTier')) return 1;
|
||||
const t = data.getInt('bnsTier');
|
||||
return (t >= 1 && t <= MAX_TIER) ? t : 1;
|
||||
},
|
||||
|
||||
// set the player's tier (1-13). Clamps to range.
|
||||
set: function(player, tier) {
|
||||
const clamped = Math.max(1, Math.min(MAX_TIER, tier|0));
|
||||
player.persistentData.putInt('bnsTier', clamped);
|
||||
return clamped;
|
||||
},
|
||||
|
||||
// get the full config row for a tier (1-13).
|
||||
config: function(tier) {
|
||||
if (tier < 1 || tier > MAX_TIER) return null;
|
||||
return TIER_CONFIG[tier];
|
||||
},
|
||||
|
||||
// get config for the player's current tier
|
||||
playerConfig: function(player) {
|
||||
return TIER_CONFIG[this.get(player)];
|
||||
},
|
||||
|
||||
// expose the table read-only
|
||||
table: function() { return TIER_CONFIG; },
|
||||
|
||||
// bazaar base fee accessor for the sell-shop module
|
||||
bazaarBaseFee: function() { return BAZAAR_BASE_FEE_PCT; },
|
||||
|
||||
// effective shop-fee percentage for the player (base + discount).
|
||||
// Returns 0..25, never negative.
|
||||
effectiveShopFeePct: function(player) {
|
||||
const cfg = this.playerConfig(player);
|
||||
return Math.max(0, BAZAAR_BASE_FEE_PCT + cfg.shopFeeDiscount);
|
||||
},
|
||||
|
||||
MAX: MAX_TIER,
|
||||
};
|
||||
|
||||
// ─── Admin commands ──────────────────────────────────────────────────
|
||||
// /bns admin tier get <player>
|
||||
// /bns admin tier set <player> <tier>
|
||||
// /bns admin tier list (show the full table)
|
||||
ServerEvents.commandRegistry(event => {
|
||||
const { commands: Commands, arguments: Arguments } = event;
|
||||
|
||||
const tierCmd = Commands.literal('bns')
|
||||
.then(Commands.literal('admin')
|
||||
.requires(src => src.hasPermission(2))
|
||||
.then(Commands.literal('tier')
|
||||
|
||||
.then(Commands.literal('get')
|
||||
.then(Commands.argument('target', Arguments.PLAYER.create(event))
|
||||
.executes(ctx => {
|
||||
const t = Arguments.PLAYER.getResult(ctx, 'target');
|
||||
const tier = bnsTier.get(t);
|
||||
const cfg = bnsTier.config(tier);
|
||||
ctx.source.sendSystemMessage(Text.aqua(
|
||||
`${t.username}: tier ${tier} (${cfg.name})`
|
||||
));
|
||||
return 1;
|
||||
})))
|
||||
|
||||
.then(Commands.literal('set')
|
||||
.then(Commands.argument('target', Arguments.PLAYER.create(event))
|
||||
.then(Commands.argument('tier', Arguments.INTEGER.create(event))
|
||||
.executes(ctx => {
|
||||
const t = Arguments.PLAYER.getResult(ctx, 'target');
|
||||
const requested = Arguments.INTEGER.getResult(ctx, 'tier');
|
||||
if (requested < 1 || requested > MAX_TIER) {
|
||||
ctx.source.sendSystemMessage(Text.red(
|
||||
`Tier must be 1..${MAX_TIER} (got ${requested})`
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
const actual = bnsTier.set(t, requested);
|
||||
const cfg = bnsTier.config(actual);
|
||||
ctx.source.sendSystemMessage(Text.gold(
|
||||
`Set ${t.username} -> tier ${actual} (${cfg.name})`
|
||||
));
|
||||
t.tell(Text.gold(`You have been promoted to ${cfg.name}.`));
|
||||
console.info(`[bns/tier] admin set ${t.username} tier=${actual} (${cfg.name})`);
|
||||
return 1;
|
||||
}))))
|
||||
|
||||
.then(Commands.literal('list').executes(ctx => {
|
||||
ctx.source.sendSystemMessage(Text.aqua('--- Tier ladder ---'));
|
||||
for (let i = 1; i <= MAX_TIER; i++) {
|
||||
const c = TIER_CONFIG[i];
|
||||
ctx.source.sendSystemMessage(Text.gray(
|
||||
` ${i}. ${c.name} cost ${c.cost} chunks ${c.wildernessChunks} plots ${c.plotSlots} bounties ${c.dailyBountySlots} fee ${c.shopFeeDiscount}% ${c.chatColour}`
|
||||
));
|
||||
}
|
||||
return 1;
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
event.register(tierCmd);
|
||||
});
|
||||
|
||||
// ─── First-join: ensure tier defaults to 1 ──────────────────────────
|
||||
// Doesn't conflict with welcome.js (different flag).
|
||||
PlayerEvents.loggedIn(event => {
|
||||
const p = event.player;
|
||||
if (!p.persistentData.contains('bnsTier')) {
|
||||
bnsTier.set(p, 1);
|
||||
console.info(`[bns/tier] initialised ${p.username} to tier 1 (Peasant)`);
|
||||
}
|
||||
});
|
||||
|
||||
console.info('[bns/tier_system] loaded — 13 tiers configured');
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing",
|
||||
"name": "Brass and Sigil",
|
||||
"version": "0.32.0",
|
||||
"version": "0.33.0",
|
||||
"minecraft": "1.21.1",
|
||||
"loader": {
|
||||
"type": "neoforge",
|
||||
"version": "21.1.228"
|
||||
},
|
||||
"lockedAt": "2026-06-06T23:21:32Z",
|
||||
"lockedAt": "2026-06-06T23:34:21Z",
|
||||
"mods": [
|
||||
{
|
||||
"source": "modrinth",
|
||||
|
||||
Reference in New Issue
Block a user