econ: sell shop + bounty engine + player commands + FTB Ranks 13-tier
Foundation economy stack — fully command-driven, ready for the custom
UI mod to layer on top later. Built tonight as a single feature batch.
KubeJS scripts:
pack/overrides/kubejs/server_scripts/economy/
00_tier_system.js — 13-tier ladder + admin commands
02_sell_shop.js — /bns sell, 9 items, DR + tier-fee
03_bounty_engine.js — /bns bounty, 20 bounties, state machine
04_player_commands.js — /bns help, /bns me, /bns tier upgrade
plots.js: updated to support multi-plot ownership based on tier slots
- plotsOwnedBy() returns list, slot cap from TIER_PLOT_SLOTS table
- /bns plot release now requires plot id (multiple plots possible)
- /bns plot mine added
FTB Ranks server config:
pack/overrides/world/serverconfig/ftbranks/ranks.snbt
13 ranks with ftbchunks.max_claimed_chunks scaling per tier:
Peasant 9 -> Sovereign 1500 (chunks)
Peasant 1 -> Sovereign 200 (force-loaded)
Name format colour codes match the design doc.
Player command surface (everything callable from chat or Telegram bridge):
/bns help full command listing
/bns me tier, balance, plots, bounties
/bns tier upgrade pay spurs, advance to next tier
/bns plot list|info|buy|release|mine
/bns sell list|<item> [count]
/bns bounty list|active|accept|cancel|turnin|completed
/bns admin tier get|set|list (op level 2)
/bns admin info|waystone-*|reset-welcome (pre-existing)
Tier upgrade flow:
1. Player runs /bns tier upgrade
2. KubeJS takes spurs from inventory
3. KubeJS sets player.persistentData.bnsTier += 1
4. KubeJS runs `ftbranks add <player> <rank>` so FTB Ranks applies
the permission nodes (chunk allowance auto-flips)
Cross-file scope: KubeJS 2101 runs all server_scripts in shared Rhino
global scope. Constants (TIER_CONFIG, COIN_VALUES, bnsTier object,
shared functions like giveCoins/countCoins/takeCoins) are declared
once in their owning file and referenced from siblings. Don't redeclare.
Blocked tonight (deferred):
- Villager trade rebalance: KubeJS 2101.7.2 doesn't ship a
villagerTrades event. ServerEvents only exposes command/loaded/
recipes/registry/tags/tick/unloaded. Verified by grepping the
KubeJS jar's ServerEvents class. No KubeJS-villager-trade addon
on Modrinth for NeoForge 1.21.1. Either build into the custom
mod or add a third-party trade-overrides mod (research pending).
- Custom mod UI screens: no Java toolchain on host, and these
need visual iteration with the user anyway.
Verified live: server boots clean, all 4 economy scripts log loaded,
port 25565 binds, no EcmaError or EvaluatorException in KubeJS log.
FTB Ranks reload via rcon confirms "Ranks reloaded from disk!"
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <id>, 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 <id>
|
||||
// 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;
|
||||
}))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user