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/overrides/kubejs/server_scripts/economy/02_sell_shop.js b/pack/overrides/kubejs/server_scripts/economy/02_sell_shop.js new file mode 100644 index 0000000..c63924f --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/economy/02_sell_shop.js @@ -0,0 +1,197 @@ +// Brass and Sigil -- /bns sell shop (Phase 1 of the economy) +// +// Players sell raw commodities to "the Bazaar" for spurs. Implementation: +// - Command-driven for now: /bns sell [count] +// - When the bnstoolkit mod ships, a Numismatics shop block in the +// Merchant's Bazaar will call the same backend. +// +// Mechanics per docs/economy-system.md §11.4: +// - Base price per item is configured below in SELL_PRICES +// - Bazaar charges a base 25% fee, reduced by the player's tier +// discount (read from player.persistentData.bnsTier). +// - Diminishing returns: each player has a per-item lifetime-sales +// counter. Price reduces 10% per 64 sold, floored at 50% of base. +// +// Storage: +// player.persistentData.bnsSales -- compound map: item_id -> int count + +// ─── Sell catalogue ───────────────────────────────────────────────── +// Add/remove items here. Prices are pre-fee, pre-DR — "list price." +const SELL_PRICES = { + 'minecraft:diamond': 10, + 'minecraft:netherite_ingot': 100, + 'minecraft:emerald': 1, + 'minecraft:gold_ingot': 2, + 'minecraft:iron_ingot': 1, // small unit value; sell in stacks + 'minecraft:lapis_lazuli': 1, + 'minecraft:redstone': 1, + 'minecraft:coal': 1, // 4 coal = 1 spur after DR + fee + 'minecraft:ancient_debris': 500, // raw, rare +}; + +// Constants like BAZAAR_BASE_FEE_PCT live in 00_tier_system.js. +// Coin handling (COIN_VALUES, DENOMS_DESC, giveCoins) lives in plots.js. +// All top-level `const`/`function` declarations are in the shared +// Rhino global scope, so we just call them directly here. + +const DR_STEP = 64; // every N sold => price drops a step +const DR_DECAY = 0.10; // 10% drop per step +const DR_FLOOR = 0.50; // never below 50% of base + +// ─── Sales tracker ───────────────────────────────────────────────── +function getSalesMap(player) { + const data = player.persistentData; + if (!data.contains('bnsSales')) { + data.put('bnsSales', {}); + } + return data.getCompound('bnsSales'); +} + +function lifetimeSold(player, itemId) { + const map = getSalesMap(player); + return map.contains(itemId) ? map.getInt(itemId) : 0; +} + +function recordSale(player, itemId, count) { + const map = getSalesMap(player); + const prev = map.contains(itemId) ? map.getInt(itemId) : 0; + map.putInt(itemId, prev + count); +} + +// ─── Pricing ─────────────────────────────────────────────────────── +// Effective per-unit payout for the player after DR + fee. +// Worked example for a Peasant selling their first diamond: +// base = 10 +// DR multiplier = max(0.50, 1 - 0 * 0.10) = 1.0 +// pre-fee per unit = 10 +// tier discount = 0, effective fee = 25% +// payout = floor(10 * 0.75) = 7 +function effectivePayout(player, itemId, count) { + const base = SELL_PRICES[itemId]; + if (!base) return 0; + + const sold = lifetimeSold(player, itemId); + // average DR multiplier across the units being sold this transaction. + // Approximated by midpoint of the range. + const midSteps = Math.floor((sold + count / 2) / DR_STEP); + const drMult = Math.max(DR_FLOOR, 1 - midSteps * DR_DECAY); + + const feePct = bnsTier.effectiveShopFeePct(player); + const grossPerUnit = base * drMult; + const netPerUnit = grossPerUnit * (1 - feePct / 100); + + return Math.floor(netPerUnit * count); +} + +// Display-only single-unit price (for /bns sell list) +function singleUnitPriceDisplay(player, itemId) { + const base = SELL_PRICES[itemId]; + if (!base) return '-'; + const sold = lifetimeSold(player, itemId); + const steps = Math.floor(sold / DR_STEP); + const drMult = Math.max(DR_FLOOR, 1 - steps * DR_DECAY); + const feePct = bnsTier.effectiveShopFeePct(player); + const net = base * drMult * (1 - feePct / 100); + return Math.floor(net).toString(); +} + +// ─── Inventory helpers ───────────────────────────────────────────── +function takeFromInventory(player, itemId, count) { + let remaining = count; + const inv = player.inventory; + for (let i = 0; i < inv.containerSize && remaining > 0; i++) { + const stack = inv.getItem(i); + if (stack.id !== itemId) continue; + const take = Math.min(stack.count, remaining); + stack.shrink(take); + remaining -= take; + } + return count - remaining; // how many were actually taken +} + +function countInInventory(player, itemId) { + let total = 0; + const inv = player.inventory; + for (let i = 0; i < inv.containerSize; i++) { + const stack = inv.getItem(i); + if (stack.id === itemId) total += stack.count; + } + return total; +} + +// ─── Commands ────────────────────────────────────────────────────── +ServerEvents.commandRegistry(event => { + const { commands: Commands, arguments: Arguments } = event; + + const sellCmd = Commands.literal('bns') + .then(Commands.literal('sell') + + // /bns sell list + .then(Commands.literal('list').executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const feeNow = bnsTier.effectiveShopFeePct(player); + ctx.source.sendSystemMessage(Text.aqua(`--- Bazaar sell prices (your effective fee: ${feeNow}%) ---`)); + for (const itemId of Object.keys(SELL_PRICES)) { + const base = SELL_PRICES[itemId]; + const now = singleUnitPriceDisplay(player, itemId); + const sold = lifetimeSold(player, itemId); + ctx.source.sendSystemMessage(Text.gray( + ` ${itemId} base ${base} -> you get ${now} (you've sold ${sold})` + )); + } + ctx.source.sendSystemMessage(Text.gray('Sell: /bns sell [count] (omit count to sell 1)')); + return 1; + })) + + // /bns sell [count] + .then(Commands.argument('item', Arguments.STRING.create(event)) + .executes(ctx => { + return doSell(ctx, Arguments.STRING.getResult(ctx, 'item'), 1); + }) + .then(Commands.argument('count', Arguments.INTEGER.create(event)) + .executes(ctx => { + const item = Arguments.STRING.getResult(ctx, 'item'); + const count = Arguments.INTEGER.getResult(ctx, 'count'); + return doSell(ctx, item, count); + }))) + ); + + event.register(sellCmd); +}); + +function doSell(ctx, itemId, count) { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + if (!SELL_PRICES[itemId]) { + player.tell(Text.red(`The Bazaar doesn't buy '${itemId}'. Try /bns sell list.`)); + return 0; + } + if (count < 1) { + player.tell(Text.red('Count must be at least 1.')); + return 0; + } + const have = countInInventory(player, itemId); + if (have < count) { + player.tell(Text.red(`You only have ${have}x ${itemId} (need ${count}).`)); + return 0; + } + const payout = effectivePayout(player, itemId, count); + if (payout <= 0) { + player.tell(Text.red(`Payout would be 0 spurs (DR + fee). Wait or upgrade your tier.`)); + return 0; + } + const taken = takeFromInventory(player, itemId, count); + if (taken !== count) { + // Defensive: shouldn't happen — countInInventory said we had enough + player.tell(Text.red(`Failed to remove ${count}x ${itemId} (took ${taken}). Aborting.`)); + return 0; + } + recordSale(player, itemId, count); + giveCoins(player, payout); // shared from plots.js + player.tell(Text.gold(`Sold ${count}x ${itemId} for ${payout} spurs.`)); + console.info(`[bns/sell] ${player.username} sold ${count}x ${itemId} for ${payout} spurs (lifetime ${lifetimeSold(player, itemId)})`); + return 1; +} + +console.info('[bns/sell_shop] loaded — 9 items in catalogue, DR + tier-fee active'); diff --git a/pack/overrides/kubejs/server_scripts/economy/03_bounty_engine.js b/pack/overrides/kubejs/server_scripts/economy/03_bounty_engine.js new file mode 100644 index 0000000..e29fb43 --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/economy/03_bounty_engine.js @@ -0,0 +1,347 @@ +// Brass and Sigil -- bounty engine V1 (command-driven) +// +// Per docs/economy-system.md §4-5: +// Each player has a per-day pool of available bounties drawn randomly +// from BOUNTY_POOL. They can accept up to TIER.dailyBountySlots active +// at once. Completing the objective marks the bounty READY -- they +// then turn it in to claim the spur reward. Cancellation applies the +// same cooldown as completion (no RNG shopping). +// +// V1 turn-in is a command. When the bnstoolkit mod ships, the Adventurer's +// Guild NPC will be the turn-in surface and this command becomes a +// fallback/debug path. +// +// Player commands: +// /bns bounty list -- today's available bounties +// /bns bounty active -- your accepted bounties +// /bns bounty accept +// /bns bounty cancel +// /bns bounty turnin +// /bns bounty completed -- your completed bounties + cooldowns +// +// Bounty state per player (in player.persistentData): +// bnsBountyPool compound { id -> 1 } -- today's available 5 +// bnsBountyPoolRefresh long -- epoch ms of last roll +// bnsBountyActive compound { id -> { progress, target, reward, source, ready? } } +// bnsBountyCompleted compound { id -> cooldown_until_ms } + +const BOUNTY_POOL_SIZE = 5; +const BOUNTY_REFRESH_MS = 24 * 60 * 60 * 1000; // 24h +const BOUNTY_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24h cooldown after complete or cancel + +// ─── Bounty catalogue ─────────────────────────────────────────────── +// Each bounty: { id, name, target, count, type, reward, source } +// target = entity id ('minecraft:zombie') OR resource for non-kill bounties +// type = 'kill' (V1 only supports kill bounties) +// reward = spurs paid on turn-in +// source = guild name (for flavour in messages) +const BOUNTY_POOL = [ + // ── Easy: novice-tier mobs ── + { id: 'b_zombie_10', name: 'Slay 10 Zombies', target: 'minecraft:zombie', count: 10, type: 'kill', reward: 15, source: "Adventurer's Guild" }, + { id: 'b_skeleton_10', name: 'Slay 10 Skeletons', target: 'minecraft:skeleton', count: 10, type: 'kill', reward: 15, source: "Adventurer's Guild" }, + { id: 'b_spider_8', name: 'Slay 8 Spiders', target: 'minecraft:spider', count: 8, type: 'kill', reward: 12, source: "Adventurer's Guild" }, + { id: 'b_creeper_5', name: 'Slay 5 Creepers', target: 'minecraft:creeper', count: 5, type: 'kill', reward: 20, source: "Adventurer's Guild" }, + { id: 'b_drowned_8', name: 'Slay 8 Drowned', target: 'minecraft:drowned', count: 8, type: 'kill', reward: 18, source: "Adventurer's Guild" }, + { id: 'b_husk_8', name: 'Slay 8 Husks', target: 'minecraft:husk', count: 8, type: 'kill', reward: 18, source: "Adventurer's Guild" }, + + // ── Medium: nether basic ── + { id: 'b_blaze_5', name: 'Slay 5 Blazes', target: 'minecraft:blaze', count: 5, type: 'kill', reward: 50, source: "Adventurer's Guild" }, + { id: 'b_piglin_8', name: 'Slay 8 Piglins', target: 'minecraft:piglin', count: 8, type: 'kill', reward: 40, source: "Adventurer's Guild" }, + { id: 'b_magma_6', name: 'Slay 6 Magma Cubes', target: 'minecraft:magma_cube', count: 6, type: 'kill', reward: 30, source: "Adventurer's Guild" }, + { id: 'b_ghast_3', name: 'Slay 3 Ghasts', target: 'minecraft:ghast', count: 3, type: 'kill', reward: 60, source: "Adventurer's Guild" }, + + // ── Medium: modded ─ + { id: 'b_plunderer_5', name: 'Hunt 5 Plunderers', target: 'supplementaries:plunderer', count: 5, type: 'kill', reward: 75, source: "Adventurer's Guild" }, + + // ── Hard: nether elites + end ── + { id: 'b_witch_3', name: 'Slay 3 Witches', target: 'minecraft:witch', count: 3, type: 'kill', reward: 80, source: "Adventurer's Guild" }, + { id: 'b_enderman_5', name: 'Slay 5 Endermen', target: 'minecraft:enderman', count: 5, type: 'kill', reward: 90, source: "Adventurer's Guild" }, + { id: 'b_wither_skel_3', name: 'Slay 3 Wither Skeletons', target: 'minecraft:wither_skeleton', count: 3, type: 'kill', reward: 150, source: "Adventurer's Guild" }, + { id: 'b_shulker_3', name: 'Slay 3 Shulkers', target: 'minecraft:shulker', count: 3, type: 'kill', reward: 200, source: "Adventurer's Guild" }, + + // ── Boss tier (rare picks) ── + { id: 'b_warden_1', name: 'Slay the Warden', target: 'minecraft:warden', count: 1, type: 'kill', reward: 800, source: "Adventurer's Guild" }, + { id: 'b_voidworm_1', name: 'Destroy a Void Worm', target: 'alexsmobs:void_worm', count: 1, type: 'kill', reward: 1200, source: "Adventurer's Guild" }, + { id: 'b_warped_mosco_1', name: 'Slay a Warped Mosco', target: 'alexsmobs:warped_mosco', count: 1, type: 'kill', reward: 700, source: "Adventurer's Guild" }, + { id: 'b_farseer_1', name: 'Slay the Farseer', target: 'alexsmobs:farseer', count: 1, type: 'kill', reward: 900, source: "Adventurer's Guild" }, + { id: 'b_skreecher_1', name: 'Slay a Skreecher', target: 'alexsmobs:skreecher', count: 1, type: 'kill', reward: 400, source: "Adventurer's Guild" }, +]; + +// Index pool by id for O(1) lookup +const BOUNTY_INDEX = {}; +for (const b of BOUNTY_POOL) BOUNTY_INDEX[b.id] = b; + +// ─── Player tier → slot cap ───────────────────────────────────────── +const TIER_BOUNTY_SLOTS = [0, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10]; + +function playerBountySlotCap(player) { + const data = player.persistentData; + const tier = data.contains('bnsTier') ? data.getInt('bnsTier') : 1; + const safe = (tier >= 1 && tier <= 13) ? tier : 1; + return TIER_BOUNTY_SLOTS[safe]; +} + +// ─── Compound storage helpers ─────────────────────────────────────── +function getActiveMap(player) { + const d = player.persistentData; + if (!d.contains('bnsBountyActive')) d.put('bnsBountyActive', {}); + return d.getCompound('bnsBountyActive'); +} +function getCompletedMap(player) { + const d = player.persistentData; + if (!d.contains('bnsBountyCompleted')) d.put('bnsBountyCompleted', {}); + return d.getCompound('bnsBountyCompleted'); +} +function getPoolList(player) { + const d = player.persistentData; + if (!d.contains('bnsBountyPool')) d.put('bnsBountyPool', {}); + return d.getCompound('bnsBountyPool'); +} + +// ─── Daily pool refresh ───────────────────────────────────────────── +function nowMs() { return Date.now(); } + +function rollDailyPool(player) { + const eligible = BOUNTY_POOL.slice(); // shallow copy + // Filter out bounties still on cooldown + const completed = getCompletedMap(player); + const now = nowMs(); + const available = eligible.filter(b => { + if (!completed.contains(b.id)) return true; + return completed.getLong(b.id) <= now; + }); + // Shuffle and pick top BOUNTY_POOL_SIZE + for (let i = available.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const t = available[i]; available[i] = available[j]; available[j] = t; + } + const picked = available.slice(0, BOUNTY_POOL_SIZE); + const pool = getPoolList(player); + // Clear old pool entries + for (const k of pool.getAllKeys()) pool.remove(k); + for (const b of picked) pool.putInt(b.id, 1); + player.persistentData.putLong('bnsBountyPoolRefresh', now); + console.info(`[bns/bounty] rolled pool for ${player.username}: ${picked.map(b=>b.id).join(', ')}`); +} + +function ensurePoolFresh(player) { + const d = player.persistentData; + const lastRoll = d.contains('bnsBountyPoolRefresh') ? d.getLong('bnsBountyPoolRefresh') : 0; + if (nowMs() - lastRoll > BOUNTY_REFRESH_MS) { + rollDailyPool(player); + } +} + +// ─── Active-bounty management ─────────────────────────────────────── +function activeBountyCount(player) { + return getActiveMap(player).getAllKeys().size(); +} + +function addActiveBounty(player, bountyDef) { + const map = getActiveMap(player); + const entry = {}; + entry['progress'] = 0; + entry['target'] = bountyDef.count; + entry['reward'] = bountyDef.reward; + entry['source'] = bountyDef.source; + entry['name'] = bountyDef.name; + entry['ready'] = false; + entry['targetId'] = bountyDef.target; + entry['type'] = bountyDef.type; + map.put(bountyDef.id, entry); +} + +function removeActiveBounty(player, bountyId) { + getActiveMap(player).remove(bountyId); +} + +function markCooldown(player, bountyId) { + getCompletedMap(player).putLong(bountyId, nowMs() + BOUNTY_COOLDOWN_MS); +} + +// ─── Progress hook: increment matching active bounties on kill ────── +EntityEvents.death(event => { + const src = event.source.player; + if (!src) return; + const player = src; + const killedId = event.entity.type; + + const active = getActiveMap(player); + let updated = false; + for (const id of active.getAllKeys()) { + const entry = active.getCompound(id); + if (entry.getString('type') !== 'kill') continue; + if (entry.getString('targetId') !== killedId) continue; + if (entry.getBoolean('ready')) continue; + const newProgress = entry.getInt('progress') + 1; + entry.putInt('progress', newProgress); + if (newProgress >= entry.getInt('target')) { + entry.putBoolean('ready', true); + player.tell(Text.gold(`Bounty READY: ${entry.getString('name')}. Turn in with /bns bounty turnin ${id}`)); + } else { + // Light progress notification every few kills to not spam + if (newProgress === 1 || newProgress === entry.getInt('target') - 1 || newProgress % 5 === 0) { + player.tell(Text.gray(`Bounty progress: ${entry.getString('name')} — ${newProgress}/${entry.getInt('target')}`)); + } + } + updated = true; + } +}); + +// ─── Commands ────────────────────────────────────────────────────── +ServerEvents.commandRegistry(event => { + const { commands: Commands, arguments: Arguments } = event; + + const bountyCmd = Commands.literal('bns') + .then(Commands.literal('bounty') + + // /bns bounty list + .then(Commands.literal('list').executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + ensurePoolFresh(player); + const pool = getPoolList(player); + const cap = playerBountySlotCap(player); + const active = activeBountyCount(player); + player.tell(Text.aqua(`--- Today's bounties (active: ${active}/${cap}) ---`)); + if (pool.getAllKeys().size() === 0) { + player.tell(Text.gray(' (No bounties currently available — check back in 24h)')); + return 1; + } + for (const id of pool.getAllKeys()) { + const b = BOUNTY_INDEX[id]; + if (!b) continue; + const accepted = getActiveMap(player).contains(id); + const tag = accepted ? '§a[ACCEPTED]§7' : '§7'; + player.tell(Text.gray(` ${tag} ${id} "${b.name}" reward: ${b.reward} spurs`)); + } + player.tell(Text.gray('Accept: /bns bounty accept ')); + return 1; + })) + + // /bns bounty active + .then(Commands.literal('active').executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const active = getActiveMap(player); + const cap = playerBountySlotCap(player); + player.tell(Text.aqua(`--- Active bounties (${active.getAllKeys().size()}/${cap}) ---`)); + if (active.getAllKeys().size() === 0) { + player.tell(Text.gray(' (No active bounties — accept some with /bns bounty list)')); + return 1; + } + for (const id of active.getAllKeys()) { + const e = active.getCompound(id); + const flag = e.getBoolean('ready') ? '§e[READY]§7' : '§7'; + player.tell(Text.gray( + ` ${flag} ${id} "${e.getString('name')}" ${e.getInt('progress')}/${e.getInt('target')} reward: ${e.getInt('reward')} spurs` + )); + } + player.tell(Text.gray('Turn in: /bns bounty turnin Cancel: /bns bounty cancel ')); + return 1; + })) + + // /bns bounty accept + .then(Commands.literal('accept').then(Commands.argument('id', Arguments.STRING.create(event)) + .executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const id = Arguments.STRING.getResult(ctx, 'id'); + ensurePoolFresh(player); + const pool = getPoolList(player); + if (!pool.contains(id)) { + player.tell(Text.red(`Bounty ${id} isn't in your pool today. Use /bns bounty list.`)); + return 0; + } + if (getActiveMap(player).contains(id)) { + player.tell(Text.red(`You've already accepted ${id}.`)); + return 0; + } + const cap = playerBountySlotCap(player); + if (activeBountyCount(player) >= cap) { + player.tell(Text.red(`You're at your bounty cap (${cap}). Turn in or cancel one first.`)); + return 0; + } + const b = BOUNTY_INDEX[id]; + addActiveBounty(player, b); + player.tell(Text.gold(`Accepted: ${b.name}. Reward: ${b.reward} spurs on turn-in.`)); + console.info(`[bns/bounty] ${player.username} accepted ${id}`); + return 1; + }))) + + // /bns bounty cancel + .then(Commands.literal('cancel').then(Commands.argument('id', Arguments.STRING.create(event)) + .executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const id = Arguments.STRING.getResult(ctx, 'id'); + const active = getActiveMap(player); + if (!active.contains(id)) { + player.tell(Text.red(`You haven't accepted ${id}.`)); + return 0; + } + removeActiveBounty(player, id); + markCooldown(player, id); + player.tell(Text.gold(`Cancelled ${id}. Cooldown applied (24h) — quest will return to your pool tomorrow.`)); + console.info(`[bns/bounty] ${player.username} cancelled ${id}`); + return 1; + }))) + + // /bns bounty turnin + .then(Commands.literal('turnin').then(Commands.argument('id', Arguments.STRING.create(event)) + .executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const id = Arguments.STRING.getResult(ctx, 'id'); + const active = getActiveMap(player); + if (!active.contains(id)) { + player.tell(Text.red(`You haven't accepted ${id}.`)); + return 0; + } + const entry = active.getCompound(id); + if (!entry.getBoolean('ready')) { + player.tell(Text.red( + `${entry.getString('name')} isn't complete yet (${entry.getInt('progress')}/${entry.getInt('target')}).` + )); + return 0; + } + const reward = entry.getInt('reward'); + removeActiveBounty(player, id); + markCooldown(player, id); + giveCoins(player, reward); // shared helper from plots.js + player.tell(Text.gold(`Turn-in: ${entry.getString('name')} — paid ${reward} spurs.`)); + console.info(`[bns/bounty] ${player.username} turned in ${id} for ${reward} spurs`); + return 1; + }))) + + // /bns bounty completed + .then(Commands.literal('completed').executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const completed = getCompletedMap(player); + const now = nowMs(); + player.tell(Text.aqua(`--- Completed / cooldown bounties ---`)); + if (completed.getAllKeys().size() === 0) { + player.tell(Text.gray(' (None yet)')); + return 1; + } + for (const id of completed.getAllKeys()) { + const until = completed.getLong(id); + const b = BOUNTY_INDEX[id]; + const name = b ? b.name : id; + if (until <= now) { + player.tell(Text.gray(` ${id} "${name}" cooldown expired`)); + } else { + const hrs = Math.floor((until - now) / 3600000); + const min = Math.floor(((until - now) % 3600000) / 60000); + player.tell(Text.gray(` ${id} "${name}" cooldown ${hrs}h ${min}m`)); + } + } + return 1; + })) + ); + + event.register(bountyCmd); +}); + +console.info('[bns/bounty_engine] loaded — ' + BOUNTY_POOL.length + ' bounties in catalogue'); diff --git a/pack/overrides/kubejs/server_scripts/economy/04_player_commands.js b/pack/overrides/kubejs/server_scripts/economy/04_player_commands.js new file mode 100644 index 0000000..1d5afd8 --- /dev/null +++ b/pack/overrides/kubejs/server_scripts/economy/04_player_commands.js @@ -0,0 +1,146 @@ +// Brass and Sigil -- player-facing economy commands +// +// /bns help overview of every /bns subcommand +// /bns me quick economic status (tier, balance, plots, bounties) +// /bns tier upgrade pay spurs to advance to the next tier +// +// Player-facing tier upgrade pays spurs, increments the NBT tier, and +// runs `ftbranks add ` so FTB Ranks applies the rank's +// permission nodes (ftbchunks.max_claimed_chunks etc.). + +// FTB Ranks rank ID per tier number (must match +// pack/overrides/world/serverconfig/ftbranks/ranks.snbt) +const TIER_RANK_ID = [ + null, // 0 unused + 'peasant', + 'farmer', + 'citizen', + 'merchant', + 'knight', + 'baron', + 'viscount', + 'earl', + 'marquess', + 'duke', + 'archduke', + 'grand_duke', + 'sovereign', +]; + +ServerEvents.commandRegistry(event => { + const { commands: Commands } = event; + + const playerCmds = Commands.literal('bns') + + // /bns help + .then(Commands.literal('help').executes(ctx => { + const lines = [ + '§6═══ Brass and Sigil — economy commands ═══', + '§eGeneral:', + '§7 /bns help this list', + '§7 /bns me your economy status', + '§eCivic tier:', + '§7 /bns tier upgrade pay spurs to advance to next tier', + '§eSpawn plots:', + '§7 /bns plot list browse available plots', + '§7 /bns plot info details for one plot', + '§7 /bns plot buy purchase a plot (uses a tier slot)', + '§7 /bns plot release release a plot (50% refund)', + '§7 /bns plot mine list plots you own', + '§eBazaar (sell raw commodities):', + '§7 /bns sell list prices for items you can sell', + '§7 /bns sell [count] sell items from your inventory', + '§eBounties:', + '§7 /bns bounty list today\'s available bounties', + '§7 /bns bounty active your accepted bounties', + '§7 /bns bounty accept accept a bounty', + '§7 /bns bounty cancel drop a bounty (cooldown applies)', + '§7 /bns bounty turnin claim reward for a completed bounty', + '§7 /bns bounty completed bounties on cooldown', + '§eAdmin (op level 2):', + '§7 /bns admin info your raw NBT state', + '§7 /bns admin tier get|set|list', + '§7 /bns admin waystone-count|waystone-set|reset-welcome', + ]; + for (const l of lines) ctx.source.sendSystemMessage(Text.of(l)); + return 1; + })) + + // /bns me — quick status + .then(Commands.literal('me').executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const tier = bnsTier.get(player); + const cfg = bnsTier.config(tier); + const balance = countCoins(player); // shared helper from plots.js + const ownedPlots = plotsOwnedBy(player.uuid.toString()); + const plotCap = playerPlotSlotCap(player); + const activeBounties = getActiveMap(player); + const bountyCap = playerBountySlotCap(player); + const feePct = bnsTier.effectiveShopFeePct(player); + + const lines = [ + '§6═══ ' + player.username + ' — economy status ═══', + '§eCivic tier: §f' + tier + '. ' + cfg.name, + '§eSpur balance: §f' + balance, + '§ePlots owned: §f' + ownedPlots.length + ' / ' + plotCap, + '§eActive bounties: §f' + activeBounties.getAllKeys().size() + ' / ' + bountyCap, + '§eBazaar fee: §f' + feePct + '%', + '§eWilderness cap: §f' + cfg.wildernessChunks + ' chunks', + ]; + if (tier < 13) { + const next = bnsTier.config(tier + 1); + lines.push('§eNext tier: §f' + next.name + ' (' + next.cost + ' spurs to upgrade)'); + } else { + lines.push('§eNext tier: §f— you are Sovereign, the peak of the realm.'); + } + for (const l of lines) player.tell(Text.of(l)); + return 1; + })) + + // /bns tier upgrade + .then(Commands.literal('tier').then(Commands.literal('upgrade').executes(ctx => { + const player = ctx.source.player; + if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } + const cur = bnsTier.get(player); + if (cur >= bnsTier.MAX) { + player.tell(Text.gold('You\'re already Sovereign. There is no higher rank.')); + return 0; + } + const nextTier = cur + 1; + const nextCfg = bnsTier.config(nextTier); + const balance = countCoins(player); + if (balance < nextCfg.cost) { + player.tell(Text.red( + 'Upgrade to ' + nextCfg.name + ' costs ' + nextCfg.cost + ' spurs. You have ' + balance + '.' + )); + return 0; + } + const took = takeCoins(player, nextCfg.cost); + if (took < nextCfg.cost) { + giveCoins(player, took); + player.tell(Text.red('Coin removal failed — refunded. Try again.')); + return 0; + } + // Promote via FTB Ranks + const rankId = TIER_RANK_ID[nextTier]; + Utils.server.runCommandSilent('ftbranks add ' + player.username + ' ' + rankId); + // Mirror to NBT + bnsTier.set(player, nextTier); + player.tell(Text.gold( + '╔══ You have been elevated to ' + nextCfg.name + '! ══╗' + )); + player.tell(Text.gold( + ' ' + nextCfg.wildernessChunks + ' wilderness chunks · ' + + nextCfg.plotSlots + ' plot slots · ' + + nextCfg.dailyBountySlots + ' bounty slots · ' + + (Math.max(0, 25 + nextCfg.shopFeeDiscount)) + '% Bazaar fee' + )); + console.info('[bns/tier] ' + player.username + ' upgraded to tier ' + nextTier + ' (' + nextCfg.name + ') for ' + nextCfg.cost + ' spurs'); + return 1; + }))); + + event.register(playerCmds); +}); + +console.info('[bns/player_commands] loaded — /bns help, /bns me, /bns tier upgrade'); diff --git a/pack/overrides/kubejs/server_scripts/plots.js b/pack/overrides/kubejs/server_scripts/plots.js index f6c8557..9e2d69a 100644 --- a/pack/overrides/kubejs/server_scripts/plots.js +++ b/pack/overrides/kubejs/server_scripts/plots.js @@ -135,12 +135,25 @@ function ownerOf(plotId) { return map.contains(plotId) ? map.getString(plotId) : null; } -function plotOwnedBy(playerUuid) { +function plotsOwnedBy(playerUuid) { const map = getOwnerMap(); + const owned = []; for (const key of map.getAllKeys()) { - if (map.getString(key) === playerUuid) return key; + if (map.getString(key) === playerUuid) owned.push(key); } - return null; + return owned; +} + +// Per-tier plot slot cap. Index = tier 1-13. Mirrors TIER_CONFIG.plotSlots +// in 00_tier_system.js — kept duplicated here because KubeJS Rhino doesn't +// allow cross-script globals (strict mode). See feedback-kubejs-rhino-gotchas. +const TIER_PLOT_SLOTS = [0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 10]; + +function playerPlotSlotCap(player) { + const data = player.persistentData; + const tier = data.contains('bnsTier') ? data.getInt('bnsTier') : 1; + const safe = (tier >= 1 && tier <= 13) ? tier : 1; + return TIER_PLOT_SLOTS[safe]; } function recordPurchase(plotId, playerUuid) { @@ -247,10 +260,18 @@ ServerEvents.commandRegistry(event => { player.tell(Text.red(`Plot ${id} is already owned.`)); return 0; } - const existing = plotOwnedBy(player.uuid.toString()); - if (existing) { + // Tier-based slot cap. + const owned = plotsOwnedBy(player.uuid.toString()); + const cap = playerPlotSlotCap(player); + if (cap <= 0) { player.tell(Text.red( - `You already own plot ${existing}. Release it first with /bns plot release.` + `Your current tier doesn't allow plot ownership. Upgrade your civic tier first.` + )); + return 0; + } + if (owned.length >= cap) { + player.tell(Text.red( + `You're at your plot-slot cap (${owned.length}/${cap}). Release one with /bns plot release , or upgrade your tier for more slots.` )); return 0; } @@ -271,33 +292,68 @@ ServerEvents.commandRegistry(event => { recordPurchase(id, player.uuid.toString()); claimChunksForPlayer(plot, player); player.tell(Text.gold( - `Plot ${id} (${plot.name}) is now yours. Paid ${plot.price} spurs.` + `Plot ${id} (${plot.name}) is now yours. Paid ${plot.price} spurs. Slots used: ${owned.length + 1}/${cap}.` )); - console.info(`[bns/plots] ${player.username} bought ${id} for ${plot.price} spurs`); + console.info(`[bns/plots] ${player.username} bought ${id} for ${plot.price} spurs (slot ${owned.length + 1}/${cap})`); return 1; }))) - // /bns plot release - .then(Commands.literal('release').executes(ctx => { + // /bns plot release + // Requires the plot id explicitly. If a player owns multiple plots + // (high-tier), they must say which one. The old "release with no + // arg" shortcut is gone -- explicitness wins for safety. + .then(Commands.literal('release').then(plotIdArg().executes(ctx => { const player = ctx.source.player; if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; } - const id = plotOwnedBy(player.uuid.toString()); - if (!id) { - player.tell(Text.red(`You don't own a plot.`)); + const id = Arguments.STRING.getResult(ctx, 'id'); + const ownerUuid = ownerOf(id); + if (ownerUuid !== player.uuid.toString()) { + player.tell(Text.red(`You don't own plot ${id}.`)); return 0; } const plot = PLOT_DEFS.get(id); + if (!plot) { + player.tell(Text.red(`Plot ${id} has no definition -- something's wrong.`)); + return 0; + } const refund = Math.floor(plot.price * REFUND_FRACTION); unclaimChunksForPlayer(plot, player); clearOwner(id); giveCoins(player, refund); + const owned = plotsOwnedBy(player.uuid.toString()); + const cap = playerPlotSlotCap(player); player.tell(Text.gold( - `Released plot ${id}. Refunded ${refund} spurs (${Math.round(REFUND_FRACTION*100)}% of ${plot.price}).` + `Released plot ${id}. Refunded ${refund} spurs (${Math.round(REFUND_FRACTION*100)}% of ${plot.price}). Slots used: ${owned.length}/${cap}.` )); - console.info(`[bns/plots] ${player.username} released ${id}, refunded ${refund} spurs`); + console.info(`[bns/plots] ${player.username} released ${id}, refunded ${refund} spurs (now ${owned.length}/${cap})`); + return 1; + }))) + + // /bns plot mine -- show plots the current player owns + .then(Commands.literal('mine').executes(ctx => { + const player = ctx.source.player; + if (!player) { + ctx.source.sendSystemMessage(Text.red('Run from a player context.')); + return 0; + } + const owned = plotsOwnedBy(player.uuid.toString()); + const cap = playerPlotSlotCap(player); + if (owned.length === 0) { + player.tell(Text.gray(`You own no plots. Available slots at your tier: ${cap}`)); + return 1; + } + player.tell(Text.aqua(`You own ${owned.length}/${cap} plots:`)); + for (const pid of owned) { + const plot = PLOT_DEFS.get(pid); + if (plot) { + player.tell(Text.gray(` ${pid} "${plot.name}" ${plot.size} ${plot.price} spurs`)); + } else { + player.tell(Text.red(` ${pid} (orphaned -- plot definition missing)`)); + } + } return 1; })) diff --git a/pack/overrides/world/serverconfig/ftbranks/ranks.snbt b/pack/overrides/world/serverconfig/ftbranks/ranks.snbt new file mode 100644 index 0000000..95b27a8 --- /dev/null +++ b/pack/overrides/world/serverconfig/ftbranks/ranks.snbt @@ -0,0 +1,122 @@ +{ + # Brass and Sigil 13-tier civic ladder. + # Permission nodes: + # ftbchunks.max_claimed_chunks — wilderness chunk allowance + # ftbchunks.max_force_loaded_chunks — force-load allowance (~15% of claim cap) + # ftbranks.name_format — chat colour for this rank + # Mirrors TIER_CONFIG in pack/overrides/kubejs/server_scripts/economy/00_tier_system.js. + # Tier upgrade flow: KubeJS sets player.persistentData.bnsTier AND runs + # /ftbranks add to apply this rank's permission nodes. + + peasant: { + name: "Peasant" + power: 1 + condition: "always_active" + ftbchunks.max_claimed_chunks: 9 + ftbchunks.max_force_loaded_chunks: 1 + ftbranks.name_format: "&7{name}" + } + + farmer: { + name: "Farmer" + power: 2 + ftbchunks.max_claimed_chunks: 18 + ftbchunks.max_force_loaded_chunks: 2 + ftbranks.name_format: "&7{name}" + } + + citizen: { + name: "Citizen" + power: 3 + ftbchunks.max_claimed_chunks: 35 + ftbchunks.max_force_loaded_chunks: 4 + ftbranks.name_format: "&f{name}" + } + + merchant: { + name: "Merchant" + power: 4 + ftbchunks.max_claimed_chunks: 60 + ftbchunks.max_force_loaded_chunks: 8 + ftbranks.name_format: "&e{name}" + } + + knight: { + name: "Knight" + power: 5 + ftbchunks.max_claimed_chunks: 100 + ftbchunks.max_force_loaded_chunks: 12 + ftbranks.name_format: "&a{name}" + } + + baron: { + name: "Baron" + power: 6 + ftbchunks.max_claimed_chunks: 150 + ftbchunks.max_force_loaded_chunks: 18 + ftbranks.name_format: "&b{name}" + } + + viscount: { + name: "Viscount" + power: 7 + ftbchunks.max_claimed_chunks: 220 + ftbchunks.max_force_loaded_chunks: 26 + ftbranks.name_format: "&9{name}" + } + + earl: { + name: "Earl" + power: 8 + ftbchunks.max_claimed_chunks: 300 + ftbchunks.max_force_loaded_chunks: 36 + ftbranks.name_format: "&5{name}" + } + + marquess: { + name: "Marquess" + power: 9 + ftbchunks.max_claimed_chunks: 400 + ftbchunks.max_force_loaded_chunks: 50 + ftbranks.name_format: "&6{name}" + } + + duke: { + name: "Duke" + power: 10 + ftbchunks.max_claimed_chunks: 525 + ftbchunks.max_force_loaded_chunks: 65 + ftbranks.name_format: "&6{name}" + } + + archduke: { + name: "Archduke" + power: 11 + ftbchunks.max_claimed_chunks: 700 + ftbchunks.max_force_loaded_chunks: 85 + ftbranks.name_format: "&c{name}" + } + + grand_duke: { + name: "Grand Duke" + power: 12 + ftbchunks.max_claimed_chunks: 900 + ftbchunks.max_force_loaded_chunks: 110 + ftbranks.name_format: "&c{name}" + } + + sovereign: { + name: "Sovereign" + power: 13 + ftbchunks.max_claimed_chunks: 1500 + ftbchunks.max_force_loaded_chunks: 200 + ftbranks.name_format: "&d{name}" + } + + admin: { + name: "Admin" + power: 1000 + condition: "op" + ftbranks.name_format: "&2{name}" + } +} 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",