Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 016985a5fc | |||
| a7f95691bb | |||
| 23efdee804 | |||
| 1859613f1f | |||
| a41d8cb531 | |||
| e770cb55cb | |||
| 338617e855 | |||
| ecd5933f9f | |||
| 38fbf80bc1 | |||
| 61dd3d3754 | |||
| 61a923cbef | |||
| 57b6cd8dea | |||
| 572671f1d4 | |||
| ffff3bc78d | |||
| b4cd66036b | |||
| 3d635d7e59 | |||
| 9eb8f540c4 | |||
| 367056e2ba | |||
| 25107efcf1 | |||
| 56e6006424 | |||
| a0aee440c3 | |||
| 4ca2f17d9d | |||
| 121ae4c603 | |||
| 107622fc6d | |||
| 95b0d1564d | |||
| 0c591faf14 | |||
| 63e9185bf8 | |||
| 5bd1ace6aa | |||
| 1af2b9a86f | |||
| 678e63497c | |||
| 2710dce479 | |||
| 99aa11c94d | |||
| 0cd43c8f2d | |||
| 7acd3f4da8 | |||
| 1b9e35efea | |||
| f88f6eaaca | |||
| 3912ec541d | |||
| 5068087a27 | |||
| d259695932 | |||
| 0a6fd851c1 | |||
| c635de501a | |||
| 52eca18f0d | |||
| 34d180404a | |||
| feb95e2e7e | |||
| d6c6da446c | |||
| 6bc44ffd2e | |||
| b5ebe26649 | |||
| 4f13fd1408 | |||
| 77d1cef1d0 | |||
| 19705118d3 | |||
| cfc1a21d25 | |||
| 4cbc8c42af | |||
| e84de6c190 |
@@ -0,0 +1,622 @@
|
||||
# bnstoolkit — Architecture & Perk Specification
|
||||
|
||||
> Status: design draft (v0.1). Not yet implemented.
|
||||
> Owner: matt
|
||||
> Last updated: 2026-06-06
|
||||
|
||||
`bnstoolkit` is the custom companion mod for the Brass and Sigil economy
|
||||
system. It provides the UI layer (GUI screens, HUD), the FTB Chunks
|
||||
map mixins for spawn plot integration, and the data models the economy
|
||||
needs. All gameplay logic (bounty rotation, state machine, villager
|
||||
trades, Numismatics integration, etc.) lives in KubeJS and is called
|
||||
into by the mod via network packets.
|
||||
|
||||
This doc is the source of truth for the mod design. It must be
|
||||
reviewed before any Java is written.
|
||||
|
||||
See also: [`economy-system.md`](./economy-system.md) for the gameplay
|
||||
side of the design.
|
||||
|
||||
---
|
||||
|
||||
## 1. Purpose & scope
|
||||
|
||||
### In scope
|
||||
|
||||
- GUI screens built on FTB Library's UI framework (matches FTB Quests / FTB Chunks visual language)
|
||||
- Mixins into FTB Chunks' map UI for spawn-plot cost overlay, hover tooltips, and click-to-purchase popups
|
||||
- Custom HUD render layer for quest progress (replaces stacked vanilla bossbars)
|
||||
- Data persistence for plot ownership (server-side, saved with world)
|
||||
- Network packets for client↔server state sync
|
||||
- Admin commands: `/bnstoolkit reload`, debugging commands
|
||||
- Config-driven tier perks (`tiers.json`) and plot definitions (`plots.json`)
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Bounty quest engine (lives in KubeJS)
|
||||
- Villager trade rebalance (lives in KubeJS)
|
||||
- Numismatics direct integration (lives in KubeJS — mod calls KubeJS commands to debit/credit)
|
||||
- FTB Quests integration (FTB Quests handles its own UI for the progression campaign)
|
||||
- FTB Ranks admin (FTB Ranks handles permissions directly; mod just reads them)
|
||||
- **Sethome / `/back` / fast-travel features** — explicitly removed. The pack uses **Waystones** as the only travel mechanic. Restrictive travel is a design feature for a Create Aeronautics pack: airships, trains, and vehicles are the intended way to cover distance.
|
||||
|
||||
---
|
||||
|
||||
## 2. High-level architecture
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Player (client) │
|
||||
└──────────┬───────────┘
|
||||
│ keybind / NPC click / chunk click
|
||||
▼
|
||||
┌───────────────────────────────────┐
|
||||
│ bnstoolkit (client) │
|
||||
│ • FTB Library UI screens │
|
||||
│ • FTB Chunks map mixin │
|
||||
│ • Quest progress HUD │
|
||||
└───────────────┬───────────────────┘
|
||||
│ network packets
|
||||
▼
|
||||
┌───────────────────────────────────┐
|
||||
│ bnstoolkit (server) │
|
||||
│ • Plot ownership data │
|
||||
│ • Sethome data │
|
||||
│ • Network packet handlers │
|
||||
│ • /bnstoolkit reload command │
|
||||
└─────┬───────────────────────┬─────┘
|
||||
│ │
|
||||
reads │ │ calls KubeJS commands /
|
||||
FTB Ranks / │ │ fires events for KubeJS
|
||||
FTB Chunks / │ │ to react to
|
||||
Numismatics ▼ ▼
|
||||
┌───────────────┐ ┌──────────────────────┐
|
||||
│ FTB / NeoForge │ │ KubeJS scripts │
|
||||
│ stack │ │ • Bounty engine │
|
||||
└───────────────┘ │ • Quest state │
|
||||
│ • Numismatics calls │
|
||||
│ • Villager trades │
|
||||
│ • Tier promotion │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
Key invariant: **the mod owns UI + plot data + sethomes. KubeJS owns
|
||||
gameplay logic.** They communicate via network packets and KubeJS
|
||||
commands. The mod never directly mutates Numismatics balances or quest
|
||||
state — it asks KubeJS to do that via commands.
|
||||
|
||||
---
|
||||
|
||||
## 3. Package layout
|
||||
|
||||
```
|
||||
bnstoolkit/
|
||||
├── build.gradle NeoForge 1.21.1 + FTB Library dep
|
||||
├── src/main/java/uk/sijbers/bnstoolkit/
|
||||
│ ├── BnsToolkit.java Mod entry, lifecycle
|
||||
│ ├── client/
|
||||
│ │ ├── ClientEvents.java Keybinds, client setup
|
||||
│ │ ├── screen/
|
||||
│ │ │ ├── QuestLogScreen.java
|
||||
│ │ │ ├── BountyBoardScreen.java
|
||||
│ │ │ ├── TierUpgradeScreen.java
|
||||
│ │ │ ├── PlotPurchaseScreen.java
|
||||
│ │ │ ├── ShopBrowserScreen.java
|
||||
│ │ │ └── widgets/ (subclass FTB Library widgets)
|
||||
│ │ ├── hud/
|
||||
│ │ │ └── QuestProgressHud.java IGuiOverlay
|
||||
│ │ └── mixin/
|
||||
│ │ ├── ChunkScreenPanelMixin.java
|
||||
│ │ └── ChunkButtonMixin.java
|
||||
│ ├── server/
|
||||
│ │ ├── command/
|
||||
│ │ │ └── BnsToolkitCommand.java /bnstoolkit reload, debug
|
||||
│ │ ├── data/
|
||||
│ │ │ ├── PlotRegistry.java loads plots.json
|
||||
│ │ │ └── PlotOwnership.java SavedData, plot owner UUIDs
|
||||
│ │ ├── event/
|
||||
│ │ │ ├── PlayerJoinHandler.java tier-based join broadcast
|
||||
│ │ │ └── ChatColourHandler.java tier-based chat name colouring
|
||||
│ │ └── net/
|
||||
│ │ └── (packet implementations — see §6)
|
||||
│ ├── common/
|
||||
│ │ ├── data/
|
||||
│ │ │ ├── TierConfig.java loads tiers.json
|
||||
│ │ │ ├── BountyDef.java DTO mirrored from KubeJS
|
||||
│ │ │ ├── QuestStateView.java snapshot for UI display
|
||||
│ │ │ └── PlotDef.java plot definition (region, price)
|
||||
│ │ └── net/
|
||||
│ │ └── BnsPacket.java network channel + payload base
|
||||
│ └── resources/
|
||||
│ ├── META-INF/neoforge.mods.toml
|
||||
│ ├── bnstoolkit.mixins.json
|
||||
│ └── assets/bnstoolkit/lang/en_us.json
|
||||
└── src/main/resources/
|
||||
└── data-config/ packed defaults, copied on first run
|
||||
├── tiers.json
|
||||
└── plots.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Screen specifications
|
||||
|
||||
All screens extend FTB Library's `BaseScreen`. They share a common
|
||||
header (player name, current tier, spur balance) so navigation feels
|
||||
consistent.
|
||||
|
||||
### 4.1 Quest Log
|
||||
|
||||
**Opened by:** keybind (default Q+B?) or right-clicking a Quest Log
|
||||
block placed at any guild building.
|
||||
|
||||
**Layout:** three tabs across the top — Active / Available / Completed.
|
||||
|
||||
- **Active tab:** list of currently-accepted bounties with progress
|
||||
bars. Each row clickable; click expands a detail panel with
|
||||
description, source guild, reward, and a `Cancel quest` button.
|
||||
Cancel triggers a confirmation modal warning about cooldown.
|
||||
- **Available tab:** list of bounties currently offered (rotated daily
|
||||
by the KubeJS engine). Each row shows the bounty source, reward,
|
||||
cooldown remaining (if any). Click → detail panel with `Accept` button.
|
||||
- **Completed tab:** historical log of completed bounties this week,
|
||||
for satisfaction value. No interaction beyond viewing.
|
||||
|
||||
**Network calls:**
|
||||
- On open: client requests current quest state from server (`S2CQuestSnapshot`)
|
||||
- On accept/cancel click: `C2SAcceptBounty(id)` / `C2SCancelQuest(id)`
|
||||
|
||||
### 4.2 Bounty Board
|
||||
|
||||
**Opened by:** right-clicking the Adventurer's Guild Quartermaster
|
||||
NPC (Easy NPC dialog action runs `/bnstoolkit openboard adventurers_guild`)
|
||||
OR clicking an in-world `bnstoolkit:bounty_board` block.
|
||||
|
||||
**Layout:** poster-board style — the day's available bounties as
|
||||
visual "cards" laid out in a grid, each with mob icon, name,
|
||||
description, reward, accept button. Friendly to non-tech users.
|
||||
|
||||
**Network calls:**
|
||||
- On open: `S2CBountyBoardSnapshot(guild_id)` returns today's bounty list
|
||||
- On accept: same `C2SAcceptBounty(id)` as Quest Log
|
||||
|
||||
### 4.3 Tier Upgrade Screen
|
||||
|
||||
**Opened by:** right-clicking the Civic Office Magistrate NPC.
|
||||
|
||||
**Layout:** full ladder shown as a vertical or horizontal track. Your
|
||||
current tier highlighted (glowing badge). Next tier shows cost +
|
||||
delta perks (e.g. "+50 chunks, +1 plot slot, +2 sethomes, +1 bounty,
|
||||
-2% shop fee, chat colour: yellow"). Big `Upgrade to <Tier>` button
|
||||
at the bottom, greyed out if you don't have the spurs.
|
||||
|
||||
Hovering any future tier shows its full perk list in a side panel.
|
||||
|
||||
**Network calls:**
|
||||
- On open: `S2CTierSnapshot` returns current tier + balance
|
||||
- On upgrade click: `C2SRequestTierUpgrade` → server verifies balance,
|
||||
calls KubeJS `/bnstoolkit_internal upgrade_rank <uuid>` which
|
||||
charges spurs and runs `ftbranks add <player> <tier>`
|
||||
|
||||
### 4.4 Plot Purchase Popup
|
||||
|
||||
**Opened by:** clicking an unowned plot region in the FTB Chunks map
|
||||
UI (via the mod's mixin into `ChunkScreenPanel`).
|
||||
|
||||
**Layout:** modal overlay on top of the FTB Chunks map. Shows: plot
|
||||
ID, plot type (standard/premium), region size in chunks, owner (none),
|
||||
price. Your current spur balance. Your remaining plot slots (e.g.
|
||||
"2 of 3 used"). `Purchase` and `Cancel` buttons. If you're at your
|
||||
slot cap or don't have enough spurs, `Purchase` is greyed out with
|
||||
the reason shown.
|
||||
|
||||
**Network calls:**
|
||||
- On purchase: `C2SBuyPlot(plot_id)` → server checks slot capacity,
|
||||
asks KubeJS to charge spurs, on success records ownership in
|
||||
`PlotOwnership` SavedData.
|
||||
|
||||
### 4.5 Shop Browser
|
||||
|
||||
**Opened by:** right-clicking the Bazaar Master NPC at the Merchant's
|
||||
Bazaar (provides a curated view of all currently-listed shop blocks
|
||||
+ NPC sell shop).
|
||||
|
||||
**Layout:** searchable item list with current prices, sort by
|
||||
category. Click an item to see who's selling it / where the shop
|
||||
block is located. The NPC sell shop (diamonds, netherite etc.) shows
|
||||
as a special "Official Bazaar" category with fixed prices and the
|
||||
current diminishing-return multiplier per item.
|
||||
|
||||
**Note:** individual Numismatics shop blocks keep their native UI
|
||||
when right-clicked — this Bazaar Master is the *curated central
|
||||
view*, not a replacement for shop block interaction.
|
||||
|
||||
**Network calls:**
|
||||
- On open: `S2CShopBrowserSnapshot` (asks KubeJS for current listings)
|
||||
- Selecting an item → `S2CShopItemDetail(item_id)` for full info
|
||||
|
||||
### 4.6 Quest Progress HUD
|
||||
|
||||
**Render layer:** `IGuiOverlay`, drawn over the vanilla HUD.
|
||||
|
||||
**Layout:** small panel in top-right (configurable position) showing
|
||||
each active quest as a row — quest name + progress bar + N/M numeric.
|
||||
Fades on inactive (no progress in last 30s) so it doesn't visually
|
||||
clutter when you're not actively bounty-hunting. Re-appears the
|
||||
moment any quest progresses.
|
||||
|
||||
**State source:** KubeJS pushes updates via `S2CHudQuestUpdate` packet
|
||||
when any quest's progress changes (debounced to once per second
|
||||
maximum per quest). The HUD reads from a client-side cache.
|
||||
|
||||
---
|
||||
|
||||
## 5. Mixin targets
|
||||
|
||||
Minimal — we don't fork FTB Chunks, we inject only what we need.
|
||||
|
||||
| Target | Method | Purpose | Mixin file |
|
||||
|---|---|---|---|
|
||||
| `ChunkScreenPanel` | `drawBackground` (TAIL) | Overlay drawing — cost preview for the currently-hovered or selected chunk, "Your plot slots: N of M" text in a corner | `ChunkScreenPanelMixin.java` |
|
||||
| `ChunkScreenPanel$ChunkButton` | `addMouseOverText` (TAIL) | Append "Cost: N spurs" or "Plot: <name>" to the per-chunk hover tooltip | `ChunkButtonMixin.java` |
|
||||
| `ChunkScreenPanel$ChunkButton` | `onClicked` (HEAD, with cancellation if applicable) | If the clicked chunk is a plot region, open `PlotPurchaseScreen` instead of triggering a regular claim | `ChunkButtonMixin.java` |
|
||||
|
||||
That's it. Three injection points across two mixin files. No
|
||||
modification of FTB Library, FTB Quests, FTB Ranks, or FTB Teams.
|
||||
|
||||
---
|
||||
|
||||
## 6. Network packet contract
|
||||
|
||||
NeoForge `PayloadRegistrar`. All packets versioned via a single
|
||||
`PROTOCOL_VERSION` constant; bump on schema change.
|
||||
|
||||
### Server → Client
|
||||
|
||||
| Packet | Payload | When sent |
|
||||
|---|---|---|
|
||||
| `S2CQuestSnapshot` | `List<QuestStateView>` | On Quest Log open, on any quest-state change |
|
||||
| `S2CBountyBoardSnapshot` | `List<BountyDef>` (5-10 entries) | On Bounty Board open |
|
||||
| `S2CTierSnapshot` | `(currentTier, balance, perksByTier)` | On Tier Upgrade Screen open |
|
||||
| `S2CShopBrowserSnapshot` | `List<ShopListing>` | On Shop Browser open |
|
||||
| `S2CHudQuestUpdate` | `(questId, progress, target)` | On quest progress (debounced 1s) |
|
||||
| `S2CPlotPurchaseResult` | `(success, message)` | After `C2SBuyPlot` resolves |
|
||||
| `S2CTierUpgradeResult` | `(success, message)` | After `C2SRequestTierUpgrade` resolves |
|
||||
|
||||
### Client → Server
|
||||
|
||||
| Packet | Payload | What server does |
|
||||
|---|---|---|
|
||||
| `C2SAcceptBounty` | `questId` | Call `/bnstoolkit_internal accept_bounty <uuid> <questId>` (KubeJS) |
|
||||
| `C2SCancelQuest` | `questId` | Call `/bnstoolkit_internal cancel_quest <uuid> <questId>` (KubeJS) |
|
||||
| `C2SBuyPlot` | `plotId` | Verify slot capacity, ask KubeJS to debit spurs, record ownership |
|
||||
| `C2SRequestTierUpgrade` | (none — server knows player) | Verify balance, ask KubeJS to debit + promote |
|
||||
| `C2SOpenBoard` | `guildId` (optional context) | Reply with appropriate snapshot |
|
||||
|
||||
All C2S packets include the player implicitly (NeoForge passes
|
||||
the `ServerPlayer` in the handler).
|
||||
|
||||
---
|
||||
|
||||
## 7. Data persistence
|
||||
|
||||
| Data | Storage | Lifetime |
|
||||
|---|---|---|
|
||||
| Plot ownership | `bnstoolkit:plots` — vanilla NeoForge `SavedData` attached to overworld | Persists with world. Authoritative source. |
|
||||
| Quest state | KubeJS-managed JSON at `world/data/bns_economy/quests/<uuid>.json` | Not the mod's concern — the mod only reads via packets |
|
||||
| Tier configuration | `config/bnstoolkit/tiers.json` (hot-reloadable) | Until edited |
|
||||
| Plot definitions | `config/bnstoolkit/plots.json` (hot-reloadable via `/bnstoolkit reload`) | Until edited |
|
||||
|
||||
Plot ownership is server-authoritative. Clients only get views via
|
||||
packets — they never write to these stores directly.
|
||||
|
||||
---
|
||||
|
||||
## 8. Configuration files
|
||||
|
||||
### `config/bnstoolkit/tiers.json`
|
||||
|
||||
Defines the 13-tier ladder. Hot-reloadable.
|
||||
|
||||
```json5
|
||||
{
|
||||
"tiers": [
|
||||
{
|
||||
"id": "peasant",
|
||||
"displayName": "Peasant",
|
||||
"cost": 0,
|
||||
"perks": {
|
||||
"wildernessChunks": 9,
|
||||
"plotSlots": 0,
|
||||
"dailyBountySlots": 3,
|
||||
"shopFeeDiscount": 0,
|
||||
"chatColour": "grey",
|
||||
"joinBroadcast": "none"
|
||||
}
|
||||
},
|
||||
/* ... 12 more tiers ... */
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `config/bnstoolkit/plots.json`
|
||||
|
||||
Defines spawn plot regions. Each plot has a unique ID, region bounds
|
||||
(chunk coordinates), type (standard/premium), and price.
|
||||
|
||||
```json5
|
||||
{
|
||||
"plots": [
|
||||
{
|
||||
"id": "bazaar_stall_01",
|
||||
"displayName": "Bazaar Stall 1",
|
||||
"type": "standard",
|
||||
"price": 2000,
|
||||
"chunks": [[10, 20], [10, 21]], // chunk coords, inclusive
|
||||
"world": "minecraft:overworld"
|
||||
},
|
||||
{
|
||||
"id": "bazaar_corner_premium",
|
||||
"displayName": "Premium Corner Plot",
|
||||
"type": "premium",
|
||||
"price": 5000,
|
||||
"chunks": [[15, 25], [15, 26], [16, 25], [16, 26]],
|
||||
"world": "minecraft:overworld"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. KubeJS integration surface
|
||||
|
||||
The mod calls into KubeJS via custom commands registered by KubeJS
|
||||
(`ServerEvents.commandRegistry`). All have the `bnstoolkit_internal`
|
||||
prefix to mark them as not-for-player-use:
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `/bnstoolkit_internal accept_bounty <uuid> <questId>` | Mark quest as ACTIVE in KubeJS state |
|
||||
| `/bnstoolkit_internal cancel_quest <uuid> <questId>` | Cancel quest, apply cooldown |
|
||||
| `/bnstoolkit_internal complete_quest <uuid> <questId>` | Mark quest READY (for testing/manual) |
|
||||
| `/bnstoolkit_internal turn_in_quest <uuid> <questId>` | Pay reward via Numismatics |
|
||||
| `/bnstoolkit_internal debit <uuid> <amount> <reason>` | Charge spurs (used by plot purchase, tier upgrade) |
|
||||
| `/bnstoolkit_internal credit <uuid> <amount> <reason>` | Pay spurs |
|
||||
| `/bnstoolkit_internal upgrade_rank <uuid> <tierId>` | Promote via FTB Ranks |
|
||||
| `/bnstoolkit_internal get_balance <uuid>` | Returns current spur balance (used for screen header) |
|
||||
|
||||
All commands return a JSON result via the command output that the
|
||||
mod parses. KubeJS handles the actual Numismatics + FTB Ranks calls
|
||||
(it already has the `Java.loadClass` helpers from the economy KubeJS
|
||||
scripts).
|
||||
|
||||
KubeJS in turn pushes events to the mod via custom payload packets
|
||||
when quest state changes (so the HUD can update without polling).
|
||||
|
||||
---
|
||||
|
||||
## 10. Build & release
|
||||
|
||||
- **Repo:** new private Gitea repo `minecraft/bnstoolkit` (separate
|
||||
from the modpack repo for clean release-versioning, but mirrored
|
||||
to the modpack's `pack/overrides/mods/` on each release)
|
||||
- **Build:** standard NeoForge 1.21.1 ModDevGradle template
|
||||
- **Dependencies:** Architectury API (provided), FTB Library
|
||||
(provided), Numismatics (compileOnly), FTB Chunks (compileOnly)
|
||||
- **CI:** Gitea Actions runs `./gradlew build` on push to main,
|
||||
uploads the jar as a release artifact
|
||||
- **Modpack integration:** the modpack's bump-version flow includes
|
||||
pulling the latest bnstoolkit release jar and updating
|
||||
`pack.lock.json` / manifest like any other mod
|
||||
- **Versioning:** semantic — `1.0.0` for V1 ship, `1.x.y` for
|
||||
patches, `2.0.0` if we ever change the network protocol or data
|
||||
format
|
||||
|
||||
---
|
||||
|
||||
## 11. Perk specifications (detailed)
|
||||
|
||||
Every perk listed in the 13-tier ladder, expanded with mechanics,
|
||||
implementation, and edge cases.
|
||||
|
||||
### 11.1 Wilderness chunks
|
||||
|
||||
**What it does:** caps how many wilderness chunks (outside spawn)
|
||||
a player can claim via the FTB Chunks map. Claiming within the cap
|
||||
is free; over the cap is refused with an in-game message.
|
||||
|
||||
**Implementation:** FTB Ranks permission node
|
||||
`ftbchunks.max_claimed_chunks` set per rank in the FTB Ranks config
|
||||
(written by the mod on first launch from `tiers.json`). FTB Chunks
|
||||
reads this permission natively — no mod-side enforcement needed.
|
||||
|
||||
**Edge case:** if a player downgrades (which they can't normally,
|
||||
but might happen via op command), they keep already-claimed chunks
|
||||
over the new cap, but can't claim more until they free some.
|
||||
|
||||
### 11.2 Plot slots
|
||||
|
||||
**What it does:** caps how many spawn plots (predefined regions at
|
||||
spawn, see `plots.json`) a player can own concurrently. Independent
|
||||
of wilderness chunks.
|
||||
|
||||
**Implementation:** stored in `PlotOwnership` SavedData. On purchase,
|
||||
mod checks `playerOwnedPlots.size() < tier.plotSlots`. If at cap,
|
||||
the Plot Purchase Popup shows the slot count and disables the
|
||||
`Purchase` button.
|
||||
|
||||
**Releasing a plot:** players can sell their plot back via a "Release"
|
||||
button in the plot-detail screen (opens when they click their own
|
||||
plot in the map). Releases the slot. Refund: 50% of purchase price.
|
||||
|
||||
### 11.3 Daily bounty slots
|
||||
|
||||
**What it does:** how many bounties a player can have ACTIVE
|
||||
simultaneously. Once the slot is freed (by completion or cancel),
|
||||
the player can accept another from any guild board.
|
||||
|
||||
**Implementation:** enforced in KubeJS bounty engine. The mod's
|
||||
Bounty Board and Quest Log screens read the slot count via packet
|
||||
and grey out the Accept button when at cap.
|
||||
|
||||
**Edge case:** a quest in READY state still counts against your
|
||||
active slots until you turn it in. This is intentional — pushes
|
||||
players to actually visit the NPC to claim.
|
||||
|
||||
### 11.4 Shop fee discount
|
||||
|
||||
**What it does:** reduces the "merchant cut" deducted when you sell
|
||||
items at the Bazaar's NPC sell shop. The Bazaar charges a base 25%
|
||||
fee on all sales; your tier provides a discount that reduces this
|
||||
toward zero.
|
||||
|
||||
**Worked example:** you sell a diamond worth 10 spurs at the Bazaar.
|
||||
- Peasant (0% discount): 25% fee → you receive 7.5 spurs (rounded
|
||||
down to 7).
|
||||
- Knight (-5% discount): 20% fee → 8 spurs.
|
||||
- Sovereign (-25% discount): 0% fee → full 10 spurs.
|
||||
|
||||
**Implementation:** KubeJS sell-shop handler reads the seller's tier
|
||||
and computes effective payout. Rounded down to nearest integer.
|
||||
|
||||
**Edge case:** player-to-player Numismatics shop blocks do NOT
|
||||
apply this fee — those are direct peer transactions, no Bazaar
|
||||
middleman. The fee only applies to the official Bazaar sell shop.
|
||||
|
||||
### 11.5 Chat colour
|
||||
|
||||
**What it does:** the player's name in chat appears in this colour,
|
||||
visible to everyone. Visual rank marker.
|
||||
|
||||
**Implementation:** KubeJS `PlayerEvents.chat` event modifies the
|
||||
chat component for the player's display name based on their tier
|
||||
permission (read from FTB Ranks). The mod also colours the player's
|
||||
nameplate above their head (via a small client packet on tier change
|
||||
that other clients use to set the display name colour locally).
|
||||
|
||||
**Colour ladder:**
|
||||
- 1-2 Peasant/Farmer: grey
|
||||
- 3 Citizen: white
|
||||
- 4 Merchant: yellow
|
||||
- 5 Knight: green
|
||||
- 6 Baron: cyan
|
||||
- 7 Viscount: blue
|
||||
- 8 Earl: purple
|
||||
- 9 Marquess: gold
|
||||
- 10 Duke: orange
|
||||
- 11 Archduke: red
|
||||
- 12 Grand Duke: bright red
|
||||
- 13 Sovereign: rainbow (animated via colour-cycling text component)
|
||||
|
||||
**Edge case:** the Founder cosmetic `[Founder]` prefix sits before
|
||||
the coloured name, in a distinct colour (probably bright yellow or
|
||||
white-bold). Sovereign + Founder is visually busy but acceptable —
|
||||
only one player on the server will have both.
|
||||
|
||||
### 11.6 Join broadcast
|
||||
|
||||
**What it does:** when the player logs in, this controls what message
|
||||
goes to everyone else on the server.
|
||||
|
||||
**Levels:**
|
||||
|
||||
- **none:** standard vanilla `<player> joined the game`.
|
||||
- **simple:** rank-prefixed message in their chat colour. Example:
|
||||
```
|
||||
[Knight] Matt has logged in.
|
||||
```
|
||||
- **fancy:** multi-line, with a separator. Example:
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
[Viscount] Matt has arrived
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
- **epic:** multi-line, with a sound effect (e.g. trumpet fanfare,
|
||||
configurable per-tier). Larger ASCII separator. Possibly an ASCII
|
||||
art banner of the rank.
|
||||
- **epic + particles:** as epic, plus a 5-second particle effect at
|
||||
the player's spawn location (firework spark, end-rod sparkle, etc.)
|
||||
for everyone nearby to see.
|
||||
- **epic + spectacular:** as epic + particles, plus a server-wide
|
||||
effect — actionbar message visible to everyone, brief light-up of
|
||||
the sky if Sovereign joins.
|
||||
|
||||
**Implementation:** KubeJS `PlayerEvents.loggedIn` reads tier and
|
||||
dispatches the appropriate message. Particle/sound effects are
|
||||
server-side commands (`/particle`, `/playsound`).
|
||||
|
||||
### 11.7 Future perk dimensions (not in V1, but easy to add later)
|
||||
|
||||
Architecture supports adding new perk columns in `tiers.json`
|
||||
without code changes (the schema is open-ended). Candidates for
|
||||
later expansion:
|
||||
|
||||
- **Waystone slot cap** — limit how many waystones a player can place
|
||||
and/or own. Starter waystone is granted; higher tiers unlock 2nd,
|
||||
3rd, etc. Cleanest in-theme perk for a Create Aeronautics pack
|
||||
where restrictive travel is intentional.
|
||||
- **Waystone XP cost reduction** — vanilla Waystones charges XP per
|
||||
teleport. Tier-based discount on this cost (or eventual exemption
|
||||
for Sovereign).
|
||||
- **Particle trail** — high tiers leave a visible particle wake when
|
||||
walking
|
||||
- **Custom join sound** — different sound effect per tier on login
|
||||
- **Custom title prefix** — text added before chat name beyond the
|
||||
colour (e.g. `⚔ Knight Matt`)
|
||||
- **Priority bounty access** — higher tiers see a larger pool of
|
||||
daily bounties to pick from
|
||||
- **Tier-gated vendor stock** — specific Numismatics shops only
|
||||
visible to Baron+ etc.
|
||||
- **Daily login bonus** — small spur reward for logging in, scaling
|
||||
by tier
|
||||
- **Death keep-inventory chance** — RNG % to keep inventory on death
|
||||
(controversial, gate carefully)
|
||||
|
||||
---
|
||||
|
||||
## 12. Open implementation questions
|
||||
|
||||
These can be deferred until they become blocking, but worth flagging:
|
||||
|
||||
1. **Sovereign tier rainbow chat name** — Minecraft text components
|
||||
don't natively support gradient text. Implementation options:
|
||||
(a) cycle through colours character by character, or (b) animate
|
||||
via packet on a tick interval. Choose during Phase 0c scaffolding.
|
||||
2. **Plot release refund percentage** — proposed 50%. Tune via
|
||||
playtest.
|
||||
3. **Sethome cooldown values per tier** — proposed scaling from 60s
|
||||
(low tiers) to 5s (Sovereign). Confirm in `tiers.json` schema.
|
||||
4. **`/back` interaction with dimension changes** — does dying in
|
||||
the End and `/back`ing return you to the End or to overworld?
|
||||
Default: returns to wherever you died including the dimension.
|
||||
5. **Founder cosmetic prefix colour** — bright yellow? Bold white?
|
||||
Animated? Settle during implementation.
|
||||
6. **Sovereign spectacular join effect** — exact effects need design
|
||||
pass (sky flash? Server-wide actionbar text?). Tune in
|
||||
`tiers.json`.
|
||||
|
||||
---
|
||||
|
||||
## 13. Implementation phases (mod-specific)
|
||||
|
||||
Aligned with the economy doc's Phase 0c (mod scaffolding):
|
||||
|
||||
| Mod phase | Deliverable |
|
||||
|---|---|
|
||||
| **M0** | Gradle project, mod metadata, FTB Library dep wired, network channel registered, empty placeholder for each screen class. Builds + loads cleanly into the running pack. |
|
||||
| **M1** | Tier system: TierConfig loads from `tiers.json`, `TierUpgradeScreen` works, perks applied via FTB Ranks promotion command. Founder cosmetic prefix. Chat colour. Join broadcast at all tiers. |
|
||||
| **M2** | Plot system: `PlotRegistry` + `PlotOwnership`, FTB Chunks mixins, `PlotPurchaseScreen`. KubeJS integration for spur debit. |
|
||||
| **M3** | Quest UI: `QuestLogScreen`, `BountyBoardScreen`, `QuestProgressHud`. Network packets between KubeJS bounty engine and the mod. |
|
||||
| **M4** | Shop browser: `ShopBrowserScreen`. |
|
||||
| **M5** | Polish — animations, sound effects, particle effects for high tiers, theming pass on all screens. |
|
||||
|
||||
Each phase is independently shippable as a `1.x.0` release of the
|
||||
mod jar.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [`economy-system.md`](./economy-system.md) — gameplay-side design (currency, quest mechanics, villager rebalance, etc.)
|
||||
- `MEMORY.md` entries — KubeJS Rhino gotchas, defaultconfigs pattern
|
||||
@@ -0,0 +1,114 @@
|
||||
# bnstoolkit feature-complete — testing notes for 2026-06-07 evening
|
||||
|
||||
> Shipped: pack v0.35.0 / bnstoolkit 0.3.1.
|
||||
> Repo: http://10.16.5.102:3000/minecraft/bnstoolkit
|
||||
> Server status: ✅ live + verified at 02:41 UTC.
|
||||
|
||||
## What's in the build
|
||||
|
||||
### 5 screens (FTB Library widgets — matches FTB Quests look)
|
||||
|
||||
All opened via key bindings. Rebindable in vanilla controls menu under
|
||||
"Brass and Sigil" category.
|
||||
|
||||
| Key | Screen | What it does |
|
||||
|---|---|---|
|
||||
| **K** | Civic Tier ladder | 13 rows, current tier highlighted. Right pane shows next tier's cost + perks + Upgrade button. Disabled if you can't afford it or are at Sovereign. |
|
||||
| **J** | Bounty Board | Today's 5-bounty pool + any active set. Detail pane: target, count, reward, progress. Accept / Cancel / Turn In buttons by state. |
|
||||
| **N** | Plot Office | Plot list with ownership state-coding (yours / taken / free). Detail pane: size, chunks, price. Buy / Release buttons. |
|
||||
| **H** | Bazaar shop | 9-cell sell item grid. Click an item, see DR-aware effective price for current tier. Sell 1 / 16 / 64 quick buttons. |
|
||||
| **M** | Quest Log | At-a-glance status: tier + balance + active bounties (with progress bars) + owned plots. |
|
||||
|
||||
### HUD overlay
|
||||
|
||||
Top-left of the screen, always visible: `<tier-colour><tier-name>` on
|
||||
one line, `<balance> spurs` on the second. Refreshes 1Hz from server
|
||||
push — picks up coin pickups immediately, no inventory polling.
|
||||
|
||||
Auto-hides when F1 is pressed or chat is open (standard NeoForge HUD
|
||||
layer behavior).
|
||||
|
||||
### Villager trade rebalance
|
||||
|
||||
Every emerald cost or result in vanilla + modded villager trades is
|
||||
now Numismatics:
|
||||
- 1..7 emeralds → spurs
|
||||
- 8..63 → bevels (1 bevel = 8 spurs)
|
||||
- 64+ → cogs (1 cog = 64 spurs)
|
||||
|
||||
Works automatically on:
|
||||
- All 14 vanilla villager professions
|
||||
- Wandering trader (generic + rare)
|
||||
- Any modded villager professions (we wrap the event's list, no enum
|
||||
of professions needed)
|
||||
|
||||
This was the "blocked on API" item. Built it into bnstoolkit instead
|
||||
of pulling a third-party mod, because the cleanest Modrinth option
|
||||
needed NeoForge 21.1.230+ and we're on 228.
|
||||
|
||||
## Testing path
|
||||
|
||||
```
|
||||
/bns me # confirm Peasant tier 1
|
||||
/give @s numismatics:spur 500 # give spurs for upgrades
|
||||
K # open tier ladder
|
||||
# click Upgrade — should pay 200 and bump you to Farmer
|
||||
|
||||
/bns admin tier set <you> 5 # jump to Knight for plot testing
|
||||
K # confirm new tier visible
|
||||
H # open bazaar
|
||||
# click diamond, click Sell 1 (you'll need a diamond in inventory)
|
||||
J # open bounty board, accept one
|
||||
# kill the target mobs — progress shows in chat AND the screen
|
||||
# turn in via the J screen
|
||||
N # open plot office, buy p001
|
||||
M # quest log shows everything
|
||||
|
||||
# Test villager trades:
|
||||
/summon villager ~ ~ ~
|
||||
# Right-click the villager once it picks a profession (place a workstation)
|
||||
# Trades should show spurs/bevels/cogs instead of emeralds
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
KubeJS server_scripts/economy/ ← single source of truth for state + logic
|
||||
↑
|
||||
│ (chat commands via server.getCommands)
|
||||
│
|
||||
ServerEconomyBridge.java ← reads NBT, sums Numismatics coins,
|
||||
routes write verbs through /bns
|
||||
↑↓
|
||||
4 snapshot + 2 command packets
|
||||
↑↓
|
||||
ClientBnsState.java ← single static state holder
|
||||
↑
|
||||
Screens read this each frame
|
||||
```
|
||||
|
||||
Source of truth stays in KubeJS. Java is just the UI layer and a thin
|
||||
write-path proxy. If you want to rebalance prices or add bounties, you
|
||||
do it in KubeJS — Java auto-reflects.
|
||||
|
||||
## Known gaps for future iteration
|
||||
|
||||
| Gap | Where to fix |
|
||||
|---|---|
|
||||
| No client toast notifications — feedback comes via KubeJS chat messages | Add a dist-isolated `ClientToastDispatcher` class (0.4.0) |
|
||||
| Bazaar grid doesn't tooltip the per-unit price | One `addMouseOverText` per `ItemCell` |
|
||||
| Plot office doesn't show a minimap | Render bluemap PNG snippet as background or use FTB Chunks' map widget |
|
||||
| Quest log doesn't show FTB Quests progression | Hook into FTB Quests' API once first chapter ships |
|
||||
| Numismatics coins as bunch — no fractional change | Bazaar pays in highest denom that fits; player converts at the Bank |
|
||||
| Wanderer rare-trade rebalance is greedy (every rare item costs are converted) | Profession-aware exclusion list if anything weird sneaks through |
|
||||
|
||||
## File map
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `pack/overrides/mods/bnstoolkit-0.3.1.jar` | The mod itself (95 KB) |
|
||||
| `pack/overrides/kubejs/server_scripts/economy/*.js` | State + business logic (unchanged from v0.33.0) |
|
||||
| `pack/overrides/world/serverconfig/ftbranks/ranks.snbt` | 13-tier rank config (unchanged) |
|
||||
| `docs/bnstoolkit-architecture.md` | Original architecture spec |
|
||||
|
||||
Source repo: http://10.16.5.102:3000/minecraft/bnstoolkit (main = 14dc69f at time of pack ship).
|
||||
@@ -0,0 +1,215 @@
|
||||
# Economy V1 — overnight build handoff
|
||||
|
||||
> Built 2026-06-06 / 23:00-23:55 UTC. All shipped via PR #67 (v0.33.0).
|
||||
> Server running clean at port 25565. All scripts verified loaded with 0 errors.
|
||||
|
||||
## TL;DR — what to test tomorrow
|
||||
|
||||
A full command-driven economy is live. Pure-server (no client mod yet), works in chat and over the Telegram bridge equally. Open the in-game chat or telnet and try:
|
||||
|
||||
```
|
||||
/bns help ← top of the iceberg
|
||||
/bns me ← your status
|
||||
```
|
||||
|
||||
That alone surfaces every command available. Below is the deeper walkthrough.
|
||||
|
||||
---
|
||||
|
||||
## What ships
|
||||
|
||||
### 1. 13-tier civic ladder (`/bns tier upgrade`)
|
||||
|
||||
Every player has a tier integer (1-13) in their NBT. New joiners are auto-set to tier 1 (Peasant). Upgrading costs spurs and runs `ftbranks add <player> <rank>` to apply the rank's permission nodes (chunk allowance, name format).
|
||||
|
||||
| # | Title | Cost to reach | Chunk allowance | Plot slots | Bounty slots | Shop fee |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1 | Peasant | 0 | 9 | 0 | 3 | 25% |
|
||||
| 2 | Farmer | 200 | 18 | 0 | 3 | 25% |
|
||||
| 3 | Citizen | 750 | 35 | 1 | 3 | 25% |
|
||||
| 4 | Merchant | 2,500 | 60 | 1 | 4 | 23% |
|
||||
| 5 | Knight | 6,500 | 100 | 2 | 4 | 20% |
|
||||
| 6 | Baron | 15,000 | 150 | 2 | 5 | 18% |
|
||||
| 7 | Viscount | 35,000 | 220 | 3 | 5 | 16% |
|
||||
| 8 | Earl | 75,000 | 300 | 3 | 6 | 14% |
|
||||
| 9 | Marquess | 150,000 | 400 | 4 | 6 | 12% |
|
||||
| 10 | Duke | 300,000 | 525 | 5 | 7 | 10% |
|
||||
| 11 | Archduke | 700,000 | 700 | 6 | 7 | 8% |
|
||||
| 12 | Grand Duke | 1,500,000 | 900 | 7 | 8 | 6% |
|
||||
| 13 | Sovereign | 10,000,000 | 1,500 | 10 | 10 | 0% |
|
||||
|
||||
**Test it:**
|
||||
- `/bns me` shows your tier + balance + everything else
|
||||
- `/bns admin tier set <player> <1-13>` to jump anyone instantly
|
||||
- `/bns admin tier list` shows the full table in-chat
|
||||
- `/give @s numismatics:spur 64` for testing money
|
||||
- `/bns tier upgrade` pays + promotes
|
||||
|
||||
### 2. Bazaar sell shop (`/bns sell`)
|
||||
|
||||
Sell raw commodities for spurs. Tier discount reduces a flat 25% Bazaar fee. Diminishing returns lower the per-unit payout as you sell more of the same item over a lifetime (10% per 64 sold, floor 50%).
|
||||
|
||||
| Item | Base price |
|
||||
|---|---|
|
||||
| Diamond | 10 spurs |
|
||||
| Netherite ingot | 100 |
|
||||
| Emerald | 1 |
|
||||
| Gold ingot | 2 |
|
||||
| Iron ingot | 1 |
|
||||
| Lapis | 1 |
|
||||
| Redstone | 1 |
|
||||
| Coal | 1 |
|
||||
| Ancient debris | 500 |
|
||||
|
||||
**Test it:**
|
||||
- `/bns sell list` — your effective prices right now (after fee + DR)
|
||||
- `/bns sell minecraft:diamond 8` sells 8 diamonds
|
||||
- Sell ~70 diamonds and watch the per-unit price drop as the DR kicks in
|
||||
|
||||
### 3. Bounty engine (`/bns bounty`)
|
||||
|
||||
20 bounties in the catalogue. Each player gets a random 5 from the pool, refreshed every 24h per-player. Slot cap = your tier (3 at Peasant, up to 10 at Sovereign).
|
||||
|
||||
States: AVAILABLE → ACTIVE → READY → COMPLETED → (24h cooldown) → AVAILABLE.
|
||||
|
||||
- Kill bounties auto-track via `EntityEvents.death` — you'll get progress messages in chat
|
||||
- Cancelling applies the same 24h cooldown as completing (no RNG-shopping)
|
||||
|
||||
Highlights from the pool:
|
||||
- Slay 10 Zombies — 15 spurs
|
||||
- Hunt 5 Plunderers — 75
|
||||
- Slay 3 Wither Skeletons — 150
|
||||
- Slay a Skreecher — 400
|
||||
- Slay the Warden — 800
|
||||
- Slay a Warped Mosco — 700
|
||||
- Slay the Farseer — 900
|
||||
- Destroy a Void Worm — 1,200
|
||||
|
||||
**Test it:**
|
||||
- `/bns bounty list` — what's on offer for you today
|
||||
- `/bns bounty accept b_zombie_10` — accept
|
||||
- Kill 10 zombies — progress chat messages every 5, ready notification at 10
|
||||
- `/bns bounty active` — see your accepted with progress
|
||||
- `/bns bounty turnin b_zombie_10` — claim 15 spurs
|
||||
|
||||
### 4. Plot system (multi-slot) (`/bns plot`)
|
||||
|
||||
Existing plot system (3 plots at chunks 10,10 / 12,10-11 / 14-15,10-11) now supports tier-based multi-ownership. Slot cap from your tier (0 at Peasant, up to 10 at Sovereign).
|
||||
|
||||
- `/bns plot list` — all plots, their state, prices
|
||||
- `/bns plot buy <id>` — claim it (must be Citizen+ for at least 1 slot)
|
||||
- `/bns plot release <id>` — 50% refund, opens slot
|
||||
- `/bns plot mine` — list your owned plots
|
||||
|
||||
You'll need at least Citizen (tier 3) for any plot. **You can't buy a plot at Peasant.**
|
||||
|
||||
### 5. Waystones + welcome (pre-existing, untouched)
|
||||
|
||||
- One starter Waystone given on first login (per existing `welcome.js`)
|
||||
- Waystone placement cap of 1 per player (per existing `waystones_policy.js`)
|
||||
- Sharestones / Portstones banned via existing config
|
||||
|
||||
### 6. FTB Ranks 13-rank config
|
||||
|
||||
Live at `world/serverconfig/ftbranks/ranks.snbt`. Each rank has:
|
||||
- `ftbchunks.max_claimed_chunks` (sets the FTB Chunks claim cap)
|
||||
- `ftbchunks.max_force_loaded_chunks` (set to ~15% of claim cap)
|
||||
- `ftbranks.name_format` (chat colour)
|
||||
|
||||
FTB Ranks auto-applies `peasant` to all players via `condition: always_active`. Tier-up commands set the rank explicitly. The `admin` rank with `condition: op` overrides everyone — you'll appear as Admin in chat regardless of your civic tier, useful for op-mode actions.
|
||||
|
||||
---
|
||||
|
||||
## What's deferred
|
||||
|
||||
### Villager trade rebalance
|
||||
|
||||
**Blocked.** KubeJS 2101.7.2 doesn't ship a villager-trade event. I verified by extracting the KubeJS jar — `ServerEvents` only exposes `command`, `loaded`, `recipes`, `registry`, `tags`, `tick`, `unloaded`. No `villagerTrades`. No KubeJS-villager-trade addon on Modrinth for NeoForge 1.21.1.
|
||||
|
||||
Options to unblock later:
|
||||
1. **Add a third-party trade-overrides mod** — needs research (Villager Trade Tables-style mod for 1.21.1 NeoForge)
|
||||
2. **Build into the custom mod** — KubeJS calls into Java mixin
|
||||
3. **Mixin via a tiny standalone mod** — minimal Java project that just rebalances trades from a JSON
|
||||
|
||||
For now: vanilla villager trades remain. The 60+ trades I drafted live in the commit history (deleted file `pack/overrides/kubejs/server_scripts/economy/01_villager_rebalance.js`, restore from git when we unblock the API).
|
||||
|
||||
### Custom UI mod (bnstoolkit)
|
||||
|
||||
**Deferred to a session where you can iterate visually.** No Java/Gradle toolchain on glados, and the screens (QuestLog, BountyBoard, TierUpgrade, PlotPurchase, ShopBrowser, HUD, FTB Chunks mixin) all need visual feedback I can't get blind overnight.
|
||||
|
||||
The complete architecture spec is in `docs/bnstoolkit-architecture.md` — when you're ready to build, that's the source of truth. The KubeJS substrate I shipped tonight is exactly the backend the mod will sit on top of.
|
||||
|
||||
### Spawn build
|
||||
|
||||
Your manual task. The economy works from anywhere right now — no spawn buildings needed.
|
||||
|
||||
### FTB Quests progression chapters
|
||||
|
||||
Needs your arc design (themes, milestones). Mod is installed; the campaign content isn't.
|
||||
|
||||
---
|
||||
|
||||
## Files added/changed (PR #67)
|
||||
|
||||
```
|
||||
pack/overrides/kubejs/server_scripts/economy/
|
||||
00_tier_system.js NEW (170 lines)
|
||||
02_sell_shop.js NEW (197 lines)
|
||||
03_bounty_engine.js NEW (347 lines)
|
||||
04_player_commands.js NEW (146 lines)
|
||||
pack/overrides/kubejs/server_scripts/
|
||||
plots.js MODIFIED (+86 lines — multi-slot)
|
||||
pack/overrides/world/serverconfig/ftbranks/
|
||||
ranks.snbt NEW (122 lines — 13 ranks)
|
||||
pack/pack.lock.json version bump → 0.33.0
|
||||
```
|
||||
|
||||
## Quick recovery if something goes wrong
|
||||
|
||||
If a script errors at startup, KubeJS logs to:
|
||||
```
|
||||
/srv/fast/brass-sigil-server/server/logs/kubejs/server.log
|
||||
```
|
||||
|
||||
To hot-reload KubeJS server scripts without a server restart:
|
||||
```
|
||||
/kubejs reload server_scripts
|
||||
```
|
||||
|
||||
To revert this PR entirely if it bricks something:
|
||||
```
|
||||
cd /home/matt/dev/brass-and-sigil
|
||||
git revert <PR-67-merge-commit-hash>
|
||||
git push origin main
|
||||
```
|
||||
|
||||
The next pack-version-sync at MC server restart will undo all the changes.
|
||||
|
||||
## Suggested testing path
|
||||
|
||||
1. `/bns me` — sanity check, you should be Peasant tier 1
|
||||
2. `/give @s numismatics:spur 500` — give yourself spurs
|
||||
3. `/bns tier upgrade` — should pay 200 and bump you to Farmer
|
||||
4. `/bns tier upgrade` again — should pay 550 (the difference up to Citizen at 750) … actually wait — the cost is per-tier-to-reach. Each upgrade costs the NEXT tier's listed cost. So Peasant → Farmer = 200, Farmer → Citizen = 750, Citizen → Merchant = 2,500. **Worth balance-checking in playtest.**
|
||||
5. `/bns admin tier set <you> 13` — jump to Sovereign for testing
|
||||
6. `/bns me` — should show Sovereign + 10 plot slots + unlimited everything
|
||||
7. `/bns plot list` — should show 3 plots available
|
||||
8. Stand in the relevant chunks (e.g. chunk 10,10), `/bns plot buy p001`
|
||||
9. `/bns bounty list` — random 5 from pool
|
||||
10. Accept one, kill enough mobs, turn in
|
||||
11. `/bns sell list` — see effective prices
|
||||
12. `/bns sell minecraft:diamond 4` (if you have diamonds)
|
||||
|
||||
If any step fails, the KubeJS log tells you why. Ping me with the error and I'll patch tomorrow.
|
||||
|
||||
---
|
||||
|
||||
## What I'd build next session
|
||||
|
||||
In priority order:
|
||||
|
||||
1. **bnstoolkit mod scaffolding (M0)** — need to install Java/Gradle on glados first. Empty Gradle project that builds + loads cleanly. Probably 4-6 hours of focused work.
|
||||
2. **TierUpgrade GUI** (M1) — first screen in the mod, replaces the `/bns tier upgrade` text flow with a visual ladder + Upgrade button.
|
||||
3. **BountyBoard GUI** (M3, after M0 is done) — replaces the chat-based `/bns bounty list/accept` with a poster-board screen.
|
||||
|
||||
Or — if you'd rather playtest the command-driven version first and only build the UI once the mechanics feel right — that's a perfectly defensible choice. The CLI version is fully functional.
|
||||
@@ -0,0 +1,517 @@
|
||||
# Brass and Sigil — Economy & Quest System Design
|
||||
|
||||
> Status: 13-tier ladder locked (v0.4). Architecture + UI confirmed in v0.3.
|
||||
> Owner: matt
|
||||
> Last updated: 2026-06-06
|
||||
|
||||
---
|
||||
|
||||
## 1. Goals
|
||||
|
||||
- **One unified currency** — spurs (Numismatics).
|
||||
- **Multiple ways to earn, each one bounded.** No infinite money printer.
|
||||
- **Multiple sinks that scale with progression.** Money disappears as you advance, so wealth never dwarfs the cost of new ambitions.
|
||||
- **Spatial design that gives spawn personality.** Themed guild buildings, each with a clear purpose, each tied to the same wallet.
|
||||
- **Forward-compat with a proper quest line** via FTB Quests, while supporting daily/repeatable activities at spawn.
|
||||
- **Anti-automation by design**, not by patches.
|
||||
|
||||
---
|
||||
|
||||
## 2. Core architecture
|
||||
|
||||
```
|
||||
┌────── MONEY IN (bounded) ──────┐
|
||||
│ │
|
||||
Adventurer's Guild — Daily bounties (cooldowns, scaling)
|
||||
Merchant's Bazaar — Sell shop (fixed + DR), dynamic exchange
|
||||
Mint — Capped emerald → spur conversion *
|
||||
First-time milestones — One-shot per player
|
||||
FTB Quests — Progression milestone payouts
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ Player wallet │ ← Numismatics
|
||||
│ (spurs) │
|
||||
└───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────── MONEY OUT (scaling) ──────┐
|
||||
│ │
|
||||
Civic Office — Plot claim costs (FTB Chunks override)
|
||||
Civic Office — Tier upgrades (FTB Ranks integration)
|
||||
Merchant's Bazaar — Player-to-player job board (no minting)
|
||||
Villagers — Re-priced trades, net sink
|
||||
Custom services — Warps, sethomes, premium cosmetics
|
||||
```
|
||||
|
||||
\* Only if we keep emeralds; see §5 — we're moving villagers to spurs, which probably retires the Mint as a currency-conversion station and repurposes it as a wallet/info hub.
|
||||
|
||||
---
|
||||
|
||||
## 3. Spawn — themed guild buildings
|
||||
|
||||
Each guild is a destination. The guild's NPC is both the **entry point** (accept work) and **exit point** (return to claim reward). This pattern applies universally — see §4.
|
||||
|
||||
| Building | Purpose | Primary mechanics inside |
|
||||
|---|---|---|
|
||||
| **Adventurer's Guild** | Combat & exploration | Bounty board (daily mob hunts), boss-kill quests, dungeon-delve quests (future) |
|
||||
| **Merchant's Bazaar** | Trade & raw-mat economy | Numismatics sell shop, dynamic exchange counter, player-driven job board |
|
||||
| **Civic Office** | Land & status | Tier upgrade screen, spawn plot registry (purchase / view your plots), sethome/warp services. NOTE: regular wilderness chunk claiming uses the FTB Chunks map and is tier-allocated, not paid per chunk — see §4. |
|
||||
| **Library / Academy** | Knowledge & magic | FTB Quests anchor NPC, Ars Nouveau themed quests, lore terminals |
|
||||
| **Tavern** | Social & onboarding | First-time newbie quests, casual NPCs, food/buff vendor |
|
||||
| **Foundry / Workshop** | Crafting & commissions | Create-themed contracts, crafting orders, raw-material market |
|
||||
| **Mint** | Currency information | Wallet info, currency tutorial, possibly emerald exchange |
|
||||
|
||||
### Forward-compat with a full quest line
|
||||
|
||||
The architecture is naturally dual-track:
|
||||
|
||||
- **FTB Quests** = the *progression* backbone — chapter-by-chapter pack content, one-time rewards, story arcs. The campaign.
|
||||
- **Spawn-building NPCs** (Easy NPC + KubeJS) = the *daily/repeatable* layer — bounties, sell shops, services. The live activities.
|
||||
|
||||
Both pay into the same wallet. Players run the campaign at their pace and the daily content for income/replay value. They reinforce each other rather than compete.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tier system & spawn plots
|
||||
|
||||
Two distinct land/status systems that often get conflated. Keeping them clean:
|
||||
|
||||
### 4a. Civic tier ladder
|
||||
|
||||
Every player has a Civic rank. Higher rank unlocks perks. Players spend spurs at the Civic Office NPC to upgrade. Powered by FTB Ranks under the hood (permission nodes drive what each rank unlocks).
|
||||
|
||||
The ladder is intentionally long (13 tiers) so endgame ranks feel rare and aspirational. Top tiers represent months of dedicated play; Sovereign is "you've finished the game."
|
||||
|
||||
| # | Title | Cost to reach | Wilderness chunks | Plot slots | Daily bounties | Shop fee | Chat colour | Join broadcast |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | **Peasant** | free (starter) | 9 | 0 | 3 | 0% | grey | — |
|
||||
| 2 | **Farmer** | 200 | 18 | 0 | 3 | 0% | grey | — |
|
||||
| 3 | **Citizen** | 750 | 35 | 1 | 3 | 0% | white | — |
|
||||
| 4 | **Merchant** | 2,500 | 60 | 1 | 4 | -2% | yellow | — |
|
||||
| 5 | **Knight** | 6,500 | 100 | 2 | 4 | -5% | green | simple |
|
||||
| 6 | **Baron** | 15,000 | 150 | 2 | 5 | -7% | cyan | simple |
|
||||
| 7 | **Viscount** | 35,000 | 220 | 3 | 5 | -9% | blue | fancy |
|
||||
| 8 | **Earl** | 75,000 | 300 | 3 | 6 | -11% | purple | fancy |
|
||||
| 9 | **Marquess** | 150,000 | 400 | 4 | 6 | -13% | gold | fancy |
|
||||
| 10 | **Duke** | 300,000 | 525 | 5 | 7 | -15% | orange | epic |
|
||||
| 11 | **Archduke** | 700,000 | 700 | 6 | 7 | -17% | red | epic |
|
||||
| 12 | **Grand Duke** | 1,500,000 | 900 | 7 | 8 | -19% | bright red | epic + particles |
|
||||
| 13 | **Sovereign** | 10,000,000 | 1,500 | 10 | 10 | -25% | rainbow | epic + spectacular |
|
||||
|
||||
**Travel philosophy:** this is a Create Aeronautics pack — the journey is the point. We deliberately do **not** give players sethomes, `/back`, or other instant-teleport conveniences. The intended travel system is **Waystones** (mod already in pack):
|
||||
|
||||
- Players get **one waystone** at the start of the game. It anchors their base.
|
||||
- They can teleport between spawn ↔ their own waystone(s).
|
||||
- Beyond that, real travel means building airships, trains, vehicles — the actual gameplay.
|
||||
|
||||
This is restrictive by design. We can layer **waystone-related perks** into the tier ladder in a later revision (e.g. "tier N unlocks 2nd waystone slot", or "tier M reduces waystone XP cost"). For V1 the ladder stays as above and travel pressure remains intentionally high.
|
||||
|
||||
**Notable shape choices:**
|
||||
|
||||
- **Knight (tier 5)** is the gentry threshold — first colored chat (green), first join broadcast, meaningful jump in plot slots
|
||||
- **Duke → Archduke jump** is the late-game wall (300k → 700k, ~2.3×) — Archduke is the start of "I've really committed to this server" territory
|
||||
- **Sovereign is dramatic** — 6.7× the Grand Duke cost. Real "finished the game" status. -25% shop fee is a step-change leap; spectacular join effects.
|
||||
|
||||
Numbers are first-pass calibration. The full perk table lives in a single JSON (`config/bnstoolkit/tiers.json`) that's hot-reloadable via `/bnstoolkit reload`. Easy to tune any column without code changes.
|
||||
|
||||
### 4a.1 Founder flag (admin cosmetic)
|
||||
|
||||
The server owner / admin has a permanent **`[Founder]`** cosmetic prefix in chat that sits alongside their normal rank. It carries no gameplay perks — it's just visible identification so friends know who runs the server. The Founder still grinds the regular tier ladder like everyone else (good for playtesting balance + sharing the experience). Op commands are the actual admin tool; the Founder tag is purely visual.
|
||||
|
||||
**Wilderness claims:** at every tier above Peasant, you get a flat allowance of N chunks you can claim anywhere outside spawn via the FTB Chunks map. Claiming is **free** within your allowance, refused above. No per-chunk pricing. Driven by FTB Ranks → FTB Chunks permission nodes (`ftbchunks.max_claimed_chunks`).
|
||||
|
||||
### 4b. Spawn plots
|
||||
|
||||
Spawn plots are a **separate system** from wilderness claims.
|
||||
|
||||
- **Pre-defined regions** at spawn — single chunks or small fixed clusters — designated by admins as "plots." Visible as fenced/bordered areas on the spawn map.
|
||||
- **Purchased with spurs** at additional cost per plot (separate from tier upgrade cost). Indicative pricing: standard plot ~2,000 spurs, premium (corner / prime location) ~5,000 spurs.
|
||||
- **Primary use:** shops. Players set up Numismatics shop blocks on their plots and sell to other players. Residence is allowed but secondary.
|
||||
- **Slot-limited by tier:** your civic rank determines how many spawn plots you can own simultaneously (column above). Hit the limit, you can't buy another until you upgrade or release a plot.
|
||||
- **Purchase happens inside the FTB Chunks map UI** — clicking a plot region in the map opens a confirmation screen (provided by our custom mod's mixin) showing cost + your remaining slots. Same UI you use for wilderness claims, integrated.
|
||||
- **Plot data lives in the custom mod** (`bnstoolkit:plots`), not in FTB Chunks. Plots are a separate ownership system that uses the FTB Chunks map as its visual surface.
|
||||
|
||||
---
|
||||
|
||||
## 5. Quest & bounty flow (universal pattern)
|
||||
|
||||
**Core principle:** for any guild, the player **accepts** work from an NPC, **completes** it in the world, then **returns** to that NPC to **turn it in** and claim the reward. No instant payouts. This:
|
||||
|
||||
- Anchors guilds as destinations
|
||||
- Creates risk/reward tension (die on the return, lose the reward)
|
||||
- Encourages waypoint use and travel
|
||||
- Lets the server centrally control payouts and detect cheating
|
||||
|
||||
### State machine
|
||||
|
||||
Each quest, per player, lives in one of these states:
|
||||
|
||||
```
|
||||
AVAILABLE ─[player accepts]─► ACTIVE ─[objective met]─► READY ─[turned in]─► COMPLETED
|
||||
▲ │
|
||||
└──────────────────────[cooldown elapses]───────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **AVAILABLE** — NPC offers the quest in dialog. Player has no current copy of it.
|
||||
- **ACTIVE** — Player accepted. KubeJS tracks objective progress via relevant events (`EntityEvents.death` for kills, `ItemEvents.firstRightClicked` for explorations, etc.).
|
||||
- **READY** — Objective met. Reward is *promised but not yet paid.* NPC dialog now offers a turn-in option.
|
||||
- **COMPLETED** — Reward paid out. Quest goes on cooldown for this player.
|
||||
- **AVAILABLE** (again) — after the cooldown, the NPC will offer it again.
|
||||
|
||||
### Worked example — daily bounty
|
||||
|
||||
1. **Adventurer's Guild NPC** has 5 randomized daily quests (e.g. "Slay 10 Plunderers — 50 spurs").
|
||||
2. Player right-clicks NPC → Easy NPC dialog → "Accept: Slay 10 Plunderers."
|
||||
3. KubeJS writes `quest:plunderer_10` to the player's quest state with `count: 0`.
|
||||
4. Player kills Plunderers in the world. `EntityEvents.death` for `supplementaries:plunderer` increments the count.
|
||||
5. At count=10, KubeJS flips state to **READY** and notifies the player ("Return to the Adventurer's Guild to claim your reward.").
|
||||
6. Player returns. Right-click NPC. Easy NPC dialog detects READY state and offers "Turn in: Slay 10 Plunderers."
|
||||
7. KubeJS pays 50 spurs into the player's Numismatics wallet, flips state to **COMPLETED**, sets cooldown to 24h.
|
||||
|
||||
### Universal applicability
|
||||
|
||||
The same accept → complete → return pattern works for every guild:
|
||||
|
||||
| Guild | Accept | Complete | Return |
|
||||
|---|---|---|---|
|
||||
| **Adventurer's** | "Hunt 10 plunderers" | kill them | NPC pays bounty |
|
||||
| **Merchant's** | "Procure 32 diamonds for our buyer" | gather them | NPC pays inflated price + completes |
|
||||
| **Civic** | "Survey 5 unclaimed chunks" | walk through them | NPC pays survey fee |
|
||||
| **Library** | "Recover this lost tome" | find the book in a structure | NPC pays + grants reputation |
|
||||
| **Foundry** | "Craft 10 Andesite Casings" | craft them | NPC pays commission |
|
||||
|
||||
### Player-facing UI (locked)
|
||||
|
||||
Polished, integrated, no chat commands to memorise. All player interaction happens through GUIs and NPC dialogue. Custom companion mod (working name `bnstoolkit`) provides the screens; FTB Library's UI framework is the visual basis so our screens match FTB Quests, FTB Chunks, and FTB Teams natively.
|
||||
|
||||
| Surface | Implementation |
|
||||
|---|---|
|
||||
| **Quest Log screen** | Custom mod screen. Tabs: Active / Available / Completed. Click a quest to inspect, cancel from the detail panel. Opened via keybind or by clicking a Quest Log block at any guild. |
|
||||
| **Bounty Board screen** | Custom mod screen, opened by right-clicking the Adventurer's Guild Quartermaster NPC OR clicking the in-world Bounty Board block. Lists today's available daily bounties with descriptions, rewards, and "Accept" buttons. |
|
||||
| **Tier upgrade screen** | Custom mod screen, opened at the Civic Office NPC. Visual ladder, your current tier highlighted, next-tier cost, perks unlocked at each rank, big Upgrade button. |
|
||||
| **Plot purchase popup** | Custom mod mixin into the FTB Chunks map UI. Hover a plot region → cost + slots tooltip. Click → confirmation popup. Owned plots also clickable to see plot info. |
|
||||
| **Shop browser** | Numismatics shop blocks render their existing inventory UI by default; optional richer browser screen for the central Merchant's Bazaar inventory. |
|
||||
| **Quest progress HUD** | Custom mod render layer — styled HUD element showing active quest names + progress bars. NOT stacked vanilla bossbars. |
|
||||
| **NPC dialogue + buttons** | Easy NPC handles all NPC interactions: accept/turn-in, info, tutorials, "click for details" buttons. Players learn the system through NPC dialogue, not by reading docs. |
|
||||
|
||||
### Why custom mod, on FTB Library's UI framework
|
||||
|
||||
- **Visual consistency:** FTB Quests, FTB Chunks, FTB Teams all use FTB Library's widget framework. Our screens built on the same framework will look and feel like part of the same product.
|
||||
- **Already a pack dependency:** no new mod added just for the UI layer.
|
||||
- **Mixin compatibility:** our spawn plot mixin into the FTB Chunks map operates on FTB Library widgets natively — no translation layer between vanilla MC widgets and FTB Library widgets.
|
||||
- **Less boilerplate than vanilla MC Screen API:** Panel, BlockButton, IconButton, Tooltip, TextField widgets already exist and behave consistently with how players already know FTB UIs to work.
|
||||
- **Forking FTB is off the table:** FTB mods are "All Rights Reserved" — depending on FTB Library as a library is fine (that's what libraries are for), forking FTB Quests or FTB Chunks for redistribution is not legally clean. Custom mod alongside the FTB stack is the correct architecture.
|
||||
|
||||
### Why NOT FTB Quests for daily bounties
|
||||
|
||||
FTB Quests stays for **long-form progression** (Phase 5+), not for daily bounties. Source review confirms:
|
||||
|
||||
- No abandon/cancel event (Ctrl+Shift+R reset fires no event we can hook)
|
||||
- Cooldown is per-team, not per-player
|
||||
- Visibility is per-team, not per-player — rotating "5 of 20 daily" doesn't fit cleanly
|
||||
|
||||
For long-form progression where one-shot rewards + team scope are fine, FTB Quests is great. Our custom bounty engine handles the daily/repeatable pattern with its own UI.
|
||||
|
||||
### Quest slot limit
|
||||
|
||||
Players have a **maximum of 3 active quests at a time.** This is the friction that makes cancel meaningful:
|
||||
|
||||
- Forces players to pick which quests they actually want — no "accept all 5, see what's easiest, cancel the rest" RNG-shopping.
|
||||
- Keeps the state files small.
|
||||
- Makes ready-to-turn-in queues meaningful (you can hold 3 unrewarded quests in your back pocket).
|
||||
|
||||
A quest moving to READY frees the slot for accepting a new one even before turn-in.
|
||||
|
||||
### KubeJS implementation — locked specifics
|
||||
|
||||
- **State storage:** per-player JSON in `world/data/bns_quests/<uuid>.json`. Schema:
|
||||
```json
|
||||
{
|
||||
"active": [
|
||||
{
|
||||
"id": "advg_plunderer_10",
|
||||
"acceptedAt": 1717684800,
|
||||
"progress": 7,
|
||||
"target": 10,
|
||||
"rewardSpurs": 50,
|
||||
"source": "adventurers_guild",
|
||||
"bossbarId": "bns:quest_advg_plunderer_10_<uuid>"
|
||||
}
|
||||
],
|
||||
"completed": {
|
||||
"advg_plunderer_10": { "cooldownUntil": 1717771200, "completions": 3 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **Event tracking:** KubeJS event handlers (`EntityEvents.death`, `ItemEvents.pickedUp`, `BlockEvents.broken`) read the player's active quests and increment progress + update bossbar value.
|
||||
|
||||
- **Bossbar lifecycle:** on quest accept, KubeJS creates a unique per-player bossbar via `server.runCommandSilent('bossbar add <id> <name>')`, assigns it to that player only, updates `value` and `max` on progress. On turn-in or cancel: `bossbar remove`.
|
||||
|
||||
- **Numismatics integration (direct Java access — no events shipped, no KubeJS bindings):**
|
||||
```js
|
||||
const Numismatics = Java.loadClass('dev.ithundxr.createnumismatics.Numismatics');
|
||||
const ReasonHolder = Java.loadClass('dev.ithundxr.createnumismatics.content.backend.ReasonHolder');
|
||||
|
||||
const acct = Numismatics.BANK.getAccount(player); // ServerPlayer
|
||||
const balance = acct.getBalance(); // int (spurs)
|
||||
acct.deposit(50); // pay quest reward
|
||||
const ok = acct.deduct(100, ReasonHolder.empty()); // returns false if broke
|
||||
Numismatics.BANK.markBankDirty(); // force persistence
|
||||
```
|
||||
|
||||
- **Dialog branching:** Easy NPC dialog branches on scoreboard objective `bns.quest.<npc_tag>` that KubeJS mirrors quest state into. Dialog `Command` action triggers `/bns_quest accept <id> <npc_tag>` which routes to KubeJS.
|
||||
|
||||
- **Commands:** registered via `ServerEvents.commandRegistry` in KubeJS. Scoped to the player who runs them. The custom `/bns_quest` namespace handles NPC-triggered actions; `/quests` is the player-facing tracker.
|
||||
|
||||
- **FTB Chunks claim cost (Phase 2):** requires `FTB XMod Compat` mod. Then:
|
||||
```js
|
||||
FTBChunksEvents.before('claim', event => {
|
||||
const player = event.source.player;
|
||||
const acct = Numismatics.BANK.getAccount(player);
|
||||
const cost = computeClaimCost(player);
|
||||
if (acct.getBalance() < cost) {
|
||||
event.setResult(ClaimResult.YOU_DO_NOT_HAVE_ENOUGH_SPURS); // custom enum value
|
||||
return;
|
||||
}
|
||||
acct.deduct(cost, ReasonHolder.empty());
|
||||
});
|
||||
```
|
||||
**Note:** the `.before` may fire twice (sim + real). Guard with idempotent state, or do balance check in `.before` and deduct in `.after('claim')`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Villager rebalance
|
||||
|
||||
**Decision:** move all villagers to **spurs** (not emeralds). Thematic consistency over preserving vanilla mental model.
|
||||
|
||||
**Decision principles:**
|
||||
|
||||
- Villagers are **net money sinks** — players spend more on villagers than they earn from them.
|
||||
- **Useful trades stay** — enchanted books, tools, food, decorative blocks. Reasonable spur prices.
|
||||
- **Trash trades get deleted** — no one ever wanted to sell rotten flesh or buy paper.
|
||||
- **Sell-to-villager trades exist but are bad** — `64 wheat → 1 spur`, `16 leather → 1 spur`. Farms still possible, but ~10× slower than any other income source.
|
||||
|
||||
### Price guidance (initial calibration — tune via playtesting)
|
||||
|
||||
#### Buy-from-villager (player pays spurs)
|
||||
|
||||
| Item | Tier-1 | Tier-3 | Tier-5 |
|
||||
|---|---|---|---|
|
||||
| Standard enchanted book (random) | 50 spurs | 120 spurs | 250 spurs |
|
||||
| Mending book | — | — | 800 spurs |
|
||||
| Diamond tool (unenchanted) | — | 80 spurs | — |
|
||||
| Diamond tool (enchanted) | — | — | 350 spurs |
|
||||
| Bookshelf | 15 spurs | — | — |
|
||||
|
||||
#### Sell-to-villager (player gets spurs)
|
||||
|
||||
| Item | Spurs per unit |
|
||||
|---|---|
|
||||
| Wheat | 1 per 64 |
|
||||
| Paper | 1 per 32 |
|
||||
| Leather | 1 per 16 |
|
||||
| Iron ingot | 2 per 1 |
|
||||
| Diamond | 8 per 1 *(matches sell-shop fixed price — players use whichever is closer)* |
|
||||
|
||||
### Scope
|
||||
|
||||
~60 trades × 5 profession tiers = ~300 trade entries to redesign. One-time data-design pass, implemented via `ServerEvents.villagerTrades` in KubeJS.
|
||||
|
||||
---
|
||||
|
||||
## 6. Money inflow sources (detailed)
|
||||
|
||||
| Source | Cap / DR mechanism | Rough rate |
|
||||
|---|---|---|
|
||||
| **Daily bounties** | Per-player per-quest cooldown (24h) | ~200-500 spurs/day for an active player |
|
||||
| **Sell shop (fixed price + DR)** | Lifetime sales tracker per player, 10% price drop per 64 sold | ~50-200 spurs/hr while diamond stockpile lasts |
|
||||
| **Dynamic exchange** | Global supply pool — price drops with sales | Self-balancing |
|
||||
| **First-time milestones** | One-shot per player (`hasFlag`) | 5-50 spurs per milestone, ~10 milestones total |
|
||||
| **FTB Quest progression** | One-time per quest | Varies; cluster rewards at chapter completion |
|
||||
| **Villager sell trades** | Inherently slow + bad prices | ~50 spurs/hr from a serious villager farm |
|
||||
|
||||
---
|
||||
|
||||
## 7. Money outflow sinks (detailed)
|
||||
|
||||
| Sink | Cost curve | Why this scales right |
|
||||
|---|---|---|
|
||||
| **Plot claims** | Free up to 9 chunks. 50 spurs/chunk to 25. 200 spurs/chunk beyond. | New players claim free starter areas; ambitious players pay real money |
|
||||
| **Tier upgrades** | Cumulative cost: 500 → 2k → 8k → 30k spurs | Endgame sink, status display |
|
||||
| **Buy-from-villager** | See §5 | Constant outflow as you build up |
|
||||
| **Premium cosmetics** | 100-2k spurs each | Optional sink for cash-flush players |
|
||||
| **Services** | Sethome: 50 spurs, warp: 25 spurs, /back: 10 spurs | Convenience taxes |
|
||||
|
||||
---
|
||||
|
||||
## 8. Mod stack (locked)
|
||||
|
||||
### Already in pack
|
||||
| Need | Mod | Version |
|
||||
|---|---|---|
|
||||
| Currency + wallets + shop blocks | Numismatics | 1.0.20 |
|
||||
| NPC dialog presentation | Easy NPC | 6.16.x (6.17.0 available, minor bump) |
|
||||
| Plot claims | FTB Chunks | 2101.1.14 |
|
||||
| Team management | FTB Teams | 2101.1.9 |
|
||||
| Core library | FTB Library | 2101.1.31 |
|
||||
| Cross-mod compat | Architectury API | 13.0.8 |
|
||||
| Glue, commands, state | KubeJS | 2101.7.2 |
|
||||
|
||||
### Adding in Phase 0a (mod-additions PR — blocks everything else)
|
||||
| Mod | Version | Reason |
|
||||
|---|---|---|
|
||||
| **FTB Quests** | 2101.1.25 | Progression quest UI (Phase 5+) |
|
||||
| **FTB XMod Compat** | latest 1.21.1 NeoForge | KubeJS hooks for FTB Chunks claim events + FTB Quests events |
|
||||
| **FTB Ranks** | 2101.1.3 | Tier system. CurseForge only, no Modrinth listing. |
|
||||
|
||||
### Building our own (custom companion mod)
|
||||
|
||||
**`bnstoolkit`** (working name) — a small companion mod in our own repo. Confirmed locked-in.
|
||||
|
||||
- Provides all custom GUI screens (Quest Log, Bounty Board, Tier upgrade, Shop browser)
|
||||
- Provides the FTB Chunks map mixins (spawn plot cost overlay, hover tooltip, purchase popup)
|
||||
- Provides custom quest-progress HUD render layer
|
||||
- Provides the `bnstoolkit:plots` data storage + ownership permissions
|
||||
- Provides network packets for client↔server state sync
|
||||
- Provides the `/bnstoolkit reload` admin command for hot-reloading tier config
|
||||
- Built on FTB Library UI framework for visual consistency
|
||||
- KubeJS handles all gameplay logic (bounty rotation, state machine, villager trades, Numismatics integration) — the mod is purely the UI + integration layer
|
||||
|
||||
Estimated scope: ~2-3k LOC, single-developer week of focused work. Bounded.
|
||||
|
||||
### Explicitly NOT adding
|
||||
- CustomNPCs (Easy NPC covers it)
|
||||
- Citizens (Bukkit-only)
|
||||
- PMMO / skill mods (out of scope)
|
||||
- Generic "quest giver" mods (FTB Quests + our KubeJS bounty engine cover it)
|
||||
|
||||
---
|
||||
|
||||
## 9. Phasing
|
||||
|
||||
Each phase is independently shippable. Earlier phases unlock value without depending on later ones.
|
||||
|
||||
| Phase | Deliverable | Spawn build required? |
|
||||
|---|---|---|
|
||||
| **0a** | **Mod additions PR:** FTB Quests + FTB XMod Compat + FTB Ranks (+ minor Easy NPC bump). New pack version. | No |
|
||||
| **0b** | Spawn skeleton — 7 guild building blockouts (rough). **User does in-game when time permits.** Non-blocking. | Yes (user) |
|
||||
| **0c** | **`bnstoolkit` companion mod — scaffolding pass.** Mod project set up, FTB Library UI dependency wired, network packet plumbing, build/release scripts. No screens yet. | No |
|
||||
| **1** | **Sell shop:** Numismatics shop blocks for diamonds (10 spurs) + netherite ingots (100 spurs). Lifetime-sale tracker for diminishing returns. KubeJS only, no mod work needed. | No |
|
||||
| **2** | **Tier upgrade flow:** FTB Ranks integrated + KubeJS `/tier upgrade` command. **`bnstoolkit` Tier upgrade screen** wired to Civic Office NPC. Tier perks driven by `tiers.json`. | No |
|
||||
| **3** | **Spawn plot system:** plot region registry in `bnstoolkit`, ownership tracking, FTB Chunks map mixin (cost overlay + purchase popup). | No |
|
||||
| **4** | **Bounty engine:** state machine, KubeJS-rotated dailies, `bnstoolkit` Bounty Board screen + Quest Log screen + Quest progress HUD. NPC dialog hooks via Easy NPC. 3-slot cap. | No (NPC layer activates once spawn is built) |
|
||||
| **5** | **Villager rebalance:** strip vanilla, ship spur-priced trade table via `ServerEvents.villagerTrades`. | No |
|
||||
| **6** | **Progression quest line:** FTB Quests anchor + first chapter. KubeJS rewards dispense spurs via Numismatics API. | No (anchor NPC at Library once spawn exists) |
|
||||
| **7** | **Dynamic exchange rates:** global supply-pool pricing for raw mats. | No |
|
||||
| **8+** | Lootr dungeons, Foundry crafting commissions, deeper FTB Quests chapters, player-vs-player bounties (if wanted). | TBD |
|
||||
|
||||
---
|
||||
|
||||
## 10. Risk register
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|---|---|---|
|
||||
| Bounty engine bugs out, players lose progress | Medium | Atomic state writes; logs of every state transition |
|
||||
| Villager farms still profitable after rebalance | Medium | Track player sell volumes; tune prices in patches |
|
||||
| KubeJS-EasyNPC dialog branching has limits | Low-Med | Verify hooks in a test world before Phase 3; fall back to command-driven NPCs if needed |
|
||||
| FTB Chunks `/claim` command override gotchas | Low | Test in dev world; FTB Chunks API is stable |
|
||||
| Plot cost feels punishing | Med | First 9 chunks free is the safety valve; tune from playtest |
|
||||
| Players bypass the system via creative / `/give` | Op-only | Document op responsibility; consider audit logs for op spur grants |
|
||||
|
||||
---
|
||||
|
||||
## 11. Decisions made
|
||||
|
||||
Resolved before implementation:
|
||||
|
||||
| Question | Decision |
|
||||
|---|---|
|
||||
| Quest UI for bounties | **Custom mod with FTB Library UI widgets** — proper GUI screens, no chat commands |
|
||||
| Quest UI for progression | FTB Quests |
|
||||
| Plot purchase UI | Custom mod mixin into FTB Chunks map — hover preview, click-to-buy popup |
|
||||
| Tier upgrade UI | Custom mod screen at Civic Office NPC |
|
||||
| State storage | KubeJS-managed per-player JSON for quest state; mod-managed for plot ownership |
|
||||
| Numismatics access | Direct Java via `Java.loadClass` from KubeJS; via API directly from the mod |
|
||||
| Wilderness chunk allowance | Tier-driven via FTB Ranks permission nodes — no per-chunk pricing |
|
||||
| Spawn plots | Predefined regions, slot-limited by tier, individually purchased with spurs |
|
||||
| Tier ladder | Civic (13 tiers): Peasant → Farmer → Citizen → Merchant → Knight → Baron → Viscount → Earl → Marquess → Duke → Archduke → Grand Duke → Sovereign |
|
||||
| Founder flag | Admin/owner has `[Founder]` cosmetic prefix in chat, grinds the regular ladder otherwise |
|
||||
| Tier perks | JSON-configured (`config/bnstoolkit/tiers.json`), hot-reloadable |
|
||||
| Custom mod | Confirmed. Built on FTB Library UI framework. Working name `bnstoolkit`. |
|
||||
| Sethomes / warps | Tier-allocated count (not paid per use) |
|
||||
| Daily quest count | Tier-driven (3 at Peasant, up to 6 at Sovereign), refresh per-player 24h after last accept |
|
||||
| Emeralds | Retired as currency, retain as a trade material for villager-redirect trades |
|
||||
| Fork FTB | Not needed — depend on FTB Library as a UI lib, FTB Chunks/Quests stay unmodified, integration via mixins + KubeJS hooks |
|
||||
|
||||
### Still to decide (non-blocking — pick during implementation)
|
||||
|
||||
- **PvP bounties** — out of scope for V1, revisit after V1 ships
|
||||
- **Lootr** — add only when structured dungeons exist (Phase 8+)
|
||||
- **Default-rank starting permissions** — finalised when FTB Ranks config is written
|
||||
- **Spawn plot pricing** — first-pass numbers (2,000 standard / 5,000 premium), tune in playtest
|
||||
|
||||
---
|
||||
|
||||
## 12. Implementation notes
|
||||
|
||||
### KubeJS scripts (gameplay logic)
|
||||
Organised under `pack/overrides/kubejs/server_scripts/economy/`:
|
||||
|
||||
- `01_numismatics_api.js` — Java.loadClass wrapper helpers (balance / deposit / deduct)
|
||||
- `02_quest_state.js` — load/save `world/data/bns_economy/quests/<uuid>.json`
|
||||
- `03_commands.js` — admin commands (`/tier upgrade`, `/bnstoolkit reload`, debug)
|
||||
- `04_bounty_engine.js` — daily rotation, state machine, KubeJS↔mod packet handlers
|
||||
- `05_sell_shop.js` — Numismatics shop tracking + diminishing returns
|
||||
- `06_villager_trades.js` — Phase 5 `ServerEvents.villagerTrades` rebalance
|
||||
- `07_tier_perks.js` — FTB Ranks promote/demote, perk application
|
||||
- `99_dynamic_exchange.js` — Phase 7 supply-priced market
|
||||
|
||||
### Custom mod codebase
|
||||
Repo: separate (or sub-tree of main repo, TBD).
|
||||
Modid: `bnstoolkit` (working name).
|
||||
Package layout:
|
||||
|
||||
```
|
||||
bnstoolkit/
|
||||
├── client/
|
||||
│ ├── screen/ QuestLog, BountyBoard, Shop, Tier, PlotConfirm (FTB Library subclasses)
|
||||
│ ├── mixin/ ChunkScreenPanel (cost overlay + plot button), ChunkButton (hover tooltip)
|
||||
│ ├── hud/ QuestProgressHud (custom render)
|
||||
│ └── ClientEvents keybind reg, screen reg, mixin loader
|
||||
├── server/
|
||||
│ ├── packet/ OpenScreen, UpgradeRank, BuyPlot, ClaimBounty, etc.
|
||||
│ ├── command/ /bnstoolkit reload (config refresh)
|
||||
│ └── data/ PlotRegistry, PlotOwnership (saved with world data)
|
||||
├── common/
|
||||
│ ├── data/ TierConfig (JSON-loaded), BountyDefinition, QuestState mirror
|
||||
│ └── BnsToolkit.java mod entry
|
||||
└── config-defaults/
|
||||
├── tiers.json copied to config/bnstoolkit/tiers.json on first run
|
||||
└── plots.json spawn plot region definitions
|
||||
```
|
||||
|
||||
Architecture doc (`docs/bnstoolkit-architecture.md`) covers package layout, screen UX sketches, network packet contract, mixin targets, KubeJS↔mod integration surface — to be written before mod work begins.
|
||||
|
||||
### State data
|
||||
- Quest state: `world/data/bns_economy/quests/<uuid>.json`. Readable JSON, easy to debug.
|
||||
- Plot ownership: persisted with world via the mod's `SavedData` (vanilla NeoForge persistence). Authoritative source of truth.
|
||||
- Tier config: `config/bnstoolkit/tiers.json` shipped from `pack/overrides/defaultconfigs/bnstoolkit/tiers.json`. Hot-reloadable.
|
||||
- Plot definitions: `config/bnstoolkit/plots.json` shipped similarly. Defines plot region bounds, type (standard/premium), prices.
|
||||
|
||||
### NPC dialog
|
||||
Authored in Easy NPC's in-game editor, exported as preset JSONs into `pack/overrides/world/datapacks/bns-fuel/data/easy_npc/`. Dialog `Command` actions call `/bnstoolkit <verb> ...` which routes to either the mod (for screen-opening) or KubeJS (for state changes).
|
||||
|
||||
### Villager rebalance
|
||||
Single source file `06_villager_trades.js`. Each profession × tier has its own block. One file = one diff per balance pass.
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- `MEMORY.md` entries:
|
||||
- `project-mc-player-economy.md` — earlier higher-level notes on Numismatics + plot system
|
||||
- `feedback-kubejs-rhino-gotchas.md` — KubeJS scripting constraints
|
||||
- `feedback-bns-pack-version-sync.md` — pack version + sync workflow
|
||||
- Existing tasks #20-#26 — to be reshuffled to mirror the phase plan above
|
||||
@@ -4,7 +4,7 @@
|
||||
#.
|
||||
#.
|
||||
#If true, cannons cannot fail whatsoever. Equivalent to setting all the chances below to zero.
|
||||
disableAllCannonFailure = true
|
||||
disableAllCannonFailure = false
|
||||
#.
|
||||
#Chance that a fired projectile will get stuck in a barrel after exceeding the squib ratio. 0 is 0%, 1 is 100%.
|
||||
# Default: 0.25
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
// -----------------------------------------------------------
|
||||
// Item Obliterator -- Brass and Sigil config
|
||||
// -----------------------------------------------------------
|
||||
// Hides + disables specific Alex's Mobs items that we don't
|
||||
// want in this modpack:
|
||||
//
|
||||
// alexsmobs:transmutation_table -- competes with Ars
|
||||
// and undermines the
|
||||
// Numismatics economy
|
||||
//
|
||||
// alexsmobs:shattered_dimensional_carver -- teleports the
|
||||
// player 1,000,000
|
||||
// blocks in a random
|
||||
// direction, breaks
|
||||
// world-bounds
|
||||
//
|
||||
// Both are removed from inventories, creative tabs, recipes
|
||||
// (including the Capsid transformation), trades, and JEI.
|
||||
// -----------------------------------------------------------
|
||||
"configVersion": 2,
|
||||
"blacklisted_items": [
|
||||
"alexsmobs:transmutation_table",
|
||||
"alexsmobs:shattered_dimensional_carver"
|
||||
],
|
||||
"blacklisted_nbt": [],
|
||||
"only_disable_interactions": [],
|
||||
"only_disable_attacks": [],
|
||||
"only_disable_recipes": [],
|
||||
"use_hashmap_optimizations": false
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#.
|
||||
#Miscellaneous settings
|
||||
[misc]
|
||||
#.
|
||||
#Coupler will require points to be on the same or adjacent track edge, this will prevent the coupler from working if there is any form of junction in between the two points.
|
||||
strictCoupler = false
|
||||
#.
|
||||
#Allow controlling Brass Switches remotely when approaching them on a train
|
||||
flipDistantSwitches = true
|
||||
#.
|
||||
#Max distance between targeted track and placed switch stand
|
||||
# Default: 64
|
||||
# Range: 16 ~ 128
|
||||
switchPlacementRange = 64
|
||||
#.
|
||||
#Allow creepers and ghast fireballs to damage tracks
|
||||
creeperTrackDamage = false
|
||||
#.
|
||||
#Multiplier used for calculating exhaustion from speed when a handcar is used.
|
||||
# Default: 0.009999999776482582
|
||||
# Range: 0.0 ~ 1.0
|
||||
handcarHungerMultiplier = 0.009999999776482582
|
||||
|
||||
#.
|
||||
#Semaphore settings
|
||||
[semaphores]
|
||||
#.
|
||||
#.
|
||||
#Simplified semaphore placement (no upside-down placement)
|
||||
simplifiedPlacement = true
|
||||
#.
|
||||
#Whether semaphore color order is reversed when the semaphores are oriented upside-down
|
||||
flipYellowOrder = false
|
||||
|
||||
#.
|
||||
#Conductor settings
|
||||
[conductors]
|
||||
#.
|
||||
#.
|
||||
#Conductor whistle is limited to the owner of a train
|
||||
mustOwnBoundTrain = false
|
||||
#.
|
||||
#Maximum length of conductor vents
|
||||
# Default: 64
|
||||
# Range: > 1
|
||||
maxConductorVentLength = 64
|
||||
#.
|
||||
#How often a conductor whistle updates the train of the bound conductor
|
||||
# Default: 10
|
||||
# Range: 1 ~ 600
|
||||
whistleRebindRate = 10
|
||||
#.
|
||||
#Maximum distance (in blocks) at which a conductor can be activated by looking at them
|
||||
# Default: 16
|
||||
# Range: 1 ~ 64
|
||||
activationDistance = 16
|
||||
|
||||
#.
|
||||
#Realism Settings
|
||||
[realism]
|
||||
#.
|
||||
#.
|
||||
#Make trains require fuel to run (either from fuel tanks or solid fuels in chests/barrels)
|
||||
realisticTrains = true
|
||||
#.
|
||||
#Make fuel tanks only accept proper liquid fuels (so water etc can't go into them)
|
||||
realisticFuelTanks = true
|
||||
@@ -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.
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"ingredients": [
|
||||
{
|
||||
"item": "minecraft:barrier"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"item": "alexsmobs:shattered_dimensional_carver",
|
||||
"count": 1
|
||||
},
|
||||
"time": 200
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["minecraft:lava"],
|
||||
"fuel_ticks": 8000
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["tfmg:crude_oil"],
|
||||
"fuel_ticks": 8000
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["tfmg:diesel"],
|
||||
"fuel_ticks": 48000
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["tfmg:furnace_gas"],
|
||||
"fuel_ticks": 12000
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["tfmg:gasoline"],
|
||||
"fuel_ticks": 36000
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["tfmg:kerosene"],
|
||||
"fuel_ticks": 24000
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fluids": ["tfmg:naphtha"],
|
||||
"fuel_ticks": 24000
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"alexsmobs:shattered_dimensional_carver",
|
||||
"alexsmobs:transmutation_table"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": 48,
|
||||
"description": "Brass and Sigil — Steam'n'Rails liquid fuel definitions"
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
}
|
||||
}
|
||||
+135
-3
@@ -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.28.0",
|
||||
"version": "0.35.0",
|
||||
"minecraft": "1.21.1",
|
||||
"loader": {
|
||||
"type": "neoforge",
|
||||
"version": "21.1.228"
|
||||
},
|
||||
"lockedAt": "2026-05-28T19:27:42Z",
|
||||
"lockedAt": "2026-06-07T02:33:07Z",
|
||||
"mods": [
|
||||
{
|
||||
"source": "modrinth",
|
||||
@@ -557,7 +557,7 @@
|
||||
"url": "https://cdn.modrinth.com/data/XaDC71GB/versions/IONexlgI/lithostitched-1.7.2-neoforge-21.1.jar",
|
||||
"sha1": "ce35206214647131ebdf14212d1986349aeba79a",
|
||||
"size": 810015,
|
||||
"side": "server"
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
@@ -998,6 +998,138 @@
|
||||
"sha1": "3a93862421d0a8cdae1e3a3d7f0d4bd01e049007",
|
||||
"size": 14702,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "improved-village-placement",
|
||||
"versionId": "A9qu7ToP",
|
||||
"version": "1.1.1",
|
||||
"path": "mods/improved-village-placement-1.1.1.jar",
|
||||
"url": "https://cdn.modrinth.com/data/KA0MllEw/versions/A9qu7ToP/improved-village-placement-1.1.1.jar",
|
||||
"sha1": "15740b951061eea637a36a967d64085f9375a932",
|
||||
"size": 34172,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "cloth-config",
|
||||
"versionId": "izKINKFg",
|
||||
"version": "15.0.140+neoforge",
|
||||
"path": "mods/cloth-config-15.0.140-neoforge.jar",
|
||||
"url": "https://cdn.modrinth.com/data/9s6osm5g/versions/izKINKFg/cloth-config-15.0.140-neoforge.jar",
|
||||
"sha1": "c3e5733ba4503b102589a026000fd5ce0212f6f2",
|
||||
"size": 1163890,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "playeranimator",
|
||||
"versionId": "HJZB6bmA",
|
||||
"version": "2.0.4+1.21.1-forge",
|
||||
"path": "mods/player-animation-lib-forge-2.0.4+1.21.1.jar",
|
||||
"url": "https://cdn.modrinth.com/data/gedNE4y2/versions/HJZB6bmA/player-animation-lib-forge-2.0.4%2B1.21.1.jar",
|
||||
"sha1": "05be8ddcc78532b9a5520c718edc6c971cd0057c",
|
||||
"size": 203149,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "citadel-(1.21.1-port)",
|
||||
"versionId": "mIylVpkN",
|
||||
"version": "2.7.6",
|
||||
"path": "mods/citadel-1.21.1-2.7.6.jar",
|
||||
"url": "https://cdn.modrinth.com/data/XjY0RcQj/versions/mIylVpkN/citadel-1.21.1-2.7.6.jar",
|
||||
"sha1": "e0392370117ec16a721f8d65728944398de77607",
|
||||
"size": 3028943,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "better-combat",
|
||||
"versionId": "2NyTus6V",
|
||||
"version": "2.3.2+1.21.1-neoforge",
|
||||
"path": "mods/bettercombat-neoforge-2.3.2+1.21.1.jar",
|
||||
"url": "https://cdn.modrinth.com/data/5sy6g3kz/versions/2NyTus6V/bettercombat-neoforge-2.3.2%2B1.21.1.jar",
|
||||
"sha1": "f752fbbf81027948ef4ab0fc0066260ccf574179",
|
||||
"size": 1041845,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "combat-roll",
|
||||
"versionId": "FT7t1n1a",
|
||||
"version": "2.0.6+1.21.1-neoforge",
|
||||
"path": "mods/combat_roll-neoforge-2.0.6+1.21.1.jar",
|
||||
"url": "https://cdn.modrinth.com/data/wGKYL7st/versions/FT7t1n1a/combat_roll-neoforge-2.0.6%2B1.21.1.jar",
|
||||
"sha1": "8b9676996e2b49520ac29d92e6068089c5f40baa",
|
||||
"size": 193381,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "alexs-mobs(1.21.1)",
|
||||
"versionId": "KSgki4uc",
|
||||
"version": "1.22.17",
|
||||
"path": "mods/alexsmobs-1.22.17.jar",
|
||||
"url": "https://cdn.modrinth.com/data/EmNhnNnt/versions/KSgki4uc/alexsmobs-1.22.17.jar",
|
||||
"sha1": "5070e8070f60650d773bfd9f1d031496f8f50259",
|
||||
"size": 26620546,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "item-obliterator",
|
||||
"versionId": "jy3ApWAm",
|
||||
"version": "2.3.0",
|
||||
"path": "mods/Item-Obliterator-NeoForge-MC1.21.1-2.3.0.jar",
|
||||
"url": "https://cdn.modrinth.com/data/3ESR84kR/versions/jy3ApWAm/Item-Obliterator-NeoForge-MC1.21.1-2.3.0.jar",
|
||||
"sha1": "0cb46a873376d7ded387e175bf30181fcc079c15",
|
||||
"size": 24037,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "modrinth",
|
||||
"slug": "necronomicon",
|
||||
"versionId": "4iO8Tbfj",
|
||||
"version": "1.6.0+1.21",
|
||||
"path": "mods/Necronomicon-NeoForge-1.6.0+1.21.jar",
|
||||
"url": "https://cdn.modrinth.com/data/P1Kv5EAO/versions/4iO8Tbfj/Necronomicon-NeoForge-1.6.0%2B1.21.jar",
|
||||
"sha1": "4d81055810e07a94b68d16ab20ec298ad0d597a0",
|
||||
"size": 29865,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "curseforge",
|
||||
"slug": "ftb-quests",
|
||||
"fileId": "8195575",
|
||||
"version": "2101.1.25",
|
||||
"path": "mods/ftb-quests-neoforge-2101.1.25.jar",
|
||||
"url": "https://mediafilez.forgecdn.net/files/8195/575/ftb-quests-neoforge-2101.1.25.jar",
|
||||
"sha1": "c51f43029930cce87e86366b6629a27e4876a03d",
|
||||
"size": 1469336,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "curseforge",
|
||||
"slug": "ftb-xmod-compat",
|
||||
"fileId": "7715134",
|
||||
"version": "21.1.8",
|
||||
"path": "mods/ftb-xmod-compat-neoforge-21.1.8.jar",
|
||||
"url": "https://mediafilez.forgecdn.net/files/7715/134/ftb-xmod-compat-neoforge-21.1.8.jar",
|
||||
"sha1": "0c25fc1a43c99eaa68c883dc77e3ebf4539629e6",
|
||||
"size": 141328,
|
||||
"side": "both"
|
||||
},
|
||||
{
|
||||
"source": "curseforge",
|
||||
"slug": "ftb-ranks",
|
||||
"fileId": "6431744",
|
||||
"version": "2101.1.3",
|
||||
"path": "mods/ftb-ranks-neoforge-2101.1.3.jar",
|
||||
"url": "https://mediafilez.forgecdn.net/files/6431/744/ftb-ranks-neoforge-2101.1.3.jar",
|
||||
"sha1": "07fdfa3a75c3481770ce0118090a347d89e3a560",
|
||||
"size": 88382,
|
||||
"side": "both"
|
||||
}
|
||||
],
|
||||
"defaultServer": {
|
||||
|
||||
Reference in New Issue
Block a user