From b4cd66036b14593901b14a1217f32f8b044df863 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 6 Jun 2026 23:38:30 +0000 Subject: [PATCH] econ: tier system foundation (00_tier_system.js) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit KubeJS module that owns the 13-tier civic ladder per economy doc §4a. Authoritative source for TIER_CONFIG (per-tier perks: cost, wilderness chunks, plot slots, daily bounty slots, shop fee discount, chat colour, join broadcast). Player tier stored as int in player.persistentData.bnsTier; default 1 (Peasant), set on first login. Admin commands (op level 2): /bns admin tier get /bns admin tier set <1-13> /bns admin tier list Module-local pattern (KubeJS Rhino runs scripts in strict mode -- no globalThis, no bare-assignment globals). Other economy scripts will duplicate TIER_CONFIG and use player.persistentData.bnsTier directly to read tier rather than trying to import. See feedback-kubejs-rhino- gotchas memory. FTB Ranks will mirror this once the bnstoolkit mod ships -- ranks will carry the FTB Chunks permission nodes. For now the NBT is the single source of truth and admin promotes manually with /bns admin tier set. Verified: server boots clean, KubeJS loads with 0 errors. First-join hook initialises new players to tier 1. Co-Authored-By: Claude Opus 4.7 --- .../server_scripts/economy/00_tier_system.js | 170 ++++++++++++++++++ pack/pack.lock.json | 4 +- 2 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 pack/overrides/kubejs/server_scripts/economy/00_tier_system.js diff --git a/pack/overrides/kubejs/server_scripts/economy/00_tier_system.js b/pack/overrides/kubejs/server_scripts/economy/00_tier_system.js new file mode 100644 index 0000000..cbe3bc2 --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/economy/00_tier_system.js @@ -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 +// /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'); diff --git a/pack/pack.lock.json b/pack/pack.lock.json index a32745c..f50c867 100644 --- a/pack/pack.lock.json +++ b/pack/pack.lock.json @@ -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",