b4cd66036b
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 <player> /bns admin tier set <player> <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 <noreply@anthropic.com>
171 lines
9.6 KiB
JavaScript
171 lines
9.6 KiB
JavaScript
// 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');
|