Compare commits

...

60 Commits

Author SHA1 Message Date
Matt 93c68325fe pack: v0.41.2 — rename Counting House to Exchange + fix click-refresh bug
bnstoolkit 0.8.1 -> 0.8.2.

Counting House -> Exchange (more intuitive naming).
- /bns open counthouse -> /bns open exchange
- screen title shown as "Exchange"
- Lang + docs updated

Click-doesn't-refresh-detail bug: ItemCell / BountyRow / PlotRow now
call getGui().refreshWidgets() after updating their static `selected`
field, so the detail sub-panel re-renders with the new selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:22:47 +00:00
Matt 95a564c5e3 Merge pull request 'pack: v0.41.1 — UI pass + spawn-building setup doc' (#83) from hotfix/ui-pass into main 2026-06-08 18:14:51 +00:00
Matt f0e6187ed0 pack: v0.41.1 — UI pass + spawn-building-setup doc
bnstoolkit 0.8.0 -> 0.8.1.

UI pass on every transactional screen:
- Counting House grid now 4-cell (one per wealth-token), with
  inventory count badge on each cell
- Detail pane shows on-hand count + friendly display name
- Sell buttons grey out when you don't have enough
- Bottom-row Refresh replaced with smart Back/Close — fixes the
  button overlap with sub-panel internals
- Panel heights reduced (height-30 -> height-50) so internal
  buttons don't clash with the screen-level action row

Naming cleanup:
- Dropped "shop" alias (Counting House is the only correct name)
- Added "journal" as the user-facing screen key for Adventurer's
  Journal

New docs/spawn-building-setup.md covers:
- /bns admin spawn-init flow for FTB Teams init
- Per-building Easy NPC dialogue setup (Counting House Clerk,
  Bounty Officer, Plot Clerk, Civic Registrar)
- Pre-wipe testing checklist for every feature

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 18:14:50 +00:00
Matt 263b8c59d8 Merge pull request 'pack: v0.41.0 — plot config JSON + FTB Chunks integration' (#82) from feature/v0.41.0-plot-config-ftb-integration into main 2026-06-08 17:53:35 +00:00
Matt a39bfee803 pack: v0.41.0 — plot system JSON config + FTB Chunks integration
bnstoolkit 0.7.0 -> 0.8.0.

Plot system migrated from static Java + KubeJS to a runtime
configurable, FTB-Chunks-integrated system.

* Plot definitions now live at world/serverconfig/bnstoolkit/plots.json.
  Default file ships with 3 starter plots; edit + /bns admin plot reload.
* /bns admin plot define <id> <name> <price>: add a plot in-game by
  standing in the chunk. Re-run with same id to add multi-chunk plots.
* /bns admin plot list / remove / reload + /bns admin spawn-init.
* /bns admin spawn-init creates Spawn (cyan #00BFFF) + Plots (gold
  #FFD700) server teams via FTB Teams + claims all defined plots for
  the Plots team. Idempotent.
* /bns plot buy now: pays via combined cash+bank, unclaims chunks
  from Plots team, claims for buyer's personal FTB Teams team.
* /bns plot release: reverse — chunks go back to Plots team, 50% refund.
* Plot chunk guard: ClaimedChunkEvent.BEFORE_UNCLAIM subscriber blocks
  manual unclaim of plot chunks (player gets chat hint redirecting
  to /bns plot release).
* KubeJS plots.js retired (.retired suffix in repo as documentation).

On the map: cyan zones = Spawn team protection (manually claimed),
gold zones = Plots team (for sale), other team colors = owned plots.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 17:53:34 +00:00
Matt 03f650ce3b Merge pull request 'pack: v0.40.0 — drop Alex's Mobs + bounty board fix + Numismatics polish' (#81) from feature/v0.40.0-drop-am-polish into main 2026-06-08 17:20:10 +00:00
Matt 35cbd84146 pack: v0.40.0 — drop Alex's Mobs + bounty board fix + Numismatics polish
Drops Alex's Mobs from the pack (105 -> 103 mods total). The 4 AM-mob
bounty entries are replaced with vanilla 1.21.x mob bounties (Breeze,
Bogged, Evoker, Vindicator, Piglin Brute, Ravager).

Bounty board bug fix: KubeJS PlayerEvents.loggedIn now ensures the
daily pool is rolled on first login. Previously the pool only rolled
when a player ran /bns bounty list, so the bounty board screen showed
empty for anyone who'd never typed that command.

/bns tier upgrade now uses combined cash+bank deduction via
NumismaticsBridge.spend (Java). Players who've sold a fortune into
their bank can now spend it on tier upgrades directly.

Every player gets a bound white_card on first login — no more "I need
to craft + bind a card before I can spend at a Vendor block" friction.

bnstoolkit 0.6.2 -> 0.7.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 17:20:09 +00:00
Matt c9f57dae9e Merge pull request 'pack: v0.39.2 — Adventurer's Journal + smart Back button' (#80) from hotfix/adventurers-journal into main 2026-06-08 16:20:03 +00:00
Matt 83949d63b5 pack: v0.39.2 — Adventurer's Journal + smart Back button
bnstoolkit 0.6.1 -> 0.6.2.

Journal (formerly Quest Log) reworked to be useful: active bounties
with progress bars, current Counting House rates per wealth-token,
owned plots. FTB Quests stays as a separate system (footer line
points players to their Quest Book item).

Back button: opens-from-Hub uses FTB Library's previous-screen
mechanism so Close becomes Back and returns to Hub. NPC-opened
journals (no previous screen) get a regular Close.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 16:20:03 +00:00
Matt 68aae5c917 Merge pull request 'pack: v0.39.1 — bnstoolkit 0.6.1 sub-panel sizing hotfix' (#79) from hotfix/sub-panel-sizing into main 2026-06-08 15:51:32 +00:00
Matt 0818fe8eab pack: v0.39.1 — bnstoolkit 0.6.1 sub-panel sizing hotfix
Hub screen showed blank content area: title bar + 3 buttons rendered,
but the status panel in between was empty. Same latent bug affected
every screen with a sub-panel (Tier, Counting House, Bounty, Plot,
Quest Log).

Root cause: FTB Library's Panel.refreshWidgets order is
addWidgets → recurse-into-child-panels.refreshWidgets → alignWidgets.
So when the parent's alignWidgets sets a sub-panel's size, the
sub-panel's children have already aligned themselves using the
sub-panel's pre-size dimensions (0x0). Every Row got setSize(0, 12)
→ invisible.

Fix: size sub-panels BEFORE add() in the parent's addWidgets so the
recursive refreshWidgets sees correct dimensions. Parent's
alignWidgets now only positions the top-level action buttons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 15:50:51 +00:00
Matt f269cc4861 Merge pull request 'pack: v0.39.0 — Counting House + remove Create: Encased' (#78) from feature/v0.39.0-counting-house into main 2026-06-08 15:34:48 +00:00
Matt ae183f27de pack: v0.39.0 — Counting House refactor + remove Create: Encased
Major economy / spawn-design pivot.

## Removed: Create: Encased
The mod's `chorium_ingot` → `creative_casing` chain unlocked 60+
infinite-output creative-tier blocks via fully renewable inputs (rose
quartz + chorus fruit + lava). Single creative_mechanical_drill in a
player base would have produced enough diamonds to trivialize the
civic ladder. Removing the mod was simpler than KubeJS recipe overrides
since none of the mod's other content (wood-variant encased shafts,
coloured casings) was load-bearing for our pack.

Parallel audit of 14 other Create-family mods confirmed none have
similar exploits — only Create: Encased was the breaker.

## Counting House refactor
What used to be "Bazaar" became "Counting House" — a wealth exchange,
not a general shop. Conceptual model:
- Players accumulate diamonds / netherite / ancient debris through
  adventure and mining (the wealth-store axis)
- Counting House converts these to spurs, credited directly to the
  player's Numismatics bank account
- Iron / gold / coal / lapis / redstone are no longer here — those
  belong on player-run Numismatics Vendor blocks at market prices

SellCatalogue cut from 9 items to 4 (diamond, diamond_block,
netherite_ingot, ancient_debris). Diminishing-returns floor tightened
50% -> 30%.

## Numismatics integration
NumismaticsBridge (reflection — no hard dep) wires up:
- ensureAccount on PlayerLoggedInEvent (every player gets a personal
  account on first login)
- balanceOf / deposit / deduct for bank operations
- SpurBalance.totalSpurs = inventory coins + own bank balance, no card
  lookups required
- HUD overlay now shows three lines: tier / cash on hand / bank
  balance — total wealth visible regardless of cards

## /bns commands migrated to Java
`/bns sell` and `/bns wallet` are now Java-handled
(CountingHouseCommands.java). Bank credit happens atomically with
inventory pull. KubeJS `02_sell_shop.js` retired (.retired suffix
preserved in the repo as documentation).

## Hub status-only
Removed the Civic Tier / Bazaar / Bounty / Plot routing buttons.
Transactional screens are now only reachable via NPC dialogue
(`/bns open <screen>` from an Easy NPC COMMAND action). The hub
shows tier + cash + bank + total + slot caps; footer reads "Visit
spawn NPCs for trades, bounties, plots, tier upgrades."

This locks the spawn-as-civic-centre design: players have to travel.

## bnstoolkit jar
0.5.1 -> 0.6.0 (network version 0.4 -> 0.5; TierStateUpdatePacket
now carries cashOnHand + bankBalance separately).

## Pack manifest
102 -> 104 mods after Encased removal. Manifest republished on glados
(publish-pack.py) before restart so the wrapper's sync sees the right
target state. Without that step, the sync was reverting our changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 15:34:25 +00:00
Matt ec8f2b83cf Merge pull request 'pack: v0.38.1 — bnstoolkit 0.5.1 UI coords hotfix' (#77) from hotfix/bnstoolkit-ui-coords into main 2026-06-08 14:43:10 +00:00
Matt 28c816990a pack: v0.38.1 — bnstoolkit 0.5.1 UI coords hotfix
Every screen positioned its child widgets with setPos(getX() + offset,
getY() + offset) under the assumption that FTB Library uses absolute
screen coordinates. It uses PARENT-RELATIVE coordinates — the panel's
own position is added back at render time — so all widgets rendered
at panel-position * 2 + offset, shoving them off the right edge.

Fix: drop the getX()/getY() from every setPos. Now using plain
parent-relative offsets, which is what FTB Library docs + FTB Quests/
Chunks source actually do.

Affects: HubScreen, TierUpgradeScreen, BountyBoardScreen,
PlotPurchaseScreen, ShopBrowserScreen, QuestLogScreen.
Also fixed BaseBnsScreen.contentTop()/contentBottom() to return
relative offsets.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 14:42:41 +00:00
Matt 73f55768c3 scripts: add publish-pack.py — Linux port of Build-Pack.ps1 + deploy
Regenerates /srv/services/sites/bns/pack/manifest.json from
pack.lock.json + pack/overrides/, mirrors changed override files into
the Caddy-served web root, and atomically swaps in the new manifest.

Resolves a class of bug where the published manifest gets stuck at the
last Windows-side build and the launcher errors with "invalid hash for
<file>" on its next sync. Caught today because the manifest had been
serving v0.33.0 since v0.34.0 shipped — five missed publishes.

Run after every pack PR with sudo on glados:
    sudo -n python3 scripts/publish-pack.py

Idempotent. Skips files whose hash already matches the served copy.

Special-cases bnstoolkit-*.jar (side: both — has client UI) so the
launcher doesn't refuse to install it on clients; brassandsigil_tweaks
stays side: server as the existing convention dictates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 14:35:54 +00:00
Matt 48a70326d2 Merge pull request 'pack: v0.38.0 — Create: Tracks+ (tank tread addon)' (#76) from feature/v0.38.0-create-tracks into main 2026-06-07 22:30:37 +00:00
Matt 5f6a5aacdd pack: v0.38.0 — Create: Tracks+ for tank-tread contraptions
Create: Tracks+ 1.0.5 (Modrinth slug create-tracks+) adds Trackwork-
style caterpillar/tank tracks to Create contraptions. Fork of qwxon's
original Create:Tracks, the only 1.21.1 NeoForge port. Required deps
(Create, Create Aeronautics, Sable) are all already in the pack.

Use case: tracked vehicles / tanks built with Create — caterpillar
treads for moving contraptions over uneven terrain, military
aesthetic, or just heavy industrial movers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 22:30:08 +00:00
Matt 8d9c04dc05 Merge pull request 'pack: v0.37.0 — village supply/demand + Structory structure mods' (#75) from feature/v0.37.0-villages-and-structures into main 2026-06-07 16:27:53 +00:00
Matt ea9326819d pack: v0.37.0 — Tier 3 village supply/demand + Structory structures
Pre-wipe consolidation. After this, the seed pick is unblocked.

bnstoolkit 0.4.1 → 0.5.0 — village supply/demand throttle
- 64×64 block tiles each have a finite demand pool per cost item
- Spur-paying trades decrement their tile's pool; pool refills over 1
  hour real-time (lazy refresh, no scheduled tick, no perf overhead)
- Saturated trades show vanilla "out of stock" in the UI until the
  villager restocks AND the tile's demand has refilled
- ~150 spurs/hour cap per tile in a fully-stocked multi-profession hall
- 50-librarian-hall exploit dies; players who want more income must
  build halls in distant tiles (overworld logistics) OR diversify
  professions (each item type has its own pool)
- Anyone with /bns admin can inspect a tile's state — full picture
  lives in server.persistentData.bnsVillageDemand

New worldgen mods (Modrinth — both NeoForge 1.21.1):
- Structory 1.3.16 — 15 small thematic ruins: chapels, graveyards,
  fire-watch towers, abandoned camps, boats, outcast huts
- Structory: Towers 1.0.16 — 21 mid/rare-tier towers: lighthouses,
  engineer's tower, ocean pillars, wizard towers, end towers

Density assessment (per earlier audit): pack goes from ~52 to ~88
generating structure types. Towers are mostly rare-tier (40-80 chunk
spacing) so they read as discoveries, not clutter. Loot is in line
with vanilla bastion/mansion tier — finite chest pool that the
diminishing-returns sell shop and village supply system already
balance against.

Pack mods: 102 → 104.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 16:27:25 +00:00
Matt f2c9cabefe Merge pull request 'pack: v0.36.0 — bnstoolkit 0.4.1 (review fixes + hub UI + NPC integration)' (#74) from feature/v0.36.0-bnstoolkit-0.4.1 into main 2026-06-07 09:28:20 +00:00
Matt ad9a261627 pack: v0.36.0 — bnstoolkit 0.4.1 (NBT fix + hub UI + NPC integration + hardening)
Post-review consolidation PR. All four open issues from the parallel
code/security/hotkey/NPC audits resolved in one ship.

Key user-facing changes:

UI
- Single keybind: B opens a hub screen with buttons to the 5 sub-screens.
- K/J/N/H/M direct shortcuts default to UNBOUND so they no longer
  compete with the ~100 other mods. Rebindable in vanilla controls menu.
- M used to clash with vanilla Social Interactions + FTB Chunks
  fullscreen map; fully resolved.

NPC dialogue + scripting integration
- New /bns open <screen> command. Easy NPC dialogue COMMAND actions
  can now open any bnstoolkit screen on the clicking player.
- FTB Quests reward commands, command blocks, and /trigger objectives
  can all route into bnstoolkit screens via the same command.
- config/easy_npc/security.cfg ships with executeAsUserCommandAllowList
  .ALL=bns so dialogue-triggered /bns commands actually run.

Bug fix (this is the load-bearing item)
- Bounty board + plot office screens were reading NBT keys that don't
  exist in KubeJS, so they always showed "no active bounties" and "all
  plots free" no matter what was actually in the world. Fixed by reading
  the real per-player keys (bnsBountyPool/Active/Completed) and the
  server-scope bnsPlots compound via KubeJS' MinecraftServer mixin
  (reflective, no compileOnly dep on KubeJS).

Hardening (no behavior change, defense-in-depth)
- Per-player token bucket on C2S packets (10/s sustain, 20 burst).
- All STRING_UTF8 codec fields capped at sane sizes (≤80 chars).
- Verb whitelist replaced with structured parse + per-verb regex
  validation + rebuilt command. No prefix-startsWith surprises.
- Tier-upgrade in-flight guard closes the concurrent-packet
  double-spend race window.
- Log lines sanitised to prevent control-char injection.

Plus the nits caught by review:
- Per-player tick counter for HUD push (was a shared static)
- VillagerTradeRebalance try/catch + null-cost-A passthrough +
  round-to-nearest denom split (was floor — ~10% silent price haircut)
- SpurHud component cache (was allocating per frame)
- BountyBoardScreen dead branch removed

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 09:27:47 +00:00
Matt bae44d0178 Merge pull request 'docs: rewrite bnstoolkit handoff for feature-complete state' (#73) from docs/bnstoolkit-feature-complete into main 2026-06-07 02:43:56 +00:00
Matt 016985a5fc docs: rewrite bnstoolkit handoff for feature-complete state
The original handoff doc described an empty M0 scaffold with placeholder
screens to "discuss tomorrow". 0.3.1 actually ships the full UI plus
villager trade rebalance, so the doc now is a testing guide for the
working features rather than a discussion-points list.

Includes the architecture diagram for how Java + KubeJS interact, the
key bindings table, a step-by-step testing path, and a known-gaps list
for future iteration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:43:42 +00:00
Matt a7f95691bb Merge pull request 'pack: v0.35.0 — bnstoolkit feature-complete UI + villager rebalance' (#72) from feature/v0.35.0-bnstoolkit-ui-full into main 2026-06-07 02:42:43 +00:00
Matt 23efdee804 pack: actually include bnstoolkit-0.3.1.jar in v0.35.0
The previous commit deleted 0.1.1 from the gitignored mods dir but
git swallowed the add for 0.3.1 because pack/overrides/mods is
ignored. Re-adding with -f.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:42:06 +00:00
Matt 1859613f1f pack: v0.35.0 — bnstoolkit 0.3.1 (feature-complete UI + villager rebalance)
bnstoolkit jumps from M0 (empty scaffold) straight to feature complete:

UI screens (all FTB Library widgets, opens via keybinds):
  K  Civic tier ladder + upgrade
  J  Bounty board (accept/cancel/turn-in)
  N  Plot office (buy/release)
  H  Bazaar sell shop (1/16/64 quick-sell)
  M  Quest log (status overview)

HUD: top-left tier + spurs balance overlay, refreshed 1Hz server-side.

Villager trade rebalance (NEW): the mod now hooks VillagerTradesEvent
+ WandererTradesEvent and swaps every emerald cost/result for the
Numismatics equivalent (1..7 emeralds -> spurs, 8..63 -> bevels, 64+
-> cogs). This was the blocked Modrinth dep — the third-party mod
options either required a NeoForge bump (numismatics-villager-currency
1.1.0 wants 21.1.230+, we're on 21.1.228) or hand-written JSON for
every trade (data-trades). The in-tree wrapper is cleaner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 02:41:49 +00:00
Matt a41d8cb531 Merge pull request 'docs: bnstoolkit postmortem' (#71) from docs/bnstoolkit-discussion-postmortem into main 2026-06-07 00:19:21 +00:00
Matt e770cb55cb docs: add bnstoolkit 0.1.0 crash postmortem + manual-restart note
Surfaces the server-needs-Start state and the fix details up top so
when matt wakes up he doesn't have to scroll past the M1+ planning
to find out the box is down.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 00:19:06 +00:00
Matt 338617e855 Merge pull request 'pack: v0.34.1 — bnstoolkit hotfix 0.1.1' (#70) from hotfix/bnstoolkit-0.1.1 into main 2026-06-07 00:18:38 +00:00
Matt ecd5933f9f pack: v0.34.1 — bnstoolkit hotfix 0.1.1 (fixes mod-load crash)
0.1.0 brought the dedicated server down during construct-mods because
the @Mod constructor called NeoForge.EVENT_BUS.register(this) without
any @SubscribeEvent methods on the class — which NeoForge 21.1 treats
as a fatal IllegalArgumentException.

Caught only after live-deploy (gradle build doesn't simulate the
construct-mods step). 0.1.1 removes the bogus register call. The
@SubscribeEvent listeners will come back in M1 alongside the first
real event handler.

Server has been brought down by the crash and is waiting for a manual
start via the web admin panel. The fixed jar is staged in
/srv/fast/brass-sigil-server/server/mods/ already.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 00:18:17 +00:00
Matt 38fbf80bc1 Merge pull request 'pack: v0.34.0 — bnstoolkit M0 scaffold' (#69) from feature/v0.34.0-bnstoolkit-m0 into main 2026-06-07 00:13:28 +00:00
Matt 61dd3d3754 pack: v0.34.0 — bnstoolkit M0 scaffold
Ships the empty bnstoolkit companion mod (0.1.0) into the pack. The mod
loads cleanly on client and server with zero visible surface — it just
proves the deploy pipeline end-to-end so M1-M4 only have to add code,
not figure out tooling.

Per-milestone scope locked in docs/bnstoolkit-discussion-points-2026-06-07.md.

Source: http://10.16.5.102:3000/minecraft/bnstoolkit

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 00:13:05 +00:00
Matt 61a923cbef Merge pull request 'docs: economy V1 overnight handoff' (#68) from docs/overnight-handoff into main 2026-06-06 23:57:24 +00:00
Matt 57b6cd8dea docs: economy V1 overnight handoff
Tomorrow-evening testing guide for the command-driven economy
shipped in PR #67. Covers: what works, how to test each system,
quick reference of every /bns command, what's deferred and why,
recovery steps if anything bricks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 23:57:23 +00:00
Matt 572671f1d4 Merge pull request 'econ: sell shop + bounty engine + player commands + FTB Ranks 13-tier' (#67) from feature/v0.33.0-economy-foundation into main 2026-06-06 23:55:43 +00:00
Matt ffff3bc78d 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>
2026-06-06 23:55:42 +00:00
Matt b4cd66036b econ: tier system foundation (00_tier_system.js)
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>
2026-06-06 23:38:30 +00:00
Matt 3d635d7e59 Merge pull request 'pack: v0.32.0 — FTB Quests + XMod Compat + Ranks; restrictive travel design' (#66) from feature/v0.32.0-and-restrictive-travel-design into main 2026-06-06 23:22:48 +00:00
Matt 9eb8f540c4 pack: v0.32.0 — FTB Quests + XMod Compat + Ranks; restrictive travel design
Mods added (Phase 0a substrate for the bnstoolkit economy work):
  - FTB Quests 2101.1.25 (progression quest UI for Phase 5+)
  - FTB XMod Compat 21.1.8 (KubeJS hooks for FTB Chunks/Quests events)
  - FTB Ranks 2101.1.3 (tier system via permission nodes)

Design revision: restrictive travel.

This is a Create Aeronautics pack — the journey is the point. Removed
all instant-teleport perks from the tier ladder. Players get one
starter Waystone and use it for spawn↔base; further travel means
building airships, trains, vehicles. Pressure on real movement is
intentional.

Removed perks: sethomes, /back after death.
Removed system: the full sethome subsystem (commands, SavedData,
cooldowns) from the bnstoolkit mod scope.

Tier table reduced from 10 columns to 8: cost, wilderness chunks,
plot slots, daily bounties, shop fee, chat colour, join broadcast.

Mod phasing collapsed from 7 phases to 6 (M2 sethome system removed,
later phases shift up).

Future perk dimensions list updated with two waystone-related
candidates (slot cap, XP cost reduction) as the natural travel-
progression hooks for later tier-perk revisions.

Verified live: all 3 mods discovered + loaded, port 25565 binds,
no FATAL/Crashed/Exception lines in startup log. Server resumed
to 0.32.0 cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 23:22:47 +00:00
Matt 367056e2ba Merge pull request 'docs: bnstoolkit architecture + detailed perk specs' (#65) from docs/bnstoolkit-architecture into main 2026-06-06 23:09:35 +00:00
Matt 25107efcf1 docs: bnstoolkit architecture + detailed perk specifications
New companion-mod design doc covering:

Architecture
- Purpose & scope (what's in the mod vs what stays in KubeJS)
- High-level diagram showing client/server/KubeJS/FTB-stack roles
- Full Java package layout under uk.sijbers.bnstoolkit
- Six GUI screens specified individually with layouts + network calls
  (Quest Log, Bounty Board, Tier Upgrade, Plot Purchase, Shop Browser,
   Quest Progress HUD)
- Three mixin targets into FTB Chunks' ChunkScreenPanel (drawBackground,
  ChunkButton.addMouseOverText, ChunkButton.onClicked) -- no FTB fork
- Network packet contract (7 S2C + 5 C2S packets, versioned)
- Data persistence model (SavedData for plots + sethomes, in-memory
  for death locations, KubeJS for quest state)
- Two config files documented (tiers.json + plots.json) with example
  JSON5 schemas
- KubeJS integration surface (8 /bnstoolkit_internal commands)
- Build & release setup (separate Gitea repo, standard NeoForge
  ModDevGradle, semver)
- 6 mod implementation phases (M0-M6), each independently shippable

Detailed perk specifications
- Wilderness chunks: FTB Ranks permission node, native FTB Chunks
  enforcement, no mod-side work
- Plot slots: SavedData-backed ownership tracking, 50% refund on
  release
- Sethomes: full sethome system built in bnstoolkit (no other sethome
  mod exists in pack). Per-tier cap, 30s cooldown, /sethome /home
  /delhome /listhomes commands
- Daily bounty slots: enforced in KubeJS engine, UI greys Accept when
  at cap
- Shop fee discount: base 25% Bazaar fee reduced by tier discount.
  Only applies to NPC Bazaar shop, not player-to-player Numismatics.
  Worked example showing -25% Sovereign = 0% fee = full 10 spurs/diamond
- Chat colour: full colour ladder per tier (grey to rainbow at
  Sovereign with character-by-character cycling)
- Join broadcast: 6 levels documented (none, simple, fancy, epic,
  epic+particles, epic+spectacular) with example ASCII
- /back after death: in-memory storage, 60s cooldown, dimension-aware
- Future perk dimensions: 7 candidates the schema supports without
  code changes (particle trails, custom join sounds, custom title
  prefix, priority bounty access, tier-gated vendors, daily login
  bonus, keep-inventory chance)

Six open implementation questions flagged for resolution during
build, none blocking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 23:09:34 +00:00
Matt 56e6006424 Merge pull request 'docs: economy v0.4 — 13-tier ladder + Sovereign 10M + Founder flag' (#64) from docs/economy-v0.4-13-tier-ladder into main 2026-06-06 23:00:45 +00:00
Matt a0aee440c3 docs: economy v0.4 -- 13-tier ladder + Sovereign 10M + Founder flag
Tier ladder extended from 8 -> 13 tiers, with cleaner historical
progression. Replaces the previous Peasant/Citizen/Patron/Notable/
Baron/Noble/Lord/Sovereign ladder which mixed commoner-economic terms
with noble terms.

New 13-tier ladder (Civic + British peerage):
  1  Peasant       free
  2  Farmer        200
  3  Citizen       750
  4  Merchant      2,500
  5  Knight        6,500       (gentry threshold)
  6  Baron         15,000      (lowest noble)
  7  Viscount      35,000
  8  Earl          75,000
  9  Marquess      150,000
  10 Duke          300,000
  11 Archduke      700,000
  12 Grand Duke    1,500,000
  13 Sovereign     10,000,000  (apex, "finished the game")

Rationale:
- Yeoman -> Farmer, Burgher -> Merchant: both replaced with
  recognizable terms while keeping the period feel.
- Earl kept as the British equivalent of Count (Viscount sits below
  Earl correctly because Viscount = vice-count).
- Top tier extended above Duke with Archduke -> Grand Duke ->
  Sovereign. Avoids singular-feeling Emperor/King/Prince titles.
- Sovereign at 10M is intentionally extreme -- months of dedicated
  play to reach, properly mythical.

Full perk table updated to spread cleanly across 13 tiers:
- Plot slots: 0/0/1/1/2/2/3/3/4/5/6/7/10
- Sethomes: 1/1/2/3/4/5/6/8/10/12/16/20/unlimited
- Daily bounties: 3/3/3/4/4/5/5/6/6/7/7/8/10
- Shop fee: 0% down to -25% at Sovereign
- Chat colours: grey -> white -> yellow -> green -> cyan -> blue
  -> purple -> gold -> orange -> red -> bright red -> rainbow

Knight (tier 5) is the gentry threshold -- the first meaningful
"I'm somebody" upgrade with colored chat, join broadcast, and /back.

Founder cosmetic flag added for matt (server owner). Permanent chat
prefix [Founder] alongside the normal rank. No gameplay perks; just
identification. Founder still grinds the regular ladder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 23:00:45 +00:00
Matt 4ca2f17d9d Merge pull request 'docs: economy v0.3 lockdown (custom mod + UI + tiers + plots)' (#63) from docs/economy-v0.3-lockdown into main 2026-06-06 22:27:07 +00:00
Matt 121ae4c603 docs: economy v0.3 — custom mod + UI approach + tier ladder + spawn plots locked
Architectural revisions:

- UI approach: custom companion mod (working name 'bnstoolkit') built
  on FTB Library's UI framework. Polished GUI screens for Quest Log,
  Bounty Board, Tier upgrade, Shop browser, Plot purchase. Quest
  progress as a custom HUD render layer (no stacked vanilla bossbars).
  All player interaction through GUI + NPC dialogue, no chat commands
  to memorise. FTB Library UI chosen for visual consistency with the
  rest of the FTB stack already in the pack.

- Tier ladder locked: 8 tiers, Civic theme, starting at Peasant.
  Peasant -> Citizen -> Patron -> Notable -> Baron -> Noble -> Lord
  -> Sovereign. Replaced Burgess with Baron; reordered ascending.

- Tier perks: full table with chunks claimable, spawn plot slots,
  sethome count, daily bounty slots, shop fee, chat colour, join
  broadcast tier, /back access. JSON-configured at
  config/bnstoolkit/tiers.json, hot-reloadable. Easy to tune any
  column without code changes.

- Spawn plots clarified as distinct from wilderness chunk claims:
  predefined regions at spawn, purchased individually for spurs,
  primary use is shops, slot-limited by tier. Purchase happens
  inside the FTB Chunks map UI via custom mod mixin. Plot data
  lives in the custom mod, not in FTB Chunks.

- Wilderness chunk claims: tier-allocated, free within allowance,
  refused above. NO per-chunk pricing (dropped from v0.2). Powered
  by FTB Ranks -> FTB Chunks permission nodes.

- Phasing updated to add Phase 0c (mod scaffolding) and reorder
  Phases 1-7 to fit the mod-supplied UI work.

- Implementation notes: KubeJS script layout, custom mod package
  layout, state data locations, config file locations all
  documented.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 22:27:06 +00:00
Matt 107622fc6d Merge pull request 'docs: economy architecture lockdown (v0.2)' (#62) from docs/economy-architecture-lockdown into main 2026-06-06 21:09:45 +00:00
Matt 95b0d1564d docs: economy architecture lockdown (v0.2)
Final architecture for the economy + quest system. No more V1/V2/V3
iteration cycles. Resolved against actual mod source code and verified
APIs via research spike.

Key decisions locked:

- Bounty UI: custom (bossbar + /quests commands + Easy NPC dialog).
  NOT FTB Quests -- it lacks per-player visibility, has no abandon
  event, and cooldown is per-team. Confirmed via FTBQ source review.

- Progression UI: FTB Quests (right tool for long-form content where
  per-team cooldowns and one-shot rewards are fine).

- Numismatics: direct Java.loadClass access (no events shipped, no
  KubeJS bindings). Documented exact API surface and call patterns.

- FTB Chunks claim cost: FTBChunksEvents.before('claim') with
  event.setResult() to refuse. Requires adding FTB XMod Compat.

- Tier system: FTB Ranks (CurseForge only) + permission-driven chunk
  limits + KubeJS-mediated upgrades via runCommandSilent('ftbranks add').

- Per-player HUD: bossbars, not scoreboards. Vanilla scoreboards are
  global; bossbars natively per-player.

- Custom mod for V1: NOT NEEDED. Re-evaluate if blocked.

Mods to add in Phase 0a (one PR before any implementation):
  - FTB Quests 2101.1.25
  - FTB XMod Compat (latest)
  - FTB Ranks 2101.1.3

Phasing updated: Phase 0a (mod additions) blocks the rest. Phase 0b
(spawn build) is non-blocking and user-paced; all systems can be
built and verified before spawn exists.

KubeJS file layout documented for clean implementation:
  pack/overrides/kubejs/server_scripts/economy/{01..99}_*.js

11 open decisions resolved. 3 remain non-blocking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 21:09:45 +00:00
Matt 0c591faf14 Merge pull request 'docs: quest tracking + cancel UX' (#61) from docs/economy-quest-tracking into main 2026-06-06 20:51:32 +00:00
Matt 63e9185bf8 docs: add quest tracking + cancel UX to economy design
Three-layer visibility plan:
  V1 (Phase 3): /quests, /quests info, /quests cancel commands.
                Mobile-friendly via the bridge, no extra mods.
  V2 (Phase 3.5): Quest Log book item with dynamic pages.
  V3 (later): optional sidebar HUD via vanilla scoreboards.

3-slot active-quest cap so cancellation is a real choice:
  - Prevents accept-all-then-cancel-easy-ones RNG shopping
  - Keeps state files small
  - READY frees the slot so turn-in backlogs are workable

State storage schema documented (active + completed/cooldown).

Phase 3.5 added to the roadmap for the book item.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:51:14 +00:00
Matt 5bd1ace6aa Merge pull request 'docs: economy & quest system design (v0.1)' (#60) from docs/economy-system-design into main 2026-06-06 20:48:13 +00:00
Matt 1af2b9a86f docs: economy & quest system design (v0.1 draft)
Full design for the unified spawn economy:

- Seven themed guild buildings (Adventurer's, Merchant's, Civic,
  Library, Tavern, Foundry, Mint)
- Universal accept → complete → return quest pattern
- Villagers moved fully to spurs with rebalance principles +
  initial price guidance
- Money flow architecture (bounded inflows, scaling sinks)
- Phasing plan (8 phases, each independently shippable)
- Mod stack confirmed (we have everything we need for V1)
- Risk register + open decisions to make before implementation

Status: design, not yet implemented. Phase 0 (spawn skeleton)
is the next concrete deliverable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:48:12 +00:00
Matt 678e63497c Merge pull request 'pack: v0.31.2 — proper JEI hide via c:hidden_from_recipe_viewers tag' (#59) from fix/v0.31.2-hidden-from-recipe-viewers-tag into main 2026-06-01 19:48:37 +00:00
Matt 2710dce479 pack: v0.31.2 — proper item hiding via c:hidden_from_recipe_viewers tag
Stop hacking around the problem -- use the standard cross-recipe-
viewer hide convention that both NeoForge and JEI define.

Source-level confirmation:

  mezz/JustEnoughItems @ 1.21.1
    CommonApi/.../api/constants/Tags.java
      HIDDEN_FROM_RECIPE_VIEWERS = c:hidden_from_recipe_viewers

    Library/.../ingredients/IngredientVisibility.java
      isIngredientVisible() calls
      ingredientHelper.isHiddenFromRecipeViewersByTags(typedIngredient)
      -> any item carrying this tag is hidden from JEI.

  neoforged/NeoForge @ 1.21.x
    common/Tags.java
      Tags.Items.HIDDEN_FROM_RECIPE_VIEWERS = c:hidden_from_recipe_viewers
      -> same tag, NeoForge agrees with JEI.

  EMI / REI also respect this tag (common convention).

So the proper modpack-shippable fix is a single datapack file:

  pack/overrides/world/datapacks/bns-fuel/data/c/tags/item/
    hidden_from_recipe_viewers.json
  {
    "replace": false,
    "values": [
      "alexsmobs:shattered_dimensional_carver",
      "alexsmobs:transmutation_table"
    ]
  }

No mod, no KubeJS, no CraftTweaker, no Item Obliterator JEI integration
(which doesn't exist for 1.21.1 NeoForge anyway). Just the tag the
recipe-viewer ecosystem agreed on years ago.

Item Obliterator + Necronomicon stay in the pack for what they actually
deliver on 1.21.1: inventory-pickup block + villager-trade removal +
right-click cancellation. The Capsid recipe override stays too as the
creation block.

The full disable stack now:
  - Tag (this commit)         -> Hidden from JEI / REI / EMI
  - Datapack capsid recipe    -> Can't be created in-world
  - Item Obliterator          -> Can't be picked up / right-clicked
                                 if obtained via /give or creative

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 19:48:36 +00:00
Matt 99aa11c94d Merge pull request 'pack: v0.31.1 — restore Capsid recipe override (Shattered Carver UX fix)' (#58) from fix/v0.31.1-restore-capsid-override into main 2026-06-01 19:34:34 +00:00
Matt 0cd43c8f2d pack: v0.31.1 — restore Capsid recipe override (Shattered Carver)
I removed this datapack override in v0.31.0 on the (incorrect)
assumption that Item Obliterator would also handle Alex's Mobs'
custom capsid_recipes recipe type. It doesn't -- Item Obliterator
intercepts vanilla and some common modded recipes, but the Capsid
uses its own SimpleJsonResourceReloadListener bound to the
"capsid_recipes" folder, and that listener still loaded the
default shipped recipe.

The visible bug: a player could put a regular Dimensional Carver
into a Capsid -- the recipe matched, the Dimensional Carver was
consumed, and the Capsid spat out a Shattered Dimensional Carver.
Item Obliterator then refused the pickup. Net result:
  - Player loses their hard-earned regular Carver
  - Player can never pick up the Shattered one
  - World has an unrecoverable floating item entity

Restore the override at:
  pack/overrides/world/datapacks/bns-fuel/data/alexsmobs/
    capsid_recipes/shattered_dimensional_carver.json

This swaps the recipe's input from `alexsmobs:dimensional_carver` to
`minecraft:barrier`, which is unobtainable in survival. The recipe
loads cleanly but can never match. The Capsid transformation for
the Shattered Carver effectively no longer exists.

Item Obliterator still handles the JEI / creative tab / inventory /
trade removal -- both layers complement each other.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 19:34:33 +00:00
Matt 7acd3f4da8 Merge pull request 'pack: v0.31.0 — Item Obliterator replaces v0.30.x hacks' (#57) from feature/v0.31.0-item-obliterator into main 2026-06-01 19:21:41 +00:00
Matt 1b9e35efea pack: v0.31.0 — replace hacks with Item Obliterator
Replace the v0.30.x ad-hoc disable mechanisms (KubeJS server script,
datapack recipe override, KubeJS client-side JEI hide -- which was
broken on this KubeJS build) with a single proper tool:

  - **Item Obliterator 2.3.0** + dep **Necronomicon API 1.6.0**
    Declarative JSON config to remove items from inventories,
    creative tabs, all recipes (including modded custom recipe
    types), villager trades, and JEI. Exactly the missing capability.

Config: pack/overrides/defaultconfigs/item_obliterator.json5
  Blacklists:
    alexsmobs:transmutation_table
    alexsmobs:shattered_dimensional_carver

Removed (no longer needed):
  - pack/overrides/kubejs/server_scripts/disable_alexsmobs_features.js
    (Item Obliterator handles transmutation_table recipe removal
    and right-click cancel automatically)
  - pack/overrides/world/datapacks/bns-fuel/data/alexsmobs/
      capsid_recipes/shattered_dimensional_carver.json
    (Item Obliterator handles modded recipe removal too)

Both items now correctly:
  - Don't appear in JEI search / item list
  - Don't appear in creative tabs
  - Have no working recipe path
  - Can't be right-clicked / used
  - Get scrubbed from inventories on join

The Void Worm boss + regular Dimensional Carver still work --
nothing else in Alex's Mobs is affected.

Verified live: both mods discovered, Necronomicon read our config
file successfully ("File exists, reading config"), Item Obliterator
loaded with the blacklist.

Note: config schema is v2; v1 configs get overwritten by the mod's
default. Always write configVersion: 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 19:21:41 +00:00
Matt f88f6eaaca Merge pull request 'pack: v0.30.4 — drop broken JEI-hide client script' (#56) from fix/v0.30.4-drop-broken-jei-hide into main 2026-06-01 19:15:19 +00:00
Matt 3912ec541d pack: v0.30.4 — drop the broken JEI-hide client script
The hide_disabled_items.js client script I added in v0.30.3 used
`JEIEvents.hideItems(...)` -- a namespace that does NOT exist in
KubeJS 2101.7.2 on NeoForge 1.21.1. There is no kubejs-jei bridge
mod for 1.21.1 NeoForge published on Modrinth at this time.

Client launchers were showing:
  KubeJS client script errors
  hide_disabled_items.js#8
  ReferenceError: \"JEIEvents\" is not defined.

Removing the script entirely. The Shattered Dimensional Carver
still:
  - Cannot be produced (Capsid recipe override with barrier input)
  - Does nothing on use (server-side right-click cancel)

Tradeoff: the item will still appear as a recipe-less entry in JEI
search if players type its name. Cosmetic only; no gameplay path
to obtain or use it.

If a JEI-hide approach becomes available in a future KubeJS update
or via a new bridge mod, we can revisit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 19:15:19 +00:00
22 changed files with 3020 additions and 78 deletions
+622
View File
@@ -0,0 +1,622 @@
# bnstoolkit — Architecture & Perk Specification
> Status: design draft (v0.1). Not yet implemented.
> Owner: matt
> Last updated: 2026-06-06
`bnstoolkit` is the custom companion mod for the Brass and Sigil economy
system. It provides the UI layer (GUI screens, HUD), the FTB Chunks
map mixins for spawn plot integration, and the data models the economy
needs. All gameplay logic (bounty rotation, state machine, villager
trades, Numismatics integration, etc.) lives in KubeJS and is called
into by the mod via network packets.
This doc is the source of truth for the mod design. It must be
reviewed before any Java is written.
See also: [`economy-system.md`](./economy-system.md) for the gameplay
side of the design.
---
## 1. Purpose & scope
### In scope
- GUI screens built on FTB Library's UI framework (matches FTB Quests / FTB Chunks visual language)
- Mixins into FTB Chunks' map UI for spawn-plot cost overlay, hover tooltips, and click-to-purchase popups
- Custom HUD render layer for quest progress (replaces stacked vanilla bossbars)
- Data persistence for plot ownership (server-side, saved with world)
- Network packets for client↔server state sync
- Admin commands: `/bnstoolkit reload`, debugging commands
- Config-driven tier perks (`tiers.json`) and plot definitions (`plots.json`)
### Out of scope
- Bounty quest engine (lives in KubeJS)
- Villager trade rebalance (lives in KubeJS)
- Numismatics direct integration (lives in KubeJS — mod calls KubeJS commands to debit/credit)
- FTB Quests integration (FTB Quests handles its own UI for the progression campaign)
- FTB Ranks admin (FTB Ranks handles permissions directly; mod just reads them)
- **Sethome / `/back` / fast-travel features** — explicitly removed. The pack uses **Waystones** as the only travel mechanic. Restrictive travel is a design feature for a Create Aeronautics pack: airships, trains, and vehicles are the intended way to cover distance.
---
## 2. High-level architecture
```
┌──────────────────────┐
│ Player (client) │
└──────────┬───────────┘
│ keybind / NPC click / chunk click
┌───────────────────────────────────┐
│ bnstoolkit (client) │
│ • FTB Library UI screens │
│ • FTB Chunks map mixin │
│ • Quest progress HUD │
└───────────────┬───────────────────┘
│ network packets
┌───────────────────────────────────┐
│ bnstoolkit (server) │
│ • Plot ownership data │
│ • Sethome data │
│ • Network packet handlers │
│ • /bnstoolkit reload command │
└─────┬───────────────────────┬─────┘
│ │
reads │ │ calls KubeJS commands /
FTB Ranks / │ │ fires events for KubeJS
FTB Chunks / │ │ to react to
Numismatics ▼ ▼
┌───────────────┐ ┌──────────────────────┐
│ FTB / NeoForge │ │ KubeJS scripts │
│ stack │ │ • Bounty engine │
└───────────────┘ │ • Quest state │
│ • Numismatics calls │
│ • Villager trades │
│ • Tier promotion │
└──────────────────────┘
```
Key invariant: **the mod owns UI + plot data + sethomes. KubeJS owns
gameplay logic.** They communicate via network packets and KubeJS
commands. The mod never directly mutates Numismatics balances or quest
state — it asks KubeJS to do that via commands.
---
## 3. Package layout
```
bnstoolkit/
├── build.gradle NeoForge 1.21.1 + FTB Library dep
├── src/main/java/uk/sijbers/bnstoolkit/
│ ├── BnsToolkit.java Mod entry, lifecycle
│ ├── client/
│ │ ├── ClientEvents.java Keybinds, client setup
│ │ ├── screen/
│ │ │ ├── QuestLogScreen.java
│ │ │ ├── BountyBoardScreen.java
│ │ │ ├── TierUpgradeScreen.java
│ │ │ ├── PlotPurchaseScreen.java
│ │ │ ├── ShopBrowserScreen.java
│ │ │ └── widgets/ (subclass FTB Library widgets)
│ │ ├── hud/
│ │ │ └── QuestProgressHud.java IGuiOverlay
│ │ └── mixin/
│ │ ├── ChunkScreenPanelMixin.java
│ │ └── ChunkButtonMixin.java
│ ├── server/
│ │ ├── command/
│ │ │ └── BnsToolkitCommand.java /bnstoolkit reload, debug
│ │ ├── data/
│ │ │ ├── PlotRegistry.java loads plots.json
│ │ │ └── PlotOwnership.java SavedData, plot owner UUIDs
│ │ ├── event/
│ │ │ ├── PlayerJoinHandler.java tier-based join broadcast
│ │ │ └── ChatColourHandler.java tier-based chat name colouring
│ │ └── net/
│ │ └── (packet implementations — see §6)
│ ├── common/
│ │ ├── data/
│ │ │ ├── TierConfig.java loads tiers.json
│ │ │ ├── BountyDef.java DTO mirrored from KubeJS
│ │ │ ├── QuestStateView.java snapshot for UI display
│ │ │ └── PlotDef.java plot definition (region, price)
│ │ └── net/
│ │ └── BnsPacket.java network channel + payload base
│ └── resources/
│ ├── META-INF/neoforge.mods.toml
│ ├── bnstoolkit.mixins.json
│ └── assets/bnstoolkit/lang/en_us.json
└── src/main/resources/
└── data-config/ packed defaults, copied on first run
├── tiers.json
└── plots.json
```
---
## 4. Screen specifications
All screens extend FTB Library's `BaseScreen`. They share a common
header (player name, current tier, spur balance) so navigation feels
consistent.
### 4.1 Quest Log
**Opened by:** keybind (default Q+B?) or right-clicking a Quest Log
block placed at any guild building.
**Layout:** three tabs across the top — Active / Available / Completed.
- **Active tab:** list of currently-accepted bounties with progress
bars. Each row clickable; click expands a detail panel with
description, source guild, reward, and a `Cancel quest` button.
Cancel triggers a confirmation modal warning about cooldown.
- **Available tab:** list of bounties currently offered (rotated daily
by the KubeJS engine). Each row shows the bounty source, reward,
cooldown remaining (if any). Click → detail panel with `Accept` button.
- **Completed tab:** historical log of completed bounties this week,
for satisfaction value. No interaction beyond viewing.
**Network calls:**
- On open: client requests current quest state from server (`S2CQuestSnapshot`)
- On accept/cancel click: `C2SAcceptBounty(id)` / `C2SCancelQuest(id)`
### 4.2 Bounty Board
**Opened by:** right-clicking the Adventurer's Guild Quartermaster
NPC (Easy NPC dialog action runs `/bnstoolkit openboard adventurers_guild`)
OR clicking an in-world `bnstoolkit:bounty_board` block.
**Layout:** poster-board style — the day's available bounties as
visual "cards" laid out in a grid, each with mob icon, name,
description, reward, accept button. Friendly to non-tech users.
**Network calls:**
- On open: `S2CBountyBoardSnapshot(guild_id)` returns today's bounty list
- On accept: same `C2SAcceptBounty(id)` as Quest Log
### 4.3 Tier Upgrade Screen
**Opened by:** right-clicking the Civic Office Magistrate NPC.
**Layout:** full ladder shown as a vertical or horizontal track. Your
current tier highlighted (glowing badge). Next tier shows cost +
delta perks (e.g. "+50 chunks, +1 plot slot, +2 sethomes, +1 bounty,
-2% shop fee, chat colour: yellow"). Big `Upgrade to <Tier>` button
at the bottom, greyed out if you don't have the spurs.
Hovering any future tier shows its full perk list in a side panel.
**Network calls:**
- On open: `S2CTierSnapshot` returns current tier + balance
- On upgrade click: `C2SRequestTierUpgrade` → server verifies balance,
calls KubeJS `/bnstoolkit_internal upgrade_rank <uuid>` which
charges spurs and runs `ftbranks add <player> <tier>`
### 4.4 Plot Purchase Popup
**Opened by:** clicking an unowned plot region in the FTB Chunks map
UI (via the mod's mixin into `ChunkScreenPanel`).
**Layout:** modal overlay on top of the FTB Chunks map. Shows: plot
ID, plot type (standard/premium), region size in chunks, owner (none),
price. Your current spur balance. Your remaining plot slots (e.g.
"2 of 3 used"). `Purchase` and `Cancel` buttons. If you're at your
slot cap or don't have enough spurs, `Purchase` is greyed out with
the reason shown.
**Network calls:**
- On purchase: `C2SBuyPlot(plot_id)` → server checks slot capacity,
asks KubeJS to charge spurs, on success records ownership in
`PlotOwnership` SavedData.
### 4.5 Shop Browser
**Opened by:** right-clicking the Bazaar Master NPC at the Merchant's
Bazaar (provides a curated view of all currently-listed shop blocks
+ NPC sell shop).
**Layout:** searchable item list with current prices, sort by
category. Click an item to see who's selling it / where the shop
block is located. The NPC sell shop (diamonds, netherite etc.) shows
as a special "Official Bazaar" category with fixed prices and the
current diminishing-return multiplier per item.
**Note:** individual Numismatics shop blocks keep their native UI
when right-clicked — this Bazaar Master is the *curated central
view*, not a replacement for shop block interaction.
**Network calls:**
- On open: `S2CShopBrowserSnapshot` (asks KubeJS for current listings)
- Selecting an item → `S2CShopItemDetail(item_id)` for full info
### 4.6 Quest Progress HUD
**Render layer:** `IGuiOverlay`, drawn over the vanilla HUD.
**Layout:** small panel in top-right (configurable position) showing
each active quest as a row — quest name + progress bar + N/M numeric.
Fades on inactive (no progress in last 30s) so it doesn't visually
clutter when you're not actively bounty-hunting. Re-appears the
moment any quest progresses.
**State source:** KubeJS pushes updates via `S2CHudQuestUpdate` packet
when any quest's progress changes (debounced to once per second
maximum per quest). The HUD reads from a client-side cache.
---
## 5. Mixin targets
Minimal — we don't fork FTB Chunks, we inject only what we need.
| Target | Method | Purpose | Mixin file |
|---|---|---|---|
| `ChunkScreenPanel` | `drawBackground` (TAIL) | Overlay drawing — cost preview for the currently-hovered or selected chunk, "Your plot slots: N of M" text in a corner | `ChunkScreenPanelMixin.java` |
| `ChunkScreenPanel$ChunkButton` | `addMouseOverText` (TAIL) | Append "Cost: N spurs" or "Plot: <name>" to the per-chunk hover tooltip | `ChunkButtonMixin.java` |
| `ChunkScreenPanel$ChunkButton` | `onClicked` (HEAD, with cancellation if applicable) | If the clicked chunk is a plot region, open `PlotPurchaseScreen` instead of triggering a regular claim | `ChunkButtonMixin.java` |
That's it. Three injection points across two mixin files. No
modification of FTB Library, FTB Quests, FTB Ranks, or FTB Teams.
---
## 6. Network packet contract
NeoForge `PayloadRegistrar`. All packets versioned via a single
`PROTOCOL_VERSION` constant; bump on schema change.
### Server → Client
| Packet | Payload | When sent |
|---|---|---|
| `S2CQuestSnapshot` | `List<QuestStateView>` | On Quest Log open, on any quest-state change |
| `S2CBountyBoardSnapshot` | `List<BountyDef>` (5-10 entries) | On Bounty Board open |
| `S2CTierSnapshot` | `(currentTier, balance, perksByTier)` | On Tier Upgrade Screen open |
| `S2CShopBrowserSnapshot` | `List<ShopListing>` | On Shop Browser open |
| `S2CHudQuestUpdate` | `(questId, progress, target)` | On quest progress (debounced 1s) |
| `S2CPlotPurchaseResult` | `(success, message)` | After `C2SBuyPlot` resolves |
| `S2CTierUpgradeResult` | `(success, message)` | After `C2SRequestTierUpgrade` resolves |
### Client → Server
| Packet | Payload | What server does |
|---|---|---|
| `C2SAcceptBounty` | `questId` | Call `/bnstoolkit_internal accept_bounty <uuid> <questId>` (KubeJS) |
| `C2SCancelQuest` | `questId` | Call `/bnstoolkit_internal cancel_quest <uuid> <questId>` (KubeJS) |
| `C2SBuyPlot` | `plotId` | Verify slot capacity, ask KubeJS to debit spurs, record ownership |
| `C2SRequestTierUpgrade` | (none — server knows player) | Verify balance, ask KubeJS to debit + promote |
| `C2SOpenBoard` | `guildId` (optional context) | Reply with appropriate snapshot |
All C2S packets include the player implicitly (NeoForge passes
the `ServerPlayer` in the handler).
---
## 7. Data persistence
| Data | Storage | Lifetime |
|---|---|---|
| Plot ownership | `bnstoolkit:plots` — vanilla NeoForge `SavedData` attached to overworld | Persists with world. Authoritative source. |
| Quest state | KubeJS-managed JSON at `world/data/bns_economy/quests/<uuid>.json` | Not the mod's concern — the mod only reads via packets |
| Tier configuration | `config/bnstoolkit/tiers.json` (hot-reloadable) | Until edited |
| Plot definitions | `config/bnstoolkit/plots.json` (hot-reloadable via `/bnstoolkit reload`) | Until edited |
Plot ownership is server-authoritative. Clients only get views via
packets — they never write to these stores directly.
---
## 8. Configuration files
### `config/bnstoolkit/tiers.json`
Defines the 13-tier ladder. Hot-reloadable.
```json5
{
"tiers": [
{
"id": "peasant",
"displayName": "Peasant",
"cost": 0,
"perks": {
"wildernessChunks": 9,
"plotSlots": 0,
"dailyBountySlots": 3,
"shopFeeDiscount": 0,
"chatColour": "grey",
"joinBroadcast": "none"
}
},
/* ... 12 more tiers ... */
]
}
```
### `config/bnstoolkit/plots.json`
Defines spawn plot regions. Each plot has a unique ID, region bounds
(chunk coordinates), type (standard/premium), and price.
```json5
{
"plots": [
{
"id": "bazaar_stall_01",
"displayName": "Bazaar Stall 1",
"type": "standard",
"price": 2000,
"chunks": [[10, 20], [10, 21]], // chunk coords, inclusive
"world": "minecraft:overworld"
},
{
"id": "bazaar_corner_premium",
"displayName": "Premium Corner Plot",
"type": "premium",
"price": 5000,
"chunks": [[15, 25], [15, 26], [16, 25], [16, 26]],
"world": "minecraft:overworld"
}
]
}
```
---
## 9. KubeJS integration surface
The mod calls into KubeJS via custom commands registered by KubeJS
(`ServerEvents.commandRegistry`). All have the `bnstoolkit_internal`
prefix to mark them as not-for-player-use:
| Command | Purpose |
|---|---|
| `/bnstoolkit_internal accept_bounty <uuid> <questId>` | Mark quest as ACTIVE in KubeJS state |
| `/bnstoolkit_internal cancel_quest <uuid> <questId>` | Cancel quest, apply cooldown |
| `/bnstoolkit_internal complete_quest <uuid> <questId>` | Mark quest READY (for testing/manual) |
| `/bnstoolkit_internal turn_in_quest <uuid> <questId>` | Pay reward via Numismatics |
| `/bnstoolkit_internal debit <uuid> <amount> <reason>` | Charge spurs (used by plot purchase, tier upgrade) |
| `/bnstoolkit_internal credit <uuid> <amount> <reason>` | Pay spurs |
| `/bnstoolkit_internal upgrade_rank <uuid> <tierId>` | Promote via FTB Ranks |
| `/bnstoolkit_internal get_balance <uuid>` | Returns current spur balance (used for screen header) |
All commands return a JSON result via the command output that the
mod parses. KubeJS handles the actual Numismatics + FTB Ranks calls
(it already has the `Java.loadClass` helpers from the economy KubeJS
scripts).
KubeJS in turn pushes events to the mod via custom payload packets
when quest state changes (so the HUD can update without polling).
---
## 10. Build & release
- **Repo:** new private Gitea repo `minecraft/bnstoolkit` (separate
from the modpack repo for clean release-versioning, but mirrored
to the modpack's `pack/overrides/mods/` on each release)
- **Build:** standard NeoForge 1.21.1 ModDevGradle template
- **Dependencies:** Architectury API (provided), FTB Library
(provided), Numismatics (compileOnly), FTB Chunks (compileOnly)
- **CI:** Gitea Actions runs `./gradlew build` on push to main,
uploads the jar as a release artifact
- **Modpack integration:** the modpack's bump-version flow includes
pulling the latest bnstoolkit release jar and updating
`pack.lock.json` / manifest like any other mod
- **Versioning:** semantic — `1.0.0` for V1 ship, `1.x.y` for
patches, `2.0.0` if we ever change the network protocol or data
format
---
## 11. Perk specifications (detailed)
Every perk listed in the 13-tier ladder, expanded with mechanics,
implementation, and edge cases.
### 11.1 Wilderness chunks
**What it does:** caps how many wilderness chunks (outside spawn)
a player can claim via the FTB Chunks map. Claiming within the cap
is free; over the cap is refused with an in-game message.
**Implementation:** FTB Ranks permission node
`ftbchunks.max_claimed_chunks` set per rank in the FTB Ranks config
(written by the mod on first launch from `tiers.json`). FTB Chunks
reads this permission natively — no mod-side enforcement needed.
**Edge case:** if a player downgrades (which they can't normally,
but might happen via op command), they keep already-claimed chunks
over the new cap, but can't claim more until they free some.
### 11.2 Plot slots
**What it does:** caps how many spawn plots (predefined regions at
spawn, see `plots.json`) a player can own concurrently. Independent
of wilderness chunks.
**Implementation:** stored in `PlotOwnership` SavedData. On purchase,
mod checks `playerOwnedPlots.size() < tier.plotSlots`. If at cap,
the Plot Purchase Popup shows the slot count and disables the
`Purchase` button.
**Releasing a plot:** players can sell their plot back via a "Release"
button in the plot-detail screen (opens when they click their own
plot in the map). Releases the slot. Refund: 50% of purchase price.
### 11.3 Daily bounty slots
**What it does:** how many bounties a player can have ACTIVE
simultaneously. Once the slot is freed (by completion or cancel),
the player can accept another from any guild board.
**Implementation:** enforced in KubeJS bounty engine. The mod's
Bounty Board and Quest Log screens read the slot count via packet
and grey out the Accept button when at cap.
**Edge case:** a quest in READY state still counts against your
active slots until you turn it in. This is intentional — pushes
players to actually visit the NPC to claim.
### 11.4 Shop fee discount
**What it does:** reduces the "merchant cut" deducted when you sell
items at the Bazaar's NPC sell shop. The Bazaar charges a base 25%
fee on all sales; your tier provides a discount that reduces this
toward zero.
**Worked example:** you sell a diamond worth 10 spurs at the Bazaar.
- Peasant (0% discount): 25% fee → you receive 7.5 spurs (rounded
down to 7).
- Knight (-5% discount): 20% fee → 8 spurs.
- Sovereign (-25% discount): 0% fee → full 10 spurs.
**Implementation:** KubeJS sell-shop handler reads the seller's tier
and computes effective payout. Rounded down to nearest integer.
**Edge case:** player-to-player Numismatics shop blocks do NOT
apply this fee — those are direct peer transactions, no Bazaar
middleman. The fee only applies to the official Bazaar sell shop.
### 11.5 Chat colour
**What it does:** the player's name in chat appears in this colour,
visible to everyone. Visual rank marker.
**Implementation:** KubeJS `PlayerEvents.chat` event modifies the
chat component for the player's display name based on their tier
permission (read from FTB Ranks). The mod also colours the player's
nameplate above their head (via a small client packet on tier change
that other clients use to set the display name colour locally).
**Colour ladder:**
- 1-2 Peasant/Farmer: grey
- 3 Citizen: white
- 4 Merchant: yellow
- 5 Knight: green
- 6 Baron: cyan
- 7 Viscount: blue
- 8 Earl: purple
- 9 Marquess: gold
- 10 Duke: orange
- 11 Archduke: red
- 12 Grand Duke: bright red
- 13 Sovereign: rainbow (animated via colour-cycling text component)
**Edge case:** the Founder cosmetic `[Founder]` prefix sits before
the coloured name, in a distinct colour (probably bright yellow or
white-bold). Sovereign + Founder is visually busy but acceptable —
only one player on the server will have both.
### 11.6 Join broadcast
**What it does:** when the player logs in, this controls what message
goes to everyone else on the server.
**Levels:**
- **none:** standard vanilla `<player> joined the game`.
- **simple:** rank-prefixed message in their chat colour. Example:
```
[Knight] Matt has logged in.
```
- **fancy:** multi-line, with a separator. Example:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[Viscount] Matt has arrived
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
- **epic:** multi-line, with a sound effect (e.g. trumpet fanfare,
configurable per-tier). Larger ASCII separator. Possibly an ASCII
art banner of the rank.
- **epic + particles:** as epic, plus a 5-second particle effect at
the player's spawn location (firework spark, end-rod sparkle, etc.)
for everyone nearby to see.
- **epic + spectacular:** as epic + particles, plus a server-wide
effect — actionbar message visible to everyone, brief light-up of
the sky if Sovereign joins.
**Implementation:** KubeJS `PlayerEvents.loggedIn` reads tier and
dispatches the appropriate message. Particle/sound effects are
server-side commands (`/particle`, `/playsound`).
### 11.7 Future perk dimensions (not in V1, but easy to add later)
Architecture supports adding new perk columns in `tiers.json`
without code changes (the schema is open-ended). Candidates for
later expansion:
- **Waystone slot cap** — limit how many waystones a player can place
and/or own. Starter waystone is granted; higher tiers unlock 2nd,
3rd, etc. Cleanest in-theme perk for a Create Aeronautics pack
where restrictive travel is intentional.
- **Waystone XP cost reduction** — vanilla Waystones charges XP per
teleport. Tier-based discount on this cost (or eventual exemption
for Sovereign).
- **Particle trail** — high tiers leave a visible particle wake when
walking
- **Custom join sound** — different sound effect per tier on login
- **Custom title prefix** — text added before chat name beyond the
colour (e.g. `⚔ Knight Matt`)
- **Priority bounty access** — higher tiers see a larger pool of
daily bounties to pick from
- **Tier-gated vendor stock** — specific Numismatics shops only
visible to Baron+ etc.
- **Daily login bonus** — small spur reward for logging in, scaling
by tier
- **Death keep-inventory chance** — RNG % to keep inventory on death
(controversial, gate carefully)
---
## 12. Open implementation questions
These can be deferred until they become blocking, but worth flagging:
1. **Sovereign tier rainbow chat name** — Minecraft text components
don't natively support gradient text. Implementation options:
(a) cycle through colours character by character, or (b) animate
via packet on a tick interval. Choose during Phase 0c scaffolding.
2. **Plot release refund percentage** — proposed 50%. Tune via
playtest.
3. **Sethome cooldown values per tier** — proposed scaling from 60s
(low tiers) to 5s (Sovereign). Confirm in `tiers.json` schema.
4. **`/back` interaction with dimension changes** — does dying in
the End and `/back`ing return you to the End or to overworld?
Default: returns to wherever you died including the dimension.
5. **Founder cosmetic prefix colour** — bright yellow? Bold white?
Animated? Settle during implementation.
6. **Sovereign spectacular join effect** — exact effects need design
pass (sky flash? Server-wide actionbar text?). Tune in
`tiers.json`.
---
## 13. Implementation phases (mod-specific)
Aligned with the economy doc's Phase 0c (mod scaffolding):
| Mod phase | Deliverable |
|---|---|
| **M0** | Gradle project, mod metadata, FTB Library dep wired, network channel registered, empty placeholder for each screen class. Builds + loads cleanly into the running pack. |
| **M1** | Tier system: TierConfig loads from `tiers.json`, `TierUpgradeScreen` works, perks applied via FTB Ranks promotion command. Founder cosmetic prefix. Chat colour. Join broadcast at all tiers. |
| **M2** | Plot system: `PlotRegistry` + `PlotOwnership`, FTB Chunks mixins, `PlotPurchaseScreen`. KubeJS integration for spur debit. |
| **M3** | Quest UI: `QuestLogScreen`, `BountyBoardScreen`, `QuestProgressHud`. Network packets between KubeJS bounty engine and the mod. |
| **M4** | Shop browser: `ShopBrowserScreen`. |
| **M5** | Polish — animations, sound effects, particle effects for high tiers, theming pass on all screens. |
Each phase is independently shippable as a `1.x.0` release of the
mod jar.
---
## See also
- [`economy-system.md`](./economy-system.md) — gameplay-side design (currency, quest mechanics, villager rebalance, etc.)
- `MEMORY.md` entries — KubeJS Rhino gotchas, defaultconfigs pattern
@@ -0,0 +1,114 @@
# bnstoolkit feature-complete — testing notes for 2026-06-07 evening
> Shipped: pack v0.35.0 / bnstoolkit 0.3.1.
> Repo: http://10.16.5.102:3000/minecraft/bnstoolkit
> Server status: ✅ live + verified at 02:41 UTC.
## What's in the build
### 5 screens (FTB Library widgets — matches FTB Quests look)
All opened via key bindings. Rebindable in vanilla controls menu under
"Brass and Sigil" category.
| Key | Screen | What it does |
|---|---|---|
| **K** | Civic Tier ladder | 13 rows, current tier highlighted. Right pane shows next tier's cost + perks + Upgrade button. Disabled if you can't afford it or are at Sovereign. |
| **J** | Bounty Board | Today's 5-bounty pool + any active set. Detail pane: target, count, reward, progress. Accept / Cancel / Turn In buttons by state. |
| **N** | Plot Office | Plot list with ownership state-coding (yours / taken / free). Detail pane: size, chunks, price. Buy / Release buttons. |
| **H** | Bazaar shop | 9-cell sell item grid. Click an item, see DR-aware effective price for current tier. Sell 1 / 16 / 64 quick buttons. |
| **M** | Quest Log | At-a-glance status: tier + balance + active bounties (with progress bars) + owned plots. |
### HUD overlay
Top-left of the screen, always visible: `<tier-colour><tier-name>` on
one line, `<balance> spurs` on the second. Refreshes 1Hz from server
push — picks up coin pickups immediately, no inventory polling.
Auto-hides when F1 is pressed or chat is open (standard NeoForge HUD
layer behavior).
### Villager trade rebalance
Every emerald cost or result in vanilla + modded villager trades is
now Numismatics:
- 1..7 emeralds → spurs
- 8..63 → bevels (1 bevel = 8 spurs)
- 64+ → cogs (1 cog = 64 spurs)
Works automatically on:
- All 14 vanilla villager professions
- Wandering trader (generic + rare)
- Any modded villager professions (we wrap the event's list, no enum
of professions needed)
This was the "blocked on API" item. Built it into bnstoolkit instead
of pulling a third-party mod, because the cleanest Modrinth option
needed NeoForge 21.1.230+ and we're on 228.
## Testing path
```
/bns me # confirm Peasant tier 1
/give @s numismatics:spur 500 # give spurs for upgrades
K # open tier ladder
# click Upgrade — should pay 200 and bump you to Farmer
/bns admin tier set <you> 5 # jump to Knight for plot testing
K # confirm new tier visible
H # open bazaar
# click diamond, click Sell 1 (you'll need a diamond in inventory)
J # open bounty board, accept one
# kill the target mobs — progress shows in chat AND the screen
# turn in via the J screen
N # open plot office, buy p001
M # quest log shows everything
# Test villager trades:
/summon villager ~ ~ ~
# Right-click the villager once it picks a profession (place a workstation)
# Trades should show spurs/bevels/cogs instead of emeralds
```
## Architecture
```
KubeJS server_scripts/economy/ ← single source of truth for state + logic
│ (chat commands via server.getCommands)
ServerEconomyBridge.java ← reads NBT, sums Numismatics coins,
routes write verbs through /bns
↑↓
4 snapshot + 2 command packets
↑↓
ClientBnsState.java ← single static state holder
Screens read this each frame
```
Source of truth stays in KubeJS. Java is just the UI layer and a thin
write-path proxy. If you want to rebalance prices or add bounties, you
do it in KubeJS — Java auto-reflects.
## Known gaps for future iteration
| Gap | Where to fix |
|---|---|
| No client toast notifications — feedback comes via KubeJS chat messages | Add a dist-isolated `ClientToastDispatcher` class (0.4.0) |
| Bazaar grid doesn't tooltip the per-unit price | One `addMouseOverText` per `ItemCell` |
| Plot office doesn't show a minimap | Render bluemap PNG snippet as background or use FTB Chunks' map widget |
| Quest log doesn't show FTB Quests progression | Hook into FTB Quests' API once first chapter ships |
| Numismatics coins as bunch — no fractional change | Bazaar pays in highest denom that fits; player converts at the Bank |
| Wanderer rare-trade rebalance is greedy (every rare item costs are converted) | Profession-aware exclusion list if anything weird sneaks through |
## File map
| File | Purpose |
|---|---|
| `pack/overrides/mods/bnstoolkit-0.3.1.jar` | The mod itself (95 KB) |
| `pack/overrides/kubejs/server_scripts/economy/*.js` | State + business logic (unchanged from v0.33.0) |
| `pack/overrides/world/serverconfig/ftbranks/ranks.snbt` | 13-tier rank config (unchanged) |
| `docs/bnstoolkit-architecture.md` | Original architecture spec |
Source repo: http://10.16.5.102:3000/minecraft/bnstoolkit (main = 14dc69f at time of pack ship).
@@ -0,0 +1,215 @@
# Economy V1 — overnight build handoff
> Built 2026-06-06 / 23:00-23:55 UTC. All shipped via PR #67 (v0.33.0).
> Server running clean at port 25565. All scripts verified loaded with 0 errors.
## TL;DR — what to test tomorrow
A full command-driven economy is live. Pure-server (no client mod yet), works in chat and over the Telegram bridge equally. Open the in-game chat or telnet and try:
```
/bns help ← top of the iceberg
/bns me ← your status
```
That alone surfaces every command available. Below is the deeper walkthrough.
---
## What ships
### 1. 13-tier civic ladder (`/bns tier upgrade`)
Every player has a tier integer (1-13) in their NBT. New joiners are auto-set to tier 1 (Peasant). Upgrading costs spurs and runs `ftbranks add <player> <rank>` to apply the rank's permission nodes (chunk allowance, name format).
| # | Title | Cost to reach | Chunk allowance | Plot slots | Bounty slots | Shop fee |
|---|---|---|---|---|---|---|
| 1 | Peasant | 0 | 9 | 0 | 3 | 25% |
| 2 | Farmer | 200 | 18 | 0 | 3 | 25% |
| 3 | Citizen | 750 | 35 | 1 | 3 | 25% |
| 4 | Merchant | 2,500 | 60 | 1 | 4 | 23% |
| 5 | Knight | 6,500 | 100 | 2 | 4 | 20% |
| 6 | Baron | 15,000 | 150 | 2 | 5 | 18% |
| 7 | Viscount | 35,000 | 220 | 3 | 5 | 16% |
| 8 | Earl | 75,000 | 300 | 3 | 6 | 14% |
| 9 | Marquess | 150,000 | 400 | 4 | 6 | 12% |
| 10 | Duke | 300,000 | 525 | 5 | 7 | 10% |
| 11 | Archduke | 700,000 | 700 | 6 | 7 | 8% |
| 12 | Grand Duke | 1,500,000 | 900 | 7 | 8 | 6% |
| 13 | Sovereign | 10,000,000 | 1,500 | 10 | 10 | 0% |
**Test it:**
- `/bns me` shows your tier + balance + everything else
- `/bns admin tier set <player> <1-13>` to jump anyone instantly
- `/bns admin tier list` shows the full table in-chat
- `/give @s numismatics:spur 64` for testing money
- `/bns tier upgrade` pays + promotes
### 2. Bazaar sell shop (`/bns sell`)
Sell raw commodities for spurs. Tier discount reduces a flat 25% Bazaar fee. Diminishing returns lower the per-unit payout as you sell more of the same item over a lifetime (10% per 64 sold, floor 50%).
| Item | Base price |
|---|---|
| Diamond | 10 spurs |
| Netherite ingot | 100 |
| Emerald | 1 |
| Gold ingot | 2 |
| Iron ingot | 1 |
| Lapis | 1 |
| Redstone | 1 |
| Coal | 1 |
| Ancient debris | 500 |
**Test it:**
- `/bns sell list` — your effective prices right now (after fee + DR)
- `/bns sell minecraft:diamond 8` sells 8 diamonds
- Sell ~70 diamonds and watch the per-unit price drop as the DR kicks in
### 3. Bounty engine (`/bns bounty`)
20 bounties in the catalogue. Each player gets a random 5 from the pool, refreshed every 24h per-player. Slot cap = your tier (3 at Peasant, up to 10 at Sovereign).
States: AVAILABLE → ACTIVE → READY → COMPLETED → (24h cooldown) → AVAILABLE.
- Kill bounties auto-track via `EntityEvents.death` — you'll get progress messages in chat
- Cancelling applies the same 24h cooldown as completing (no RNG-shopping)
Highlights from the pool:
- Slay 10 Zombies — 15 spurs
- Hunt 5 Plunderers — 75
- Slay 3 Wither Skeletons — 150
- Slay a Skreecher — 400
- Slay the Warden — 800
- Slay a Warped Mosco — 700
- Slay the Farseer — 900
- Destroy a Void Worm — 1,200
**Test it:**
- `/bns bounty list` — what's on offer for you today
- `/bns bounty accept b_zombie_10` — accept
- Kill 10 zombies — progress chat messages every 5, ready notification at 10
- `/bns bounty active` — see your accepted with progress
- `/bns bounty turnin b_zombie_10` — claim 15 spurs
### 4. Plot system (multi-slot) (`/bns plot`)
Existing plot system (3 plots at chunks 10,10 / 12,10-11 / 14-15,10-11) now supports tier-based multi-ownership. Slot cap from your tier (0 at Peasant, up to 10 at Sovereign).
- `/bns plot list` — all plots, their state, prices
- `/bns plot buy <id>` — claim it (must be Citizen+ for at least 1 slot)
- `/bns plot release <id>` — 50% refund, opens slot
- `/bns plot mine` — list your owned plots
You'll need at least Citizen (tier 3) for any plot. **You can't buy a plot at Peasant.**
### 5. Waystones + welcome (pre-existing, untouched)
- One starter Waystone given on first login (per existing `welcome.js`)
- Waystone placement cap of 1 per player (per existing `waystones_policy.js`)
- Sharestones / Portstones banned via existing config
### 6. FTB Ranks 13-rank config
Live at `world/serverconfig/ftbranks/ranks.snbt`. Each rank has:
- `ftbchunks.max_claimed_chunks` (sets the FTB Chunks claim cap)
- `ftbchunks.max_force_loaded_chunks` (set to ~15% of claim cap)
- `ftbranks.name_format` (chat colour)
FTB Ranks auto-applies `peasant` to all players via `condition: always_active`. Tier-up commands set the rank explicitly. The `admin` rank with `condition: op` overrides everyone — you'll appear as Admin in chat regardless of your civic tier, useful for op-mode actions.
---
## What's deferred
### Villager trade rebalance
**Blocked.** KubeJS 2101.7.2 doesn't ship a villager-trade event. I verified by extracting the KubeJS jar — `ServerEvents` only exposes `command`, `loaded`, `recipes`, `registry`, `tags`, `tick`, `unloaded`. No `villagerTrades`. No KubeJS-villager-trade addon on Modrinth for NeoForge 1.21.1.
Options to unblock later:
1. **Add a third-party trade-overrides mod** — needs research (Villager Trade Tables-style mod for 1.21.1 NeoForge)
2. **Build into the custom mod** — KubeJS calls into Java mixin
3. **Mixin via a tiny standalone mod** — minimal Java project that just rebalances trades from a JSON
For now: vanilla villager trades remain. The 60+ trades I drafted live in the commit history (deleted file `pack/overrides/kubejs/server_scripts/economy/01_villager_rebalance.js`, restore from git when we unblock the API).
### Custom UI mod (bnstoolkit)
**Deferred to a session where you can iterate visually.** No Java/Gradle toolchain on glados, and the screens (QuestLog, BountyBoard, TierUpgrade, PlotPurchase, ShopBrowser, HUD, FTB Chunks mixin) all need visual feedback I can't get blind overnight.
The complete architecture spec is in `docs/bnstoolkit-architecture.md` — when you're ready to build, that's the source of truth. The KubeJS substrate I shipped tonight is exactly the backend the mod will sit on top of.
### Spawn build
Your manual task. The economy works from anywhere right now — no spawn buildings needed.
### FTB Quests progression chapters
Needs your arc design (themes, milestones). Mod is installed; the campaign content isn't.
---
## Files added/changed (PR #67)
```
pack/overrides/kubejs/server_scripts/economy/
00_tier_system.js NEW (170 lines)
02_sell_shop.js NEW (197 lines)
03_bounty_engine.js NEW (347 lines)
04_player_commands.js NEW (146 lines)
pack/overrides/kubejs/server_scripts/
plots.js MODIFIED (+86 lines — multi-slot)
pack/overrides/world/serverconfig/ftbranks/
ranks.snbt NEW (122 lines — 13 ranks)
pack/pack.lock.json version bump → 0.33.0
```
## Quick recovery if something goes wrong
If a script errors at startup, KubeJS logs to:
```
/srv/fast/brass-sigil-server/server/logs/kubejs/server.log
```
To hot-reload KubeJS server scripts without a server restart:
```
/kubejs reload server_scripts
```
To revert this PR entirely if it bricks something:
```
cd /home/matt/dev/brass-and-sigil
git revert <PR-67-merge-commit-hash>
git push origin main
```
The next pack-version-sync at MC server restart will undo all the changes.
## Suggested testing path
1. `/bns me` — sanity check, you should be Peasant tier 1
2. `/give @s numismatics:spur 500` — give yourself spurs
3. `/bns tier upgrade` — should pay 200 and bump you to Farmer
4. `/bns tier upgrade` again — should pay 550 (the difference up to Citizen at 750) … actually wait — the cost is per-tier-to-reach. Each upgrade costs the NEXT tier's listed cost. So Peasant → Farmer = 200, Farmer → Citizen = 750, Citizen → Merchant = 2,500. **Worth balance-checking in playtest.**
5. `/bns admin tier set <you> 13` — jump to Sovereign for testing
6. `/bns me` — should show Sovereign + 10 plot slots + unlimited everything
7. `/bns plot list` — should show 3 plots available
8. Stand in the relevant chunks (e.g. chunk 10,10), `/bns plot buy p001`
9. `/bns bounty list` — random 5 from pool
10. Accept one, kill enough mobs, turn in
11. `/bns sell list` — see effective prices
12. `/bns sell minecraft:diamond 4` (if you have diamonds)
If any step fails, the KubeJS log tells you why. Ping me with the error and I'll patch tomorrow.
---
## What I'd build next session
In priority order:
1. **bnstoolkit mod scaffolding (M0)** — need to install Java/Gradle on glados first. Empty Gradle project that builds + loads cleanly. Probably 4-6 hours of focused work.
2. **TierUpgrade GUI** (M1) — first screen in the mod, replaces the `/bns tier upgrade` text flow with a visual ladder + Upgrade button.
3. **BountyBoard GUI** (M3, after M0 is done) — replaces the chat-based `/bns bounty list/accept` with a poster-board screen.
Or — if you'd rather playtest the command-driven version first and only build the UI once the mechanics feel right — that's a perfectly defensible choice. The CLI version is fully functional.
+517
View File
@@ -0,0 +1,517 @@
# Brass and Sigil — Economy & Quest System Design
> Status: 13-tier ladder locked (v0.4). Architecture + UI confirmed in v0.3.
> Owner: matt
> Last updated: 2026-06-06
---
## 1. Goals
- **One unified currency** — spurs (Numismatics).
- **Multiple ways to earn, each one bounded.** No infinite money printer.
- **Multiple sinks that scale with progression.** Money disappears as you advance, so wealth never dwarfs the cost of new ambitions.
- **Spatial design that gives spawn personality.** Themed guild buildings, each with a clear purpose, each tied to the same wallet.
- **Forward-compat with a proper quest line** via FTB Quests, while supporting daily/repeatable activities at spawn.
- **Anti-automation by design**, not by patches.
---
## 2. Core architecture
```
┌────── MONEY IN (bounded) ──────┐
│ │
Adventurer's Guild — Daily bounties (cooldowns, scaling)
Merchant's Bazaar — Sell shop (fixed + DR), dynamic exchange
Mint — Capped emerald → spur conversion *
First-time milestones — One-shot per player
FTB Quests — Progression milestone payouts
┌───────────────────┐
│ Player wallet │ ← Numismatics
│ (spurs) │
└───────────────────┘
┌────── MONEY OUT (scaling) ──────┐
│ │
Civic Office — Plot claim costs (FTB Chunks override)
Civic Office — Tier upgrades (FTB Ranks integration)
Merchant's Bazaar — Player-to-player job board (no minting)
Villagers — Re-priced trades, net sink
Custom services — Warps, sethomes, premium cosmetics
```
\* Only if we keep emeralds; see §5 — we're moving villagers to spurs, which probably retires the Mint as a currency-conversion station and repurposes it as a wallet/info hub.
---
## 3. Spawn — themed guild buildings
Each guild is a destination. The guild's NPC is both the **entry point** (accept work) and **exit point** (return to claim reward). This pattern applies universally — see §4.
| Building | Purpose | Primary mechanics inside |
|---|---|---|
| **Adventurer's Guild** | Combat & exploration | Bounty board (daily mob hunts), boss-kill quests, dungeon-delve quests (future) |
| **Merchant's Bazaar** | Trade & raw-mat economy | Numismatics sell shop, dynamic exchange counter, player-driven job board |
| **Civic Office** | Land & status | Tier upgrade screen, spawn plot registry (purchase / view your plots), sethome/warp services. NOTE: regular wilderness chunk claiming uses the FTB Chunks map and is tier-allocated, not paid per chunk — see §4. |
| **Library / Academy** | Knowledge & magic | FTB Quests anchor NPC, Ars Nouveau themed quests, lore terminals |
| **Tavern** | Social & onboarding | First-time newbie quests, casual NPCs, food/buff vendor |
| **Foundry / Workshop** | Crafting & commissions | Create-themed contracts, crafting orders, raw-material market |
| **Mint** | Currency information | Wallet info, currency tutorial, possibly emerald exchange |
### Forward-compat with a full quest line
The architecture is naturally dual-track:
- **FTB Quests** = the *progression* backbone — chapter-by-chapter pack content, one-time rewards, story arcs. The campaign.
- **Spawn-building NPCs** (Easy NPC + KubeJS) = the *daily/repeatable* layer — bounties, sell shops, services. The live activities.
Both pay into the same wallet. Players run the campaign at their pace and the daily content for income/replay value. They reinforce each other rather than compete.
---
## 4. Tier system & spawn plots
Two distinct land/status systems that often get conflated. Keeping them clean:
### 4a. Civic tier ladder
Every player has a Civic rank. Higher rank unlocks perks. Players spend spurs at the Civic Office NPC to upgrade. Powered by FTB Ranks under the hood (permission nodes drive what each rank unlocks).
The ladder is intentionally long (13 tiers) so endgame ranks feel rare and aspirational. Top tiers represent months of dedicated play; Sovereign is "you've finished the game."
| # | Title | Cost to reach | Wilderness chunks | Plot slots | Daily bounties | Shop fee | Chat colour | Join broadcast |
|---|---|---|---|---|---|---|---|---|
| 1 | **Peasant** | free (starter) | 9 | 0 | 3 | 0% | grey | — |
| 2 | **Farmer** | 200 | 18 | 0 | 3 | 0% | grey | — |
| 3 | **Citizen** | 750 | 35 | 1 | 3 | 0% | white | — |
| 4 | **Merchant** | 2,500 | 60 | 1 | 4 | -2% | yellow | — |
| 5 | **Knight** | 6,500 | 100 | 2 | 4 | -5% | green | simple |
| 6 | **Baron** | 15,000 | 150 | 2 | 5 | -7% | cyan | simple |
| 7 | **Viscount** | 35,000 | 220 | 3 | 5 | -9% | blue | fancy |
| 8 | **Earl** | 75,000 | 300 | 3 | 6 | -11% | purple | fancy |
| 9 | **Marquess** | 150,000 | 400 | 4 | 6 | -13% | gold | fancy |
| 10 | **Duke** | 300,000 | 525 | 5 | 7 | -15% | orange | epic |
| 11 | **Archduke** | 700,000 | 700 | 6 | 7 | -17% | red | epic |
| 12 | **Grand Duke** | 1,500,000 | 900 | 7 | 8 | -19% | bright red | epic + particles |
| 13 | **Sovereign** | 10,000,000 | 1,500 | 10 | 10 | -25% | rainbow | epic + spectacular |
**Travel philosophy:** this is a Create Aeronautics pack — the journey is the point. We deliberately do **not** give players sethomes, `/back`, or other instant-teleport conveniences. The intended travel system is **Waystones** (mod already in pack):
- Players get **one waystone** at the start of the game. It anchors their base.
- They can teleport between spawn ↔ their own waystone(s).
- Beyond that, real travel means building airships, trains, vehicles — the actual gameplay.
This is restrictive by design. We can layer **waystone-related perks** into the tier ladder in a later revision (e.g. "tier N unlocks 2nd waystone slot", or "tier M reduces waystone XP cost"). For V1 the ladder stays as above and travel pressure remains intentionally high.
**Notable shape choices:**
- **Knight (tier 5)** is the gentry threshold — first colored chat (green), first join broadcast, meaningful jump in plot slots
- **Duke → Archduke jump** is the late-game wall (300k → 700k, ~2.3×) — Archduke is the start of "I've really committed to this server" territory
- **Sovereign is dramatic** — 6.7× the Grand Duke cost. Real "finished the game" status. -25% shop fee is a step-change leap; spectacular join effects.
Numbers are first-pass calibration. The full perk table lives in a single JSON (`config/bnstoolkit/tiers.json`) that's hot-reloadable via `/bnstoolkit reload`. Easy to tune any column without code changes.
### 4a.1 Founder flag (admin cosmetic)
The server owner / admin has a permanent **`[Founder]`** cosmetic prefix in chat that sits alongside their normal rank. It carries no gameplay perks — it's just visible identification so friends know who runs the server. The Founder still grinds the regular tier ladder like everyone else (good for playtesting balance + sharing the experience). Op commands are the actual admin tool; the Founder tag is purely visual.
**Wilderness claims:** at every tier above Peasant, you get a flat allowance of N chunks you can claim anywhere outside spawn via the FTB Chunks map. Claiming is **free** within your allowance, refused above. No per-chunk pricing. Driven by FTB Ranks → FTB Chunks permission nodes (`ftbchunks.max_claimed_chunks`).
### 4b. Spawn plots
Spawn plots are a **separate system** from wilderness claims.
- **Pre-defined regions** at spawn — single chunks or small fixed clusters — designated by admins as "plots." Visible as fenced/bordered areas on the spawn map.
- **Purchased with spurs** at additional cost per plot (separate from tier upgrade cost). Indicative pricing: standard plot ~2,000 spurs, premium (corner / prime location) ~5,000 spurs.
- **Primary use:** shops. Players set up Numismatics shop blocks on their plots and sell to other players. Residence is allowed but secondary.
- **Slot-limited by tier:** your civic rank determines how many spawn plots you can own simultaneously (column above). Hit the limit, you can't buy another until you upgrade or release a plot.
- **Purchase happens inside the FTB Chunks map UI** — clicking a plot region in the map opens a confirmation screen (provided by our custom mod's mixin) showing cost + your remaining slots. Same UI you use for wilderness claims, integrated.
- **Plot data lives in the custom mod** (`bnstoolkit:plots`), not in FTB Chunks. Plots are a separate ownership system that uses the FTB Chunks map as its visual surface.
---
## 5. Quest & bounty flow (universal pattern)
**Core principle:** for any guild, the player **accepts** work from an NPC, **completes** it in the world, then **returns** to that NPC to **turn it in** and claim the reward. No instant payouts. This:
- Anchors guilds as destinations
- Creates risk/reward tension (die on the return, lose the reward)
- Encourages waypoint use and travel
- Lets the server centrally control payouts and detect cheating
### State machine
Each quest, per player, lives in one of these states:
```
AVAILABLE ─[player accepts]─► ACTIVE ─[objective met]─► READY ─[turned in]─► COMPLETED
▲ │
└──────────────────────[cooldown elapses]───────────────────────────────────────────┘
```
- **AVAILABLE** — NPC offers the quest in dialog. Player has no current copy of it.
- **ACTIVE** — Player accepted. KubeJS tracks objective progress via relevant events (`EntityEvents.death` for kills, `ItemEvents.firstRightClicked` for explorations, etc.).
- **READY** — Objective met. Reward is *promised but not yet paid.* NPC dialog now offers a turn-in option.
- **COMPLETED** — Reward paid out. Quest goes on cooldown for this player.
- **AVAILABLE** (again) — after the cooldown, the NPC will offer it again.
### Worked example — daily bounty
1. **Adventurer's Guild NPC** has 5 randomized daily quests (e.g. "Slay 10 Plunderers — 50 spurs").
2. Player right-clicks NPC → Easy NPC dialog → "Accept: Slay 10 Plunderers."
3. KubeJS writes `quest:plunderer_10` to the player's quest state with `count: 0`.
4. Player kills Plunderers in the world. `EntityEvents.death` for `supplementaries:plunderer` increments the count.
5. At count=10, KubeJS flips state to **READY** and notifies the player ("Return to the Adventurer's Guild to claim your reward.").
6. Player returns. Right-click NPC. Easy NPC dialog detects READY state and offers "Turn in: Slay 10 Plunderers."
7. KubeJS pays 50 spurs into the player's Numismatics wallet, flips state to **COMPLETED**, sets cooldown to 24h.
### Universal applicability
The same accept → complete → return pattern works for every guild:
| Guild | Accept | Complete | Return |
|---|---|---|---|
| **Adventurer's** | "Hunt 10 plunderers" | kill them | NPC pays bounty |
| **Merchant's** | "Procure 32 diamonds for our buyer" | gather them | NPC pays inflated price + completes |
| **Civic** | "Survey 5 unclaimed chunks" | walk through them | NPC pays survey fee |
| **Library** | "Recover this lost tome" | find the book in a structure | NPC pays + grants reputation |
| **Foundry** | "Craft 10 Andesite Casings" | craft them | NPC pays commission |
### Player-facing UI (locked)
Polished, integrated, no chat commands to memorise. All player interaction happens through GUIs and NPC dialogue. Custom companion mod (working name `bnstoolkit`) provides the screens; FTB Library's UI framework is the visual basis so our screens match FTB Quests, FTB Chunks, and FTB Teams natively.
| Surface | Implementation |
|---|---|
| **Quest Log screen** | Custom mod screen. Tabs: Active / Available / Completed. Click a quest to inspect, cancel from the detail panel. Opened via keybind or by clicking a Quest Log block at any guild. |
| **Bounty Board screen** | Custom mod screen, opened by right-clicking the Adventurer's Guild Quartermaster NPC OR clicking the in-world Bounty Board block. Lists today's available daily bounties with descriptions, rewards, and "Accept" buttons. |
| **Tier upgrade screen** | Custom mod screen, opened at the Civic Office NPC. Visual ladder, your current tier highlighted, next-tier cost, perks unlocked at each rank, big Upgrade button. |
| **Plot purchase popup** | Custom mod mixin into the FTB Chunks map UI. Hover a plot region → cost + slots tooltip. Click → confirmation popup. Owned plots also clickable to see plot info. |
| **Shop browser** | Numismatics shop blocks render their existing inventory UI by default; optional richer browser screen for the central Merchant's Bazaar inventory. |
| **Quest progress HUD** | Custom mod render layer — styled HUD element showing active quest names + progress bars. NOT stacked vanilla bossbars. |
| **NPC dialogue + buttons** | Easy NPC handles all NPC interactions: accept/turn-in, info, tutorials, "click for details" buttons. Players learn the system through NPC dialogue, not by reading docs. |
### Why custom mod, on FTB Library's UI framework
- **Visual consistency:** FTB Quests, FTB Chunks, FTB Teams all use FTB Library's widget framework. Our screens built on the same framework will look and feel like part of the same product.
- **Already a pack dependency:** no new mod added just for the UI layer.
- **Mixin compatibility:** our spawn plot mixin into the FTB Chunks map operates on FTB Library widgets natively — no translation layer between vanilla MC widgets and FTB Library widgets.
- **Less boilerplate than vanilla MC Screen API:** Panel, BlockButton, IconButton, Tooltip, TextField widgets already exist and behave consistently with how players already know FTB UIs to work.
- **Forking FTB is off the table:** FTB mods are "All Rights Reserved" — depending on FTB Library as a library is fine (that's what libraries are for), forking FTB Quests or FTB Chunks for redistribution is not legally clean. Custom mod alongside the FTB stack is the correct architecture.
### Why NOT FTB Quests for daily bounties
FTB Quests stays for **long-form progression** (Phase 5+), not for daily bounties. Source review confirms:
- No abandon/cancel event (Ctrl+Shift+R reset fires no event we can hook)
- Cooldown is per-team, not per-player
- Visibility is per-team, not per-player — rotating "5 of 20 daily" doesn't fit cleanly
For long-form progression where one-shot rewards + team scope are fine, FTB Quests is great. Our custom bounty engine handles the daily/repeatable pattern with its own UI.
### Quest slot limit
Players have a **maximum of 3 active quests at a time.** This is the friction that makes cancel meaningful:
- Forces players to pick which quests they actually want — no "accept all 5, see what's easiest, cancel the rest" RNG-shopping.
- Keeps the state files small.
- Makes ready-to-turn-in queues meaningful (you can hold 3 unrewarded quests in your back pocket).
A quest moving to READY frees the slot for accepting a new one even before turn-in.
### KubeJS implementation — locked specifics
- **State storage:** per-player JSON in `world/data/bns_quests/<uuid>.json`. Schema:
```json
{
"active": [
{
"id": "advg_plunderer_10",
"acceptedAt": 1717684800,
"progress": 7,
"target": 10,
"rewardSpurs": 50,
"source": "adventurers_guild",
"bossbarId": "bns:quest_advg_plunderer_10_<uuid>"
}
],
"completed": {
"advg_plunderer_10": { "cooldownUntil": 1717771200, "completions": 3 }
}
}
```
- **Event tracking:** KubeJS event handlers (`EntityEvents.death`, `ItemEvents.pickedUp`, `BlockEvents.broken`) read the player's active quests and increment progress + update bossbar value.
- **Bossbar lifecycle:** on quest accept, KubeJS creates a unique per-player bossbar via `server.runCommandSilent('bossbar add <id> <name>')`, assigns it to that player only, updates `value` and `max` on progress. On turn-in or cancel: `bossbar remove`.
- **Numismatics integration (direct Java access — no events shipped, no KubeJS bindings):**
```js
const Numismatics = Java.loadClass('dev.ithundxr.createnumismatics.Numismatics');
const ReasonHolder = Java.loadClass('dev.ithundxr.createnumismatics.content.backend.ReasonHolder');
const acct = Numismatics.BANK.getAccount(player); // ServerPlayer
const balance = acct.getBalance(); // int (spurs)
acct.deposit(50); // pay quest reward
const ok = acct.deduct(100, ReasonHolder.empty()); // returns false if broke
Numismatics.BANK.markBankDirty(); // force persistence
```
- **Dialog branching:** Easy NPC dialog branches on scoreboard objective `bns.quest.<npc_tag>` that KubeJS mirrors quest state into. Dialog `Command` action triggers `/bns_quest accept <id> <npc_tag>` which routes to KubeJS.
- **Commands:** registered via `ServerEvents.commandRegistry` in KubeJS. Scoped to the player who runs them. The custom `/bns_quest` namespace handles NPC-triggered actions; `/quests` is the player-facing tracker.
- **FTB Chunks claim cost (Phase 2):** requires `FTB XMod Compat` mod. Then:
```js
FTBChunksEvents.before('claim', event => {
const player = event.source.player;
const acct = Numismatics.BANK.getAccount(player);
const cost = computeClaimCost(player);
if (acct.getBalance() < cost) {
event.setResult(ClaimResult.YOU_DO_NOT_HAVE_ENOUGH_SPURS); // custom enum value
return;
}
acct.deduct(cost, ReasonHolder.empty());
});
```
**Note:** the `.before` may fire twice (sim + real). Guard with idempotent state, or do balance check in `.before` and deduct in `.after('claim')`.
---
## 5. Villager rebalance
**Decision:** move all villagers to **spurs** (not emeralds). Thematic consistency over preserving vanilla mental model.
**Decision principles:**
- Villagers are **net money sinks** — players spend more on villagers than they earn from them.
- **Useful trades stay** — enchanted books, tools, food, decorative blocks. Reasonable spur prices.
- **Trash trades get deleted** — no one ever wanted to sell rotten flesh or buy paper.
- **Sell-to-villager trades exist but are bad** — `64 wheat → 1 spur`, `16 leather → 1 spur`. Farms still possible, but ~10× slower than any other income source.
### Price guidance (initial calibration — tune via playtesting)
#### Buy-from-villager (player pays spurs)
| Item | Tier-1 | Tier-3 | Tier-5 |
|---|---|---|---|
| Standard enchanted book (random) | 50 spurs | 120 spurs | 250 spurs |
| Mending book | — | — | 800 spurs |
| Diamond tool (unenchanted) | — | 80 spurs | — |
| Diamond tool (enchanted) | — | — | 350 spurs |
| Bookshelf | 15 spurs | — | — |
#### Sell-to-villager (player gets spurs)
| Item | Spurs per unit |
|---|---|
| Wheat | 1 per 64 |
| Paper | 1 per 32 |
| Leather | 1 per 16 |
| Iron ingot | 2 per 1 |
| Diamond | 8 per 1 *(matches sell-shop fixed price — players use whichever is closer)* |
### Scope
~60 trades × 5 profession tiers = ~300 trade entries to redesign. One-time data-design pass, implemented via `ServerEvents.villagerTrades` in KubeJS.
---
## 6. Money inflow sources (detailed)
| Source | Cap / DR mechanism | Rough rate |
|---|---|---|
| **Daily bounties** | Per-player per-quest cooldown (24h) | ~200-500 spurs/day for an active player |
| **Sell shop (fixed price + DR)** | Lifetime sales tracker per player, 10% price drop per 64 sold | ~50-200 spurs/hr while diamond stockpile lasts |
| **Dynamic exchange** | Global supply pool — price drops with sales | Self-balancing |
| **First-time milestones** | One-shot per player (`hasFlag`) | 5-50 spurs per milestone, ~10 milestones total |
| **FTB Quest progression** | One-time per quest | Varies; cluster rewards at chapter completion |
| **Villager sell trades** | Inherently slow + bad prices | ~50 spurs/hr from a serious villager farm |
---
## 7. Money outflow sinks (detailed)
| Sink | Cost curve | Why this scales right |
|---|---|---|
| **Plot claims** | Free up to 9 chunks. 50 spurs/chunk to 25. 200 spurs/chunk beyond. | New players claim free starter areas; ambitious players pay real money |
| **Tier upgrades** | Cumulative cost: 500 → 2k → 8k → 30k spurs | Endgame sink, status display |
| **Buy-from-villager** | See §5 | Constant outflow as you build up |
| **Premium cosmetics** | 100-2k spurs each | Optional sink for cash-flush players |
| **Services** | Sethome: 50 spurs, warp: 25 spurs, /back: 10 spurs | Convenience taxes |
---
## 8. Mod stack (locked)
### Already in pack
| Need | Mod | Version |
|---|---|---|
| Currency + wallets + shop blocks | Numismatics | 1.0.20 |
| NPC dialog presentation | Easy NPC | 6.16.x (6.17.0 available, minor bump) |
| Plot claims | FTB Chunks | 2101.1.14 |
| Team management | FTB Teams | 2101.1.9 |
| Core library | FTB Library | 2101.1.31 |
| Cross-mod compat | Architectury API | 13.0.8 |
| Glue, commands, state | KubeJS | 2101.7.2 |
### Adding in Phase 0a (mod-additions PR — blocks everything else)
| Mod | Version | Reason |
|---|---|---|
| **FTB Quests** | 2101.1.25 | Progression quest UI (Phase 5+) |
| **FTB XMod Compat** | latest 1.21.1 NeoForge | KubeJS hooks for FTB Chunks claim events + FTB Quests events |
| **FTB Ranks** | 2101.1.3 | Tier system. CurseForge only, no Modrinth listing. |
### Building our own (custom companion mod)
**`bnstoolkit`** (working name) — a small companion mod in our own repo. Confirmed locked-in.
- Provides all custom GUI screens (Quest Log, Bounty Board, Tier upgrade, Shop browser)
- Provides the FTB Chunks map mixins (spawn plot cost overlay, hover tooltip, purchase popup)
- Provides custom quest-progress HUD render layer
- Provides the `bnstoolkit:plots` data storage + ownership permissions
- Provides network packets for client↔server state sync
- Provides the `/bnstoolkit reload` admin command for hot-reloading tier config
- Built on FTB Library UI framework for visual consistency
- KubeJS handles all gameplay logic (bounty rotation, state machine, villager trades, Numismatics integration) — the mod is purely the UI + integration layer
Estimated scope: ~2-3k LOC, single-developer week of focused work. Bounded.
### Explicitly NOT adding
- CustomNPCs (Easy NPC covers it)
- Citizens (Bukkit-only)
- PMMO / skill mods (out of scope)
- Generic "quest giver" mods (FTB Quests + our KubeJS bounty engine cover it)
---
## 9. Phasing
Each phase is independently shippable. Earlier phases unlock value without depending on later ones.
| Phase | Deliverable | Spawn build required? |
|---|---|---|
| **0a** | **Mod additions PR:** FTB Quests + FTB XMod Compat + FTB Ranks (+ minor Easy NPC bump). New pack version. | No |
| **0b** | Spawn skeleton — 7 guild building blockouts (rough). **User does in-game when time permits.** Non-blocking. | Yes (user) |
| **0c** | **`bnstoolkit` companion mod — scaffolding pass.** Mod project set up, FTB Library UI dependency wired, network packet plumbing, build/release scripts. No screens yet. | No |
| **1** | **Sell shop:** Numismatics shop blocks for diamonds (10 spurs) + netherite ingots (100 spurs). Lifetime-sale tracker for diminishing returns. KubeJS only, no mod work needed. | No |
| **2** | **Tier upgrade flow:** FTB Ranks integrated + KubeJS `/tier upgrade` command. **`bnstoolkit` Tier upgrade screen** wired to Civic Office NPC. Tier perks driven by `tiers.json`. | No |
| **3** | **Spawn plot system:** plot region registry in `bnstoolkit`, ownership tracking, FTB Chunks map mixin (cost overlay + purchase popup). | No |
| **4** | **Bounty engine:** state machine, KubeJS-rotated dailies, `bnstoolkit` Bounty Board screen + Quest Log screen + Quest progress HUD. NPC dialog hooks via Easy NPC. 3-slot cap. | No (NPC layer activates once spawn is built) |
| **5** | **Villager rebalance:** strip vanilla, ship spur-priced trade table via `ServerEvents.villagerTrades`. | No |
| **6** | **Progression quest line:** FTB Quests anchor + first chapter. KubeJS rewards dispense spurs via Numismatics API. | No (anchor NPC at Library once spawn exists) |
| **7** | **Dynamic exchange rates:** global supply-pool pricing for raw mats. | No |
| **8+** | Lootr dungeons, Foundry crafting commissions, deeper FTB Quests chapters, player-vs-player bounties (if wanted). | TBD |
---
## 10. Risk register
| Risk | Likelihood | Mitigation |
|---|---|---|
| Bounty engine bugs out, players lose progress | Medium | Atomic state writes; logs of every state transition |
| Villager farms still profitable after rebalance | Medium | Track player sell volumes; tune prices in patches |
| KubeJS-EasyNPC dialog branching has limits | Low-Med | Verify hooks in a test world before Phase 3; fall back to command-driven NPCs if needed |
| FTB Chunks `/claim` command override gotchas | Low | Test in dev world; FTB Chunks API is stable |
| Plot cost feels punishing | Med | First 9 chunks free is the safety valve; tune from playtest |
| Players bypass the system via creative / `/give` | Op-only | Document op responsibility; consider audit logs for op spur grants |
---
## 11. Decisions made
Resolved before implementation:
| Question | Decision |
|---|---|
| Quest UI for bounties | **Custom mod with FTB Library UI widgets** — proper GUI screens, no chat commands |
| Quest UI for progression | FTB Quests |
| Plot purchase UI | Custom mod mixin into FTB Chunks map — hover preview, click-to-buy popup |
| Tier upgrade UI | Custom mod screen at Civic Office NPC |
| State storage | KubeJS-managed per-player JSON for quest state; mod-managed for plot ownership |
| Numismatics access | Direct Java via `Java.loadClass` from KubeJS; via API directly from the mod |
| Wilderness chunk allowance | Tier-driven via FTB Ranks permission nodes — no per-chunk pricing |
| Spawn plots | Predefined regions, slot-limited by tier, individually purchased with spurs |
| Tier ladder | Civic (13 tiers): Peasant → Farmer → Citizen → Merchant → Knight → Baron → Viscount → Earl → Marquess → Duke → Archduke → Grand Duke → Sovereign |
| Founder flag | Admin/owner has `[Founder]` cosmetic prefix in chat, grinds the regular ladder otherwise |
| Tier perks | JSON-configured (`config/bnstoolkit/tiers.json`), hot-reloadable |
| Custom mod | Confirmed. Built on FTB Library UI framework. Working name `bnstoolkit`. |
| Sethomes / warps | Tier-allocated count (not paid per use) |
| Daily quest count | Tier-driven (3 at Peasant, up to 6 at Sovereign), refresh per-player 24h after last accept |
| Emeralds | Retired as currency, retain as a trade material for villager-redirect trades |
| Fork FTB | Not needed — depend on FTB Library as a UI lib, FTB Chunks/Quests stay unmodified, integration via mixins + KubeJS hooks |
### Still to decide (non-blocking — pick during implementation)
- **PvP bounties** — out of scope for V1, revisit after V1 ships
- **Lootr** — add only when structured dungeons exist (Phase 8+)
- **Default-rank starting permissions** — finalised when FTB Ranks config is written
- **Spawn plot pricing** — first-pass numbers (2,000 standard / 5,000 premium), tune in playtest
---
## 12. Implementation notes
### KubeJS scripts (gameplay logic)
Organised under `pack/overrides/kubejs/server_scripts/economy/`:
- `01_numismatics_api.js` — Java.loadClass wrapper helpers (balance / deposit / deduct)
- `02_quest_state.js` — load/save `world/data/bns_economy/quests/<uuid>.json`
- `03_commands.js` — admin commands (`/tier upgrade`, `/bnstoolkit reload`, debug)
- `04_bounty_engine.js` — daily rotation, state machine, KubeJS↔mod packet handlers
- `05_sell_shop.js` — Numismatics shop tracking + diminishing returns
- `06_villager_trades.js` — Phase 5 `ServerEvents.villagerTrades` rebalance
- `07_tier_perks.js` — FTB Ranks promote/demote, perk application
- `99_dynamic_exchange.js` — Phase 7 supply-priced market
### Custom mod codebase
Repo: separate (or sub-tree of main repo, TBD).
Modid: `bnstoolkit` (working name).
Package layout:
```
bnstoolkit/
├── client/
│ ├── screen/ QuestLog, BountyBoard, Shop, Tier, PlotConfirm (FTB Library subclasses)
│ ├── mixin/ ChunkScreenPanel (cost overlay + plot button), ChunkButton (hover tooltip)
│ ├── hud/ QuestProgressHud (custom render)
│ └── ClientEvents keybind reg, screen reg, mixin loader
├── server/
│ ├── packet/ OpenScreen, UpgradeRank, BuyPlot, ClaimBounty, etc.
│ ├── command/ /bnstoolkit reload (config refresh)
│ └── data/ PlotRegistry, PlotOwnership (saved with world data)
├── common/
│ ├── data/ TierConfig (JSON-loaded), BountyDefinition, QuestState mirror
│ └── BnsToolkit.java mod entry
└── config-defaults/
├── tiers.json copied to config/bnstoolkit/tiers.json on first run
└── plots.json spawn plot region definitions
```
Architecture doc (`docs/bnstoolkit-architecture.md`) covers package layout, screen UX sketches, network packet contract, mixin targets, KubeJS↔mod integration surface — to be written before mod work begins.
### State data
- Quest state: `world/data/bns_economy/quests/<uuid>.json`. Readable JSON, easy to debug.
- Plot ownership: persisted with world via the mod's `SavedData` (vanilla NeoForge persistence). Authoritative source of truth.
- Tier config: `config/bnstoolkit/tiers.json` shipped from `pack/overrides/defaultconfigs/bnstoolkit/tiers.json`. Hot-reloadable.
- Plot definitions: `config/bnstoolkit/plots.json` shipped similarly. Defines plot region bounds, type (standard/premium), prices.
### NPC dialog
Authored in Easy NPC's in-game editor, exported as preset JSONs into `pack/overrides/world/datapacks/bns-fuel/data/easy_npc/`. Dialog `Command` actions call `/bnstoolkit <verb> ...` which routes to either the mod (for screen-opening) or KubeJS (for state changes).
### Villager rebalance
Single source file `06_villager_trades.js`. Each profession × tier has its own block. One file = one diff per balance pass.
---
## See also
- `MEMORY.md` entries:
- `project-mc-player-economy.md` — earlier higher-level notes on Numismatics + plot system
- `feedback-kubejs-rhino-gotchas.md` — KubeJS scripting constraints
- `feedback-bns-pack-version-sync.md` — pack version + sync workflow
- Existing tasks #20-#26 — to be reshuffled to mirror the phase plan above
+143
View File
@@ -0,0 +1,143 @@
# Spawn building setup — wiring NPCs to bnstoolkit screens
How to give players access to the Exchange / Bounty Board / Plot
Office / Civic Registrar via Easy NPC dialogue at spawn.
## Prerequisites (one-time, post-wipe)
```
/setworldspawn ~ ~ ~ # set the canonical world spawn
/bns admin spawn-init # creates Spawn (cyan) + Plots (gold)
# FTB Teams and claims existing plots
```
If you also want spawn protection: switch to the Spawn team and claim
your spawn town chunks via the FTB Chunks map (M key, left-click).
## The five screens you can expose
| Screen | Open command | What it does |
|---|---|---|
| Hub | `/bns open hub` | Status overview (tier, balance, bounties, plots) |
| Exchange | `/bns open exchange` | Wealth exchange — sell diamonds/netherite/ancient debris for spurs |
| Bounty Board | `/bns open bounty` | Today's 5 bounties + accept/cancel/turn-in |
| Plot Office | `/bns open plot` | Buy / release plots |
| Civic Tier | `/bns open tier` | Pay spurs to upgrade tier |
| Adventurer's Journal | `/bns open journal` | Status overview + active bounties + exchange rates |
`hub` and `journal` are status-only (read). The other four are
transactional — they should be gated behind an NPC at the relevant
spawn building.
## Per-building setup (Exchange example)
Repeat for each of the 4 transactional buildings, varying just the
screen name and NPC name.
### 1. Spawn the NPC
In creative mode, get an Easy NPC spawn egg:
```
/give @s easy_npc:custom_egg
```
Right-click the ground where you want the NPC. It spawns a customisable
NPC.
### 2. Configure appearance + name
Right-click the NPC → **Configuration** (or use the wrench).
- **Skin**: pick a fitting villager / human skin
- **Name**: "Exchange Clerk" (or whatever fits the building)
- **Profession**: optional — purely cosmetic
### 3. Configure dialog
In the NPC config → **Dialog** tab.
Create a new STANDARD dialog. Suggested text:
> Welcome to the Exchange. We exchange diamonds, netherite, and
> ancient debris for spurs deposited directly to your bank account.
> Care to make a trade?
Add a **Button** to this dialog:
- **Label**: "Open the exchange"
- **Type**: ACTION
- **Action**: COMMAND
- **Command**: `bns open exchange`
- **Execute as user**: ✅ (must be enabled)
- **Permission level**: 0 (no special permission needed)
- **Trailing action**: CLOSE_DIALOG (optional — closes the dialog after click)
Add another button "Goodbye" → CLOSE_DIALOG for the polite exit.
### 4. Configure trigger
In the NPC config → **Actions** tab:
- Event: **ON_INTERACTION**
- Action: **OPEN_DEFAULT_DIALOG**
Now when a player right-clicks the NPC, the dialog opens. Clicking
"Open the exchange" runs `/bns open exchange` as that player and
the Exchange screen pops up.
## Building-to-screen mapping
| Building | NPC name suggestion | Open command |
|---|---|---|
| Exchange | Exchange Clerk | `bns open exchange` |
| Adventurer's Guild | Bounty Officer | `bns open bounty` |
| Plot Office | Plot Clerk | `bns open plot` |
| Civic Hall / Town Hall | Civic Registrar | `bns open tier` |
| (anywhere — optional) | Quartermaster | `bns open hub` |
| (anywhere — optional) | Cartographer | `bns open journal` |
## Security gate (one-time, already shipped)
The Easy NPC config at `/srv/fast/brass-sigil-server/server/config/
easy_npc/security.cfg` has `executeAsUserCommandAllowList.ALL=bns`,
so dialogue actions can run `/bns ...` on the player's behalf. This
ships with the pack as of v0.36.0 — no manual edit required after
fresh installs.
## Testing your setup
1. Build the building
2. Place the NPC
3. Configure the dialog as above
4. Switch to survival, walk up, right-click the NPC
5. Click the action button
6. The screen should open on your client immediately
If nothing happens:
- Check chat for errors ("Blocked execute-as-user command root ...")
- If you see that, the security.cfg allowlist is missing `bns` — see
the security gate section above
- Otherwise: check the server log for `[bnstoolkit] /bns open <screen>`
to confirm the command actually fired
## What if I just want to test as op (no NPC)
You can always run `/bns open <screen>` directly from chat or console
to open a screen for yourself. The op-level requirement is none — the
command works for any player. NPCs are just the in-world surface.
## Pre-wipe testing checklist (current world)
Each feature, where you'd manually test it before the wipe:
| Feature | How to test |
|---|---|
| Hub | Press B → see status panel with tier/cash/bank/total/bounties/plots |
| Exchange | `/bns open exchange` → 4 items in grid (diamond, diamond_block, netherite_ingot, ancient_debris), inventory count badge bottom-right of each cell, fee + per-unit price in detail pane, Sell 1/16/64 buttons grey out when you don't have enough |
| Sell flow | `/give @s minecraft:diamond 64` → click diamond → Sell 16 → bank balance ticks up, sell button updates |
| Bounty Board | `/bns open bounty` → see today's pool, click a bounty for detail, accept, kill the targets, turn in |
| Plot Office | `/bns open plot` → see 3 default plots, buy p001 (tier 3+ required), check FTB Chunks map M — chunk should turn your team colour |
| Plot unclaim guard | After buying a plot, try to unclaim its chunk via FTB Chunks UI → should reject with redirect message |
| Civic Tier | `/give @s numismatics:spur 500` or `/bns wallet deposit ...``/bns open tier` → click Upgrade — pays from cash + bank combined |
| Wallet | `/bns wallet`, `/bns wallet deposit 50`, `/bns wallet withdraw 50` |
| Auto-bound card | Look in inventory — should already have a white_card bound to your account |
| Villager rebalance | Trade with a villager — emerald-side replaced with spurs/bevels/cogs |
| Quest Log auto-open population | Press B → click Journal → should show active bounties, rates, plots |
If any of these break, ping with the details. Pre-wipe is the time to find these bugs.
@@ -0,0 +1,50 @@
#Easy NPC Security Configuration
#
#Permission values use enum names: ALL, MODERATORS, GAMEMASTERS, ADMINS, OWNERS.
#Legacy numeric Minecraft command permission levels are accepted and rewritten to enum names.
#
#normalPlayerCommandLevel: Max command level for normal player-owned imports (default: ALL)
#creativePlayerCommandLevel: Max command level for creative non-admin imports (default: MODERATORS)
#maxAdminImportedCommandLevel: Max command level for admin imports (default: ADMINS)
#serverTrustedCommandLevel: Max command level for server-side trusted imports without player context (default: ADMINS)
#blockUnsafeNpcCommands: Blocks critical server management commands from NPC command execution (default: true)
#npcSpawnRateLimitCreative: Max new NPCs a creative (non-admin) player may spawn via browser per minute (default: 5)
#npcSpawnRateLimitAdmin: Max new NPCs an admin player may spawn via browser per minute (default: 20)
#Feature values use enum names: NORMAL_PLAYER, CREATIVE_PLAYER, ADMIN, SERVER_TRUSTED.
#feature.SPAWN_NPC: Minimum role required to spawn new NPCs from the preset browser (default: CREATIVE_PLAYER)
#feature.WORLD_PRESET: Minimum role required to import/export world presets (default: CREATIVE_PLAYER)
#feature.CUSTOM_PRESET: Minimum role required to import/export custom server presets (default: CREATIVE_PLAYER)
#feature.LOCAL_PRESET: Minimum role required to export presets locally to the client (default: NORMAL_PLAYER)
#feature.DEFAULT_PRESET_IMPORT: Minimum role required to import built-in default presets (default: CREATIVE_PLAYER)
#feature.URL_RESOURCE: Minimum role required to use URL-based skin loading (default: CREATIVE_PLAYER)
#Sat Jun 06 23:34:43 UTC 2026
blockUnsafeNpcCommands=true
creativePlayerCommandLevel=MODERATORS
executeAsUserCommandAllowList.ADMINS=
executeAsUserCommandAllowList.ALL=bns
executeAsUserCommandAllowList.GAMEMASTERS=
executeAsUserCommandAllowList.MODERATORS=
executeAsUserCommandAllowList.OWNERS=
feature.BASE_ATTRIBUTE=CREATIVE_PLAYER
feature.COMBAT_ATTRIBUTE=CREATIVE_PLAYER
feature.COMMAND_ACTION=CREATIVE_PLAYER
feature.CUSTOM_PRESET=CREATIVE_PLAYER
feature.DEFAULT_PRESET_IMPORT=CREATIVE_PLAYER
feature.DIALOG=NORMAL_PLAYER
feature.INTERACT_BLOCK_ACTION=CREATIVE_PLAYER
feature.LOCAL_PRESET=NORMAL_PLAYER
feature.MOVEMENT=CREATIVE_PLAYER
feature.OBJECTIVE=CREATIVE_PLAYER
feature.OPEN_TRADING_ACTION=CREATIVE_PLAYER
feature.POSITION=CREATIVE_PLAYER
feature.SCOREBOARD_ACTION=CREATIVE_PLAYER
feature.SPAWN_NPC=CREATIVE_PLAYER
feature.TRADING=CREATIVE_PLAYER
feature.URL_RESOURCE=CREATIVE_PLAYER
feature.WORLD_PRESET=CREATIVE_PLAYER
maxAdminImportedCommandLevel=ADMINS
normalPlayerCommandLevel=ALL
npcSpawnRateLimitAdmin=20
npcSpawnRateLimitCreative=5
serverTrustedCommandLevel=ADMINS
unsafeNpcCommands=ban,ban-ip,banlist,debug,deop,difficulty,forceload,function,gamerule,kick,op,pardon,reload,save-all,save-off,save-on,schedule,setidletimeout,setworldspawn,spawnpoint,spreadplayers,stop,whitelist
@@ -0,0 +1,31 @@
{
// -----------------------------------------------------------
// Item Obliterator -- Brass and Sigil config
// -----------------------------------------------------------
// Hides + disables specific Alex's Mobs items that we don't
// want in this modpack:
//
// alexsmobs:transmutation_table -- competes with Ars
// and undermines the
// Numismatics economy
//
// alexsmobs:shattered_dimensional_carver -- teleports the
// player 1,000,000
// blocks in a random
// direction, breaks
// world-bounds
//
// Both are removed from inventories, creative tabs, recipes
// (including the Capsid transformation), trades, and JEI.
// -----------------------------------------------------------
"configVersion": 2,
"blacklisted_items": [
"alexsmobs:transmutation_table",
"alexsmobs:shattered_dimensional_carver"
],
"blacklisted_nbt": [],
"only_disable_interactions": [],
"only_disable_attacks": [],
"only_disable_recipes": [],
"use_hashmap_optimizations": false
}
@@ -1,11 +0,0 @@
// Brass and Sigil -- client-side: hide items disabled server-side from JEI
//
// The Shattered Dimensional Carver is disabled on the server (the
// Capsid recipe is overridden to require barriers, and the right-click
// is cancelled). Hiding it from JEI completes the illusion -- players
// don't see a recipe-less ghost item cluttering search.
JEIEvents.hideItems(event => {
event.hide('alexsmobs:shattered_dimensional_carver');
console.info('[bns/hide_disabled_items] hid alexsmobs:shattered_dimensional_carver from JEI');
});
@@ -1,32 +0,0 @@
// Brass and Sigil -- disable specific Alex's Mobs features
//
// 1. Transmutation Table
// Competes with Ars Nouveau's crafting depth and bypasses the
// Numismatics-based player economy by offering free item-to-item
// conversion. Remove the crafting recipe so no one can make one.
// Existing tables (none yet) are left as-is.
//
// 2. Shattered Dimensional Carver
// Teleports the player 1,000,000 blocks in a random direction on
// use. Breaks any server-side world-boundary or chunk-loading
// assumptions, drops the player into an unloaded chunk far from
// anyone else. Cancel right-click on the item so it does nothing.
//
// We can't easily prevent the Capsid-shatters-Carver interaction
// (it is a block contact reaction, not a recipe), so the player
// can still produce the item -- it just won't fire.
ServerEvents.recipes(event => {
event.remove({ output: 'alexsmobs:transmutation_table' });
console.info('[bns/disable_alexsmobs_features] removed transmutation_table crafting recipe');
});
ItemEvents.firstRightClicked('alexsmobs:shattered_dimensional_carver', event => {
event.cancel();
const player = event.player;
if (player && !event.level.isClientSide()) {
player.tell('The Shattered Dimensional Carver is disabled on this server.');
}
});
console.info('[bns/disable_alexsmobs_features] loaded');
@@ -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 <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');
@@ -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 <item> [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 <item-id> [count] (omit count to sell 1)'));
return 1;
}))
// /bns sell <item-id> [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');
@@ -0,0 +1,359 @@
// 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 <id>
// /bns bounty cancel <id>
// /bns bounty turnin <id>
// /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) — all vanilla 1.21.x ──
{ id: 'b_warden_1', name: 'Slay the Warden', target: 'minecraft:warden', count: 1, type: 'kill', reward: 800, source: "Adventurer's Guild" },
{ id: 'b_breeze_3', name: 'Hunt 3 Breezes', target: 'minecraft:breeze', count: 3, type: 'kill', reward: 250, source: "Adventurer's Guild" },
{ id: 'b_bogged_5', name: 'Slay 5 Bogged', target: 'minecraft:bogged', count: 5, type: 'kill', reward: 120, source: "Adventurer's Guild" },
{ id: 'b_evoker_2', name: 'Slay 2 Evokers', target: 'minecraft:evoker', count: 2, type: 'kill', reward: 350, source: "Adventurer's Guild" },
{ id: 'b_vindicator_4', name: 'Slay 4 Vindicators', target: 'minecraft:vindicator', count: 4, type: 'kill', reward: 180, source: "Adventurer's Guild" },
{ id: 'b_piglin_brute_2', name: 'Slay 2 Piglin Brutes', target: 'minecraft:piglin_brute', count: 2, type: 'kill', reward: 400, source: "Adventurer's Guild" },
{ id: 'b_ravager_1', name: 'Hunt down a Ravager', target: 'minecraft:ravager', count: 1, type: 'kill', reward: 500, 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 <id>'));
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 <id> Cancel: /bns bounty cancel <id>'));
return 1;
}))
// /bns bounty accept <id>
.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 <id>
.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 <id>
.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);
});
// ─── First-login pool roll ─────────────────────────────────────────
// Without this, new players see an empty Bounty Board until they type
// /bns bounty list (which triggers ensurePoolFresh). The board reads
// bnsBountyPool from NBT; bnsBountyPool stays empty until rollDailyPool
// runs. This hook closes the gap so the screen is populated the first
// time it's opened.
PlayerEvents.loggedIn(event => {
ensurePoolFresh(event.player);
});
console.info('[bns/bounty_engine] loaded — ' + BOUNTY_POOL.length + ' bounties in catalogue');
@@ -0,0 +1,147 @@
// 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 <player> <rankId>` 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 <id> details for one plot',
'§7 /bns plot buy <id> purchase a plot (uses a tier slot)',
'§7 /bns plot release <id> 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 <item-id> [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 <id> accept a bounty',
'§7 /bns bounty cancel <id> drop a bounty (cooldown applies)',
'§7 /bns bounty turnin <id> 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);
// Combined cash+bank deduction via bnstoolkit Java bridge.
// Pulls inventory coins first, then bank for the remainder.
const Bridge = Java.loadClass('uk.sijbers.bnstoolkit.economy.NumismaticsBridge');
const ok = Bridge.spend(player, nextCfg.cost);
if (!ok) {
const cash = countCoins(player);
const bank = Bridge.balanceOf(player);
player.tell(Text.red(
'Upgrade to ' + nextCfg.name + ' costs ' + nextCfg.cost + ' spurs. ' +
'You have ' + cash + ' cash + ' + bank + ' bank = ' + (cash + bank) + ' total.'
));
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');
@@ -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;
}))
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,7 @@
{
"replace": false,
"values": [
"alexsmobs:shattered_dimensional_carver",
"alexsmobs:transmutation_table"
]
}
@@ -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 <player> <rankId> 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}"
}
}
+86 -20
View File
@@ -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.30.3",
"version": "0.41.2",
"minecraft": "1.21.1",
"loader": {
"type": "neoforge",
"version": "21.1.228"
},
"lockedAt": "2026-05-30T21:15:04Z",
"lockedAt": "2026-06-08T18:22:06Z",
"mods": [
{
"source": "modrinth",
@@ -130,17 +130,6 @@
"size": 38629,
"side": "both"
},
{
"source": "modrinth",
"slug": "create-encased",
"versionId": "3sUW27Ho",
"version": "1.8.1-ht1",
"path": "mods/Create Encased-1.21.1-1.8.1-ht1.jar",
"url": "https://cdn.modrinth.com/data/hSSqdyU1/versions/3sUW27Ho/Create%20Encased-1.21.1-1.8.1-ht1.jar",
"sha1": "5ca3d7674676dda5dd472b0433505a54d5e920d0",
"size": 2574837,
"side": "both"
},
{
"source": "modrinth",
"slug": "bellsandwhistles",
@@ -1067,13 +1056,90 @@
},
{
"source": "modrinth",
"slug": "alexs-mobs(1.21.1)",
"versionId": "KSgki4uc",
"version": "1.22.17",
"path": "mods/alexsmobs-1.22.17.jar",
"url": "https://cdn.modrinth.com/data/EmNhnNnt/versions/KSgki4uc/alexsmobs-1.22.17.jar",
"sha1": "5070e8070f60650d773bfd9f1d031496f8f50259",
"size": 26620546,
"slug": "item-obliterator",
"versionId": "jy3ApWAm",
"version": "2.3.0",
"path": "mods/Item-Obliterator-NeoForge-MC1.21.1-2.3.0.jar",
"url": "https://cdn.modrinth.com/data/3ESR84kR/versions/jy3ApWAm/Item-Obliterator-NeoForge-MC1.21.1-2.3.0.jar",
"sha1": "0cb46a873376d7ded387e175bf30181fcc079c15",
"size": 24037,
"side": "both"
},
{
"source": "modrinth",
"slug": "necronomicon",
"versionId": "4iO8Tbfj",
"version": "1.6.0+1.21",
"path": "mods/Necronomicon-NeoForge-1.6.0+1.21.jar",
"url": "https://cdn.modrinth.com/data/P1Kv5EAO/versions/4iO8Tbfj/Necronomicon-NeoForge-1.6.0%2B1.21.jar",
"sha1": "4d81055810e07a94b68d16ab20ec298ad0d597a0",
"size": 29865,
"side": "both"
},
{
"source": "curseforge",
"slug": "ftb-quests",
"fileId": "8195575",
"version": "2101.1.25",
"path": "mods/ftb-quests-neoforge-2101.1.25.jar",
"url": "https://mediafilez.forgecdn.net/files/8195/575/ftb-quests-neoforge-2101.1.25.jar",
"sha1": "c51f43029930cce87e86366b6629a27e4876a03d",
"size": 1469336,
"side": "both"
},
{
"source": "curseforge",
"slug": "ftb-xmod-compat",
"fileId": "7715134",
"version": "21.1.8",
"path": "mods/ftb-xmod-compat-neoforge-21.1.8.jar",
"url": "https://mediafilez.forgecdn.net/files/7715/134/ftb-xmod-compat-neoforge-21.1.8.jar",
"sha1": "0c25fc1a43c99eaa68c883dc77e3ebf4539629e6",
"size": 141328,
"side": "both"
},
{
"source": "curseforge",
"slug": "ftb-ranks",
"fileId": "6431744",
"version": "2101.1.3",
"path": "mods/ftb-ranks-neoforge-2101.1.3.jar",
"url": "https://mediafilez.forgecdn.net/files/6431/744/ftb-ranks-neoforge-2101.1.3.jar",
"sha1": "07fdfa3a75c3481770ce0118090a347d89e3a560",
"size": 88382,
"side": "both"
},
{
"source": "modrinth",
"slug": "structory",
"versionId": "Nvh2sSPX",
"version": "1.3.16",
"path": "mods/Structory_26.1_v1.3.16.jar",
"url": "https://cdn.modrinth.com/data/aKCwCJlY/versions/Nvh2sSPX/Structory_26.1_v1.3.16.jar",
"sha1": "ee48f145a3a09fdb3f492576cbc21ed26abc74bb",
"size": 1261336,
"side": "both"
},
{
"source": "modrinth",
"slug": "structory-towers",
"versionId": "ioKwCTuD",
"version": "1.0.16",
"path": "mods/Structory_Towers_26.1_v1.0.16.jar",
"url": "https://cdn.modrinth.com/data/j3FONRYr/versions/ioKwCTuD/Structory_Towers_26.1_v1.0.16.jar",
"sha1": "ed22fe659cb3020f2e86b7619ce721714b9546be",
"size": 491545,
"side": "both"
},
{
"source": "modrinth",
"slug": "create-tracks+",
"versionId": "cju2ayQC",
"version": "1.0.5",
"path": "mods/tracks_plus-1.0.5.jar",
"url": "https://cdn.modrinth.com/data/E8eHF2Yl/versions/cju2ayQC/tracks_plus-1.0.5.jar",
"sha1": "87f72a49f53d7cf4202a11e4fc68f9bd18ab2c38",
"size": 610174,
"side": "both"
}
],
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
Linux-side port of Build-Pack.ps1 + Deploy-Brass.ps1 (publish step).
Reads pack/pack.lock.json + walks pack/overrides/, rebuilds manifest.json,
mirrors every override file to the live web root, and republishes the
manifest. The launcher will then see fresh hashes on its next sync.
Run on glados with passwordless sudo. Idempotent.
Web root: /srv/services/sites/bns (mounted into Caddy as /srv/sites/bns).
"""
import hashlib
import json
import shutil
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
REPO = Path("/home/matt/dev/brass-and-sigil")
LOCK_PATH = REPO / "pack" / "pack.lock.json"
OVERRIDES = REPO / "pack" / "overrides"
WEB_ROOT = Path("/srv/services/sites/bns/pack")
WEB_FILES = WEB_ROOT / "files"
WEB_MANIFEST = WEB_ROOT / "manifest.json"
SELF_HOST_BASE = "https://bns.sijbers.uk/pack/files"
MANAGED_ROOTS = ["mods", "config", "resourcepacks", "shaderpacks", "kubejs", "defaultconfigs", "patchouli_books"]
# bnstoolkit ships client+server surface (screens, HUD, network bridge),
# so it must NOT inherit the default "server-only" tag that Build-Pack.ps1
# stamps on every pack/overrides/mods/*.jar. brassandsigil_tweaks is
# data-only (worldgen modifiers / recipe overrides) so server-side is
# correct for it.
CLIENT_AND_SERVER_LOCAL_MODS = {"bnstoolkit"}
def sha1_of(p: Path) -> str:
h = hashlib.sha1()
with p.open("rb") as f:
for chunk in iter(lambda: f.read(1 << 16), b""):
h.update(chunk)
return h.hexdigest()
def main():
with LOCK_PATH.open() as f:
lock = json.load(f)
files = []
total_bytes = 0
# 1. Mods from the lockfile (Modrinth-hosted)
for m in lock["mods"]:
entry = {
"path": m["path"],
"url": m["url"],
"sha1": m["sha1"],
"size": m["size"],
}
side = m.get("side")
if side and side != "both":
entry["side"] = side
files.append(entry)
total_bytes += m["size"]
# 2. Local overrides
for root in MANAGED_ROOTS:
root_dir = OVERRIDES / root
if not root_dir.exists():
continue
for p in sorted(root_dir.rglob("*")):
if not p.is_file():
continue
rel = p.relative_to(OVERRIDES).as_posix()
sha1 = sha1_of(p)
size = p.stat().st_size
entry = {
"path": rel,
"url": f"{SELF_HOST_BASE}/{rel}",
"sha1": sha1,
"size": size,
}
if root == "mods":
# See CLIENT_AND_SERVER_LOCAL_MODS — bnstoolkit must be loaded
# on both sides; the other locally-built mods are server-only.
stem = p.name.split("-")[0].split("_")[0] # before first - or _
if not any(p.name.startswith(prefix) for prefix in CLIENT_AND_SERVER_LOCAL_MODS):
entry["side"] = "server"
files.append(entry)
total_bytes += size
# 3. Compose manifest matching the existing schema
manifest = {
"name": lock["name"],
"version": lock["version"],
"minecraft": {"version": lock["minecraft"]},
"loader": {"type": lock["loader"]["type"], "version": lock["loader"]["version"]},
"files": files,
}
for opt in ("defaultServer", "defaultShader", "panelUrl"):
if opt in lock:
manifest[opt] = lock[opt]
# Carry the launcher metadata from the existing published manifest
# (we can't read a Windows .exe's FileVersion from Linux easily, and
# the launcher version isn't related to pack content).
try:
with WEB_MANIFEST.open() as f:
existing = json.load(f)
for k in ("launcherVersion", "launcherUrl"):
if k in existing:
manifest[k] = existing[k]
except Exception:
pass
# 4. Mirror override files to web root (uses sudo + cp -a). Only update
# files whose hash differs from what's already published to keep the
# rsync footprint tiny.
print(f"\nMirroring {sum(1 for r in MANAGED_ROOTS if (OVERRIDES / r).exists())} override roots to {WEB_FILES}\n")
changed = 0
skipped = 0
for root in MANAGED_ROOTS:
root_dir = OVERRIDES / root
if not root_dir.exists():
continue
target_root = WEB_FILES / root
for p in root_dir.rglob("*"):
if not p.is_file():
continue
rel = p.relative_to(OVERRIDES)
target = WEB_FILES / rel
try:
if target.exists() and sha1_of(target) == sha1_of(p):
skipped += 1
continue
except PermissionError:
# Need sudo to read
pass
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(p, target)
changed += 1
print(f" changed: {changed}, skipped (matching hash): {skipped}\n")
# 5. Write the manifest atomically (write to .new, then move)
new_manifest = WEB_MANIFEST.with_suffix(".json.new")
bak_manifest = WEB_MANIFEST.with_suffix(".json.bak")
if WEB_MANIFEST.exists():
shutil.copy2(WEB_MANIFEST, bak_manifest)
with new_manifest.open("w") as f:
json.dump(manifest, f, indent=2, ensure_ascii=False)
f.write("\n")
new_manifest.replace(WEB_MANIFEST)
print(f"Manifest written: {WEB_MANIFEST}")
print(f" Version: {lock['version']}")
print(f" Files: {len(files)}")
print(f" Total: {total_bytes / 1024 / 1024:.1f} MB download")
print()
# Quick verify
rel = "kubejs/server_scripts/economy/00_tier_system.js"
entry = next((f for f in files if f["path"] == rel), None)
if entry:
print(f" {rel} -> sha1={entry['sha1'][:12]}... size={entry['size']}")
if __name__ == "__main__":
main()