Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecd5933f9f | |||
| 38fbf80bc1 | |||
| 61dd3d3754 | |||
| 61a923cbef | |||
| 57b6cd8dea | |||
| 572671f1d4 | |||
| ffff3bc78d | |||
| b4cd66036b | |||
| 3d635d7e59 | |||
| 9eb8f540c4 | |||
| 367056e2ba | |||
| 25107efcf1 | |||
| 56e6006424 | |||
| a0aee440c3 | |||
| 4ca2f17d9d | |||
| 121ae4c603 | |||
| 107622fc6d |
@@ -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,143 @@
|
||||
# bnstoolkit M0 — discussion points for testing session
|
||||
|
||||
> Built 2026-06-07 (early hours UTC). Shipped via pack v0.34.0.
|
||||
> Repo: http://10.16.5.102:3000/minecraft/bnstoolkit
|
||||
> Jar: `bnstoolkit-0.1.0.jar` (15.6 KB)
|
||||
|
||||
## What this is
|
||||
|
||||
The skeleton of the custom companion mod. It loads cleanly on the client
|
||||
and the dedicated server, registers nothing visible yet, and exposes the
|
||||
package layout we'll fill in over M1–M4. The point of this milestone is:
|
||||
|
||||
1. Tooling proven end-to-end (Java 21, ModDevGradle, NeoForge 21.1.228, jar in pack)
|
||||
2. Mod-id, package, and resource paths locked so future commits only ADD code
|
||||
3. One C2S/S2C packet pair wired to prove the network plumbing
|
||||
4. Placeholder screens in place so wiring keybinds to real UI is M1's first task
|
||||
|
||||
## Open decisions — please walk through these tomorrow
|
||||
|
||||
Each is marked with `TODO(matt …)` in the source.
|
||||
|
||||
### 1. Key bindings (BnsKeyBindings.java)
|
||||
|
||||
Defaults I picked, all unbinding is in the vanilla controls menu:
|
||||
|
||||
| Binding | Key | Opens |
|
||||
|---|---|---|
|
||||
| Tier ladder | T | `TierUpgradeScreen` |
|
||||
| Bounty board | B | `BountyBoardScreen` |
|
||||
| Plot map | P | `PlotPurchaseScreen` |
|
||||
|
||||
**Question**: any conflicts with mods you already use? `T` is vanilla's
|
||||
chat-open IIRC — I should probably move it. Pick a free combo when you
|
||||
playtest, I'll bump the defaults.
|
||||
|
||||
### 2. Screen open triggers — keybind only, or block + keybind?
|
||||
|
||||
Each major screen has two viable open paths:
|
||||
|
||||
- **Keybind** (already drafted): bnstoolkit binds T/B/P
|
||||
- **In-world block**: e.g. a "Bounty Board" block placed at the spawn
|
||||
guild building. Right-click opens the screen. More immersive, ties the
|
||||
feature to a physical location.
|
||||
|
||||
Both can coexist easily — they're not exclusive. I'd default to "both for
|
||||
TierUpgrade + Plots, keybind-only for QuestLog, block-only for ShopBrowser
|
||||
and BountyBoard". But that's a 30-second call you make tomorrow.
|
||||
|
||||
### 3. FTB Library UI swap timing
|
||||
|
||||
Right now the placeholder screens extend vanilla `Screen`. The
|
||||
architecture doc commits to `dev.ftb.mods.ftblibrary.ui.BaseScreen` for
|
||||
visual consistency with FTB Quests / Chunks. The swap is one line per
|
||||
screen — happens in M1 alongside the first real screen layout.
|
||||
|
||||
Reason it's deferred: the FTB maven coords need to be wired into
|
||||
`build.gradle`, and I don't want to pin the wrong artifact name. Will
|
||||
verify against the pack's installed FTB Library version (2101.1.31) before
|
||||
the M1 build.
|
||||
|
||||
### 4. HUD overlay layout
|
||||
|
||||
The architecture doc shows a small panel "tier-icon | tier-name | spurs"
|
||||
at top-left. Drop me a sketch when you have time and I'll lay it out for
|
||||
M4. Current placeholder draws nothing — won't appear in game at all yet.
|
||||
|
||||
### 5. Network smoke-test packet
|
||||
|
||||
`RequestTierStatePacket` / `TierStateUpdatePacket` are the only wire
|
||||
pair I shipped. They prove the codec + handler pattern. M1 fills in the
|
||||
server-side handler that reads `player.persistentData.bnsTier` (the
|
||||
KubeJS-owned NBT) and replies. **No client code yet sends the request** —
|
||||
the round trip is dormant in M0.
|
||||
|
||||
### 6. Mod settings screen
|
||||
|
||||
Not addressed. Will be a small screen in M4 with toggles for the HUD,
|
||||
keybind reminders, and (eventually) a colour-blind mode for tier ranks.
|
||||
Low priority unless you've got a specific UX concern.
|
||||
|
||||
---
|
||||
|
||||
## Files added/changed (PR for v0.34.0)
|
||||
|
||||
```
|
||||
pack/overrides/mods/bnstoolkit-0.1.0.jar NEW (15.6 KB)
|
||||
pack/pack.lock.json version 0.33.0 -> 0.34.0
|
||||
docs/bnstoolkit-discussion-points-2026-06-07.md NEW (this file)
|
||||
```
|
||||
|
||||
The bnstoolkit source itself lives at `http://10.16.5.102:3000/minecraft/bnstoolkit`
|
||||
— one initial commit on `main` with the full M0 scaffold. Build:
|
||||
|
||||
```
|
||||
cd /home/matt/dev/bnstoolkit
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
## What you'll see in-game when you log in
|
||||
|
||||
**Nothing yet.** That's the point of M0 — it loads cleanly with zero
|
||||
visible surface. Verification path:
|
||||
|
||||
1. Server log: grep for `[bnstoolkit] mod entry constructed` and
|
||||
`common setup complete (M0 scaffold — no surface yet)`
|
||||
2. Client log on connect: `registered 3 key bindings under key.category.bnstoolkit`
|
||||
3. Controls menu → "Brass and Sigil" category → 3 unbound-looking lines
|
||||
(Open Tier Ladder, Open Bounty Board, Open Plot Map) on T/B/P
|
||||
|
||||
If you bind one of those keys and press it in-game, **nothing happens**.
|
||||
That's correct — the handlers are M1 work.
|
||||
|
||||
## Milestones — concrete next deliverables
|
||||
|
||||
| Milestone | Scope | Estimated work |
|
||||
|---|---|---|
|
||||
| M1 | TierUpgradeScreen + KubeJS bridge (tier/balance) + FTB Library swap | 1 evening |
|
||||
| M2 | QuestLogScreen + ShopBrowserScreen | 1 evening |
|
||||
| M3 | BountyBoardScreen + PlotPurchaseScreen | 1 evening |
|
||||
| M4 | SpurHud, mod settings screen, polish | 1 evening |
|
||||
|
||||
Each milestone ends with a deployable jar + a screen you can poke in
|
||||
game. None of them require server downtime — drop the new jar in
|
||||
`pack/overrides/mods/`, the wrapper sync handles the rest.
|
||||
|
||||
## Quick recovery
|
||||
|
||||
If the mod misbehaves and bricks the world:
|
||||
|
||||
```
|
||||
# 1. Stop the server
|
||||
sudo systemctl stop brass-sigil-server.service
|
||||
|
||||
# 2. Pull the jar out of the live mods dir
|
||||
sudo rm /srv/fast/brass-sigil-server/server/mods/bnstoolkit-0.1.0.jar
|
||||
|
||||
# 3. Restart
|
||||
sudo systemctl start brass-sigil-server.service
|
||||
```
|
||||
|
||||
That leaves `pack/overrides/mods/bnstoolkit-0.1.0.jar` in the pack repo
|
||||
so the next sync would put it back — to fully revert, also `git revert`
|
||||
the v0.34.0 commit on the pack.
|
||||
@@ -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.
|
||||
+176
-54
@@ -1,6 +1,6 @@
|
||||
# Brass and Sigil — Economy & Quest System Design
|
||||
|
||||
> Status: architecture locked (v0.2). Implementation ready to begin.
|
||||
> Status: 13-tier ladder locked (v0.4). Architecture + UI confirmed in v0.3.
|
||||
> Owner: matt
|
||||
> Last updated: 2026-06-06
|
||||
|
||||
@@ -56,7 +56,7 @@ Each guild is a destination. The guild's NPC is both the **entry point** (accept
|
||||
|---|---|---|
|
||||
| **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 | Plot claim office, tier upgrades, sethome/warp services |
|
||||
| **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 |
|
||||
@@ -73,7 +73,68 @@ Both pay into the same wallet. Players run the campaign at their pace and the da
|
||||
|
||||
---
|
||||
|
||||
## 4. Quest & bounty flow (universal pattern)
|
||||
## 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:
|
||||
|
||||
@@ -120,31 +181,37 @@ The same accept → complete → return pattern works for every guild:
|
||||
| **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 tracking & cancellation (locked)
|
||||
### Player-facing UI (locked)
|
||||
|
||||
Three surfaces, **all shipping together in Phase 3.** Not iterated, not optional.
|
||||
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 | Role | Mechanism |
|
||||
|---|---|---|
|
||||
| **Bossbars** | Always-visible HUD — one bossbar per active quest, shows name + N/M progress bar | Vanilla `/bossbar` commands driven by KubeJS. Native per-player display (scoreboards are global; bossbars aren't). Up to 3 stacked at top of screen. |
|
||||
| **`/quests` commands** | Management — list, inspect, cancel | KubeJS `ServerEvents.commandRegistry`. Works in chat and over the Telegram bridge. Clickable chat components for `[cancel]` and `[details]`. |
|
||||
| **Easy NPC dialog** | Accept + turn-in at guild NPCs | Dialog action runs `/bns_quest accept <quest_id> <npc_tag>` (KubeJS-registered command). Dialog branching uses scoreboard objectives that KubeJS mirrors quest state into. |
|
||||
|
||||
| Command | Effect |
|
||||
| Surface | Implementation |
|
||||
|---|---|
|
||||
| `/quests` | Lists your ACTIVE / READY quests with progress, source guild, reward |
|
||||
| `/quests info <id>` | Detail view |
|
||||
| `/quests cancel <id>` | Drops an active quest, frees the slot. Applies the same cooldown as if completed (prevents accept-cancel-accept RNG-shopping). |
|
||||
| **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 NOT FTB Quests for bounties
|
||||
### Why custom mod, on FTB Library's UI framework
|
||||
|
||||
FTB Quests is the right tool for **progression** (Phase 5+), not for daily bounties. Confirmed via source review:
|
||||
- **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.
|
||||
|
||||
- **No abandon/cancel event** — players can `Ctrl+Shift+R` to reset quest progress, but nothing fires that KubeJS can hook → can't enforce cancellation cooldown.
|
||||
- **Cooldown is per-team, not per-player** — in a 4-player team, one member completing puts it on cooldown for all four.
|
||||
- **No per-player quest visibility** — gating "5 of 20 active today" requires `CustomTask` gates rather than first-class per-player visibility.
|
||||
### Why NOT FTB Quests for daily bounties
|
||||
|
||||
For long-form progression questing, all three are non-issues. So: FTB Quests for the campaign, custom KubeJS system 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
|
||||
|
||||
@@ -297,10 +364,22 @@ A quest moving to READY frees the slot for accepting a new one even before turn-
|
||||
|---|---|---|
|
||||
| **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 (Phase 6). CurseForge only, no Modrinth listing. |
|
||||
| **FTB Ranks** | 2101.1.3 | Tier system. CurseForge only, no Modrinth listing. |
|
||||
|
||||
### Custom mod
|
||||
**Not needed for V1.** Re-evaluate only if implementation hits a hard wall on a specific feature.
|
||||
### 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)
|
||||
@@ -317,13 +396,14 @@ Each phase is independently shippable. Earlier phases unlock value without depen
|
||||
| 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 for everything else. | Yes (user) |
|
||||
| **1** | **Sell shop:** Numismatics shop blocks for diamonds (10 spurs) + netherite ingots (100 spurs). Lifetime-sale tracker for diminishing returns. | No — test anywhere, drop into Bazaar later |
|
||||
| **2** | **Plot claim cost:** `FTBChunksEvents.before('claim')` deducts spurs. Free first 9 chunks, 50/chunk to 25, 200/chunk beyond. | No |
|
||||
| **3** | **Bounty engine:** state machine + `/quests` commands + bossbar HUD + 3-slot cap. Daily quest pool with KubeJS-rotated dailies. NPC dialog hooks ready but inert until guild NPCs exist at spawn. | No (NPC layer activates once spawn is built) |
|
||||
| **4** | **Villager rebalance:** strip vanilla, ship spur-priced trade table via `ServerEvents.villagerTrades`. | No |
|
||||
| **5** | **Progression quest line:** FTB Quests anchor + first chapter. KubeJS rewards dispense spurs via Numismatics API. | No (anchor NPC at Library once spawn exists) |
|
||||
| **6** | **Tier upgrades:** FTB Ranks integrated. `/tier upgrade` deducts spurs and promotes via `ftbranks add`. Rank perms gate FTB Chunks claim limits. | 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 |
|
||||
|
||||
@@ -348,41 +428,83 @@ Resolved before implementation:
|
||||
|
||||
| Question | Decision |
|
||||
|---|---|
|
||||
| Quest UI for bounties | Custom (bossbars + commands + Easy NPC) — NOT FTB Quests |
|
||||
| Quest UI for bounties | **Custom mod with FTB Library UI widgets** — proper GUI screens, no chat commands |
|
||||
| Quest UI for progression | FTB Quests |
|
||||
| State storage | Per-player JSON in `world/data/bns_economy/` |
|
||||
| Numismatics access | Direct Java via `Java.loadClass` |
|
||||
| Chunk claim refusal | `FTBChunksEvents.before('claim')` with `event.setResult(...)` |
|
||||
| Tier system | FTB Ranks + permission-driven chunk limits |
|
||||
| Custom mod for V1 | No — only if a hard wall appears |
|
||||
| Sethomes / warps | Paid services (`/sethome` 50 spurs, `/warp` 25 spurs, `/back` 10 spurs) |
|
||||
| Daily quest count | 5 active at a time, refresh per-player 24h after last accept |
|
||||
| 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** — to be set with the first FTB Ranks config pass
|
||||
- **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 organized under `pack/overrides/kubejs/server_scripts/economy/`:
|
||||
- `01_numismatics_api.js` — Java.loadClass wrapper helpers (read/deposit/deduct)
|
||||
- `02_quest_state.js` — load/save `world/data/bns_economy/<uuid>.json`
|
||||
- `03_bossbar_hud.js` — per-player bossbar lifecycle
|
||||
- `04_commands.js` — `/quests`, `/bns_quest`, `/tier` registrations
|
||||
- `05_bounty_engine.js` — Phase 3 quest engine
|
||||
- `06_sell_shop.js` — Phase 1 shop block tracking + DR
|
||||
- `07_plot_costs.js` — Phase 2 FTB Chunks event handler
|
||||
- `08_villager_trades.js` — Phase 4 rebalance
|
||||
- `09_tier_upgrades.js` — Phase 6 rank upgrades
|
||||
- `99_dynamic_exchange.js` — Phase 7
|
||||
- State data under `world/data/bns_economy/<uuid>.json` per player. Readable, easy to debug. Migration to SQLite considered if dataset grows beyond ~100 players.
|
||||
- All NPC dialog trees 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 into `/bns_quest` namespace which routes to KubeJS handlers.
|
||||
- Trade table for villager rebalance: single source file `08_villager_trades.js`. Each profession × tier has its own block. One file = one diff to review during balance passes.
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,347 @@
|
||||
// Brass and Sigil -- bounty engine V1 (command-driven)
|
||||
//
|
||||
// Per docs/economy-system.md §4-5:
|
||||
// Each player has a per-day pool of available bounties drawn randomly
|
||||
// from BOUNTY_POOL. They can accept up to TIER.dailyBountySlots active
|
||||
// at once. Completing the objective marks the bounty READY -- they
|
||||
// then turn it in to claim the spur reward. Cancellation applies the
|
||||
// same cooldown as completion (no RNG shopping).
|
||||
//
|
||||
// V1 turn-in is a command. When the bnstoolkit mod ships, the Adventurer's
|
||||
// Guild NPC will be the turn-in surface and this command becomes a
|
||||
// fallback/debug path.
|
||||
//
|
||||
// Player commands:
|
||||
// /bns bounty list -- today's available bounties
|
||||
// /bns bounty active -- your accepted bounties
|
||||
// /bns bounty accept <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) ──
|
||||
{ id: 'b_warden_1', name: 'Slay the Warden', target: 'minecraft:warden', count: 1, type: 'kill', reward: 800, source: "Adventurer's Guild" },
|
||||
{ id: 'b_voidworm_1', name: 'Destroy a Void Worm', target: 'alexsmobs:void_worm', count: 1, type: 'kill', reward: 1200, source: "Adventurer's Guild" },
|
||||
{ id: 'b_warped_mosco_1', name: 'Slay a Warped Mosco', target: 'alexsmobs:warped_mosco', count: 1, type: 'kill', reward: 700, source: "Adventurer's Guild" },
|
||||
{ id: 'b_farseer_1', name: 'Slay the Farseer', target: 'alexsmobs:farseer', count: 1, type: 'kill', reward: 900, source: "Adventurer's Guild" },
|
||||
{ id: 'b_skreecher_1', name: 'Slay a Skreecher', target: 'alexsmobs:skreecher', count: 1, type: 'kill', reward: 400, source: "Adventurer's Guild" },
|
||||
];
|
||||
|
||||
// Index pool by id for O(1) lookup
|
||||
const BOUNTY_INDEX = {};
|
||||
for (const b of BOUNTY_POOL) BOUNTY_INDEX[b.id] = b;
|
||||
|
||||
// ─── Player tier → slot cap ─────────────────────────────────────────
|
||||
const TIER_BOUNTY_SLOTS = [0, 3, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10];
|
||||
|
||||
function playerBountySlotCap(player) {
|
||||
const data = player.persistentData;
|
||||
const tier = data.contains('bnsTier') ? data.getInt('bnsTier') : 1;
|
||||
const safe = (tier >= 1 && tier <= 13) ? tier : 1;
|
||||
return TIER_BOUNTY_SLOTS[safe];
|
||||
}
|
||||
|
||||
// ─── Compound storage helpers ───────────────────────────────────────
|
||||
function getActiveMap(player) {
|
||||
const d = player.persistentData;
|
||||
if (!d.contains('bnsBountyActive')) d.put('bnsBountyActive', {});
|
||||
return d.getCompound('bnsBountyActive');
|
||||
}
|
||||
function getCompletedMap(player) {
|
||||
const d = player.persistentData;
|
||||
if (!d.contains('bnsBountyCompleted')) d.put('bnsBountyCompleted', {});
|
||||
return d.getCompound('bnsBountyCompleted');
|
||||
}
|
||||
function getPoolList(player) {
|
||||
const d = player.persistentData;
|
||||
if (!d.contains('bnsBountyPool')) d.put('bnsBountyPool', {});
|
||||
return d.getCompound('bnsBountyPool');
|
||||
}
|
||||
|
||||
// ─── Daily pool refresh ─────────────────────────────────────────────
|
||||
function nowMs() { return Date.now(); }
|
||||
|
||||
function rollDailyPool(player) {
|
||||
const eligible = BOUNTY_POOL.slice(); // shallow copy
|
||||
// Filter out bounties still on cooldown
|
||||
const completed = getCompletedMap(player);
|
||||
const now = nowMs();
|
||||
const available = eligible.filter(b => {
|
||||
if (!completed.contains(b.id)) return true;
|
||||
return completed.getLong(b.id) <= now;
|
||||
});
|
||||
// Shuffle and pick top BOUNTY_POOL_SIZE
|
||||
for (let i = available.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const t = available[i]; available[i] = available[j]; available[j] = t;
|
||||
}
|
||||
const picked = available.slice(0, BOUNTY_POOL_SIZE);
|
||||
const pool = getPoolList(player);
|
||||
// Clear old pool entries
|
||||
for (const k of pool.getAllKeys()) pool.remove(k);
|
||||
for (const b of picked) pool.putInt(b.id, 1);
|
||||
player.persistentData.putLong('bnsBountyPoolRefresh', now);
|
||||
console.info(`[bns/bounty] rolled pool for ${player.username}: ${picked.map(b=>b.id).join(', ')}`);
|
||||
}
|
||||
|
||||
function ensurePoolFresh(player) {
|
||||
const d = player.persistentData;
|
||||
const lastRoll = d.contains('bnsBountyPoolRefresh') ? d.getLong('bnsBountyPoolRefresh') : 0;
|
||||
if (nowMs() - lastRoll > BOUNTY_REFRESH_MS) {
|
||||
rollDailyPool(player);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Active-bounty management ───────────────────────────────────────
|
||||
function activeBountyCount(player) {
|
||||
return getActiveMap(player).getAllKeys().size();
|
||||
}
|
||||
|
||||
function addActiveBounty(player, bountyDef) {
|
||||
const map = getActiveMap(player);
|
||||
const entry = {};
|
||||
entry['progress'] = 0;
|
||||
entry['target'] = bountyDef.count;
|
||||
entry['reward'] = bountyDef.reward;
|
||||
entry['source'] = bountyDef.source;
|
||||
entry['name'] = bountyDef.name;
|
||||
entry['ready'] = false;
|
||||
entry['targetId'] = bountyDef.target;
|
||||
entry['type'] = bountyDef.type;
|
||||
map.put(bountyDef.id, entry);
|
||||
}
|
||||
|
||||
function removeActiveBounty(player, bountyId) {
|
||||
getActiveMap(player).remove(bountyId);
|
||||
}
|
||||
|
||||
function markCooldown(player, bountyId) {
|
||||
getCompletedMap(player).putLong(bountyId, nowMs() + BOUNTY_COOLDOWN_MS);
|
||||
}
|
||||
|
||||
// ─── Progress hook: increment matching active bounties on kill ──────
|
||||
EntityEvents.death(event => {
|
||||
const src = event.source.player;
|
||||
if (!src) return;
|
||||
const player = src;
|
||||
const killedId = event.entity.type;
|
||||
|
||||
const active = getActiveMap(player);
|
||||
let updated = false;
|
||||
for (const id of active.getAllKeys()) {
|
||||
const entry = active.getCompound(id);
|
||||
if (entry.getString('type') !== 'kill') continue;
|
||||
if (entry.getString('targetId') !== killedId) continue;
|
||||
if (entry.getBoolean('ready')) continue;
|
||||
const newProgress = entry.getInt('progress') + 1;
|
||||
entry.putInt('progress', newProgress);
|
||||
if (newProgress >= entry.getInt('target')) {
|
||||
entry.putBoolean('ready', true);
|
||||
player.tell(Text.gold(`Bounty READY: ${entry.getString('name')}. Turn in with /bns bounty turnin ${id}`));
|
||||
} else {
|
||||
// Light progress notification every few kills to not spam
|
||||
if (newProgress === 1 || newProgress === entry.getInt('target') - 1 || newProgress % 5 === 0) {
|
||||
player.tell(Text.gray(`Bounty progress: ${entry.getString('name')} — ${newProgress}/${entry.getInt('target')}`));
|
||||
}
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Commands ──────────────────────────────────────────────────────
|
||||
ServerEvents.commandRegistry(event => {
|
||||
const { commands: Commands, arguments: Arguments } = event;
|
||||
|
||||
const bountyCmd = Commands.literal('bns')
|
||||
.then(Commands.literal('bounty')
|
||||
|
||||
// /bns bounty list
|
||||
.then(Commands.literal('list').executes(ctx => {
|
||||
const player = ctx.source.player;
|
||||
if (!player) { ctx.source.sendSystemMessage(Text.red('Run from a player context.')); return 0; }
|
||||
ensurePoolFresh(player);
|
||||
const pool = getPoolList(player);
|
||||
const cap = playerBountySlotCap(player);
|
||||
const active = activeBountyCount(player);
|
||||
player.tell(Text.aqua(`--- Today's bounties (active: ${active}/${cap}) ---`));
|
||||
if (pool.getAllKeys().size() === 0) {
|
||||
player.tell(Text.gray(' (No bounties currently available — check back in 24h)'));
|
||||
return 1;
|
||||
}
|
||||
for (const id of pool.getAllKeys()) {
|
||||
const b = BOUNTY_INDEX[id];
|
||||
if (!b) continue;
|
||||
const accepted = getActiveMap(player).contains(id);
|
||||
const tag = accepted ? '§a[ACCEPTED]§7' : '§7';
|
||||
player.tell(Text.gray(` ${tag} ${id} "${b.name}" reward: ${b.reward} spurs`));
|
||||
}
|
||||
player.tell(Text.gray('Accept: /bns bounty accept <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);
|
||||
});
|
||||
|
||||
console.info('[bns/bounty_engine] loaded — ' + BOUNTY_POOL.length + ' bounties in catalogue');
|
||||
@@ -0,0 +1,146 @@
|
||||
// Brass and Sigil -- player-facing economy commands
|
||||
//
|
||||
// /bns help overview of every /bns subcommand
|
||||
// /bns me quick economic status (tier, balance, plots, bounties)
|
||||
// /bns tier upgrade pay spurs to advance to the next tier
|
||||
//
|
||||
// Player-facing tier upgrade pays spurs, increments the NBT tier, and
|
||||
// runs `ftbranks add <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);
|
||||
const balance = countCoins(player);
|
||||
if (balance < nextCfg.cost) {
|
||||
player.tell(Text.red(
|
||||
'Upgrade to ' + nextCfg.name + ' costs ' + nextCfg.cost + ' spurs. You have ' + balance + '.'
|
||||
));
|
||||
return 0;
|
||||
}
|
||||
const took = takeCoins(player, nextCfg.cost);
|
||||
if (took < nextCfg.cost) {
|
||||
giveCoins(player, took);
|
||||
player.tell(Text.red('Coin removal failed — refunded. Try again.'));
|
||||
return 0;
|
||||
}
|
||||
// Promote via FTB Ranks
|
||||
const rankId = TIER_RANK_ID[nextTier];
|
||||
Utils.server.runCommandSilent('ftbranks add ' + player.username + ' ' + rankId);
|
||||
// Mirror to NBT
|
||||
bnsTier.set(player, nextTier);
|
||||
player.tell(Text.gold(
|
||||
'╔══ You have been elevated to ' + nextCfg.name + '! ══╗'
|
||||
));
|
||||
player.tell(Text.gold(
|
||||
' ' + nextCfg.wildernessChunks + ' wilderness chunks · ' +
|
||||
nextCfg.plotSlots + ' plot slots · ' +
|
||||
nextCfg.dailyBountySlots + ' bounty slots · ' +
|
||||
(Math.max(0, 25 + nextCfg.shopFeeDiscount)) + '% Bazaar fee'
|
||||
));
|
||||
console.info('[bns/tier] ' + player.username + ' upgraded to tier ' + nextTier + ' (' + nextCfg.name + ') for ' + nextCfg.cost + ' spurs');
|
||||
return 1;
|
||||
})));
|
||||
|
||||
event.register(playerCmds);
|
||||
});
|
||||
|
||||
console.info('[bns/player_commands] loaded — /bns help, /bns me, /bns tier upgrade');
|
||||
@@ -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.
@@ -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}"
|
||||
}
|
||||
}
|
||||
+35
-2
@@ -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.31.2",
|
||||
"version": "0.34.1",
|
||||
"minecraft": "1.21.1",
|
||||
"loader": {
|
||||
"type": "neoforge",
|
||||
"version": "21.1.228"
|
||||
},
|
||||
"lockedAt": "2026-06-01T19:47:31Z",
|
||||
"lockedAt": "2026-06-07T00:18:08Z",
|
||||
"mods": [
|
||||
{
|
||||
"source": "modrinth",
|
||||
@@ -1097,6 +1097,39 @@
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"defaultServer": {
|
||||
|
||||
Reference in New Issue
Block a user