// 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 // /bns admin tier set // /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');